diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index 725ddaf3fce9..880708a866c3 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -12,6 +12,7 @@ import { nullThrows, typeOrValueSpecifiersSchema, typeMatchesSomeSpecifier, + valueMatchesSomeSpecifier, } from '../util'; type IdentifierLike = @@ -375,7 +376,10 @@ export default createRule({ } const type = services.getTypeAtLocation(node); - if (typeMatchesSomeSpecifier(type, allow, services.program)) { + if ( + typeMatchesSomeSpecifier(type, allow, services.program) || + valueMatchesSomeSpecifier(node, allow, services.program, type) + ) { return; } diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index 1c8f354886df..6231f9404944 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -332,6 +332,21 @@ ruleTester.run('no-deprecated', rule, { { code: ` /** @deprecated */ +function A() { + return
; +} + +const a = ; + `, + options: [ + { + allow: [{ from: 'file', name: 'A' }], + }, + ], + }, + { + code: ` +/** @deprecated */ declare class A {} new A(); @@ -344,7 +359,62 @@ new A(); }, { code: ` +/** @deprecated */ +const deprecatedValue = 45; +const bar = deprecatedValue; + `, + options: [ + { + allow: [{ from: 'file', name: 'deprecatedValue' }], + }, + ], + }, + { + code: ` +class MyClass { + /** @deprecated */ + #privateProp = 42; + value = this.#privateProp; +} + `, + options: [ + { + allow: [{ from: 'file', name: 'privateProp' }], + }, + ], + }, + { + code: ` +/** @deprecated */ +const deprecatedValue = 45; +const bar = deprecatedValue; + `, + options: [ + { + allow: ['deprecatedValue'], + }, + ], + }, + { + code: ` import { exists } from 'fs'; +exists('/foo'); + `, + options: [ + { + allow: [ + { + from: 'package', + name: 'exists', + package: 'fs', + }, + ], + }, + ], + }, + { + code: ` +const { exists } = import('fs'); exists('/foo'); `, options: [ @@ -2915,6 +2985,37 @@ class B extends A { }, ], }, + { + code: ` +import { exists } from 'fs'; +exists('/foo'); + `, + errors: [ + { + column: 1, + data: { + name: 'exists', + reason: + 'Since v1.0.0 - Use {@link stat} or {@link access} instead.', + }, + endColumn: 7, + endLine: 3, + line: 3, + messageId: 'deprecatedWithReason', + }, + ], + options: [ + { + allow: [ + { + from: 'package', + name: 'exists', + package: 'hoge', + }, + ], + }, + ], + }, { code: ` declare class A { diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 4c8f1211a8be..bbfb99ddcc78 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -1,6 +1,8 @@ +import type { TSESTree } from '@typescript-eslint/types'; import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; import type * as ts from 'typescript'; +import { AST_NODE_TYPES } from '@typescript-eslint/types'; import * as tsutils from 'ts-api-utils'; import { specifierNameMatches } from './typeOrValueSpecifiers/specifierNameMatches'; @@ -219,3 +221,69 @@ export const typeMatchesSomeSpecifier = ( program: ts.Program, ): boolean => specifiers.some(specifier => typeMatchesSpecifier(type, specifier, program)); + +const getSpecifierNames = (specifierName: string | string[]): string[] => { + return typeof specifierName === 'string' ? [specifierName] : specifierName; +}; + +const getStaticName = (node: TSESTree.Node): string | undefined => { + if ( + node.type === AST_NODE_TYPES.Identifier || + node.type === AST_NODE_TYPES.JSXIdentifier || + node.type === AST_NODE_TYPES.PrivateIdentifier + ) { + return node.name; + } + + if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') { + return node.value; + } + + return undefined; +}; + +export function valueMatchesSpecifier( + node: TSESTree.Node, + specifier: TypeOrValueSpecifier, + program: ts.Program, + type: ts.Type, +): boolean { + const staticName = getStaticName(node); + if (!staticName) { + return false; + } + + if (typeof specifier === 'string') { + return specifier === staticName; + } + + if (!getSpecifierNames(specifier.name).includes(staticName)) { + return false; + } + + if (specifier.from === 'package') { + const symbol = type.getSymbol() ?? type.aliasSymbol; + const declarations = symbol?.getDeclarations() ?? []; + const declarationFiles = declarations.map(declaration => + declaration.getSourceFile(), + ); + return typeDeclaredInPackageDeclarationFile( + specifier.package, + declarations, + declarationFiles, + program, + ); + } + + return true; +} + +export const valueMatchesSomeSpecifier = ( + node: TSESTree.Node, + specifiers: TypeOrValueSpecifier[] = [], + program: ts.Program, + type: ts.Type, +): boolean => + specifiers.some(specifier => + valueMatchesSpecifier(node, specifier, program, type), + ); diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 8aee949efb37..ed9c5614b209 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -1,8 +1,17 @@ +import type { TSESTree } from '@typescript-eslint/utils'; + +import { parseForESLint } from '@typescript-eslint/parser'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as path from 'node:path'; import type { TypeOrValueSpecifier } from '../src/index.js'; -import { typeMatchesSpecifier } from '../src/index.js'; +import { + typeMatchesSomeSpecifier, + typeMatchesSpecifier, + valueMatchesSomeSpecifier, + valueMatchesSpecifier, +} from '../src/index.js'; const ROOT_DIR = path.posix.join( ...path.relative(process.cwd(), path.join(__dirname, '..')).split(path.sep), @@ -628,4 +637,313 @@ describe('TypeOrValueSpecifier', () => { }, ); }); + + describe(valueMatchesSpecifier, () => { + function parseCode(code: string) { + const rootDir = path.join(__dirname, 'fixtures'); + const { ast, services } = parseForESLint(code, { + disallowAutomaticSingleRunInference: true, + filePath: path.join(rootDir, 'file.ts'), + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }); + assert.isNotNull(services.program); + + return { ast, services }; + } + + describe(AST_NODE_TYPES.VariableDeclaration, () => { + function runTests( + code: string, + specifier: TypeOrValueSpecifier, + expected: boolean, + ) { + const { ast, services } = parseCode(code); + const declaration = ast.body.at(-1) as TSESTree.VariableDeclaration; + const { id, init } = declaration.declarations[0]; + const type = services.getTypeAtLocation(id); + expect( + valueMatchesSpecifier(init!, specifier, services.program, type), + ).toBe(expected); + } + + function runTestPositive( + code: string, + specifier: TypeOrValueSpecifier, + ): void { + runTests(code, specifier, true); + } + + function runTestNegative( + code: string, + specifier: TypeOrValueSpecifier, + ): void { + runTests(code, specifier, false); + } + + it.each<[string, TypeOrValueSpecifier]>([ + ['const value = 45;', 'value'], + ['let value = 45;', 'value'], + ['var value = 45;', 'value'], + ])( + 'does not match for non-Identifier or non-JSXIdentifier node: %s', + runTestNegative, + ); + + it.each<[string, TypeOrValueSpecifier]>([ + ['const value = 45; const hoge = value;', 'value'], + ['let value = 45; const hoge = value;', 'value'], + ['var value = 45; const hoge = value;', 'value'], + ])('matches a matching universal string specifier: %s', runTestPositive); + + it.each<[string, TypeOrValueSpecifier]>([ + ['const value = 45; const hoge = value;', 'incorrect'], + ])( + "doesn't match a mismatched universal string specifier: %s", + runTestNegative, + ); + + it.each<[string, TypeOrValueSpecifier]>([ + [ + 'const value = 45; const hoge = value;', + { from: 'file', name: 'value' }, + ], + [ + 'const value = 45; const hoge = value;', + { from: 'file', name: ['value', 'hoge'] }, + ], + [ + 'let value = 45; const hoge = value;', + { from: 'file', name: 'value' }, + ], + [ + 'var value = 45; const hoge = value;', + { from: 'file', name: 'value' }, + ], + ])('matches a matching file specifier: %s', runTestPositive); + + it.each<[string, TypeOrValueSpecifier]>([ + [ + 'const value = 45; const hoge = value;', + { from: 'file', name: 'incorrect' }, + ], + [ + 'const value = 45; const hoge = value;', + { from: 'file', name: ['incorrect', 'invalid'] }, + ], + ])("doesn't match a mismatched file specifier: %s", runTestNegative); + + it.each<[string, TypeOrValueSpecifier]>([ + ['const value = console', { from: 'lib', name: 'console' }], + ['const value = console', { from: 'lib', name: ['console', 'hoge'] }], + ['let value = console', { from: 'lib', name: 'console' }], + ['var value = console', { from: 'lib', name: 'console' }], + ])('matches a matching lib specifier: %s', runTestPositive); + + it.each<[string, TypeOrValueSpecifier]>([ + ['const value = console', { from: 'lib', name: 'incorrect' }], + [ + 'const value = console', + { from: 'lib', name: ['incorrect', 'window'] }, + ], + ])("doesn't match a mismatched lib specifier: %s", runTestNegative); + + it.each<[string, TypeOrValueSpecifier]>([ + [ + 'import { mock } from "node:test"; const hoge = mock;', + { from: 'package', name: 'mock', package: 'node:test' }, + ], + [ + 'import { mock } from "node:test"; const hoge = mock;', + { from: 'package', name: ['mock', 'hoge'], package: 'node:test' }, + ], + [ + `const fs: typeof import("fs"); const module = fs;`, + { from: 'package', name: 'fs', package: 'fs' }, + ], + ])('matches a matching package specifier: %s', runTestPositive); + + it.each<[string, TypeOrValueSpecifier]>([ + [ + 'import { mock } from "node:test"; const hoge = mock;', + { from: 'package', name: 'hoge', package: 'node:test' }, + ], + [ + 'import { mock } from "node"; const hoge = mock;', + { from: 'package', name: 'mock', package: 'node:test' }, + ], + [ + 'const mock = 42; const hoge = mock;', + { from: 'package', name: 'mock', package: 'node:test' }, + ], + ])("doesn't match a mismatched package specifier: %s", runTestNegative); + }); + + describe(AST_NODE_TYPES.ClassDeclaration, () => { + function runTests( + code: string, + specifier: TypeOrValueSpecifier, + expected: boolean, + ) { + const { ast, services } = parseCode(code); + const declaration = ast.body.at(-1) as TSESTree.ClassDeclaration; + const definition = declaration.body.body.at( + -1, + ) as TSESTree.PropertyDefinition; + const { property } = definition.value as TSESTree.MemberExpression; + const type = services.getTypeAtLocation(property); + expect( + valueMatchesSpecifier(property, specifier, services.program, type), + ).toBe(expected); + } + + function runTestPositive( + code: string, + specifier: TypeOrValueSpecifier, + ): void { + runTests(code, specifier, true); + } + + function runTestNegative( + code: string, + specifier: TypeOrValueSpecifier, + ): void { + runTests(code, specifier, false); + } + + it.each<[string, TypeOrValueSpecifier]>([ + [ + `class MyClass { + #privateProp = 42; + value = this.#privateProp; + }`, + 'privateProp', + ], + [ + ` + class MyClass { + ['computed prop'] = 42; + value = this['computed prop']; + }`, + `computed prop`, + ], + ])('matches a matching universal string specifier: %s', runTestPositive); + + it.each<[string, TypeOrValueSpecifier]>([ + [ + `class MyClass { + #privateProp = 42; + value = this.#privateProp; + }`, + 'incorrect', + ], + ])('matches a matching universal string specifier: %s', runTestNegative); + }); + }); + + describe(typeMatchesSomeSpecifier, () => { + function runTests( + code: string, + specifiers: TypeOrValueSpecifier[], + expected: boolean, + ): void { + const rootDir = path.join(__dirname, 'fixtures'); + const { ast, services } = parseForESLint(code, { + disallowAutomaticSingleRunInference: true, + filePath: path.join(rootDir, 'file.ts'), + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }); + const type = services + .program!.getTypeChecker() + .getTypeAtLocation( + services.esTreeNodeToTSNodeMap.get( + (ast.body[ast.body.length - 1] as TSESTree.TSTypeAliasDeclaration) + .id, + ), + ); + expect( + typeMatchesSomeSpecifier(type, specifiers, services.program!), + ).toBe(expected); + } + + function runTestPositive( + code: string, + specifiers: TypeOrValueSpecifier[], + ): void { + runTests(code, specifiers, true); + } + + function runTestNegative( + code: string, + specifiers: TypeOrValueSpecifier[], + ): void { + runTests(code, specifiers, false); + } + + it.each<[string, TypeOrValueSpecifier[]]>([ + ['interface Foo {prop: string}; type Test = Foo;', ['Foo', 'Hoge']], + ['type Test = RegExp;', ['RegExp', 'BigInt']], + ])('matches a matching universal string specifiers', runTestPositive); + + it.each<[string, TypeOrValueSpecifier[]]>([ + ['interface Foo {prop: string}; type Test = Foo;', ['Bar', 'Hoge']], + ['type Test = RegExp;', ['Foo', 'BigInt']], + ])( + "doesn't match a mismatched universal string specifiers", + runTestNegative, + ); + }); + + describe(valueMatchesSomeSpecifier, () => { + function runTests( + code: string, + specifiers: TypeOrValueSpecifier[] | undefined, + expected: boolean, + ): void { + const rootDir = path.join(__dirname, 'fixtures'); + const { ast, services } = parseForESLint(code, { + disallowAutomaticSingleRunInference: true, + filePath: path.join(rootDir, 'file.ts'), + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }); + assert.isNotNull(services.program); + + const declaration = ast.body.at(-1) as TSESTree.VariableDeclaration; + const { id, init } = declaration.declarations[0]; + const type = services.getTypeAtLocation(id); + expect( + valueMatchesSomeSpecifier(init!, specifiers, services.program, type), + ).toBe(expected); + } + + function runTestPositive( + code: string, + specifiers: TypeOrValueSpecifier[], + ): void { + runTests(code, specifiers, true); + } + + function runTestNegative( + code: string, + specifiers: TypeOrValueSpecifier[] | undefined, + ): void { + runTests(code, specifiers, false); + } + + it.each<[string, TypeOrValueSpecifier[]]>([ + ['const value = 45; const hoge = value;', ['value', 'hoge']], + ['let value = 45; const hoge = value;', ['value', 'hoge']], + ['var value = 45; const hoge = value;', ['value', 'hoge']], + ])('matches a matching universal string specifiers: %s', runTestPositive); + + it.each<[string, TypeOrValueSpecifier[] | undefined]>([ + ['const value = 45; const hoge = value;', ['incorrect', 'invalid']], + ['const value = 45; const hoge = value;', undefined], + ])( + "doesn't match a mismatched universal string specifiers: %s", + runTestNegative, + ); + }); });