Files
teamai-test/.teamai/skills/common/golang-refactoring/references/go-tooling.md
T

128 lines
7.9 KiB
Markdown

# Go Tooling for Refactoring
This file is the tool reference for `samber/cc-skills-golang@golang-refactoring`: every mechanical-rewrite tool worth reaching for, from the primary actuator (`gopls`) down to hand-rolled `go/analysis` fixers, ordered so you can pick the least-powerful tool that solves the problem. See [catalog.md](catalog.md) for which tool maps to which Fowler refactoring, and [workflow.md](workflow.md) for how a tool-driven step fits into the staged-PR process.
## 1. gopls — the Primary Actuator
- gopls performs most of this skill's Low- and Medium-risk transforms — Rename, Inline, Extract, and the `refactor.rewrite.*` family.
- See the Risk Stratification table in [SKILL.md](../SKILL.md) and the `Tool:` line on each entry in [catalog.md](catalog.md) for which gopls action maps to which refactoring.
- The full code-action reference, CLI invocation, safety behavior, and MCP server setup belong to `samber/cc-skills-golang@golang-gopls` — this file covers the tools that skill doesn't.
## 2. Bulk Mechanical Rewrite Tools
When a change recurs across many call sites, reach for a generated rewrite tool instead of hand-editing each one. The tools below are ordered by increasing power — start at the top and move down only when the current tool's limits block the rewrite you need.
### `gofmt -r` — syntactic, single-expression
- Purely syntactic and type-unaware: it matches expression shape, not the types involved, and a rewrite is limited to a single expression.
- Wildcards are single lowercase identifiers that match any sub-expression.
```bash
gofmt -r 'bytes.Compare(a, b) == 0 -> bytes.Equal(a, b)' -w file.go
gofmt -r 'bytes.Compare(a, b) != 0 -> !bytes.Equal(a, b)' -w file.go
gofmt -s -w file.go # -s additionally simplifies (e.g. s[a:len(s)] -> s[a:])
gofmt -l . # list non-conforming files — useful as a CI gate
gofmt -d file.go # show the diff without writing
```
- Because it can't see types, it cannot target "every `bytes.Compare` call on a `[]byte`, but not a look-alike function of the same name from another package" — that distinction needs `eg`.
### `eg` — type-aware, example-based
`golang.org/x/tools/cmd/eg` rewrites by example, à la Refaster: a template file declares `before`/`after` functions of identical type, each with a single-expression body.
```go
// template.go
package template
import (
"errors"
"fmt"
)
func before(s string) error { return fmt.Errorf("%s", s) }
func after(s string) error { return errors.New(s) }
```
```bash
eg -t template.go -w ./...
```
- Matching is semantic, not textual — `func(x int)` in the template also matches `func(y int)` at the call site, since `eg` matches by type and structure.
- Limits: expressions only, no statements or function-literal patterns; a rewrite can't change the expression's type; imports are added but never removed (run `goimports` afterward); and duplicating a wildcard variable in the `after` template duplicates whatever side effect the matched expression had.
### `gopatch` — statement-level, import-aware
`github.com/uber-go/gopatch` operates at the statement level and tracks imports as part of the patch, so it can work on code that doesn't fully compile mid-refactor — useful for the messy in-between states of a large migration. A patch declares metavariables between `@@` markers, then a diff-like body:
```
@@
var x expression
@@
-errors.New(fmt.Sprintf(x))
+fmt.Errorf(x)
```
```bash
gopatch -p rewrite.patch ./...
gopatch -d -p rewrite.patch ./... # dry-run — show the diff only
```
- Still beta; the project frames it as covering roughly 80% of a migration, not 100%.
- A pattern can't match an import statement in isolation — something must follow it in the pattern for a match to occur.
### `go/analysis` + SuggestedFixes — bespoke, testable
- For a rewrite too specific for the above three, write an `analysis.Analyzer`. Its `Run(pass)` walks the type-checked AST and reports `analysis.Diagnostic{SuggestedFixes: [...]}` at each match.
- Test it against `.golden` files with `analysistest.RunWithSuggestedFixes`, then ship it as a `singlechecker`/`multichecker -fix` binary, or run it through `go vet -vettool=<path>`.
### `go fix` — the `go/analysis`-based fixer suite
- As of Go 1.26, `go fix` is rewritten onto the `go/analysis` framework and has converged with `go vet` — this is where the `modernize` fixer suite lives (`rangeint`, `mapsloop`, `minmax`, `any`, `stringscut`, `fmtappendf`, `omitzero`, and more; → See `samber/cc-skills-golang@golang-modernize` skill for the idiom-by-idiom breakdown).
- On an older toolchain without this convergence, run the equivalent analyzers through `singlechecker`/`multichecker -fix` instead of `go fix`.
- The `//go:fix inline` directive marks a function or constant so that `go fix` inlines every call site into its replacement — a machine-executable way to complete a deprecation migration once the replacement exists:
```bash
go fix ./...
go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./...
```
### `dave/dst` — comment- and formatting-preserving AST edits
- `go/ast` stores comments in a side table keyed by byte offset, so reordering, moving, or deleting nodes desyncs comments from the code they were attached to — the root cause of the Extract/Inline comment-loss caveat above.
- `github.com/dave/dst` (Decorated Syntax Tree) attaches comments and blank-line spacing as node-local decorations instead, so a hand-rolled AST rewrite round-trips them correctly via `decorator.Parse` / `decorator.Print`.
- `dstutil.Apply` mirrors `astutil.Apply`'s visitor API, so existing `go/ast` rewrite logic ports over directly.
- Reach for this only when a bespoke `go/analysis` fixer needs to preserve comments that `go/ast`-based rewriting would otherwise scatter.
### Always run after a bulk rewrite
```bash
goimports -w . # organizes imports added/left dangling by gofmt -r, eg, or a hand-rolled fixer
```
```bash
deadcode ./... # find code orphaned by a removal-heavy refactor
deadcode -test ./... # include test binaries — unreached public API here signals a coverage gap, not dead code
deadcode -whylive=funcName ./... # shortest reachability path proving a function is still live
```
- `golang.org/x/tools/cmd/deadcode` builds its reachability graph with Rapid Type Analysis from `main`/`init`, so it is unsound with respect to assembly, `go:linkname`, and reflection-driven dispatch — treat a "dead" verdict as a strong hint, not a proof, on code that uses any of those.
- **Never `sed`/`perl` a structural Go change by hand.** None of these text tools have grammar awareness, so a pattern that happens to match inside a string literal or a comment gets rewritten right alongside real code.
- Always finish a bulk rewrite with `goimports`, even after a tool that claims to manage imports itself.
## 4. Structure-Discovery Tools (blast-radius mapping)
These feed the planning gate in [workflow.md](workflow.md) — map the blast radius before choosing a tool from the sections above.
| Tool | What it answers |
| --- | --- |
| `golang.org/x/tools/go/callgraph` (`rta`/`cha`/`static`/`vta` algorithms) | Who can reach this function, statically, across the whole build — algorithms trade precision for speed differently |
| gopls call hierarchy (`textDocument/prepareCallHierarchy`) | Incoming/outgoing calls for one symbol, interactively |
| `go_references` / `go_symbol_references` (gopls MCP) | Every reference to a symbol, from an agent context, without a full callgraph build |
| `go mod graph` | Module-level dependency edges — which modules require which, for blast radius that crosses module boundaries |
## Cross-References
- [catalog.md](catalog.md) — the Fowler refactoring catalog mapped to Go, with the tool from this file that mechanizes each entry.
- [workflow.md](workflow.md) — the planning gate this section's structure-discovery tools feed, and where a tool-driven step fits into the staged-PR process.