Max Heiber
Max Heiber

Reputation: 15502

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

Upvotes: 9

Views: 2460

Answers (2)

David Sherret
David Sherret

Reputation: 106660

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

Upvotes: 8

Max Heiber
Max Heiber

Reputation: 15502

For resolving module names, use ts.resolveModuleName, as recommended on this TS issue.

Here is the signature, from typescript.d.ts:

resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedModule | undefined)[];

Alternatively, for going directly to the original definition of a symbol, see @DavidSherret's answer

Upvotes: 0

Related Questions