Skip to content

Commit

Permalink
deep-exit: ignore testable examples
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandear committed Dec 3, 2024
1 parent dde8344 commit bc56d2d
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 1 deletion.
32 changes: 31 additions & 1 deletion rule/deep_exit.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package rule
import (
"fmt"
"go/ast"
"strings"
"unicode"
"unicode/utf8"

"github.com/mgechev/revive/lint"
)
Expand Down Expand Up @@ -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
}
88 changes: 88 additions & 0 deletions rule/deep_exit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package rule

import (
"go/ast"
"go/parser"
"go/token"
"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.Fatalf("failed to parse function declaration: %v", err)
}

var fd *ast.FuncDecl
for _, decl := range node.Decls {
if fn, ok := decl.(*ast.FuncDecl); ok {
fd = fn
break
}
}

if fd == nil {
t.Fatalf("failed to find function declaration")
}

w := &lintDeepExit{isTestFile: tt.isTestFile}
got := w.isTestExample(fd)
if got != tt.want {
t.Errorf("isTestExample() = %v, want %v", got, tt.want)
}
})
}
}
22 changes: 22 additions & 0 deletions testdata/deep_exit_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package fixtures

import (
"errors"
"log"
"os"
"testing"
)
Expand All @@ -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/
}

0 comments on commit bc56d2d

Please sign in to comment.