Skip to content

Add redundant-test-main-exit rule #1208

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

Merged
merged 5 commits into from
Jan 29, 2025
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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ Here's how `revive` is different from `golint`:
- [SARIF](#sarif)
- [Extensibility](#extensibility)
- [Writing a Custom Rule](#writing-a-custom-rule)
- [Example](#example)
- [Using `revive` as a library](#using-revive-as-a-library)
- [Custom Formatter](#custom-formatter)
- [Speed Comparison](#speed-comparison)
Expand Down Expand Up @@ -561,6 +560,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a
| [`filename-format`](./RULES_DESCRIPTIONS.md#filename-format) | regular expression (optional) | Enforces the formatting of filenames | no | no |
| [`redundant-build-tag`](./RULES_DESCRIPTIONS.md#redundant-build-tag) | n/a | Warns about redundant `// +build` comment lines | no | no |
| [`use-errors-new`](./RULES_DESCRIPTIONS.md#use-errors-new) | n/a | Spots calls to `fmt.Errorf` that can be replaced by `errors.New` | no | no |
| [`redundant-test-main-exit`](./RULES_DESCRIPTIONS.md#redundant-test-main-exit) | n/a | Suggests removing `Exit` call in `TestMain` function for test files | no | no |

## Configurable rules

Expand Down
9 changes: 8 additions & 1 deletion RULES_DESCRIPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ List of all available rules.
- [redefines-builtin-id](#redefines-builtin-id)
- [redundant-import-alias](#redundant-import-alias)
- [redundant-build-tag](#redundant-build-tag)
- [redundant-test-main-exit](#redundant-test-main-exit)
- [string-format](#string-format)
- [string-of-int](#string-of-int)
- [struct-tag](#struct-tag)
Expand Down Expand Up @@ -284,7 +285,7 @@ _Configuration_: N/A
_Description_: This rule warns on some common mistakes when using `defer` statement. It currently alerts on the following situations:

| name | description |
|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| call-chain | even if deferring call-chains of the form `foo()()` is valid, it does not helps code understanding (only the last call is deferred) |
| loop | deferring inside loops can be misleading (deferred functions are not executed at the end of the loop iteration but of the current function) and it could lead to exhausting the execution stack |
| method-call | deferring a call to a method can lead to subtle bugs if the method does not have a pointer receiver |
Expand Down Expand Up @@ -809,6 +810,12 @@ _Description_: This rule warns about redundant build tag comments `// +build` wh

_Configuration_: N/A

## redundant-test-main-exit

_Description_: This rule warns about redundant `Exit` calls in the `TestMain` function, as the Go test runner automatically handles program termination starting from Go 1.15.

_Configuration_: N/A

## string-format

_Description_: This rule allows you to configure a list of regular expressions that string literals in certain function calls are checked against.
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ var allRules = append([]lint.Rule{
&rule.FilenameFormatRule{},
&rule.RedundantBuildTagRule{},
&rule.UseErrorsNewRule{},
&rule.RedundantTestMainExitRule{},
}, defaultRules...)

// allFormatters is a list of all available formatters to output the linting results.
Expand Down
75 changes: 75 additions & 0 deletions rule/redundant_test_main_exit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package rule

import (
"fmt"
"go/ast"

"github.com/mgechev/revive/lint"
)

// RedundantTestMainExitRule suggests removing Exit call in TestMain function for test files.
type RedundantTestMainExitRule struct{}

// Apply applies the rule to given file.
func (*RedundantTestMainExitRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
var failures []lint.Failure

onFailure := func(failure lint.Failure) {
failures = append(failures, failure)
}

w := &lintRedundantTestMainExit{onFailure: onFailure, isTestFile: file.IsTest()}
ast.Walk(w, file.AST)
return failures
}

// Name returns the rule name.
func (*RedundantTestMainExitRule) Name() string {
return "redundant-test-main-exit"
}

type lintRedundantTestMainExit struct {
onFailure func(lint.Failure)
isTestFile bool
}

func (w *lintRedundantTestMainExit) Visit(node ast.Node) ast.Visitor {
if !w.isTestFile {
return nil // skip analysis of this file if it is not a test file
}

se, ok := node.(*ast.ExprStmt)
if !ok {
return w
}
ce, ok := se.X.(*ast.CallExpr)
if !ok {
return w
}

fc, ok := ce.Fun.(*ast.SelectorExpr)
if !ok {
return w
}
id, ok := fc.X.(*ast.Ident)
if !ok {
return w
}

pkg := id.Name
fn := fc.Sel.Name
if isCallToExitFunction(pkg, fn) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

isCallToExitFunction will return true even for function calls other than os.Exit (for example log.Fatal, log.Panic ...) That behavior is okay for what we check here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @chavacava, thank you for your detailed review and suggestions!

I’m not entirely sure about including logging functions other than os.Exit or syscall.Exit. It’s uncommon (in my experience) to call logging functions that internally invoke Exit, but I don't see any downside to including them in this check.

What’s your opinion on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The issue with including logging functions is that the common failure message may not be appropriate for them, as these functions are not necessarily redundant: they first log a message and then exit.

w.onFailure(lint.Failure{
Confidence: 1,
Node: ce,
Category: lint.FailureCategoryBadPractice,
Failure: fmt.Sprintf("redundant call to %s.%s in TestMain function, the test runner will handle it automatically as of Go 1.15", pkg, fn),
})
}

return w
}

func (w *lintRedundantTestMainExit) isTestMain(fd *ast.FuncDecl) bool {
return w.isTestFile && fd.Name.Name == "TestMain"
}
12 changes: 12 additions & 0 deletions test/redundant_test_main_exit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package test

import (
"testing"

"github.com/mgechev/revive/rule"
)

func TestRedundantTestMainExit(t *testing.T) {
testRule(t, "redundant_test_main_exit_test", &rule.RedundantTestMainExitRule{})
testRule(t, "redundant_test_main_exit", &rule.RedundantTestMainExitRule{})
}
23 changes: 23 additions & 0 deletions testdata/redundant_test_main_exit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fixtures

import (
"fmt"
"os"
"testing"
)

func TestMain(m *testing.M) {
setup()
i := m.Run()
teardown()
// must not match because this is not a test file
os.Exit(i)
}

func setup() {
fmt.Println("Setup")
}

func teardown() {
fmt.Println("Teardown")
}
28 changes: 28 additions & 0 deletions testdata/redundant_test_main_exit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package fixtures

import (
"fmt"
"os"
"syscall"
"testing"
)

func TestMain(m *testing.M) {
setup()
i := m.Run()
teardown()
os.Exit(i) // MATCH /redundant call to os.Exit in TestMain function, the test runner will handle it automatically as of Go 1.15/
syscall.Exit(i) // MATCH /redundant call to syscall.Exit in TestMain function, the test runner will handle it automatically as of Go 1.15/
}

func setup() {
fmt.Println("Setup")
}

func teardown() {
fmt.Println("Teardown")
}

func Test_function(t *testing.T) {
t.Error("Fail")
}