mirror of
				https://github.com/mgechev/revive.git
				synced 2025-10-30 23:37:49 +02:00 
			
		
		
		
	deep-exit: ignore testable examples (#1155)
This commit is contained in:
		| @@ -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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										82
									
								
								rule/deep_exit_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								rule/deep_exit_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										22
									
								
								testdata/deep_exit_test.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								testdata/deep_exit_test.go
									
									
									
									
										vendored
									
									
								
							| @@ -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/ | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user