/**
 * @fileoverview Rule to flag non-quoted property names in object literals.
 * @author Mathias Bynens <http://mathiasbynens.be/>
 * @deprecated in ESLint v8.53.0
 */
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const espree = require("espree");
const astUtils = require("./utils/ast-utils");
const keywords = require("./utils/keywords");

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('../types').Rule.RuleModule} */
module.exports = {
	meta: {
		deprecated: {
			message: "Formatting rules are being moved out of ESLint core.",
			url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
			deprecatedSince: "8.53.0",
			availableUntil: "11.0.0",
			replacedBy: [
				{
					message:
						"ESLint Stylistic now maintains deprecated stylistic core rules.",
					url: "https://eslint.style/guide/migration",
					plugin: {
						name: "@stylistic/eslint-plugin",
						url: "https://eslint.style",
					},
					rule: {
						name: "quote-props",
						url: "https://eslint.style/rules/quote-props",
					},
				},
			],
		},
		type: "suggestion",

		docs: {
			description: "Require quotes around object literal property names",
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/quote-props",
		},

		schema: {
			anyOf: [
				{
					type: "array",
					items: [
						{
							enum: [
								"always",
								"as-needed",
								"consistent",
								"consistent-as-needed",
							],
						},
					],
					minItems: 0,
					maxItems: 1,
				},
				{
					type: "array",
					items: [
						{
							enum: [
								"always",
								"as-needed",
								"consistent",
								"consistent-as-needed",
							],
						},
						{
							type: "object",
							properties: {
								keywords: {
									type: "boolean",
								},
								unnecessary: {
									type: "boolean",
								},
								numbers: {
									type: "boolean",
								},
							},
							additionalProperties: false,
						},
					],
					minItems: 0,
					maxItems: 2,
				},
			],
		},

		fixable: "code",
		messages: {
			requireQuotesDueToReservedWord:
				"Properties should be quoted as '{{property}}' is a reserved word.",
			inconsistentlyQuotedProperty:
				"Inconsistently quoted property '{{key}}' found.",
			unnecessarilyQuotedProperty:
				"Unnecessarily quoted property '{{property}}' found.",
			unquotedReservedProperty:
				"Unquoted reserved word '{{property}}' used as key.",
			unquotedNumericProperty:
				"Unquoted number literal '{{property}}' used as key.",
			unquotedPropertyFound: "Unquoted property '{{property}}' found.",
			redundantQuoting:
				"Properties shouldn't be quoted as all quotes are redundant.",
		},
	},

	create(context) {
		const MODE = context.options[0],
			KEYWORDS = context.options[1] && context.options[1].keywords,
			CHECK_UNNECESSARY =
				!context.options[1] || context.options[1].unnecessary !== false,
			NUMBERS = context.options[1] && context.options[1].numbers,
			sourceCode = context.sourceCode;

		/**
		 * Checks whether a certain string constitutes an ES3 token
		 * @param {string} tokenStr The string to be checked.
		 * @returns {boolean} `true` if it is an ES3 token.
		 */
		function isKeyword(tokenStr) {
			return keywords.includes(tokenStr);
		}

		/**
		 * Checks if an espree-tokenized key has redundant quotes (i.e. whether quotes are unnecessary)
		 * @param {string} rawKey The raw key value from the source
		 * @param {espreeTokens} tokens The espree-tokenized node key
		 * @param {boolean} [skipNumberLiterals=false] Indicates whether number literals should be checked
		 * @returns {boolean} Whether or not a key has redundant quotes.
		 * @private
		 */
		function areQuotesRedundant(rawKey, tokens, skipNumberLiterals) {
			return (
				tokens.length === 1 &&
				tokens[0].start === 0 &&
				tokens[0].end === rawKey.length &&
				(["Identifier", "Keyword", "Null", "Boolean"].includes(
					tokens[0].type,
				) ||
					(tokens[0].type === "Numeric" &&
						!skipNumberLiterals &&
						String(+tokens[0].value) === tokens[0].value))
			);
		}

		/**
		 * Returns a string representation of a property node with quotes removed
		 * @param {ASTNode} key Key AST Node, which may or may not be quoted
		 * @returns {string} A replacement string for this property
		 */
		function getUnquotedKey(key) {
			return key.type === "Identifier" ? key.name : key.value;
		}

		/**
		 * Returns a string representation of a property node with quotes added
		 * @param {ASTNode} key Key AST Node, which may or may not be quoted
		 * @returns {string} A replacement string for this property
		 */
		function getQuotedKey(key) {
			if (key.type === "Literal" && typeof key.value === "string") {
				// If the key is already a string literal, don't replace the quotes with double quotes.
				return sourceCode.getText(key);
			}

			// Otherwise, the key is either an identifier or a number literal.
			return `"${key.type === "Identifier" ? key.name : key.value}"`;
		}

		/**
		 * Ensures that a property's key is quoted only when necessary
		 * @param {ASTNode} node Property AST node
		 * @returns {void}
		 */
		function checkUnnecessaryQuotes(node) {
			const key = node.key;

			if (node.method || node.computed || node.shorthand) {
				return;
			}

			if (key.type === "Literal" && typeof key.value === "string") {
				let tokens;

				try {
					tokens = espree.tokenize(key.value);
				} catch {
					return;
				}

				if (tokens.length !== 1) {
					return;
				}

				const isKeywordToken = isKeyword(tokens[0].value);

				if (isKeywordToken && KEYWORDS) {
					return;
				}

				if (
					CHECK_UNNECESSARY &&
					areQuotesRedundant(key.value, tokens, NUMBERS)
				) {
					context.report({
						node,
						messageId: "unnecessarilyQuotedProperty",
						data: { property: key.value },
						fix: fixer =>
							fixer.replaceText(key, getUnquotedKey(key)),
					});
				}
			} else if (
				KEYWORDS &&
				key.type === "Identifier" &&
				isKeyword(key.name)
			) {
				context.report({
					node,
					messageId: "unquotedReservedProperty",
					data: { property: key.name },
					fix: fixer => fixer.replaceText(key, getQuotedKey(key)),
				});
			} else if (
				NUMBERS &&
				key.type === "Literal" &&
				astUtils.isNumericLiteral(key)
			) {
				context.report({
					node,
					messageId: "unquotedNumericProperty",
					data: { property: key.value },
					fix: fixer => fixer.replaceText(key, getQuotedKey(key)),
				});
			}
		}

		/**
		 * Ensures that a property's key is quoted
		 * @param {ASTNode} node Property AST node
		 * @returns {void}
		 */
		function checkOmittedQuotes(node) {
			const key = node.key;

			if (
				!node.method &&
				!node.computed &&
				!node.shorthand &&
				!(key.type === "Literal" && typeof key.value === "string")
			) {
				context.report({
					node,
					messageId: "unquotedPropertyFound",
					data: { property: key.name || key.value },
					fix: fixer => fixer.replaceText(key, getQuotedKey(key)),
				});
			}
		}

		/**
		 * Ensures that an object's keys are consistently quoted, optionally checks for redundancy of quotes
		 * @param {ASTNode} node Property AST node
		 * @param {boolean} checkQuotesRedundancy Whether to check quotes' redundancy
		 * @returns {void}
		 */
		function checkConsistency(node, checkQuotesRedundancy) {
			const quotedProps = [],
				unquotedProps = [];
			let keywordKeyName = null,
				necessaryQuotes = false;

			node.properties.forEach(property => {
				const key = property.key;

				if (
					!key ||
					property.method ||
					property.computed ||
					property.shorthand
				) {
					return;
				}

				if (key.type === "Literal" && typeof key.value === "string") {
					quotedProps.push(property);

					if (checkQuotesRedundancy) {
						let tokens;

						try {
							tokens = espree.tokenize(key.value);
						} catch {
							necessaryQuotes = true;
							return;
						}

						necessaryQuotes =
							necessaryQuotes ||
							!areQuotesRedundant(key.value, tokens) ||
							(KEYWORDS && isKeyword(tokens[0].value));
					}
				} else if (
					KEYWORDS &&
					checkQuotesRedundancy &&
					key.type === "Identifier" &&
					isKeyword(key.name)
				) {
					unquotedProps.push(property);
					necessaryQuotes = true;
					keywordKeyName = key.name;
				} else {
					unquotedProps.push(property);
				}
			});

			if (
				checkQuotesRedundancy &&
				quotedProps.length &&
				!necessaryQuotes
			) {
				quotedProps.forEach(property => {
					context.report({
						node: property,
						messageId: "redundantQuoting",
						fix: fixer =>
							fixer.replaceText(
								property.key,
								getUnquotedKey(property.key),
							),
					});
				});
			} else if (unquotedProps.length && keywordKeyName) {
				unquotedProps.forEach(property => {
					context.report({
						node: property,
						messageId: "requireQuotesDueToReservedWord",
						data: { property: keywordKeyName },
						fix: fixer =>
							fixer.replaceText(
								property.key,
								getQuotedKey(property.key),
							),
					});
				});
			} else if (quotedProps.length && unquotedProps.length) {
				unquotedProps.forEach(property => {
					context.report({
						node: property,
						messageId: "inconsistentlyQuotedProperty",
						data: { key: property.key.name || property.key.value },
						fix: fixer =>
							fixer.replaceText(
								property.key,
								getQuotedKey(property.key),
							),
					});
				});
			}
		}

		return {
			Property(node) {
				if (MODE === "always" || !MODE) {
					checkOmittedQuotes(node);
				}
				if (MODE === "as-needed") {
					checkUnnecessaryQuotes(node);
				}
			},
			ObjectExpression(node) {
				if (MODE === "consistent") {
					checkConsistency(node, false);
				}
				if (MODE === "consistent-as-needed") {
					checkConsistency(node, true);
				}
			},
		};
	},
};
