Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve linting for defer Close type issues #2826

Closed
3 tasks done
willmurphyscode opened this issue May 1, 2024 · 2 comments
Closed
3 tasks done

Improve linting for defer Close type issues #2826

willmurphyscode opened this issue May 1, 2024 · 2 comments
Assignees

Comments

@willmurphyscode
Copy link
Contributor

willmurphyscode commented May 1, 2024

This issue is a follow up to prevent #2819 from happening again.

Syft has a method that should be called to clean up after file IO:

func CloseAndLogError(closer io.Closer, location string) {

Ideally, we'd like to enforce via linting:

That way, every time someone asks a file resolver for a ReadCloser, it would be guaranteed not to leak the handle.

@willmurphyscode
Copy link
Contributor Author

willmurphyscode commented May 1, 2024

Here's a quick implementation that could be wired up to golangci-lint. It hard-codes the name of the function that must defer for now, but that could be passed via config.

package analyzer

import (
	"go/ast"
	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/analysis/passes/inspect"
	"golang.org/x/tools/go/ast/inspector"
)

func run(pass *analysis.Pass) (any, error) {
	insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
	nodeFilter := []ast.Node{
		(*ast.ExprStmt)(nil),
		(*ast.DeferStmt)(nil),
	}
	insp.Preorder(nodeFilter, func(node ast.Node) {
		// if we have a *ast.ExprStmt that calls internal.CloseAndLogError, report a problem.
		// (if the function is correctly called in a defer statement, the node will have type *ast.DeferStmt)
		switch t := node.(type) {
		case *ast.ExprStmt:
			if !isExprStmtAllowed(t, pass) {
				pass.Reportf(t.Pos(), "internal.CloseAndLogError must be called in defer")
			}
		}
	})
	return nil, nil
}

func isExprStmtAllowed(e *ast.ExprStmt, pass *analysis.Pass) bool {
	call, ok := e.X.(*ast.CallExpr)
	if !ok {
		return true
	}
	sel, ok := call.Fun.(*ast.SelectorExpr)
	if !ok {
		return true
	}

	obj := pass.TypesInfo.Uses[sel.Sel]
	if obj == nil {
		return true
	}
	pkg := obj.Pkg()
	if pkg == nil {
		return true
	}

	if pkg.Path() == "github.com/anchore/syft/internal" && sel.Sel.Name == "CloseAndLogError" {
		return false
	}

	return true
}

func NewAnalyzer() *analysis.Analyzer {
	analyzer := analysis.Analyzer{
		Name:     "mustdefer",
		Doc:      "functions whose doc comemnt includes `*mustdefer*` must be invoked with the defer keyword",
		Run:      run,
		Requires: []*analysis.Analyzer{inspect.Analyzer},
	}
	return &analyzer
}

With a main function like this:

func main() {
	singlechecker.Main(analyzer.NewAnalyzer())
}

This is invoked like this:

$ ../investigations/golangci-custom-lint/mustdefer/main ./...
/Users/willmurphy/work/syft-clean/syft/linux/identify_release.go:74:4: internal.CloseAndLogError must be called in defer
/Users/willmurphy/work/syft-clean/syft/pkg/cataloger/generic/cataloger.go:132:3: internal.CloseAndLogError must be called in defer
/Users/willmurphy/work/syft-clean/syft/pkg/cataloger/golang/parse_go_binary.go:70:2: internal.CloseAndLogError must be called in defer
/Users/willmurphy/work/syft-clean/syft/pkg/cataloger/java/graalvm_native_image_cataloger.go:595:3: internal.CloseAndLogError must be called in defer

@willmurphyscode
Copy link
Contributor Author

Fixed by #2837

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

No branches or pull requests

1 participant