Repository: mgechev/revive
Pull request #1151 (2024-11-30 09:33:38)

chore: Improve sortables detection
This PR resolves TODO comment that proposed to check sortable methods signatures.

* Ensures that only methods with the correct signature are identified as implementing required behavior (`sort.Interface`).
* Prevents accidental false positives from methods that happen to have the same name but differ in their functionality.


Patch:
diff --git a/README.md b/README.md
index 753b27f..823452f 100644
--- a/README.md
+++ b/README.md
@@ -655,7 +655,7 @@ With the snippet above we:
 - Enable the rule with the name `ban-struct-name`. The `Name()` method of our rule should return a string that matches `ban-struct-name`.
 - Configure the rule with the argument `Foo`. The list of arguments will be passed to `Apply(*File, Arguments)` together with the target file we're linting currently.
 
-A sample rule implementation can be found [here](/rule/argument-limit.go).
+A sample rule implementation can be found [here](/rule/argument_limit.go).
 
 #### Using `revive` as a library
 
diff --git a/go.sum b/go.sum
index ef22115..aa5c98b 100644
--- a/go.sum
+++ b/go.sum
@@ -5,8 +5,6 @@ github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+U
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
-github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
 github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
 github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
 github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
@@ -42,24 +40,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
-golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
-golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 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/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
 golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
-golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
-golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
-golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
 golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
 golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/astutils/ast_utils.go b/internal/astutils/ast_utils.go
