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.
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.
import { parse, Lang, type SgNode } from '@ast-grep/napi';
const ast = parse(Lang.TypeScript, sourceCode);
const root: SgNode = ast.root();
// 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
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[]
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)
// After finding with a pattern containing $VAR or $$$VARS:
node.getMatch('VAR'): SgNode | null
node.getMultipleMatches('VARS'): SgNode[]
const edit = node.replace('newCode'); // Returns Edit object
const newSource = root.commitEdits([edit1, edit2]); // Apply batch edits
The find and findAll methods accept a NapiConfig object for complex queries:
node.findAll({
rule: { /* rule object */ },
constraints?: { /* meta-variable constraints */ },
})
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: '^_' } })
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 IDroot.findAll({
rule: { matches: 'is-ember-decorator' },
utils: {
'is-ember-decorator': {
kind: 'decorator',
has: { pattern: '@$NAME', inside: { kind: 'class_body' } },
}
}
})
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 siblingroot.findAll({
rule: {
kind: 'decorator',
precedes: { kind: 'method_definition' },
}
})
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' },
}
})
field ParameterRestricts 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.
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 } });
}
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'"
}
// 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;
}
// 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());
// Match decorated class fields like: @attr('string') name;
root.findAll({
rule: {
pattern: {
context: 'class A { @$DECORATOR $FIELD = $VALUE }',
selector: 'field_definition',
}
}
})
node.kind() liberally - When a rule isn't matching, log the actual kinds: classBody.children().map(c => c.kind())findAll with rules - Invalid kind names throw at runtime, not compile timestopBy behavior - The default 'neighbor' only searches one level. Use 'end' for recursive search.