# Ast Grep Codemods

> ast-grep NAPI reference and patterns for the packages/codemods project. Use when working with @ast-grep/napi in schema-migration codemods or packages/codemods/ directory, writing AST queries, or debugging tree-sitter node matching.

- Skill: `warp-drive-data/ast-grep-codemods` (Agent Skill)
- Install (CLI): `npx skillmds@latest add warp-drive-data/ast-grep-codemods`
- Raw SKILL.md: https://api.skillmd.com/api/skills/warp-drive-data/ast-grep-codemods/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: warp-drive-data (https://skillmd.com/u/warp-drive-data)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/warp-drive-data/ast-grep-codemods

---


# ast-grep NAPI Reference for Codemods

This skill provides the ast-grep rule system reference used by `packages/codemods/src/schema-migration/`.
The codemods use `@ast-grep/napi` (the Node.js binding) to parse and transform TypeScript/JavaScript ASTs.

## Parsing

```typescript
import { parse, Lang, type SgNode } from '@ast-grep/napi';

const ast = parse(Lang.TypeScript, sourceCode);
const root: SgNode = ast.root();
```

## SgNode Core Methods

### Search

```typescript
// Find first match (returns null if not found)
node.find(matcher: string | number | NapiConfig): SgNode | null

// Find all matches
node.findAll(matcher: string | number | NapiConfig): SgNode[]

// Boolean checks
node.matches(pattern: string): boolean
node.inside(pattern: string): boolean
node.has(pattern: string): boolean
```

### Traversal

```typescript
node.children(): SgNode[]           // Direct children
node.parent(): SgNode | null        // Parent node
node.child(nth: number): SgNode | null
node.field(name: string): SgNode | null  // Named field (e.g., 'name', 'body', 'source')
node.ancestors(): SgNode[]
node.next(): SgNode | null          // Next sibling
node.nextAll(): SgNode[]
node.prev(): SgNode | null          // Previous sibling
node.prevAll(): SgNode[]
```

### Inspection

```typescript
node.kind(): string       // Tree-sitter node type (e.g., 'field_definition', 'class_body')
node.text(): string       // Full source text
node.isLeaf(): boolean
node.isNamed(): boolean
node.range(): Range       // { start: Pos, end: Pos } (0-indexed)
```

### Meta-variable Extraction

```typescript
// After finding with a pattern containing $VAR or $$$VARS:
node.getMatch('VAR'): SgNode | null
node.getMultipleMatches('VARS'): SgNode[]
```

### Code Editing

```typescript
const edit = node.replace('newCode');  // Returns Edit object
const newSource = root.commitEdits([edit1, edit2]);  // Apply batch edits
```

## NapiConfig Rule Object

The `find` and `findAll` methods accept a `NapiConfig` object for complex queries:

```typescript
node.findAll({
  rule: { /* rule object */ },
  constraints?: { /* meta-variable constraints */ },
})
```

## Rule Types

### 1. Atomic Rules

Match individual nodes by their properties.

#### `kind` - Match by tree-sitter node type

```typescript
// Find all class declarations
root.findAll({ rule: { kind: 'class_declaration' } })

// Common TypeScript/JavaScript kinds:
// class_declaration, class_body, field_definition, method_definition,
// import_statement, identifier, property_identifier, decorator,
// call_expression, member_expression, string, template_string
```

**Gotcha:** Not all kind names are valid in all grammars. TypeScript uses `field_definition`,
some JavaScript grammars use `public_field_definition` or `class_field`. Wrap in try/catch
when iterating over multiple possible kinds.

#### `pattern` - Match by code pattern with meta-variables

```typescript
// Simple pattern
root.findAll({ rule: { pattern: 'console.log($ARG)' } })

// Pattern with context (for ambiguous syntax like class members)
root.findAll({
  rule: {
    pattern: {
      context: 'class A { $FIELD = $INIT }',
      selector: 'field_definition',
    }
  }
})
```

Meta-variables:
- `$NAME` - matches a single AST node
- `$$NAME` - matches zero or more nodes (non-greedy)
- `$$$NAME` - matches zero or more nodes (greedy)

#### `regex` - Match node text against regex

```typescript
// Match identifiers starting with underscore
root.findAll({ rule: { kind: 'identifier', regex: '^_' } })
```

### 2. Composite Rules

Combine rules with boolean logic.

#### `all` - Every rule must match (AND)

```typescript
root.findAll({
  rule: {
    all: [
      { kind: 'call_expression' },
      { pattern: '$OBJ.$METHOD($$$ARGS)' },
    ]
  }
})
```

#### `any` - At least one rule must match (OR)

```typescript
root.findAll({
  rule: {
    any: [
      { kind: 'field_definition' },
      { kind: 'public_field_definition' },
      { kind: 'class_field' },
    ]
  }
})
```

#### `not` - Negate a rule

```typescript
// Find all identifiers that aren't 'constructor'
root.findAll({
  rule: {
    kind: 'identifier',
    not: { regex: '^constructor$' },
  }
})
```

#### `matches` - Reference a utility rule by ID

```typescript
root.findAll({
  rule: { matches: 'is-ember-decorator' },
  utils: {
    'is-ember-decorator': {
      kind: 'decorator',
      has: { pattern: '@$NAME', inside: { kind: 'class_body' } },
    }
  }
})
```

### 3. Relational Rules

Filter nodes by their position relative to other nodes in the AST.

#### `inside` - Node is contained within a matching ancestor

