diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index fb426747edd..8a4481cafd0 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -1,6 +1,8 @@ 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'; @@ -10,15 +12,22 @@ import { createRule, getParserServices, nullThrows, - typeOrValueSpecifiersSchema, typeMatchesSomeSpecifier, + typeOrValueSpecifiersSchema, } from '../util'; 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'; @@ -188,6 +197,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 { @@ -209,13 +231,25 @@ export default createRule({ return displayParts ? ts.displayPartsToString(displayParts) : ''; } - type CallLikeNode = - | TSESTree.CallExpression - | TSESTree.JSXOpeningElement - | TSESTree.NewExpression - | TSESTree.TaggedTemplateExpression; + 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 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 +266,7 @@ export default createRule({ } } - function getCallLikeNode(node: TSESTree.Node): CallLikeNode | undefined { + function getCalleeNode(node: TSESTree.Node): CalleeNode | undefined { let callee = node; while ( @@ -245,7 +279,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. @@ -326,8 +360,45 @@ export default createRule({ return getJsDocDeprecation(symbol); } + 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 || + isInComputedProperty(node) + ) { + return ( + getPropertyName( + node.parent as TSESTree.MemberExpression, + context.sourceCode.getScope(node), + ) || '' + ); + } + + return node.name; + } + function getDeprecationReason(node: IdentifierLike): string | undefined { - const callLikeNode = getCallLikeNode(node); + if (isInComputedProperty(node)) { + return getComputedPropertyDeprecation( + node.parent as TSESTree.MemberExpression, + ); + } + + const callLikeNode = getCalleeNode(node); if (callLikeNode) { return getCallLikeDeprecation(callLikeNode); } @@ -379,7 +450,7 @@ export default createRule({ return; } - const name = getReportedNodeName(node); + const name = getReportedNodeName(node, context); context.report({ ...(reason @@ -402,20 +473,10 @@ export default createRule({ checkIdentifier(node); } }, + 'MemberExpression > Literal': checkIdentifier, + 'MemberExpression > TemplateLiteral': checkIdentifier, 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 43ea34aa547..6824c970978 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -45,8 +45,42 @@ 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, + }; + + const key = 'b'; + + a[key]; + `, + ` + 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 +89,52 @@ 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; + }; + + 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; @@ -63,6 +143,41 @@ ruleTester.run('no-deprecated', rule, { new A().b; `, + ` + class A { + b: 1; + /** @deprecated */ c: 2; + } + + new A()['b']; + `, + ` + class A { + b: 1; + /** @deprecated */ c: 2; + } + const key = 'b'; + + 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 +186,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 +203,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 +221,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 +239,15 @@ ruleTester.run('no-deprecated', rule, { A.c; `, + ` + namespace A { + /** @deprecated */ + export const b = ''; + export const c = ''; + } + + A['c']; + `, ` enum A { /** @deprecated */ @@ -107,6 +257,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 +768,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 +800,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 +864,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 +898,43 @@ exists('/foo'); }, ], }, + { + code: ` + const a = { + /** @deprecated */ b: 1, + }; + const key = 'b'; + a[key]; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 14, + endLine: 6, + line: 6, + messageId: 'deprecated', + }, + ], + }, + { + 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: { @@ -925,90 +1169,314 @@ exists('/foo'); column: 13, data: { name: 'A' }, endColumn: 14, - endLine: 7, - line: 7, + endLine: 7, + line: 7, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + const A = class { + /** @deprecated */ + constructor(); + constructor(arg: string); + constructor(arg?: string) {} + }; + + new A(); + `, + errors: [ + { + column: 13, + data: { name: 'A' }, + endColumn: 14, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare const A: { + /** @deprecated */ + new (): string; + }; + + new A(); + `, + errors: [ + { + column: 13, + data: { name: 'A' }, + endColumn: 14, + endLine: 7, + line: 7, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + /** @deprecated */ + declare class A { + constructor(); + } + + new A(); + `, + errors: [ + { + column: 13, + data: { name: 'A' }, + endColumn: 14, + endLine: 7, + line: 7, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + class A { + /** @deprecated */ + b: string; + } + + declare const a: A; + + const { b } = a; + `, + errors: [ + { + column: 17, + data: { name: 'b' }, + endColumn: 18, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare class A { + /** @deprecated */ + b(): string; + } + + declare const a: A; + + a.b; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 12, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare class A { + /** @deprecated */ + b(): string; + } + + declare const a: A; + const key = 'b'; + + a[key]; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + 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: 'b' }, + endColumn: 14, + endLine: 10, + line: 10, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare class A { + /** @deprecated */ + b(): string; + } + + declare const a: A; + + a.b(); + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 12, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + declare class A { + /** @deprecated */ + b: () => string; + } + + declare const a: A; + + a.b; + `, + errors: [ + { + column: 11, + data: { name: 'b' }, + endColumn: 12, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, + { + 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: ` - const A = class { + declare class A { /** @deprecated */ - constructor(); - constructor(arg: string); - constructor(arg?: string) {} - }; + b(): string; + } - new A(); + declare const a: A; + const key = 'b'; + + a[\`\${key}\`]; `, errors: [ { - column: 13, - data: { name: 'A' }, - endColumn: 14, - endLine: 9, - line: 9, + column: 11, + data: { name: 'b' }, + endColumn: 19, + endLine: 10, + line: 10, messageId: 'deprecated', }, ], }, { code: ` - declare const A: { + declare class A { /** @deprecated */ - new (): string; - }; + computed(): string; + } - new A(); + declare const a: A; + const k1 = 'comp'; + const k2 = 'uted'; + + a[\`\${k1}\${k2}\`]; `, errors: [ { - column: 13, - data: { name: 'A' }, - endColumn: 14, - endLine: 7, - line: 7, + column: 11, + data: { name: 'computed' }, + endColumn: 23, + endLine: 11, + line: 11, messageId: 'deprecated', }, ], }, { code: ` - /** @deprecated */ declare class A { - constructor(); + /** @deprecated */ + b(): string; } - new A(); + declare const a: A; + const c = \`\${a.b}\`; `, errors: [ { - column: 13, - data: { name: 'A' }, - endColumn: 14, - endLine: 7, - line: 7, + column: 24, + data: { name: 'b' }, + endColumn: 25, + endLine: 8, + line: 8, messageId: 'deprecated', }, ], }, { code: ` - class A { + declare class A { /** @deprecated */ - b: string; + b: () => string; } declare const a: A; - const { b } = a; + a.b(); `, errors: [ { - column: 17, + column: 11, data: { name: 'b' }, - endColumn: 18, + endColumn: 12, endLine: 9, line: 9, messageId: 'deprecated', @@ -1019,18 +1487,18 @@ exists('/foo'); code: ` declare class A { /** @deprecated */ - b(): string; + b: () => string; } declare const a: A; - a.b; + a['b'](); `, errors: [ { column: 11, data: { name: 'b' }, - endColumn: 12, + endColumn: 14, endLine: 9, line: 9, messageId: 'deprecated', @@ -1039,9 +1507,9 @@ exists('/foo'); }, { code: ` - declare class A { + interface A { /** @deprecated */ - b(): string; + b: () => string; } declare const a: A; @@ -1061,44 +1529,46 @@ exists('/foo'); }, { code: ` - declare class A { + interface A { /** @deprecated */ b: () => string; } declare const a: A; + const key = 'b'; - a.b; + a[key]; `, errors: [ { column: 11, data: { name: 'b' }, - endColumn: 12, - endLine: 9, - line: 9, + endColumn: 14, + endLine: 10, + line: 10, messageId: 'deprecated', }, ], }, { code: ` - declare class A { + interface A { /** @deprecated */ b: () => string; } declare const a: A; + const key = 'b'; - a.b(); + a[key](); `, errors: [ { column: 11, data: { name: 'b' }, - endColumn: 12, - endLine: 9, - line: 9, + endColumn: 14, + endLine: 10, + line: 10, messageId: 'deprecated', }, ], @@ -1112,13 +1582,13 @@ exists('/foo'); declare const a: A; - a.b(); + a['b'](); `, errors: [ { column: 11, data: { name: 'b' }, - endColumn: 12, + endColumn: 14, endLine: 9, line: 9, messageId: 'deprecated', @@ -1192,6 +1662,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 +1702,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 +1894,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 +1954,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 +2060,48 @@ exists('/foo'); }, ], }, + { + code: ` + enum A { + /** @deprecated */ + a, + } + + const key = 'a'; + + A[key]; + `, + errors: [ + { + column: 11, + data: { name: 'a' }, + endColumn: 14, + endLine: 9, + line: 9, + messageId: 'deprecated', + }, + ], + }, + { + code: ` + enum A { + /** @deprecated */ + a, + } + + A['a']; + `, + errors: [ + { + column: 11, + data: { name: 'a' }, + endColumn: 14, + endLine: 7, + line: 7, + messageId: 'deprecated', + }, + ], + }, { code: ` /** @deprecated */ @@ -2833,6 +3425,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: [ @@ -2846,6 +3460,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 {