---
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>
