How to Track TypeScript API Usage with AST Analysis and Custom Loaders
This article explains how to replace inefficient global searches for third‑party APIs with a TypeScript‑based solution that parses source files into an AST, traverses import declarations, distinguishes import styles, records call counts and line numbers, and overcomes symbol resolution challenges using the TypeScript compiler API.
Why Global Search Fails for Plugin Usage
Searching the whole codebase for a specific plugin or third‑party package by global text search is slow and error‑prone, especially when trying to determine whether a cookie API is being called and how many times.
Building an AST with the TypeScript Compiler
By using the TypeScript compiler ( ts‑compiler) we can convert a source file into an Abstract Syntax Tree (AST) and then analyze it programmatically.
// test.ts
import { cookie } from 'sheer';
const dataLen = 3;
let name = 'iceman';
if (cookie) {
console.log(name);
}
function getInfos(info: string) {
const result = cookie.get(info);
return result;
}The AST contains nodes such as ImportDeclaration, VariableStatement, IfStatement, and FunctionDeclaration, each with its own position information.
Analyzing Import Declarations
We traverse the AST with tsCompiler.forEachChild and look for nodes where tsCompiler.isImportDeclaration(node) is true. For each import we examine the importClause and moduleSpecifier to determine the import type:
If importClause.name exists without namedBindings, it is a default import.
If namedBindings is a NamespaceImport, it is a namespace import (e.g., * as cookie).
If namedBindings is a NamedImports, we iterate its elements to handle named imports and possible aliasing with as.
For each import we record its name, origin module, and the start/end positions of both the import symbol and the identifier.
function _findImportItems(ast: any, baseLine = 0) {
let importItems = {};
function dealImports(temp: any) {
importItems[temp.name] = {
origin: temp.origin,
symbolPos: temp.symbolPos,
symbolEnd: temp.symbolEnd,
identifierPos: temp.identifierPos,
identifierEnd: temp.identifierEnd,
line: temp.line
};
}
function walk(node: any) {
tsCompiler.forEachChild(node, walk);
const line = ast.getLineAndCharacterOfPosition(node.getStart()).line + baseLine + 1;
if (tsCompiler.isImportDeclaration(node)) {
if (node.moduleSpecifier?.text === "sheer") {
if (node.importClause) {
if (node.importClause.name) {
dealImports({
name: node.importClause.name.escapedText,
origin: null,
symbolPos: node.importClause.pos,
symbolEnd: node.importClause.end,
identifierPos: node.importClause.name.pos,
identifierEnd: node.importClause.name.end,
line
});
}
if (node.importClause.namedBindings) {
if (tsCompiler.isNamedImports(node.importClause.namedBindings)) {
node.importClause.namedBindings.elements?.forEach((element: any) => {
if (tsCompiler.isImportSpecifier(element)) {
dealImports({
name: element.name.escapedText,
origin: element.propertyName?.escapedText ?? null,
symbolPos: element.pos,
symbolEnd: element.end,
identifierPos: element.name.pos,
identifierEnd: element.name.end,
line
});
}
});
}
if (tsCompiler.isNamespaceImport(node.importClause.namedBindings) && node.importClause.namedBindings.name) {
dealImports({
name: node.importClause.namedBindings.name.escapedText,
origin: "*",
symbolPos: node.importClause.namedBindings.pos,
symbolEnd: node.importClause.namedBindings.end,
identifierPos: node.importClause.namedBindings.name.pos,
identifierEnd: node.importClause.namedBindings.name.end,
line
});
}
}
}
}
}
walk(ast);
}
walk(ast);
return importItems;
}Resolving API Calls
After collecting import information we walk the AST again, looking for identifier nodes whose name matches an imported API. We skip identifiers that belong to the original import declaration by comparing their pos and end with the recorded identifier positions.
Using the type checker ( program.getTypeChecker()) we retrieve the symbol for each identifier and verify that its declaration matches the import symbol, ensuring we only count real usages.
function _dealAST(importItems: any, ast: any, checker: any, baseLine = 0) {
const importNames = Object.keys(importItems);
function walk(node: any) {
tsCompiler.forEachChild(node, walk);
const line = ast.getLineAndCharacterOfPosition(node.getStart()).line + baseLine + 1;
if (tsCompiler.isIdentifier(node) && importNames.includes(node.escapedText)) {
const imp = importItems[node.escapedText];
if (node.pos !== imp.identifierPos && node.end !== imp.identifierEnd) {
const symbol = checker.getSymbolAtLocation(node);
if (symbol && symbol.declarations?.length > 0) {
const decl = symbol.declarations[0];
if (imp.symbolPos === decl.pos && imp.symbolEnd === decl.end) {
// Here we have a real usage of the imported API
// Additional analysis (e.g., property access depth) can be performed
console.log('Usage', node.escapedText, 'at line', line);
}
}
}
}
}
walk(ast);
}Putting It All Together
We expose a helper parseTs(fileName) that creates a program, obtains the AST and the type checker, and then run _findImportItems followed by _dealAST to collect call counts and line numbers for a target API such as cookie.
function parseTs(fileName: string) {
const program = tsCompiler.createProgram([fileName], {});
const ast = program.getSourceFile(fileName);
const checker = program.getTypeChecker();
return { ast, checker };
}
const { ast, checker } = parseTs("./test.ts");
const importItems = _findImportItems(ast);
_dealAST(importItems, ast, checker);The article also discusses remaining challenges, such as distinguishing between chained and direct calls, handling same‑name local variables, and dealing with undefined symbols when parsing virtual files.
By integrating these steps into a custom Webpack loader or plugin, developers can automatically generate a visual platform that shows how many times and where a specific third‑party API is used across a project.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
