UNPKG

eslint-plugin-unicorn

Version:
276 lines (243 loc) • 5.39 kB
import {findVariable, getPropertyName} from '@eslint-community/eslint-utils'; import {isIdentifierNamed, isMethodCall} from './ast/index.js'; import { getOutermostChainAndTypeScriptExpression, isArray, isLeftHandSide, isString, unwrapTypeScriptExpression, } from './utils/index.js'; const MESSAGE_ID = 'no-uncalled-method'; const messages = { [MESSAGE_ID]: 'Call `{{method}}()` instead of referencing the method.', }; const arrayMethods = new Set([ 'at', 'concat', 'copyWithin', 'entries', 'every', 'fill', 'filter', 'find', 'findIndex', 'findLast', 'findLastIndex', 'flat', 'flatMap', 'forEach', 'includes', 'indexOf', 'join', 'keys', 'lastIndexOf', 'map', 'pop', 'push', 'reduce', 'reduceRight', 'reverse', 'shift', 'slice', 'some', 'sort', 'splice', 'toLocaleString', 'toReversed', 'toSorted', 'toSpliced', 'toString', 'unshift', 'values', 'with', ]); const stringMethods = new Set([ 'at', 'anchor', 'big', 'blink', 'charAt', 'charCodeAt', 'codePointAt', 'concat', 'endsWith', 'fixed', 'fontcolor', 'fontsize', 'includes', 'indexOf', 'isWellFormed', 'italics', 'lastIndexOf', 'link', 'localeCompare', 'match', 'matchAll', 'normalize', 'padEnd', 'padStart', 'repeat', 'replace', 'replaceAll', 'search', 'slice', 'small', 'split', 'startsWith', 'strike', 'sub', 'substr', 'substring', 'sup', 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toString', 'toUpperCase', 'toWellFormed', 'trim', 'trimEnd', 'trimLeft', 'trimRight', 'trimStart', 'valueOf', ]); const callableReferenceMethods = new Set([ 'apply', 'bind', 'call', ]); function isNamedOrConstAlias(node, name, context, visitedVariables = new Set()) { node = unwrapTypeScriptExpression(node); if (isIdentifierNamed(node, name)) { return true; } if (node.type !== 'Identifier') { return false; } const variable = findVariable(context.sourceCode.getScope(node), node); if ( !variable || visitedVariables.has(variable) || variable.defs.length !== 1 ) { return false; } visitedVariables.add(variable); const [definition] = variable.defs; if ( definition.type !== 'Variable' || definition.parent.kind !== 'const' || definition.node.id !== definition.name || !definition.node.init ) { return false; } return isNamedOrConstAlias(definition.node.init, name, context, visitedVariables); } const isArrayReceiver = (node, context) => isNamedOrConstAlias(node, 'array', context) || isArray(node, context); const isStringReceiver = (node, context) => isNamedOrConstAlias(node, 'string', context) || isString(node, context); const isPrototypeMethod = (node, context) => { if (node.object.type !== 'MemberExpression') { return false; } const property = getPropertyName(node.object, context.sourceCode.getScope(node.object)); return property === 'prototype' && ( isIdentifierNamed(node.object.object, 'Array') || isIdentifierNamed(node.object.object, 'String') ); }; const isCallee = node => { node = getOutermostChainAndTypeScriptExpression(node); // A tagged template invokes the tag too return ( ['CallExpression', 'NewExpression', 'TaggedTemplateExpression'].includes(node.parent.type) && (node.parent.callee ?? node.parent.tag) === node ); }; const isCallableReference = (node, context) => { node = getOutermostChainAndTypeScriptExpression(node); if (node.parent.type !== 'MemberExpression' || node.parent.object !== node) { return false; } const method = getPropertyName(node.parent, context.sourceCode.getScope(node.parent)); return callableReferenceMethods.has(method) && isCallee(node.parent); }; const isReflectApplyArgument = node => { const argument = getOutermostChainAndTypeScriptExpression(node); return argument.parent.type === 'CallExpression' && argument.parent.arguments[0] === argument && isMethodCall(argument.parent, { object: 'Reflect', method: 'apply', }); }; const isTypeofArgument = node => { node = getOutermostChainAndTypeScriptExpression(node); return ( node.parent.type === 'UnaryExpression' && node.parent.operator === 'typeof' ); }; const shouldSkip = (node, context) => isCallee(node) || isCallableReference(node, context) || isReflectApplyArgument(node) || isTypeofArgument(node) || isLeftHandSide(getOutermostChainAndTypeScriptExpression(node)) || isPrototypeMethod(node, context); function shouldReport({receiver, method, context}) { if ( arrayMethods.has(method) && isArrayReceiver(receiver, context) ) { return true; } return Boolean(stringMethods.has(method) && isStringReceiver(receiver, context)); } /** @param {import('eslint').Rule.RuleContext} context */ const create = context => { const {sourceCode} = context; context.on('MemberExpression', node => { if (shouldSkip(node, context)) { return; } const method = getPropertyName(node, sourceCode.getScope(node)); if (!method || !shouldReport({ receiver: node.object, method, context, })) { return; } return { node: node.property, messageId: MESSAGE_ID, data: {method}, }; }); }; /** @type {import('eslint').Rule.RuleModule} */ const config = { create, meta: { type: 'problem', docs: { description: 'Disallow referencing methods without calling them.', recommended: true, }, messages, languages: [ 'js/js', ], }, }; export default config;