diff --git a/src/services/importTracker.ts b/src/services/importTracker.ts index 41df9bcd583..ccee9c90a02 100644 --- a/src/services/importTracker.ts +++ b/src/services/importTracker.ts @@ -84,6 +84,10 @@ namespace ts.FindAllReferences { switch (direct.kind) { case SyntaxKind.CallExpression: + if (isImportCall(direct)) { + handleImportCall(direct); + break; + } if (!isAvailableThroughGlobal) { const parent = direct.parent; if (exportKind === ExportKind.ExportEquals && parent.kind === SyntaxKind.VariableDeclaration) { @@ -121,7 +125,7 @@ namespace ts.FindAllReferences { } else if (direct.exportClause.kind === SyntaxKind.NamespaceExport) { // `export * as foo from "foo"` add to indirect uses - addIndirectUsers(getSourceFileLikeForImportDeclaration(direct)); + addIndirectUser(getSourceFileLikeForImportDeclaration(direct), /** addTransitiveDependencies */ true); } else { // This is `export { foo } from "foo"` and creates an alias symbol, so recursive search will get handle re-exports. @@ -130,6 +134,10 @@ namespace ts.FindAllReferences { break; case SyntaxKind.ImportType: + // Only check for typeof import('xyz') + if (direct.isTypeOf && !direct.qualifier && isExported(direct)) { + addIndirectUser(direct.getSourceFile(), /** addTransitiveDependencies */ true); + } directImports.push(direct); break; @@ -140,6 +148,18 @@ namespace ts.FindAllReferences { } } + function handleImportCall(importCall: ImportCall) { + const top = findAncestor(importCall, isAmbientModuleDeclaration) || importCall.getSourceFile(); + addIndirectUser(top, /** addTransitiveDependencies */ !!isExported(importCall, /** stopAtAmbientModule */ true)); + } + + function isExported(node: Node, stopAtAmbientModule = false) { + return findAncestor(node, node => { + if (stopAtAmbientModule && isAmbientModuleDeclaration(node)) return "quit"; + return some(node.modifiers, mod => mod.kind === SyntaxKind.ExportKeyword); + }); + } + function handleNamespaceImport(importDeclaration: ImportEqualsDeclaration | ImportDeclaration, name: Identifier, isReExport: boolean, alreadyAddedDirect: boolean): void { if (exportKind === ExportKind.ExportEquals) { // This is a direct import, not import-as-namespace. @@ -149,7 +169,7 @@ namespace ts.FindAllReferences { const sourceFileLike = getSourceFileLikeForImportDeclaration(importDeclaration); Debug.assert(sourceFileLike.kind === SyntaxKind.SourceFile || sourceFileLike.kind === SyntaxKind.ModuleDeclaration); if (isReExport || findNamespaceReExports(sourceFileLike, name, checker)) { - addIndirectUsers(sourceFileLike); + addIndirectUser(sourceFileLike, /** addTransitiveDependencies */ true); } else { addIndirectUser(sourceFileLike); @@ -157,28 +177,22 @@ namespace ts.FindAllReferences { } } - function addIndirectUser(sourceFileLike: SourceFileLike): boolean { + /** Adds a module and all of its transitive dependencies as possible indirect users. */ + function addIndirectUser(sourceFileLike: SourceFileLike, addTransitiveDependencies = false): void { Debug.assert(!isAvailableThroughGlobal); const isNew = markSeenIndirectUser(sourceFileLike); - if (isNew) { - indirectUserDeclarations!.push(sourceFileLike); // TODO: GH#18217 - } - return isNew; - } - - /** Adds a module and all of its transitive dependencies as possible indirect users. */ - function addIndirectUsers(sourceFileLike: SourceFileLike): void { - if (!addIndirectUser(sourceFileLike)) { - return; - } + if (!isNew) return; + indirectUserDeclarations!.push(sourceFileLike); // TODO: GH#18217 + if (!addTransitiveDependencies) return; const moduleSymbol = checker.getMergedSymbol(sourceFileLike.symbol); + if (!moduleSymbol) return; Debug.assert(!!(moduleSymbol.flags & SymbolFlags.Module)); const directImports = getDirectImports(moduleSymbol); if (directImports) { for (const directImport of directImports) { if (!isImportTypeNode(directImport)) { - addIndirectUsers(getSourceFileLikeForImportDeclaration(directImport)); + addIndirectUser(getSourceFileLikeForImportDeclaration(directImport), /** addTransitiveDependencies */ true); } } } diff --git a/tests/baselines/reference/findAllRefsForImportCall.baseline.jsonc b/tests/baselines/reference/findAllRefsForImportCall.baseline.jsonc new file mode 100644 index 00000000000..cfff0227865 --- /dev/null +++ b/tests/baselines/reference/findAllRefsForImportCall.baseline.jsonc @@ -0,0 +1,98 @@ +// === /app.ts === +// /*FIND ALL REFS*/export function [|hello|]() {}; + +// === /indirect-use.ts === +// import("./re-export").then(mod => mod.services.app.[|hello|]()); + +// === /direct-use.ts === +// async function main() { +// const mod = await import("./app") +// mod.[|hello|](); +// } + +[ + { + "definition": { + "containerKind": "", + "containerName": "", + "fileName": "/app.ts", + "kind": "function", + "name": "function hello(): void", + "textSpan": { + "start": 16, + "length": 5 + }, + "displayParts": [ + { + "text": "function", + "kind": "keyword" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "hello", + "kind": "functionName" + }, + { + "text": "(", + "kind": "punctuation" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "void", + "kind": "keyword" + } + ], + "contextSpan": { + "start": 0, + "length": 26 + } + }, + "references": [ + { + "textSpan": { + "start": 16, + "length": 5 + }, + "fileName": "/app.ts", + "contextSpan": { + "start": 0, + "length": 26 + }, + "isWriteAccess": true, + "isDefinition": true + }, + { + "textSpan": { + "start": 51, + "length": 5 + }, + "fileName": "/indirect-use.ts", + "isWriteAccess": false, + "isDefinition": false + }, + { + "textSpan": { + "start": 70, + "length": 5 + }, + "fileName": "/direct-use.ts", + "isWriteAccess": false, + "isDefinition": false + } + ] + } +] \ No newline at end of file diff --git a/tests/baselines/reference/findAllRefsForImportCallType.baseline.jsonc b/tests/baselines/reference/findAllRefsForImportCallType.baseline.jsonc new file mode 100644 index 00000000000..1f512403f02 --- /dev/null +++ b/tests/baselines/reference/findAllRefsForImportCallType.baseline.jsonc @@ -0,0 +1,85 @@ +// === /app.ts === +// /*FIND ALL REFS*/export function [|hello|]() {}; + +// === /indirect-use.ts === +// import type { app } from "./re-export"; +// declare const app: app +// app.[|hello|](); + +[ + { + "definition": { + "containerKind": "", + "containerName": "", + "fileName": "/app.ts", + "kind": "function", + "name": "function hello(): void", + "textSpan": { + "start": 16, + "length": 5 + }, + "displayParts": [ + { + "text": "function", + "kind": "keyword" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "hello", + "kind": "functionName" + }, + { + "text": "(", + "kind": "punctuation" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": ":", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "void", + "kind": "keyword" + } + ], + "contextSpan": { + "start": 0, + "length": 26 + } + }, + "references": [ + { + "textSpan": { + "start": 16, + "length": 5 + }, + "fileName": "/app.ts", + "contextSpan": { + "start": 0, + "length": 26 + }, + "isWriteAccess": true, + "isDefinition": true + }, + { + "textSpan": { + "start": 67, + "length": 5 + }, + "fileName": "/indirect-use.ts", + "isWriteAccess": false, + "isDefinition": false + } + ] + } +] \ No newline at end of file diff --git a/tests/baselines/reference/findAllRefs_importType_js.1.baseline.jsonc b/tests/baselines/reference/findAllRefs_importType_js.1.baseline.jsonc new file mode 100644 index 00000000000..fa8e4c790bf --- /dev/null +++ b/tests/baselines/reference/findAllRefs_importType_js.1.baseline.jsonc @@ -0,0 +1,152 @@ +// === /b.js === +// /** @type {import("[|./a|]")} */ +// const x = 0; +// /** @type {import("./a").D} */ +// const y = 0; + +// === /a.js === +// /*FIND ALL REFS*/module.exports = class [|C|] {}; +// module.exports.D = class D {}; + +[ + { + "definition": { + "containerKind": "", + "containerName": "", + "fileName": "/a.js", + "kind": "local class", + "name": "(local class) C", + "textSpan": { + "start": 23, + "length": 1 + }, + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "local class", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "C", + "kind": "className" + } + ], + "contextSpan": { + "start": 17, + "length": 10 + } + }, + "references": [ + { + "textSpan": { + "start": 23, + "length": 1 + }, + "fileName": "/a.js", + "contextSpan": { + "start": 17, + "length": 10 + }, + "isWriteAccess": true, + "isDefinition": true + } + ] + }, + { + "definition": { + "containerKind": "", + "containerName": "", + "fileName": "/a.js", + "kind": "alias", + "name": "(alias) (local class) export=\nimport export=", + "textSpan": { + "start": 0, + "length": 27 + }, + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "alias", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "local class", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "export=", + "kind": "aliasName" + }, + { + "text": "\n", + "kind": "lineBreak" + }, + { + "text": "import", + "kind": "keyword" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "export=", + "kind": "aliasName" + } + ], + "contextSpan": { + "start": 0, + "length": 28 + } + }, + "references": [ + { + "textSpan": { + "start": 19, + "length": 3 + }, + "fileName": "/b.js", + "contextSpan": { + "start": 4, + "length": 21 + }, + "isWriteAccess": false, + "isDefinition": false + } + ] + } +] \ No newline at end of file diff --git a/tests/baselines/reference/findAllRefs_importType_js.2.baseline.jsonc b/tests/baselines/reference/findAllRefs_importType_js.2.baseline.jsonc new file mode 100644 index 00000000000..5e6981dd49a --- /dev/null +++ b/tests/baselines/reference/findAllRefs_importType_js.2.baseline.jsonc @@ -0,0 +1,107 @@ +// === /a.js === +// /*FIND ALL REFS*/module.exports = class C {}; +// module.exports.[|D|] = class D {}; + +// === /b.js === +// /** @type {import("./a")} */ +// const x = 0; +// /** @type {import("./a").[|D|]} */ +// const y = 0; + +[ + { + "definition": { + "containerKind": "", + "containerName": "", + "fileName": "/a.js", + "kind": "alias", + "name": "(alias) (local class) D\nimport D", + "textSpan": { + "start": 44, + "length": 1 + }, + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "alias", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "local class", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "D", + "kind": "aliasName" + }, + { + "text": "\n", + "kind": "lineBreak" + }, + { + "text": "import", + "kind": "keyword" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "D", + "kind": "aliasName" + } + ], + "contextSpan": { + "start": 29, + "length": 16 + } + }, + "references": [ + { + "textSpan": { + "start": 44, + "length": 1 + }, + "fileName": "/a.js", + "contextSpan": { + "start": 29, + "length": 30 + }, + "isWriteAccess": true, + "isDefinition": true + }, + { + "textSpan": { + "start": 67, + "length": 1 + }, + "fileName": "/b.js", + "isWriteAccess": false, + "isDefinition": false + } + ] + } +] \ No newline at end of file diff --git a/tests/baselines/reference/findAllRefs_importType_js.3.baseline.jsonc b/tests/baselines/reference/findAllRefs_importType_js.3.baseline.jsonc new file mode 100644 index 00000000000..b949b88ae8b --- /dev/null +++ b/tests/baselines/reference/findAllRefs_importType_js.3.baseline.jsonc @@ -0,0 +1,148 @@ +// === /a.js === +// /*FIND ALL REFS*/module.exports = class C {}; +// module.exports.D = class [|D|] {}; + +// === /b.js === +// /** @type {import("./a")} */ +// const x = 0; +// /** @type {import("./a").[|D|]} */ +// const y = 0; + +[ + { + "definition": { + "containerKind": "", + "containerName": "", + "fileName": "/a.js", + "kind": "local class", + "name": "(local class) D", + "textSpan": { + "start": 54, + "length": 1 + }, + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "local class", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "D", + "kind": "className" + } + ], + "contextSpan": { + "start": 48, + "length": 10 + } + }, + "references": [ + { + "textSpan": { + "start": 54, + "length": 1 + }, + "fileName": "/a.js", + "contextSpan": { + "start": 48, + "length": 10 + }, + "isWriteAccess": true, + "isDefinition": true + } + ] + }, + { + "definition": { + "containerKind": "", + "containerName": "", + "fileName": "/a.js", + "kind": "alias", + "name": "(alias) (local class) D\nimport D", + "textSpan": { + "start": 44, + "length": 1 + }, + "displayParts": [ + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "alias", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "(", + "kind": "punctuation" + }, + { + "text": "local class", + "kind": "text" + }, + { + "text": ")", + "kind": "punctuation" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "D", + "kind": "aliasName" + }, + { + "text": "\n", + "kind": "lineBreak" + }, + { + "text": "import", + "kind": "keyword" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "D", + "kind": "aliasName" + } + ], + "contextSpan": { + "start": 29, + "length": 16 + } + }, + "references": [ + { + "textSpan": { + "start": 67, + "length": 1 + }, + "fileName": "/b.js", + "isWriteAccess": false, + "isDefinition": false + } + ] + } +] \ No newline at end of file diff --git a/tests/baselines/reference/findAllRefs_importType_js.baseline.jsonc b/tests/baselines/reference/findAllRefs_importType_js.baseline.jsonc new file mode 100644 index 00000000000..f87ee79a297 --- /dev/null +++ b/tests/baselines/reference/findAllRefs_importType_js.baseline.jsonc @@ -0,0 +1,80 @@ +// === /a.js === +// /*FIND ALL REFS*/[|module|].exports = class C {}; +// module.exports.D = class D {}; + +// === /b.js === +// /** @type {import("[|./a|]")} */ +// const x = 0; +// /** @type {import("[|./a|]").D} */ +// const y = 0; + +[ + { + "definition": { + "containerKind": "", + "containerName": "", + "fileName": "/a.js", + "kind": "module", + "name": "module \"/a\"", + "textSpan": { + "start": 0, + "length": 59 + }, + "displayParts": [ + { + "text": "module", + "kind": "keyword" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "\"/a\"", + "kind": "stringLiteral" + } + ] + }, + "references": [ + { + "textSpan": { + "start": 19, + "length": 3 + }, + "fileName": "/b.js", + "contextSpan": { + "start": 4, + "length": 21 + }, + "isWriteAccess": false, + "isDefinition": false + }, + { + "textSpan": { + "start": 61, + "length": 3 + }, + "fileName": "/b.js", + "contextSpan": { + "start": 46, + "length": 23 + }, + "isWriteAccess": false, + "isDefinition": false + }, + { + "textSpan": { + "start": 0, + "length": 6 + }, + "fileName": "/a.js", + "contextSpan": { + "start": 0, + "length": 28 + }, + "isWriteAccess": false, + "isDefinition": false + } + ] + } +] \ No newline at end of file diff --git a/tests/cases/fourslash/findAllRefsForImportCall.ts b/tests/cases/fourslash/findAllRefsForImportCall.ts new file mode 100644 index 00000000000..c23bb0ac810 --- /dev/null +++ b/tests/cases/fourslash/findAllRefsForImportCall.ts @@ -0,0 +1,19 @@ +/// + +// @Filename: /app.ts +//// export function he/**/llo() {}; + +// @Filename: /re-export.ts +//// export const services = { app: setup(() => import('./app')) } +//// function setup(importee: () => Promise): T { return {} as any } + +// @Filename: /indirect-use.ts +//// import("./re-export").then(mod => mod.services.app.hello()); + +// @Filename: /direct-use.ts +//// async function main() { +//// const mod = await import("./app") +//// mod.hello(); +//// } + +verify.baselineFindAllReferences(""); diff --git a/tests/cases/fourslash/findAllRefsForImportCallType.ts b/tests/cases/fourslash/findAllRefsForImportCallType.ts new file mode 100644 index 00000000000..1c5505afbc3 --- /dev/null +++ b/tests/cases/fourslash/findAllRefsForImportCallType.ts @@ -0,0 +1,14 @@ +/// + +// @Filename: /app.ts +//// export function he/**/llo() {}; + +// @Filename: /re-export.ts +//// export type app = typeof import("./app") + +// @Filename: /indirect-use.ts +//// import type { app } from "./re-export"; +//// declare const app: app +//// app.hello(); + +verify.baselineFindAllReferences(""); diff --git a/tests/cases/fourslash/findAllRefs_importType_js.1.ts b/tests/cases/fourslash/findAllRefs_importType_js.1.ts new file mode 100644 index 00000000000..f068d94ce14 --- /dev/null +++ b/tests/cases/fourslash/findAllRefs_importType_js.1.ts @@ -0,0 +1,17 @@ +/// + +// @allowJs: true +// @checkJs: true + +// @Filename: /a.js +////module.exports = class /**/C {}; +////module.exports.D = class D {}; + +// @Filename: /b.js +/////** @type {import("./a")} */ +////const x = 0; +/////** @type {import("./a").D} */ +////const y = 0; + +verify.noErrors(); +verify.baselineFindAllReferences(""); \ No newline at end of file diff --git a/tests/cases/fourslash/findAllRefs_importType_js.2.ts b/tests/cases/fourslash/findAllRefs_importType_js.2.ts new file mode 100644 index 00000000000..56087ae4d0d --- /dev/null +++ b/tests/cases/fourslash/findAllRefs_importType_js.2.ts @@ -0,0 +1,17 @@ +/// + +// @allowJs: true +// @checkJs: true + +// @Filename: /a.js +////module.exports = class C {}; +////module.exports./**/D = class D {}; + +// @Filename: /b.js +/////** @type {import("./a")} */ +////const x = 0; +/////** @type {import("./a").D} */ +////const y = 0; + +verify.noErrors(); +verify.baselineFindAllReferences(""); \ No newline at end of file diff --git a/tests/cases/fourslash/findAllRefs_importType_js.3.ts b/tests/cases/fourslash/findAllRefs_importType_js.3.ts new file mode 100644 index 00000000000..63852ba8548 --- /dev/null +++ b/tests/cases/fourslash/findAllRefs_importType_js.3.ts @@ -0,0 +1,17 @@ +/// + +// @allowJs: true +// @checkJs: true + +// @Filename: /a.js +////module.exports = class C {}; +////module.exports.D = class /**/D {}; + +// @Filename: /b.js +/////** @type {import("./a")} */ +////const x = 0; +/////** @type {import("./a").D} */ +////const y = 0; + +verify.noErrors(); +verify.baselineFindAllReferences(""); \ No newline at end of file diff --git a/tests/cases/fourslash/findAllRefs_importType_js.ts b/tests/cases/fourslash/findAllRefs_importType_js.ts index 6802f055d40..41e3965d727 100644 --- a/tests/cases/fourslash/findAllRefs_importType_js.ts +++ b/tests/cases/fourslash/findAllRefs_importType_js.ts @@ -4,40 +4,14 @@ // @checkJs: true // @Filename: /a.js -////[|[|{| "contextRangeIndex": 0 |}module|].exports = [|class [|{| "isWriteAccess": true, "isDefinition": true, "contextRangeIndex": 2 |}C|] {}|];|] -////[|module.exports.[|{| "isWriteAccess": true, "isDefinition": true, "contextRangeIndex": 4 |}D|] = [|class [|{| "isWriteAccess": true, "isDefinition": true, "contextRangeIndex": 6 |}D|] {}|];|] +/////**/module.exports = class C {}; +////module.exports.D = class D {}; // @Filename: /b.js -/////** [|@type {import("[|{| "contextRangeIndex": 8 |}./a|]")}|] */ +/////** @type {import("./a")} */ ////const x = 0; -/////** [|@type {import("[|{| "contextRangeIndex": 10 |}./a|]").[|D|]}|] */ +/////** @type {import("./a").D} */ ////const y = 0; verify.noErrors(); - -// TODO: GH#24025 - -const [rModuleDef, rModule, r0Def, r0, r1Def, r1, r2Def, r2, r3Def, r3, r4Def, r4, r5] = test.ranges(); -verify.referenceGroups([r3, r4], [ - { definition: 'module "/a"', ranges: [r4, rModule] }, - { definition: "(local class) C", ranges: [r0] }, - { definition: "(alias) (local class) export=\nimport export=", ranges: [r3] }, -]); -verify.referenceGroups(rModule, [{ definition: 'module "/a"', ranges: [r3, r4, rModule] }]); -verify.referenceGroups(r0, [ - { definition: "(local class) C", ranges: [r0] }, - // TODO: This definition is really ugly - { definition: "(alias) (local class) export=\nimport export=", ranges: [r3] }, -]); -verify.referenceGroups([r1, r5], [ - { definition: "(alias) (local class) D\nimport D", ranges: [r1, r5] }, -]); -verify.referenceGroups(r2, [ - { definition: "(local class) D", ranges: [r2] }, - { definition: "(alias) (local class) D\nimport D", ranges: [r5] }, -]); -verify.referenceGroups([r3, r4], [ - { definition: 'module "/a"', ranges: [r4, rModule] }, - //{ definition: "(local class) C", ranges: [r0] }, - //{ definition: "(alias) (local class) export=\nimport export=", ranges: [r3] }, -]); +verify.baselineFindAllReferences(""); \ No newline at end of file