diff --git a/packages/eslint-plugin/src/rules/class-literal-property-style.ts b/packages/eslint-plugin/src/rules/class-literal-property-style.ts index 816c7803604a..28d4a5cc78ba 100644 --- a/packages/eslint-plugin/src/rules/class-literal-property-style.ts +++ b/packages/eslint-plugin/src/rules/class-literal-property-style.ts @@ -3,9 +3,11 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { createRule, + getStaticMemberAccessValue, getStaticStringValue, isAssignee, isFunction, + isStaticMemberAccessOfValue, nullThrows, } from '../util'; @@ -79,10 +81,6 @@ export default createRule({ create(context, [style]) { const propertiesInfoStack: PropertiesInfo[] = []; - function getStringValue(node: TSESTree.Node): string { - return getStaticStringValue(node) ?? context.sourceCode.getText(node); - } - function enterClassBody(): void { propertiesInfoStack.push({ properties: [], @@ -102,8 +100,8 @@ export default createRule({ return; } - const name = getStringValue(node.key); - if (excludeSet.has(name)) { + const name = getStaticMemberAccessValue(node, context); + if (name && excludeSet.has(name)) { return; } @@ -167,15 +165,17 @@ export default createRule({ return; } - const name = getStringValue(node.key); - - const hasDuplicateKeySetter = node.parent.body.some(element => { - return ( - element.type === AST_NODE_TYPES.MethodDefinition && - element.kind === 'set' && - getStringValue(element.key) === name - ); - }); + const name = getStaticMemberAccessValue(node, context); + + const hasDuplicateKeySetter = + name && + node.parent.body.some(element => { + return ( + element.type === AST_NODE_TYPES.MethodDefinition && + element.kind === 'set' && + isStaticMemberAccessOfValue(element, context, name) + ); + }); if (hasDuplicateKeySetter) { return; } diff --git a/packages/eslint-plugin/src/rules/class-methods-use-this.ts b/packages/eslint-plugin/src/rules/class-methods-use-this.ts index 4f574471e03b..6e27a6229f12 100644 --- a/packages/eslint-plugin/src/rules/class-methods-use-this.ts +++ b/packages/eslint-plugin/src/rules/class-methods-use-this.ts @@ -5,7 +5,7 @@ import { createRule, getFunctionHeadLoc, getFunctionNameWithKind, - getStaticStringValue, + getStaticMemberAccessValue, } from '../util'; type Options = [ @@ -182,10 +182,7 @@ export default createRule({ const hashIfNeeded = node.key.type === AST_NODE_TYPES.PrivateIdentifier ? '#' : ''; - const name = - node.key.type === AST_NODE_TYPES.Literal - ? getStaticStringValue(node.key) - : node.key.name || ''; + const name = getStaticMemberAccessValue(node, context); return !exceptMethods.has(hashIfNeeded + (name ?? '')); } diff --git a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts index 2da841b06e82..2bcd6cf4df43 100644 --- a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts +++ b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts @@ -2,7 +2,7 @@ import { DefinitionType } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { createRule, isFunction } from '../util'; +import { createRule, isFunction, isStaticMemberAccessOfValue } from '../util'; import type { FunctionExpression, FunctionInfo, @@ -268,21 +268,11 @@ export default createRule({ (node.type === AST_NODE_TYPES.Property && node.method) || node.type === AST_NODE_TYPES.PropertyDefinition ) { - if ( - node.key.type === AST_NODE_TYPES.Literal && - typeof node.key.value === 'string' - ) { - return options.allowedNames.includes(node.key.value); - } - if ( - node.key.type === AST_NODE_TYPES.TemplateLiteral && - node.key.expressions.length === 0 - ) { - return options.allowedNames.includes(node.key.quasis[0].value.raw); - } - if (!node.computed && node.key.type === AST_NODE_TYPES.Identifier) { - return options.allowedNames.includes(node.key.name); - } + return isStaticMemberAccessOfValue( + node, + context, + ...options.allowedNames, + ); } return false; diff --git a/packages/eslint-plugin/src/rules/no-floating-promises.ts b/packages/eslint-plugin/src/rules/no-floating-promises.ts index 3b0265e1718f..5a83a338055b 100644 --- a/packages/eslint-plugin/src/rules/no-floating-promises.ts +++ b/packages/eslint-plugin/src/rules/no-floating-promises.ts @@ -8,6 +8,7 @@ import { createRule, getOperatorPrecedence, getParserServices, + getStaticMemberAccessValue, isBuiltinSymbolLike, OperatorPrecedence, readonlynessOptionsDefaults, @@ -334,27 +335,38 @@ export default createRule({ // If the outer expression is a call, a `.catch()` or `.then()` with // rejection handler handles the promise. - const catchRejectionHandler = getRejectionHandlerFromCatchCall(node); - if (catchRejectionHandler) { - if (isValidRejectionHandler(catchRejectionHandler)) { - return { isUnhandled: false }; + const { callee } = node; + if (callee.type === AST_NODE_TYPES.MemberExpression) { + const methodName = getStaticMemberAccessValue(callee, context); + const catchRejectionHandler = + methodName === 'catch' && node.arguments.length >= 1 + ? node.arguments[0] + : undefined; + if (catchRejectionHandler) { + if (isValidRejectionHandler(catchRejectionHandler)) { + return { isUnhandled: false }; + } + return { isUnhandled: true, nonFunctionHandler: true }; } - return { isUnhandled: true, nonFunctionHandler: true }; - } - const thenRejectionHandler = getRejectionHandlerFromThenCall(node); - if (thenRejectionHandler) { - if (isValidRejectionHandler(thenRejectionHandler)) { - return { isUnhandled: false }; + const thenRejectionHandler = + methodName === 'then' && node.arguments.length >= 2 + ? node.arguments[1] + : undefined; + if (thenRejectionHandler) { + if (isValidRejectionHandler(thenRejectionHandler)) { + return { isUnhandled: false }; + } + return { isUnhandled: true, nonFunctionHandler: true }; } - return { isUnhandled: true, nonFunctionHandler: true }; - } - // `x.finally()` is transparent to resolution of the promise, so check `x`. - // ("object" in this context is the `x` in `x.finally()`) - const promiseFinallyObject = getObjectFromFinallyCall(node); - if (promiseFinallyObject) { - return isUnhandledPromise(checker, promiseFinallyObject); + // `x.finally()` is transparent to resolution of the promise, so check `x`. + // ("object" in this context is the `x` in `x.finally()`) + const promiseFinallyObject = + methodName === 'finally' ? callee.object : undefined; + if (promiseFinallyObject) { + return isUnhandledPromise(checker, promiseFinallyObject); + } } // All other cases are unhandled. @@ -485,41 +497,3 @@ function isFunctionParam( } return false; } - -function getRejectionHandlerFromCatchCall( - expression: TSESTree.CallExpression, -): TSESTree.CallExpressionArgument | undefined { - if ( - expression.callee.type === AST_NODE_TYPES.MemberExpression && - expression.callee.property.type === AST_NODE_TYPES.Identifier && - expression.callee.property.name === 'catch' && - expression.arguments.length >= 1 - ) { - return expression.arguments[0]; - } - return undefined; -} - -function getRejectionHandlerFromThenCall( - expression: TSESTree.CallExpression, -): TSESTree.CallExpressionArgument | undefined { - if ( - expression.callee.type === AST_NODE_TYPES.MemberExpression && - expression.callee.property.type === AST_NODE_TYPES.Identifier && - expression.callee.property.name === 'then' && - expression.arguments.length >= 2 - ) { - return expression.arguments[1]; - } - return undefined; -} - -function getObjectFromFinallyCall( - expression: TSESTree.CallExpression, -): TSESTree.Expression | undefined { - return expression.callee.type === AST_NODE_TYPES.MemberExpression && - expression.callee.property.type === AST_NODE_TYPES.Identifier && - expression.callee.property.name === 'finally' - ? expression.callee.object - : undefined; -} diff --git a/packages/eslint-plugin/src/rules/prefer-find.ts b/packages/eslint-plugin/src/rules/prefer-find.ts index 7d1c6fed86dc..389d4f581378 100644 --- a/packages/eslint-plugin/src/rules/prefer-find.ts +++ b/packages/eslint-plugin/src/rules/prefer-find.ts @@ -1,6 +1,6 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import type { RuleFix, Scope } from '@typescript-eslint/utils/ts-eslint'; +import type { RuleFix } from '@typescript-eslint/utils/ts-eslint'; import * as tsutils from 'ts-api-utils'; import type { Type } from 'typescript'; @@ -9,6 +9,7 @@ import { getConstrainedTypeAtLocation, getParserServices, getStaticValue, + isStaticMemberAccessOfValue, nullThrows, } from '../util'; @@ -89,7 +90,7 @@ export default createRule({ // or the optional chaining variants. if (callee.type === AST_NODE_TYPES.MemberExpression) { const isBracketSyntaxForFilter = callee.computed; - if (isStaticMemberAccessOfValue(callee, 'filter', globalScope)) { + if (isStaticMemberAccessOfValue(callee, context, 'filter')) { const filterNode = callee.property; const filteredObjectType = getConstrainedTypeAtLocation( @@ -162,7 +163,7 @@ export default createRule({ if ( callee.type === AST_NODE_TYPES.MemberExpression && !callee.optional && - isStaticMemberAccessOfValue(callee, 'at', globalScope) + isStaticMemberAccessOfValue(callee, context, 'at') ) { const atArgument = getStaticValue(node.arguments[0], globalScope); if (atArgument != null && isTreatedAsZeroByArrayAt(atArgument.value)) { @@ -321,25 +322,3 @@ export default createRule({ }; }, }); - -/** - * Answers whether the member expression looks like - * `x.memberName`, `x['memberName']`, - * or even `const mn = 'memberName'; x[mn]` (or optional variants thereof). - */ -function isStaticMemberAccessOfValue( - memberExpression: - | TSESTree.MemberExpressionComputedName - | TSESTree.MemberExpressionNonComputedName, - value: string, - scope?: Scope.Scope, -): boolean { - if (!memberExpression.computed) { - // x.memberName case. - return memberExpression.property.name === value; - } - - // x['memberName'] cases. - const staticValueResult = getStaticValue(memberExpression.property, scope); - return staticValueResult != null && value === staticValueResult.value; -} diff --git a/packages/eslint-plugin/src/rules/prefer-includes.ts b/packages/eslint-plugin/src/rules/prefer-includes.ts index 1a8f705b312f..98a24fcc8917 100644 --- a/packages/eslint-plugin/src/rules/prefer-includes.ts +++ b/packages/eslint-plugin/src/rules/prefer-includes.ts @@ -8,6 +8,7 @@ import { getConstrainedTypeAtLocation, getParserServices, getStaticValue, + isStaticMemberAccessOfValue, } from '../util'; export default createRule({ @@ -146,6 +147,9 @@ export default createRule({ node: TSESTree.MemberExpression, allowFixing: boolean, ): void { + if (!isStaticMemberAccessOfValue(node, context, 'indexOf')) { + return; + } // Check if the comparison is equivalent to `includes()`. const callNode = node.parent as TSESTree.CallExpression; const compareNode = ( @@ -204,14 +208,14 @@ export default createRule({ return { // a.indexOf(b) !== 1 - "BinaryExpression > CallExpression.left > MemberExpression.callee[property.name='indexOf'][computed=false]"( + 'BinaryExpression > CallExpression.left > MemberExpression'( node: TSESTree.MemberExpression, ): void { checkArrayIndexOf(node, /* allowFixing */ true); }, // a?.indexOf(b) !== 1 - "BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name='indexOf'][computed=false]"( + 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression'( node: TSESTree.MemberExpression, ): void { checkArrayIndexOf(node, /* allowFixing */ false); diff --git a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts index ae5a344a299e..acad1d6ef8f3 100644 --- a/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts +++ b/packages/eslint-plugin/src/rules/prefer-promise-reject-errors.ts @@ -10,6 +10,7 @@ import { isPromiseConstructorLike, isPromiseLike, isReadonlyErrorLike, + isStaticMemberAccessOfValue, } from '../util'; export type MessageIds = 'rejectAnError'; @@ -99,13 +100,8 @@ export default createRule({ return; } - const rejectMethodCalled = callee.computed - ? callee.property.type === AST_NODE_TYPES.Literal && - callee.property.value === 'reject' - : callee.property.name === 'reject'; - if ( - !rejectMethodCalled || + !isStaticMemberAccessOfValue(callee, context, 'reject') || !typeAtLocationIsLikePromise(callee.object) ) { return; diff --git a/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts b/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts index 7eb781ed0d18..a8c820a7b370 100644 --- a/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts +++ b/packages/eslint-plugin/src/rules/prefer-reduce-type-parameter.ts @@ -1,5 +1,4 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import type * as ts from 'typescript'; @@ -7,6 +6,7 @@ import { createRule, getConstrainedTypeAtLocation, getParserServices, + isStaticMemberAccessOfValue, isTypeAssertion, } from '../util'; @@ -14,23 +14,6 @@ type MemberExpressionWithCallExpressionParent = TSESTree.MemberExpression & { parent: TSESTree.CallExpression; }; -const getMemberExpressionName = ( - member: TSESTree.MemberExpression, -): string | null => { - if (!member.computed) { - return member.property.name; - } - - if ( - member.property.type === AST_NODE_TYPES.Literal && - typeof member.property.value === 'string' - ) { - return member.property.value; - } - - return null; -}; - export default createRule({ name: 'prefer-reduce-type-parameter', meta: { @@ -67,7 +50,7 @@ export default createRule({ 'CallExpression > MemberExpression.callee'( callee: MemberExpressionWithCallExpressionParent, ): void { - if (getMemberExpressionName(callee) !== 'reduce') { + if (!isStaticMemberAccessOfValue(callee, context, 'reduce')) { return; } diff --git a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts index b19e0305c7bb..9254589a624e 100644 --- a/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts +++ b/packages/eslint-plugin/src/rules/prefer-regexp-exec.ts @@ -9,6 +9,7 @@ import { getStaticValue, getTypeName, getWrappingFixer, + isStaticMemberAccessOfValue, } from '../util'; enum ArgumentType { @@ -98,9 +99,12 @@ export default createRule({ } return { - "CallExpression[arguments.length=1] > MemberExpression.callee[property.name='match'][computed=false]"( + 'CallExpression[arguments.length=1] > MemberExpression'( memberNode: TSESTree.MemberExpression, ): void { + if (!isStaticMemberAccessOfValue(memberNode, context, 'match')) { + return; + } const objectNode = memberNode.object; const callNode = memberNode.parent as TSESTree.CallExpression; const [argumentNode] = callNode.arguments; diff --git a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts index f6a22152043c..d65fd9736ad5 100644 --- a/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts +++ b/packages/eslint-plugin/src/rules/prefer-string-starts-ends-with.ts @@ -9,6 +9,7 @@ import { getStaticValue, getTypeName, isNotClosingParenToken, + isStaticMemberAccessOfValue, nullThrows, NullThrowsReasons, } from '../util'; @@ -580,11 +581,12 @@ export default createRule({ // foo.substring(foo.length - 3) === 'bar' // foo.substring(foo.length - 3, foo.length) === 'bar' [[ - 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="slice"][computed=false]', - 'BinaryExpression > CallExpression.left > MemberExpression.callee[property.name="substring"][computed=false]', - 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name="slice"][computed=false]', - 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression.callee[property.name="substring"][computed=false]', + 'BinaryExpression > CallExpression.left > MemberExpression', + 'BinaryExpression > ChainExpression.left > CallExpression > MemberExpression', ].join(', ')](node: TSESTree.MemberExpression): void { + if (!isStaticMemberAccessOfValue(node, context, 'slice', 'substring')) { + return; + } const callNode = getParent(node) as TSESTree.CallExpression; const parentNode = getParent(callNode); diff --git a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts index e6f83e800bbb..ec1f1e1fdb15 100644 --- a/packages/eslint-plugin/src/rules/require-array-sort-compare.ts +++ b/packages/eslint-plugin/src/rules/require-array-sort-compare.ts @@ -5,6 +5,7 @@ import { getConstrainedTypeAtLocation, getParserServices, getTypeName, + isStaticMemberAccessOfValue, isTypeArrayTypeOrUnionOfArrayTypes, } from '../util'; @@ -66,6 +67,9 @@ export default createRule({ } function checkSortArgument(callee: TSESTree.MemberExpression): void { + if (!isStaticMemberAccessOfValue(callee, context, 'sort', 'toSorted')) { + return; + } const calleeObjType = getConstrainedTypeAtLocation( services, callee.object, @@ -81,9 +85,7 @@ export default createRule({ } return { - "CallExpression[arguments.length=0] > MemberExpression[property.name='sort'][computed=false]": - checkSortArgument, - "CallExpression[arguments.length=0] > MemberExpression[property.name='toSorted'][computed=false]": + 'CallExpression[arguments.length=0] > MemberExpression': checkSortArgument, }; }, diff --git a/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts b/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts index 0c9cf63adf2b..a6060cd3f00e 100644 --- a/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts +++ b/packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts @@ -1,4 +1,3 @@ -import type { Scope } from '@typescript-eslint/scope-manager'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { ReportDescriptor } from '@typescript-eslint/utils/ts-eslint'; @@ -8,7 +7,7 @@ import type * as ts from 'typescript'; import { createRule, getParserServices, - getStaticValue, + getStaticMemberAccessValue, isParenlessArrowFunction, isRestParameterDeclaration, nullThrows, @@ -26,19 +25,6 @@ type MessageIds = const useUnknownMessageBase = 'Prefer the safe `: unknown` for a `{{method}}`{{append}} callback variable.'; -/** - * `x.memberName` => 'memberKey' - * - * `const mk = 'memberKey'; x[mk]` => 'memberKey' - * - * `const mk = 1234; x[mk]` => 1234 - */ -const getStaticMemberAccessKey = ( - { computed, property }: TSESTree.MemberExpression, - scope: Scope, -): { value: unknown } | null => - computed ? getStaticValue(property, scope) : { value: property.name }; - export default createRule<[], MessageIds>({ name: 'use-unknown-in-catch-callback-variable', meta: { @@ -242,9 +228,9 @@ export default createRule<[], MessageIds>({ return; } - const staticMemberAccessKey = getStaticMemberAccessKey( + const staticMemberAccessKey = getStaticMemberAccessValue( callee, - context.sourceCode.getScope(callee), + context, ); if (!staticMemberAccessKey) { return; @@ -259,7 +245,7 @@ export default createRule<[], MessageIds>({ append: string; argIndexToCheck: number; }[] - ).find(({ method }) => staticMemberAccessKey.value === method); + ).find(({ method }) => staticMemberAccessKey === method); if (!promiseMethodInfo) { return; } diff --git a/packages/eslint-plugin/src/util/misc.ts b/packages/eslint-plugin/src/util/misc.ts index 64f23f2e81bc..aeed9f96d039 100644 --- a/packages/eslint-plugin/src/util/misc.ts +++ b/packages/eslint-plugin/src/util/misc.ts @@ -1,13 +1,13 @@ /** * @fileoverview Really small utility functions that didn't deserve their own files */ - import { requiresQuoting } from '@typescript-eslint/type-utils'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; import * as ts from 'typescript'; -import { isParenthesized } from './astUtils'; +import { getStaticValue, isParenthesized } from './astUtils'; const DEFINITION_EXTENSIONS = [ ts.Extension.Dts, @@ -232,6 +232,46 @@ function isParenlessArrowFunction( ); } +type NodeWithKey = + | TSESTree.MemberExpression + | TSESTree.MethodDefinition + | TSESTree.Property + | TSESTree.PropertyDefinition + | TSESTree.TSAbstractMethodDefinition + | TSESTree.TSAbstractPropertyDefinition; +function getStaticMemberAccessValue( + node: NodeWithKey, + { sourceCode }: RuleContext, +): string | undefined { + const key = + node.type === AST_NODE_TYPES.MemberExpression ? node.property : node.key; + if (!node.computed) { + return key.type === AST_NODE_TYPES.Literal + ? `${key.value}` + : (key as TSESTree.Identifier | TSESTree.PrivateIdentifier).name; + } + const value = getStaticValue(key, sourceCode.getScope(node))?.value as + | string + | number + | null + | undefined; + return value == null ? undefined : `${value}`; +} + +/** + * Answers whether the member expression looks like + * `x.memberName`, `x['memberName']`, + * or even `const mn = 'memberName'; x[mn]` (or optional variants thereof). + */ +const isStaticMemberAccessOfValue = ( + memberExpression: NodeWithKey, + context: RuleContext, + ...values: string[] +): boolean => + (values as (string | undefined)[]).includes( + getStaticMemberAccessValue(memberExpression, context), + ); + export { arrayGroupByToMap, arraysAreEqual, @@ -240,11 +280,13 @@ export { findFirstResult, formatWordList, getEnumNames, + getStaticMemberAccessValue, getNameFromIndexSignature, getNameFromMember, isDefinitionFile, isRestParameterDeclaration, isParenlessArrowFunction, + isStaticMemberAccessOfValue, MemberNameType, type RequireKeys, typeNodeRequiresParentheses,