Repository: mgechev/revive (go)

Issue:
False positive with var-declaration
**Describe the bug**
The `var-declaration` check incorrectly warns if explicitly assigning a zero value in a `var` declaration, even if the zero value being assigned isn't the default for a variable of that type.

**To Reproduce**
Steps to reproduce the behavior:
1. I updated revive `go install github.com/mgechev/revive@latest`
2. I run it with the following flags:

```shell
revive main.go
```

main.go:

```go
// Package main is a package.
package main

func main() {
	var a any = 0
	_ = a

	var b any = ""
	_ = b
}
```

**Expected behavior**
I would expect no errors for `var-declaration`, because the zero value for a variable of type `any` is `nil`, not `0` or `""`.

**Logs**
```
main.go:5:14: should drop = 0 from declaration of var a; it is the zero value
main.go:8:14: should drop = "" from declaration of var b; it is the zero value
```

**Desktop (please complete the following information):**
 - OS: macOS 15.1.1
 - Version of Go: `go version go1.23.3 darwin/arm64`

**Additional context**
N/A


Pull request description:
chore: add missing tests for var-declarations, fix any/interface{} type issue
Fixes #1152


Patch:
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/var_declarations.go b/rule/var_declarations.go
index 7d60efd..6b30dfa 100644
--- a/rule/var_declarations.go
+++ b/rule/var_declarations.go
@@ -85,7 +85,7 @@ func (w *lintVarDeclarations) Visit(node ast.Node) ast.Visitor {
 		// If the RHS is a isZero value, suggest dropping it.
 		isZero := false
 		if lit, ok := rhs.(*ast.BasicLit); ok {
-			isZero = zeroLiteral[lit.Value]
+			isZero = isZeroValue(lit.Value, v.Type)
 		} else if isIdent(rhs, "nil") {
 			isZero = true
 		}
@@ -142,3 +142,16 @@ func validType(t types.Type) bool {
 		t != types.Typ[types.Invalid] &&
 		!strings.Contains(t.String(), "invalid type") // good but not foolproof
 }
+
+func isZeroValue(litValue string, typ ast.Expr) bool {
+	switch val := typ.(type) {
+	case *ast.Ident:
+		if val.Name == "any" {
+			return litValue == "nil"
+		}
+	case *ast.InterfaceType:
+		return litValue == "nil"
+	}
+
+	return zeroLiteral[litValue]
+}


Test patch:
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/
diff --git a/testdata/golint/var_declaration.go b/testdata/golint/var_declaration.go
index fba01c3..55305ae 100644
--- a/testdata/golint/var_declaration.go
+++ b/testdata/golint/var_declaration.go
@@ -84,3 +84,9 @@ var y string = q(1).String() // MATCH /should omit type string from declaration
 type q int
 
 func (q) String() string { return "I'm a q" }
+
+// The only true zero value for any/interface{} is nil. Others will be considered non-zero.
+var z1 any = 0           // No warning, zero value for any is nil
+var z2 any = nil         // MATCH /should drop = nil from declaration of var z2; it is the zero value/
+var z3 interface{} = 0   // No warning, zero value for any is nil
+var z4 interface{} = nil // MATCH /should drop = nil from declaration of var z4; it is the zero value/