diff --git a/packages/type-utils/src/TypeOrValueSpecifier.ts b/packages/type-utils/src/TypeOrValueSpecifier.ts index 3ba6b02868d9..34e18ac5f5ce 100644 --- a/packages/type-utils/src/TypeOrValueSpecifier.ts +++ b/packages/type-utils/src/TypeOrValueSpecifier.ts @@ -146,6 +146,21 @@ function typeDeclaredInFile( ); } +function typeDeclaredInPackage( + packageName: string, + declarationFiles: ts.SourceFile[], +): boolean { + // Handle scoped packages - if the name starts with @, remove it and replace / with __ + const typesPackageName = + '@types/' + packageName.replace(/^@([^/]+)\//, '$1__'); + const matcher = new RegExp( + `node_modules/(?:${packageName}|${typesPackageName})/`, + ); + return declarationFiles.some(declaration => + matcher.test(declaration.fileName), + ); +} + export function typeMatchesSpecifier( type: ts.Type, specifier: TypeOrValueSpecifier, @@ -170,12 +185,6 @@ export function typeMatchesSpecifier( program.isSourceFileDefaultLibrary(declaration), ); case 'package': - return declarationFiles.some( - declaration => - declaration.fileName.includes(`node_modules/${specifier.package}/`) || - declaration.fileName.includes( - `node_modules/@types/${specifier.package}/`, - ), - ); + return typeDeclaredInPackage(specifier.package, declarationFiles); } } diff --git a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts index d768911528a3..c64ce7e6bac3 100644 --- a/packages/type-utils/tests/TypeOrValueSpecifier.test.ts +++ b/packages/type-utils/tests/TypeOrValueSpecifier.test.ts @@ -139,7 +139,8 @@ describe('TypeOrValueSpecifier', () => { .program!.getTypeChecker() .getTypeAtLocation( services.esTreeNodeToTSNodeMap.get( - (ast.body[0] as TSESTree.TSTypeAliasDeclaration).id, + (ast.body[ast.body.length - 1] as TSESTree.TSTypeAliasDeclaration) + .id, ), ); expect(typeMatchesSpecifier(type, specifier, services.program!)).toBe( @@ -232,6 +233,90 @@ describe('TypeOrValueSpecifier', () => { ['type Test = RegExp;', { from: 'lib', name: ['BigInt', 'Date'] }], ])("doesn't match a mismatched lib specifier: %s", runTestNegative); + it.each<[string, TypeOrValueSpecifier]>([ + [ + 'import type {Node} from "typescript"; type Test = Node;', + { from: 'package', name: 'Node', package: 'typescript' }, + ], + [ + 'import type {Node} from "typescript"; type Test = Node;', + { from: 'package', name: ['Node', 'Symbol'], package: 'typescript' }, + ], + [ + 'import {Node} from "typescript"; type Test = Node;', + { from: 'package', name: 'Node', package: 'typescript' }, + ], + [ + 'import {Node} from "typescript"; type Test = Node;', + { from: 'package', name: ['Node', 'Symbol'], package: 'typescript' }, + ], + [ + 'import * as ts from "typescript"; type Test = ts.Node;', + { from: 'package', name: 'Node', package: 'typescript' }, + ], + [ + 'import * as ts from "typescript"; type Test = ts.Node;', + { from: 'package', name: ['Node', 'Symbol'], package: 'typescript' }, + ], + [ + 'import type * as ts from "typescript"; type Test = ts.Node;', + { from: 'package', name: 'Node', package: 'typescript' }, + ], + [ + 'import type * as ts from "typescript"; type Test = ts.Node;', + { from: 'package', name: ['Node', 'Symbol'], package: 'typescript' }, + ], + [ + 'import type {Node as TsNode} from "typescript"; type Test = TsNode;', + { from: 'package', name: 'Node', package: 'typescript' }, + ], + [ + 'import type {Node as TsNode} from "typescript"; type Test = TsNode;', + { from: 'package', name: ['Node', 'Symbol'], package: 'typescript' }, + ], + // The following type is available from the @types/semver package. + [ + 'import {SemVer} from "semver"; type Test = SemVer;', + { from: 'package', name: 'SemVer', package: 'semver' }, + ], + // The following type is available from the scoped @types/babel__code-frame package. + [ + 'import {BabelCodeFrameOptions} from "@babel/code-frame"; type Test = BabelCodeFrameOptions;', + { + from: 'package', + name: 'BabelCodeFrameOptions', + package: '@babel/code-frame', + }, + ], + ])('matches a matching package specifier: %s', runTestPositive); + + it.each<[string, TypeOrValueSpecifier]>([ + [ + 'import type {Node} from "typescript"; type Test = Node;', + { from: 'package', name: 'Symbol', package: 'typescript' }, + ], + [ + 'import type {Node} from "typescript"; type Test = Node;', + { from: 'package', name: ['Symbol', 'Checker'], package: 'typescript' }, + ], + [ + 'import type {Node} from "typescript"; type Test = Node;', + { from: 'package', name: 'Node', package: 'other-package' }, + ], + [ + 'import type {Node} from "typescript"; type Test = Node;', + { from: 'package', name: ['Node', 'Symbol'], package: 'other-package' }, + ], + [ + 'interface Node {prop: string}; type Test = Node;', + { from: 'package', name: 'Node', package: 'typescript' }, + ], + [ + 'import type {Node as TsNode} from "typescript"; type Test = TsNode;', + { from: 'package', name: 'TsNode', package: 'typescript' }, + ], + ])("doesn't match a mismatched lib specifier: %s", runTestNegative); + it.each<[string, TypeOrValueSpecifier]>([ [ 'interface Foo {prop: string}; type Test = Foo;',