diff --git a/cmd/linters/main.go b/cmd/linters/main.go index d27850f6053..c9397e6cf5e 100644 --- a/cmd/linters/main.go +++ b/cmd/linters/main.go @@ -18,11 +18,13 @@ import ( "github.com/github/gh-aw/pkg/linters/excessivefuncparams" "github.com/github/gh-aw/pkg/linters/largefunc" + "github.com/github/gh-aw/pkg/linters/osexitinlibrary" ) func main() { multichecker.Main( excessivefuncparams.Analyzer, largefunc.Analyzer, + osexitinlibrary.Analyzer, ) } diff --git a/pkg/linters/osexitinlibrary/osexitinlibrary.go b/pkg/linters/osexitinlibrary/osexitinlibrary.go new file mode 100644 index 00000000000..fced873fc9b --- /dev/null +++ b/pkg/linters/osexitinlibrary/osexitinlibrary.go @@ -0,0 +1,56 @@ +// Package osexitinlibrary implements a Go analysis linter that flags +// os.Exit calls in library (pkg/) packages. +package osexitinlibrary + +import ( + "go/ast" + "path/filepath" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +// Analyzer is the os-exit-in-library analysis pass. +var Analyzer = &analysis.Analyzer{ + Name: "osexitinlibrary", + Doc: "reports os.Exit calls inside library packages where they bypass deferred cleanup and prevent testing", + URL: "https://github.com/github/gh-aw/tree/main/pkg/linters/osexitinlibrary", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, +} + +func run(pass *analysis.Pass) (any, error) { + pkgPath := pass.Pkg.Path() + // Skip packages under cmd/ entry-points — they are allowed to call os.Exit. + if strings.HasSuffix(pkgPath, "/main") || strings.Contains(pkgPath, "/cmd/") { + return nil, nil + } + + insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.CallExpr)(nil), + } + + insp.Preorder(nodeFilter, func(n ast.Node) { + call := n.(*ast.CallExpr) + if strings.HasSuffix(pkgPath, ".test") || strings.HasSuffix(filepath.Base(pass.Fset.Position(call.Pos()).Filename), "_test.go") { + return + } + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return + } + ident, ok := sel.X.(*ast.Ident) + if !ok { + return + } + if ident.Name == "os" && sel.Sel.Name == "Exit" { + pass.Reportf(call.Pos(), "os.Exit called in library package %s; move process termination to a cmd/ entry-point", pkgPath) + } + }) + + return nil, nil +} diff --git a/pkg/linters/osexitinlibrary/osexitinlibrary_test.go b/pkg/linters/osexitinlibrary/osexitinlibrary_test.go new file mode 100644 index 00000000000..a0cd11e3cc9 --- /dev/null +++ b/pkg/linters/osexitinlibrary/osexitinlibrary_test.go @@ -0,0 +1,16 @@ +//go:build !integration + +package osexitinlibrary_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + + "github.com/github/gh-aw/pkg/linters/osexitinlibrary" +) + +func TestOsExitInLibrary(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, osexitinlibrary.Analyzer, "osexitinlibrary") +} diff --git a/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary.go b/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary.go new file mode 100644 index 00000000000..0032ad5f146 --- /dev/null +++ b/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary.go @@ -0,0 +1,13 @@ +package osexitinlibrary + +import "os" + +// bad: os.Exit in a pkg/ package. +func stopProcess() { + os.Exit(1) // want `os.Exit called in library package` +} + +// ok: helper that does NOT call os.Exit. +func doWork() error { + return nil +} diff --git a/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary_test.go b/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary_test.go new file mode 100644 index 00000000000..da948ad9abd --- /dev/null +++ b/pkg/linters/osexitinlibrary/testdata/src/osexitinlibrary/osexitinlibrary_test.go @@ -0,0 +1,10 @@ +package osexitinlibrary + +import ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +}