AST-based code search, lint, and rewrite using ast-grep patterns and rules
80
100%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
ast-grep (sg) is a CLI tool that searches, lints, and rewrites code using Abstract Syntax Trees instead of text patterns. It uses tree-sitter parsers, supports 20+ languages (including Markdown), and runs in seconds across large codebases.
Write a code snippet as a pattern, and ast-grep matches it structurally against the AST -- ignoring whitespace, comments, and formatting differences.
Patterns are one atomic building block. When a pattern alone can't express what you need (disambiguation, naming constraints, relational checks, transforms), graduate to a full YAML rule. Patterns handle ~60% of searches; YAML rules handle the rest.
$NAME, $$$ARGS, $_. Lowercase $name is treated as literal code, not a capture.$X captures exactly one AST node. Use $$$X for zero-or-more. The mismatch is the most common cause of "pattern doesn't match" -- a single-arg pattern won't match a two-arg call.$A == $A matches x == x but rejects x == y. This is structural equality, not variable binding -- use it intentionally.pattern, kind, or regex). A not rule alone is invalid because ast-grep needs something to anchor the search.regex matches the full node text. Partial matches fail. /foo/ does not match fooBar -- use ^foo if you want prefix matching.fix replaces the single matched node. It cannot patch multiple locations. Use expandStart/expandEnd to consume surrounding tokens (trailing commas, semicolons).fix. This is intentional for optional captures, but verify your pattern actually captures what you expect before relying on it in a rewrite.stopBy: end on relational rules (inside, has, follows, precedes) unless you specifically want neighbor-only matching. The default stopBy: neighbor stops at the first non-matching node and misses deeper results -- this is the second most common cause of "rule doesn't match."--inline-rules: the shell interprets $ as a variable. Wrap YAML in single quotes or escape each metavariable with \$VAR.--debug-query=cst, then build the rule against that snippet.sg scan --inline-rules '...' --stdin or sg scan -r rule.yml test.file. This catches composition errors before they waste a full codebase scan.Rules are compositions of atomic parts. A single error in one part cascades, so verify at each step.
sg run --pattern 'TARGET_CODE' --debug-query=cst -l LANG to see node kinds and structure of the example code.echo "example code" | sg scan --inline-rules '...' --stdin or sg scan -r rule.yml example.file. Confirm it matches the positive case and rejects the negative case.sg scan for the full project. Review a sample of results to confirm precision.id, message, severity, fix. Write test YAML with sg new test, generate snapshots with sg test -U.When a rule doesn't work, go back to step 3. The AST structure frequently contradicts how source code looks visually -- verify with --debug-query=cst rather than assuming.
Before reporting a rule as done, run it against both the positive and negative example. Confirm it matches what it should and rejects what it shouldn't.
sg run -p 'fetch($$$ARGS)' -l javascriptrule:
pattern: $HOOK($$$ARGS)
constraints:
HOOK: { regex: '^use' }sg run -p 'oldFunction($$$ARGS)' -r 'newFunction($$$ARGS)' -l typescript -UUse context + selector when the target fragment needs surrounding syntax to parse:
rule:
pattern:
context: 'class A { a = 123 }'
selector: field_definitionid: no-await-in-loop
language: TypeScript
rule:
pattern: await $EXPR
inside:
any:
- kind: for_statement
- kind: for_in_statement
- kind: while_statement
stopBy: end