eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
271 lines (227 loc) • 7.05 kB
JavaScript
import {findVariable} from '@eslint-community/eslint-utils';
import {functionTypes} from './ast/index.js';
const MESSAGE_ID = 'preferDefaultParameters';
const MESSAGE_ID_SUGGEST = 'preferDefaultParametersSuggest';
const getDefaultAssignment = (left, right, operator = '=') => {
if (!left || !right || left.type !== 'Identifier') {
return;
}
if (
operator === '='
&& right.type === 'LogicalExpression'
&& (right.operator === '||' || right.operator === '??')
&& right.left.type === 'Identifier'
&& right.right.type === 'Literal'
) {
return {
assignedIdentifier: left,
parameterIdentifier: right.left,
defaultValue: right.right,
};
}
if ((operator === '||=' || operator === '??=') && right.type === 'Literal') {
return {
assignedIdentifier: left,
parameterIdentifier: left,
defaultValue: right,
};
}
};
// Call-like expressions that may run side effects before the default-assignment.
const callLikeExpressionTypes = new Set([
'CallExpression',
'NewExpression',
'ImportExpression',
'TaggedTemplateExpression',
]);
const containsCallExpression = (sourceCode, node) => {
if (!node) {
return false;
}
if (callLikeExpressionTypes.has(node.type)) {
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 = (isAssignment, references, left) => {
// Parameter is referenced prior to default-assignment
if (isAssignment && references[0].identifier !== left) {
return true;
}
// Old parameter is still referenced somewhere else
return !isAssignment && references.length > 1;
};
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} = sourceCode.getLoc(node).start;
const {column} = sourceCode.getLoc(node).end;
const nodeText = sourceCode.getText(node);
const lineText = sourceCode.lines[line - 1];
const isOnlyNodeOnLine = lineText.trim() === nodeText;
if (isOnlyNodeOnLine) {
return fixer.removeRange([
sourceCode.getIndexFromLoc({line, column: 0}),
sourceCode.getIndexFromLoc({line: line + 1, column: 0}),
]);
}
const isEndsWithWhitespace = lineText[column] === ' ';
if (isEndsWithWhitespace) {
const [start, end] = sourceCode.getRange(node);
return fixer.removeRange([start, end + 1]);
}
return fixer.remove(node);
};
/**
@param {import('eslint').Rule.RuleContext} context
*/
const create = context => {
const {sourceCode} = context;
const functionStack = [];
const getDefaultParameterProblem = (node, left, right, operator) => {
const currentFunction = functionStack.at(-1);
const defaultAssignment = getDefaultAssignment(left, right, operator);
if (
!currentFunction
|| !defaultAssignment
|| node.parent !== currentFunction.body
|| currentFunction.body.body.some(statement => statement.directive === 'use strict')
) {
return;
}
const {
assignedIdentifier,
parameterIdentifier: {name: parameterName},
defaultValue: {raw: defaultValueText},
} = defaultAssignment;
const {name: assignedName} = assignedIdentifier;
const isAssignment = node.type === 'ExpressionStatement';
// Parameter is reassigned to a different identifier
if (isAssignment && assignedName !== parameterName) {
return;
}
const scope = sourceCode.getScope(node);
const variable = findVariable(scope, parameterName);
// 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.at(-1);
// See 'default-param-last' rule
if (parameter?.type !== 'Identifier' || parameter.name !== parameterName) {
return;
}
const assignedVariable = assignedName === parameterName ? variable : findVariable(scope, assignedName);
const hasParameterNameCollision = assignedVariable?.defs.some(definition =>
definition.type === 'Parameter'
&& definition.name !== parameter) ?? false;
if (
hasSideEffects(sourceCode, currentFunction, node)
|| hasExtraReferences(isAssignment, references, left)
|| hasParameterNameCollision
) {
return;
}
const typeAnnotation = isAssignment ? parameter.typeAnnotation : assignedIdentifier.typeAnnotation;
const parameterText = typeAnnotation
? `${assignedName}${sourceCode.getText(typeAnnotation)}`
: assignedName;
const replacement = needsParentheses(sourceCode, currentFunction)
? `(${parameterText} = ${defaultValueText})`
: `${parameterText} = ${defaultValueText}`;
return {
node,
messageId: MESSAGE_ID,
suggest: [{
messageId: MESSAGE_ID_SUGGEST,
* fix(fixer, {abort}) {
if (
sourceCode.getCommentsInside(node).length > 0
|| sourceCode.getCommentsInside(parameter).length > 0
) {
return abort();
}
yield fixer.replaceText(parameter, replacement);
yield fixDefaultExpression(fixer, sourceCode, node);
},
}],
};
};
context.on(functionTypes, node => {
functionStack.push(node);
});
context.onExit(functionTypes, () => {
functionStack.pop();
});
context.on('AssignmentExpression', node => {
if (node.parent.type === 'ExpressionStatement' && node.parent.expression === node) {
return getDefaultParameterProblem(node.parent, node.left, node.right, node.operator);
}
});
context.on('VariableDeclarator', node => {
if (node.parent.type === 'VariableDeclaration' && node.parent.declarations.length === 1) {
return getDefaultParameterProblem(node.parent, node.id, node.init);
}
});
};
/**
@type {import('eslint').Rule.RuleModule}
*/
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer default parameters over reassignment.',
recommended: 'unopinionated',
},
hasSuggestions: true,
messages: {
[MESSAGE_ID]: 'Prefer default parameters over reassignment.',
[MESSAGE_ID_SUGGEST]: 'Replace reassignment with default parameter.',
},
languages: [
'js/js',
],
},
};
export default config;