Skip to content

Commit a48710b

Browse files
authored
deep-exit: ignore testable examples (#1155)
1 parent dde8344 commit a48710b

File tree

3 files changed

+135
-1
lines changed

3 files changed

+135
-1
lines changed

rule/deep_exit.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package rule
33
import (
44
"fmt"
55
"go/ast"
6+
"strings"
7+
"unicode"
8+
"unicode/utf8"
69

710
"github.com/mgechev/revive/lint"
811
)
@@ -76,5 +79,32 @@ func (w *lintDeepExit) Visit(node ast.Node) ast.Visitor {
7679
func (w *lintDeepExit) mustIgnore(fd *ast.FuncDecl) bool {
7780
fn := fd.Name.Name
7881

79-
return fn == "init" || fn == "main" || (w.isTestFile && fn == "TestMain")
82+
return fn == "init" || fn == "main" || w.isTestMain(fd) || w.isTestExample(fd)
83+
}
84+
85+
func (w *lintDeepExit) isTestMain(fd *ast.FuncDecl) bool {
86+
return w.isTestFile && fd.Name.Name == "TestMain"
87+
}
88+
89+
// isTestExample returns true if the function is a testable example function.
90+
// See https://go.dev/blog/examples#examples-are-tests for more information.
91+
//
92+
// Inspired by https://github.com/golang/go/blob/go1.23.0/src/go/doc/example.go#L72-L77
93+
func (w *lintDeepExit) isTestExample(fd *ast.FuncDecl) bool {
94+
if !w.isTestFile {
95+
return false
96+
}
97+
name := fd.Name.Name
98+
const prefix = "Example"
99+
if !strings.HasPrefix(name, prefix) {
100+
return false
101+
}
102+
if len(name) == len(prefix) { // "Example" is a package level example
103+
return len(fd.Type.Params.List) == 0
104+
}
105+
r, _ := utf8.DecodeRuneInString(name[len(prefix):])
106+
if unicode.IsLower(r) {
107+
return false
108+
}
109+
return len(fd.Type.Params.List) == 0
80110
}

rule/deep_exit_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package rule
2+
3+
import (
4+
"go/ast"
5+
"go/parser"
6+
"go/token"
7+
"slices"
8+
"testing"
9+
)
10+
11+
func TestLintDeepExit_isTestExample(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
funcDecl string
15+
isTestFile bool
16+
want bool
17+
}{
18+
{
19+
name: "Package level example",
20+
funcDecl: "func Example() {}",
21+
isTestFile: true,
22+
want: true,
23+
},
24+
{
25+
name: "Function example",
26+
funcDecl: "func ExampleFunction() {}",
27+
isTestFile: true,
28+
want: true,
29+
},
30+
{
31+
name: "Method example",
32+
funcDecl: "func ExampleType_Method() {}",
33+
isTestFile: true,
34+
want: true,
35+
},
36+
{
37+
name: "Wrong example function",
38+
funcDecl: "func Examplemethod() {}",
39+
isTestFile: true,
40+
want: false,
41+
},
42+
{
43+
name: "Not an example",
44+
funcDecl: "func NotAnExample() {}",
45+
isTestFile: true,
46+
want: false,
47+
},
48+
{
49+
name: "Example with parameters",
50+
funcDecl: "func ExampleWithParams(a int) {}",
51+
isTestFile: true,
52+
want: false,
53+
},
54+
{
55+
name: "Not a test file",
56+
funcDecl: "func Example() {}",
57+
isTestFile: false,
58+
want: false,
59+
},
60+
}
61+
62+
for _, tt := range tests {
63+
t.Run(tt.name, func(t *testing.T) {
64+
fs := token.NewFileSet()
65+
node, err := parser.ParseFile(fs, "", "package main\n"+tt.funcDecl, parser.AllErrors)
66+
if err != nil {
67+
t.Fatal(err)
68+
}
69+
idx := slices.IndexFunc(node.Decls, func(decl ast.Decl) bool {
70+
_, ok := decl.(*ast.FuncDecl)
71+
return ok
72+
})
73+
fd := node.Decls[idx].(*ast.FuncDecl)
74+
75+
w := &lintDeepExit{isTestFile: tt.isTestFile}
76+
got := w.isTestExample(fd)
77+
if got != tt.want {
78+
t.Errorf("isTestExample() = %v, want %v", got, tt.want)
79+
}
80+
})
81+
}
82+
}

testdata/deep_exit_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package fixtures
22

33
import (
4+
"errors"
5+
"log"
46
"os"
57
"testing"
68
)
@@ -9,3 +11,23 @@ func TestMain(m *testing.M) {
911
// call flag.Parse() here if TestMain uses flags
1012
os.Exit(m.Run())
1113
}
14+
15+
// Testable package level example
16+
func Example() {
17+
log.Fatal(errors.New("example"))
18+
}
19+
20+
// Testable function example
21+
func ExampleFoo() {
22+
log.Fatal(errors.New("example"))
23+
}
24+
25+
// Testable method example
26+
func ExampleBar_Qux() {
27+
log.Fatal(errors.New("example"))
28+
}
29+
30+
// Not an example because it has an argument
31+
func ExampleBar(int) {
32+
log.Fatal(errors.New("example")) // MATCH /calls to log.Fatal only in main() or init() functions/
33+
}

0 commit comments

Comments
 (0)