Repository: mgechev/revive (go)

Issue:
Distinguish generic types
**Describe the bug**
Revive reports receiver name error at first and only declaration.

**To Reproduce**
Run `revive` on this code:
```go
package example

type Foo[T any] struct{}

func (f *Foo[T]) Foo() {}

type Bar[T comparable] struct{}

func (b *Bar[T]) Bar() {}
```

```shell
 $ revive --version && revive
version 1.1.4
example.go:9:1: receiver name b should be consistent with previous receiver name f for invalid-type
example.go:3:6: exported type Foo should have comment or be unexported
example.go:7:6: exported type Bar should have comment or be unexported
```

**Expected behavior**
1. I expected to *not* see any errors regarding receiver types.
2. The receiver error should not report "invalid-type".

**Desktop (please complete the following information):**
 - OS: Arch Linux
 - go version go1.18 linux/amd64


Pull request:
fix(receiver-naming): distinguish types with parameters
This PR adds a new `internal/typeparams` package that provides support for type parameters when revive is built with Go 1.18 and higher.

It fixes an issue with `receiver-naming` rule being unable to parse receiver type from AST method declarations the receiver has type parameters.

Closes #669


Patch:
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 7dc3def..b6b6d0b 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -1,5 +1,6 @@
 name: Lint
 on:
+  workflow_dispatch:
   pull_request:
     types: [opened, edited, synchronize, reopened]
 
diff --git a/go.mod b/go.mod
index 39e7056..3b25243 100644
--- a/go.mod
+++ b/go.mod
@@ -4,12 +4,12 @@ go 1.12
 
 require (
 	github.com/BurntSushi/toml v1.1.0
-	github.com/chavacava/garif v0.0.0-20220316182200-5cad0b5181d4
+	github.com/chavacava/garif v0.0.0-20220608063557-908ad76c2c48
 	github.com/fatih/color v1.13.0
 	github.com/fatih/structtag v1.2.0
 	github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/pkg/errors v0.9.1
-	golang.org/x/tools v0.1.10
+	golang.org/x/tools v0.1.11
 )
diff --git a/go.sum b/go.sum
index 11eaab0..8764688 100644
--- a/go.sum
+++ b/go.sum
@@ -6,6 +6,8 @@ github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af h1:spmv8nSH9h5oCQf
 github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3//PWVzTeCezG2b9IRn6myJxJSr4TD/xo6ojU=
 github.com/chavacava/garif v0.0.0-20220316182200-5cad0b5181d4 h1:tFXjAxje9thrTF4h57Ckik+scJjTWdwAtZqZPtOT48M=
 github.com/chavacava/garif v0.0.0-20220316182200-5cad0b5181d4/go.mod h1:W8EnPSQ8Nv4fUjc/v1/8tHFqhuOJXnRub0dTfuAQktU=
+github.com/chavacava/garif v0.0.0-20220608063557-908ad76c2c48 h1:fGu6x/t2goOrSv+9j9On3OUTOSKOSbdBWE2KhdeaGkA=
+github.com/chavacava/garif v0.0.0-20220608063557-908ad76c2c48/go.mod h1:4WaA5vrs1EZSrRpOkp6W5szFQp2/FJjNj/TJEIsKmgs=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
@@ -33,6 +35,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -40,6 +43,7 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
 golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -67,6 +71,8 @@ golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
 golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
 golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
+golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
+golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
@@ -75,3 +81,4 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/typeparams/typeparams.go b/internal/typeparams/typeparams.go
new file mode 100644
index 0000000..33c4f20
--- /dev/null
+++ b/internal/typeparams/typeparams.go
@@ -0,0 +1,29 @@
+// Package typeparams provides utilities for working with Go ASTs with support
+// for type parameters when built with Go 1.18 and higher.
+package typeparams
+
+import (
+	"go/ast"
+)
+
+// Enabled reports whether type parameters are enabled in the current build
+// environment.
+func Enabled() bool {
+	return enabled
+}
+
+// ReceiverType returns the named type of the method receiver, sans "*" and type
+// parameters, or "invalid-type" if fn.Recv is ill formed.
+func ReceiverType(fn *ast.FuncDecl) string {
+	e := fn.Recv.List[0].Type
+	if s, ok := e.(*ast.StarExpr); ok {
+		e = s.X
+	}
+	if enabled {
+		e = unpackIndexExpr(e)
+	}
+	if id, ok := e.(*ast.Ident); ok {
+		return id.Name
+	}
+	return "invalid-type"
+}
diff --git a/internal/typeparams/typeparams_go117.go b/internal/typeparams/typeparams_go117.go
new file mode 100644
index 0000000..913a731
--- /dev/null
+++ b/internal/typeparams/typeparams_go117.go
@@ -0,0 +1,12 @@
+//go:build !go1.18
+// +build !go1.18
+
+package typeparams
+
+import (
+	"go/ast"
+)
+
+const enabled = false
+
+func unpackIndexExpr(e ast.Expr) ast.Expr { return e }
diff --git a/internal/typeparams/typeparams_go118.go b/internal/typeparams/typeparams_go118.go
new file mode 100644
index 0000000..0f7fd88
--- /dev/null
+++ b/internal/typeparams/typeparams_go118.go
@@ -0,0 +1,20 @@
+//go:build go1.18
+// +build go1.18
+
+package typeparams
+
+import (
+	"go/ast"
+)
+
+const enabled = true
+
+func unpackIndexExpr(e ast.Expr) ast.Expr {
+	switch e := e.(type) {
+	case *ast.IndexExpr:
+		return e.X
+	case *ast.IndexListExpr:
+		return e.X
+	}
+	return e
+}
diff --git a/lint/package.go b/lint/package.go
index 7cfcab3..52505f3 100644
--- a/lint/package.go
+++ b/lint/package.go
@@ -7,6 +7,8 @@ import (
 	"sync"
 
 	"golang.org/x/tools/go/gcexportdata"
+
+	"github.com/mgechev/revive/internal/typeparams"
 )
 
 // Package represents a package in the project.
@@ -146,7 +148,7 @@ func (w *walker) Visit(n ast.Node) ast.Visitor {
 		return w
 	}
 	// TODO(dsymonds): We could check the signature to be more precise.
-	recv := receiverType(fn)
+	recv := typeparams.ReceiverType(fn)
 	if i, ok := w.nmap[fn.Name.Name]; ok {
 		w.has[recv] |= i
 	}
@@ -174,21 +176,6 @@ func (p *Package) scanSortable() {
 	}
 }
 
-// receiverType returns the named type of the method receiver, sans "*",
-// or "invalid-type" if fn.Recv is ill formed.
-func receiverType(fn *ast.FuncDecl) string {
-	switch e := fn.Recv.List[0].Type.(type) {
-	case *ast.Ident:
-		return e.Name
-	case *ast.StarExpr:
-		if id, ok := e.X.(*ast.Ident); ok {
-			return id.Name
-		}
-	}
-	// The parser accepts much more than just the legal forms.
-	return "invalid-type"
-}
-
 func (p *Package) lint(rules []Rule, config Config, failures chan Failure) {
 	p.scanSortable()
 	var wg sync.WaitGroup
diff --git a/rule/exported.go b/rule/exported.go
index ed24396..cf2915d 100644
--- a/rule/exported.go
+++ b/rule/exported.go
@@ -9,6 +9,7 @@ import (
 	"unicode"
 	"unicode/utf8"
 
+	"github.com/mgechev/revive/internal/typeparams"
 	"github.com/mgechev/revive/lint"
 )
 
@@ -116,7 +117,7 @@ func (w *lintExported) lintFuncDoc(fn *ast.FuncDecl) {
 	if fn.Recv != nil && len(fn.Recv.List) > 0 {
 		// method
 		kind = "method"
-		recv := receiverType(fn)
+		recv := typeparams.ReceiverType(fn)
 		if !w.checkPrivateReceivers && !ast.IsExported(recv) {
 			// receiver is unexported
 			return
diff --git a/rule/receiver-naming.go b/rule/receiver-naming.go
index fee5777..d79bb9f 100644
--- a/rule/receiver-naming.go
+++ b/rule/receiver-naming.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"go/ast"
 
+	"github.com/mgechev/revive/internal/typeparams"
 	"github.com/mgechev/revive/lint"
 )
 
@@ -65,7 +66,7 @@ func (w lintReceiverName) Visit(n ast.Node) ast.Visitor {
 		})
 		return w
 	}
-	recv := receiverType(fn)
+	recv := typeparams.ReceiverType(fn)
 	if prev, ok := w.typeReceiver[recv]; ok && prev != name {
 		w.onFailure(lint.Failure{
 			Node:       n,
diff --git a/rule/unexported-return.go b/rule/unexported-return.go
index a4b9422..10f8e3f 100644
--- a/rule/unexported-return.go
+++ b/rule/unexported-return.go
@@ -5,6 +5,7 @@ import (
 	"go/ast"
 	"go/types"
 
+	"github.com/mgechev/revive/internal/typeparams"
 	"github.com/mgechev/revive/lint"
 )
 
@@ -55,7 +56,7 @@ func (w lintUnexportedReturn) Visit(n ast.Node) ast.Visitor {
 	thing := "func"
 	if fn.Recv != nil && len(fn.Recv.List) > 0 {
 		thing = "method"
-		if !ast.IsExported(receiverType(fn)) {
+		if !ast.IsExported(typeparams.ReceiverType(fn)) {
 			// Don't report exported methods of unexported types,
 			// such as private implementations of sort.Interface.
 			return nil
diff --git a/rule/utils.go b/rule/utils.go
index 689988e..dca1674 100644
--- a/rule/utils.go
+++ b/rule/utils.go
@@ -26,19 +26,6 @@ var commonMethods = map[string]bool{
 	"Unwrap":    true,
 }
 
-func receiverType(fn *ast.FuncDecl) string {
-	switch e := fn.Recv.List[0].Type.(type) {
-	case *ast.Ident:
-		return e.Name
-	case *ast.StarExpr:
-		if id, ok := e.X.(*ast.Ident); ok {
-			return id.Name
-		}
-	}
-	// The parser accepts much more than just the legal forms.
-	return "invalid-type"
-}
-
 var knownNameExceptions = map[string]bool{
 	"LastInsertId": true, // must match database/sql
 	"kWh":          true,


Test patch:
diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build.yaml
similarity index 58%
rename from .github/workflows/build-test.yaml
rename to .github/workflows/build.yaml
index b6ca02a..751d8ee 100644
--- a/.github/workflows/build-test.yaml
+++ b/.github/workflows/build.yaml
@@ -1,5 +1,6 @@
-name: Build and Test
+name: Build
 on:
+  workflow_dispatch:
   pull_request:
     types: [opened, edited, synchronize, reopened]
 
@@ -11,10 +12,3 @@ jobs:
       - uses: actions/checkout@v2
       - name: build
         run: make build
-  test:
-    name: Test
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-      - name: test
-        run: make test
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000..bea1ab1
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,28 @@
+name: Test
+on:
+  workflow_dispatch:
+  pull_request:
+    types: [opened, edited, synchronize, reopened]
+
+jobs:
+  test:
+    name: Test
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        go-version:
+          - 1.16.x
+          - 1.17.x
+          - 1.18.x
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v3.0.2
+      - name: Set up Go
+        uses: actions/setup-go@v3.2.0
+        with:
+          go-version: ${{ matrix.go-version }}
+          cache: true
+          cache-dependency-path: '**/go.sum'
+      - name: Run tests
+        run: go test -race ./...
diff --git a/test/receiver-naming_test.go b/test/receiver-naming_test.go
new file mode 100644
index 0000000..1d530cb
--- /dev/null
+++ b/test/receiver-naming_test.go
@@ -0,0 +1,15 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/mgechev/revive/internal/typeparams"
+	"github.com/mgechev/revive/rule"
+)
+
+func TestReceiverNamingTypeParams(t *testing.T) {
+	if !typeparams.Enabled() {
+		t.Skip("type parameters are not enabled in the current build environment")
+	}
+	testRule(t, "receiver-naming-issue-669", &rule.ReceiverNamingRule{})
+}
diff --git a/testdata/receiver-naming-issue-669.go b/testdata/receiver-naming-issue-669.go
new file mode 100644
index 0000000..58c5301
--- /dev/null
+++ b/testdata/receiver-naming-issue-669.go
@@ -0,0 +1,37 @@
+package fixtures
+
+type gen1[T any] struct{}
+
+func (g gen1[T]) f1() {}
+
+func (g gen1[U]) f2() {}
+
+func (n gen1[T]) f3() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen1/
+
+func (n gen1[U]) f4() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen1/
+
+func (n gen1[V]) f5() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen1/
+
+func (n *gen1[T]) f6() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen1/
+
+func (n *gen1[U]) f7() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen1/
+
+func (n *gen1[V]) f8() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen1/
+
+type gen2[T1, T2 any] struct{}
+
+func (g gen2[T1, T2]) f1() {}
+
+func (g gen2[U1, U2]) f2() {}
+
+func (n gen2[T1, T2]) f3() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen2/
+
+func (n gen2[U1, U2]) f4() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen2/
+
+func (n gen2[V1, V2]) f5() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen2/
+
+func (n *gen2[T1, T2]) f6() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen2/
+
+func (n *gen2[U1, U2]) f7() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen2/
+
+func (n *gen2[V1, V2]) f8() {} // MATCH /receiver name n should be consistent with previous receiver name g for gen2/