SDK Reference
Tern provides a TypeScript SDK embedded in the CLI to quickly compute attributes of hits in your migration. It can be used to explore codeowners, file complexity, cross-file dependencies, type information, and more.
Overview
A few examples:
// Count imports in a file
return typescriptImports().length;
// Check if this is a test file
return filename().includes(".test.") || filename().includes(".spec.");
// Follow an import to another file
const resolved = resolveTypescriptImport("./utils");
const handle = at(resolved.resolvedPath);
return handle.typescriptExports().length;
// Get type info from the language server
const info = await at(filepath()).getType({ line: 10, pos: 0 });
return info.detail;Context
Every script runs with an implicit context:
filepath: Absolute path to the filelineStart: Starting line (1-indexed), or undefined for whole filelineEnd: Ending line (1-indexed), or undefined for whole filecolumn: Access values from other columns on the same row
column(nameOrId)
Returns the value of another column on the same row, looked up by column name or column ID.
column("Language");
// → "TypeScript"
// Use it to branch logic based on another column's value
const lang = column("Language");
if (lang === "Python") return pythonImports().length;
if (lang === "TypeScript") return typescriptImports().length;
return null;Path Functions
Pure string operations on the file path. Always available.
| Function | Returns | Description |
|---|---|---|
filepath() |
string |
Absolute file path |
directory() |
string |
Directory containing the file |
filename() |
string |
File name with extension |
extension() |
string |
File extension (e.g., .tsx) |
filepath(); // → "/project/src/components/Button.tsx"
directory(); // → "/project/src/components"
filename(); // → "Button.tsx"
extension(); // → ".tsx"File Content Functions
Read from the filesystem.
| Function | Returns | Description |
|---|---|---|
fileContent() |
string | null |
Entire file content |
lineContent() |
string | null |
Content for context line range |
Line Range Functions
| Function | Returns | Description |
|---|---|---|
lineStart() |
number | undefined |
Starting line (1-indexed) from context |
lineEnd() |
number | undefined |
Ending line (1-indexed) from context |
Git Functions
Shell out to git. Return null if not in a git repo.
| Function | Returns | Description |
|---|---|---|
codeowners() |
string[] |
CODEOWNERS entries matching this file |
gitBlame() |
BlameInfo[] | null |
Blame info per line |
gitLastModified() |
Date | null |
Last modification date |
codeowners();
// → ["@frontend-platform", "@design-systems"]
gitLastModified();
// → Date("2025-12-23T17:29:50.000Z")
// Days since last modified:
const d = gitLastModified();
return d ? Math.floor((Date.now() - d.getTime()) / 86400000) : null;TypeScript Functions
Parse TypeScript/JavaScript files (.ts, .tsx, .js, .jsx, .mjs, .cjs). Return empty arrays for non-TS/JS files.
| Function | Returns | Description |
|---|---|---|
typescriptImports() |
TypeScriptImport[] |
All imports in the file |
typescriptSymbols() |
TypeScriptSymbol[] |
All symbols (functions, classes, types, etc.) |
typescriptExports() |
TypeScriptExport[] |
All exports from the file |
typescriptCalls(filter?) |
TypeScriptCall[] |
All function/method calls (filter: {method?, func?}) |
resolveTypescriptImport(source) |
ResolvedTypeScriptImport |
Resolve an import specifier to a file path |
resolveTypescriptImports() |
ResolvedTypeScriptImport[] |
Resolve all imports in the file |
typescriptImports();
// → [
// {source: "react", specifiers: [{name: "useState"}], default: "React", line: 1},
// {source: "./utils", specifiers: [{name: "formatDate", alias: "format"}], line: 2}
// ]
typescriptSymbols();
// → [
// {name: "Button", kind: "function", line: 14, endLine: 45, exported: true},
// {name: "ButtonProps", kind: "interface", line: 5, endLine: 12, exported: true}
// ]
typescriptExports();
// → [
// {name: "Button", kind: "function", line: 14},
// {name: "default", kind: "default", line: 100, originalName: "App"}
// ]
// Resolve where an import points to
resolveTypescriptImport("./utils");
// → {source: "./utils", resolvedPath: "/project/src/utils.ts", isExternal: false, isNotFound: false}
// Resolve all imports at once
resolveTypescriptImports();
// → [{source: "react", resolvedPath: null, isExternal: true, isNotFound: false}, ...]Scope Analysis Functions
Navigate the AST to find enclosing scopes. Available for TypeScript/JavaScript, Python, and Go files.
| Function | Returns | Description |
|---|---|---|
enclosingScope() |
ScopeEntry[] |
Full scope stack for the current line, outermost first |
enclosingComponent() |
string | null |
Nearest exported PascalCase function (React component) |
enclosingNamedFunction() |
string | null |
Nearest named function, skipping anonymous callbacks |
enclosingClass() |
string | null |
Enclosing class name, if any |
scopeDepth() |
number |
Depth of nesting (how many scopes contain the line) |
namedScopeAt(n) |
string | null |
Nth named scope from outermost (0-indexed) |
All scope functions accept an optional line parameter (1-indexed). If not provided, they default to lineStart() from context.
// Given this code structure:
// 1: export function BenefitsList() {
// 2: const formatIcon = (item) => {
// 3: return items.map((x) => {
// 4: // lineStart = 4
// 5: });
// 6: };
// 7: }
enclosingScope();
// → [
// {name: "BenefitsList", kind: "function", line: 1, endLine: 7, exported: true},
// {name: "formatIcon", kind: "arrow_function", line: 2, endLine: 6, exported: false},
// {name: null, kind: "arrow_function", line: 3, endLine: 5, exported: false}
// ]
enclosingComponent();
// → "BenefitsList"
enclosingNamedFunction();
// → "formatIcon" (skips the anonymous .map callback)
scopeDepth();
// → 3
namedScopeAt(0);
// → "BenefitsList" (outermost named scope)
namedScopeAt(1);
// → "formatIcon" (next level)Python Functions
Parse Python files (.py, .pyi, .pyw). Return empty arrays for non-Python files.
| Function | Returns | Description |
|---|---|---|
pythonImports() |
PythonImport[] |
All imports in the file |
pythonSymbols() |
PythonSymbol[] |
All symbols (functions, classes, variables) |
pythonCalls(filter?) |
PythonCall[] |
All function/method calls (filter: {method?, func?}) |
resolvePythonImport(module) |
ResolvedPythonImport |
Resolve a module to a file path |
resolvePythonImports() |
ResolvedPythonImport[] |
Resolve all imports in the file |
pythonImports();
// → [
// {module: "os", names: [], isFrom: false, line: 1},
// {module: "typing", names: [{name: "List"}, {name: "Dict"}], isFrom: true, line: 2}
// ]
pythonSymbols();
// → [
// {name: "MyClass", kind: "class", line: 5, endLine: 18, private: false},
// {name: "process_data", kind: "function", line: 20, endLine: 32, private: false, decorators: ["staticmethod"]}
// ]
resolvePythonImport("django.http");
// → {module: "django.http", resolvedPath: null, isExternal: true, isPackage: false, isNotFound: false}Go Functions
Parse Go files (.go). Return empty arrays for non-Go files.
| Function | Returns | Description |
|---|---|---|
goImports() |
GoImport[] |
All imports in the file |
goSymbols() |
GoSymbol[] |
All symbols (functions, types, structs, etc.) |
goCalls(filter?) |
GoCall[] |
All function/method calls (filter: {method?, func?}) |
resolveGoImport(importPath) |
ResolvedGoImport |
Resolve an import to a directory path |
resolveGoImports() |
ResolvedGoImport[] |
Resolve all imports in the file |
goImports();
// → [
// {path: "fmt", line: 3},
// {path: "github.com/pkg/errors", alias: "errors", line: 4}
// ]
goSymbols();
// → [
// {name: "MyStruct", kind: "struct", line: 10, endLine: 15, exported: true},
// {name: "ProcessData", kind: "function", line: 20, endLine: 30, exported: true},
// {name: "String", kind: "method", line: 40, endLine: 45, exported: true, receiver: "*MyStruct"}
// ]
goCalls({ method: "Println" });
// → [
// {callee: "fmt.Println", object: "fmt", method: "Println", line: 25, args: [...]}
// ]
resolveGoImport("github.com/pkg/errors");
// → {importPath: "github.com/pkg/errors", resolvedDir: null, isStdlib: false, isExternal: true, isNotFound: false}Call Graph
Trace function calls across files. Available for all three languages: typescriptCallGraph, pythonCallGraph, goCallGraph.
The call graph starts from a function and follows calls outward across files, returning every function reached during traversal.
Basic usage
// Get all functions called (directly or transitively) from the current function
typescriptCallGraph();
// → [
// {file: "/project/src/utils.ts", func: "formatDate", count: 2},
// {file: "/project/src/api.ts", func: "fetchUser", count: 1}
// ]
// Start from a specific function
typescriptCallGraph({ func: "handleSubmit" });
// Limit traversal depth (1-3, default 2)
typescriptCallGraph({ func: "main", depth: 3 });Filtering
Pass a filter object or predicate to get back a count instead of the full array.
// Count functions in a specific directory
typescriptCallGraph({ file: "src/utils" });
// → 5
// Count with a predicate
typescriptCallGraph((entry) => entry.func.startsWith("use"));
// → 3Finding call chains
*FindCallChains does a BFS from start points until an end condition is met, useful for answering “can function A reach function B?”
typescriptFindCallChains(
[{ file: "/project/src/api.ts", func: "handleRequest" }],
{
endCondition: (entry) => entry.func.includes("database"),
maxExplorations: 100,
},
);
// → [{file: "/project/src/db.ts", func: "queryDatabase"}]Cross-File Analysis (at)
at() creates a scoped handle to another file. All SDK functions called on the returned handle operate on that file instead of the current row’s file.
import { at, typescriptImports, resolveTypescriptImport } from "tern-sdk";
export default async () => {
// Analyze another file with tree-sitter
const other = at("src/utils.ts");
other.typescriptSymbols();
other.fileContent();
// Follow an import to its source
const imp = typescriptImports().find((i) => i.source === "./Button");
if (!imp) return null;
const resolved = resolveTypescriptImport(imp.source);
if (!resolved.resolvedPath) return null;
const source = at(resolved.resolvedPath);
return source.typescriptExports().length;
};What at() accepts
at() accepts a file path string, or any object with a .file property — results from call graphs, references, and import resolution all work directly.
// String path (absolute or repo-relative)
at("src/utils.ts");
// Call graph entry
const entries = typescriptCallGraph();
const handle = at(entries[0]);
// Reference (from definition(), references(), etc.)
const def = await at("src/api.ts").definition({ line: 10, pos: 5 });
const target = at(def);Available on file handles
Every FileHandle exposes:
file(readonly) — absolute path to the target filecursor(readonly) — 0-indexed cursor position, if the target carried one- All sync SDK methods (
fileContent,typescriptSymbols,pythonImports,enclosingScope, etc.) - All language server methods (
getType,definition,references,inboundCalls,outboundCalls) — see below
Language Server
Language server methods provide IDE-quality code intelligence: type information, go-to-definition, find-references, and call hierarchy. These are async and available on any FileHandle created by at().
| Method | Returns | Description |
|---|---|---|
getType(cursor?) |
Promise<SymbolInfo> |
Symbol information at a code location |
definition(cursor?) |
Promise<Reference> |
Go-to-definition for the symbol at cursor |
references(cursor?) |
Promise<RefList> |
All references to the symbol at cursor |
inboundCalls(cursor?) |
Promise<InboundCalls> |
Functions that call the function at cursor |
outboundCalls(cursor?) |
Promise<OutboundCalls> |
Functions called by the function at cursor |
Each method takes an optional cursor parameter ({line, pos}, both 0-indexed). If omitted, the cursor from the at() target is used.
import { at, typescriptSymbols } from "tern-sdk";
export default async () => {
// Get type info for an exported symbol
const sym = typescriptSymbols().find(
(s) => s.exported && s.kind === "function",
);
if (!sym) return null;
const handle = at(filepath());
const info = await handle.getType({ line: sym.line - 1, pos: 0 });
return info.detail;
// → "function Button(props: ButtonProps): JSX.Element"
};// Find all references to a function across the codebase
const handle = at("src/utils.ts");
const refs = await handle.references({ line: 5, pos: 10 });
return refs.references.length;
// → 12// Who calls this function?
const handle = at("src/api.ts");
const callers = await handle.inboundCalls({ line: 20, pos: 0 });
return callers.source.map((c) => c.name);
// → ["handleSubmit", "processQueue", "runMigration"]Logging
Use log() for debugging. Output goes to stderr so it doesn’t interfere with the JSON result.
import { log, filepath } from "tern-sdk";
export default () => {
log("processing", filepath());
return codeowners();
};Types
TypeScriptImport
interface TypeScriptImport {
source: string; // Module specifier
specifiers: Array<{ name: string; alias?: string }>; // Named imports
default?: string; // Default import name
namespace?: string; // Namespace import (import * as X)
line: number; // Line number (1-indexed)
}TypeScriptSymbol
interface TypeScriptSymbol {
name: string;
kind:
| "function"
| "class"
| "variable"
| "interface"
| "type"
| "enum"
| "const";
line: number;
endLine: number;
exported: boolean;
}TypeScriptExport
interface TypeScriptExport {
name: string;
kind:
| "function"
| "class"
| "variable"
| "interface"
| "type"
| "enum"
| "const"
| "default"
| "re-export";
line: number;
originalName?: string; // Original name if aliased or default export
source?: string; // Source module if re-export
}PythonImport
interface PythonImport {
module: string; // Module being imported
names: Array<{ name: string; alias?: string }>; // Specific names imported
isFrom: boolean; // "from X import Y" style
alias?: string; // Module alias (import X as Y)
line: number;
}PythonSymbol
interface PythonSymbol {
name: string;
kind: "function" | "class" | "variable" | "async_function";
line: number;
endLine: number;
private: boolean; // Starts with _
decorators?: string[]; // Applied decorators
}GoImport
interface GoImport {
path: string; // Import path
alias?: string; // Alias if any
line: number;
}GoSymbol
interface GoSymbol {
name: string;
kind:
| "function"
| "type"
| "const"
| "var"
| "method"
| "interface"
| "struct";
line: number;
endLine: number;
exported: boolean; // Starts with uppercase
receiver?: string; // Receiver type for methods
}ScopeEntry
interface ScopeEntry {
name: string | null; // Symbol name, or null if anonymous
kind:
| "function"
| "arrow_function"
| "class"
| "method"
| "interface"
| "object_method";
line: number;
endLine: number;
exported: boolean;
}BlameInfo
interface BlameInfo {
commit: string; // Commit hash
author: string; // Author name
email: string; // Author email
date: Date; // Commit timestamp
line: string; // Line content
}TypeScriptCall
interface TypeScriptCall {
callee: string; // e.g., "console.log", "useState"
calleeType: "identifier" | "member_expression" | "call_expression";
object?: string; // For method calls: "console", "this"
method?: string; // For method calls: "log", "map"
line: number;
endLine: number;
args: Array<{ value: string; line: number }>;
}PythonCall
interface PythonCall {
callee: string; // e.g., "ti.xcom_pull", "print"
calleeType: "identifier" | "attribute" | "call" | "subscript";
object?: string; // For method calls: "ti", "foo.bar"
method?: string; // For method calls: "xcom_pull"
line: number;
endLine: number;
args: Array<{
type: "positional" | "keyword";
name?: string;
value: string;
line: number;
}>;
}GoCall
interface GoCall {
callee: string; // e.g., "fmt.Println", "myFunc"
calleeType: "identifier" | "selector_expression" | "call_expression";
object?: string; // For selectors: "fmt", "obj"
method?: string; // For selectors: "Println", "Method"
line: number;
endLine: number;
args: Array<{ value: string; line: number }>;
}ResolvedTypeScriptImport
interface ResolvedTypeScriptImport {
source: string; // Original import source
resolvedPath: string | null; // Absolute file path, or null
isExternal: boolean; // node_modules package
isNotFound: boolean; // Resolution failed
}ResolvedPythonImport
interface ResolvedPythonImport {
module: string; // Original module path
resolvedPath: string | null; // Absolute file path, or null
isExternal: boolean; // stdlib or installed package
isPackage: boolean; // Directory with __init__.py
isNotFound: boolean; // Resolution failed
}ResolvedGoImport
interface ResolvedGoImport {
importPath: string; // Original import path
resolvedDir: string | null; // Absolute directory path, or null
isStdlib: boolean; // Standard library package
isExternal: boolean; // Not in current module
isNotFound: boolean; // Resolution failed
}CallGraphEntry
interface CallGraphEntry {
file: string; // Absolute file path
func: string; // Function name
count: number; // Number of call edges in traversal
}CallChainEntry
interface CallChainEntry {
file: string; // Absolute file path
func: string; // Function name
}Language Server Types
// Cursor position (0-indexed)
interface Location {
line: number;
pos: number;
}
// A reference to a code location
interface Reference {
file: string; // Path relative to project root
source: Location;
expanse?: Range;
}
// Symbol information from the language server
interface SymbolInfo {
name: string;
kind: string; // "function", "method", "class", etc.
detail: string; // Signature or type annotation
docs?: string;
container?: string; // Enclosing symbol name
definition?: Reference;
}
// A caller or callee in the call hierarchy
interface CallEntry {
name: string;
kind: string;
detail: string;
call_site: Reference;
}
interface RefList {
references: Reference[];
}
interface InboundCalls {
source: CallEntry[]; // Functions that call this one
}
interface OutboundCalls {
target: CallEntry[]; // Functions called by this one
}Common Patterns
Is this a test file?
return filename().includes(".test.") || filename().includes(".spec.");Days since last modified
const d = gitLastModified();
return d ? Math.floor((Date.now() - d.getTime()) / 86400000) : null;Count external dependencies
return typescriptImports().filter((i) => !i.source.startsWith(".")).length;Find React components
return typescriptExports().filter(
(e) => e.kind === "function" && /^[A-Z]/.test(e.name),
);Get primary owner
const owners = codeowners();
return owners.length > 0 ? owners[0] : null;File has side effects (bare imports)
return typescriptImports().some(
(i) => i.specifiers.length === 0 && !i.default && !i.namespace,
);Group by React component
return enclosingComponent() ?? filename();Get the function containing this line
return enclosingNamedFunction();Check if inside a class
return enclosingClass() !== null;Count transitive dependencies of a function
return typescriptCallGraph().length;Does this function call anything in the database layer?
return typescriptCallGraph({ file: "src/db" }) > 0;Get the type signature of the main export
const sym = typescriptSymbols().find(
(s) => s.exported && s.kind === "function",
);
if (!sym) return null;
const info = await at(filepath()).getType({ line: sym.line - 1, pos: 0 });
return info.detail;How many files import this one?
const refs = await at(filepath()).references({ line: 0, pos: 0 });
return new Set(refs.references.map((r) => r.file)).size;Read a value from another column
return column("Owner") ?? "unowned";Caching Expensive Computations
Scripts run once per row, but the module loads only once. Code outside the default function runs once and is shared across all rows. Use this for expensive operations like loading large JSON files.
import * as fs from "fs";
import { filepath } from "tern-sdk";
// Runs ONCE when module loads
const graph = JSON.parse(fs.readFileSync("/path/to/graph.json", "utf-8"));
// Runs PER ROW
export default () => {
return graph.targets[filepath()]?.deps.length ?? 0;
};Build an index once, use it per row
import * as fs from "fs";
import { filepath } from "tern-sdk";
// Load and index once
const data = JSON.parse(fs.readFileSync("/repo/deps.json", "utf-8"));
const reverseDeps = new Map<string, string[]>();
for (const [target, info] of Object.entries(data.targets)) {
for (const dep of (info as any).deps || []) {
if (!reverseDeps.has(dep)) reverseDeps.set(dep, []);
reverseDeps.get(dep)!.push(target);
}
}
// Query per row
export default () => reverseDeps.get(filepath())?.length ?? 0;When to use this pattern
- Loading large JSON files (dependency graphs, coverage data)
- Building indexes or lookup tables
- Any computation that’s the same for all rows
The JSON file must exist before the script runs (generate via CI, bazel, etc.).
Cleaning up after batch execution
If your cached resources need cleanup (temp files, file handles, etc.), export a named teardown function. It runs once after all rows are processed.
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { filepath } from "tern-sdk";
// Accumulate results across rows
const tmpFile = path.join(os.tmpdir(), `sdk-summary-${Date.now()}.json`);
const results: Record<string, number> = {};
// Runs once after all rows complete
export const teardown = () => {
fs.writeFileSync(tmpFile, JSON.stringify(results, null, 2));
};
export default () => {
const lines = fs.readFileSync(filepath(), "utf-8").split("\n").length;
results[filepath()] = lines;
return lines;
};The teardown function is optional, runs once (not per row), can be async, and errors in it are logged but don’t affect results.
