Skip to content

Commit 1455abb

Browse files
committed
deep-exit: ignore testable examples
1 parent dde8344 commit 1455abb

File tree

3 files changed

+136
-1
lines changed

3 files changed

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

testdata/deep_exit_test.go

Lines changed: 17 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,18 @@ func TestMain(m *testing.M) {
911
// call flag.Parse() here if TestMain uses flags
1012
os.Exit(m.Run())
1113
}
14+
15+
// Package level example
16+
func Example() {
17+
log.Fatal(errors.New("example"))
18+
}
19+
20+
// Testable example
21+
func ExampleFoo() {
22+
log.Fatal(errors.New("example"))
23+
}
24+
25+
// Not an example because it has an argument
26+
func ExampleBar(int) {
27+
log.Fatal(errors.New("example")) // MATCH /calls to log.Fatal only in main() or init() functions/
28+
}

0 commit comments

Comments
 (0)