From f86c4f0878ca1306c1c483a461d742d1dfd76e6d Mon Sep 17 00:00:00 2001 From: Benjamin Borbe Date: Tue, 2 Jun 2026 11:27:56 +0200 Subject: [PATCH] docs(ast-grep): propagate counter-total-suffix syntax findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #11 cracked the ast-grep 0.43.0 recipe for struct-literal field matching after PR #4 and PR #8 both burned cycles iterating on zero-match patterns. Folding the discoveries into the guide so the next bootstrap PR doesn't re-learn them. New section 'Struct-literal field matching' covers the 3-piece recipe: 1. pattern.context + selector for sub-node targeting (bare 'Name: $X' parses as labeled_statement, not keyed_element — must wrap in context) 2. inside.pattern with stopBy: end for type anchoring (prevents GaugeOpts / HistogramOpts / SummaryOpts false-flags through the selector) 3. constraints at rule-top-level (NOT under rule.pattern.constraints), with not.regex inside the metavariable spec Pitfalls Learned grew by 3 entries documenting the specific traps: - 'Name: $X' parses as labeled_statement - pattern.context alone leaks to sibling types - constraints placement: top-level sibling of rule, not nested Canonical example entry added pointing to rules/go/counter-total-suffix.yml (landed in PR #11). 153 → 193 lines. No personal vault paths, no trading-domain terms — pre-emptive grep clean. --- docs/ast-grep-rule-writing-guide.md | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/ast-grep-rule-writing-guide.md b/docs/ast-grep-rule-writing-guide.md index 82c4cb1..eb3820a 100644 --- a/docs/ast-grep-rule-writing-guide.md +++ b/docs/ast-grep-rule-writing-guide.md @@ -72,6 +72,42 @@ rule: This matches a `for` loop whose body, anywhere down the tree, does NOT contain `<-ctx.Done()`. Canonical example: `rules/go/cancel-check-in-loop.yml`. +### Struct-literal field matching + +For "field X inside struct literal Y must satisfy constraint Z" shapes — the common Go case is checking a single field of a `prometheus.CounterOpts{}` / `http.Cookie{}` / etc. Requires three composed pieces: + +1. **`pattern.context + selector`** to target the field as a structural sub-node. A bare `'Name: $X'` does NOT work — Go parses it as a `labeled_statement` (Name treated as a goto label), not as a struct `keyed_element`. The `context` supplies surrounding code that disambiguates the parse; `selector` picks the sub-node to report: + + ```yaml + pattern: + context: 'prometheus.CounterOpts{Name: $V, $$$}' + selector: 'keyed_element' + ``` + +2. **`inside` with `stopBy: end`** to re-anchor on the enclosing literal type so sibling types with the same field shape (e.g. `GaugeOpts`, `HistogramOpts`, `SummaryOpts` all carry a `Name:` field) do not false-flag through the context match: + + ```yaml + inside: + pattern: 'prometheus.CounterOpts{$$$}' + stopBy: end + ``` + +3. **`constraints` at rule-top-level**, sibling to `rule:` (NOT a child of `rule.pattern.constraints` — that form is rejected by the parser). `not.regex` works inside the metavariable spec: + + ```yaml + constraints: + V: + not: + regex: '_total"$' + ``` + +Canonical example: `rules/go/counter-total-suffix.yml`. Verified 4 TP / 0 FP across: + +- `NewCounterVec(CounterOpts{Name: "X"})` — matches if `X` missing `_total` +- `NewCounter(CounterOpts{Name: "X"})` — same struct, also matches +- Field-position-agnostic — flags regardless of whether `Name` is first, middle, or last +- `GaugeOpts` / `HistogramOpts` with `Name:` — does NOT match (anchored to CounterOpts) + ### Kind matchers When matching by AST node type rather than a literal pattern. Examples: `kind: for_statement`, `kind: field_declaration`, `kind: function_declaration`. @@ -97,6 +133,9 @@ Reference for available node kinds: .not`. ## Smoke Testing @@ -149,5 +188,6 @@ If a rule is MUST-level but can't be mechanical, that is a smell. Either downgra - `rules/go/cancel-check-in-loop.yml` — `not.has` deep walk - `rules/go/no-time-now-direct.yml` — simple `pattern:` presence - `rules/go/no-time-time-in-fields.yml` — struct field detection via `any:` alternation + - `rules/go/counter-total-suffix.yml` — struct-literal field matching with `pattern.context + selector + inside.stopBy + constraints.not.regex` - ast-grep reference: - ast-grep playground (verify node kinds before committing):