Skip to content

feat(eslint-plugin): add new rule: @typescript-eslint/deprecation #9346

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
33 changes: 33 additions & 0 deletions packages/eslint-plugin/docs/rules/deprecation.mdx
Original file line number Diff line number Diff line change
@@ -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`

<Tabs>
<TabItem value="❌ Incorrect">

```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()`).
```

</TabItem>
</Tabs>
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/disable-type-checked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/strict-type-checked.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
298 changes: 298 additions & 0 deletions packages/eslint-plugin/src/rules/deprecation.ts
Original file line number Diff line number Diff line change
@@ -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<JSDocComment>): 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<JSDocLink | JSDocLinkCode | JSDocLinkPlain>(
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<RuleContext<MessageIds, Options>>,
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<Options, MessageIds>({
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;
}
}
},
};
},
});
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading