Repository: mgechev/revive
Pull request #1156 (2024-12-03 13:05:34)

unused-param,unused-receiver: fix panic message
The option for `unused-param` and `unused-receiver` rules is `"allowRegex"`, not `"allowedRegex"`.


Patch:
diff --git a/rule/deep_exit.go b/rule/deep_exit.go
index 8441b50..bc09604 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/unused_param.go b/rule/unused_param.go
index a8514ac..08945da 100644
--- a/rule/unused_param.go
+++ b/rule/unused_param.go
@@ -21,28 +21,27 @@ type UnusedParamRule struct {
 func (r *UnusedParamRule) configure(args lint.Arguments) {
 	// while by default args is an array, i think it's good to provide structures inside it by default, not arrays or primitives
 	// it's more compatible to JSON nature of configurations
-	var allowedRegexStr string
+	var allowRegexStr string
 	if len(args) == 0 {
-		allowedRegexStr = "^_$"
+		allowRegexStr = "^_$"
 		r.failureMsg = "parameter '%s' seems to be unused, consider removing or renaming it as _"
 	} else {
 		// Arguments = [{}]
 		options := args[0].(map[string]any)
-		// Arguments = [{allowedRegex="^_"}]
+		// Arguments = [{allowRegex="^_"}]
 
-		if allowedRegexParam, ok := options["allowRegex"]; ok {
-			allowedRegexStr, ok = allowedRegexParam.(string)
+		if allowRegexParam, ok := options["allowRegex"]; ok {
+			allowRegexStr, ok = allowRegexParam.(string)
 			if !ok {
-				panic(fmt.Errorf("error configuring %s rule: allowedRegex is not string but [%T]", r.Name(), allowedRegexParam))
+				panic(fmt.Errorf("error configuring %s rule: allowRegex is not string but [%T]", r.Name(), allowRegexParam))
 			}
 		}
 	}
 	var err error
-	r.allowRegex, err = regexp.Compile(allowedRegexStr)
+	r.allowRegex, err = regexp.Compile(allowRegexStr)
 	if err != nil {
-		panic(fmt.Errorf("error configuring %s rule: allowedRegex is not valid regex [%s]: %v", r.Name(), allowedRegexStr, err))
+		panic(fmt.Errorf("error configuring %s rule: allowRegex is not valid regex [%s]: %v", r.Name(), allowRegexStr, err))
 	}
-
 	if r.failureMsg == "" {
 		r.failureMsg = "parameter '%s' seems to be unused, consider removing or renaming it to match " + r.allowRegex.String()
 	}
diff --git a/rule/unused_receiver.go b/rule/unused_receiver.go
index f1866cf..c88c0e3 100644
--- a/rule/unused_receiver.go
+++ b/rule/unused_receiver.go
@@ -21,26 +21,26 @@ type UnusedReceiverRule struct {
 func (r *UnusedReceiverRule) configure(args lint.Arguments) {
 	// while by default args is an array, i think it's good to provide structures inside it by default, not arrays or primitives
 	// it's more compatible to JSON nature of configurations
-	var allowedRegexStr string
+	var allowRegexStr string
 	if len(args) == 0 {
-		allowedRegexStr = "^_$"
+		allowRegexStr = "^_$"
 		r.failureMsg = "method receiver '%s' is not referenced in method's body, consider removing or renaming it as _"
 	} else {
 		// Arguments = [{}]
 		options := args[0].(map[string]any)
-		// Arguments = [{allowedRegex="^_"}]
+		// Arguments = [{allowRegex="^_"}]
 
-		if allowedRegexParam, ok := options["allowRegex"]; ok {
-			allowedRegexStr, ok = allowedRegexParam.(string)
+		if allowRegexParam, ok := options["allowRegex"]; ok {
+			allowRegexStr, ok = allowRegexParam.(string)
 			if !ok {
-				panic(fmt.Errorf("error configuring [unused-receiver] rule: allowedRegex is not string but [%T]", allowedRegexParam))
+				panic(fmt.Errorf("error configuring [unused-receiver] rule: allowRegex is not string but [%T]", allowRegexParam))
 			}
 		}
 	}
 	var err error
-	r.allowRegex, err = regexp.Compile(allowedRegexStr)
+	r.allowRegex, err = regexp.Compile(allowRegexStr)
 	if err != nil {
-		panic(fmt.Errorf("error configuring [unused-receiver] rule: allowedRegex is not valid regex [%s]: %v", allowedRegexStr, err))
+		panic(fmt.Errorf("error configuring [unused-receiver] rule: allowRegex is not valid regex [%s]: %v", allowRegexStr, err))
 	}
 	if r.failureMsg == "" {
 		r.failureMsg = "method receiver '%s' is not referenced in method's body, consider removing or renaming it to match " + r.allowRegex.String()


Test patch:
diff --git a/rule/deep_exit_test.go b/rule/deep_exit_test.go
new file mode 100644
index 0000000..a2e7d93
--- /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 697643a..2477113 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/
+}