Repository: kythe/kythe
Pull request #5514 (2023-02-04 02:22:44)

feat(typescript_indexer): support dynamic imports (ref to modules only)
This is a partial support for dynamic imports. In example like 

```typescript
const {StuffDoer} = await import('./module');
```
`./module` is now referencing the file module.ts. But `StuffDoer` isn't yet referencing the original definition.


Patch:
diff --git a/kythe/typescript/indexer.ts b/kythe/typescript/indexer.ts
index 68db5702d..b59899ef0 100644
--- a/kythe/typescript/indexer.ts
+++ b/kythe/typescript/indexer.ts
@@ -920,6 +920,14 @@ class Visitor {
     ts.forEachChild(node, n => {
       this.visit(n);
     });
+
+    // Special case dynamic imports as they are represendted as CallExpressions.
+    // We don't want to emit ref/call as we don't have anything to point it at:
+    // there is no import() function
+    if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
+      this.visitDynamicImportCall(node);
+      return;
+    }
     const callAnchor = this.newAnchor(node);
     const symbol = this.host.getSymbolAtLocation(node.expression);
     if (!symbol) {
@@ -1301,6 +1309,42 @@ class Visitor {
     }
   }
 
+  /**
+   * Emits `ref/import` edge from a module name to its definition.
+   * It is used by static and dynamic imports:
+   *
+   * import {Bar} from './foo'; // static
+   *
+   * const {Bar} = await import('./foo'); // dynamic
+   *
+   * @param moduleRef Module reference. E.g. './foo' from the examples above.
+   * @returns true if corresponding module was found and an edge was emitted.
+   */
+  emitRefToImportedModule(moduleRef: ts.Node): boolean {
+    const moduleSym = this.host.getSymbolAtLocation(moduleRef);
+    if (!moduleSym) {
+      // This can occur when the module failed to resolve to anything.
+      // See testdata/import_missing.ts for more on how that could happen.
+      return false;
+    }
+    const modulePath = this.getModulePathFromModuleReference(moduleSym);
+    if (modulePath) {
+      const kModule = this.newVName('module', modulePath);
+      this.emitEdge(this.newAnchor(moduleRef), EdgeKind.REF_IMPORTS, kModule);
+    } else {
+      // Check if module being imported is declared via `declare module`
+      // and if so - output ref to that statement.
+      const decl = moduleSym.valueDeclaration;
+      if (decl && ts.isModuleDeclaration(decl)) {
+        const kModule =
+            this.host.getSymbolName(moduleSym, TSNamespace.NAMESPACE);
+        if (!kModule) return false;
+        this.emitEdge(this.newAnchor(moduleRef), EdgeKind.REF_IMPORTS, kModule);
+      }
+    }
+    return true;
+  }
+
   /** visitImportDeclaration handles the various forms of "import ...". */
   visitImportDeclaration(decl: ts.ImportDeclaration|
                          ts.ImportEqualsDeclaration) {
@@ -1323,27 +1367,10 @@ class Visitor {
           decl.moduleReference.expression :
           decl.moduleReference;
     }
-    const moduleSym = this.host.getSymbolAtLocation(moduleRef);
-    if (!moduleSym) {
-      // This can occur when the module failed to resolve to anything.
-      // See testdata/import_missing.ts for more on how that could happen.
+    const moduleFound = this.emitRefToImportedModule(moduleRef);
+    if (!moduleFound) {
       return;
     }
-    const modulePath = this.getModulePathFromModuleReference(moduleSym);
-    if (modulePath) {
-      const kModule = this.newVName('module', modulePath);
-      this.emitEdge(this.newAnchor(moduleRef), EdgeKind.REF_IMPORTS, kModule);
-    } else {
-      // Check if module being imported is declared via `declare module`
-      // and if so - output ref to that statement.
-      const decl = moduleSym.valueDeclaration;
-      if (decl && ts.isModuleDeclaration(decl)) {
-        const kModule =
-            this.host.getSymbolName(moduleSym, TSNamespace.NAMESPACE);
-        if (!kModule) return;
-        this.emitEdge(this.newAnchor(moduleRef), EdgeKind.REF_IMPORTS, kModule);
-      }
-    }
 
     // TODO(#4021): See discussion.
     // Pending changes, an anchor in a Code Search UI cannot currently be
@@ -1444,6 +1471,20 @@ class Visitor {
     }
   }
 
+  /**
+   * Handles dynamic imports like:
+   *
+   * const {SomeSym} = await import('./another/file');
+   *
+   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
+   *
+   * @param node import() statement. TS represents dynamic imports as CallExpressions.
+   */
+  visitDynamicImportCall(node: ts.CallExpression) {
+    const moduleRef = node.arguments[0];
+    this.emitRefToImportedModule(moduleRef);
+  }
+
   /**
    * When a file imports another file, with syntax like
    *   import * as x from 'some/path';


Test patch:
diff --git a/kythe/typescript/testdata/dynamic_import_group/import.ts b/kythe/typescript/testdata/dynamic_import_group/import.ts
new file mode 100644
index 000000000..7c0ef2acd
--- /dev/null
+++ b/kythe/typescript/testdata/dynamic_import_group/import.ts
@@ -0,0 +1,20 @@
+
+function regularFunction() {
+    //- @"'./module'" ref/imports Mod
+    import('./module').then(({StuffDoer}) => {
+        new StuffDoer().doStuff();
+    })
+
+    // Supported by JS but not working in TS.
+    // Make sure that TS indexer doesn't crash.
+    import('./modu' + 'le').then(({StuffDoer}) => {
+        new StuffDoer().doStuff();
+    })
+}
+
+async function asyncFunction() {
+    //- @"'./module'" ref/imports Mod
+    const {StuffDoer} = await import('./module');
+    new StuffDoer().doStuff();
+}
+
diff --git a/kythe/typescript/testdata/dynamic_import_group/module.ts b/kythe/typescript/testdata/dynamic_import_group/module.ts
new file mode 100644
index 000000000..d8692fa92
--- /dev/null
+++ b/kythe/typescript/testdata/dynamic_import_group/module.ts
@@ -0,0 +1,5 @@
+//- Mod=vname("module", _, _, "testdata/dynamic_import_group/module", _).node/kind record
+
+export class StuffDoer {
+    doStuff() {}
+}
\ No newline at end of file