new file mode 100644
index 0000000..5a66f11
--- /dev/null
+++ b/internal/astutils/ast_utils.go
@@ -0,0 +1,60 @@
+package astutils
+
+import "go/ast"
+
+// FuncSignatureIs returns true if the given func decl satisfies a signature characterized
+// by the given name, parameters types and return types; false otherwise.
+//
+// Example: to check if a function declaration has the signature Foo(int, string) (bool,error)
+// call to FuncSignatureIs(funcDecl,"Foo",[]string{"int","string"},[]string{"bool","error"})
+func FuncSignatureIs(funcDecl *ast.FuncDecl, wantName string, wantParametersTypes, wantResultsTypes []string) bool {
+	if wantName != funcDecl.Name.String() {
+		return false // func name doesn't match expected one
+	}
+
+	funcParametersTypes := getTypeNames(funcDecl.Type.Params)
+	if len(wantParametersTypes) != len(funcParametersTypes) {
+		return false // func has not the expected number of parameters
+	}
+
+	funcResultsTypes := getTypeNames(funcDecl.Type.Results)
+	if len(wantResultsTypes) != len(funcResultsTypes) {
+		return false // func has not the expected number of return values
+	}
+
+	for i, wantType := range wantParametersTypes {
+		if wantType != funcParametersTypes[i] {
+			return false // type of a func's parameter does not match the type of the corresponding expected parameter
+		}
+	}
+
+	for i, wantType := range wantResultsTypes {
+		if wantType != funcResultsTypes[i] {
+			return false // type of a func's return value does not match the type of the corresponding expected return value
+		}
+	}
+
+	return true
+}
+
+func getTypeNames(fields *ast.FieldList) []string {
+	result := []string{}
+
+	if fields == nil {
+		return result
+	}
+
+	for _, field := range fields.List {
+		typeName := field.Type.(*ast.Ident).Name
+		if field.Names == nil { // unnamed field
+			result = append(result, typeName)
+			continue
+		}
+
+		for range field.Names { // add one type name for each field name
+			result = append(result, typeName)
+		}
+	}
+
+	return result
+}
diff --git a/lint/package.go b/lint/package.go
index 873f8a0..41575f4 100644
--- a/lint/package.go
+++ b/lint/package.go
@@ -1,6 +1,7 @@
 package lint
 
 import (
+	"errors"
 	"go/ast"
 	"go/importer"
 	"go/token"
@@ -9,6 +10,7 @@ import (
 
 	goversion "github.com/hashicorp/go-version"
 
+	"github.com/mgechev/revive/internal/astutils"
 	"github.com/mgechev/revive/internal/typeparams"
 )
 
@@ -31,7 +33,6 @@ type Package struct {
 var (
 	trueValue  = 1
 	falseValue = 2
-	notSet     = 3
 
 	go121 = goversion.Must(goversion.NewVersion("1.21"))
 	go122 = goversion.Must(goversion.NewVersion("1.22"))
@@ -111,6 +112,11 @@ func (p *Package) TypeCheck() error {
 		astFiles = append(astFiles, f.AST)
 	}
 
+	if anyFile == nil {
+		// this is unlikely to happen, but technically guarantees anyFile to not be nil
+		return errors.New("no ast.File found")
+	}
+
 	typesPkg, err := check(config, anyFile.AST.Name.Name, p.fset, astFiles, info)
 
 	// Remember the typechecking info, even if config.Check failed,
@@ -135,7 +141,7 @@ func check(config *types.Config, n string, fset *token.FileSet, astFiles []*ast.
 	return config.Check(n, fset, astFiles, info)
 }
 
-// TypeOf returns the type of an expression.
+// TypeOf returns the type of expression.
 func (p *Package) TypeOf(expr ast.Expr) types.Type {
 	if p.typesInfo == nil {
 		return nil
@@ -143,39 +149,32 @@ func (p *Package) TypeOf(expr ast.Expr) types.Type {
 	return p.typesInfo.TypeOf(expr)
 }
 
-type walker struct {
-	nmap map[string]int
-	has  map[string]int
-}
+type sortableMethodsFlags int
 
-func (w *walker) Visit(n ast.Node) ast.Visitor {
-	fn, ok := n.(*ast.FuncDecl)
-	if !ok || fn.Recv == nil || len(fn.Recv.List) == 0 {
-		return w
-	}
-	// TODO(dsymonds): We could check the signature to be more precise.
-	recv := typeparams.ReceiverType(fn)
-	if i, ok := w.nmap[fn.Name.Name]; ok {
-		w.has[recv] |= i
-	}
-	return w
-}
+// flags for sortable interface methods.
+const (
+	bfLen sortableMethodsFlags = 1 << iota
+	bfLess
+	bfSwap
+)
 
 func (p *Package) scanSortable() {
-	p.sortable = map[string]bool{}
-
-	// bitfield for which methods exist on each type.
-	const (
-		bfLen = 1 << iota
-		bfLess
-		bfSwap
-	)
-	nmap := map[string]int{"Len": bfLen, "Less": bfLess, "Swap": bfSwap}
-	has := map[string]int{}
+	sortableFlags := map[string]sortableMethodsFlags{}
 	for _, f := range p.files {
-		ast.Walk(&walker{nmap, has}, f.AST)
+		for _, decl := range f.AST.Decls {
+			fn, ok := decl.(*ast.FuncDecl)
+			isAMethodDeclaration := ok && fn.Recv != nil && len(fn.Recv.List) != 0
+			if !isAMethodDeclaration {
+				continue
+			}
+
+			recvType := typeparams.ReceiverType(fn)
+			sortableFlags[recvType] |= getSortableMethodFlagForFunction(fn)
+		}
 	}
-	for typ, ms := range has {
+
+	p.sortable = make(map[string]bool, len(sortableFlags))
+	for typ, ms := range sortableFlags {
 		if ms == bfLen|bfLess|bfSwap {
 			p.sortable[typ] = true
 		}
@@ -204,3 +203,16 @@ func (p *Package) IsAtLeastGo121() bool {
 func (p *Package) IsAtLeastGo122() bool {
 	return p.goVersion.GreaterThanOrEqual(go122)
 }
+
+func getSortableMethodFlagForFunction(fn *ast.FuncDecl) sortableMethodsFlags {
+	switch {
+	case astutils.FuncSignatureIs(fn, "Len", []string{}, []string{"int"}):
+		return bfLen
+	case astutils.FuncSignatureIs(fn, "Less", []string{"int", "int"}, []string{"bool"}):
+		return bfLess
+	case astutils.FuncSignatureIs(fn, "Swap", []string{"int", "int"}, []string{}):
+		return bfSwap
+	default:
+		return 0
+	}
+}
diff --git a/rule/add_constant.go b/rule/add_constant.go
index bf822e7..5d7cd60 100644
--- a/rule/add_constant.go
+++ b/rule/add_constant.go
@@ -31,7 +31,7 @@ func (wl allowList) add(kind, list string) {
 	}
 }
 
-// AddConstantRule lints unused params in functions.
+// AddConstantRule suggests using constants instead of magic numbers and string literals.
 type AddConstantRule struct {
 	allowList       allowList
 	ignoreFunctions []*regexp.Regexp
diff --git a/rule/argument_limit.go b/rule/argument_limit.go
index b4d56de..ce831e5 100644
--- a/rule/argument_limit.go
+++ b/rule/argument_limit.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ArgumentsLimitRule lints given else constructs.
+// ArgumentsLimitRule lints the number of arguments a function can receive.
 type ArgumentsLimitRule struct {
 	max int
 
diff --git a/rule/atomic.go b/rule/atomic.go
index 287b28c..6121976 100644
--- a/rule/atomic.go
+++ b/rule/atomic.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// AtomicRule lints given else constructs.
+// AtomicRule lints usages of the `sync/atomic` package.
 type AtomicRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/bare_return.go b/rule/bare_return.go
index 147fa84..d81807b 100644
--- a/rule/bare_return.go
+++ b/rule/bare_return.go
@@ -6,7 +6,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// BareReturnRule lints given else constructs.
+// BareReturnRule lints bare returns.
 type BareReturnRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/blank_imports.go b/rule/blank_imports.go
index 10d84a4..f578ad3 100644
--- a/rule/blank_imports.go
+++ b/rule/blank_imports.go
@@ -7,7 +7,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// BlankImportsRule lints given else constructs.
+// BlankImportsRule lints blank imports.
 type BlankImportsRule struct{}
 
 // Name returns the rule name.
diff --git a/rule/cognitive_complexity.go b/rule/cognitive_complexity.go
index ecde388..e943207 100644
--- a/rule/cognitive_complexity.go
+++ b/rule/cognitive_complexity.go
@@ -10,7 +10,7 @@ import (
 	"golang.org/x/tools/go/ast/astutil"
 )
 
-// CognitiveComplexityRule lints given else constructs.
+// CognitiveComplexityRule sets restriction for maximum cognitive complexity.
 type CognitiveComplexityRule struct {
 	maxComplexity int
 
@@ -67,7 +67,9 @@ func (w cognitiveComplexityLinter) lintCognitiveComplexity() {
 	f := w.file
 	for _, decl := range f.AST.Decls {
 		if fn, ok := decl.(*ast.FuncDecl); ok && fn.Body != nil {
-			v := cognitiveComplexityVisitor{}
+			v := cognitiveComplexityVisitor{
+				name: fn.Name,
+			}
 			c := v.subTreeComplexity(fn.Body)
 			if c > w.maxComplexity {
 				w.onFailure(lint.Failure{
@@ -82,6 +84,7 @@ func (w cognitiveComplexityLinter) lintCognitiveComplexity() {
 }
 
 type cognitiveComplexityVisitor struct {
+	name         *ast.Ident
 	complexity   int
 	nestingLevel int
 }
@@ -125,8 +128,15 @@ func (v *cognitiveComplexityVisitor) Visit(n ast.Node) ast.Visitor {
 		if n.Label != nil {
 			v.complexity++
 		}
+	case *ast.CallExpr:
+		if ident, ok := n.Fun.(*ast.Ident); ok {
+			if ident.Obj == v.name.Obj && ident.Name == v.name.Name {
+				// called by same function directly (direct recursion)
+				v.complexity++
+				return nil
+			}
+		}
 	}
-	// TODO handle (at least) direct recursion
 
 	return v
 }
diff --git a/rule/comments_density.go b/rule/comments_density.go
index f2382b1..8cd4928 100644
--- a/rule/comments_density.go
+++ b/rule/comments_density.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// CommentsDensityRule lints given else constructs.
+// CommentsDensityRule enforces a minimum comment / code relation.
 type CommentsDensityRule struct {
 	minimumCommentsDensity int64
 
diff --git a/rule/context_as_argument.go b/rule/context_as_argument.go
index 8bc5f8b..06dc0f2 100644
--- a/rule/context_as_argument.go
+++ b/rule/context_as_argument.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ContextAsArgumentRule lints given else constructs.
+// ContextAsArgumentRule suggests that `context.Context` should be the first argument of a function.
 type ContextAsArgumentRule struct {
 	allowTypesLUT map[string]struct{}
 
diff --git a/rule/context_keys_type.go b/rule/context_keys_type.go
index 60ccec5..43369e7 100644
--- a/rule/context_keys_type.go
+++ b/rule/context_keys_type.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ContextKeysType lints given else constructs.
+// ContextKeysType disallows the usage of basic types in `context.WithValue`.
 type ContextKeysType struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/cyclomatic.go b/rule/cyclomatic.go
index c1a2de9..60f8441 100644
--- a/rule/cyclomatic.go
+++ b/rule/cyclomatic.go
@@ -11,7 +11,7 @@ import (
 
 // Based on https://github.com/fzipp/gocyclo
 
-// CyclomaticRule lints given else constructs.
+// CyclomaticRule sets restriction for maximum cyclomatic complexity.
 type CyclomaticRule struct {
 	maxComplexity int
 
diff --git a/rule/defer.go b/rule/defer.go
index f7c716e..f6aae45 100644
--- a/rule/defer.go
+++ b/rule/defer.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// DeferRule lints unused params in functions.
+// DeferRule lints gotchas in defer statements.
 type DeferRule struct {
 	allow map[string]bool
 
diff --git a/rule/dot_imports.go b/rule/dot_imports.go
index 03deb4a..3bbd37c 100644
--- a/rule/dot_imports.go
+++ b/rule/dot_imports.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// DotImportsRule lints given else constructs.
+// DotImportsRule forbids . imports.
 type DotImportsRule struct {
 	allowedPackages allowPackages
 
diff --git a/rule/duplicated_imports.go b/rule/duplicated_imports.go
index 2b177fa..55ecb55 100644
--- a/rule/duplicated_imports.go
+++ b/rule/duplicated_imports.go
@@ -6,7 +6,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// DuplicatedImportsRule lints given else constructs.
+// DuplicatedImportsRule looks for packages that are imported two or more times.
 type DuplicatedImportsRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/empty_block.go b/rule/empty_block.go
index ba5652a..8d2d912 100644
--- a/rule/empty_block.go
+++ b/rule/empty_block.go
@@ -6,7 +6,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// EmptyBlockRule lints given else constructs.
+// EmptyBlockRule warns on empty code blocks.
 type EmptyBlockRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/error_naming.go b/rule/error_naming.go
index a4f24f3..e64fdf2 100644
--- a/rule/error_naming.go
+++ b/rule/error_naming.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ErrorNamingRule lints given else constructs.
+// ErrorNamingRule lints naming of error variables.
 type ErrorNamingRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/error_return.go b/rule/error_return.go
index a724e00..f26d807 100644
--- a/rule/error_return.go
+++ b/rule/error_return.go
@@ -6,7 +6,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ErrorReturnRule lints given else constructs.
+// ErrorReturnRule ensures that the error return parameter is the last parameter.
 type ErrorReturnRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/error_strings.go b/rule/error_strings.go
index 97a0f4d..a993368 100644
--- a/rule/error_strings.go
+++ b/rule/error_strings.go
@@ -12,7 +12,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ErrorStringsRule lints given else constructs.
+// ErrorStringsRule lints error strings.
 type ErrorStringsRule struct {
 	errorFunctions map[string]map[string]struct{}
 
diff --git a/rule/errorf.go b/rule/errorf.go
index 1588a74..228a3c7 100644
--- a/rule/errorf.go
+++ b/rule/errorf.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ErrorfRule lints given else constructs.
+// ErrorfRule suggests using `fmt.Errorf` instead of `errors.New(fmt.Sprintf())`.
 type ErrorfRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/exported.go b/rule/exported.go
index 1fdd4af..4bb56f4 100644
--- a/rule/exported.go
+++ b/rule/exported.go
@@ -62,7 +62,7 @@ var commonMethods = map[string]bool{
 	"Unwrap":    true,
 }
 
-// ExportedRule lints given else constructs.
+// ExportedRule lints naming and commenting conventions on exported symbols.
 type ExportedRule struct {
 	stuttersMsg    string
 	disabledChecks disabledChecks
diff --git a/rule/file_header.go b/rule/file_header.go
index 52513d8..a8c9ea2 100644
--- a/rule/file_header.go
+++ b/rule/file_header.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// FileHeaderRule lints given else constructs.
+// FileHeaderRule lints the header that each file should have.
 type FileHeaderRule struct {
 	header string
 
diff --git a/rule/flag_param.go b/rule/flag_param.go
index f9bfb71..8156121 100644
--- a/rule/flag_param.go
+++ b/rule/flag_param.go
@@ -7,7 +7,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// FlagParamRule lints given else constructs.
+// FlagParamRule warns on boolean parameters that create a control coupling.
 type FlagParamRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/function_result_limit.go b/rule/function_result_limit.go
index 5b72f01..bd4fe37 100644
--- a/rule/function_result_limit.go
+++ b/rule/function_result_limit.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// FunctionResultsLimitRule lints given else constructs.
+// FunctionResultsLimitRule limits the maximum number of results a function can return.
 type FunctionResultsLimitRule struct {
 	max int
 
diff --git a/rule/get_return.go b/rule/get_return.go
index 06323a0..638c202 100644
--- a/rule/get_return.go
+++ b/rule/get_return.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// GetReturnRule lints given else constructs.
+// GetReturnRule warns on getters that do not yield any result.
 type GetReturnRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/if_return.go b/rule/if_return.go
index a6a3113..f9e5ef2 100644
--- a/rule/if_return.go
+++ b/rule/if_return.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// IfReturnRule lints given else constructs.
+// IfReturnRule searches for redundant `if` when returning an error.
 type IfReturnRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/import_shadowing.go b/rule/import_shadowing.go
index 046aeb6..474d5b8 100644
--- a/rule/import_shadowing.go
+++ b/rule/import_shadowing.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ImportShadowingRule lints given else constructs.
+// ImportShadowingRule spots identifiers that shadow an import.
 type ImportShadowingRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/imports_blocklist.go b/rule/imports_blocklist.go
index 18d77ca..6ae8be1 100644
--- a/rule/imports_blocklist.go
+++ b/rule/imports_blocklist.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ImportsBlocklistRule lints given else constructs.
+// ImportsBlocklistRule disallows importing the specified packages.
 type ImportsBlocklistRule struct {
 	blocklist []*regexp.Regexp
 
diff --git a/rule/increment_decrement.go b/rule/increment_decrement.go
index 34a8e1e..8158cca 100644
--- a/rule/increment_decrement.go
+++ b/rule/increment_decrement.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// IncrementDecrementRule lints given else constructs.
+// IncrementDecrementRule lints `i += 1` and `i -= 1` constructs.
 type IncrementDecrementRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/indent_error_flow.go b/rule/indent_error_flow.go
index 2abbfbf..5e554d5 100644
--- a/rule/indent_error_flow.go
+++ b/rule/indent_error_flow.go
@@ -5,7 +5,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// IndentErrorFlowRule lints given else constructs.
+// IndentErrorFlowRule prevents redundant else statements.
 type IndentErrorFlowRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/line_length_limit.go b/rule/line_length_limit.go
index 415761e..dcac2ad 100644
--- a/rule/line_length_limit.go
+++ b/rule/line_length_limit.go
@@ -12,7 +12,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// LineLengthLimitRule lints given else constructs.
+// LineLengthLimitRule lints number of characters in a line.
 type LineLengthLimitRule struct {
 	max int
 
diff --git a/rule/max_control_nesting.go b/rule/max_control_nesting.go
index b2c5af7..2898af6 100644
--- a/rule/max_control_nesting.go
+++ b/rule/max_control_nesting.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// MaxControlNestingRule lints given else constructs.
+// MaxControlNestingRule sets restriction for maximum nesting of control structures.
 type MaxControlNestingRule struct {
 	max int64
 
diff --git a/rule/max_public_structs.go b/rule/max_public_structs.go
index d6f91e3..bbb2071 100644
--- a/rule/max_public_structs.go
+++ b/rule/max_public_structs.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// MaxPublicStructsRule lints given else constructs.
+// MaxPublicStructsRule lints the number of public structs in a file.
 type MaxPublicStructsRule struct {
 	max int64
 
diff --git a/rule/modifies_param.go b/rule/modifies_param.go
index a68ae25..caaebe6 100644
--- a/rule/modifies_param.go
+++ b/rule/modifies_param.go
@@ -7,7 +7,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ModifiesParamRule lints given else constructs.
+// ModifiesParamRule warns on assignments to function parameters.
 type ModifiesParamRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/optimize_operands_order.go b/rule/optimize_operands_order.go
index 43d982d..3c8f30f 100644
--- a/rule/optimize_operands_order.go
+++ b/rule/optimize_operands_order.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// OptimizeOperandsOrderRule lints given else constructs.
+// OptimizeOperandsOrderRule checks inefficient conditional expressions.
 type OptimizeOperandsOrderRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/range.go b/rule/range.go
index 9d483a6..b54078e 100644
--- a/rule/range.go
+++ b/rule/range.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// RangeRule lints given else constructs.
+// RangeRule prevents redundant variables when iterating over a collection.
 type RangeRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/range_val_in_closure.go b/rule/range_val_in_closure.go
index 6f9255a..9207828 100644
--- a/rule/range_val_in_closure.go
+++ b/rule/range_val_in_closure.go
@@ -7,7 +7,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// RangeValInClosureRule lints given else constructs.
+// RangeValInClosureRule warns if range value is used in a closure dispatched as goroutine.
 type RangeValInClosureRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/receiver_naming.go b/rule/receiver_naming.go
index c83bacc..e1060a5 100644
--- a/rule/receiver_naming.go
+++ b/rule/receiver_naming.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// ReceiverNamingRule lints given else constructs.
+// ReceiverNamingRule lints a receiver name.
 type ReceiverNamingRule struct {
 	receiverNameMaxLength int
 
diff --git a/rule/redundant_import_alias.go b/rule/redundant_import_alias.go
index fa5281f..93f8936 100644
--- a/rule/redundant_import_alias.go
+++ b/rule/redundant_import_alias.go
@@ -8,7 +8,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// RedundantImportAlias lints given else constructs.
+// RedundantImportAlias warns on import aliases matching the imported package name.
 type RedundantImportAlias struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/time_naming.go b/rule/time_naming.go
index 5bccf8a..67e79ea 100644
--- a/rule/time_naming.go
+++ b/rule/time_naming.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// TimeNamingRule lints given else constructs.
+// TimeNamingRule lints the name of a time variable.
 type TimeNamingRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/unconditional_recursion.go b/rule/unconditional_recursion.go
index 271c3fa..9cdee38 100644
--- a/rule/unconditional_recursion.go
+++ b/rule/unconditional_recursion.go
@@ -6,7 +6,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// UnconditionalRecursionRule lints given else constructs.
+// UnconditionalRecursionRule warns on function calls that will lead to infinite recursion.
 type UnconditionalRecursionRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/unexported_return.go b/rule/unexported_return.go
index 10f8e3f..e2400af 100644
--- a/rule/unexported_return.go
+++ b/rule/unexported_return.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// UnexportedReturnRule lints given else constructs.
+// UnexportedReturnRule warns when a public return is from unexported type.
 type UnexportedReturnRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/unhandled_error.go b/rule/unhandled_error.go
index 4fad8cc..fabc4fb 100644
--- a/rule/unhandled_error.go
+++ b/rule/unhandled_error.go
@@ -11,7 +11,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// UnhandledErrorRule lints given else constructs.
+// UnhandledErrorRule warns on unhandled errors returned by function calls.
 type UnhandledErrorRule struct {
 	ignoreList []*regexp.Regexp
 
diff --git a/rule/unused_receiver.go b/rule/unused_receiver.go
index 131aae5..f1866cf 100644
--- a/rule/unused_receiver.go
+++ b/rule/unused_receiver.go
@@ -9,7 +9,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// UnusedReceiverRule lints unused params in functions.
+// UnusedReceiverRule lints unused receivers in functions.
 type UnusedReceiverRule struct {
 	// regex to check if some name is valid for unused parameter, "^_$" by default
 	allowRegex *regexp.Regexp
diff --git a/rule/use_any.go b/rule/use_any.go
index 88160c2..cd01d5b 100644
--- a/rule/use_any.go
+++ b/rule/use_any.go
@@ -6,7 +6,7 @@ import (
 	"github.com/mgechev/revive/lint"
 )
 
-// UseAnyRule lints given else constructs.
+// UseAnyRule proposes to replace `interface{}` with its alias `any`.
 type UseAnyRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/var_declarations.go b/rule/var_declarations.go
index 0186c3d..7d60efd 100644
--- a/rule/var_declarations.go
+++ b/rule/var_declarations.go
@@ -25,7 +25,7 @@ var zeroLiteral = map[string]bool{
 	"0i":  true,
 }
 
-// VarDeclarationsRule lints given else constructs.
+// VarDeclarationsRule reduces redundancies around variable declaration.
 type VarDeclarationsRule struct{}
 
 // Apply applies the rule to given file.
diff --git a/rule/var_naming.go b/rule/var_naming.go
index 61f621a..320eb39 100644
--- a/rule/var_naming.go
+++ b/rule/var_naming.go
@@ -23,7 +23,7 @@ var knownNameExceptions = map[string]bool{
 	"kWh":          true,
 }
 
-// VarNamingRule lints given else constructs.
+// VarNamingRule lints the name of a variable.
 type VarNamingRule struct {
 	allowList             []string
 	blockList             []string


Test patch:
diff --git a/testdata/cognitive_complexity.go b/testdata/cognitive_complexity.go
index c20794b..17fab4e 100644
--- a/testdata/cognitive_complexity.go
+++ b/testdata/cognitive_complexity.go
@@ -281,3 +281,13 @@ func (m *Migrator) MigrateIfNeeded(target *EtcdVersionPair) error { // MATCH /fu
 
 // no regression test for issue #451
 func myFunc()
+
+// Recursive functions
+func Walk(t *Tree, ch chan int) { // MATCH /function Walk has cognitive complexity 3 (> max enabled 0)/
+	if t == nil { // +1
+		return
+	}
+	Walk(t.Left, ch) // +1
+	ch <- t.Value
+	Walk(t.Right, ch) // +1
+}
diff --git a/testdata/golint/sort.go b/testdata/golint/sort.go
index 331ce71..80901f4 100644
--- a/testdata/golint/sort.go
+++ b/testdata/golint/sort.go
@@ -18,3 +18,21 @@ func (u U) Less(i, j int) bool { return u[i] < u[j] }
 func (u U) Swap(i, j int)      { u[i], u[j] = u[j], u[i] }
 
 func (u U) Other() {} // MATCH /exported method U.Other should have comment or be unexported/
+
+// V is ...
+type V []int
+
+func (v V) Len() (result int)               { return len(w) }
+func (v V) Less(i int, j int) (result bool) { return w[i] < w[j] }
+func (v V) Swap(i int, j int)               { v[i], v[j] = v[j], v[i] }
+
+// W is ...
+type W []int
+
+func (w W) Swap(i int, j int) {} // MATCH /exported method W.Swap should have comment or be unexported/
+
+// Vv is ...
+type Vv []int
+
+func (vv Vv) Len() (result int)               { return len(w) }      // MATCH /exported method Vv.Len should have comment or be unexported/
+func (vv Vv) Less(i int, j int) (result bool) { return w[i] < w[j] } // MATCH /exported method Vv.Less should have comment or be unexported/