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 file
  • lineStart: Starting line (1-indexed), or undefined for whole file
  • lineEnd: Ending line (1-indexed), or undefined for whole file
  • column: 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"));
// → 3

Finding 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 file
  • cursor (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.