-
Notifications
You must be signed in to change notification settings - Fork 395
[linter-miner] feat(linters): add osexitinlibrary linter #32448
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
Changes from all commits
aa8ee20
ad8139e
52fa808
235ceb0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The test only exercises the "flagged" case. The skip logic ( // testdata/src/cmd/main/main.go
package main
import "os"
func main() {
os.Exit(0) // should NOT be flagged
}Then add to the test: analysistest.Run(t, testdata, osexitinlibrary.Analyzer, "osexitinlibrary", "cmd/main") |
||
| testdata := analysistest.TestData() | ||
| analysistest.Run(t, testdata, osexitinlibrary.Analyzer, "osexitinlibrary") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package osexitinlibrary | ||
|
|
||
| import ( | ||
| "os" | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestMain(m *testing.M) { | ||
| os.Exit(m.Run()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/tdd] The check
ident.Name == "os"matches by identifier name only — it doesn't verify that theosidentifier actually refers to the standard libraryospackage. If a file imports a third-party package with the aliasos(unusual but valid Go), the linter would produce a false positive. Thegolang.org/x/tools/go/analysisframework provides type information viapass.TypesInfothat lets you confirm the import path:This is the recommended pattern in the
golang.org/x/tools/go/analysisdocumentation and makes the linter robust to aliased imports.