```typescript
// Find field_definition nodes that are DIRECT children of class_body
root.findAll({
  rule: {
    kind: 'field_definition',
    inside: {
      kind: 'class_body',
      stopBy: 'neighbor',  // Only check immediate parent
    }
  }
})
```

#### `has` - Node contains a matching descendant

```typescript
// Find class declarations that have a decorator
root.findAll({
  rule: {
    kind: 'class_declaration',
    has: {
      kind: 'decorator',
      stopBy: 'neighbor',  // Only check direct children
    }
  }
})
```

#### `follows` - Node appears after a matching sibling

```typescript
// Find nodes that follow a decorator
root.findAll({
  rule: {
    kind: 'field_definition',
    follows: { kind: 'decorator' },
  }
})
```

#### `precedes` - Node appears before a matching sibling

```typescript
root.findAll({
  rule: {
    kind: 'decorator',
    precedes: { kind: 'method_definition' },
  }
})
```

### The `stopBy` Parameter (Critical)

Controls how far relational rules search. **This is the most important parameter for correct queries.**

| Value | Behavior |
|-------|----------|
| `'neighbor'` | **(Default)** Only checks one level (immediate parent for `inside`, direct children for `has`) |
| `'end'` | Searches all the way (all ancestors for `inside`, all descendants for `has`) |
| `{ rule }` | Stops when a node matching the rule is found (inclusive) |

**Common pattern: matching only direct class members**

```typescript
// WRONG: findAll with just kind searches ALL descendants recursively
classBody.findAll({ rule: { kind: 'field_definition' } })
// ^ This picks up nested properties inside object literals!

// RIGHT: Use inside rule with stopBy: 'neighbor' to match direct children only
classBody.findAll({
  rule: {
    kind: 'field_definition',
    inside: { kind: 'class_body', stopBy: 'neighbor' },
  }
})
```

### The `field` Parameter

Restricts matches to a specific named field position in the parent node.

```typescript
// Match only the KEY in a key-value pair, not values that happen to match
root.findAll({
  rule: {
    kind: 'pair',
    has: {
      field: 'key',         // Only match the 'key' field position
      regex: 'prototype',
    }
  }
})
```

Common tree-sitter fields: `name`, `body`, `source`, `key`, `value`, `left`, `right`,
`arguments`, `decorator`, `type_annotation`.

## Patterns Used in This Codebase

### Finding direct class members (not nested)

```typescript
import { NODE_KIND_CLASS_BODY, NODE_KIND_FIELD_DEFINITION, NODE_KIND_METHOD_DEFINITION } from './code-processing.js';

const DIRECT_CLASS_MEMBER = { inside: { kind: NODE_KIND_CLASS_BODY, stopBy: 'neighbor' } } as const;

// Properties - try multiple kinds since grammar varies
function findPropertyDefinitions(classBody: SgNode): SgNode[] {
  for (const nodeType of ['field_definition', 'public_field_definition', 'class_field']) {
    try {
      const props = classBody.findAll({ rule: { kind: nodeType, ...DIRECT_CLASS_MEMBER } });
      if (props.length > 0) return props;
    } catch {
      // Kind not valid in this grammar
    }
  }
  return [];
}

// Methods
function findMethodDefinitions(classBody: SgNode): SgNode[] {
  return classBody.findAll({ rule: { kind: NODE_KIND_METHOD_DEFINITION, ...DIRECT_CLASS_MEMBER } });
}
```

### Finding import statements and extracting source

```typescript
const imports = root.findAll({ rule: { kind: 'import_statement' } });
for (const imp of imports) {
  const source = imp.field('source');     // The string literal after 'from'
  const clause = imp.field('import');     // The import clause (specifiers)
  const sourcePath = source?.text();      // e.g., "'@ember-data/model'"
}
```

### Finding decorators preceding a node

```typescript
// Walk backwards through siblings collecting decorator nodes
function collectPrecedingDecorators(node: SgNode): string[] {
  const decorators: string[] = [];
  const siblings = node.parent()?.children() ?? [];
  const idx = siblings.indexOf(node);
  for (let i = idx - 1; i >= 0; i--) {
    const sib = siblings[i];
    if (!sib) continue;
    if (sib.kind() === 'decorator') decorators.unshift(sib.text());
    else if (sib.text().trim() !== '') break;
  }
  return decorators;
}
```

### Finding a class that extends a specific base

```typescript
// Find class with heritage clause
const classDecl = root.find({ rule: { kind: 'class_declaration' } });
const heritage = classDecl?.find({ rule: { kind: 'class_heritage' } });
const identifiers = heritage?.findAll({ rule: { kind: 'identifier' } }) ?? [];
const baseClasses = identifiers.map((id) => id.text());
```

### Pattern matching with context for class fields

```typescript
// Match decorated class fields like: @attr('string') name;
root.findAll({
  rule: {
    pattern: {
      context: 'class A { @$DECORATOR $FIELD = $VALUE }',
      selector: 'field_definition',
    }
  }
})
```

## Debugging Tips

1. **Use `node.kind()` liberally** - When a rule isn't matching, log the actual kinds: `classBody.children().map(c => c.kind())`
2. **Try/catch around `findAll` with rules** - Invalid kind names throw at runtime, not compile time
3. **Check `stopBy` behavior** - The default `'neighbor'` only searches one level. Use `'end'` for recursive search.
4. **Use the ast-grep playground** - https://ast-grep.github.io/playground.html to test rules interactively

