Files
dotfiles/pi/.pi/agent/skills/ast-grep/SKILL.md
2026-08-04 11:48:23 +02:00

3.5 KiB

name, description
name description
ast-grep Structural code search and rewriting with ast-grep (AST-aware grep). Use instead of rg/grep when searching for code *structure* rather than text: find all calls/definitions matching a shape, match across formatting/line breaks, extract matched sub-parts (function names, arguments), or perform safe codemod-style rewrites. Works for Rust, TS/JS, Python, Go, Java, C/C++, and most tree-sitter languages.

ast-grep: structural search & rewrite

ast-grep matches code by AST shape, not text. Prefer it over rg when the target spans lines, when formatting varies, or when you need to rewrite code safely.

Pattern syntax

Patterns are valid code snippets with meta-variables:

Syntax Matches
$VAR exactly one AST node (UPPERCASE names only)
$$$ / $$$ARGS zero or more nodes (args, params, statements)
$_ one node, non-capturing
repeated $A backreference: $A == $A matches x == x, not x == y
ast-grep -p '$X.unwrap()' -l rust src/                 # find all .unwrap() calls
ast-grep -p 'fn $NAME($$$) -> $RET { $$$ }' -l rust .  # multi-line shapes ok
ast-grep -p 'await $EXPR' -l ts src/
  • -l <lang> is required unless searching by file extension is unambiguous; use it anyway.
  • Always single-quote patterns — double quotes let the shell eat $VAR.
  • Pattern must parse as valid code for that language (e.g. a bare match $E { $$$ } works in Rust because it's a valid expression).

Structured output (for scripting / extraction)

ast-grep -p 'pub fn $F($$$) { $$$ }' -l rust src/ --json=compact

JSON gives file, range, text, and metaVariables.single.F.text — use this to extract e.g. all matching function names instead of parsing grep output.

Rewrite

# Preview as diff (default: prints diff, does NOT modify files)
ast-grep -p '$X.lock().unwrap()' --rewrite '$X.lock().expect("poisoned")' -l rust src/
# Apply all edits
ast-grep -p '...' --rewrite '...' -l rust src/ --update-all

Preview first, then --update-all. The rewrite is a text template: metavars are substituted, unmatched metavars become empty strings.

YAML rules (when patterns aren't enough)

For context-sensitive matches (inside/has/not), use scan --inline-rules:

ast-grep scan --inline-rules 'id: unwrap-in-closure
language: rust
rule:
  pattern: $X.unwrap()
  inside:
    kind: closure_expression
    stopBy: end
message: unwrap inside closure' src/

Rule keys:

  • Atomic: pattern, kind (node type, e.g. closure_expression), regex
  • Relational: inside, has, follows, precedes — add stopBy: end to search beyond direct parent/child
  • Composite: all, any, not
  • Optional fix: <template> per rule; constraints: to filter metavars (e.g. X: {regex: '^foo'})

To discover node kinds for kind:, match a sample with --json (each match reports its node) or just use a pattern instead.

Gotchas

  • $var (lowercase) is not a metavariable; use $VAR.
  • Single-quote patterns in the shell.
  • kind combined with pattern doesn't re-parse the pattern; if a snippet is ambiguous, use the pattern object form: pattern: {context: 'class A { $F = $V }', selector: field_definition}.
  • all: [{kind: a}, {kind: b}] tests the same node twice — for "contains both", use all: [has: {...}, has: {...}].
  • Regexes use Rust regex syntax (no backreferences/lookaround).
  • Fall back to rg for plain-text/comment/string searches — ast-grep only matches code nodes.