ast-grep-codemods

star 3.1k

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.

warp-drive-data By warp-drive-data schedule Updated 2/24/2026

name: ast-grep-codemods description: 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. user-invocable: false

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

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

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

SgNode Core Methods

Search

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

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

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

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

Code Editing

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:

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

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

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

// 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)

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

any - At least one rule must match (OR)

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

not - Negate a rule

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

matches - Reference a utility rule by ID

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

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

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

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

precedes - Node appears before a matching sibling

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

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

// 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)

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

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

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

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

// 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
Install via CLI
npx skills add https://github.com/warp-drive-data/warp-drive --skill ast-grep-codemods
Repository Details
star Stars 3,147
call_split Forks 1,337
navigation Branch main
article Path SKILL.md
More from Creator
warp-drive-data
warp-drive-data Explore all skills →