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.',
- },
- },
- };
|