Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I use the TS Compiler API to find where a variable was defined in another file

given:

// foo.ts
import { bar } from "./bar"

// bar.ts
export const bar = 3;

If I have a ts.Symbol for the bar in "foo.ts", how can I get to the bar in "bar.ts"?

Ideally, TS compiler API would expose a definition-use chain that I can traverse to find the definition. I don't think it does, though.

So now I'm trying to:

  • use the module specifier "./bar.ts" and the current ts.SourceFile to get a ts.ResolvedModule object representing "bar.ts", which contains the full file path.
  • Do ts.SourceFile(fullFilePath) to get a ts.SourceFile for "bar.ts"
  • Do checker.getExportsOfModule(symbolForBarDotTs) to get the exports from "bar.ts" and find the one with a matching name.

The tricky part seems to be resolving the module from the module specifier. I don't want to write the logic for module resolution from scratch because the algorithm is complex and depends on the interaction of at least six compiler options. Two parts of the TS Compiler API seemed promising:

  • host.resolveModuleNames, which is unfortunately only available if the host has implemented it, and the default compiler host does not implement it.
  • use (program.getSourceFile(pathToFoo) as any).resolvedModules. The resolvedModules property seems to have exactly what I'm looking for, but is not part of the public API.

Is there a better way? I'm hoping to:

  • stop using non-private API
  • stop doing so much work that the compiler already knows how to do

    "bar.ts"

In this case, it is easy to see that "./bar" refers to the adjacent source file on the file system with a matching name, but when someone uses "paths" or "node_modules" or "@types", etc. then module resolution is non-trivial.

Update

For the general question of:

If I have a ts.Symbol for the bar in "foo.ts", how can I get to the bar in "bar.ts"?

@DavidSherret's answer will work most of the time.

However, it doesn't do what I'm looking for in the following case:

// foo.ts
import { bar } from "./bar"

// bar.ts
export { bar } from "./baz"

// baz.ts
export const bar = 3;

TypeChecker#getAliasedSymbol says that baz in "foo.ts" points to bar in "baz.ts", skipping "bar.ts" entirely. This worn't work for my purposes, because I'm trying to find out, given a set of entrypoints, which parts of .d.ts files are no longer needed, and remove the unneeded parts. In this case it would be a bad idea to remove "bar.ts".

like image 507
Max Heiber Avatar asked Oct 16 '22 07:10

Max Heiber


1 Answers

The named import's symbol will have an associated "aliased symbol", which represents the declaration. So to get the variable declaration's symbol you can use the TypeChecker#getAliasedSymbol method, then from that get the declaration.

For example:

const barNamedImportSymbol = typeChecker.getSymbolAtLocation(barNamedImport.name)!;
const barSymbol = typeChecker.getAliasedSymbol(barNamedImportSymbol);
const barDeclaration = barSymbol.declarations[0] as ts.VariableDeclaration;

console.log(barDeclaration.getText(barFile)); // outputs `bar = 3`

The import declaration's named import has a separate symbol because that's the symbol specific to the "foo.ts" file.

Update: Getting symbol of file referenced in module specifier

To get the symbol of a file referenced in an import or export declarations module specifier, you can get the symbol of the module specifier node:

const otherFileSymbol = typeChecker.getSymbolAtLocation(importDeclaration.moduleSpecifier)!;

From there, you can check its exports for a certain name:

const barSymbol = otherFileSymbol.exports!.get(ts.escapeLeadingUnderscores("bar"))!;
// outputs: export { bar } from "./baz"; in second example above
console.log(barSymbol.declarations[0].parent.parent.getText());
like image 196
David Sherret Avatar answered Jan 04 '23 07:01

David Sherret