| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 | 'use strict';const {findVariable} = require('@eslint-community/eslint-utils');const MESSAGE_ID = 'preferDefaultParameters';const MESSAGE_ID_SUGGEST = 'preferDefaultParametersSuggest';const assignmentSelector = [	'ExpressionStatement',	'[expression.type="AssignmentExpression"]',].join('');const declarationSelector = [	'VariableDeclaration',	'[declarations.0.type="VariableDeclarator"]',].join('');const isDefaultExpression = (left, right) =>	left	&& right	&& left.type === 'Identifier'	&& right.type === 'LogicalExpression'	&& (right.operator === '||' || right.operator === '??')	&& right.left.type === 'Identifier'	&& right.right.type === 'Literal';const containsCallExpression = (sourceCode, node) => {	if (!node) {		return false;	}	if (node.type === 'CallExpression') {		return true;	}	const keys = sourceCode.visitorKeys[node.type];	for (const key of keys) {		const value = node[key];		if (Array.isArray(value)) {			for (const element of value) {				if (containsCallExpression(sourceCode, element)) {					return true;				}			}		} else if (containsCallExpression(sourceCode, value)) {			return true;		}	}	return false;};const hasSideEffects = (sourceCode, function_, node) => {	for (const element of function_.body.body) {		if (element === node) {			break;		}		// Function call before default-assignment		if (containsCallExpression(sourceCode, element)) {			return true;		}	}	return false;};const hasExtraReferences = (assignment, references, left) => {	// Parameter is referenced prior to default-assignment	if (assignment && references[0].identifier !== left) {		return true;	}	// Old parameter is still referenced somewhere else	if (!assignment && references.length > 1) {		return true;	}	return false;};const isLastParameter = (parameters, parameter) => {	const lastParameter = parameters[parameters.length - 1];	// See 'default-param-last' rule	return parameter && parameter === lastParameter;};const needsParentheses = (sourceCode, function_) => {	if (function_.type !== 'ArrowFunctionExpression' || function_.params.length > 1) {		return false;	}	const [parameter] = function_.params;	const before = sourceCode.getTokenBefore(parameter);	const after = sourceCode.getTokenAfter(parameter);	return !after || !before || before.value !== '(' || after.value !== ')';};/** @param {import('eslint').Rule.RuleFixer} fixer */const fixDefaultExpression = (fixer, sourceCode, node) => {	const {line} = node.loc.start;	const {column} = node.loc.end;	const nodeText = sourceCode.getText(node);	const lineText = sourceCode.lines[line - 1];	const isOnlyNodeOnLine = lineText.trim() === nodeText;	const endsWithWhitespace = lineText[column] === ' ';	if (isOnlyNodeOnLine) {		return fixer.removeRange([			sourceCode.getIndexFromLoc({line, column: 0}),			sourceCode.getIndexFromLoc({line: line + 1, column: 0}),		]);	}	if (endsWithWhitespace) {		return fixer.removeRange([			node.range[0],			node.range[1] + 1,		]);	}	return fixer.remove(node);};/** @param {import('eslint').Rule.RuleContext} context */const create = context => {	const sourceCode = context.getSourceCode();	const functionStack = [];	const checkExpression = (node, left, right, assignment) => {		const currentFunction = functionStack[functionStack.length - 1];		if (!currentFunction || !isDefaultExpression(left, right)) {			return;		}		const {name: firstId} = left;		const {			left: {name: secondId},			right: {raw: literal},		} = right;		// Parameter is reassigned to a different identifier		if (assignment && firstId !== secondId) {			return;		}		const variable = findVariable(context.getScope(), secondId);		// This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1122		// But can't reproduce, just ignore this case		/* c8 ignore next 3 */		if (!variable) {			return;		}		const {references} = variable;		const {params} = currentFunction;		const parameter = params.find(parameter =>			parameter.type === 'Identifier'			&& parameter.name === secondId,		);		if (			hasSideEffects(sourceCode, currentFunction, node)			|| hasExtraReferences(assignment, references, left)			|| !isLastParameter(params, parameter)		) {			return;		}		const replacement = needsParentheses(sourceCode, currentFunction)			? `(${firstId} = ${literal})`			: `${firstId} = ${literal}`;		return {			node,			messageId: MESSAGE_ID,			suggest: [{				messageId: MESSAGE_ID_SUGGEST,				fix: fixer => [					fixer.replaceText(parameter, replacement),					fixDefaultExpression(fixer, sourceCode, node),				],			}],		};	};	return {		':function'(node) {			functionStack.push(node);		},		':function:exit'() {			functionStack.pop();		},		[assignmentSelector](node) {			const {left, right} = node.expression;			return checkExpression(node, left, right, true);		},		[declarationSelector](node) {			const {id, init} = node.declarations[0];			return checkExpression(node, id, init, false);		},	};};/** @type {import('eslint').Rule.RuleModule} */module.exports = {	create,	meta: {		type: 'suggestion',		docs: {			description: 'Prefer default parameters over reassignment.',		},		fixable: 'code',		hasSuggestions: true,		messages: {			[MESSAGE_ID]: 'Prefer default parameters over reassignment.',			[MESSAGE_ID_SUGGEST]: 'Replace reassignment with default parameter.',		},	},};
 |