Skip to content

uncheckedflushreturn: node filter omits DeferStmt — defer w.Flush() (the most common discard pattern) is never flagged #49902

Description

@github-actions

Problem

pkg/linters/uncheckedflushreturn/uncheckedflushreturn.go is meant to catch discarded error returns from Flush(). Its AST node filter is:

nodeFilter := []ast.Node{(*ast.ExprStmt)(nil), (*ast.AssignStmt)(nil)}

(uncheckedflushreturn.go:45). This covers a bare w.Flush() statement (ExprStmt) and _ = w.Flush() (AssignStmt), but ast.DeferStmt is not in the filter at all. defer w.Flush() wraps the call in its own *ast.DeferStmt node — it is not an ExprStmt or AssignStmt — so insp.Preorder never visits it and the call is invisible to this analyzer, regardless of type.

Evidence

  • uncheckedflushreturn.go:45-63: run() only registers ExprStmt/AssignStmt in nodeFilter; there is no case *ast.DeferStmt anywhere in the file.
  • testdata/src/uncheckedflushreturn/uncheckedflushreturn.go has zero test cases using defer. bad()/badBlankAssign() test the bare-statement and blank-assign forms only; good()/suppressed() likewise never exercise defer. This is an untested gap, not a documented/intentional scope decision (nothing in the package doc or ADR calls out defer as out of scope).
  • defer bufWriter.Flush() (or defer w.Flush() for tabwriter/gzip.Writer/similar) is widely recognized as the single most common way Go code silently drops a flush error — it is the canonical motivating example in Go's own error-handling guidance for exactly this class of bug, and is the pattern errcheck-style tools specifically target for Close/Flush. That makes this the highest-value case for a linter with this Doc string ("reports Flush() method calls where the error return is discarded") to miss.
  • No current production hits in pkg/ (grep -rn "defer.*\.Flush()" pkg/ is empty), so this is a coverage gap rather than an active false negative today — but it means the linter provides no protection if such code is added later, which is exactly the scenario it exists to prevent.

Impact

A developer who writes defer w.Flush() — the idiomatic-looking but incorrect pattern for flush error handling — gets no diagnostic from this linter, even though w.Flush() and _ = w.Flush() right next to it would both be flagged. The analyzer's own two non-deferred forms are covered; the deferred form, arguably the primary motivating case, silently passes through.

Recommendation

Add *ast.DeferStmt to nodeFilter and a case in the switch that unwraps stmt.Call (an *ast.CallExpr) and runs it through the existing isFlushCallReturningError check, then reportUncheckedFlush — mirroring checkDiscardedFlushExpr but sourced from DeferStmt.Call instead of ExprStmt.X. Add a deferBad() golden test case (and a deferGood()/deferSuppressed() if the project's convention is to test the nolint escape hatch per form, matching the existing bad/good/suppressed triplet structure) to testdata/src/uncheckedflushreturn/uncheckedflushreturn.go.

Validation checklist

  • nodeFilter includes (*ast.DeferStmt)(nil)
  • New case handles stmt.Call via isFlushCallReturningError + reportUncheckedFlush
  • New testdata case: defer w.Flush() flagged with the same diagnostic message
  • New testdata case: defer w.Flush() (nolint/redacted):uncheckedflushreturn correctly suppressed
  • analysistest.Run passes with updated golden expectations

Effort

Small - one new node-filter entry, one new case function (~10-15 lines mirroring checkDiscardedFlushExpr), plus testdata additions.

Generated by 🤖 Sergo - Serena Go Expert · agent · 167.6 AIC · ⌖ 33.2 AIC · ⊞ 6K ·

  • expires on Aug 9, 2026, 9:05 PM UTC-08:00

Metadata

Metadata

Labels

cookieIssue Monster Loves Cookies!sergo

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions