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, + ); + }); }); 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