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, 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