/**
 * @fileoverview Rule to require function names to match the name of the variable or property to which they are assigned.
 * @author Annie Zhang, Pavel Strashkin
 */

"use strict";

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

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

//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------

/**
 * Determines if a pattern is `module.exports` or `module["exports"]`
 * @param {ASTNode} pattern The left side of the AssignmentExpression
 * @returns {boolean} True if the pattern is `module.exports` or `module["exports"]`
 */
function isModuleExports(pattern) {
	if (
		pattern.type === "MemberExpression" &&
		pattern.object.type === "Identifier" &&
		pattern.object.name === "module"
	) {
		// module.exports
		if (
			pattern.property.type === "Identifier" &&
			pattern.property.name === "exports"
		) {
			return true;
		}

		// module["exports"]
		if (
			pattern.property.type === "Literal" &&
			pattern.property.value === "exports"
		) {
			return true;
		}
	}
	return false;
}

/**
 * Determines if a string name is a valid identifier
 * @param {string} name The string to be checked
 * @param {number} ecmaVersion The ECMAScript version if specified in the parserOptions config
 * @returns {boolean} True if the string is a valid identifier
 */
function isIdentifier(name, ecmaVersion) {
	if (ecmaVersion >= 2015) {
		return esutils.keyword.isIdentifierES6(name);
	}
	return esutils.keyword.isIdentifierES5(name);
}

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

const alwaysOrNever = { enum: ["always", "never"] };
const optionsObject = {
	type: "object",
	properties: {
		considerPropertyDescriptor: {
			type: "boolean",
		},
		includeCommonJSModuleExports: {
			type: "boolean",
		},
	},
	additionalProperties: false,
};

