Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/linters/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"github.com/github/gh-aw/pkg/linters/typeassertionnil"
"github.com/github/gh-aw/pkg/linters/typeassertionokdiscarded"
uncheckedsliceindex "github.com/github/gh-aw/pkg/linters/unchecked-slice-index"
unchecked_deferredclose "github.com/github/gh-aw/pkg/linters/unchecked-deferred-close"

Check failure on line 79 in pkg/linters/registry.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (2)

File is not properly formatted (gofmt)

Check failure on line 79 in pkg/linters/registry.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (0)

File is not properly formatted (gofmt)

Check failure on line 79 in pkg/linters/registry.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (1)

File is not properly formatted (gofmt)
"github.com/github/gh-aw/pkg/linters/uncheckedflushreturn"
"github.com/github/gh-aw/pkg/linters/uncheckedtypeassertion"
"github.com/github/gh-aw/pkg/linters/walkfuncerrshadow"
Expand Down Expand Up @@ -166,6 +167,7 @@
typeassertionnil.Analyzer,
typeassertionokdiscarded.Analyzer,
uncheckedsliceindex.Analyzer,
unchecked_deferredclose.Analyzer,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

uncheckedtypeassertion.Analyzer,
uncheckedflushreturn.Analyzer,
walkfuncerrshadow.Analyzer,
Expand Down
15 changes: 15 additions & 0 deletions pkg/linters/unchecked-deferred-close/testdata/src/basic/bad.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package basic

import (
"os"
)

func bad(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // want "defer close\\(\\) call ignores error return value"
// ... use f
return nil
}
30 changes: 30 additions & 0 deletions pkg/linters/unchecked-deferred-close/testdata/src/basic/good.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package basic

import (
"fmt"
"os"
)

func good(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
fmt.Fprintf(os.Stderr, "close error: %v\n", err)
}
}()
// ... use f
return nil
}

// NoReturnClose is a mock type that has Close() with no error return
type NoReturnClose struct{}

func (n *NoReturnClose) Close() {}

func goodNoError() {
n := &NoReturnClose{}
defer n.Close() // OK: Close() returns nothing
}
132 changes: 132 additions & 0 deletions pkg/linters/unchecked-deferred-close/unchecked_deferredclose.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Package unchecked deferredclose implements a Go analysis linter that flags
// defer close() calls without error checking, which can hide resource cleanup failures.
package unchecked_deferredclose

import (
"go/ast"
"go/types"

"golang.org/x/tools/go/analysis"

"github.com/github/gh-aw/pkg/linters/internal/analyzerutil"
"github.com/github/gh-aw/pkg/linters/internal/filecheck"
"github.com/github/gh-aw/pkg/linters/internal/nolint"
)

// Analyzer is the unchecked-deferred-close analysis pass.
var Analyzer = analyzerutil.New("unchecked_deferredclose", "reports defer close() calls without error checking that may hide resource cleanup failures", run)

var builtinErrorType = types.Universe.Lookup("error").Type()

func run(pass *analysis.Pass) (any, error) {
noLintIndex, generatedFiles, err := analyzerutil.Indexes(pass)
if err != nil {
return nil, err
}

nodeFilter := []ast.Node{(*ast.DeferStmt)(nil)}
return analyzerutil.Preorder(pass, nodeFilter, func(n ast.Node) {
analyzeDeferClose(pass, n, generatedFiles, noLintIndex)
})
}

// analyzeDeferClose checks for defer close() calls that don't handle errors.
func analyzeDeferClose(pass *analysis.Pass, n ast.Node, generatedFiles filecheck.GeneratedIndex, noLintIndex nolint.DirectiveIndex) {
deferStmt, ok := n.(*ast.DeferStmt)
if !ok {
return
}

// The defer statement's Call is already a *CallExpr
callExpr := deferStmt.Call
if callExpr == nil {
return
}

// Check if the call is to a Close() method
selector, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok || selector.Sel.Name != "Close" {
return
}

// Ensure it has no arguments
if len(callExpr.Args) != 0 {
return
}

position := pass.Fset.PositionFor(deferStmt.Pos(), false)
if filecheck.ShouldSkipFilename(position.Filename, generatedFiles) {
return
}

if nolint.HasDirectiveForLinter(position, noLintIndex, "unchecked_deferredclose") {
return
}

// Get the type of the receiver to check if Close() returns an error
if pass.TypesInfo == nil {
return
}

// 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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.


// Report: defer close() ignores error return
pass.Reportf(
deferStmt.Pos(),
"defer close() call ignores error return value; consider handling errors explicitly",
)
}
}

// lookupClose searches for a Close method on the given type.
func lookupClose(typ types.Type) (types.Object, bool) {
// Unwrap pointer type if necessary
ptr, ok := typ.(*types.Pointer)
if ok {
typ = ptr.Elem()
}

// If it's a named type, get its underlying type
if named, ok := typ.(*types.Named); ok {
for i := 0; i < named.NumMethods(); i++ {

Check failure on line 113 in pkg/linters/unchecked-deferred-close/unchecked_deferredclose.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (2)

stditerators: NumMethods/Method loop can simplified using Named.Methods iteration (modernize)

Check failure on line 113 in pkg/linters/unchecked-deferred-close/unchecked_deferredclose.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (0)

for loop can be changed to use an integer range (Go 1.22+)
method := named.Method(i)
if method.Name() == "Close" {
return method, true
}
}
}

// Check interface types
if iface, ok := typ.(*types.Interface); ok {
for i := 0; i < iface.NumMethods(); i++ {

Check failure on line 123 in pkg/linters/unchecked-deferred-close/unchecked_deferredclose.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (2)

stditerators: NumMethods/Method loop can simplified using Interface.Methods iteration (modernize)

Check failure on line 123 in pkg/linters/unchecked-deferred-close/unchecked_deferredclose.go

View workflow job for this annotation

GitHub Actions / lint-go-golangci (0)

for loop can be changed to use an integer range (Go 1.22+)
method := iface.Method(i)
if method.Name() == "Close" {
return method, true
}
}
}

return nil, false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//go:build !integration

// Package unchecked_deferredclose_test provides tests for the unchecked_deferredclose analyzer.
package unchecked_deferredclose_test

import (
"testing"

"golang.org/x/tools/go/analysis/analysistest"

unchecked_deferredclose "github.com/github/gh-aw/pkg/linters/unchecked-deferred-close"
)

func TestUncheckedDeferredClose(t *testing.T) {
t.Parallel()
analysistest.Run(t, analysistest.TestData(), unchecked_deferredclose.Analyzer, "basic")
}
Loading