fix(rules): no-fmt-errorf — rewrite as structural rule to bypass Go type-conversion ambiguity#43
Merged
Merged
Conversation
…ype-conversion ambiguity The original 'pattern: fmt.Errorf($$$ARGS)' was parsed by tree-sitter Go grammar as a type_conversion_expression at pattern-compile time (because 'Type(arg)' is a valid Go type cast with no surrounding expression context to disambiguate). Real call sites like 'return fmt.Errorf(...)' parse as call_expression because the surrounding context fixes the parse. Net effect: the unanchored pattern matched NOTHING in real code. The YAML had been silently emitting zero findings since the rule shipped. Surfaced 2026-06-03 during scenario 004's walk when 4 of 5 deliberate violations fired but the fmt.Errorf one did not. 'kind: call_expression' alone does NOT fix it — that just filters output; the pattern still mis-parses. Use a fully structural rule: match 'call_expression' whose function field is a selector_expression with operand 'fmt' and field 'Errorf'. Verified: - local fixture (2 fmt.Errorf calls) → 2 hits - maintainer#2 fixture (1 fmt.Errorf in Boom func) → 1 hit - type conversion call shape (e.g. 'string(b)') → 0 hits (correct) Scenario 004 floor lifted ≥4 → ≥5 in the same commit. Knowledge captured 2026-06-03 in [[ast-grep - Structural Code Search]] Gotchas section.
There was a problem hiding this comment.
PR Review: fix/no-fmt-errorf-call-expression → master
Scope
3 files changed — all directly related to the bugfix contract.
Automated Checks
JSON syntax validation — all three files pass:
rules/index.json— valid.claude-plugin/plugin.json— valid.claude-plugin/marketplace.json— valid
make precommit — skipped (requires approval; make touches build tooling). The individual JSON lint checks above cover the precommit criteria relevant to this diff.
Change Analysis
1. rules/go/no-fmt-errorf.yml (core fix)
| Aspect | Assessment |
|---|---|
| Root cause diagnosis | Correct. tree-sitter Go grammar parses bare fmt.Errorf($A) at pattern-compile time as type_conversion_expression (ambiguous with Type(arg) cast), not call_expression. Without surrounding expression context there is no disambiguation, so the pattern matched nothing in real code. |
| Fix strategy | Correct. Structural rule: kind: call_expression + has.selector_expression with operand: identifier[/^fmt$/] + field: field_identifier[/^Errorf$/]. This forces tree-sitter to interpret the node in call context, not cast context. |
| Ignores | Unchanged — main.go, *_test.go, vendor/**, mocks/** correctly excluded. |
| Rule ID alignment | go-errors/no-fmt-errorf exists in rules/index.json with owner: go-error-assistant, level: MUST, enforcement: rules/go/no-fmt-errorf.yml — consistent. |
| Doc reference | docs/go-error-wrapping-guide.md has ### RULE go-errors/no-fmt-errorf (MUST) at line 242 — consistent. |
Finding: ✅ No issues.
2. scenarios/004-findings-exist-path.md
| Aspect | Assessment |
|---|---|
| Narrative update | Line 19: Added "All 5 violations now fire after…" clause — accurately reflects the fix. |
| Expected count | Line 42: findings_count floor lifted from ≥ 4 to ≥ 5. Correct per the fixture (pkg/scenarios-test-fixture/violations.go in bborbe/maintainer#2 contains exactly 5 deliberate violations). |
| Removed stale caveat | Previous text noting the fmt.Errorf violation didn't fire has been replaced with the corrected expectation — no longer misleading. |
Finding: ✅ No issues.
3. CHANGELOG.md
| Aspect | Assessment |
|---|---|
| Location | New ## Unreleased section at top — correct changelog ordering. |
| Entry content | Names the specific YAML, explains the root cause (tree-sitter ambiguity), describes the fix (structural rule), references the verification fixture and scenario. Concise but complete. |
| Version strings | No version bump — correctly leaves v0.15.0 as the last released version. |
Finding: ✅ No issues.
Must Fix (Critical)
None.
Should Fix (Important)
None.
Nice to Have (Optional)
None.
Concerns from PR Plan
| Concern | Status |
|---|---|
correctness: Rule silently matching zero findings since ship |
Addressed — structural rewrite correctly targets call_expression with selector_expression on fmt.Errorf |
tests: Scenario 004 findings floor lifted ≥4 → ≥5 |
Addressed — scenario doc updated; floor now ≥5 consistent with 5 actual violations in fixture |
Next Steps
No additional work needed. The changes are minimal, targeted, and correct.
{
"verdict": "approve",
"summary": "Bugfix correctly rewrites the `go-errors/no-fmt-errorf` ast-grep rule as a structural pattern that resolves the tree-sitter Go grammar ambiguity causing zero findings since ship. Scenario 004 doc updated to reflect the fix, and CHANGELOG entry is accurate. No issues identified.",
"comments": [],
"concerns_addressed": [
"correctness: structural rule fix in rules/go/no-fmt-errorf.yml bypasses tree-sitter type_conversion_expression ambiguity",
"tests: scenario 004 findings_count floor lifted ≥4 → ≥5 in scenarios/004-findings-exist-path.md"
]
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
`rules/go/no-fmt-errorf.yml` was parse-broken since the YAML shipped — the original `pattern: fmt.Errorf($$$ARGS)` was parsed by tree-sitter Go grammar as a `type_conversion_expression` because `Type(arg)` is a valid Go type cast at pattern-compile time (no surrounding expression context disambiguates). Real call sites like `return fmt.Errorf(...)` parse as `call_expression` because their surrounding context fixes the parse. Net effect: the rule matched nothing in real code and silently emitted zero findings on every PR review since it shipped.
Surfaced 2026-06-03 during scenario 004's walk: 4 of 5 deliberate violations fired, the fmt.Errorf one did not.
Why `kind: call_expression` alone doesn't fix it
`kind:` filters output (which AST node kinds are reported), not how the `pattern:` line is parsed at compile time. The pattern still mis-parses as type-conversion and produces no matches to filter. Anchoring `kind` is necessary but not sufficient.
Fix
Replace the broken pattern with a fully structural rule:
```yaml
rule:
kind: call_expression
has:
field: function
kind: selector_expression
all:
- has: { field: operand, kind: identifier, regex: '^fmt$' }
- has: { field: field, kind: field_identifier, regex: '^Errorf$' }
```
Verification
Scenario 004's `findings_count` floor lifted ≥4 → ≥5 in the same commit (the documented baseline assumption is now satisfied).
Test plan