pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
195 lines (137 loc) • 8.03 kB
Markdown
---
name: pi-lens-ast-grep
description: Use when searching or replacing code patterns - use ast-grep instead of text search for semantic accuracy
---
# AST-Grep Code Search
Use `ast_grep_search` and `ast_grep_replace` for semantic code search/replace. ast-grep understands code structure, not just text.
These tools (plus `ast_grep_outline`, `lsp_navigation`) are registered but inactive by default on hosts that support pi's dynamic tooling. If a call to one of them isn't recognized, activate it first: `pi_lens_activate_tools tools=["ast_grep_search", "ast_grep_replace"]`.
On hosts that aggregate pi-lens behind a single `lens` tool, the `ast_grep_*` tools and `pi_lens_activate_tools` may not be registered at all. There, fall back to the `ast-grep` CLI (alias `sg`) — patterns, metavariables, and YAML rules are identical:
```bash
# search (≈ ast_grep_search)
ast-grep run -p 'fetchMetrics($$$ARGS)' -l ts src/
# rewrite (≈ ast_grep_replace; -U applies all)
ast-grep run -p 'var $X' -r 'let $X' -l js src/ -U
# full YAML rule (≈ the rule: parameter)
ast-grep scan --rule my-rule.yml src/
```
Structural-intent parameters map to YAML constraints: `insideKind` → `inside: { kind: ..., stopBy: end }`, `hasKind`/`hasDescendantKind` → `has: { kind: ... }`, `follows` → `follows: { pattern: ... }`, `precedes` → `precedes: { pattern: ... }`.
## When to Use
- Function calls, imports, class methods (structured code)
- Safe replacements across files
- **Use LSP first for:** definitions/references/types — then scope ast-grep to files discovered by LSP
- **Use grep for:** partial string patterns, comments, URLs, or after one simplified ast-grep retry still returns zero matches
## Golden Rules
1. **Be specific** — `fetchMetrics($ARGS)` not `fetchMetrics`
2. **Scope it** — always specify `paths` to relevant files
3. **Retry once on zero matches** — simplify the pattern, same `paths`, then fall back to grep
4. **Dry-run first** — `apply: false` before `apply: true`
5. **Valid code only** — `function $NAME($$$) { $$$ }` not `function $NAME(`
6. **Avoid `selector` unless expert** — narrows to AST node kind; does not extract metavariables
7. **Metavariables don't work inside strings** — `from "$PATH"` matches literal `"$PATH"`, not a wildcard
## Metavariables
| Syntax | Matches | Named? |
|---|---|---|
| `$X` | single node | yes — captures the node |
| `$$$` | zero or more nodes | no — unnamed wildcard |
| `$$$ARGS` | zero or more nodes | yes — captures the list |
Use `$$$` when you don't need the captured value; `$$$NAME` when you do.
## Quick Reference
### Patterns
| Pattern | Matches |
|---|---|
| `fetchMetrics($ARGS)` | call with any single arg |
| `fetchMetrics($$$ARGS)` | call with any number of args |
| `function $NAME($$$) { $$$ }` | function declaration |
| `import { $NAMES } from $PATH` | named import (no quotes on path) |
| `const $X = $Y` | variable declaration |
### Structural-intent parameters (preferred for cross-context queries)
Use these instead of writing raw YAML:
| Parameter | Tool | What it does |
|---|---|---|
| `insideKind` | both | Only match inside an ancestor of this node kind (searches ALL ancestors, `stopBy: end`) |
| `hasKind` | both | Only match nodes whose **immediate child** has this kind (`stopBy: neighbor` — NOT recursive) |
| `hasDescendantKind` | both | Only match nodes containing this kind **anywhere in their descendants** (`stopBy: end`) — use this instead of `hasKind` when the target isn't a direct child |
| `follows` | both | Only match nodes preceded by a sibling matching this pattern |
| `precedes` | both | Only match nodes followed by a sibling matching this pattern |
`hasKind` and `hasDescendantKind` are mutually exclusive on both tools — combining them errors.
⚠ `insideKind` searches ALL ancestors (`stopBy: end`) with no boundary of its own — on a deeply nested file it can escalate past the enclosing function you meant and match against an unrelated outer scope; scope with `paths` or a raw YAML `rule:` with its own `stopBy` boundary if that matters.
```
# console.log only inside functions
ast_grep_search pattern="console.log($MSG)" lang="typescript" insideKind="function_declaration"
# replace var with let, scoped to functions only
ast_grep_replace pattern="var $X" rewrite="let $X" lang="javascript" insideKind="function_declaration"
```
These synthesize a YAML rule automatically. Use `rule:` for the full DSL when you need `all`/`any`/`not`, `nthChild`, `regex`, or other advanced constraints.
### Raw YAML rule (`rule:` parameter)
Pass a complete ast-grep YAML rule to unlock the full DSL:
```
ast_grep_search rule="id: my-rule
language: TypeScript
rule:
pattern: console.log($MSG)
inside:
kind: function_declaration
stopBy: end" lang="typescript"
```
### Debugging unknown node kinds — `ast_grep_search` dump mode
When a pattern returns zero matches and you don't know the correct node kind or field name, use `ast_grep_search` with `dump=true` to inspect a SMALL representative snippet:
```
ast_grep_search dump=true pattern="function foo() { return 1; }" lang="typescript"
```
Returns the full indented AST with node kinds and positions. Then use the correct kind in your pattern or `insideKind`.
### Composite (has/inside) in raw YAML
```yaml
# console.log inside a class method
pattern: console.log($$$)
inside:
kind: method_definition
stopBy: end
```
Use `kind:` directly when you want to match a node type without a pattern:
```yaml
# any arrow function
kind: arrow_function
```
## Common Gotchas
```
❌ $VAR inside quotes — matches literal "$VAR", not a metavar
from "$PATH" → use grep for wildcard path matching
from "./utils" → ✅ exact string literal works fine
❌ Trailing comma in objects
{ type: $T, } → use { type: $T }
❌ Shorthand property mismatch
{ runnerId: $RID } → won't match { runnerId }
use { runnerId } or { runnerId, $$$REST }
❌ Unnamed $$$ when you need the value
foo($$$) → captures nothing; use foo($$$ARGS) to inspect matches
❌ Multiple top-level statements — triggers "Multiple AST nodes are detected"
Two shapes, two fixes:
1. Sequence inside a block — wrap in braces:
foo(); bar(); → { foo(); bar(); }
2. Cross-context (module-level + block-level together, e.g. an import AND a call) —
wrapping in {} makes the pattern invalid (imports can't live inside a block).
Use two searches: find files containing the import, then scope the call search
to those paths. Or use a YAML `inside:`/`has:` rule (see Composite section above).
```
**No matches?**
For `nodeKind`, do not also pass `pattern` or `rule`; those forms are mutually exclusive. For `rule`, provide YAML containing both `id` and `language` fields. Use `strictness: relaxed` when unnamed punctuation is the only mismatch.
1. Try `strictness: relaxed` — ignores unnamed punctuation (trailing commas, semicolons) that `smart` mode requires
2. Use `ast_grep_search dump=true` on a sample snippet to verify the correct node kind
3. Simplify the pattern and retry once
4. Fall back to `grep` or `lsp_navigation`
## Agent task recipes
Use these as starting points, then scope `paths` tightly.
| Task | Pattern / params |
|---|---|
| Find object-literal function dependency by name | `pattern: { resetLSPService: $FN, $$$REST }` |
| Find empty catches | `pattern: try { $$$BODY } catch ($ERR) { }` |
| Find fire-and-forget async calls | `pattern: void $CALL` |
For lifecycle bugs, search first, then use the returned `details.matchLocations[].readSlice` handle for bounded context.
**Metavar captures** appear automatically below each match line:
```
src/foo.ts:1:1: const x = foo(a, b)
$VAR=x $$$ARGS=a,b
```
Named captures (`$X`, `$$$NAME`) are shown; unnamed wildcards (`$$$`) are not.
**Pagination** — use `skip: N` when results are truncated (next-page hint appears in output).
Debug: <https://ast-grep.github.io/playground.html>