/**
 * @fileoverview Disallow reassigning function parameters.
 * @author Nat Burns
 */
"use strict";

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

const stopNodePattern =
	/(?:Statement|Declaration|Function(?:Expression)?|Program)$/u;

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

		docs: {
			description: "Disallow reassigning function parameters",
			recommended: false,
			url: "https://eslint.org/docs/latest/rules/no-param-reassign",
		},

		schema: [
			{
				oneOf: [
					{
						type: "object",
						properties: {
							props: {
								enum: [false],
							},
						},
						additionalProperties: false,
					},
					{
						type: "object",
						properties: {
							props: {
								enum: [true],
							},
							ignorePropertyModificationsFor: {
								type: "array",
								items: {
									type: "string",
								},
								uniqueItems: true,
							},
							ignorePropertyModificationsForRegex: {
								type: "array",
								items: {
									type: "string",
								},
								uniqueItems: true,
							},
						},
						additionalProperties: false,
					},
				],
			},
		],

		messages: {
			assignmentToFunctionParam:
				"Assignment to function parameter '{{name}}'.",
			assignmentToFunctionParamProp:
				"Assignment to property of function parameter '{{name}}'.",
		},
	},

	create(context) {
		const props = context.options[0] && context.options[0].props;
		const ignoredPropertyAssignmentsFor =
			(context.options[0] &&
				context.options[0].ignorePropertyModificationsFor) ||
			[];
		const ignoredPropertyAssignmentsForRegex =
			(context.options[0] &&
				context.options[0].ignorePropertyModificationsForRegex) ||
			[];
		const sourceCode = context.sourceCode;

		/**
		 * Checks whether or not the reference modifies properties of its variable.
		 * @param {Reference} reference A reference to check.
		 * @returns {boolean} Whether or not the reference modifies properties of its variable.
		 */
		function isModifyingProp(reference) {
			let node = reference.identifier;
			let parent = node.parent;

			while (
				parent &&
				(!stopNodePattern.test(parent.type) ||
					parent.type === "ForInStatement" ||
					parent.type === "ForOfStatement")
			) {
				switch (parent.type) {
					// e.g. foo.a = 0;
					case "AssignmentExpression":
						return parent.left === node;

					// e.g. ++foo.a;
					case "UpdateExpression":
						return true;

					// e.g. delete foo.a;
					case "UnaryExpression":
						if (parent.operator === "delete") {
							return true;
						}
						break;

					// e.g. for (foo.a in b) {}
					case "ForInStatement":
					case "ForOfStatement":
						if (parent.left === node) {
							return true;
						}

						// this is a stop node for parent.right and parent.body
						return false;

					// EXCLUDES: e.g. cache.get(foo.a).b = 0;
					case "CallExpression":
						if (parent.callee !== node) {
							return false;
						}
						break;

					// EXCLUDES: e.g. cache[foo.a] = 0;
					case "MemberExpression":
						if (parent.property === node) {
							return false;
						}
						break;

					// EXCLUDES: e.g. ({ [foo]: a }) = bar;
					case "Property":
						if (parent.key === node) {
							return false;
						}

						break;

					// EXCLUDES: e.g. (foo ? a : b).c = bar;
					case "ConditionalExpression":
						if (parent.test === node) {
							return false;
						}

						break;

					// no default
				}

				node = parent;
				parent = node.parent;
			}

			return false;
		}

		/**
		 * Tests that an identifier name matches any of the ignored property assignments.
		 * First we test strings in ignoredPropertyAssignmentsFor.
		 * Then we instantiate and test RegExp objects from ignoredPropertyAssignmentsForRegex strings.
		 * @param {string} identifierName A string that describes the name of an identifier to
		 * ignore property assignments for.
		 * @returns {boolean} Whether the string matches an ignored property assignment regular expression or not.
		 */
		function isIgnoredPropertyAssignment(identifierName) {
			return (
				ignoredPropertyAssignmentsFor.includes(identifierName) ||
				ignoredPropertyAssignmentsForRegex.some(ignored =>
					new RegExp(ignored, "u").test(identifierName),
				)
			);
		}

		/**
		 * Reports a reference if is non initializer and writable.
		 * @param {Reference} reference A reference to check.
		 * @param {number} index The index of the reference in the references.
		 * @param {Reference[]} references The array that the reference belongs to.
		 * @returns {void}
		 */
		function checkReference(reference, index, references) {
			const identifier = reference.identifier;

			if (
				identifier &&
				!reference.init &&
				/*
				 * Destructuring assignments can have multiple default value,
				 * so possibly there are multiple writeable references for the same identifier.
				 */
				(index === 0 || references[index - 1].identifier !== identifier)
			) {
				if (reference.isWrite()) {
					context.report({
						node: identifier,
						messageId: "assignmentToFunctionParam",
						data: { name: identifier.name },
					});
				} else if (
					props &&
					isModifyingProp(reference) &&
					!isIgnoredPropertyAssignment(identifier.name)
				) {
					context.report({
						node: identifier,
						messageId: "assignmentToFunctionParamProp",
						data: { name: identifier.name },
					});
				}
			}
		}

		/**
		 * Finds and reports references that are non initializer and writable.
		 * @param {Variable} variable A variable to check.
		 * @returns {void}
		 */
		function checkVariable(variable) {
			if (variable.defs[0].type === "Parameter") {
				variable.references.forEach(checkReference);
			}
		}

		/**
		 * Checks parameters of a given function node.
		 * @param {ASTNode} node A function node to check.
		 * @returns {void}
		 */
		function checkForFunction(node) {
			sourceCode.getDeclaredVariables(node).forEach(checkVariable);
		}

		return {
			// `:exit` is needed for the `node.parent` property of identifier nodes.
			"FunctionDeclaration:exit": checkForFunction,
			"FunctionExpression:exit": checkForFunction,
			"ArrowFunctionExpression:exit": checkForFunction,
		};
	},
};
