Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
import {
createRule,
getStaticMemberAccessValue,
getStaticStringValue,
isAssignee,
isFunction,
isStaticMemberAccessOfValue,
Expand All @@ -25,7 +24,7 @@ interface NodeWithModifiers {

interface PropertiesInfo {
properties: TSESTree.PropertyDefinition[];
excludeSet: Set<string>;
excludeSet: Set<string | symbol>;
}

const printNodeModifiers = (
Expand Down Expand Up @@ -132,9 +131,7 @@ export default createRule<Options, MessageIds>({
const { excludeSet } =
propertiesInfoStack[propertiesInfoStack.length - 1];

const name =
getStaticStringValue(node.property) ??
context.sourceCode.getText(node.property);
const name = getStaticMemberAccessValue(node, context);

if (name) {
excludeSet.add(name);
Expand Down
4 changes: 3 additions & 1 deletion packages/eslint-plugin/src/rules/class-methods-use-this.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ export default createRule<Options, MessageIds>({
node.key.type === AST_NODE_TYPES.PrivateIdentifier ? '#' : '';
const name = getStaticMemberAccessValue(node, context);

return !exceptMethods.has(hashIfNeeded + (name ?? ''));
return (
typeof name !== 'string' || !exceptMethods.has(hashIfNeeded + name)
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as tsutils from 'ts-api-utils';

import { getStaticMemberAccessValue } from './misc';

const ARRAY_PREDICATE_FUNCTIONS = new Set([
const ARRAY_PREDICATE_FUNCTIONS = new Set<unknown>([
'filter',
'find',
'findIndex',
Expand All @@ -30,7 +30,7 @@ export function isArrayMethodCallWithPredicate(

const staticAccessValue = getStaticMemberAccessValue(node.callee, context);

if (!staticAccessValue || !ARRAY_PREDICATE_FUNCTIONS.has(staticAccessValue)) {
if (!ARRAY_PREDICATE_FUNCTIONS.has(staticAccessValue)) {
return false;
}

Expand Down
79 changes: 64 additions & 15 deletions packages/eslint-plugin/src/util/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,36 +239,85 @@ type NodeWithKey =
| TSESTree.PropertyDefinition
| TSESTree.TSAbstractMethodDefinition
| TSESTree.TSAbstractPropertyDefinition;

/**
* Gets a member being accessed or declared if its value can be determined statically, and
* resolves it to the string or symbol value that will be used as the actual member
* access key at runtime. Otherwise, returns `undefined`.
*
* ```ts
* x.member // returns 'member'
* ^^^^^^^^
*
* x?.member // returns 'member' (optional chaining is treated the same)
* ^^^^^^^^^
*
* x['value'] // returns 'value'
* ^^^^^^^^^^
*
* x[Math.random()] // returns undefined (not a static value)
* ^^^^^^^^^^^^^^^^
*
* arr[0] // returns '0' (NOT 0)
* ^^^^^^
*
* arr[0n] // returns '0' (NOT 0n)
* ^^^^^^^
*
* const s = Symbol.for('symbolName')
* x[s] // returns `Symbol.for('symbolName')` (since it's a static/global symbol)
* ^^^^
*
* const us = Symbol('symbolName')
* x[us] // returns undefined (since it's a unique symbol, so not statically analyzable)
* ^^^^^
*
* var object = {
* 1234: '4567', // returns '1234' (NOT 1234)
* ^^^^^^^^^^^^
* method() { } // returns 'method'
* ^^^^^^^^^^^^
* }
*
* class WithMembers {
* foo: string // returns 'foo'
* ^^^^^^^^^^^
* }
* ```
*/
function getStaticMemberAccessValue(
node: NodeWithKey,
{ sourceCode }: RuleContext<string, unknown[]>,
): string | undefined {
): string | symbol | 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 { type } = key;
if (
!node.computed &&
(type === AST_NODE_TYPES.Identifier ||
type === AST_NODE_TYPES.PrivateIdentifier)
) {
return key.name;
}
const result = getStaticValue(key, sourceCode.getScope(node));
if (!result) {
return undefined;
}
const value = getStaticValue(key, sourceCode.getScope(node))?.value as
| string
| number
| null
| undefined;
return value == null ? undefined : `${value}`;
const { value } = result;
return typeof value === 'symbol' ? value : String(value);
}

/**
* Answers whether the member expression looks like
* `x.memberName`, `x['memberName']`,
* or even `const mn = 'memberName'; x[mn]` (or optional variants thereof).
* `x.value`, `x['value']`,
* or even `const v = 'value'; x[v]` (or optional variants thereof).
*/
const isStaticMemberAccessOfValue = (
memberExpression: NodeWithKey,
context: RuleContext<string, unknown[]>,
...values: string[]
...values: (string | symbol)[]
): boolean =>
(values as (string | undefined)[]).includes(
(values as (string | symbol | undefined)[]).includes(
getStaticMemberAccessValue(memberExpression, context),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export class Test {
'method'() {}
['prop']() {}
[\`prop\`]() {}
[null]() {}
[\`\${v}\`](): void {}

foo = () => {
Expand All @@ -416,7 +417,7 @@ export class Test {
`,
options: [
{
allowedNames: ['prop', 'method', 'foo'],
allowedNames: ['prop', 'method', 'null', 'foo'],
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,15 @@ ruleTester.run('prefer-promise-reject-errors', rule, {
declare const foo: PromiseConstructor;
foo.reject(new Error());
`,
'console[Symbol.iterator]();',
`
class A {
a = [];
[Symbol.iterator]() {
return this.a[Symbol.iterator]();
}
}
`,
],
invalid: [
{
Expand Down
Loading