/** @type {import('../types').Rule.RuleModule} */
module.exports = {
	meta: {
		type: "suggestion",

		docs: {
			description:
				"Require function names to match the name of the variable or property to which they are assigned",
			recommended: false,
			frozen: true,
			url: "https://eslint.org/docs/latest/rules/func-name-matching",
		},

		schema: {
			anyOf: [
				{
					type: "array",
					additionalItems: false,
					items: [alwaysOrNever, optionsObject],
				},
				{
					type: "array",
					additionalItems: false,
					items: [optionsObject],
				},
			],
		},

		defaultOptions: ["always"],

		messages: {
			matchProperty:
				"Function name `{{funcName}}` should match property name `{{name}}`.",
			matchVariable:
				"Function name `{{funcName}}` should match variable name `{{name}}`.",
			notMatchProperty:
				"Function name `{{funcName}}` should not match property name `{{name}}`.",
			notMatchVariable:
				"Function name `{{funcName}}` should not match variable name `{{name}}`.",
		},
	},

	create(context) {
		const options =
			(typeof context.options[0] === "object"
				? context.options[0]
				: context.options[1]) || {};
		const nameMatches =
			typeof context.options[0] === "string"
				? context.options[0]
				: "always";
		const considerPropertyDescriptor = options.considerPropertyDescriptor;
		const includeModuleExports = options.includeCommonJSModuleExports;
		const ecmaVersion = context.languageOptions.ecmaVersion;

		/**
		 * Check whether node is a certain CallExpression.
		 * @param {string} objName object name
		 * @param {string} funcName function name
		 * @param {ASTNode} node The node to check
		 * @returns {boolean} `true` if node matches CallExpression
		 */
		function isPropertyCall(objName, funcName, node) {
			if (!node) {
				return false;
			}
			return (
				node.type === "CallExpression" &&
				astUtils.isSpecificMemberAccess(node.callee, objName, funcName)
			);
		}

		/**
		 * Compares identifiers based on the nameMatches option
		 * @param {string} x the first identifier
		 * @param {string} y the second identifier
		 * @returns {boolean} whether the two identifiers should warn.
		 */
		function shouldWarn(x, y) {
			return (
				(nameMatches === "always" && x !== y) ||
				(nameMatches === "never" && x === y)
			);
		}

		/**
		 * Reports
		 * @param {ASTNode} node The node to report
		 * @param {string} name The variable or property name
		 * @param {string} funcName The function name
		 * @param {boolean} isProp True if the reported node is a property assignment
		 * @returns {void}
		 */
		function report(node, name, funcName, isProp) {
			let messageId;

			if (nameMatches === "always" && isProp) {
				messageId = "matchProperty";
			} else if (nameMatches === "always") {
				messageId = "matchVariable";
			} else if (isProp) {
				messageId = "notMatchProperty";
			} else {
				messageId = "notMatchVariable";
			}
			context.report({
				node,
				messageId,
				data: {
					name,
					funcName,
				},
			});
		}

		/**
		 * Determines whether a given node is a string literal
		 * @param {ASTNode} node The node to check
		 * @returns {boolean} `true` if the node is a string literal
		 */
		function isStringLiteral(node) {
			return node.type === "Literal" && typeof node.value === "string";
		}

		//--------------------------------------------------------------------------
		// Public
		//--------------------------------------------------------------------------

		return {
			VariableDeclarator(node) {
				if (
					!node.init ||
					node.init.type !== "FunctionExpression" ||
					node.id.type !== "Identifier"
				) {
					return;
				}
				if (
					node.init.id &&
					shouldWarn(node.id.name, node.init.id.name)
				) {
					report(node, node.id.name, node.init.id.name, false);
				}
			},

			AssignmentExpression(node) {
				if (
					node.right.type !== "FunctionExpression" ||
					(node.left.computed &&
						node.left.property.type !== "Literal") ||
					(!includeModuleExports && isModuleExports(node.left)) ||
					(node.left.type !== "Identifier" &&
						node.left.type !== "MemberExpression")
				) {
					return;
				}

				const isProp = node.left.type === "MemberExpression";
				const name = isProp
					? astUtils.getStaticPropertyName(node.left)
					: node.left.name;

				if (
					node.right.id &&
					name &&
					isIdentifier(name) &&
					shouldWarn(name, node.right.id.name)
				) {
					report(node, name, node.right.id.name, isProp);
				}
			},

			"Property, PropertyDefinition[value]"(node) {
				if (!(
					node.value.type === "FunctionExpression" && node.value.id
				)) {
					return;
				}

				if (node.key.type === "Identifier" && !node.computed) {
					const functionName = node.value.id.name;
					let propertyName = node.key.name;

					if (
						considerPropertyDescriptor &&
						propertyName === "value" &&
						node.parent.type === "ObjectExpression"
					) {
						if (
							isPropertyCall(
								"Object",
								"defineProperty",
								node.parent.parent,
							) ||
							isPropertyCall(
								"Reflect",
								"defineProperty",
								node.parent.parent,
							)
						) {
							const property = node.parent.parent.arguments[1];

							if (
								isStringLiteral(property) &&
								shouldWarn(property.value, functionName)
							) {
								report(
									node,
									property.value,
									functionName,
									true,
								);
							}
						} else if (
							isPropertyCall(
								"Object",
								"defineProperties",
								node.parent.parent.parent.parent,
							)
						) {
							propertyName = node.parent.parent.key.name;
							if (
								!node.parent.parent.computed &&
								shouldWarn(propertyName, functionName)
							) {
								report(node, propertyName, functionName, true);
							}
						} else if (
							isPropertyCall(
								"Object",
								"create",
								node.parent.parent.parent.parent,
							)
						) {
							propertyName = node.parent.parent.key.name;
							if (
								!node.parent.parent.computed &&
								shouldWarn(propertyName, functionName)
							) {
								report(node, propertyName, functionName, true);
							}
						} else if (shouldWarn(propertyName, functionName)) {
							report(node, propertyName, functionName, true);
						}
					} else if (shouldWarn(propertyName, functionName)) {
						report(node, propertyName, functionName, true);
					}
					return;
				}

				if (
					isStringLiteral(node.key) &&
					isIdentifier(node.key.value, ecmaVersion) &&
					shouldWarn(node.key.value, node.value.id.name)
				) {
					report(node, node.key.value, node.value.id.name, true);
				}
			},
		};
	},
};
