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
- Use
node.kind()liberally - When a rule isn't matching, log the actual kinds:classBody.children().map(c => c.kind()) - Try/catch around
findAllwith rules - Invalid kind names throw at runtime, not compile time - Check
stopBybehavior - The default'neighbor'only searches one level. Use'end'for recursive search. - Use the ast-grep playground - https://ast-grep.github.io/playground.html to test rules interactively