Repository navigation
[linter-miner] Add unchecked-deferred-close linter - #67481
Conversation
The unchecked-deferred-close linter reports defer close() calls that ignore
error return values, which can silently hide resource cleanup failures.
This linter addresses a common pattern found throughout the codebase where
file/network resource Close() methods are deferred without checking errors.
Key findings from codebase analysis (discovered via code-pattern-scanner):
- Detected in pkg/cli/outcome_eval_jsonl.go
- Detected in pkg/cli/firewall_log.go
- Detected in pkg/cli/update_workflows.go
- And 10+ other files with similar patterns
The linter integrates with the existing error-checking infrastructure and
respects nolint directives for cases where errors should be explicitly ignored.
Example flagged pattern:
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // ERROR: ignores error return
Recommended fix:
defer func() {
if err := f.Close(); err != nil {
// handle error appropriately
}
}()
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🔎 PR Code Quality Reviewer is reviewing code quality for this pull request... |
|
🔬 Test Quality Sentinel is analyzing test quality on this pull request... |
|
✂️ Ponytail Reviewer has started processing this pull request |
|
🔍 Design Decision Gate 🏗️ is checking for design decision records on this pull request... |
|
🧠 Matt Pocock Skills Reviewer is reviewing this pull request using Matt Pocock's engineering skills... |
There was a problem hiding this comment.
🟡 Changes recommended
The analyzer misses common method-set cases, and registration leaves repository synchronization tests failing.
2 open findings
What changed in this PR
Adds a Go analyzer that detects deferred Close() calls whose errors are ignored.
Changes:
- Implements and registers
unchecked_deferredclose. - Adds analyzer tests and fixtures for accepted and rejected patterns.
| File | Description |
|---|---|
unchecked_deferredclose.go |
Implements deferred-close analysis. |
unchecked_deferredclose_test.go |
Runs analyzer fixtures. |
testdata/src/basic/good.go |
Adds accepted examples. |
testdata/src/basic/bad.go |
Adds a reported example. |
registry.go |
Registers the analyzer. |
🧠 Review effort: Balanced
| typeassertionnil.Analyzer, | ||
| typeassertionokdiscarded.Analyzer, | ||
| uncheckedsliceindex.Analyzer, | ||
| unchecked_deferredclose.Analyzer, |
There was a problem hiding this comment.
Updated the analyzer inventory in doc.go, both README lists, documentedAnalyzers(), and the doc-sync enforcement tracking. It is documented as not yet enforced pending remediation of existing findings. Committed in 31de3ea.
| // Get type of the object being closed (the receiver) | ||
| receiverType := pass.TypesInfo.TypeOf(selector.X) | ||
| if receiverType == nil { | ||
| return | ||
| } | ||
|
|
||
| // Look for a Close method on this type | ||
| closeMethod, found := lookupClose(receiverType) | ||
| if !found { | ||
| return | ||
| } | ||
|
|
||
| // Check if Close() returns an error | ||
| if sig, ok := closeMethod.Type().(*types.Signature); ok { | ||
| if sig.Results() == nil || sig.Results().Len() == 0 { | ||
| return | ||
| } | ||
|
|
||
| // Check if the return type is error | ||
| lastResult := sig.Results().At(sig.Results().Len() - 1) | ||
| if lastResult.Type() != builtinErrorType { | ||
| return | ||
| } |
There was a problem hiding this comment.
The analyzer now checks the resolved method selection signature, with fixtures for io.Closer, a named interface, an embedded closer, and a type parameter. Committed in 31de3ea.
|
@copilot address the following outstanding work in one pass:
Push the necessary fixes, reply to each listed review thread and resolve it when addressed. Ignore feedback already answered or resolved. Use the pr-finisher skill and stop when only human review or CI remains; do not trigger CI. Sous-chef head: 15a9f5f
|
…d-deferred-close-d23cdf3b8bd27b8e Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>


Summary
This PR implements a new Go static analyzer linter that reports
defer close()calls that ignore error return values, which can silently hide resource cleanup failures.Problem
Go's
io.Closerinterface (and many similar types) defineClose()to return an error that callers should handle. However, it's common to see code that defers Close() without checking errors:This can mask real issues during resource cleanup, such as:
Evidence
The
code-pattern-scanneragent found this pattern in the actual gh-aw codebase:pkg/cli/outcome_eval_jsonl.go:14-defer f.Close()pkg/cli/firewall_log.go:18-defer file.Close()pkg/cli/update_workflows.go:25-defer resp.Body.Close()pkg/cli/bootstrap_profile_github_app.go:42-defer listener.Close()Solution
The new
unchecked_deferredcloselinter:defer obj.Close()statements(nolint/redacted):unchecked_deferredclosedirectivesRecommended Fix
Replace simple defer with explicit error handling:
Testing
pkg/linters/registry.go✅Implementation Details
pkg/linters/unchecked-deferred-close/unchecked_deferredclosego/typesto verify Close() signatureFiles Changed
pkg/linters/unchecked-deferred-close/unchecked_deferredclose.go- Main linter implementationpkg/linters/unchecked-deferred-close/unchecked_deferredclose_test.go- Test suitepkg/linters/unchecked-deferred-close/testdata/src/basic/- Test fixturespkg/linters/registry.go- Register new analyzer