From 38e69ffbfa1efe8487f16ec90c589df0d767b405 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Fri, 17 Jan 2025 21:17:07 +0900 Subject: [PATCH 01/21] feat: create valueMatchesSomeSpecifier function --- .../type-utils/src/TypeOrValueSpecifier.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 21c1871cba65..a2f52061c773 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'; @@ -202,3 +204,24 @@ export const typeMatchesSomeSpecifier = ( program: ts.Program, ): boolean => specifiers.some(specifier => typeMatchesSpecifier(type, specifier, program)); + +export function valueMatchesSpecifier( + node: TSESTree.Node, + specifier: TypeOrValueSpecifier, +): boolean { + if (node.type !== AST_NODE_TYPES.Identifier) { + return false; + } + + if (typeof specifier === 'string') { + return node.name === specifier; + } + + return node.name === specifier.name; +} + +export const valueMatchesSomeSpecifier = ( + node: TSESTree.Node, + specifiers: TypeOrValueSpecifier[] = [], +): boolean => + specifiers.some(specifier => valueMatchesSpecifier(node, specifier)); From ae6e55ac6e327d1342878fe5ef31bc6e2751004c Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Fri, 17 Jan 2025 21:19:49 +0900 Subject: [PATCH 02/21] fix: ignore deprecated value --- packages/eslint-plugin/src/rules/no-deprecated.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index e8e0402e3241..e8a8bac78ff1 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 = @@ -372,7 +373,10 @@ export default createRule({ } const type = services.getTypeAtLocation(node); - if (typeMatchesSomeSpecifier(type, allow, services.program)) { + if ( + typeMatchesSomeSpecifier(type, allow, services.program) || + valueMatchesSomeSpecifier(node, allow) + ) { return; } From 36d80559717c2f89bb9c26b21b497a030a2c8b94 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Fri, 17 Jan 2025 21:20:17 +0900 Subject: [PATCH 03/21] test: add test --- .../tests/rules/no-deprecated.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index 9bacc19eff23..5b5bdee40a7e 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -327,6 +327,30 @@ new A(); }, { code: ` +/** @deprecated */ +const deprecatedValue = 45; +const bar = deprecatedValue; + `, + options: [ + { + allow: [{ from: 'file', name: 'deprecatedValue' }], + }, + ], + }, + { + code: ` +/** @deprecated */ +const deprecatedValue = 45; +const bar = deprecatedValue; + `, + options: [ + { + allow: ['deprecatedValue'], + }, + ], + }, + { + code: ` import { exists } from 'fs'; exists('/foo'); `, From d545ea1bebe72acbfe51ef4bd8f5bd7492ae5d4e Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 25 Jan 2025 16:44:51 +0900 Subject: [PATCH 04/21] test: add test --- .../type-utils/src/TypeOrValueSpecifier.ts | 17 +-- .../tests/TypeOrValueSpecifier.test.ts | 107 +++++++++++++++++- 2 files changed, 116 insertions(+), 8 deletions(-) diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index a2f52061c773..80b9304c68c8 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -2,7 +2,6 @@ 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'; @@ -209,15 +208,19 @@ export function valueMatchesSpecifier( node: TSESTree.Node, specifier: TypeOrValueSpecifier, ): boolean { - if (node.type !== AST_NODE_TYPES.Identifier) { - return false; - } + if ('name' in node && typeof node.name === 'string') { + if (typeof specifier === 'string') { + return node.name === specifier; + } - if (typeof specifier === 'string') { - return node.name === specifier; + if (typeof specifier.name === 'string') { + return node.name === specifier.name; + } + + return specifier.name.includes(node.name); } - return node.name === specifier.name; + return false; } export const valueMatchesSomeSpecifier = ( diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 7af6d7dac2b0..c5722629d55c 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -6,7 +6,11 @@ import path from 'node:path'; import type { TypeOrValueSpecifier } from '../src/TypeOrValueSpecifier'; -import { typeMatchesSpecifier, typeOrValueSpecifiersSchema } from '../src'; +import { + typeMatchesSpecifier, + typeOrValueSpecifiersSchema, + valueMatchesSpecifier, +} from '../src'; describe('TypeOrValueSpecifier', () => { describe('Schema', () => { @@ -506,4 +510,105 @@ describe('TypeOrValueSpecifier', () => { ['type Test = Foo;', { from: 'lib', name: ['Foo', 'number'] }], ])("doesn't match an error type: %s", runTestNegative); }); + + describe('valueMatchesSpecifier', () => { + function runTests( + code: string, + specifier: TypeOrValueSpecifier, + expected: boolean, + ): void { + const rootDir = path.join(__dirname, 'fixtures'); + const { ast } = parseForESLint(code, { + disallowAutomaticSingleRunInference: true, + filePath: path.join(rootDir, 'file.ts'), + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }); + + const declaration = ast.body.at(-1) as TSESTree.VariableDeclaration; + const { init } = declaration.declarations[0]; + expect(valueMatchesSpecifier(init!, specifier)).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; 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' }, + ], + ])('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' }, + ], + ])("doesn't match a mismatched package specifier: %s", runTestNegative); + }); }); From a4463a4dae6c2c5c619d8ee63db0ac858c75ef36 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 27 Feb 2025 23:18:28 +0900 Subject: [PATCH 05/21] refactor: use AST node narrowing --- packages/type-utils/src/TypeOrValueSpecifier.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 80b9304c68c8..f536a37fc291 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -2,6 +2,7 @@ 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'; @@ -208,7 +209,10 @@ export function valueMatchesSpecifier( node: TSESTree.Node, specifier: TypeOrValueSpecifier, ): boolean { - if ('name' in node && typeof node.name === 'string') { + if ( + node.type === AST_NODE_TYPES.Identifier || + node.type === AST_NODE_TYPES.JSXIdentifier + ) { if (typeof specifier === 'string') { return node.name === specifier; } From 95839f6ee0fc0b66e65a1b58be6e73d69d4340ac Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 27 Feb 2025 23:19:08 +0900 Subject: [PATCH 06/21] feat: support package --- .../eslint-plugin/src/rules/no-deprecated.ts | 7 ++++++- .../type-utils/src/TypeOrValueSpecifier.ts | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index e8a8bac78ff1..30c921a687cf 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -375,7 +375,12 @@ export default createRule({ const type = services.getTypeAtLocation(node); if ( typeMatchesSomeSpecifier(type, allow, services.program) || - valueMatchesSomeSpecifier(node, allow) + (context.sourceCode.scopeManager && + valueMatchesSomeSpecifier( + node, + allow, + context.sourceCode.scopeManager, + )) ) { return; } diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index f536a37fc291..56dfbe6504ed 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -1,3 +1,4 @@ +import type { ScopeManager } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/types'; import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; import type * as ts from 'typescript'; @@ -208,6 +209,7 @@ export const typeMatchesSomeSpecifier = ( export function valueMatchesSpecifier( node: TSESTree.Node, specifier: TypeOrValueSpecifier, + scopeManager: ScopeManager, ): boolean { if ( node.type === AST_NODE_TYPES.Identifier || @@ -217,6 +219,17 @@ export function valueMatchesSpecifier( return node.name === specifier; } + if (specifier.from === 'package') { + const variable = scopeManager.variables.find(v => v.name === node.name); + const targetNode = variable?.defs[0].parent; + if (targetNode?.type !== AST_NODE_TYPES.ImportDeclaration) { + return false; + } + if (targetNode.source.value !== specifier.package) { + return false; + } + } + if (typeof specifier.name === 'string') { return node.name === specifier.name; } @@ -230,5 +243,8 @@ export function valueMatchesSpecifier( export const valueMatchesSomeSpecifier = ( node: TSESTree.Node, specifiers: TypeOrValueSpecifier[] = [], + scopeManager: ScopeManager, ): boolean => - specifiers.some(specifier => valueMatchesSpecifier(node, specifier)); + specifiers.some(specifier => + valueMatchesSpecifier(node, specifier, scopeManager), + ); From b80196b47f46fab71db60e830d4069a567b6580d Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 27 Feb 2025 23:36:35 +0900 Subject: [PATCH 07/21] test: add test --- .../tests/rules/no-deprecated.test.ts | 46 +++++++++++++++++++ .../tests/TypeOrValueSpecifier.test.ts | 10 +++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index 5b5bdee40a7e..1cef506e734f 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -315,6 +315,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(); @@ -2781,5 +2796,36 @@ 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', + }, + ], + }, + ], + }, ], }); diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index c5722629d55c..f2e515861a24 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -518,7 +518,7 @@ describe('TypeOrValueSpecifier', () => { expected: boolean, ): void { const rootDir = path.join(__dirname, 'fixtures'); - const { ast } = parseForESLint(code, { + const { ast, scopeManager } = parseForESLint(code, { disallowAutomaticSingleRunInference: true, filePath: path.join(rootDir, 'file.ts'), project: './tsconfig.json', @@ -527,7 +527,9 @@ describe('TypeOrValueSpecifier', () => { const declaration = ast.body.at(-1) as TSESTree.VariableDeclaration; const { init } = declaration.declarations[0]; - expect(valueMatchesSpecifier(init!, specifier)).toBe(expected); + expect(valueMatchesSpecifier(init!, specifier, scopeManager)).toBe( + expected, + ); } function runTestPositive( @@ -609,6 +611,10 @@ describe('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' }, + ], ])("doesn't match a mismatched package specifier: %s", runTestNegative); }); }); From af4be160dff8e3dcad24d0f4836532e9e30955b7 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 1 Mar 2025 12:54:50 +0900 Subject: [PATCH 08/21] test: add test --- .../type-utils/tests/TypeOrValueSpecifier.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 34206a9801a8..4e053f2a31e8 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -597,6 +597,15 @@ describe('TypeOrValueSpecifier', () => { 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'], @@ -666,6 +675,10 @@ describe('TypeOrValueSpecifier', () => { '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); }); }); From 356721226495aea37c099ad6ace2b8e11d93b54a Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sun, 2 Mar 2025 17:57:48 +0900 Subject: [PATCH 09/21] test: add test for typeMatchesSomeSpecifier function --- .../tests/TypeOrValueSpecifier.test.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 4e053f2a31e8..283de9d24aed 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -7,6 +7,7 @@ import path from 'node:path'; import type { TypeOrValueSpecifier } from '../src/TypeOrValueSpecifier'; import { + typeMatchesSomeSpecifier, typeMatchesSpecifier, typeOrValueSpecifiersSchema, valueMatchesSpecifier, @@ -681,4 +682,58 @@ describe('TypeOrValueSpecifier', () => { ], ])("doesn't match a mismatched package 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, + ); + }); }); From 1cd8ea67c9243577a368a0eb9befb84abc0e7cf1 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sun, 2 Mar 2025 18:31:51 +0900 Subject: [PATCH 10/21] test: add test for valueMatchesSomeSpecifier function --- .../tests/TypeOrValueSpecifier.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 283de9d24aed..a9c4a62e48d6 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -10,6 +10,7 @@ import { typeMatchesSomeSpecifier, typeMatchesSpecifier, typeOrValueSpecifiersSchema, + valueMatchesSomeSpecifier, valueMatchesSpecifier, } from '../src'; @@ -736,4 +737,53 @@ describe('TypeOrValueSpecifier', () => { runTestNegative, ); }); + + describe('valueMatchesSomeSpecifier', () => { + function runTests( + code: string, + specifiers: TypeOrValueSpecifier[], + expected: boolean, + ): void { + const rootDir = path.join(__dirname, 'fixtures'); + const { ast, scopeManager } = parseForESLint(code, { + disallowAutomaticSingleRunInference: true, + filePath: path.join(rootDir, 'file.ts'), + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }); + + const declaration = ast.body.at(-1) as TSESTree.VariableDeclaration; + const { init } = declaration.declarations[0]; + expect(valueMatchesSomeSpecifier(init!, specifiers, scopeManager)).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[]]>([ + ['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[]]>([ + ['const value = 45; const hoge = value;', ['incorrect', 'invalid']], + ])( + "doesn't match a mismatched universal string specifiers: %s", + runTestNegative, + ); + }); }); From 3673d2160c3f9c1317de6485d3f3228b700245df Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sun, 2 Mar 2025 19:28:10 +0900 Subject: [PATCH 11/21] refactor: remove default empty array from specifiers --- packages/type-utils/src/TypeOrValueSpecifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index d9e6cebcb616..d86e4fd1c6fc 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -259,7 +259,7 @@ export function valueMatchesSpecifier( export const valueMatchesSomeSpecifier = ( node: TSESTree.Node, - specifiers: TypeOrValueSpecifier[] = [], + specifiers: TypeOrValueSpecifier[], scopeManager: ScopeManager, ): boolean => specifiers.some(specifier => From 325e13254caf2445633e41d9f6e4824d54d13de1 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 13 Mar 2025 20:28:59 +0900 Subject: [PATCH 12/21] Revert "refactor: remove default empty array from specifiers" This reverts commit 3673d2160c3f9c1317de6485d3f3228b700245df. --- packages/type-utils/src/TypeOrValueSpecifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index d86e4fd1c6fc..d9e6cebcb616 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -259,7 +259,7 @@ export function valueMatchesSpecifier( export const valueMatchesSomeSpecifier = ( node: TSESTree.Node, - specifiers: TypeOrValueSpecifier[], + specifiers: TypeOrValueSpecifier[] = [], scopeManager: ScopeManager, ): boolean => specifiers.some(specifier => From 767fc7ac5b37c6fa08125c2ac08e579aa309c7e9 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 13 Mar 2025 20:34:49 +0900 Subject: [PATCH 13/21] test: add test case with undefined argument --- packages/type-utils/tests/TypeOrValueSpecifier.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index a9c4a62e48d6..fdf8556d1b96 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -741,7 +741,7 @@ describe('TypeOrValueSpecifier', () => { describe('valueMatchesSomeSpecifier', () => { function runTests( code: string, - specifiers: TypeOrValueSpecifier[], + specifiers: TypeOrValueSpecifier[] | undefined, expected: boolean, ): void { const rootDir = path.join(__dirname, 'fixtures'); @@ -768,7 +768,7 @@ describe('TypeOrValueSpecifier', () => { function runTestNegative( code: string, - specifiers: TypeOrValueSpecifier[], + specifiers: TypeOrValueSpecifier[] | undefined, ): void { runTests(code, specifiers, false); } @@ -779,8 +779,9 @@ describe('TypeOrValueSpecifier', () => { ['var value = 45; const hoge = value;', ['value', 'hoge']], ])('matches a matching universal string specifiers: %s', runTestPositive); - it.each<[string, TypeOrValueSpecifier[]]>([ + 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, From 54f820b778743aa0dd54fee4c566c79f9c647571 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 13 Mar 2025 20:45:44 +0900 Subject: [PATCH 14/21] fix: sync workspace --- packages/type-utils/tsconfig.build.json | 3 +++ packages/type-utils/tsconfig.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/type-utils/tsconfig.build.json b/packages/type-utils/tsconfig.build.json index f409f6784f7c..fa511f0317fb 100644 --- a/packages/type-utils/tsconfig.build.json +++ b/packages/type-utils/tsconfig.build.json @@ -14,6 +14,9 @@ { "path": "../types/tsconfig.build.json" }, + { + "path": "../scope-manager/tsconfig.build.json" + }, { "path": "../utils/tsconfig.build.json" }, diff --git a/packages/type-utils/tsconfig.json b/packages/type-utils/tsconfig.json index a57d8d1814a8..183402d66a49 100644 --- a/packages/type-utils/tsconfig.json +++ b/packages/type-utils/tsconfig.json @@ -6,6 +6,9 @@ { "path": "../types" }, + { + "path": "../scope-manager" + }, { "path": "../utils" }, From 3b3520b66d05a77cb2db79a003bec79bab5c8b18 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Fri, 21 Mar 2025 18:30:04 +0900 Subject: [PATCH 15/21] feat: support PrivateIdentifier --- .../type-utils/src/TypeOrValueSpecifier.ts | 55 ++-- .../tests/TypeOrValueSpecifier.test.ts | 277 +++++++++++------- 2 files changed, 204 insertions(+), 128 deletions(-) diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index d9e6cebcb616..5d83d6bd8e65 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -223,38 +223,45 @@ export const typeMatchesSomeSpecifier = ( ): boolean => specifiers.some(specifier => typeMatchesSpecifier(type, specifier, program)); -export function valueMatchesSpecifier( - node: TSESTree.Node, - specifier: TypeOrValueSpecifier, - scopeManager: ScopeManager, -): boolean { +const getSpecifierNames = (specifier: TypeOrValueSpecifier): string[] => { + if (typeof specifier === 'string') { + return [specifier]; + } + + return typeof specifier.name === 'string' ? [specifier.name] : specifier.name; +}; + +const getNodeName = (node: TSESTree.Node): string | undefined => { if ( node.type === AST_NODE_TYPES.Identifier || - node.type === AST_NODE_TYPES.JSXIdentifier + node.type === AST_NODE_TYPES.JSXIdentifier || + node.type === AST_NODE_TYPES.PrivateIdentifier ) { - if (typeof specifier === 'string') { - return node.name === specifier; - } + return node.name; + } - if (specifier.from === 'package') { - const variable = scopeManager.variables.find(v => v.name === node.name); - const targetNode = variable?.defs[0].parent; - if (targetNode?.type !== AST_NODE_TYPES.ImportDeclaration) { - return false; - } - if (targetNode.source.value !== specifier.package) { - return false; - } - } + return undefined; +}; - if (typeof specifier.name === 'string') { - return node.name === specifier.name; +export function valueMatchesSpecifier( + node: TSESTree.Node, + specifier: TypeOrValueSpecifier, + scopeManager: ScopeManager, +): boolean { + const specifierNames = getSpecifierNames(specifier); + const nodeName = getNodeName(node); + if (typeof specifier !== 'string' && specifier.from === 'package') { + const variable = scopeManager.variables.find(v => nodeName === v.name); + const targetNode = variable?.defs[0].parent; + if ( + targetNode?.type !== AST_NODE_TYPES.ImportDeclaration || + targetNode.source.value !== specifier.package + ) { + return false; } - - return specifier.name.includes(node.name); } - return false; + return nodeName ? specifierNames.includes(nodeName) : false; } export const valueMatchesSomeSpecifier = ( diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index fdf8556d1b96..68bf73e8859a 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -1,6 +1,7 @@ import type { TSESTree } from '@typescript-eslint/utils'; import { parseForESLint } from '@typescript-eslint/parser'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import Ajv from 'ajv'; import path from 'node:path'; @@ -565,11 +566,7 @@ describe('TypeOrValueSpecifier', () => { }); describe('valueMatchesSpecifier', () => { - function runTests( - code: string, - specifier: TypeOrValueSpecifier, - expected: boolean, - ): void { + function parseCode(code: string) { const rootDir = path.join(__dirname, 'fixtures'); const { ast, scopeManager } = parseForESLint(code, { disallowAutomaticSingleRunInference: true, @@ -578,110 +575,182 @@ describe('TypeOrValueSpecifier', () => { tsconfigRootDir: rootDir, }); - const declaration = ast.body.at(-1) as TSESTree.VariableDeclaration; - const { init } = declaration.declarations[0]; - expect(valueMatchesSpecifier(init!, specifier, scopeManager)).toBe( - expected, - ); + return { ast, scopeManager }; } - 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); + describe(AST_NODE_TYPES.VariableDeclaration, () => { + function runTests( + code: string, + specifier: TypeOrValueSpecifier, + expected: boolean, + ) { + const { ast, scopeManager } = parseCode(code); + const declaration = ast.body.at(-1) as TSESTree.VariableDeclaration; + const { init } = declaration.declarations[0]; + expect(valueMatchesSpecifier(init!, specifier, scopeManager)).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 = 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]>([ + ['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]>([ - [ - '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' }, - ], - ])('matches a matching package specifier: %s', runTestPositive); + 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' }, + ], + ])('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); + }); - 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, scopeManager } = 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; + expect(valueMatchesSpecifier(property, specifier, scopeManager)).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', + ], + ])('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', () => { From 0e61bcf1a293d549ed12fe17a732e212b828f66c Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Thu, 10 Apr 2025 21:19:44 +0900 Subject: [PATCH 16/21] feat: support literal case --- packages/type-utils/src/TypeOrValueSpecifier.ts | 12 ++++++++---- .../type-utils/tests/TypeOrValueSpecifier.test.ts | 8 ++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 5d83d6bd8e65..6188aebc4562 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -231,7 +231,7 @@ const getSpecifierNames = (specifier: TypeOrValueSpecifier): string[] => { return typeof specifier.name === 'string' ? [specifier.name] : specifier.name; }; -const getNodeName = (node: TSESTree.Node): string | undefined => { +const getStaticName = (node: TSESTree.Node): string | undefined => { if ( node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.JSXIdentifier || @@ -240,6 +240,10 @@ const getNodeName = (node: TSESTree.Node): string | undefined => { return node.name; } + if (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string') { + return node.value; + } + return undefined; }; @@ -249,9 +253,9 @@ export function valueMatchesSpecifier( scopeManager: ScopeManager, ): boolean { const specifierNames = getSpecifierNames(specifier); - const nodeName = getNodeName(node); + const staticName = getStaticName(node); if (typeof specifier !== 'string' && specifier.from === 'package') { - const variable = scopeManager.variables.find(v => nodeName === v.name); + const variable = scopeManager.variables.find(v => staticName === v.name); const targetNode = variable?.defs[0].parent; if ( targetNode?.type !== AST_NODE_TYPES.ImportDeclaration || @@ -261,7 +265,7 @@ export function valueMatchesSpecifier( } } - return nodeName ? specifierNames.includes(nodeName) : false; + return staticName ? specifierNames.includes(staticName) : false; } export const valueMatchesSomeSpecifier = ( diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 68bf73e8859a..70ac5150204b 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -739,6 +739,14 @@ describe('TypeOrValueSpecifier', () => { }`, 'privateProp', ], + [ + ` + class MyClass { + ['computed prop'] = 42; + value = this['computed prop']; + }`, + `computed prop`, + ], ])('matches a matching universal string specifier: %s', runTestPositive); it.each<[string, TypeOrValueSpecifier]>([ From 15781373b072fcfc641fe28abd7628ea4676a31f Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Fri, 11 Apr 2025 21:23:42 +0900 Subject: [PATCH 17/21] feat: support dynamic import --- .../eslint-plugin/src/rules/no-deprecated.ts | 7 +-- .../type-utils/src/TypeOrValueSpecifier.ts | 53 +++++++++++-------- .../tests/TypeOrValueSpecifier.test.ts | 42 +++++++++------ 3 files changed, 59 insertions(+), 43 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-deprecated.ts b/packages/eslint-plugin/src/rules/no-deprecated.ts index 7f5c3facbd67..6f04a3501f88 100644 --- a/packages/eslint-plugin/src/rules/no-deprecated.ts +++ b/packages/eslint-plugin/src/rules/no-deprecated.ts @@ -377,12 +377,7 @@ export default createRule({ const type = services.getTypeAtLocation(node); if ( typeMatchesSomeSpecifier(type, allow, services.program) || - (context.sourceCode.scopeManager && - valueMatchesSomeSpecifier( - node, - allow, - context.sourceCode.scopeManager, - )) + valueMatchesSomeSpecifier(node, allow, services.program, type) ) { return; } diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 6188aebc4562..663dd58aacdc 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -1,4 +1,3 @@ -import type { ScopeManager } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/types'; import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema'; import type * as ts from 'typescript'; @@ -223,12 +222,8 @@ export const typeMatchesSomeSpecifier = ( ): boolean => specifiers.some(specifier => typeMatchesSpecifier(type, specifier, program)); -const getSpecifierNames = (specifier: TypeOrValueSpecifier): string[] => { - if (typeof specifier === 'string') { - return [specifier]; - } - - return typeof specifier.name === 'string' ? [specifier.name] : specifier.name; +const getSpecifierNames = (specifierName: string | string[]): string[] => { + return typeof specifierName === 'string' ? [specifierName] : specifierName; }; const getStaticName = (node: TSESTree.Node): string | undefined => { @@ -250,29 +245,45 @@ const getStaticName = (node: TSESTree.Node): string | undefined => { export function valueMatchesSpecifier( node: TSESTree.Node, specifier: TypeOrValueSpecifier, - scopeManager: ScopeManager, + program: ts.Program, + type: ts.Type, ): boolean { - const specifierNames = getSpecifierNames(specifier); const staticName = getStaticName(node); - if (typeof specifier !== 'string' && specifier.from === 'package') { - const variable = scopeManager.variables.find(v => staticName === v.name); - const targetNode = variable?.defs[0].parent; - if ( - targetNode?.type !== AST_NODE_TYPES.ImportDeclaration || - targetNode.source.value !== specifier.package - ) { - return false; - } + if (!staticName) { + return false; + } + + if (typeof specifier === 'string') { + return specifier === staticName; + } + + if (!getSpecifierNames(specifier.name).includes(staticName)) { + return false; } - return staticName ? specifierNames.includes(staticName) : 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[] = [], - scopeManager: ScopeManager, + program: ts.Program, + type: ts.Type, ): boolean => specifiers.some(specifier => - valueMatchesSpecifier(node, specifier, scopeManager), + valueMatchesSpecifier(node, specifier, program, type), ); diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 70ac5150204b..5c406865acac 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -14,6 +14,7 @@ import { valueMatchesSomeSpecifier, valueMatchesSpecifier, } from '../src'; +import { expectToHaveParserServices } from './test-utils/expectToHaveParserServices'; describe('TypeOrValueSpecifier', () => { describe('Schema', () => { @@ -568,14 +569,15 @@ describe('TypeOrValueSpecifier', () => { describe('valueMatchesSpecifier', () => { function parseCode(code: string) { const rootDir = path.join(__dirname, 'fixtures'); - const { ast, scopeManager } = parseForESLint(code, { + const { ast, services } = parseForESLint(code, { disallowAutomaticSingleRunInference: true, filePath: path.join(rootDir, 'file.ts'), project: './tsconfig.json', tsconfigRootDir: rootDir, }); + expectToHaveParserServices(services); - return { ast, scopeManager }; + return { ast, services }; } describe(AST_NODE_TYPES.VariableDeclaration, () => { @@ -584,12 +586,13 @@ describe('TypeOrValueSpecifier', () => { specifier: TypeOrValueSpecifier, expected: boolean, ) { - const { ast, scopeManager } = parseCode(code); + const { ast, services } = parseCode(code); const declaration = ast.body.at(-1) as TSESTree.VariableDeclaration; - const { init } = declaration.declarations[0]; - expect(valueMatchesSpecifier(init!, specifier, scopeManager)).toBe( - expected, - ); + const { id, init } = declaration.declarations[0]; + const type = services.getTypeAtLocation(id); + expect( + valueMatchesSpecifier(init!, specifier, services.program, type), + ).toBe(expected); } function runTestPositive( @@ -682,6 +685,10 @@ describe('TypeOrValueSpecifier', () => { '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]>([ @@ -706,15 +713,16 @@ describe('TypeOrValueSpecifier', () => { specifier: TypeOrValueSpecifier, expected: boolean, ) { - const { ast, scopeManager } = parseCode(code); + 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; - expect(valueMatchesSpecifier(property, specifier, scopeManager)).toBe( - expected, - ); + const type = services.getTypeAtLocation(property); + expect( + valueMatchesSpecifier(property, specifier, services.program, type), + ).toBe(expected); } function runTestPositive( @@ -822,18 +830,20 @@ describe('TypeOrValueSpecifier', () => { expected: boolean, ): void { const rootDir = path.join(__dirname, 'fixtures'); - const { ast, scopeManager } = parseForESLint(code, { + const { ast, services } = parseForESLint(code, { disallowAutomaticSingleRunInference: true, filePath: path.join(rootDir, 'file.ts'), project: './tsconfig.json', tsconfigRootDir: rootDir, }); + expectToHaveParserServices(services); const declaration = ast.body.at(-1) as TSESTree.VariableDeclaration; - const { init } = declaration.declarations[0]; - expect(valueMatchesSomeSpecifier(init!, specifiers, scopeManager)).toBe( - expected, - ); + const { id, init } = declaration.declarations[0]; + const type = services.getTypeAtLocation(id); + expect( + valueMatchesSomeSpecifier(init!, specifiers, services.program, type), + ).toBe(expected); } function runTestPositive( From d65268c861133436bd33c202c0a821051e7ede36 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Fri, 11 Apr 2025 21:35:15 +0900 Subject: [PATCH 18/21] test: add no-deprecated test --- .../tests/rules/no-deprecated.test.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts index 70e9d8a4764e..3f8631b7861e 100644 --- a/packages/eslint-plugin/tests/rules/no-deprecated.test.ts +++ b/packages/eslint-plugin/tests/rules/no-deprecated.test.ts @@ -371,6 +371,20 @@ const bar = deprecatedValue; }, { code: ` +class MyClass { + /** @deprecated */ + #privateProp = 42; + value = this.#privateProp; +} + `, + options: [ + { + allow: [{ from: 'file', name: 'privateProp' }], + }, + ], + }, + { + code: ` /** @deprecated */ const deprecatedValue = 45; const bar = deprecatedValue; @@ -384,6 +398,23 @@ const bar = deprecatedValue; { code: ` import { exists } from 'fs'; +exists('/foo'); + `, + options: [ + { + allow: [ + { + from: 'package', + name: 'exists', + package: 'fs', + }, + ], + }, + ], + }, + { + code: ` +const { exists } = import('fs'); exists('/foo'); `, options: [ From 72641c015414b1a4dd9543513bd469f00540ec81 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Fri, 11 Apr 2025 21:41:37 +0900 Subject: [PATCH 19/21] fix: sync workspace --- packages/type-utils/tsconfig.build.json | 3 --- packages/type-utils/tsconfig.json | 3 --- 2 files changed, 6 deletions(-) diff --git a/packages/type-utils/tsconfig.build.json b/packages/type-utils/tsconfig.build.json index fa511f0317fb..f409f6784f7c 100644 --- a/packages/type-utils/tsconfig.build.json +++ b/packages/type-utils/tsconfig.build.json @@ -14,9 +14,6 @@ { "path": "../types/tsconfig.build.json" }, - { - "path": "../scope-manager/tsconfig.build.json" - }, { "path": "../utils/tsconfig.build.json" }, diff --git a/packages/type-utils/tsconfig.json b/packages/type-utils/tsconfig.json index 183402d66a49..a57d8d1814a8 100644 --- a/packages/type-utils/tsconfig.json +++ b/packages/type-utils/tsconfig.json @@ -6,9 +6,6 @@ { "path": "../types" }, - { - "path": "../scope-manager" - }, { "path": "../utils" }, From 7e6b840459d6bee79f48f7d11681c45fd4e9df1b Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Fri, 16 May 2025 19:11:12 +0900 Subject: [PATCH 20/21] refactor: use named functions instead of string descriptions in describe blocks --- packages/type-utils/tests/TypeOrValueSpecifier.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index df42652e72b3..9f45066707a1 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -634,7 +634,7 @@ describe('TypeOrValueSpecifier', () => { ); }); - describe('valueMatchesSpecifier', () => { + describe(valueMatchesSpecifier, () => { function parseCode(code: string) { const rootDir = path.join(__dirname, 'fixtures'); const { ast, services } = parseForESLint(code, { @@ -837,7 +837,7 @@ describe('TypeOrValueSpecifier', () => { }); }); - describe('typeMatchesSomeSpecifier', () => { + describe(typeMatchesSomeSpecifier, () => { function runTests( code: string, specifiers: TypeOrValueSpecifier[], @@ -891,7 +891,7 @@ describe('TypeOrValueSpecifier', () => { ); }); - describe('valueMatchesSomeSpecifier', () => { + describe(valueMatchesSomeSpecifier, () => { function runTests( code: string, specifiers: TypeOrValueSpecifier[] | undefined, From 8ef57d277f953c8c586637c51abcdaea64026363 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Fri, 16 May 2025 23:08:39 +0900 Subject: [PATCH 21/21] fix: lint fix --- packages/type-utils/tests/TypeOrValueSpecifier.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index 9f45066707a1..ed9c5614b209 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -1,8 +1,12 @@ -import * as path from 'node:path'; +import type { TSESTree } from '@typescript-eslint/utils'; + import { parseForESLint } from '@typescript-eslint/parser'; -import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as path from 'node:path'; + +import type { TypeOrValueSpecifier } from '../src/index.js'; + import { - type TypeOrValueSpecifier, typeMatchesSomeSpecifier, typeMatchesSpecifier, valueMatchesSomeSpecifier,