Skip to content

fix(eslint-plugin): properly coerce all types to string in getStaticMemberAccessValue #10004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Sep 22, 2024
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