| 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
node.find(matcher: string | number | NapiConfig): SgNode | null
node.findAll(matcher: string | number | NapiConfig): SgNode[]
node.matches(pattern: string): boolean
node.inside(pattern: string): boolean
node.has(pattern: string): boolean
Traversal
node.children(): SgNode[]
node.parent(): SgNode | null
node.child(nth: number): SgNode | null
node.field(name: string): SgNode | null
node.ancestors(): SgNode[]
node.next(): SgNode | null
node.nextAll(): SgNode[]
node.prev(): SgNode | null
node.prevAll(): SgNode[]
Inspection
node.kind(): string
node.text(): string
node.isLeaf(): boolean
node.isNamed(): boolean
node.range(): Range
Meta-variable Extraction
node.getMatch('VAR'): SgNode | null
node.getMultipleMatches('VARS'): SgNode[]
Code Editing
const edit = node.replace('newCode');
const newSource = root.commitEdits([edit1, edit2]);
NapiConfig Rule Object
The find and findAll methods accept a NapiConfig object for complex queries:
node.findAll({
rule: { },
constraints?: { },
})
Rule Types
1. Atomic Rules
Match individual nodes by their properties.
kind - Match by tree-sitter node type
root.findAll({ rule: { kind: 'class_declaration' } })
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
root.findAll({ rule: { pattern: 'console.log($ARG)' } })
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
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
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
root.findAll({
rule: {
kind: 'field_definition',
inside: {
kind: 'class_body',
stopBy: 'neighbor',
}
}
})
has - Node contains a matching descendant
root.findAll({
rule: {
kind: 'class_declaration',
has: {
kind: 'decorator',
stopBy: 'neighbor',
}
}
})
follows - Node appears after a matching sibling
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
classBody.findAll({ rule: { kind: 'field_definition' } })
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.
root.findAll({
rule: {
kind: 'pair',
has: {
field: 'key',
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;
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 {
}
}
return [];
}
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');
const clause = imp.field('import');
const sourcePath = source?.text();
}
Finding decorators preceding a node
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
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
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
findAll with rules - Invalid kind names throw at runtime, not compile time
- Check
stopBy behavior - 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