From 845839aa0cb2152494319553f48f37a4960a9b5a Mon Sep 17 00:00:00 2001 From: undsoft Date: Fri, 28 Mar 2025 21:17:32 +0100 Subject: [PATCH 01/12] fix(eslint-plugin): [no-deprecated] adds support for string literal member access (#10958) --- .../eslint-plugin/src/rules/no-deprecated.ts | 31 +- .../tests/rules/no-deprecated.test.ts | 378 ++++++++++++++++++ 2 files changed, 397 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index 94d7a5ed7262..7c2c0bbd1546 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -10,13 +10,14 @@ import { createRule, getParserServices, nullThrows, - typeOrValueSpecifiersSchema, typeMatchesSomeSpecifier, + typeOrValueSpecifiersSchema, } from '../util'; type IdentifierLike = | TSESTree.Identifier | TSESTree.JSXIdentifier + | TSESTree.Literal | TSESTree.PrivateIdentifier | TSESTree.Super; @@ -209,13 +210,9 @@ export default createRule({ return displayParts ? ts.displayPartsToString(displayParts) : ''; } - type CallLikeNode = - | TSESTree.CallExpression - | TSESTree.JSXOpeningElement - | TSESTree.NewExpression - | TSESTree.TaggedTemplateExpression; + type CalleeNode = TSESTree.Expression | TSESTree.JSXTagNameExpression; - function isNodeCalleeOfParent(node: TSESTree.Node): node is CallLikeNode { + function isNodeCalleeOfParent(node: TSESTree.Node): node is CalleeNode { switch (node.parent?.type) { case AST_NODE_TYPES.NewExpression: case AST_NODE_TYPES.CallExpression: @@ -232,7 +229,7 @@ export default createRule({ } } - function getCallLikeNode(node: TSESTree.Node): CallLikeNode | undefined { + function getCallLikeNode(node: TSESTree.Node): CalleeNode | undefined { let callee = node; while ( @@ -245,7 +242,7 @@ export default createRule({ return isNodeCalleeOfParent(callee) ? callee : undefined; } - function getCallLikeDeprecation(node: CallLikeNode): string | undefined { + function getCallLikeDeprecation(node: CalleeNode): string | undefined { const tsNode = services.esTreeNodeToTSNodeMap.get(node.parent); // If the node is a direct function call, we look for its signature. @@ -254,7 +251,10 @@ export default createRule({ 'Expected call like node to have signature', ); - const symbol = services.getSymbolAtLocation(node); + const nodeToFind = + node.type === AST_NODE_TYPES.MemberExpression ? node.property : node; + + const symbol = services.getSymbolAtLocation(nodeToFind); const aliasedSymbol = symbol != null && tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) @@ -334,14 +334,16 @@ export default createRule({ if ( node.parent.type === AST_NODE_TYPES.JSXAttribute && - node.type !== AST_NODE_TYPES.Super + node.type !== AST_NODE_TYPES.Super && + node.type !== AST_NODE_TYPES.Literal ) { return getJSXAttributeDeprecation(node.parent.parent, node.name); } if ( node.parent.type === AST_NODE_TYPES.Property && - node.type !== AST_NODE_TYPES.Super + node.type !== AST_NODE_TYPES.Super && + node.type !== AST_NODE_TYPES.Literal ) { const property = services .getTypeAtLocation(node.parent.parent) @@ -401,6 +403,7 @@ export default createRule({ checkIdentifier(node); } }, + 'MemberExpression > Literal': checkIdentifier, PrivateIdentifier: checkIdentifier, Super: checkIdentifier, }; @@ -416,5 +419,9 @@ function getReportedNodeName(node: IdentifierLike): string { return `#${node.name}`; } + if (node.type === AST_NODE_TYPES.Literal) { + return String(node.value); + } + return node.name; } diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index 6469c0fe5c23..8c31d2f46fb8 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -45,8 +45,32 @@ ruleTester.run('no-deprecated', rule, { /** @deprecated */ c: 2, }; + a['b']; + `, + ` + const a = { + b: 1, + /** @deprecated */ c: 2, + }; + + a['b' + 'c']; + `, + ` + const a = { + b: 1, + /** @deprecated */ c: 2, + }; + a?.b; `, + ` + const a = { + b: 1, + /** @deprecated */ c: 2, + }; + + a?.['b']; + `, ` declare const a: { b: 1; @@ -55,6 +79,14 @@ ruleTester.run('no-deprecated', rule, { a.b; `, + ` + declare const a: { + b: 1; + /** @deprecated */ c: 2; + }; + + a['b']; + `, ` class A { b: 1; @@ -63,6 +95,32 @@ ruleTester.run('no-deprecated', rule, { new A().b; `, + ` + class A { + b: 1; + /** @deprecated */ c: 2; + } + + new A()['b']; + `, + ` + class A { + c: 1; + } + class B { + /** @deprecated */ c: 2; + } + + new A()['c']; + `, + ` + class A { + b: () => {}; + /** @deprecated */ c: () => {}; + } + + new A()['b'](); + `, ` class A { accessor b: 1; @@ -71,6 +129,14 @@ ruleTester.run('no-deprecated', rule, { new A().b; `, + ` + class A { + accessor b: 1; + /** @deprecated */ accessor c: 2; + } + + new A()['b']; + `, ` declare class A { /** @deprecated */ @@ -80,6 +146,15 @@ ruleTester.run('no-deprecated', rule, { A.c; `, + ` + declare class A { + /** @deprecated */ + static b: string; + static c: string; + } + + A['c']; + `, ` declare class A { /** @deprecated */ @@ -89,6 +164,15 @@ ruleTester.run('no-deprecated', rule, { A.c; `, + ` + declare class A { + /** @deprecated */ + static accessor b: string; + static accessor c: string; + } + + A['c']; + `, ` namespace A { /** @deprecated */ @@ -98,6 +182,15 @@ ruleTester.run('no-deprecated', rule, { A.c; `, + ` + namespace A { + /** @deprecated */ + export const b = ''; + export const c = ''; + } + + A['c']; + `, ` enum A { /** @deprecated */ @@ -107,6 +200,15 @@ ruleTester.run('no-deprecated', rule, { A.c; `, + ` + enum A { + /** @deprecated */ + b = 'b', + c = 'c', + } + + A['c']; + `, ` function a(value: 'b' | undefined): void; /** @deprecated */ @@ -609,6 +711,22 @@ exists('/foo'); }, ], }, + { + code: ` + /** @deprecated */ const a = { b: 1 }; + console.log(a['b']); + `, + errors: [ + { + column: 21, + data: { name: 'a' }, + endColumn: 22, + endLine: 3, + line: 3, + messageId: 'deprecated', + }, + ], + }, { code: ` /** @deprecated */ const a = { b: 1 }; @@ -625,6 +743,22 @@ exists('/foo'); }, ], }, + { + code: ` + /** @deprecated */ const a = { b: 1 }; + console.log(a?.['b']); + `, + errors: [ + { + column: 21, + data: { name: 'a' }, + endColumn: 22, + endLine: 3, + line: 3, + messageId: 'deprecated', + }, + ], + }, { code: ` /** @deprecated */ const a = { b: { c: 1 } }; @@ -673,6 +807,22 @@ exists('/foo'); }, ], }, + { + code: ` + /** @deprecated */ const a = { b: { c: 1 } }; + a?.['b']?.['c']; + `, + errors: [ + { + column: 9, + data: { name: 'a' }, + endColumn: 10, + endLine: 3, + line: 3, + messageId: 'deprecated', + }, + ], + }, { code: ` const a = { @@ -691,6 +841,24 @@ exists('/foo'); }, ], }, + { + code: ` + const a = { + /** @deprecated */ b: { c: 1 }, + }; + a['b']['c']; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 5, + line: 5, + messageId: 'deprecated', + }, + ], + }, { code: ` declare const a: { @@ -1090,6 +1258,28 @@ exists('/foo'); declare const a: A; + a['b']; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare class A { + /** @deprecated */ + b: () => string; + } + + declare const a: A; + a.b(); `, errors: [ @@ -1103,6 +1293,28 @@ exists('/foo'); }, ], }, + { + code: ` + declare class A { + /** @deprecated */ + b: () => string; + } + + declare const a: A; + + a['b'](); + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, { code: ` interface A { @@ -1125,6 +1337,28 @@ exists('/foo'); }, ], }, + { + code: ` + interface A { + /** @deprecated */ + b: () => string; + } + + declare const a: A; + + a['b'](); + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, { code: ` class A { @@ -1192,6 +1426,26 @@ exists('/foo'); }, ], }, + { + code: ` + declare class A { + /** @deprecated */ + static b: string; + } + + A['b']; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 7, + line: 7, + messageId: 'deprecated', + }, + ], + }, { code: ` declare const a: { @@ -1212,6 +1466,26 @@ exists('/foo'); }, ], }, + { + code: ` + declare const a: { + /** @deprecated */ + b: string; + }; + + a['b']; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 7, + line: 7, + messageId: 'deprecated', + }, + ], + }, { code: ` interface A { @@ -1384,6 +1658,26 @@ exists('/foo'); }, ], }, + { + code: ` + namespace A { + /** @deprecated */ + export const b = ''; + } + + A['b']; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 7, + line: 7, + messageId: 'deprecated', + }, + ], + }, { code: ` export namespace A { @@ -1424,6 +1718,26 @@ exists('/foo'); }, ], }, + { + code: ` + namespace A { + /** @deprecated */ + export function b() {} + } + + A['b'](); + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 7, + line: 7, + messageId: 'deprecated', + }, + ], + }, { code: ` namespace assert { @@ -1510,6 +1824,26 @@ exists('/foo'); }, ], }, + { + code: ` + enum A { + /** @deprecated */ + a, + } + + A['a']; + `, + errors: [ + { + column: 11, + data: { name: 'a' }, + endColumn: 14, + endLine: 7, + line: 7, + messageId: 'deprecated', + }, + ], + }, { code: ` /** @deprecated */ @@ -2814,6 +3148,28 @@ class B extends A { declare const a: A; + a['b']; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare class A { + /** @deprecated */ + accessor b: () => string; + } + + declare const a: A; + a.b(); `, errors: [ @@ -2827,6 +3183,28 @@ class B extends A { }, ], }, + { + code: ` + declare class A { + /** @deprecated */ + accessor b: () => string; + } + + declare const a: A; + + a['b'](); + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, { code: ` class A { From 82f748f78a7ca1d2b10e4f113bd450a5f740e5da Mon Sep 17 00:00:00 2001 From: undsoft Date: Fri, 4 Apr 2025 16:00:46 +0200 Subject: [PATCH 02/12] fix(eslint-plugin): [no-deprecated] adds support for computed member access (#10958) --- .../eslint-plugin/src/rules/no-deprecated.ts | 82 +++++- .../tests/rules/no-deprecated.test.ts | 258 ++++++++++++++++++ 2 files changed, 325 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index 7c2c0bbd1546..3eea60e2a707 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -1,25 +1,30 @@ -import type { TSESTree } from '@typescript-eslint/utils'; - -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; -import type { TypeOrValueSpecifier } from '../util'; - import { createRule, getParserServices, nullThrows, typeMatchesSomeSpecifier, + TypeOrValueSpecifier, typeOrValueSpecifiersSchema, } from '../util'; +import { getPropertyName } from '@typescript-eslint/utils/ast-utils'; +import { RuleContext } from '@typescript-eslint/utils/ts-eslint'; type IdentifierLike = | TSESTree.Identifier | TSESTree.JSXIdentifier | TSESTree.Literal | TSESTree.PrivateIdentifier - | TSESTree.Super; + | TSESTree.Super + | TSESTree.TemplateLiteral; + +type IdentifierInComputedProperty = + | TSESTree.Identifier + | TSESTree.Literal + | TSESTree.TemplateLiteral; type MessageIds = 'deprecated' | 'deprecatedWithReason'; @@ -189,6 +194,19 @@ export default createRule({ } } + function isInComputedProperty( + node: IdentifierLike, + ): node is IdentifierInComputedProperty { + return ( + node.parent.type === AST_NODE_TYPES.MemberExpression && + node.parent.computed && + node === node.parent.property && + (node.type === AST_NODE_TYPES.Literal || + node.type === AST_NODE_TYPES.TemplateLiteral || + node.type === AST_NODE_TYPES.Identifier) + ); + } + function getJsDocDeprecation( symbol: ts.Signature | ts.Symbol | undefined, ): string | undefined { @@ -210,6 +228,22 @@ export default createRule({ return displayParts ? ts.displayPartsToString(displayParts) : ''; } + function getComputedPropertyDeprecation( + node: TSESTree.MemberExpression, + ): string | undefined { + const propertyName = getPropertyName( + node, + context.sourceCode.getScope(node), + ); + if (!propertyName) { + return undefined; + } + + const objectType = services.getTypeAtLocation(node.object); + const property = objectType.getProperty(propertyName); + return getJsDocDeprecation(property); + } + type CalleeNode = TSESTree.Expression | TSESTree.JSXTagNameExpression; function isNodeCalleeOfParent(node: TSESTree.Node): node is CalleeNode { @@ -251,10 +285,11 @@ export default createRule({ 'Expected call like node to have signature', ); - const nodeToFind = - node.type === AST_NODE_TYPES.MemberExpression ? node.property : node; + if (node.type === AST_NODE_TYPES.MemberExpression && node.computed) { + return getComputedPropertyDeprecation(node); + } - const symbol = services.getSymbolAtLocation(nodeToFind); + const symbol = services.getSymbolAtLocation(node); const aliasedSymbol = symbol != null && tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) @@ -332,18 +367,22 @@ export default createRule({ return getCallLikeDeprecation(callLikeNode); } + if (isInComputedProperty(node)) { + return getComputedPropertyDeprecation( + node.parent as TSESTree.MemberExpression, + ); + } + if ( node.parent.type === AST_NODE_TYPES.JSXAttribute && - node.type !== AST_NODE_TYPES.Super && - node.type !== AST_NODE_TYPES.Literal + node.type !== AST_NODE_TYPES.Super ) { return getJSXAttributeDeprecation(node.parent.parent, node.name); } if ( node.parent.type === AST_NODE_TYPES.Property && - node.type !== AST_NODE_TYPES.Super && - node.type !== AST_NODE_TYPES.Literal + node.type !== AST_NODE_TYPES.Super ) { const property = services .getTypeAtLocation(node.parent.parent) @@ -380,7 +419,7 @@ export default createRule({ return; } - const name = getReportedNodeName(node); + const name = getReportedNodeName(node, context); context.report({ ...(reason @@ -404,13 +443,17 @@ export default createRule({ } }, 'MemberExpression > Literal': checkIdentifier, + 'MemberExpression > TemplateLiteral': checkIdentifier, PrivateIdentifier: checkIdentifier, Super: checkIdentifier, }; }, }); -function getReportedNodeName(node: IdentifierLike): string { +function getReportedNodeName( + node: IdentifierLike, + context: Readonly>, +): string { if (node.type === AST_NODE_TYPES.Super) { return 'super'; } @@ -423,5 +466,14 @@ function getReportedNodeName(node: IdentifierLike): string { return String(node.value); } + if (node.type === AST_NODE_TYPES.TemplateLiteral) { + return ( + getPropertyName( + node.parent as TSESTree.MemberExpression, + context.sourceCode.getScope(node), + ) || '' + ); + } + return node.name; } diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index 8c31d2f46fb8..5144a1afa9c4 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -61,6 +61,16 @@ ruleTester.run('no-deprecated', rule, { /** @deprecated */ c: 2, }; + const key = 'b'; + + a[key]; + `, + ` + const a = { + b: 1, + /** @deprecated */ c: 2, + }; + a?.b; `, ` @@ -87,6 +97,44 @@ ruleTester.run('no-deprecated', rule, { a['b']; `, + ` + declare const a: { + b: 1; + /** @deprecated */ c: 2; + }; + + a[\`\${'b'}\`]; + `, + ` + declare const a: { + b: 1; + /** @deprecated */ c: 2; + }; + + const key = 'b'; + + a[\`\${key}\`]; + `, + ` + declare const a: { + /** @deprecated */ c: 1; + cc: 2; + }; + + const key = 'c'; + + a[\`\${key + key}\`]; + `, + ` + declare const a: { + /** @deprecated */ c: 1; + cc: 2; + }; + + const key = 'c'; + + a[\`\${key}\${key}\`]; + `, ` class A { b: 1; @@ -103,6 +151,15 @@ ruleTester.run('no-deprecated', rule, { new A()['b']; `, + ` + class A { + b: 1; + /** @deprecated */ c: 2; + } + const key = 'b'; + + new A()[b]; + `, ` class A { c: 1; @@ -841,6 +898,25 @@ exists('/foo'); }, ], }, + { + code: ` + const a = { + /** @deprecated */ b: 1, + }; + const key = 'b'; + a[key]; + `, + errors: [ + { + column: 11, + data: { name: 'key' }, + endColumn: 14, + endLine: 6, + line: 6, + messageId: 'deprecated', + }, + ], + }, { code: ` const a = { @@ -1212,6 +1288,52 @@ exists('/foo'); b(): string; } + declare const a: A; + const key = 'b'; + + a[key]; + `, + errors: [ + { + column: 11, + data: { name: 'key' }, + endColumn: 14, + endLine: 10, + line: 10, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare class A { + /** @deprecated */ + b(): string; + } + + declare const a: A; + const key = 'b'; + + a[key](); + `, + errors: [ + { + column: 11, + data: { name: 'key' }, + endColumn: 14, + endLine: 10, + line: 10, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare class A { + /** @deprecated */ + b(): string; + } + declare const a: A; a.b(); @@ -1271,6 +1393,74 @@ exists('/foo'); }, ], }, + { + code: ` + declare class A { + /** @deprecated */ + b(): string; + } + + declare const a: A; + const key = 'b'; + + a[\`\${key}\`]; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 19, + endLine: 10, + line: 10, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare class A { + /** @deprecated */ + computed(): string; + } + + declare const a: A; + const k1 = 'comp'; + const k2 = 'uted'; + + a[\`\${k1}\${k2}\`]; + `, + errors: [ + { + column: 11, + data: { name: 'computed' }, + endColumn: 23, + endLine: 11, + line: 11, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare class A { + /** @deprecated */ + b(): string; + } + + declare const a: A; + const c = \`\${a.b}\`; + `, + errors: [ + { + column: 24, + data: { name: 'b' }, + endColumn: 25, + endLine: 8, + line: 8, + messageId: 'deprecated', + }, + ], + }, { code: ` declare class A { @@ -1344,6 +1534,52 @@ exists('/foo'); b: () => string; } + declare const a: A; + const key = 'b'; + + a[key]; + `, + errors: [ + { + column: 11, + data: { name: 'key' }, + endColumn: 14, + endLine: 10, + line: 10, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + interface A { + /** @deprecated */ + b: () => string; + } + + declare const a: A; + const key = 'b'; + + a[key](); + `, + errors: [ + { + column: 11, + data: { name: 'key' }, + endColumn: 14, + endLine: 10, + line: 10, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + interface A { + /** @deprecated */ + b: () => string; + } + declare const a: A; a['b'](); @@ -1831,6 +2067,28 @@ exists('/foo'); a, } + const key = 'a'; + + A[key]; + `, + errors: [ + { + column: 11, + data: { name: 'key' }, + endColumn: 14, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + enum A { + /** @deprecated */ + a, + } + A['a']; `, errors: [ From 74c79d9a546fd7390115d73943c73e494b39a5b6 Mon Sep 17 00:00:00 2001 From: undsoft Date: Fri, 4 Apr 2025 16:23:44 +0200 Subject: [PATCH 03/12] fix(eslint-plugin): [no-deprecated] adds support for computed member access (#10958) --- packages/eslint-plugin/src/rules/no-deprecated.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index 3eea60e2a707..b1aba1fe6b6a 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -1,17 +1,20 @@ -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import type { TSESTree } from '@typescript-eslint/utils'; +import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; + +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import { getPropertyName } from '@typescript-eslint/utils/ast-utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; +import type { TypeOrValueSpecifier } from '../util'; + import { createRule, getParserServices, nullThrows, typeMatchesSomeSpecifier, - TypeOrValueSpecifier, typeOrValueSpecifiersSchema, } from '../util'; -import { getPropertyName } from '@typescript-eslint/utils/ast-utils'; -import { RuleContext } from '@typescript-eslint/utils/ts-eslint'; type IdentifierLike = | TSESTree.Identifier From cb50efed3d43e873f59835e981a7fe575317de0a Mon Sep 17 00:00:00 2001 From: undsoft Date: Sun, 20 Apr 2025 16:55:37 +0200 Subject: [PATCH 04/12] fix(eslint-plugin): [no-deprecated] adds support for computed member access (#10958) --- .../eslint-plugin/src/rules/no-deprecated.ts | 79 ++++++++++--------- .../tests/rules/no-deprecated.test.ts | 12 +-- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index b1aba1fe6b6a..4c59104262ed 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -266,7 +266,7 @@ export default createRule({ } } - function getCallLikeNode(node: TSESTree.Node): CalleeNode | undefined { + function getCalleeNode(node: TSESTree.Node): CalleeNode | undefined { let callee = node; while ( @@ -288,10 +288,6 @@ export default createRule({ 'Expected call like node to have signature', ); - if (node.type === AST_NODE_TYPES.MemberExpression && node.computed) { - return getComputedPropertyDeprecation(node); - } - const symbol = services.getSymbolAtLocation(node); const aliasedSymbol = symbol != null && tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.Alias) @@ -364,18 +360,55 @@ export default createRule({ return getJsDocDeprecation(symbol); } - function getDeprecationReason(node: IdentifierLike): string | undefined { - const callLikeNode = getCallLikeNode(node); - if (callLikeNode) { - return getCallLikeDeprecation(callLikeNode); + function getReportedNodeName( + node: IdentifierLike, + context: Readonly>, + ): string { + if (node.type === AST_NODE_TYPES.Super) { + return 'super'; + } + + if (node.type === AST_NODE_TYPES.PrivateIdentifier) { + return `#${node.name}`; + } + + if (node.type === AST_NODE_TYPES.Literal) { + return String(node.value); + } + + if (node.type === AST_NODE_TYPES.TemplateLiteral) { + return ( + getPropertyName( + node.parent as TSESTree.MemberExpression, + context.sourceCode.getScope(node), + ) || '' + ); } + if (isInComputedProperty(node)) { + return ( + getPropertyName( + node.parent as TSESTree.MemberExpression, + context.sourceCode.getScope(node), + ) || '' + ); + } + + return node.name; + } + + function getDeprecationReason(node: IdentifierLike): string | undefined { if (isInComputedProperty(node)) { return getComputedPropertyDeprecation( node.parent as TSESTree.MemberExpression, ); } + const callLikeNode = getCalleeNode(node); + if (callLikeNode) { + return getCallLikeDeprecation(callLikeNode); + } + if ( node.parent.type === AST_NODE_TYPES.JSXAttribute && node.type !== AST_NODE_TYPES.Super @@ -452,31 +485,3 @@ export default createRule({ }; }, }); - -function getReportedNodeName( - node: IdentifierLike, - context: Readonly>, -): string { - if (node.type === AST_NODE_TYPES.Super) { - return 'super'; - } - - if (node.type === AST_NODE_TYPES.PrivateIdentifier) { - return `#${node.name}`; - } - - if (node.type === AST_NODE_TYPES.Literal) { - return String(node.value); - } - - if (node.type === AST_NODE_TYPES.TemplateLiteral) { - return ( - getPropertyName( - node.parent as TSESTree.MemberExpression, - context.sourceCode.getScope(node), - ) || '' - ); - } - - return node.name; -} diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index 5144a1afa9c4..d4553816dfe7 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -909,7 +909,7 @@ exists('/foo'); errors: [ { column: 11, - data: { name: 'key' }, + data: { name: 'b' }, endColumn: 14, endLine: 6, line: 6, @@ -1296,7 +1296,7 @@ exists('/foo'); errors: [ { column: 11, - data: { name: 'key' }, + data: { name: 'b' }, endColumn: 14, endLine: 10, line: 10, @@ -1319,7 +1319,7 @@ exists('/foo'); errors: [ { column: 11, - data: { name: 'key' }, + data: { name: 'b' }, endColumn: 14, endLine: 10, line: 10, @@ -1542,7 +1542,7 @@ exists('/foo'); errors: [ { column: 11, - data: { name: 'key' }, + data: { name: 'b' }, endColumn: 14, endLine: 10, line: 10, @@ -1565,7 +1565,7 @@ exists('/foo'); errors: [ { column: 11, - data: { name: 'key' }, + data: { name: 'b' }, endColumn: 14, endLine: 10, line: 10, @@ -2074,7 +2074,7 @@ exists('/foo'); errors: [ { column: 11, - data: { name: 'key' }, + data: { name: 'a' }, endColumn: 14, endLine: 9, line: 9, From 72234962e99e7b2822fcd98869a3e61a018eb81d Mon Sep 17 00:00:00 2001 From: undsoft Date: Sun, 20 Apr 2025 17:16:17 +0200 Subject: [PATCH 05/12] fix(eslint-plugin): [no-deprecated] adds support for computed member access (#10958) --- packages/eslint-plugin/src/rules/no-deprecated.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index 7349011f1987..8a4481cafd0e 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -376,16 +376,10 @@ export default createRule({ return String(node.value); } - if (node.type === AST_NODE_TYPES.TemplateLiteral) { - return ( - getPropertyName( - node.parent as TSESTree.MemberExpression, - context.sourceCode.getScope(node), - ) || '' - ); - } - - if (isInComputedProperty(node)) { + if ( + node.type === AST_NODE_TYPES.TemplateLiteral || + isInComputedProperty(node) + ) { return ( getPropertyName( node.parent as TSESTree.MemberExpression, From 335a66c14b0095bdd9fb0b09e2dba90f38b78cc0 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 29 Sep 2025 09:35:27 -0400 Subject: [PATCH 06/12] Unified branches of logic --- .vscode/launch.json | 1 + .../eslint-plugin/src/rules/no-deprecated.ts | 104 ++++++++---------- .../tests/rules/no-deprecated.test.ts | 24 ---- 3 files changed, 47 insertions(+), 82 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 3a8ab3719b3d..34859b988c28 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,6 +9,7 @@ "request": "launch", "name": "Test Current eslint-plugin Rule", "cwd": "${workspaceFolder}/packages/eslint-plugin/", + "runtimeExecutable": "/Users/josh/.nvm/versions/node/v24.3.0/bin/node", "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", "args": [ "--no-cache", diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index 111c59f1caae..1ef63ae2aa25 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -1,5 +1,4 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import { getPropertyName } from '@typescript-eslint/utils/ast-utils'; @@ -232,22 +231,44 @@ export default createRule({ return displayParts ? ts.displayPartsToString(displayParts) : ''; } - function getComputedPropertyDeprecation( - node: TSESTree.MemberExpression, - ): string | undefined { - const propertyName = getPropertyName( - node, - context.sourceCode.getScope(node), - ); + function getComputedPropertyDeprecation(node: TSESTree.MemberExpression) { + const objectType = services.getTypeAtLocation(node.object); + const propertyName = getPropertyNameFromScopeOrTypes(node); if (!propertyName) { return undefined; } - const objectType = services.getTypeAtLocation(node.object); const property = objectType.getProperty(propertyName); return getJsDocDeprecation(property); } + /** + * Gets the static name of a MemberExpression's .property. This first attempts + * a quick lookup from the ESTree scope, then falls back as needed to slower + * type information for non-scoped information such as enum keys as needed. + */ + function getPropertyNameFromScopeOrTypes(node: TSESTree.MemberExpression) { + const fromScope = getPropertyName( + node, + context.sourceCode.getScope(node), + ); + if (fromScope) { + return fromScope; + } + + const type = services.getTypeAtLocation(node.property); + + if (type.isStringLiteral()) { + return type.value; + } + + if (type.isLiteral()) { + return String(type.value as number); + } + + return undefined; + } + type CalleeNode = TSESTree.Expression | TSESTree.JSXTagNameExpression; function isNodeCalleeOfParent(node: TSESTree.Node): node is CalleeNode { @@ -361,10 +382,7 @@ export default createRule({ return getJsDocDeprecation(symbol); } - function getReportedNodeName( - node: IdentifierLike, - context: Readonly>, - ): string { + function getReportedNodeName(node: IdentifierLike): string { if (node.type === AST_NODE_TYPES.Super) { return 'super'; } @@ -382,9 +400,8 @@ export default createRule({ isInComputedProperty(node) ) { return ( - getPropertyName( + getPropertyNameFromScopeOrTypes( node.parent as TSESTree.MemberExpression, - context.sourceCode.getScope(node), ) || '' ); } @@ -392,7 +409,7 @@ export default createRule({ return node.name; } - function getDeprecationReason(node: IdentifierLike): string | undefined { + function getDeprecationReason(node: IdentifierLike) { if (isInComputedProperty(node)) { return getComputedPropertyDeprecation( node.parent as TSESTree.MemberExpression, @@ -448,6 +465,18 @@ export default createRule({ } const type = services.getTypeAtLocation(node); + + // TypeScript doesn't allow non-literal keys for computed properties, + // even though in cases like String('...') we can still find a deprecation. + if ( + node.parent.type === AST_NODE_TYPES.MemberExpression && + node.parent.computed && + node.parent.property === node && + !type.isLiteral() + ) { + return; + } + if ( typeMatchesSomeSpecifier(type, allow, services.program) || valueMatchesSomeSpecifier(node, allow, services.program, type) @@ -455,7 +484,7 @@ export default createRule({ return; } - const name = getReportedNodeName(node, context); + const name = getReportedNodeName(node); context.report({ ...(reason @@ -471,46 +500,6 @@ export default createRule({ }); } - function checkMemberExpression(node: TSESTree.MemberExpression): void { - if (!node.computed) { - return; - } - - const propertyType = services.getTypeAtLocation(node.property); - - if (propertyType.isLiteral()) { - const objectType = services.getTypeAtLocation(node.object); - - const propertyName = propertyType.isStringLiteral() - ? propertyType.value - : String(propertyType.value as number); - - const property = objectType.getProperty(propertyName); - - const reason = getJsDocDeprecation(property); - if (reason == null) { - return; - } - - if (typeMatchesSomeSpecifier(objectType, allow, services.program)) { - return; - } - - context.report({ - ...(reason - ? { - messageId: 'deprecatedWithReason', - data: { name: propertyName, reason }, - } - : { - messageId: 'deprecated', - data: { name: propertyName }, - }), - node: node.property, - }); - } - } - return { Identifier(node): void { const { parent } = node; @@ -544,7 +533,6 @@ export default createRule({ checkIdentifier(node); } }, - MemberExpression: checkMemberExpression, 'MemberExpression > Literal': checkIdentifier, 'MemberExpression > TemplateLiteral': checkIdentifier, PrivateIdentifier: checkIdentifier, diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index 5b0ad843a0f1..77babd37db5c 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -620,30 +620,6 @@ exists('/foo'); const c = a['b']; `, - { - code: ` - interface AllowedType { - /** @deprecated */ - prop: string; - } - - const obj: AllowedType = { - prop: 'test', - }; - - const value = obj['prop']; - `, - options: [ - { - allow: [ - { - from: 'file', - name: 'AllowedType', - }, - ], - }, - ], - }, ` const a = { /** @deprecated */ From c4eb20eca2c4835401da80b3bf598d6ea2b0708e Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 29 Sep 2025 09:37:27 -0400 Subject: [PATCH 07/12] git checkout main -- .vscode/launch.json --- .vscode/launch.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 34859b988c28..3a8ab3719b3d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,6 @@ "request": "launch", "name": "Test Current eslint-plugin Rule", "cwd": "${workspaceFolder}/packages/eslint-plugin/", - "runtimeExecutable": "/Users/josh/.nvm/versions/node/v24.3.0/bin/node", "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", "args": [ "--no-cache", From 61ba88f990a0ee8087de54dad1be47cea77c71fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Mon, 3 Nov 2025 14:05:30 -0500 Subject: [PATCH 08/12] Update packages/eslint-plugin/tests/rules/no-deprecated.test.ts Co-authored-by: Ronen Amiel --- packages/eslint-plugin/tests/rules/no-deprecated.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index 77babd37db5c..d63b5a02812b 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -942,7 +942,7 @@ exists('/foo'); { code: ` /** @deprecated */ const a = { b: 1 }; - console.log(a['b']); + a['b']; `, errors: [ { From 72b1f1d67ff2cd1a24226fc8a7d1aab0ea32a588 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 3 Nov 2025 14:09:54 -0500 Subject: [PATCH 09/12] Remove remaining unnecessary console.logs --- .../tests/rules/no-deprecated.test.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index d63b5a02812b..e2a702f1ae0b 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -875,13 +875,13 @@ exists('/foo'); { code: ` /** @deprecated */ const a = { b: 1 }; - console.log(a); + a; `, errors: [ { - column: 21, + column: 9, data: { name: 'a' }, - endColumn: 22, + endColumn: 10, endLine: 3, line: 3, messageId: 'deprecated', @@ -926,13 +926,13 @@ exists('/foo'); { code: ` /** @deprecated */ const a = { b: 1 }; - console.log(a.b); + a.b; `, errors: [ { - column: 21, + column: 9, data: { name: 'a' }, - endColumn: 22, + endColumn: 10, endLine: 3, line: 3, messageId: 'deprecated', @@ -946,9 +946,9 @@ exists('/foo'); `, errors: [ { - column: 21, + column: 9, data: { name: 'a' }, - endColumn: 22, + endColumn: 10, endLine: 3, line: 3, messageId: 'deprecated', @@ -958,13 +958,13 @@ exists('/foo'); { code: ` /** @deprecated */ const a = { b: 1 }; - console.log(a?.b); + a?.b; `, errors: [ { - column: 21, + column: 9, data: { name: 'a' }, - endColumn: 22, + endColumn: 10, endLine: 3, line: 3, messageId: 'deprecated', @@ -974,13 +974,13 @@ exists('/foo'); { code: ` /** @deprecated */ const a = { b: 1 }; - console.log(a?.['b']); + a?.['b']; `, errors: [ { - column: 21, + column: 9, data: { name: 'a' }, - endColumn: 22, + endColumn: 10, endLine: 3, line: 3, messageId: 'deprecated', From 3b6fe1fcbee1cfb016e59f74e9274c40e0921080 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 3 Nov 2025 14:17:00 -0500 Subject: [PATCH 10/12] Literal test for numbers --- .../eslint-plugin/src/rules/no-deprecated.ts | 13 ++++------ .../tests/rules/no-deprecated.test.ts | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index 1ef63ae2aa25..0174c8129ca4 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -248,6 +248,7 @@ export default createRule({ * type information for non-scoped information such as enum keys as needed. */ function getPropertyNameFromScopeOrTypes(node: TSESTree.MemberExpression) { + // console.log({ node: context.sourceCode.getText(node) }); const fromScope = getPropertyName( node, context.sourceCode.getScope(node), @@ -258,12 +259,12 @@ export default createRule({ const type = services.getTypeAtLocation(node.property); - if (type.isStringLiteral()) { - return type.value; + if (type.isNumberLiteral()) { + return String(type.value); } - if (type.isLiteral()) { - return String(type.value as number); + if (type.isStringLiteral()) { + return type.value; } return undefined; @@ -391,10 +392,6 @@ export default createRule({ return `#${node.name}`; } - if (node.type === AST_NODE_TYPES.Literal) { - return String(node.value); - } - if ( node.type === AST_NODE_TYPES.TemplateLiteral || isInComputedProperty(node) diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index e2a702f1ae0b..c83cf47997d7 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -3857,6 +3857,31 @@ exists('/foo'); }, ], }, + { + code: ` + declare const Keys: { + a: 1; + }; + + const a = { + /** @deprecated reason for deprecation */ + [1]: 'string', + }; + + const key = Keys.a; + const c = a[key]; + `, + errors: [ + { + column: 21, + data: { name: '1', reason: 'reason for deprecation' }, + endColumn: 24, + endLine: 12, + line: 12, + messageId: 'deprecatedWithReason', + }, + ], + }, { code: ` const a = { From b5c33714e6d495afab048cbdf02911e7016ef573 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 3 Nov 2025 14:31:17 -0500 Subject: [PATCH 11/12] Retarget to just the computed identifier --- .../eslint-plugin/src/rules/no-deprecated.ts | 190 +++++++----------- .../tests/rules/no-deprecated.test.ts | 45 +++++ 2 files changed, 123 insertions(+), 112 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index 0174c8129ca4..2bf1d5d7e2c4 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -1,7 +1,6 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { getPropertyName } from '@typescript-eslint/utils/ast-utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -11,23 +10,16 @@ import { createRule, getParserServices, nullThrows, - typeMatchesSomeSpecifier, typeOrValueSpecifiersSchema, + typeMatchesSomeSpecifier, valueMatchesSomeSpecifier, } from '../util'; type IdentifierLike = | TSESTree.Identifier | TSESTree.JSXIdentifier - | TSESTree.Literal | TSESTree.PrivateIdentifier - | TSESTree.Super - | TSESTree.TemplateLiteral; - -type IdentifierInComputedProperty = - | TSESTree.Identifier - | TSESTree.Literal - | TSESTree.TemplateLiteral; + | TSESTree.Super; type MessageIds = 'deprecated' | 'deprecatedWithReason'; @@ -165,6 +157,10 @@ export default createRule({ case AST_NODE_TYPES.TSTypeParameter: return true; + // treat `export import Bar = Foo;` (and `import Foo = require('...')`) as declarations + case AST_NODE_TYPES.TSImportEqualsDeclaration: + return parent.id === node; + default: return false; } @@ -197,19 +193,6 @@ export default createRule({ } } - function isInComputedProperty( - node: IdentifierLike, - ): node is IdentifierInComputedProperty { - return ( - node.parent.type === AST_NODE_TYPES.MemberExpression && - node.parent.computed && - node === node.parent.property && - (node.type === AST_NODE_TYPES.Literal || - node.type === AST_NODE_TYPES.TemplateLiteral || - node.type === AST_NODE_TYPES.Identifier) - ); - } - function getJsDocDeprecation( symbol: ts.Signature | ts.Symbol | undefined, ): string | undefined { @@ -231,48 +214,13 @@ export default createRule({ return displayParts ? ts.displayPartsToString(displayParts) : ''; } - function getComputedPropertyDeprecation(node: TSESTree.MemberExpression) { - const objectType = services.getTypeAtLocation(node.object); - const propertyName = getPropertyNameFromScopeOrTypes(node); - if (!propertyName) { - return undefined; - } - - const property = objectType.getProperty(propertyName); - return getJsDocDeprecation(property); - } - - /** - * Gets the static name of a MemberExpression's .property. This first attempts - * a quick lookup from the ESTree scope, then falls back as needed to slower - * type information for non-scoped information such as enum keys as needed. - */ - function getPropertyNameFromScopeOrTypes(node: TSESTree.MemberExpression) { - // console.log({ node: context.sourceCode.getText(node) }); - const fromScope = getPropertyName( - node, - context.sourceCode.getScope(node), - ); - if (fromScope) { - return fromScope; - } - - const type = services.getTypeAtLocation(node.property); + type CallLikeNode = + | TSESTree.CallExpression + | TSESTree.JSXOpeningElement + | TSESTree.NewExpression + | TSESTree.TaggedTemplateExpression; - if (type.isNumberLiteral()) { - return String(type.value); - } - - if (type.isStringLiteral()) { - return type.value; - } - - return undefined; - } - - type CalleeNode = TSESTree.Expression | TSESTree.JSXTagNameExpression; - - function isNodeCalleeOfParent(node: TSESTree.Node): node is CalleeNode { + function isNodeCalleeOfParent(node: TSESTree.Node): node is CallLikeNode { switch (node.parent?.type) { case AST_NODE_TYPES.NewExpression: case AST_NODE_TYPES.CallExpression: @@ -289,7 +237,7 @@ export default createRule({ } } - function getCalleeNode(node: TSESTree.Node): CalleeNode | undefined { + function getCallLikeNode(node: TSESTree.Node): CallLikeNode | undefined { let callee = node; while ( @@ -302,7 +250,7 @@ export default createRule({ return isNodeCalleeOfParent(callee) ? callee : undefined; } - function getCallLikeDeprecation(node: CalleeNode): string | undefined { + function getCallLikeDeprecation(node: CallLikeNode): string | undefined { const tsNode = services.esTreeNodeToTSNodeMap.get(node.parent); // If the node is a direct function call, we look for its signature. @@ -383,37 +331,8 @@ export default createRule({ return getJsDocDeprecation(symbol); } - function getReportedNodeName(node: IdentifierLike): string { - if (node.type === AST_NODE_TYPES.Super) { - return 'super'; - } - - if (node.type === AST_NODE_TYPES.PrivateIdentifier) { - return `#${node.name}`; - } - - if ( - node.type === AST_NODE_TYPES.TemplateLiteral || - isInComputedProperty(node) - ) { - return ( - getPropertyNameFromScopeOrTypes( - node.parent as TSESTree.MemberExpression, - ) || '' - ); - } - - return node.name; - } - - function getDeprecationReason(node: IdentifierLike) { - if (isInComputedProperty(node)) { - return getComputedPropertyDeprecation( - node.parent as TSESTree.MemberExpression, - ); - } - - const callLikeNode = getCalleeNode(node); + function getDeprecationReason(node: IdentifierLike): string | undefined { + const callLikeNode = getCallLikeNode(node); if (callLikeNode) { return getCallLikeDeprecation(callLikeNode); } @@ -456,24 +375,11 @@ export default createRule({ } const reason = getDeprecationReason(node); - if (reason == null) { return; } const type = services.getTypeAtLocation(node); - - // TypeScript doesn't allow non-literal keys for computed properties, - // even though in cases like String('...') we can still find a deprecation. - if ( - node.parent.type === AST_NODE_TYPES.MemberExpression && - node.parent.computed && - node.parent.property === node && - !type.isLiteral() - ) { - return; - } - if ( typeMatchesSomeSpecifier(type, allow, services.program) || valueMatchesSomeSpecifier(node, allow, services.program, type) @@ -497,6 +403,46 @@ export default createRule({ }); } + function checkMemberExpression(node: TSESTree.MemberExpression): void { + if (!node.computed) { + return; + } + + const propertyType = services.getTypeAtLocation(node.property); + + if (propertyType.isLiteral()) { + const objectType = services.getTypeAtLocation(node.object); + + const propertyName = propertyType.isStringLiteral() + ? propertyType.value + : String(propertyType.value as number); + + const property = objectType.getProperty(propertyName); + + const reason = getJsDocDeprecation(property); + if (reason == null) { + return; + } + + if (typeMatchesSomeSpecifier(objectType, allow, services.program)) { + return; + } + + context.report({ + ...(reason + ? { + messageId: 'deprecatedWithReason', + data: { name: propertyName, reason }, + } + : { + messageId: 'deprecated', + data: { name: propertyName }, + }), + node: node.property, + }); + } + } + return { Identifier(node): void { const { parent } = node; @@ -508,6 +454,15 @@ export default createRule({ return; } + // Computed identifier expressions are handled by checkMemberExpression + if ( + parent.type === AST_NODE_TYPES.MemberExpression && + parent.computed && + parent.property === node + ) { + return; + } + if (parent.type === AST_NODE_TYPES.ExportSpecifier) { // only deal with the alias (exported) side, not the local binding if (parent.exported !== node) { @@ -530,10 +485,21 @@ export default createRule({ checkIdentifier(node); } }, - 'MemberExpression > Literal': checkIdentifier, - 'MemberExpression > TemplateLiteral': checkIdentifier, + MemberExpression: checkMemberExpression, PrivateIdentifier: checkIdentifier, Super: checkIdentifier, }; }, }); + +function getReportedNodeName(node: IdentifierLike): string { + if (node.type === AST_NODE_TYPES.Super) { + return 'super'; + } + + if (node.type === AST_NODE_TYPES.PrivateIdentifier) { + return `#${node.name}`; + } + + return node.name; +} diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index c83cf47997d7..81ad5c7516c9 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -1476,6 +1476,7 @@ exists('/foo'); ], }, { + // only: true, code: ` declare class A { /** @deprecated */ @@ -3923,6 +3924,50 @@ exists('/foo'); }, ], }, + { + code: ` + declare function x(): 'b'; + + const a = { + /** @deprecated */ + b: 'string', + }; + + const c = a[x()]; + `, + errors: [ + { + column: 21, + data: { name: 'b' }, + endColumn: 24, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare const x: { y: 'b' }; + + const a = { + /** @deprecated */ + b: 'string', + }; + + const c = a[x.y]; + `, + errors: [ + { + column: 21, + data: { name: 'b' }, + endColumn: 24, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, { code: ` import { deprecatedFunction } from './deprecated'; From f710059a9521dce023e16ef43c451e0662eae826 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Mon, 3 Nov 2025 14:33:09 -0500 Subject: [PATCH 12/12] Remove change noise --- packages/eslint-plugin/src/rules/no-deprecated.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index 2bf1d5d7e2c4..91b5755b8d3c 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -157,10 +157,6 @@ export default createRule({ case AST_NODE_TYPES.TSTypeParameter: return true; - // treat `export import Bar = Foo;` (and `import Foo = require('...')`) as declarations - case AST_NODE_TYPES.TSImportEqualsDeclaration: - return parent.id === node; - default: return false; } @@ -375,6 +371,7 @@ export default createRule({ } const reason = getDeprecationReason(node); + if (reason == null) { return; }