Find symbol definitions and references across your codebase using semantic analysis in JSSG codemods
Semantic analysis enables your codemods to understand symbol relationships in code—finding where variables are defined, tracking all references to a function, or discovering cross-file dependencies. This goes beyond pattern matching to provide IDE-like intelligence for your transformations.
Enterprise features available. For JSSG open-source, it returns no-op results.
Enterprise customers can get semantic analysis for Go, Rust, Java, C, C++, C Sharp, Ruby, PHP, Swift, Kotlin, Haskell, and Elixir. Contact us to learn more.
Workspace-wide analysis that resolves cross-file imports and finds references across your entire project. This mode requires specifying a workspace root.Best for:
Renaming symbols across files
Finding all usages of exported functions
Dependency analysis and migration codemods
Requirements:
Workspace root path must be specified
Files must be processed (indexed) before cross-file queries work
Returns an object containing the definition node, its root, and the kind of
definition, or null if not found.
interface DefinitionOptions { /** If false, stop at import statements without resolving. Default: true (in workspace scope mode) */ resolveExternal?: boolean;}interface DefinitionResult<M> { /** The AST node at the definition location */ node: SgNode<M>; /** The SgRoot for the file containing the definition */ root: SgRoot<M>; /** The kind of definition: 'local', 'import', or 'external' */ kind: "local" | "import" | "external";}
Definition kinds:
'local' — Definition is in the same file (local variable, function, class, etc.)
'import' — Definition traced to an import statement, but module couldn’t be resolved (e.g., external package)
'external' — Definition resolved to a different file in the workspace
Returns null when:
No semantic provider is configured
No symbol is found at this position
When a symbol comes from an unresolved import
(e.g., import x from "some-external-module"), definition() now returns the import statement
with kind: 'import' instead of returning null. This allows you to at least
trace the symbol back to where it was imported.
Returns an array of file references, grouped by file.
interface FileReferences<M> { /** The SgRoot for the file containing references */ root: SgRoot<M>; /** Array of SgNode objects for each reference in this file */ nodes: Array<SgNode<M>>;}
Returns empty array when:
No semantic provider is configured
No symbol is found at this position
In file scope mode, references() only searches the current file. In
workspace scope mode, it searches all indexed files in the workspace.
Path to the target directory containing files to transform.
When semantic analysis returns an SgRoot for another file, you can use root.relativeFilename() to get that file’s path relative to the active target directory.
Renaming a Utility Function Across Files (TypeScript)
This example renames formatDate to formatDateTime across a multi-file TypeScript project.Source codebase:
export function formatDate(date: Date): string { return date.toISOString().split('T')[0];}export function parseDate(str: string): Date { return new Date(str);}
import { formatDate } from "../utils/date";interface Event { title: string; date: Date;}export function EventCard({ title, date }: Event) { return ( <div className="event-card"> <h3>{title}</h3> <span>{formatDate(date)}</span> </div> );}
npx codemod workflow run -w /path/to/codemod/workflow.yaml -t ./src
Result:
export function formatDateTime(date: Date): string { return date.toISOString().split('T')[0];}export function parseDate(str: string): Date { return new Date(str);}
import { formatDateTime } from "../utils/date";interface Event { title: string; date: Date;}export function EventCard({ title, date }: Event) { return ( <div className="event-card"> <h3>{title}</h3> <span>{formatDateTime(date)}</span> </div> );}
from models.user import Userdef authenticate(username: str, password: str) -> User | None: # Verify credentials if verify_password(username, password): return User(name=username, email=f"{username}@example.com") return Nonedef create_guest() -> User: return User(name="Guest", email="guest@example.com")
from models.user import Userfrom services.auth import authenticatedef get_current_user(request) -> User: user = authenticate(request.username, request.password) if not user: raise AuthError("Invalid credentials") return user
Codemod and workflow:
export default function transform(root) { const rootNode = root.root(); // Find the User class definition const classDefName = rootNode.find({ rule: { pattern: "User", inside: { pattern: "class User: $$$BODY" } }, }); if (!classDefName) return null; const refs = classDefName.references(); // Log usage analysis console.log("=== User class usage analysis ===\n"); let totalUsages = 0; for (const fileRef of refs) { const filename = fileRef.root.filename(); console.log(`📄 ${filename}:`); for (const node of fileRef.nodes) { const line = node.range().start.line + 1; const context = node.parent()?.text().slice(0, 50) || node.text(); console.log(` Line ${line}: ${context}...`); totalUsages++; } console.log(""); } console.log(`Total: ${totalUsages} usages across ${refs.length} files`); return null; // Analysis only, no changes}
npx codemod workflow run -w /path/to/codemod/workflow.yaml -t ./
Output:
=== User class usage analysis ===📄 models/user.py: Line 1: class User:...📄 services/auth.py: Line 1: from models.user import User... Line 6: return User(name=username, email=f"{userna... Line 10: return User(name="Guest", email="guest@exa...📄 api/routes.py: Line 1: from models.user import User... Line 4: def get_current_user(request) -> User:...Total: 6 usages across 3 files
Find where external dependencies are imported when node_modules isn’t available.Source codebase:
import axios from 'axios';import { z } from 'zod';const UserSchema = z.object({ id: z.string(), name: z.string(),});export async function fetchUser(id: string) { const response = await axios.get(`/api/users/${id}`); return UserSchema.parse(response.data);}
import { useQuery } from "@tanstack/react-query";import { fetchUser } from "../api/client";export function useUser(userId: string) { return useQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId), });}
Codemod and workflow:
export default function transform(root) { const rootNode = root.root(); // Find all identifiers that might be external imports const identifiers = ["axios", "z", "useQuery"]; for (const name of identifiers) { const node = rootNode.find({ rule: { pattern: name } }); for (const node of nodes) { const def = node.definition(); if (def) { if (def.kind === "import") { // External module - couldn't resolve, but we have the import console.log(`${name}: External import`); console.log(` Import statement: ${def.node.text()}`); } else if (def.kind === "local") { console.log( `${name}: Defined locally at line ${ def.node.range().start.line + 1 }` ); } else if (def.kind === "external") { console.log(`${name}: Defined in ${def.root.filename()}`); } } } }return null;}
npx codemod workflow run -w /path/to/codemod/workflow.yaml -t ./src
Output:
axios: External import Import statement: import axios from 'axios'z: External import Import statement: import { z } from 'zod'useQuery: External import Import statement: import { useQuery } from '@tanstack/react-query'
When a symbol comes from an unresolved import (e.g., import x from "some-external-module"),
definition() returns the import statement with kind: 'import'. This allows you to trace
symbols back to their import source even when the module can’t be resolved or the semantic
mode is set to file scope.
You cannot call write() on the current file being processed. For the current
file, return the modified content from transform() instead. This ensures the
engine properly tracks and applies changes.
When you call write() on an SgRoot obtained from definition() or
references(), the semantic provider’s cache is automatically updated. This
ensures subsequent semantic queries reflect the changes.
File scope analysis is faster and doesn’t require workspace configuration. Use it when your codemod only needs to understand symbols within a single file.
Semantic analysis may return null or empty results for various reasons. Always check return values:
const def = node.definition();if (!def) { // Definition not found - could be external, unresolved, or no provider return null;}const refs = node.references();if (refs.length === 0) { // No references found return null;}
Check file ownership before editing
When processing references across files, verify you’re editing the correct file:
for (const fileRef of refs) { // Only edit the current file if (fileRef.root.filename() === root.filename()) { for (const node of fileRef.nodes) { edits.push(node.replace("newText")); } }}