typescripttypescript-compiler-api

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:

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:

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

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".


Solution

  • 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());