Recently Written · git

ai-config

One canonical AI agent config (rules, skills, MCP servers) rendered into Claude Code, Codex, Copilot CLI and Antigravity by acsync

git clone https://github.com/equwal/ai-config

Log | Files | Refs


skills/structural-refactor/SKILL.md (5043 bytes)

1 ---
2 name: structural-refactor
3 description: AST-aware structural search and replace across a codebase using ast-grep. Use for renames, function signature changes, API migrations, deprecation sweeps, or codemods spanning more than two files; for finding all call sites of a pattern rather than a string; and for writing project-specific lint rules that encode team conventions. Use instead of sed, awk, or regex whenever the target is code rather than plain text.
4 ---
5 
6 # Structural refactor
7 
8 Regex does not understand code. It matches inside strings, comments, and
9 unrelated identifiers, and it misses anything reformatted across lines. That is
10 how half-finished renames ship. `ast-grep` matches on the syntax tree.
11 
12 Order of preference for a rename or signature change:
13 
14 1. **LSP rename** (`rename_symbol`) if the language server supports it — it
15    understands scope and imports, and is always correct.
16 2. **ast-grep** for anything the LSP cannot express: cross-language sweeps,
17    call-pattern rewrites, argument reordering, API migrations.
18 3. **sed** never, for code.
19 
20 ## Preflight
21 
22 ```bash
23 command -v ast-grep || command -v sg
24 ast-grep --version
25 ```
26 
27 Install: `cargo install ast-grep`, `npm i -g @ast-grep/cli`, `brew install
28 ast-grep`. The binary is `ast-grep`, aliased `sg` (which collides with
29 `sg(1)` on some Linux systems — prefer the full name in scripts).
30 
31 ## Pattern syntax
32 
33 | Token | Matches |
34 |---|---|
35 | `$VAR` | exactly one named node, captured |
36 | `$$$ARGS` | zero or more nodes (argument lists, statement bodies), captured |
37 | `$_` | one node, not captured |
38 
39 Patterns are written as code, not as regex. Whitespace and formatting are
40 irrelevant; structure is what matters.
41 
42 ## Workflow — always in this order
43 
44 **1. Search first. Read the matches before rewriting anything.**
45 
46 ```bash
47 ast-grep run -p 'db.query($SQL, $$$ARGS)' -l python
48 ```
49 
50 Count them, and check for false positives:
51 
52 ```bash
53 ast-grep run -p 'db.query($SQL, $$$ARGS)' -l python --json=compact | jq length
54 ```
55 
56 **2. Preview the rewrite as a diff.** `run` with `--rewrite` prints a diff and
57 changes nothing until told to apply.
58 
59 ```bash
60 ast-grep run -p 'db.query($SQL, $$$ARGS)' \
61              --rewrite 'db.execute($SQL, params=[$$$ARGS])' \
62              -l python
63 ```
64 
65 **3. Apply.** Use `--interactive` to accept or reject each hunk when the pattern
66 is loose, or `--update-all` for a bulk apply once the diff has been reviewed.
67 
68 ```bash
69 ast-grep run -p '...' --rewrite '...' -l python --update-all
70 ```
71 
72 **4. Verify.** Formatter, type checker, tests. A structural rewrite that
73 type-checks is usually correct; one that does not, is not. Commit the codemod
74 separately from any behavioural change so the diff stays reviewable.
75 
76 Work on a clean tree — `git status` before, `git diff` after. `--update-all`
77 edits files in place with no undo.
78 
79 ## Rule files
80 
81 Anything run more than once, or enforced on every PR, belongs in a rule file
82 rather than a shell command. Rules support constraints patterns cannot express:
83 `inside`, `has`, `not`, `follows`, and metavariable regex filters.
84 
85 ```yaml
86 # rules/no-bare-except.yml
87 id: no-bare-except
88 language: python
89 severity: error
90 message: Bare except swallows KeyboardInterrupt and SystemExit.
91 note: Catch a specific exception type, or `except Exception` if truly generic.
92 rule:
93   pattern: |
94     try:
95       $$$BODY
96     except:
97       $$$HANDLER
98 ```
99 
100 ```bash
101 ast-grep scan --rule rules/no-bare-except.yml
102 ast-grep scan                      # all rules, via sgconfig.yml
103 ```
104 
105 `sgconfig.yml` at the repo root registers the rule directories:
106 
107 ```yaml
108 ruleDirs:
109   - rules
110 ```
111 
112 Verify exact rule schema fields against `ast-grep scan --help` and the
113 installed version's docs — the YAML schema has evolved.
114 
115 This is the cheapest way to encode a convention that keeps recurring in review.
116 When the same correction is made twice, write a rule instead of a third comment.
117 
118 ## Common patterns
119 
120 ```bash
121 # Find all callers of a method, regardless of receiver name
122 ast-grep run -p '$OBJ.deprecated_method($$$)' -l ts
123 
124 # Add an argument to every call
125 ast-grep run -p 'connect($HOST, $PORT)' \
126              --rewrite 'connect($HOST, $PORT, timeout=30)' -l python
127 
128 # Unwrap a redundant wrapper
129 ast-grep run -p 'str($X)' --rewrite '$X' -l python     # review each: too loose
130 
131 # Locate every unsafe block in a Rust crate
132 ast-grep run -p 'unsafe { $$$BODY }' -l rust
133 ```
134 
135 The `str($X)` example is deliberately included as a warning: it matches cases
136 where the conversion is load-bearing. Loose patterns need `--interactive`, not
137 `--update-all`.
138 
139 ## Limits
140 
141 - ast-grep is syntactic, not semantic. It cannot resolve imports, aliases, or
142   types. `foo.bar()` and `from x import bar; bar()` are different patterns.
143 - For type-directed refactors, use the language's own tooling: `cargo fix`,
144   `tsc` codemods via ts-morph, `gofmt -r` or `gopls rename`.
145 - After any large sweep, grep for the old name anyway to catch what the pattern
146   missed — string literals, docs, config, comments. Those are text, and text
147   tools are correct for them.