Skip to main content
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.

Supported Languages

Semantic analysis is currently supported for JavaScript/TypeScript and Python only. For other languages, the semantic methods return no-op results (null for definitions, empty arrays for references).
LanguageProviderFeatures
JavaScript/TypeScriptoxcDefinitions, references, cross-file resolution
PythonruffDefinitions, references, cross-file resolution
Other languagesReturns null/empty (no-op)

Analysis Modes

Semantic analysis operates in two modes, each with different performance and accuracy trade-offs:

File Scope (Default)

Single-file analysis that processes symbols within the current file only. This mode is fast and requires no additional configuration. Best for:
  • Quick analysis of local variables
  • Single-file transformations
  • Dry runs and exploratory analysis
Limitations:
  • Cannot resolve cross-file imports
  • Cannot find references in other files

Workspace Scope

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

API Reference

node.definition(options?)

Get the definition location for the symbol at this node’s position.
definition(options?)
DefinitionResult | null
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.

node.references()

Find all references to the symbol at this node’s position.
references()
Array<FileReferences>
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.

root.write(content)

Write content to a file obtained via definition() or references(). This method allows cross-file editing within a single codemod execution.
write(content)
void
Writes the provided content to the file and updates the semantic provider’s cache.
// Write to a file obtained from definition() or references()
const def = node.definition();
if (def && def.root.filename() !== root.filename()) {
  const edits = [def.node.replace("newName")];
  const newContent = def.root.root().commitEdits(edits);
  def.root.write(newContent);
}
Throws an error when:
  • Called on the current file being processed (use return instead)
  • The file has no path
  • The write operation fails
You cannot call write() on the current file. For the current file, return the modified content from transform() instead.

Using Semantic Analysis

The recommended way to use semantic analysis is through workflow files. Create a workflow.yaml that references your codemod script:
version: "1"
nodes:
  transform:
    js-ast-grep:
      js_file: scripts/codemod.ts
      semantic_analysis: file
semantic_analysis
string | object
Configure semantic analysis mode. Can be:
  • "file" — Single-file analysis (default)
  • "workspace" — Workspace-wide analysis using the target path
  • { mode: "workspace", root: "./path" } — Workspace-wide with custom root
Run the workflow:
npx codemod workflow run -w /path/to/codemod/workflow.yaml -t /path/to/target
-w, --workflow
PATH
Path to the workflow YAML file.
-t, --target
PATH
Path to the target directory containing files to transform.

Via CLI Commands

For quick testing or simple use cases, you can also use the jssg CLI directly:
  • jssg run
  • jssg test
# File scope (default)
npx codemod jssg run ./scripts/codemod.ts \
  --language tsx \
  --target /path/to/target

# Workspace scope

npx codemod jssg run ./scripts/codemod.ts \
 --language tsx \
 --target /path/to/target \
 --semantic-workspace /path/to/target

--semantic-workspace
PATH
Enable workspace-wide semantic analysis using the provided path as the workspace root.

Examples

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);
}
Codemod and workflow:
export default function transform(root) {
  const rootNode = root.root();
  const currentFile = root.filename();

  // Find the function declaration we want to rename
  const funcDeclName = rootNode.find({
    rule: {
      pattern: "formatDate",
      inside: {
        pattern: "export function formatDate($$$PARAMS) { $$$BODY }",
        stopBy: {
          kind: "export_statement",
        },
      },
    },
  });

  if (!funcDeclName) return null;

  // Find all references across the workspace
  const refs = funcDeclName.references();
  const currentFileEdits = [];

  for (const fileRef of refs) {
    const edits = fileRef.nodes.map((node) => node.replace("formatDateTime"));

    if (fileRef.root.filename() === currentFile) {
      currentFileEdits.push(...edits);
    } else {
      // Write changes to other files
      const newContent = fileRef.root.root().commitEdits(edits);
      fileRef.root.write(newContent);
    }
  }

  return rootNode.commitEdits(currentFileEdits);
}

Run the workflow:
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);
}

Finding Usages of a Python Class

Analyze how a class is used across a Python project without making changes. Source codebase:
class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email
    
    def display_name(self) -> str:
        return f"{self.name} <{self.email}>"
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
}

Run the workflow:
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

Tracing External Module Imports

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

Run the workflow:
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.

Cross-File Editing with Definition Lookup

Rename a constant and update all files that import it. Source codebase:
export const API_BASE_URL = "https://api.example.com";
export const API_TIMEOUT = 5000;
export const MAX_RETRIES = 3;
Codemod and workflow:
export default function transform(root) {
  const rootNode = root.root();
  const currentFile = root.filename();

  // Find the constant declaration
  const constDeclName = rootNode.find({
    rule: {
      pattern: "API_BASE_URL",
      inside: {
        pattern: "export const API_BASE_URL = $VALUE",
        stopBy: {
          kind: "export_statement",
        },
      },
    },
  });

  if (!constDeclName) return null;

  // Find all references to API_BASE_URL
  const refs = constDeclName.references();
  const currentFileEdits = [];

  for (const fileRef of refs) {
    const edits = fileRef.nodes.map((node) => node.replace("API_ENDPOINT"));

    if (fileRef.root.filename() === currentFile) {
      currentFileEdits.push(...edits);
    } else {
      const newContent = fileRef.root.root().commitEdits(edits);
      fileRef.root.write(newContent);
    }
  }

  return rootNode.commitEdits(currentFileEdits);
}

Run the workflow:
npx codemod workflow run -w /path/to/codemod/workflow.yaml -t ./src
Result:
export const API_ENDPOINT = "https://api.example.com";
export const API_TIMEOUT = 5000;
export const MAX_RETRIES = 3;
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.

Best Practices

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.
workflow.yaml
version: "1"
nodes:
  transform:
    js-ast-grep:
      js_file: scripts/codemod.ts
      semantic_analysis: file # Fast, single-file analysis
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;
}
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"));
    }
  }
}

Troubleshooting

Possible causes:
  • No semantic analysis configured in workflow (add semantic_analysis: workspace)
  • The language isn’t supported (only JavaScript/TypeScript and Python)
  • The symbol couldn’t be resolved (external library, syntax error)
Debug steps:
const def = node.definition();
console.log("Definition:", def);
console.log("Node text:", node.text());
console.log("Node kind:", node.kind());
Possible causes:
  • Using file scope mode instead of workspace scope
  • The target file hasn’t been indexed yet
  • Import resolution failed
Solution: Ensure you’re using workspace scope mode in your workflow:
workflow.yaml
version: "1"
nodes:
  transform:
    js-ast-grep:
      js_file: scripts/codemod.ts
      semantic_analysis: workspace
Then run:
npx codemod workflow run -w /path/to/codemod/workflow.yaml -t /path/to/target
Tips:
  • Start with file scope for initial development
  • Use workspace scope only when cross-file analysis is needed
  • Consider running workflows on subsets of your codebase by targeting specific directories

Next Steps