diff --git a/eslint.config.mjs b/eslint.config.mjs index b4d50a9435af..966d32aa8418 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -95,7 +95,7 @@ export default tseslint.config( }, rules: { // make sure we're not leveraging any deprecated APIs - 'deprecation/deprecation': 'error', + '@typescript-eslint/deprecation': 'error', // TODO: https://github.com/typescript-eslint/typescript-eslint/issues/8538 '@typescript-eslint/no-confusing-void-expression': 'off', diff --git a/packages/eslint-plugin/docs/rules/deprecation.mdx b/packages/eslint-plugin/docs/rules/deprecation.mdx new file mode 100644 index 000000000000..4e4633147800 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/deprecation.mdx @@ -0,0 +1,33 @@ +--- +description: 'Prevent usage of deprecated members' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/deprecation** for documentation. + +This rule supersedes the `deprecation/deprecation` rule from `eslint-plugin-deprecation` + + + + +```ts +escape('Hello'); // The signature '(string: string): string' of 'escape' is deprecated: A legacy feature for browser compatibility +unescape('Hello'); // The signature '(string: string): string' of 'unescape' is deprecated: A legacy feature for browser compatibility +RegExp.lastMatch; // 'lastMatch' is deprecated: A legacy feature for browser compatibility + +/** + * @deprecated for some reason + */ +declare const someValue: string; + +console.log(someValue); // 'someValue' is deprecated: for some reason + +new Buffer(38); // 'Buffer' is deprecated. since v10.0.0 - Use `Buffer.alloc()` instead (also see `Buffer.allocUnsafe()`). +``` + + + diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 5c1e1d7725ac..bceea0ba26f5 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -29,6 +29,7 @@ export = { '@typescript-eslint/consistent-type-imports': 'error', 'default-param-last': 'off', '@typescript-eslint/default-param-last': 'error', + '@typescript-eslint/deprecation': 'error', 'dot-notation': 'off', '@typescript-eslint/dot-notation': 'error', '@typescript-eslint/explicit-function-return-type': 'error', diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index 9a45d83452cf..45a91be9a216 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -13,6 +13,7 @@ export = { '@typescript-eslint/await-thenable': 'off', '@typescript-eslint/consistent-return': 'off', '@typescript-eslint/consistent-type-exports': 'off', + '@typescript-eslint/deprecation': 'off', '@typescript-eslint/dot-notation': 'off', '@typescript-eslint/naming-convention': 'off', '@typescript-eslint/no-array-delete': 'off', diff --git a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts index 53f13d96748f..eaa8296a0160 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts @@ -11,6 +11,7 @@ export = { extends: ['./configs/base', './configs/eslint-recommended'], rules: { '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/deprecation': 'error', '@typescript-eslint/no-array-delete': 'error', '@typescript-eslint/no-base-to-string': 'error', '@typescript-eslint/no-confusing-void-expression': 'error', diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts index 7987868204db..5a3eba335729 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts @@ -16,6 +16,7 @@ export = { { minimumDescriptionLength: 10 }, ], '@typescript-eslint/ban-types': 'error', + '@typescript-eslint/deprecation': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-array-delete': 'error', diff --git a/packages/eslint-plugin/src/rules/deprecation.ts b/packages/eslint-plugin/src/rules/deprecation.ts new file mode 100644 index 000000000000..f08983b4ab55 --- /dev/null +++ b/packages/eslint-plugin/src/rules/deprecation.ts @@ -0,0 +1,298 @@ +import type { + ParserServicesWithTypeInformation, + TSESTree, +} from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; +import type { + EntityName, + JSDocComment, + JSDocMemberName, + NodeArray, + Symbol as TSSymbol, + TypeChecker, +} from 'typescript'; +import { + getAllJSDocTags, + isIdentifier, + isJSDocDeprecatedTag, + isJSDocLinkLike, + isJSDocMemberName, + isQualifiedName, + isShorthandPropertyAssignment, + TypeFormatFlags, +} from 'typescript'; + +import { createRule, getParserServices } from '../util'; + +type Options = []; +type MessageIds = + | 'deprecated' + | 'deprecatedWithReason' + | 'deprecatedSignature' + | 'deprecatedSignatureWithReason'; + +function shouldIgnoreIdentifier(node: TSESTree.Identifier): boolean { + switch (node.parent.type) { + case AST_NODE_TYPES.FunctionDeclaration: + case AST_NODE_TYPES.TSDeclareFunction: + case AST_NODE_TYPES.ClassDeclaration: + case AST_NODE_TYPES.TSInterfaceDeclaration: + case AST_NODE_TYPES.TSTypeAliasDeclaration: + case AST_NODE_TYPES.Property: + return true; + case AST_NODE_TYPES.VariableDeclarator: + return node.parent.init !== node; + case AST_NODE_TYPES.TSPropertySignature: + case AST_NODE_TYPES.PropertyDefinition: + return node.parent.key === node; + } + return false; +} + +function formatEntityName(name: EntityName | JSDocMemberName): string { + let current = ''; + let currentName: EntityName | JSDocMemberName | undefined = name; + + while (currentName) { + if (isQualifiedName(currentName) || isJSDocMemberName(currentName)) { + if (current === '') { + current = currentName.right.text; + } else { + current = `${currentName.right.text}#${current}`; + } + currentName = currentName.left; + continue; + } + if (isIdentifier(currentName)) { + if (current === '') { + return currentName.text; + } + current = `${currentName.text}#${current}`; + currentName = undefined; + continue; + } + break; + } + // + return current; +} + +function formatComments(comment: string | NodeArray): string { + if (typeof comment === 'string') { + return comment; + } + + // TODO: Implement a detection algorithm to detect "Use X instead", resolve types and give a different error message + /* + const links = comment.filter( + isJSDocLinkLike, + ); + if (links.length === 1) { + const link = links[0]; + + if (link.name !== undefined) { + return `Use '${formatEntityName(link.name)}' instead.`; + } + } + */ + + return comment + .map(single => { + if (isJSDocLinkLike(single)) { + if (single.name) { + return formatEntityName(single.name); + } + return single.text; + } + return single.text; + }) + .join(''); +} + +function handleMaybeDeprecatedSymbol( + ctx: Readonly>, + services: ParserServicesWithTypeInformation, + checker: TypeChecker, + node: TSESTree.Node, + sym: TSSymbol, + name: string, +): void { + if ( + node.type === AST_NODE_TYPES.Identifier && + (node.parent.type === AST_NODE_TYPES.CallExpression || + node.parent.type === AST_NODE_TYPES.NewExpression) && + node.parent.callee === node + ) { + /* + Function call + We should in this case check the resolved signature instead + */ + + const tsParent = services.esTreeNodeToTSNodeMap.get(node.parent); + const sig = checker.getResolvedSignature(tsParent); + if (sig === undefined) { + return; + } + const decl = sig.getDeclaration(); + if ((decl as undefined | typeof decl) === undefined) { + // May happen if we have an implicit constructor on a class + return; + } + + for (const tag of getAllJSDocTags(decl, isJSDocDeprecatedTag)) { + if (tag.comment) { + ctx.report({ + messageId: 'deprecatedSignatureWithReason', + node, + data: { + name, + signature: checker.signatureToString( + sig, + tsParent, + TypeFormatFlags.WriteTypeArgumentsOfSignature, + ), + reason: formatComments(tag.comment), + }, + }); + return; + } + ctx.report({ + messageId: 'deprecatedSignature', + node, + data: { + name, + signature: checker.signatureToString( + sig, + tsParent, + TypeFormatFlags.WriteTypeArgumentsOfSignature, + ), + }, + }); + } + return; + } + + for (const decl of sym.getDeclarations() ?? []) { + for (const tag of getAllJSDocTags(decl, isJSDocDeprecatedTag)) { + if (tag.comment) { + ctx.report({ + messageId: 'deprecatedWithReason', + node, + data: { + name, + reason: formatComments(tag.comment), + }, + }); + return; + } + ctx.report({ + messageId: 'deprecated', + node, + data: { + name, + }, + }); + } + } +} + +export default createRule({ + name: 'deprecation', + meta: { + docs: { + description: 'Disallow usage of deprecated APIs', + requiresTypeChecking: true, + recommended: 'strict', + }, + messages: { + deprecated: `'{{name}}' is deprecated.`, + deprecatedWithReason: `'{{name}}' is deprecated: {{reason}}`, + deprecatedSignature: `The signature '{{signature}}' of '{{name}}' is deprecated.`, + deprecatedSignatureWithReason: `The signature '{{signature}}' of '{{name}}' is deprecated: {{reason}}`, + }, + schema: [], + type: 'problem', + }, + defaultOptions: [], + create(ctx) { + const services = getParserServices(ctx); + const checker = services.program.getTypeChecker(); + + return { + // TODO: Support a[b] syntax + Property(node): void { + const par = services.esTreeNodeToTSNodeMap.get(node); + + if (node.key.type !== AST_NODE_TYPES.Identifier) { + return; + } + + if (isShorthandPropertyAssignment(par)) { + const sym = checker.getTypeAtLocation(par.name).getSymbol(); + if (sym === undefined) { + return; + } + + handleMaybeDeprecatedSymbol( + ctx, + services, + checker, + node, + sym, + node.key.name, + ); + } + return; + }, + Identifier(node): void { + if (shouldIgnoreIdentifier(node)) { + return; + } + + const sym = services.getSymbolAtLocation(node); + if (sym === undefined) { + // Types unavailable + return; + } + + try { + handleMaybeDeprecatedSymbol( + ctx, + services, + checker, + node, + sym, + node.name, + ); + } catch { + return; + } + }, + MemberExpression(node): void { + if (node.property.type === AST_NODE_TYPES.PrivateIdentifier) { + const identifier = node.property; + + const sym = services.getSymbolAtLocation(identifier); + if (sym === undefined) { + // Types unavailable + return; + } + + try { + handleMaybeDeprecatedSymbol( + ctx, + services, + checker, + identifier, + sym, + `#${identifier.name}`, + ); + } catch { + return; + } + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index c167013211c0..0faf30ce7b44 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -16,6 +16,7 @@ import consistentTypeDefinitions from './consistent-type-definitions'; import consistentTypeExports from './consistent-type-exports'; import consistentTypeImports from './consistent-type-imports'; import defaultParamLast from './default-param-last'; +import deprecation from './deprecation'; import dotNotation from './dot-notation'; import explicitFunctionReturnType from './explicit-function-return-type'; import explicitMemberAccessibility from './explicit-member-accessibility'; @@ -140,6 +141,7 @@ export default { 'consistent-type-exports': consistentTypeExports, 'consistent-type-imports': consistentTypeImports, 'default-param-last': defaultParamLast, + deprecation: deprecation, 'dot-notation': dotNotation, 'explicit-function-return-type': explicitFunctionReturnType, 'explicit-member-accessibility': explicitMemberAccessibility, diff --git a/packages/eslint-plugin/tests/rules/deprecation.test.ts b/packages/eslint-plugin/tests/rules/deprecation.test.ts new file mode 100644 index 000000000000..d6904f17b2d9 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/deprecation.test.ts @@ -0,0 +1,318 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/deprecation'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('deprecation', rule, { + valid: [ + ` +declare const b: string; +if (false as boolean) { + /** + * @deprecated + */ + const b = ''; +} + +const a = b; + `.trim(), + ` +const a = 'a'; + `.trim(), + ` +/** + * @deprecated + */ +const a = 'a'; + `.trim(), + ` +/** + * @deprecated + */ +class A {} + `.trim(), + ` +/** + * @deprecated + */ +interface A {} + `.trim(), + ` +/** + * @deprecated + */ +declare class A {} + `.trim(), + ` +class A { + /** + * @deprecated + */ + b: string; +} + `.trim(), + ` +declare class A { + /** + * @deprecated + */ + b: string; +} + `.trim(), + ` +interface A { + /** + * @deprecated + */ + b: string; +} + `.trim(), + ` +class A { + /** + * @deprecated + */ + b: string; +} + +class B extends A { + b: string; +} + `.trim(), + ` +/** @deprecated */ +declare function a(val: string): string; +declare function a(val: number): number; + `.trim(), + ` +/** @deprecated */ +declare function a(val: string): string; +declare function a(val: number): number; + +a(2); + `.trim(), + ` +/** @deprecated */ +declare function a(val: K): K; +declare function a(val: number): number; +declare function a(val: boolean): boolean; + +a(2); + `.trim(), + ], + invalid: [ + { + code: ` +/** + * @deprecated EXAMPLE + */ +const a = 'a'; + +console.log(a); + `.trim(), + errors: [ + { + messageId: 'deprecatedWithReason', + data: { + name: 'a', + reason: 'EXAMPLE', + }, + line: 6, + }, + ], + }, + { + code: ` +const a = { + /** + * @deprecated + */ + b: 'Hi!', +}; + +const c = a.b; + `.trim(), + errors: [ + { + messageId: 'deprecated', + line: 8, + data: { + name: 'b', + }, + }, + ], + }, + { + code: ` +/** + * @deprecated + */ +const a = { + b: 'Hi!', +}; + +const b = { + a, +}; + `.trim(), + errors: [ + { + messageId: 'deprecated', + data: { + name: 'a', + line: 9, + }, + }, + ], + }, + { + code: ` +const a = { + /** + * @deprecated + */ + b: 'Hi!', +}; + +function c(d: string = a.b) {} + `.trim(), + errors: [ + { + messageId: 'deprecated', + data: { + name: 'b', + }, + line: 8, + }, + ], + }, + { + code: ` +/** + * @deprecated + */ +type C = string; + +class A {} + `.trim(), + errors: [ + { + line: 6, + messageId: 'deprecated', + data: { + name: 'C', + }, + }, + ], + }, + { + code: ` +class A { + /** + * @deprecated + */ + #b: string; + + constructor() { + this.#b = 'Hi!'; + } +} + `.trim(), + errors: [ + { + line: 8, + messageId: 'deprecated', + data: { + name: '#b', + }, + }, + ], + }, + { + code: ` +declare namespace a { + /** + * @deprecated + */ + const a: string; +} +declare namespace a { + const a: string; +} + +const b = a.a; + `, + errors: [ + { + messageId: 'deprecated', + line: 12, + data: { + name: 'a', + }, + }, + ], + }, + { + code: ` +/** @deprecated */ +declare function a(val: K): K; +declare function a(val: number): number; +declare function a(val: boolean): boolean; + +a('B'); + `.trim(), + errors: [ + { + messageId: 'deprecatedSignature', + line: 6, + data: { + signature: `<"B">(val: "B"): "B"`, + name: 'a', + }, + }, + ], + }, + { + code: ` +class A { + /** @deprecated */ + constructor(value: string) {} +} + +new A('VALUE'); + `, + errors: [ + { + messageId: 'deprecatedSignature', + }, + ], + }, + { + code: ` +declare interface A { + /** @deprecated */ + new (value: string): A; +} +declare const A: A; + +new A('VALUE'); + `, + errors: [ + { + messageId: 'deprecatedSignature', + }, + ], + }, + ], +}); diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts index f01ac17c8ddb..19dad3f81543 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -38,6 +38,7 @@ export default ( '@typescript-eslint/consistent-type-imports': 'error', 'default-param-last': 'off', '@typescript-eslint/default-param-last': 'error', + '@typescript-eslint/deprecation': 'error', 'dot-notation': 'off', '@typescript-eslint/dot-notation': 'error', '@typescript-eslint/explicit-function-return-type': 'error', diff --git a/packages/typescript-eslint/src/configs/disable-type-checked.ts b/packages/typescript-eslint/src/configs/disable-type-checked.ts index 9df504415e37..43f946a69afc 100644 --- a/packages/typescript-eslint/src/configs/disable-type-checked.ts +++ b/packages/typescript-eslint/src/configs/disable-type-checked.ts @@ -16,6 +16,7 @@ export default ( '@typescript-eslint/await-thenable': 'off', '@typescript-eslint/consistent-return': 'off', '@typescript-eslint/consistent-type-exports': 'off', + '@typescript-eslint/deprecation': 'off', '@typescript-eslint/dot-notation': 'off', '@typescript-eslint/naming-convention': 'off', '@typescript-eslint/no-array-delete': 'off', diff --git a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts index 415dd3eb342b..1bd10ccfa719 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts @@ -20,6 +20,7 @@ export default ( name: 'typescript-eslint/strict-type-checked-only', rules: { '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/deprecation': 'error', '@typescript-eslint/no-array-delete': 'error', '@typescript-eslint/no-base-to-string': 'error', '@typescript-eslint/no-confusing-void-expression': 'error', diff --git a/packages/typescript-eslint/src/configs/strict-type-checked.ts b/packages/typescript-eslint/src/configs/strict-type-checked.ts index 61d0a4d579a2..acee87d8c307 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked.ts @@ -25,6 +25,7 @@ export default ( { minimumDescriptionLength: 10 }, ], '@typescript-eslint/ban-types': 'error', + '@typescript-eslint/deprecation': 'error', 'no-array-constructor': 'off', '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-array-delete': 'error', pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy