From a48710b04b4dbe5e81af5c2a391d8a11444291e5 Mon Sep 17 00:00:00 2001 From: Oleksandr Redko Date: Tue, 3 Dec 2024 16:39:07 +0200 Subject: [PATCH] deep-exit: ignore testable examples (#1155) --- rule/deep_exit.go | 32 ++++++++++++++- rule/deep_exit_test.go | 82 ++++++++++++++++++++++++++++++++++++++ testdata/deep_exit_test.go | 22 ++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 rule/deep_exit_test.go diff --git a/rule/deep_exit.go b/rule/deep_exit.go index 8441b50de..bc0960433 100644 --- a/rule/deep_exit.go +++ b/rule/deep_exit.go @@ -3,6 +3,9 @@ package rule import ( "fmt" "go/ast" + "strings" + "unicode" + "unicode/utf8" "github.com/mgechev/revive/lint" ) @@ -76,5 +79,32 @@ func (w *lintDeepExit) Visit(node ast.Node) ast.Visitor { func (w *lintDeepExit) mustIgnore(fd *ast.FuncDecl) bool { fn := fd.Name.Name - return fn == "init" || fn == "main" || (w.isTestFile && fn == "TestMain") + return fn == "init" || fn == "main" || w.isTestMain(fd) || w.isTestExample(fd) +} + +func (w *lintDeepExit) isTestMain(fd *ast.FuncDecl) bool { + return w.isTestFile && fd.Name.Name == "TestMain" +} + +// isTestExample returns true if the function is a testable example function. +// See https://go.dev/blog/examples#examples-are-tests for more information. +// +// Inspired by https://github.com/golang/go/blob/go1.23.0/src/go/doc/example.go#L72-L77 +func (w *lintDeepExit) isTestExample(fd *ast.FuncDecl) bool { + if !w.isTestFile { + return false + } + name := fd.Name.Name + const prefix = "Example" + if !strings.HasPrefix(name, prefix) { + return false + } + if len(name) == len(prefix) { // "Example" is a package level example + return len(fd.Type.Params.List) == 0 + } + r, _ := utf8.DecodeRuneInString(name[len(prefix):]) + if unicode.IsLower(r) { + return false + } + return len(fd.Type.Params.List) == 0 } diff --git a/rule/deep_exit_test.go b/rule/deep_exit_test.go new file mode 100644 index 000000000..a2e7d93c0 --- /dev/null +++ b/rule/deep_exit_test.go @@ -0,0 +1,82 @@ +package rule + +import ( + "go/ast" + "go/parser" + "go/token" + "slices" + "testing" +) + +func TestLintDeepExit_isTestExample(t *testing.T) { + tests := []struct { + name string + funcDecl string + isTestFile bool + want bool + }{ + { + name: "Package level example", + funcDecl: "func Example() {}", + isTestFile: true, + want: true, + }, + { + name: "Function example", + funcDecl: "func ExampleFunction() {}", + isTestFile: true, + want: true, + }, + { + name: "Method example", + funcDecl: "func ExampleType_Method() {}", + isTestFile: true, + want: true, + }, + { + name: "Wrong example function", + funcDecl: "func Examplemethod() {}", + isTestFile: true, + want: false, + }, + { + name: "Not an example", + funcDecl: "func NotAnExample() {}", + isTestFile: true, + want: false, + }, + { + name: "Example with parameters", + funcDecl: "func ExampleWithParams(a int) {}", + isTestFile: true, + want: false, + }, + { + name: "Not a test file", + funcDecl: "func Example() {}", + isTestFile: false, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := token.NewFileSet() + node, err := parser.ParseFile(fs, "", "package main\n"+tt.funcDecl, parser.AllErrors) + if err != nil { + t.Fatal(err) + } + idx := slices.IndexFunc(node.Decls, func(decl ast.Decl) bool { + _, ok := decl.(*ast.FuncDecl) + return ok + }) + fd := node.Decls[idx].(*ast.FuncDecl) + + w := &lintDeepExit{isTestFile: tt.isTestFile} + got := w.isTestExample(fd) + if got != tt.want { + t.Errorf("isTestExample() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/testdata/deep_exit_test.go b/testdata/deep_exit_test.go index 697643ae9..2477113fb 100644 --- a/testdata/deep_exit_test.go +++ b/testdata/deep_exit_test.go @@ -1,6 +1,8 @@ package fixtures import ( + "errors" + "log" "os" "testing" ) @@ -9,3 +11,23 @@ func TestMain(m *testing.M) { // call flag.Parse() here if TestMain uses flags os.Exit(m.Run()) } + +// Testable package level example +func Example() { + log.Fatal(errors.New("example")) +} + +// Testable function example +func ExampleFoo() { + log.Fatal(errors.New("example")) +} + +// Testable method example +func ExampleBar_Qux() { + log.Fatal(errors.New("example")) +} + +// Not an example because it has an argument +func ExampleBar(int) { + log.Fatal(errors.New("example")) // MATCH /calls to log.Fatal only in main() or init() functions/ +}