UNPKG

eslint-plugin-unicorn

Version:
220 lines (181 loc) 5.32 kB
'use strict'; const eslintVisitorKeys = require('eslint-visitor-keys'); const {findVariable} = require('eslint-utils'); const getDocumentationUrl = require('./utils/get-documentation-url'); 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 = (source, node) => { if (!node) { return false; } if (node.type === 'CallExpression') { return true; } // The Babel AST doesn't have visitor keys for certain types of nodes // Use `eslint-visitor-keys` in those cases // TODO: Remove this when we drop support for `babel-eslint` #1040 const keys = source.visitorKeys[node.type] || eslintVisitorKeys.KEYS[node.type]; for (const key of keys) { const value = node[key]; if (Array.isArray(value)) { for (const element of value) { if (containsCallExpression(source, element)) { return true; } } } else if (containsCallExpression(source, value)) { return true; } } return false; }; const hasSideEffects = (source, function_, node) => { for (const element of function_.body.body) { if (element === node) { break; } // Function call before default-assignment if (containsCallExpression(source, 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 = (source, function_) => { if (function_.type !== 'ArrowFunctionExpression' || function_.params.length > 1) { return false; } const [parameter] = function_.params; const before = source.getTokenBefore(parameter); const after = source.getTokenAfter(parameter); return !after || !before || before.value !== '(' || after.value !== ')'; }; const fixDefaultExpression = (fixer, source, node) => { const {line} = source.getLocFromIndex(node.range[0]); const {column} = source.getLocFromIndex(node.range[1]); const nodeText = source.getText(node); const lineText = source.lines[line - 1]; const isOnlyNodeOnLine = lineText.trim() === nodeText; const endsWithWhitespace = lineText[column] === ' '; if (isOnlyNodeOnLine) { return fixer.removeRange([ source.getIndexFromLoc({line, column: 0}), source.getIndexFromLoc({line: line + 1, column: 0}) ]); } if (endsWithWhitespace) { return fixer.removeRange([ node.range[0], node.range[1] + 1 ]); } return fixer.removeRange(node.range); }; const create = context => { const source = 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 {references} = findVariable(context.getScope(), secondId); const {params} = currentFunction; const parameter = params.find(parameter => parameter.type === 'Identifier' && parameter.name === secondId ); if ( hasSideEffects(source, currentFunction, node) || hasExtraReferences(assignment, references, left) || !isLastParameter(params, parameter) ) { return; } const replacement = needsParentheses(source, currentFunction) ? `(${firstId} = ${literal})` : `${firstId} = ${literal}`; context.report({ node, messageId: MESSAGE_ID, suggest: [{ messageId: MESSAGE_ID_SUGGEST, fix: fixer => [ fixer.replaceText(parameter, replacement), fixDefaultExpression(fixer, source, node) ] }] }); }; return { ':function': node => { functionStack.push(node); }, ':function:exit': () => { functionStack.pop(); }, [assignmentSelector]: node => { const {left, right} = node.expression; checkExpression(node, left, right, true); }, [declarationSelector]: node => { const {id, init} = node.declarations[0]; checkExpression(node, id, init, false); } }; }; module.exports = { create, meta: { type: 'suggestion', docs: { url: getDocumentationUrl(__filename) }, fixable: 'code', messages: { [MESSAGE_ID]: 'Prefer default parameters over reassignment.', [MESSAGE_ID_SUGGEST]: 'Replace reassignment with default parameter.' } } };