Skip to content

Guard code-quality recipes against emitting invalid Go - #53

Merged
bmuschko merged 5 commits into
mainfrom
bmuschko/pr-description-draft
Aug 11, 2026
Merged

Guard code-quality recipes against emitting invalid Go#53
bmuschko merged 5 commits into
mainfrom
bmuschko/pr-description-draft

Conversation

@bmuschko

@bmuschko bmuschko commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Problem

Several recipes rewrote code without checking the surrounding type or arity, so they could produce Go that does not compile. Issue #30 reported six: PreferStrconvAtoi, PreferOsReadDir, PreferStringsNewReader, PreferRawStringForRegex, ReduceNestingDepth, and HandleErrorReturn. The failures fall into two modes. The first swaps a call or expression for one of a different type or arity, for example replacing a value with strconv.Atoi(...) where the code needs an int64, or using a two-value regexp call in a single-value position. The second synthesizes a statement, a return, an if err != nil { return err }, or an assignment, without checking the enclosing function signature or variable scope.

How it has been addressed

Every affected recipe now guards its rewrite and skips whenever it cannot prove the output compiles. Type-substitution recipes check the required type at the rewrite site, whether that is the function's result position, a typed declaration, or an argument, before firing. Statement-synthesis recipes confirm they sit at a function-body block, that the enclosing function returns the expected single error, and that the value being handled is genuinely an error. Shared checks are consolidated into helpers (lstutil, type_context, errors_is_common) so the guards live in one place. The audit went past the reported six, since the same root cause was present elsewhere, and the fix now spans more than thirty recipes across the repository. The guards are deliberately conservative: when a recipe cannot confirm safety, it leaves the code untouched rather than risk an invalid rewrite.

Potential gaps

  • PreferStrconvAtoi and PreferOsReadDir are guarded for direct contexts such as a return or a typed declaration, but can still emit invalid code when the result escapes through a short variable declaration, for example entries := os.ReadDir(...) followed by entries[i].Size(). The LST collapses all integer widths to one type and drops slice element types, so these contexts cannot be distinguished yet. The residuals are documented in the recipes.
  • PreferStrconvItoa is left as a documented limitation for int64 and uint arguments for the same integer-width reason, pending the upstream LST fix in openrewrite/rewrite#8406.
  • The audit also surfaced a separate class of invalid-code defects driven by control flow and scope rather than type or arity, including SimplifySingleCaseSelect and RemoveUnconditionalValueOverwrite. These are out of scope here and are not addressed.

@bmuschko

bmuschko commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Validated the recipes against open-source repositories

Ran every recipe changed or added in this PR against 5 open-source Go repos: cobra, gin, logrus, mux, and websocket. For each recipe that changed a file, the result was applied and compiled with go build ./....

15 fired, 24 no-op, 0 errors, and every fired recipe compiled on every repo it touched (no invalid Go).

Recipe Repos it changed Compiles
WrapErrorWithContext cobra, logrus, mux, websocket
CheckCloseError cobra, logrus, websocket
HandleDeferredCloseError cobra, logrus, websocket
PreferErrorsIsOverEquality mux, websocket
MergeCollapsibleIf cobra, mux
PreferEmptyStringCheck cobra, mux
ReduceNestingDepth cobra, mux
PreferErrorsIsEOF websocket
PreferErrorsIsForFieldAccess websocket
UseErrorsAs websocket
UseStrongHash websocket
HandleErrorReturn mux
PreferIoWriteString cobra
SimplifySprintfConcat cobra
UseStructuredLogging logrus

The remaining 24 recipes were no-ops (no matching idioms in these repos).

Coverage caveats (pre-existing rewrite-go tooling, not these recipes)

  • gin contributed no usable results. Despite clear matches, nothing fired because gin's dependencies were not resolvable during the build and the rewrite-go printer hit a nil-pointer panic building its LST. The run was effectively 4 repos, not 5.

These numbers are therefore a floor, not a ceiling. Where the tooling could process the code, the recipes were clean.

@greg-at-moderne greg-at-moderne left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.
I have mostly reviewed the tests - i.e. focusing on the behavior of the recipes. I think these are very good additions. I found it hard to analyze each recipe change as they were all kind of different changes.

One tiny remark is the one-line test summaries are probably excessive, e.g.

// Skips a real newline escape, which a raw string would embed as a literal line break.

* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
*/

// Package lstutil holds small helpers shared across recipe packages for

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, these are generic enough to belong in rewrite-go. I'd suggest doing it as a follow-up rather than in this PR, since moving them upstream needs a rewrite-go PR, a release, and then a go.mod bump here to consume it, which would block this (already-approved) change on the release cycle.

When I pick up the follow-up I'll first check whether rewrite-go already exposes equivalents (e.g. a cursor helper for "is this block a function body") so we don't duplicate, and probably only upstream the generic ones (IsFunctionBodyBlock, BaseIndent, IsInitWrappedIf); IsErrNotNil is recipe-specific enough that it may stay local.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the follow-up PR: openrewrite/rewrite#8458

@bmuschko
bmuschko merged commit 4d18617 into main Aug 11, 2026
2 checks passed
@bmuschko
bmuschko deleted the bmuschko/pr-description-draft branch August 11, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants