> ## Documentation Index
> Fetch the complete documentation index at: https://docs.codemod.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Semantic Analysis

> 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.

## Supported Languages

| Language              | Provider                             | Features                                                                            |
| --------------------- | ------------------------------------ | ----------------------------------------------------------------------------------- |
| JavaScript/TypeScript | [oxc](https://oxc.rs/)               | Definitions, references, cross-file resolution                                      |
| Python                | [ruff](https://docs.astral.sh/ruff/) | Definitions, references, cross-file resolution                                      |
| Other languages       | Codemod                              | Enterprise features available.<br />For JSSG open-source, it returns no-op results. |

<Info>
  **Enterprise customers** can get semantic analysis for Go, Rust, Java, C, C++, C Sharp, Ruby, PHP, Swift, Kotlin, Haskell, and Elixir. [Contact us](https://codemod.com/contact) to learn more.
</Info>

## 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.

<ParamField path="definition(options?)" type="DefinitionResult | null">
  Returns an object containing the definition node, its root, and the kind of
  definition, or null if not found.
</ParamField>

```ts theme={null}
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

<Note>
  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.
</Note>

### `node.references()`

Find all references to the symbol at this node's position.

<ParamField path="references()" type="Array<FileReferences>">
  Returns an array of file references, grouped by file.
</ParamField>

```ts theme={null}
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

<Note>
  In file scope mode, `references()` only searches the current file. In
  workspace scope mode, it searches all indexed files in the workspace.
</Note>

### `root.write(content)`

Write content to a file obtained via `definition()` or `references()`. This method allows cross-file editing within a single codemod execution.

<ParamField path="write(content)" type="void">
  Writes the provided content to the file and updates the semantic provider's
  cache.
</ParamField>

```ts theme={null}
// 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

<Warning>
  You cannot call `write()` on the current file. For the current file, return
  the modified content from `transform()` instead.
</Warning>

## Using Semantic Analysis

### Via Workflow Configuration (Recommended)

The recommended way to use semantic analysis is through workflow files. Create a `workflow.yaml` that references your codemod script:

<CodeGroup>
  ```yaml workflow.yaml (File Scope) theme={null}
  version: "1"
  nodes:
    transform:
      js-ast-grep:
        js_file: scripts/codemod.ts
        semantic_analysis: file
  ```

  ```yaml workflow.yaml (Workspace Scope) theme={null}
  version: "1"
  nodes:
    transform:
      js-ast-grep:
        js_file: scripts/codemod.ts
        semantic_analysis: workspace
  ```

  ```yaml workflow.yaml (Custom Root) theme={null}
  version: "1"
  nodes:
    transform:
      js-ast-grep:
        js_file: scripts/codemod.ts
        semantic_analysis:
          mode: workspace
          root: ./path/to/workspace
  ```
</CodeGroup>

<ParamField path="semantic_analysis" type="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
</ParamField>

**Run the workflow:**

```bash theme={null}
npx codemod workflow run -w /path/to/codemod/workflow.yaml -t /path/to/target
```

<ParamField path="-w, --workflow" type="PATH">
  Path to the workflow YAML file.
</ParamField>

<ParamField path="-t, --target" type="PATH">
  Path to the target directory containing files to transform.
</ParamField>

<Info>
  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.
</Info>

### Via CLI Commands

For quick testing or simple use cases, you can also use the `JSSG` CLI directly:

<Tabs>
  <Tab title="jssg run">
    ```bash theme={null}
    # 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

    ```

    <ParamField path="--semantic-workspace" type="PATH">
      Enable workspace-wide semantic analysis using the provided path as the workspace root.
    </ParamField>
  </Tab>

  <Tab title="jssg test">
    ```bash theme={null}
    # File scope (default)
    npx codemod jssg test ./scripts/codemod.ts --language tsx

    # Workspace scope
    npx codemod jssg test ./scripts/codemod.ts \
      --language tsx \
      --semantic-workspace /path/to/project
    ```
  </Tab>
</Tabs>

## Examples

### Renaming a Utility Function Across Files (TypeScript)

This example renames `formatDate` to `formatDateTime` across a multi-file TypeScript project.

**Source codebase:**

<CodeGroup>
  ```typescript src/utils/date.ts theme={null}
  export function formatDate(date: Date): string {
    return date.toISOString().split('T')[0];
  }

  export function parseDate(str: string): Date {
    return new Date(str);
  }
  ```

  ```typescript src/components/EventCard.tsx theme={null}
  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>
    );
  }
  ```

  ```typescript src/pages/Dashboard.tsx theme={null}
  import { formatDate } from "../utils/date";

  export function Dashboard({ events }) {
    return (
      <div>
        <h1>Upcoming Events</h1>
        {events.map((event) => (
          <p key={event.id}>
            {event.name} - {formatDate(event.scheduledAt)}
          </p>
        ))}
      </div>
    );
  }
  ```
</CodeGroup>

**Codemod and workflow:**

<CodeGroup>
  ```typescript scripts/rename-format-date.ts theme={null}
  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);
  }

  ```

  ```yaml workflow.yaml theme={null}
  version: "1"
  nodes:
    rename-format-date:
      js-ast-grep:
        js_file: scripts/rename-format-date.ts
        semantic_analysis: workspace
  ```
</CodeGroup>

**Run the workflow:**

```bash theme={null}
npx codemod workflow run -w /path/to/codemod/workflow.yaml -t ./src
```

**Result:**

<CodeGroup>
  ```typescript src/utils/date.ts theme={null}
  export function formatDateTime(date: Date): string {
    return date.toISOString().split('T')[0];
  }

  export function parseDate(str: string): Date {
    return new Date(str);
  }
  ```

  ```tsx src/components/EventCard.tsx theme={null}
  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>
    );
  }
  ```

  ```tsx src/pages/Dashboard.tsx theme={null}
  import { formatDateTime } from "../utils/date";

  export function Dashboard({ events }) {
    return (
      <div>
        <h1>Upcoming Events</h1>
        {events.map((event) => (
          <p key={event.id}>
            {event.name} - {formatDateTime(event.scheduledAt)}
          </p>
        ))}
      </div>
    );
  }
  ```
</CodeGroup>

### Finding Usages of a Python Class

Analyze how a class is used across a Python project without making changes.

**Source codebase:**

<CodeGroup>
  ```python models/user.py theme={null}
  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}>"
  ```

  ```python services/auth.py theme={null}
  from models.user import User

  def authenticate(username: str, password: str) -> User | None:
      # Verify credentials
      if verify_password(username, password):
          return User(name=username, email=f"{username}@example.com")
      return None

  def create_guest() -> User:
      return User(name="Guest", email="guest@example.com")
  ```

  ```python api/routes.py theme={null}
  from models.user import User
  from services.auth import authenticate

  def get_current_user(request) -> User:
      user = authenticate(request.username, request.password)
      if not user:
          raise AuthError("Invalid credentials")
      return user
  ```
</CodeGroup>

**Codemod and workflow:**

<CodeGroup>
  ```typescript scripts/analyze-user-class.ts theme={null}
  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
  }

  ```

  ```yaml workflow.yaml theme={null}
  version: "1"
  nodes:
    analyze-user-class:
      js-ast-grep:
        js_file: scripts/analyze-user-class.ts
        semantic_analysis: workspace
  ```
</CodeGroup>

**Run the workflow:**

```bash theme={null}
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:**

<CodeGroup>
  ```typescript src/api/client.ts theme={null}
  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);
  }
  ```

  ```typescript src/hooks/useUser.ts theme={null}
  import { useQuery } from "@tanstack/react-query";
  import { fetchUser } from "../api/client";

  export function useUser(userId: string) {
    return useQuery({
      queryKey: ["user", userId],
      queryFn: () => fetchUser(userId),
    });
  }
  ```
</CodeGroup>

**Codemod and workflow:**

<CodeGroup>
  ```typescript scripts/trace-external-imports.ts theme={null}
  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;
  }

  ```

  ```yaml workflow.yaml theme={null}
  version: "1"
  nodes:
    trace-imports:
      js-ast-grep:
        js_file: scripts/trace-external-imports.ts
        semantic_analysis: workspace
  ```
</CodeGroup>

**Run the workflow:**

```bash theme={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'
```

<Note>
  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.
</Note>

### Cross-File Editing with Definition Lookup

Rename a constant and update all files that import it.

**Source codebase:**

<CodeGroup>
  ```typescript src/constants.ts theme={null}
  export const API_BASE_URL = "https://api.example.com";
  export const API_TIMEOUT = 5000;
  export const MAX_RETRIES = 3;
  ```

  ```typescript src/api/client.ts theme={null}
  import { API_BASE_URL, API_TIMEOUT } from "../constants";

  export const client = {
    baseURL: API_BASE_URL,
    timeout: API_TIMEOUT,
  };
  ```

  ```typescript src/config/index.ts theme={null}
  import { API_BASE_URL } from "../constants";

  export const config = {
    apiUrl: API_BASE_URL,
    debug: process.env.NODE_ENV === "development",
  };
  ```
</CodeGroup>

**Codemod and workflow:**

<CodeGroup>
  ```typescript scripts/rename-constant.ts theme={null}
  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);
  }

  ```

  ```yaml workflow.yaml theme={null}
  version: "1"
  nodes:
    rename-constant:
      js-ast-grep:
        js_file: scripts/rename-constant.ts
        semantic_analysis: workspace
  ```
</CodeGroup>

**Run the workflow:**

```bash theme={null}
npx codemod workflow run -w /path/to/codemod/workflow.yaml -t ./src
```

**Result:**

<CodeGroup>
  ```typescript src/constants.ts theme={null}
  export const API_ENDPOINT = "https://api.example.com";
  export const API_TIMEOUT = 5000;
  export const MAX_RETRIES = 3;
  ```

  ```typescript src/api/client.ts theme={null}
  import { API_ENDPOINT, API_TIMEOUT } from "../constants";

  export const client = {
    baseURL: API_ENDPOINT,
    timeout: API_TIMEOUT,
  };
  ```

  ```typescript src/config/index.ts theme={null}
  import { API_ENDPOINT } from "../constants";

  export const config = {
    apiUrl: API_ENDPOINT,
    debug: process.env.NODE_ENV === "development",
  };
  ```
</CodeGroup>

<Warning>
  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.
</Warning>

<Note>
  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.
</Note>

## Best Practices

<AccordionGroup>
  <Accordion title="Use file scope for single-file transformations">
    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.

    ```yaml workflow.yaml theme={null}
    version: "1"
    nodes:
      transform:
        js-ast-grep:
          js_file: scripts/codemod.ts
          semantic_analysis: file # Fast, single-file analysis
    ```
  </Accordion>

  <Accordion title="Handle null/empty results gracefully">
    Semantic analysis may return null or empty results for various reasons. Always check return values:

    ```ts theme={null}
    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;
    }
    ```
  </Accordion>

  <Accordion title="Check file ownership before editing">
    When processing references across files, verify you're editing the correct file:

    ```ts theme={null}
    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"));
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Semantic methods return null/empty">
    **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:**

    ```ts theme={null}
    const def = node.definition();
    console.log("Definition:", def);
    console.log("Node text:", node.text());
    console.log("Node kind:", node.kind());
    ```
  </Accordion>

  <Accordion title="Cross-file references not found">
    **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:

    ```yaml workflow.yaml theme={null}
    version: "1"
    nodes:
      transform:
        js-ast-grep:
          js_file: scripts/codemod.ts
          semantic_analysis: workspace
    ```

    Then run:

    ```bash theme={null}
    npx codemod workflow run -w /path/to/codemod/workflow.yaml -t /path/to/target
    ```
  </Accordion>

  <Accordion title="Performance issues with large workspaces">
    **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
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="API Reference" icon="book" href="/jssg/reference">
    Complete API documentation for SgNode and SgRoot methods.
  </Card>

  <Card title="Advanced Patterns" icon="code" href="/jssg/advanced">
    Learn advanced transformation techniques and best practices.
  </Card>

  <Card title="Testing" icon="flask" href="/jssg/testing">
    Test your codemods with fixtures and the test runner.
  </Card>

  <Card title="Security" icon="shield-check" href="/jssg/security">
    Understand JSSG's security model and capabilities.
  </Card>
</CardGroup>
