Repository: mgechev/revive
Pull request #1168 (2024-12-04 15:26:16)

refactor (rule/get-return): replace AST walker by iteration over declarations
Simplifies get-return rule by replacing AST walker by an iteration over global declarations


Patch:
diff --git a/go.mod b/go.mod
index 347fd70..2c48d4b 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/spf13/afero v1.11.0
 	golang.org/x/mod v0.22.0
-	golang.org/x/tools v0.27.0
+	golang.org/x/tools v0.28.0
 )
 
 require (
@@ -22,6 +22,6 @@ require (
 	github.com/mattn/go-runewidth v0.0.16 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
 	github.com/stretchr/testify v1.9.0 // indirect
-	golang.org/x/sys v0.27.0 // indirect
+	golang.org/x/sys v0.28.0 // indirect
 	golang.org/x/text v0.18.0 // indirect
 )
diff --git a/go.sum b/go.sum
index aa5c98b..1cd6350 100644
--- a/go.sum
+++ b/go.sum
@@ -44,12 +44,12 @@ golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
 golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
-golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
 golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
-golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
+golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
+golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/rule/argument_limit.go b/rule/argument_limit.go
index ce831e5..b44e1fd 100644
--- a/rule/argument_limit.go
+++ b/rule/argument_limit.go
@@ -35,50 +35,33 @@ func (r *ArgumentsLimitRule) Apply(file *lint.File, arguments lint.Arguments) []
 	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
-	onFailure := func(failure lint.Failure) {
-		failures = append(failures, failure)
-	}
-
-	walker := lintArgsNum{
-		max:       r.max,
-		onFailure: onFailure,
-	}
-
-	ast.Walk(walker, file.AST)
-
-	return failures
-}
-
-// Name returns the rule name.
-func (*ArgumentsLimitRule) Name() string {
-	return "argument-limit"
-}
 
-type lintArgsNum struct {
-	max       int
-	onFailure func(lint.Failure)
-}
+	for _, decl := range file.AST.Decls {
+		funcDecl, ok := decl.(*ast.FuncDecl)
+		if !ok {
+			continue
+		}
 
-func (w lintArgsNum) Visit(n ast.Node) ast.Visitor {
-	node, ok := n.(*ast.FuncDecl)
-	if !ok {
-		return w
-	}
+		numParams := 0
+		for _, l := range funcDecl.Type.Params.List {
+			numParams += len(l.Names)
+		}
 
-	num := 0
-	for _, l := range node.Type.Params.List {
-		for range l.Names {
-			num++
+		if numParams <= r.max {
+			continue
 		}
-	}
 
-	if num > w.max {
-		w.onFailure(lint.Failure{
+		failures = append(failures, lint.Failure{
 			Confidence: 1,
-			Failure:    fmt.Sprintf("maximum number of arguments per function exceeded; max %d but got %d", w.max, num),
-			Node:       node.Type,
+			Failure:    fmt.Sprintf("maximum number of arguments per function exceeded; max %d but got %d", r.max, numParams),
+			Node:       funcDecl.Type,
 		})
 	}
 
-	return nil // skip visiting the body of the function
+	return failures
+}
+
+// Name returns the rule name.
+func (*ArgumentsLimitRule) Name() string {
+	return "argument-limit"
 }
diff --git a/rule/confusing_results.go b/rule/confusing_results.go
index 1b79ada..c1804d1 100644
--- a/rule/confusing_results.go
+++ b/rule/confusing_results.go
@@ -13,54 +13,43 @@ type ConfusingResultsRule struct{}
 func (*ConfusingResultsRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
 	var failures []lint.Failure
 
-	fileAst := file.AST
-	walker := lintConfusingResults{
-		onFailure: func(failure lint.Failure) {
-			failures = append(failures, failure)
-		},
-	}
+	for _, decl := range file.AST.Decls {
+		funcDecl, ok := decl.(*ast.FuncDecl)
 
-	ast.Walk(walker, fileAst)
+		isFunctionWithMoreThanOneResult := ok && funcDecl.Type.Results != nil && len(funcDecl.Type.Results.List) > 1
+		if !isFunctionWithMoreThanOneResult {
+			continue
+		}
 
-	return failures
-}
+		resultsAreNamed := len(funcDecl.Type.Results.List[0].Names) > 0
+		if resultsAreNamed {
+			continue
+		}
 
-// Name returns the rule name.
-func (*ConfusingResultsRule) Name() string {
-	return "confusing-results"
-}
+		lastType := ""
+		for _, result := range funcDecl.Type.Results.List {
 
-type lintConfusingResults struct {
-	onFailure func(lint.Failure)
-}
+			resultTypeName := gofmt(result.Type)
 
-func (w lintConfusingResults) Visit(n ast.Node) ast.Visitor {
-	fn, ok := n.(*ast.FuncDecl)
-	if !ok || fn.Type.Results == nil || len(fn.Type.Results.List) < 2 {
-		return w
-	}
-	lastType := ""
-	for _, result := range fn.Type.Results.List {
-		if len(result.Names) > 0 {
-			return w
-		}
+			if resultTypeName == lastType {
+				failures = append(failures, lint.Failure{
+					Node:       result,
+					Confidence: 1,
+					Category:   "naming",
+					Failure:    "unnamed results of the same type may be confusing, consider using named results",
+				})
 
-		t, ok := result.Type.(*ast.Ident)
-		if !ok {
-			return w
-		}
+				break
+			}
 
-		if t.Name == lastType {
-			w.onFailure(lint.Failure{
-				Node:       n,
-				Confidence: 1,
-				Category:   "naming",
-				Failure:    "unnamed results of the same type may be confusing, consider using named results",
-			})
-			break
+			lastType = resultTypeName
 		}
-		lastType = t.Name
 	}
 
-	return w
+	return failures
+}
+
+// Name returns the rule name.
+func (*ConfusingResultsRule) Name() string {
+	return "confusing-results"
 }
diff --git a/rule/context_as_argument.go b/rule/context_as_argument.go
index 06dc0f2..f2f9aee 100644
--- a/rule/context_as_argument.go
+++ b/rule/context_as_argument.go
@@ -11,7 +11,7 @@ import (
 
 // ContextAsArgumentRule suggests that `context.Context` should be the first argument of a function.
 type ContextAsArgumentRule struct {
-	allowTypesLUT map[string]struct{}
+	allowTypes map[string]struct{}
 
 	configureOnce sync.Once
 }
@@ -21,20 +21,37 @@ func (r *ContextAsArgumentRule) Apply(file *lint.File, args lint.Arguments) []li
 	r.configureOnce.Do(func() { r.configure(args) })
 
 	var failures []lint.Failure
-	walker := lintContextArguments{
-		allowTypesLUT: r.allowTypesLUT,
-		onFailure: func(failure lint.Failure) {
-			failures = append(failures, failure)
-		},
-	}
+	for _, decl := range file.AST.Decls {
+		fn, ok := decl.(*ast.FuncDecl)
+		if !ok || len(fn.Type.Params.List) <= 1 {
+			continue // not a function or a function with less than 2 parameters
+		}
 
-	ast.Walk(walker, file.AST)
+		fnArgs := fn.Type.Params.List
+
+		// A context.Context should be the first parameter of a function.
+		// Flag any that show up after the first.
+		isCtxStillAllowed := true
+		for _, arg := range fnArgs {
+			argIsCtx := isPkgDot(arg.Type, "context", "Context")
+			if argIsCtx && !isCtxStillAllowed {
+				failures = append(failures, lint.Failure{
+					Node:       arg,
+					Category:   "arg-order",
+					Failure:    "context.Context should be the first parameter of a function",
+					Confidence: 0.9,
+				})
+
+				break // only flag one
+			}
 
-	return failures
-}
+			typeName := gofmt(arg.Type)
+			// a parameter of type context.Context is still allowed if the current arg type is in the allow types LookUpTable
+			_, isCtxStillAllowed = r.allowTypes[typeName]
+		}
+	}
 
-func (r *ContextAsArgumentRule) configure(arguments lint.Arguments) {
-	r.allowTypesLUT = getAllowTypesFromArguments(arguments)
+	return failures
 }
 
 // Name returns the rule name.
@@ -42,43 +59,11 @@ func (*ContextAsArgumentRule) Name() string {
 	return "context-as-argument"
 }
 
-type lintContextArguments struct {
-	allowTypesLUT map[string]struct{}
-	onFailure     func(lint.Failure)
-}
-
-func (w lintContextArguments) Visit(n ast.Node) ast.Visitor {
-	fn, ok := n.(*ast.FuncDecl)
-	if !ok || len(fn.Type.Params.List) <= 1 {
-		return w
-	}
-
-	fnArgs := fn.Type.Params.List
-
-	// A context.Context should be the first parameter of a function.
-	// Flag any that show up after the first.
-	isCtxStillAllowed := true
-	for _, arg := range fnArgs {
-		argIsCtx := isPkgDot(arg.Type, "context", "Context")
-		if argIsCtx && !isCtxStillAllowed {
-			w.onFailure(lint.Failure{
-				Node:       arg,
-				Category:   "arg-order",
-				Failure:    "context.Context should be the first parameter of a function",
-				Confidence: 0.9,
-			})
-			break // only flag one
-		}
-
-		typeName := gofmt(arg.Type)
-		// a parameter of type context.Context is still allowed if the current arg type is in the LUT
-		_, isCtxStillAllowed = w.allowTypesLUT[typeName]
-	}
-
-	return nil // avoid visiting the function body
+func (r *ContextAsArgumentRule) configure(arguments lint.Arguments) {
+	r.allowTypes = r.getAllowTypesFromArguments(arguments)
 }
 
-func getAllowTypesFromArguments(args lint.Arguments) map[string]struct{} {
+func (r *ContextAsArgumentRule) getAllowTypesFromArguments(args lint.Arguments) map[string]struct{} {
 	allowTypesBefore := []string{}
 	if len(args) >= 1 {
 		argKV, ok := args[0].(map[string]any)
diff --git a/rule/cyclomatic.go b/rule/cyclomatic.go
index 60f8441..7f42a1c 100644
--- a/rule/cyclomatic.go
+++ b/rule/cyclomatic.go
@@ -38,53 +38,30 @@ func (r *CyclomaticRule) Apply(file *lint.File, arguments lint.Arguments) []lint
 	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
-	fileAst := file.AST
-
-	walker := lintCyclomatic{
-		file:       file,
-		complexity: r.maxComplexity,
-		onFailure: func(failure lint.Failure) {
-			failures = append(failures, failure)
-		},
-	}
-
-	ast.Walk(walker, fileAst)
-
-	return failures
-}
-
-// Name returns the rule name.
-func (*CyclomaticRule) Name() string {
-	return "cyclomatic"
-}
-
-type lintCyclomatic struct {
-	file       *lint.File
-	complexity int
-	onFailure  func(lint.Failure)
-}
-
-func (w lintCyclomatic) Visit(_ ast.Node) ast.Visitor {
-	f := w.file
-	for _, decl := range f.AST.Decls {
+	for _, decl := range file.AST.Decls {
 		fn, ok := decl.(*ast.FuncDecl)
 		if !ok {
 			continue
 		}
 
 		c := complexity(fn)
-		if c > w.complexity {
-			w.onFailure(lint.Failure{
+		if c > r.maxComplexity {
+			failures = append(failures, lint.Failure{
 				Confidence: 1,
 				Category:   "maintenance",
 				Failure: fmt.Sprintf("function %s has cyclomatic complexity %d (> max enabled %d)",
-					funcName(fn), c, w.complexity),
+					funcName(fn), c, r.maxComplexity),
 				Node: fn,
 			})
 		}
 	}
 
-	return nil
+	return failures
+}
+
+// Name returns the rule name.
+func (*CyclomaticRule) Name() string {
+	return "cyclomatic"
 }
 
 // funcName returns the name representation of a function or method:
diff --git a/rule/datarace.go b/rule/datarace.go
index 21a7a70..d95775d 100644
--- a/rule/datarace.go
+++ b/rule/datarace.go
@@ -11,50 +11,45 @@ import (
 type DataRaceRule struct{}
 
 // Apply applies the rule to given file.
-func (*DataRaceRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
+func (r *DataRaceRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
+	isGo122 := file.Pkg.IsAtLeastGo122()
 	var failures []lint.Failure
-	onFailure := func(failure lint.Failure) {
-		failures = append(failures, failure)
-	}
-	w := lintDataRaces{onFailure: onFailure, go122for: file.Pkg.IsAtLeastGo122()}
+	for _, decl := range file.AST.Decls {
+		funcDecl, ok := decl.(*ast.FuncDecl)
+		if !ok || funcDecl.Body == nil {
+			continue // not function declaration or empty function
+		}
 
-	ast.Walk(w, file.AST)
+		funcResults := funcDecl.Type.Results
 
-	return failures
-}
+		returnIDs := map[*ast.Object]struct{}{}
+		if funcResults != nil {
+			returnIDs = r.ExtractReturnIDs(funcResults.List)
+		}
 
-// Name returns the rule name.
-func (*DataRaceRule) Name() string {
-	return "datarace"
-}
+		onFailure := func(failure lint.Failure) {
+			failures = append(failures, failure)
+		}
 
-type lintDataRaces struct {
-	onFailure func(failure lint.Failure)
-	go122for  bool
-}
+		fl := &lintFunctionForDataRaces{
+			onFailure: onFailure,
+			returnIDs: returnIDs,
+			rangeIDs:  map[*ast.Object]struct{}{},
+			go122for:  isGo122,
+		}
 
-func (w lintDataRaces) Visit(n ast.Node) ast.Visitor {
-	node, ok := n.(*ast.FuncDecl)
-	if !ok {
-		return w // not function declaration
+		ast.Walk(fl, funcDecl.Body)
 	}
-	if node.Body == nil {
-		return nil // empty body
-	}
-
-	results := node.Type.Results
 
-	returnIDs := map[*ast.Object]struct{}{}
-	if results != nil {
-		returnIDs = w.ExtractReturnIDs(results.List)
-	}
-	fl := &lintFunctionForDataRaces{onFailure: w.onFailure, returnIDs: returnIDs, rangeIDs: map[*ast.Object]struct{}{}, go122for: w.go122for}
-	ast.Walk(fl, node.Body)
+	return failures
+}
 
-	return nil
+// Name returns the rule name.
+func (*DataRaceRule) Name() string {
+	return "datarace"
 }
 
-func (lintDataRaces) ExtractReturnIDs(fields []*ast.Field) map[*ast.Object]struct{} {
+func (*DataRaceRule) ExtractReturnIDs(fields []*ast.Field) map[*ast.Object]struct{} {
 	r := map[*ast.Object]struct{}{}
 	for _, f := range fields {
 		for _, id := range f.Names {
diff --git a/rule/error_return.go b/rule/error_return.go
index f26d807..c5d3f46 100644
--- a/rule/error_return.go
+++ b/rule/error_return.go
@@ -13,16 +13,34 @@ type ErrorReturnRule struct{}
 func (*ErrorReturnRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
 	var failures []lint.Failure
 
-	fileAst := file.AST
-	walker := lintErrorReturn{
-		file:    file,
-		fileAst: fileAst,
-		onFailure: func(failure lint.Failure) {
-			failures = append(failures, failure)
-		},
-	}
+	for _, decl := range file.AST.Decls {
+		funcDecl, ok := decl.(*ast.FuncDecl)
+		isFunctionWithMoreThanOneResult := ok && funcDecl.Type.Results != nil && len(funcDecl.Type.Results.List) > 1
+		if !isFunctionWithMoreThanOneResult {
+			continue
+		}
+
+		funcResults := funcDecl.Type.Results.List
+		isLastResultError := isIdent(funcResults[len(funcResults)-1].Type, "error")
+		if isLastResultError {
+			continue
+		}
 
-	ast.Walk(walker, fileAst)
+		// An error return parameter should be the last parameter.
+		// Flag any error parameters found before the last.
+		for _, r := range funcResults[:len(funcResults)-1] {
+			if isIdent(r.Type, "error") {
+				failures = append(failures, lint.Failure{
+					Category:   "style",
+					Confidence: 0.9,
+					Node:       funcDecl,
+					Failure:    "error should be the last type when returning multiple items",
+				})
+
+				break // only flag one
+			}
+		}
+	}
 
 	return failures
 }
@@ -31,37 +49,3 @@ func (*ErrorReturnRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure
 func (*ErrorReturnRule) Name() string {
 	return "error-return"
 }
-
-type lintErrorReturn struct {
-	file      *lint.File
-	fileAst   *ast.File
-	onFailure func(lint.Failure)
-}
-
-func (w lintErrorReturn) Visit(n ast.Node) ast.Visitor {
-	fn, ok := n.(*ast.FuncDecl)
-	if !ok || fn.Type.Results == nil {
-		return w
-	}
-	ret := fn.Type.Results.List
-	if len(ret) <= 1 {
-		return w
-	}
-	if isIdent(ret[len(ret)-1].Type, "error") {
-		return nil
-	}
-	// An error return parameter should be the last parameter.
-	// Flag any error parameters found before the last.
-	for _, r := range ret[:len(ret)-1] {
-		if isIdent(r.Type, "error") {
-			w.onFailure(lint.Failure{
-				Category:   "arg-order",
-				Confidence: 0.9,
-				Node:       fn,
-				Failure:    "error should be the last type when returning multiple items",
-			})
-			break // only flag one
-		}
-	}
-	return w
-}
diff --git a/rule/flag_param.go b/rule/flag_param.go
index 8156121..0a67f33 100644
--- a/rule/flag_param.go
+++ b/rule/flag_param.go
@@ -13,58 +13,48 @@ type FlagParamRule struct{}
 // Apply applies the rule to given file.
 func (*FlagParamRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
 	var failures []lint.Failure
-
 	onFailure := func(failure lint.Failure) {
 		failures = append(failures, failure)
 	}
 
-	w := lintFlagParamRule{onFailure: onFailure}
-	ast.Walk(w, file.AST)
-	return failures
-}
-
-// Name returns the rule name.
-func (*FlagParamRule) Name() string {
-	return "flag-parameter"
-}
-
-type lintFlagParamRule struct {
-	onFailure func(lint.Failure)
-}
-
-func (w lintFlagParamRule) Visit(node ast.Node) ast.Visitor {
-	fd, ok := node.(*ast.FuncDecl)
-	if !ok {
-		return w
-	}
-
-	if fd.Body == nil {
-		return nil // skip whole function declaration
-	}
+	for _, decl := range file.AST.Decls {
+		fd, ok := decl.(*ast.FuncDecl)
+		isFuncWithNonEmptyBody := ok && fd.Body != nil
+		if !isFuncWithNonEmptyBody {
+			continue
+		}
 
-	for _, p := range fd.Type.Params.List {
-		t := p.Type
+		boolParams := map[string]struct{}{}
+		for _, param := range fd.Type.Params.List {
+			if !isIdent(param.Type, "bool") {
+				continue
+			}
 
-		id, ok := t.(*ast.Ident)
-		if !ok {
-			continue
+			for _, paramIdent := range param.Names {
+				boolParams[paramIdent.Name] = struct{}{}
+			}
 		}
 
-		if id.Name != "bool" {
+		if len(boolParams) == 0 {
 			continue
 		}
 
-		cv := conditionVisitor{p.Names, fd, w}
+		cv := conditionVisitor{boolParams, fd, onFailure}
 		ast.Walk(cv, fd.Body)
 	}
 
-	return w
+	return failures
+}
+
+// Name returns the rule name.
+func (*FlagParamRule) Name() string {
+	return "flag-parameter"
 }
 
 type conditionVisitor struct {
-	ids    []*ast.Ident
-	fd     *ast.FuncDecl
-	linter lintFlagParamRule
+	idents    map[string]struct{}
+	fd        *ast.FuncDecl
+	onFailure func(lint.Failure)
 }
 
 func (w conditionVisitor) Visit(node ast.Node) ast.Visitor {
@@ -73,28 +63,22 @@ func (w conditionVisitor) Visit(node ast.Node) ast.Visitor {
 		return w
 	}
 
-	fselect := func(n ast.Node) bool {
+	findUsesOfIdents := func(n ast.Node) bool {
 		ident, ok := n.(*ast.Ident)
 		if !ok {
 			return false
 		}
 
-		for _, id := range w.ids {
-			if ident.Name == id.Name {
-				return true
-			}
-		}
-
-		return false
+		return w.idents[ident.Name] == struct{}{}
 	}
 
-	uses := pick(ifStmt.Cond, fselect)
+	uses := pick(ifStmt.Cond, findUsesOfIdents)
 
 	if len(uses) < 1 {
 		return w
 	}
 
-	w.linter.onFailure(lint.Failure{
+	w.onFailure(lint.Failure{
 		Confidence: 1,
 		Node:       w.fd.Type.Params,
 		Category:   "bad practice",
diff --git a/rule/function_length.go b/rule/function_length.go
index c58cd4c..6f5a59e 100644
--- a/rule/function_length.go
+++ b/rule/function_length.go
@@ -28,17 +28,40 @@ func (r *FunctionLength) Apply(file *lint.File, arguments lint.Arguments) []lint
 	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
+	for _, decl := range file.AST.Decls {
+		funcDecl, ok := decl.(*ast.FuncDecl)
+		if !ok {
+			continue
+		}
 
-	walker := lintFuncLength{
-		file:     file,
-		maxStmt:  r.maxStmt,
-		maxLines: r.maxLines,
-		onFailure: func(failure lint.Failure) {
-			failures = append(failures, failure)
-		},
-	}
+		body := funcDecl.Body
+		emptyBody := body == nil || len(body.List) == 0
+		if emptyBody {
+			return nil
+		}
+
+		if r.maxStmt > 0 {
+			stmtCount := r.countStmts(body.List)
+			if stmtCount > r.maxStmt {
+				failures = append(failures, lint.Failure{
+					Confidence: 1,
+					Failure:    fmt.Sprintf("maximum number of statements per function exceeded; max %d but got %d", r.maxStmt, stmtCount),
+					Node:       funcDecl,
+				})
+			}
+		}
 
-	ast.Walk(walker, file.AST)
+		if r.maxLines > 0 {
+			lineCount := r.countLines(body, file)
+			if lineCount > r.maxLines {
+				failures = append(failures, lint.Failure{
+					Confidence: 1,
+					Failure:    fmt.Sprintf("maximum number of lines per function exceeded; max %d but got %d", r.maxLines, lineCount),
+					Node:       funcDecl,
+				})
+			}
+		}
+	}
 
 	return failures
 }
@@ -80,79 +103,35 @@ func (*FunctionLength) parseArguments(arguments lint.Arguments) (maxStmt, maxLin
 	return maxStmt, maxLines
 }
 
-type lintFuncLength struct {
-	file      *lint.File
-	maxStmt   int
-	maxLines  int
-	onFailure func(lint.Failure)
-}
-
-func (w lintFuncLength) Visit(n ast.Node) ast.Visitor {
-	node, ok := n.(*ast.FuncDecl)
-	if !ok {
-		return w
-	}
-
-	body := node.Body
-	emptyBody := body == nil || len(node.Body.List) == 0
-	if emptyBody {
-		return nil
-	}
-
-	if w.maxStmt > 0 {
-		stmtCount := w.countStmts(node.Body.List)
-		if stmtCount > w.maxStmt {
-			w.onFailure(lint.Failure{
-				Confidence: 1,
-				Failure:    fmt.Sprintf("maximum number of statements per function exceeded; max %d but got %d", w.maxStmt, stmtCount),
-				Node:       node,
-			})
-		}
-	}
-
-	if w.maxLines > 0 {
-		lineCount := w.countLines(node.Body)
-		if lineCount > w.maxLines {
-			w.onFailure(lint.Failure{
-				Confidence: 1,
-				Failure:    fmt.Sprintf("maximum number of lines per function exceeded; max %d but got %d", w.maxLines, lineCount),
-				Node:       node,
-			})
-		}
-	}
-
-	return nil
-}
-
-func (w lintFuncLength) countLines(b *ast.BlockStmt) int {
-	return w.file.ToPosition(b.End()).Line - w.file.ToPosition(b.Pos()).Line - 1
+func (*FunctionLength) countLines(b *ast.BlockStmt, file *lint.File) int {
+	return file.ToPosition(b.End()).Line - file.ToPosition(b.Pos()).Line - 1
 }
 
-func (w lintFuncLength) countStmts(b []ast.Stmt) int {
+func (r *FunctionLength) countStmts(b []ast.Stmt) int {
 	count := 0
 	for _, s := range b {
 		switch stmt := s.(type) {
 		case *ast.BlockStmt:
-			count += w.countStmts(stmt.List)
+			count += r.countStmts(stmt.List)
 		case *ast.IfStmt:
-			count += 1 + w.countBodyListStmts(stmt)
+			count += 1 + r.countBodyListStmts(stmt)
 			if stmt.Else != nil {
 				elseBody, ok := stmt.Else.(*ast.BlockStmt)
 				if ok {
-					count += w.countStmts(elseBody.List)
+					count += r.countStmts(elseBody.List)
 				}
 			}
 		case *ast.ForStmt, *ast.RangeStmt,
 			*ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt:
-			count += 1 + w.countBodyListStmts(stmt)
+			count += 1 + r.countBodyListStmts(stmt)
 		case *ast.CaseClause:
-			count += w.countStmts(stmt.Body)
+			count += r.countStmts(stmt.Body)
 		case *ast.AssignStmt:
-			count += 1 + w.countFuncLitStmts(stmt.Rhs[0])
+			count += 1 + r.countFuncLitStmts(stmt.Rhs[0])
 		case *ast.GoStmt:
-			count += 1 + w.countFuncLitStmts(stmt.Call.Fun)
+			count += 1 + r.countFuncLitStmts(stmt.Call.Fun)
 		case *ast.DeferStmt:
-			count += 1 + w.countFuncLitStmts(stmt.Call.Fun)
+			count += 1 + r.countFuncLitStmts(stmt.Call.Fun)
 		default:
 			count++
 		}
@@ -161,14 +140,15 @@ func (w lintFuncLength) countStmts(b []ast.Stmt) int {
 	return count
 }
 
-func (w lintFuncLength) countFuncLitStmts(stmt ast.Expr) int {
+func (r *FunctionLength) countFuncLitStmts(stmt ast.Expr) int {
 	if block, ok := stmt.(*ast.FuncLit); ok {
-		return w.countStmts(block.Body.List)
+		return r.countStmts(block.Body.List)
 	}
+
 	return 0
 }
 
-func (w lintFuncLength) countBodyListStmts(t any) int {
+func (r *FunctionLength) countBodyListStmts(t any) int {
 	i := reflect.ValueOf(t).Elem().FieldByName(`Body`).Elem().FieldByName(`List`).Interface()
-	return w.countStmts(i.([]ast.Stmt))
+	return r.countStmts(i.([]ast.Stmt))
 }
diff --git a/rule/function_result_limit.go b/rule/function_result_limit.go
index bd4fe37..0686545 100644
--- a/rule/function_result_limit.go
+++ b/rule/function_result_limit.go
@@ -15,39 +15,33 @@ type FunctionResultsLimitRule struct {
 	configureOnce sync.Once
 }
 
-const defaultResultsLimit = 3
-
-func (r *FunctionResultsLimitRule) configure(arguments lint.Arguments) {
-	if len(arguments) < 1 {
-		r.max = defaultResultsLimit
-		return
-	}
-
-	maxResults, ok := arguments[0].(int64) // Alt. non panicking version
-	if !ok {
-		panic(fmt.Sprintf(`invalid value passed as return results number to the "function-result-limit" rule; need int64 but got %T`, arguments[0]))
-	}
-	if maxResults < 0 {
-		panic(`the value passed as return results number to the "function-result-limit" rule cannot be negative`)
-	}
-
-	r.max = int(maxResults)
-}
-
 // Apply applies the rule to given file.
 func (r *FunctionResultsLimitRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
 	r.configureOnce.Do(func() { r.configure(arguments) })
 
 	var failures []lint.Failure
+	for _, decl := range file.AST.Decls {
+		funcDecl, ok := decl.(*ast.FuncDecl)
+		if !ok {
+			continue
+		}
 
-	walker := lintFunctionResultsNum{
-		max: r.max,
-		onFailure: func(failure lint.Failure) {
-			failures = append(failures, failure)
-		},
-	}
+		num := 0
+		hasResults := funcDecl.Type.Results != nil
+		if hasResults {
+			num = funcDecl.Type.Results.NumFields()
+		}
+
+		if num <= r.max {
+			continue
+		}
 
-	ast.Walk(walker, file.AST)
+		failures = append(failures, lint.Failure{
+			Confidence: 1,
+			Failure:    fmt.Sprintf("maximum number of return results per function exceeded; max %d but got %d", r.max, num),
+			Node:       funcDecl.Type,
+		})
+	}
 
 	return failures
 }
@@ -57,29 +51,21 @@ func (*FunctionResultsLimitRule) Name() string {
 	return "function-result-limit"
 }
 
-type lintFunctionResultsNum struct {
-	max       int
-	onFailure func(lint.Failure)
-}
+const defaultResultsLimit = 3
 
-func (w lintFunctionResultsNum) Visit(n ast.Node) ast.Visitor {
-	node, ok := n.(*ast.FuncDecl)
-	if ok {
-		num := 0
-		hasResults := node.Type.Results != nil
-		if hasResults {
-			num = node.Type.Results.NumFields()
-		}
-		if num > w.max {
-			w.onFailure(lint.Failure{
-				Confidence: 1,
-				Failure:    fmt.Sprintf("maximum number of return results per function exceeded; max %d but got %d", w.max, num),
-				Node:       node.Type,
-			})
-		}
+func (r *FunctionResultsLimitRule) configure(arguments lint.Arguments) {
+	if len(arguments) < 1 {
+		r.max = defaultResultsLimit
+		return
+	}
 
-		return nil // skip visiting function's body
+	maxResults, ok := arguments[0].(int64) // Alt. non panicking version
+	if !ok {
+		panic(fmt.Sprintf(`invalid value passed as return results number to the "function-result-limit" rule; need int64 but got %T`, arguments[0]))
+	}
+	if maxResults < 0 {
+		panic(`the value passed as return results number to the "function-result-limit" rule cannot be negative`)
 	}
 
-	return w
+	r.max = int(maxResults)
 }
diff --git a/rule/get_return.go b/rule/get_return.go
index 638c202..b16e271 100644
--- a/rule/get_return.go
+++ b/rule/get_return.go
@@ -15,12 +15,28 @@ type GetReturnRule struct{}
 func (*GetReturnRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
 	var failures []lint.Failure
 
-	onFailure := func(failure lint.Failure) {
-		failures = append(failures, failure)
+	for _, decl := range file.AST.Decls {
+		fd, ok := decl.(*ast.FuncDecl)
+		if !ok {
+			continue
+		}
+
+		if !isGetter(fd.Name.Name) {
+			continue
+		}
+
+		if hasResults(fd.Type.Results) {
+			continue
+		}
+
+		failures = append(failures, lint.Failure{
+			Confidence: 0.8,
+			Node:       fd,
+			Category:   "logic",
+			Failure:    fmt.Sprintf("function '%s' seems to be a getter but it does not return any result", fd.Name.Name),
+		})
 	}
 
-	w := lintReturnRule{onFailure}
-	ast.Walk(w, file.AST)
 	return failures
 }
 
@@ -29,10 +45,6 @@ func (*GetReturnRule) Name() string {
 	return "get-return"
 }
 
-type lintReturnRule struct {
-	onFailure func(lint.Failure)
-}
-
 const getterPrefix = "GET"
 
 var lenGetterPrefix = len(getterPrefix)
@@ -57,24 +69,3 @@ func isGetter(name string) bool {
 func hasResults(rs *ast.FieldList) bool {
 	return rs != nil && len(rs.List) > 0
 }
-
-func (w lintReturnRule) Visit(node ast.Node) ast.Visitor {
-	fd, ok := node.(*ast.FuncDecl)
-	if !ok {
-		return w
-	}
-
-	if !isGetter(fd.Name.Name) {
-		return w
-	}
-	if !hasResults(fd.Type.Results) {
-		w.onFailure(lint.Failure{
-			Confidence: 0.8,
-			Node:       fd,
-			Category:   "logic",
-			Failure:    fmt.Sprintf("function '%s' seems to be a getter but it does not return any result", fd.Name.Name),
-		})
-	}
-
-	return w
-}
diff --git a/rule/modifies_value_receiver.go b/rule/modifies_value_receiver.go
index 2f92991..d811c48 100644
--- a/rule/modifies_value_receiver.go
+++ b/rule/modifies_value_receiver.go
@@ -12,99 +12,35 @@ import (
 type ModifiesValRecRule struct{}
 
 // Apply applies the rule to given file.
-func (*ModifiesValRecRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
+func (r *ModifiesValRecRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
 	var failures []lint.Failure
 
-	onFailure := func(failure lint.Failure) {
-		failures = append(failures, failure)
-	}
-
-	w := lintModifiesValRecRule{file: file, onFailure: onFailure}
 	file.Pkg.TypeCheck()
-	ast.Walk(w, file.AST)
-
-	return failures
-}
-
-// Name returns the rule name.
-func (*ModifiesValRecRule) Name() string {
-	return "modifies-value-receiver"
-}
-
-type lintModifiesValRecRule struct {
-	file      *lint.File
-	onFailure func(lint.Failure)
-}
-
-func (w lintModifiesValRecRule) Visit(node ast.Node) ast.Visitor {
-	switch n := node.(type) {
-	case *ast.FuncDecl:
-		if n.Recv == nil {
-			return nil // skip, not a method
-		}
-
-		receiver := n.Recv.List[0]
-		if _, ok := receiver.Type.(*ast.StarExpr); ok {
-			return nil // skip, method with pointer receiver
+	for _, decl := range file.AST.Decls {
+		funcDecl, ok := decl.(*ast.FuncDecl)
+		isAMethod := ok && funcDecl.Recv != nil
+		if !isAMethod {
+			continue // skip, not a method
 		}
 
-		if w.skipType(receiver.Type) {
-			return nil // skip, receiver is a map or array
-		}
-
-		if len(receiver.Names) < 1 {
-			return nil // skip, anonymous receiver
+		receiver := funcDecl.Recv.List[0]
+		if r.mustSkip(receiver, file.Pkg) {
+			continue
 		}
 
 		receiverName := receiver.Names[0].Name
-		if receiverName == "_" {
-			return nil // skip, anonymous receiver
-		}
-
-		receiverAssignmentFinder := func(n ast.Node) bool {
-			// look for assignments with the receiver in the right hand
-			assignment, ok := n.(*ast.AssignStmt)
-			if !ok {
-				return false
-			}
-
-			for _, exp := range assignment.Lhs {
-				switch e := exp.(type) {
-				case *ast.IndexExpr: // receiver...[] = ...
-					continue
-				case *ast.StarExpr: // *receiver = ...
-					continue
-				case *ast.SelectorExpr: // receiver.field = ...
-					name := w.getNameFromExpr(e.X)
-					if name == "" || name != receiverName {
-						continue
-					}
-				case *ast.Ident: // receiver := ...
-					if e.Name != receiverName {
-						continue
-					}
-				default:
-					continue
-				}
-
-				return true
-			}
-
-			return false
-		}
-
-		assignmentsToReceiver := pick(n.Body, receiverAssignmentFinder)
+		assignmentsToReceiver := r.getAssignmentsToReceiver(receiverName, funcDecl.Body)
 		if len(assignmentsToReceiver) == 0 {
-			return nil // receiver is not modified
+			continue // receiver is not modified
 		}
 
-		methodReturnsReceiver := len(w.findReturnReceiverStatements(receiverName, n.Body)) > 0
+		methodReturnsReceiver := len(r.findReturnReceiverStatements(receiverName, funcDecl.Body)) > 0
 		if methodReturnsReceiver {
-			return nil // modification seems legit (see issue #1066)
+			continue // modification seems legit (see issue #1066)
 		}
 
 		for _, assignment := range assignmentsToReceiver {
-			w.onFailure(lint.Failure{
+			failures = append(failures, lint.Failure{
 				Node:       assignment,
 				Confidence: 1,
 				Failure:    "suspicious assignment to a by-value method receiver",
@@ -112,11 +48,16 @@ func (w lintModifiesValRecRule) Visit(node ast.Node) ast.Visitor {
 		}
 	}
 
-	return w
+	return failures
+}
+
+// Name returns the rule name.
+func (*ModifiesValRecRule) Name() string {
+	return "modifies-value-receiver"
 }
 
-func (w lintModifiesValRecRule) skipType(t ast.Expr) bool {
-	rt := w.file.Pkg.TypeOf(t)
+func (r *ModifiesValRecRule) skipType(t ast.Expr, pkg *lint.Package) bool {
+	rt := pkg.TypeOf(t)
 	if rt == nil {
 		return false
 	}
@@ -128,7 +69,7 @@ func (w lintModifiesValRecRule) skipType(t ast.Expr) bool {
 	return strings.HasPrefix(rtName, "[]") || strings.HasPrefix(rtName, "map[")
 }
 
-func (lintModifiesValRecRule) getNameFromExpr(ie ast.Expr) string {
+func (*ModifiesValRecRule) getNameFromExpr(ie ast.Expr) string {
 	ident, ok := ie.(*ast.Ident)
 	if !ok {
 		return ""
@@ -137,7 +78,7 @@ func (lintModifiesValRecRule) getNameFromExpr(ie ast.Expr) string {
 	return ident.Name
 }
 
-func (w lintModifiesValRecRule) findReturnReceiverStatements(receiverName string, target ast.Node) []ast.Node {
+func (r *ModifiesValRecRule) findReturnReceiverStatements(receiverName string, target ast.Node) []ast.Node {
 	finder := func(n ast.Node) bool {
 		// look for returns with the receiver as value
 		returnStatement, ok := n.(*ast.ReturnStmt)
@@ -148,7 +89,7 @@ func (w lintModifiesValRecRule) findReturnReceiverStatements(receiverName string
 		for _, exp := range returnStatement.Results {
 			switch e := exp.(type) {
 			case *ast.SelectorExpr: // receiver.field = ...
-				name := w.getNameFromExpr(e.X)
+				name := r.getNameFromExpr(e.X)
 				if name == "" || name != receiverName {
 					continue
 				}
@@ -160,7 +101,7 @@ func (w lintModifiesValRecRule) findReturnReceiverStatements(receiverName string
 				if e.Op != token.AND {
 					continue
 				}
-				name := w.getNameFromExpr(e.X)
+				name := r.getNameFromExpr(e.X)
 				if name == "" || name != receiverName {
 					continue
 				}
@@ -177,3 +118,60 @@ func (w lintModifiesValRecRule) findReturnReceiverStatements(receiverName string
 
 	return pick(target, finder)
 }
+
+func (r *ModifiesValRecRule) mustSkip(receiver *ast.Field, pkg *lint.Package) bool {
+	if _, ok := receiver.Type.(*ast.StarExpr); ok {
+		return true // skip, method with pointer receiver
+	}
+
+	if len(receiver.Names) < 1 {
+		return true // skip, anonymous receiver
+	}
+
+	receiverName := receiver.Names[0].Name
+	if receiverName == "_" {
+		return true // skip, anonymous receiver
+	}
+
+	if r.skipType(receiver.Type, pkg) {
+		return true // skip, receiver is a map or array
+	}
+
+	return false
+}
+
+func (r *ModifiesValRecRule) getAssignmentsToReceiver(receiverName string, funcBody *ast.BlockStmt) []ast.Node {
+	receiverAssignmentFinder := func(n ast.Node) bool {
+		// look for assignments with the receiver in the right hand
+		assignment, ok := n.(*ast.AssignStmt)
+		if !ok {
+			return false
+		}
+
+		for _, exp := range assignment.Lhs {
+			switch e := exp.(type) {
+			case *ast.IndexExpr: // receiver...[] = ...
+				continue
+			case *ast.StarExpr: // *receiver = ...
+				continue
+			case *ast.SelectorExpr: // receiver.field = ...
+				name := r.getNameFromExpr(e.X)
+				if name == "" || name != receiverName {
+					continue
+				}
+			case *ast.Ident: // receiver := ...
+				if e.Name != receiverName {
+					continue
+				}
+			default:
+				continue
+			}
+
+			return true
+		}
+
+		return false
+	}
+
+	return pick(funcBody, receiverAssignmentFinder)
+}
diff --git a/rule/unexported_return.go b/rule/unexported_return.go
index e2400af..448b4c7 100644
--- a/rule/unexported_return.go
+++ b/rule/unexported_return.go
@@ -16,17 +16,47 @@ type UnexportedReturnRule struct{}
 func (*UnexportedReturnRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
 	var failures []lint.Failure
 
-	fileAst := file.AST
-	walker := lintUnexportedReturn{
-		file:    file,
-		fileAst: fileAst,
-		onFailure: func(failure lint.Failure) {
-			failures = append(failures, failure)
-		},
-	}
+	for _, decl := range file.AST.Decls {
+		fn, ok := decl.(*ast.FuncDecl)
+		if !ok {
+			continue
+		}
+
+		if fn.Type.Results == nil {
+			continue
+		}
+
+		if !fn.Name.IsExported() {
+			continue
+		}
+
+		thing := "func"
+		if fn.Recv != nil && len(fn.Recv.List) > 0 {
+			thing = "method"
+			if !ast.IsExported(typeparams.ReceiverType(fn)) {
+				// Don't report exported methods of unexported types,
+				// such as private implementations of sort.Interface.
+				continue
+			}
+		}
 
-	file.Pkg.TypeCheck()
-	ast.Walk(walker, fileAst)
+		for _, ret := range fn.Type.Results.List {
+			typ := file.Pkg.TypeOf(ret.Type)
+			if exportedType(typ) {
+				continue
+			}
+
+			failures = append(failures, lint.Failure{
+				Category:   "unexported-type-in-api",
+				Node:       ret.Type,
+				Confidence: 0.8,
+				Failure: fmt.Sprintf("exported %s %s returns unexported type %s, which can be annoying to use",
+					thing, fn.Name.Name, typ),
+			})
+
+			break // only flag one
+		}
+	}
 
 	return failures
 }
@@ -36,49 +66,6 @@ func (*UnexportedReturnRule) Name() string {
 	return "unexported-return"
 }
 
-type lintUnexportedReturn struct {
-	file      *lint.File
-	fileAst   *ast.File
-	onFailure func(lint.Failure)
-}
-
-func (w lintUnexportedReturn) Visit(n ast.Node) ast.Visitor {
-	fn, ok := n.(*ast.FuncDecl)
-	if !ok {
-		return w
-	}
-	if fn.Type.Results == nil {
-		return nil
-	}
-	if !fn.Name.IsExported() {
-		return nil
-	}
-	thing := "func"
-	if fn.Recv != nil && len(fn.Recv.List) > 0 {
-		thing = "method"
-		if !ast.IsExported(typeparams.ReceiverType(fn)) {
-			// Don't report exported methods of unexported types,
-			// such as private implementations of sort.Interface.
-			return nil
-		}
-	}
-	for _, ret := range fn.Type.Results.List {
-		typ := w.file.Pkg.TypeOf(ret.Type)
-		if exportedType(typ) {
-			continue
-		}
-		w.onFailure(lint.Failure{
-			Category:   "unexported-type-in-api",
-			Node:       ret.Type,
-			Confidence: 0.8,
-			Failure: fmt.Sprintf("exported %s %s returns unexported type %s, which can be annoying to use",
-				thing, fn.Name.Name, typ),
-		})
-		break // only flag one
-	}
-	return nil
-}
-
 // exportedType reports whether typ is an exported type.
 // It is imprecise, and will err on the side of returning true,
 // such as for composite types.
diff --git a/rule/unused_receiver.go b/rule/unused_receiver.go
index b3be2a7..173306d 100644
--- a/rule/unused_receiver.go
+++ b/rule/unused_receiver.go
@@ -51,74 +51,51 @@ func (r *UnusedReceiverRule) Apply(file *lint.File, args lint.Arguments) []lint.
 	r.configureOnce.Do(func() { r.configure(args) })
 	var failures []lint.Failure
 
-	onFailure := func(failure lint.Failure) {
-		failures = append(failures, failure)
-	}
-
-	w := lintUnusedReceiverRule{
-		onFailure:  onFailure,
-		allowRegex: r.allowRegex,
-		failureMsg: r.failureMsg,
-	}
-
-	ast.Walk(w, file.AST)
-
-	return failures
-}
-
-// Name returns the rule name.
-func (*UnusedReceiverRule) Name() string {
-	return "unused-receiver"
-}
-
-type lintUnusedReceiverRule struct {
-	onFailure  func(lint.Failure)
-	allowRegex *regexp.Regexp
-	failureMsg string
-}
-
-func (w lintUnusedReceiverRule) Visit(node ast.Node) ast.Visitor {
-	switch n := node.(type) {
-	case *ast.FuncDecl:
-		if n.Recv == nil {
-			return nil // skip this func decl, not a method
+	for _, decl := range file.AST.Decls {
+		funcDecl, ok := decl.(*ast.FuncDecl)
+		isMethod := ok && funcDecl.Recv != nil
+		if !isMethod {
+			continue
 		}
 
-		rec := n.Recv.List[0] // safe to access only the first (unique) element of the list
+		rec := funcDecl.Recv.List[0] // safe to access only the first (unique) element of the list
 		if len(rec.Names) < 1 {
-			return nil // the receiver is anonymous: func (aType) Foo(...) ...
+			continue // the receiver is anonymous: func (aType) Foo(...) ...
 		}
 
 		recID := rec.Names[0]
 		if recID.Name == "_" {
-			return nil // the receiver is already named _
+			continue // the receiver is already named _
 		}
 
-		if w.allowRegex != nil && w.allowRegex.FindStringIndex(recID.Name) != nil {
-			return nil
+		if r.allowRegex != nil && r.allowRegex.FindStringIndex(recID.Name) != nil {
+			continue
 		}
 
 		// inspect the func body looking for references to the receiver id
-		fselect := func(n ast.Node) bool {
+		selectReceiverUses := func(n ast.Node) bool {
 			ident, isAnID := n.(*ast.Ident)
 
 			return isAnID && ident.Obj == recID.Obj
 		}
-		refs2recID := pick(n.Body, fselect)
+		receiverUses := pick(funcDecl.Body, selectReceiverUses)
 
-		if len(refs2recID) > 0 {
-			return nil // the receiver is referenced in the func body
+		if len(receiverUses) > 0 {
+			continue // the receiver is referenced in the func body
 		}
 
-		w.onFailure(lint.Failure{
+		failures = append(failures, lint.Failure{
 			Confidence: 1,
 			Node:       recID,
 			Category:   "bad practice",
-			Failure:    fmt.Sprintf(w.failureMsg, recID.Name),
+			Failure:    fmt.Sprintf(r.failureMsg, recID.Name),
 		})
-
-		return nil // full method body already inspected
 	}
 
-	return w
+	return failures
+}
+
+// Name returns the rule name.
+func (*UnusedReceiverRule) Name() string {
+	return "unused-receiver"
 }


Test patch:
diff --git a/test/argument_limit_test.go b/test/argument_limit_test.go
index cbe6be2..b89b0ec 100644
--- a/test/argument_limit_test.go
+++ b/test/argument_limit_test.go
@@ -7,12 +7,19 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-func TestArgumentLimitDefault(t *testing.T) {
+func TestArgumentsLimitDefault(t *testing.T) {
 	testRule(t, "argument_limit_default", &rule.ArgumentsLimitRule{}, &lint.RuleConfig{})
 }
 
-func TestArgumentLimit(t *testing.T) {
+func TestArgumentsLimit(t *testing.T) {
 	testRule(t, "argument_limit", &rule.ArgumentsLimitRule{}, &lint.RuleConfig{
 		Arguments: []any{int64(3)},
 	})
 }
+
+func BenchmarkArgumentsLimit(b *testing.B) {
+	var t *testing.T
+	for i := 0; i <= b.N; i++ {
+		testRule(t, "argument_limit_default", &rule.ArgumentsLimitRule{}, &lint.RuleConfig{})
+	}
+}
diff --git a/test/atomic_test.go b/test/atomic_test.go
index 1bf29bb..2da7bef 100644
--- a/test/atomic_test.go
+++ b/test/atomic_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// Atomic rule.
 func TestAtomic(t *testing.T) {
 	testRule(t, "atomic", &rule.AtomicRule{})
 }
diff --git a/test/bool_literal_in_expr_test.go b/test/bool_literal_in_expr_test.go
index d67c6d2..501db69 100644
--- a/test/bool_literal_in_expr_test.go
+++ b/test/bool_literal_in_expr_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// BoolLiteral rule.
 func TestBoolLiteral(t *testing.T) {
 	testRule(t, "bool_literal_in_expr", &rule.BoolLiteralRule{})
 }
diff --git a/test/call_to_gc_test.go b/test/call_to_gc_test.go
index 9c11ad4..5944a9d 100644
--- a/test/call_to_gc_test.go
+++ b/test/call_to_gc_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestCallToGC test call-to-gc rule
 func TestCallToGC(t *testing.T) {
 	testRule(t, "call_to_gc", &rule.CallToGCRule{})
 }
diff --git a/test/confusing_naming_test.go b/test/confusing_naming_test.go
index 22f44fe..ce60d92 100644
--- a/test/confusing_naming_test.go
+++ b/test/confusing_naming_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestConfusingNaming rule.
 func TestConfusingNaming(t *testing.T) {
 	testRule(t, "confusing_naming1", &rule.ConfusingNamingRule{})
 }
diff --git a/test/constant_logical_expr_test.go b/test/constant_logical_expr_test.go
index d3d6d90..ce53098 100644
--- a/test/constant_logical_expr_test.go
+++ b/test/constant_logical_expr_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// ConstantLogicalExpr rule.
 func TestConstantLogicalExpr(t *testing.T) {
 	testRule(t, "constant_logical_expr", &rule.ConstantLogicalExprRule{})
 }
diff --git a/test/defer_test.go b/test/defer_test.go
index ce2904a..78ce52a 100644
--- a/test/defer_test.go
+++ b/test/defer_test.go
@@ -7,7 +7,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// Defer rule.
 func TestDefer(t *testing.T) {
 	testRule(t, "defer", &rule.DeferRule{})
 }
diff --git a/test/early_return_test.go b/test/early_return_test.go
index 493ee6f..cc4ef66 100644
--- a/test/early_return_test.go
+++ b/test/early_return_test.go
@@ -8,7 +8,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestEarlyReturn tests early-return rule.
 func TestEarlyReturn(t *testing.T) {
 	testRule(t, "early_return", &rule.EarlyReturnRule{})
 	testRule(t, "early_return_scope", &rule.EarlyReturnRule{}, &lint.RuleConfig{Arguments: []any{ifelse.PreserveScope}})
diff --git a/test/empty_block_test.go b/test/empty_block_test.go
index bc496d4..c5b8c5b 100644
--- a/test/empty_block_test.go
+++ b/test/empty_block_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestEmptyBlock rule.
 func TestEmptyBlock(t *testing.T) {
 	testRule(t, "empty_block", &rule.EmptyBlockRule{})
 }
diff --git a/test/empty_lines_test.go b/test/empty_lines_test.go
index e74d9f1..faad9b7 100644
--- a/test/empty_lines_test.go
+++ b/test/empty_lines_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestEmptyLines rule.
 func TestEmptyLines(t *testing.T) {
 	testRule(t, "empty_lines", &rule.EmptyLinesRule{})
 }
diff --git a/test/identical_branches_test.go b/test/identical_branches_test.go
index a0fc161..341f981 100644
--- a/test/identical_branches_test.go
+++ b/test/identical_branches_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// IdenticalBranches rule.
 func TestIdenticalBranches(t *testing.T) {
 	testRule(t, "identical_branches", &rule.IdenticalBranchesRule{})
 }
diff --git a/test/if_return_test.go b/test/if_return_test.go
index 2e7348b..ce23fa0 100644
--- a/test/if_return_test.go
+++ b/test/if_return_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestIfReturn rule.
 func TestIfReturn(t *testing.T) {
 	testRule(t, "if_return", &rule.IfReturnRule{})
 }
diff --git a/test/modifies_param_test.go b/test/modifies_param_test.go
index d6ab7c9..2c1d99f 100644
--- a/test/modifies_param_test.go
+++ b/test/modifies_param_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestModifiesParam rule.
 func TestModifiesParam(t *testing.T) {
 	testRule(t, "modifies_param", &rule.ModifiesParamRule{})
 }
diff --git a/test/redefines_builtin_id_test.go b/test/redefines_builtin_id_test.go
index 68bb94b..f8cdf69 100644
--- a/test/redefines_builtin_id_test.go
+++ b/test/redefines_builtin_id_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// Tests RedefinesBuiltinID rule.
 func TestRedefinesBuiltinID(t *testing.T) {
 	testRule(t, "redefines_builtin_id", &rule.RedefinesBuiltinIDRule{})
 }
diff --git a/test/redundant_import_alias_test.go b/test/redundant_import_alias_test.go
index eeb3e2e..a91e5ca 100644
--- a/test/redundant_import_alias_test.go
+++ b/test/redundant_import_alias_test.go
@@ -5,7 +5,6 @@ import (
 	"testing"
 )
 
-// TestRedundantImportAlias rule.
 func TestRedundantImportAlias(t *testing.T) {
 	testRule(t, "redundant_import_alias", &rule.RedundantImportAlias{})
 }
diff --git a/test/string_of_int_test.go b/test/string_of_int_test.go
index c6e2f0c..f01148c 100644
--- a/test/string_of_int_test.go
+++ b/test/string_of_int_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// String-of-int rule.
 func TestStringOfInt(t *testing.T) {
 	testRule(t, "string_of_int", &rule.StringOfIntRule{})
 }
diff --git a/test/struct_tag_test.go b/test/struct_tag_test.go
index 44381e3..c930b10 100644
--- a/test/struct_tag_test.go
+++ b/test/struct_tag_test.go
@@ -7,7 +7,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestStructTag tests struct-tag rule
 func TestStructTag(t *testing.T) {
 	testRule(t, "struct_tag", &rule.StructTagRule{})
 }
diff --git a/test/superfluous_else_test.go b/test/superfluous_else_test.go
index 88815c8..0ec40ac 100644
--- a/test/superfluous_else_test.go
+++ b/test/superfluous_else_test.go
@@ -8,7 +8,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestSuperfluousElse rule.
 func TestSuperfluousElse(t *testing.T) {
 	testRule(t, "superfluous_else", &rule.SuperfluousElseRule{})
 	testRule(t, "superfluous_else_scope", &rule.SuperfluousElseRule{}, &lint.RuleConfig{Arguments: []any{ifelse.PreserveScope}})
diff --git a/test/time_equal_test.go b/test/time_equal_test.go
index 8b2a98a..d574991 100644
--- a/test/time_equal_test.go
+++ b/test/time_equal_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestTimeEqual rule.
 func TestTimeEqual(t *testing.T) {
 	testRule(t, "time_equal", &rule.TimeEqualRule{})
 }
diff --git a/test/time_naming_test.go b/test/time_naming_test.go
index be5fbf3..d2ea0d8 100644
--- a/test/time_naming_test.go
+++ b/test/time_naming_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestTimeNamingRule rule.
 func TestTimeNaming(t *testing.T) {
 	testRule(t, "time_naming", &rule.TimeNamingRule{})
 }
diff --git a/test/unnecessary_stmt_test.go b/test/unnecessary_stmt_test.go
index 79c27ba..a77b5e8 100644
--- a/test/unnecessary_stmt_test.go
+++ b/test/unnecessary_stmt_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// TestUnnecessaryStmt rule.
 func TestUnnecessaryStmt(t *testing.T) {
 	testRule(t, "unnecessary_stmt", &rule.UnnecessaryStmtRule{})
 }
diff --git a/test/useless_break_test.go b/test/useless_break_test.go
index 439bca5..4757dcc 100644
--- a/test/useless_break_test.go
+++ b/test/useless_break_test.go
@@ -6,7 +6,6 @@ import (
 	"github.com/mgechev/revive/rule"
 )
 
-// UselessBreak rule.
 func TestUselessBreak(t *testing.T) {
 	testRule(t, "useless_break", &rule.UselessBreak{})
 }
diff --git a/testdata/confusing_results.go b/testdata/confusing_results.go
index fc47abe..6f45414 100644
--- a/testdata/confusing_results.go
+++ b/testdata/confusing_results.go
@@ -15,6 +15,14 @@ func GetTaz(a string, b int) string {
 
 }
 
-func (t *t) GetTaz(a int, b int)  {
+func (t *t) GetTaz(a int, b int) {
 
 }
+
+func namedResults() (a string, b string) {
+	return "nil", "nil"
+}
+
+func pointerResults() (*string, *string) { // MATCH /unnamed results of the same type may be confusing, consider using named results/
+	return nil, nil
+}