go/analysis/passes/composite: update for generics
Warn about unkeyed composite literals via literals of type parameter
type.
Updates golang/go#48704
Change-Id: Ia75139b56cb73288c133bd547d71664464015813
Reviewed-on: https://go-review.googlesource.com/c/tools/+/357756
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Tim King <taking@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
diff --git a/go/analysis/passes/composite/composite.go b/go/analysis/passes/composite/composite.go
index 4c3ac66..025952e 100644
--- a/go/analysis/passes/composite/composite.go
+++ b/go/analysis/passes/composite/composite.go
@@ -14,6 +14,7 @@
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/internal/typeparams"
)
const Doc = `check for unkeyed composite literals
@@ -67,41 +68,52 @@
// skip whitelisted types
return
}
- under := typ.Underlying()
- for {
- ptr, ok := under.(*types.Pointer)
- if !ok {
- break
+ terms, err := typeparams.StructuralTerms(typ)
+ if err != nil {
+ return // invalid type
+ }
+ for _, term := range terms {
+ under := deref(term.Type().Underlying())
+ if _, ok := under.(*types.Struct); !ok {
+ // skip non-struct composite literals
+ continue
}
- under = ptr.Elem().Underlying()
- }
- if _, ok := under.(*types.Struct); !ok {
- // skip non-struct composite literals
- return
- }
- if isLocalType(pass, typ) {
- // allow unkeyed locally defined composite literal
- return
- }
-
- // check if the CompositeLit contains an unkeyed field
- allKeyValue := true
- for _, e := range cl.Elts {
- if _, ok := e.(*ast.KeyValueExpr); !ok {
- allKeyValue = false
- break
+ if isLocalType(pass, term.Type()) {
+ // allow unkeyed locally defined composite literal
+ continue
}
- }
- if allKeyValue {
- // all the composite literal fields are keyed
+
+ // check if the CompositeLit contains an unkeyed field
+ allKeyValue := true
+ for _, e := range cl.Elts {
+ if _, ok := e.(*ast.KeyValueExpr); !ok {
+ allKeyValue = false
+ break
+ }
+ }
+ if allKeyValue {
+ // all the composite literal fields are keyed
+ continue
+ }
+
+ pass.ReportRangef(cl, "%s composite literal uses unkeyed fields", typeName)
return
}
-
- pass.ReportRangef(cl, "%s composite literal uses unkeyed fields", typeName)
})
return nil, nil
}
+func deref(typ types.Type) types.Type {
+ for {
+ ptr, ok := typ.(*types.Pointer)
+ if !ok {
+ break
+ }
+ typ = ptr.Elem().Underlying()
+ }
+ return typ
+}
+
func isLocalType(pass *analysis.Pass, typ types.Type) bool {
switch x := typ.(type) {
case *types.Struct:
@@ -112,6 +124,8 @@
case *types.Named:
// names in package foo are local to foo_test too
return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test")
+ case *typeparams.TypeParam:
+ return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test")
}
return false
}
diff --git a/go/analysis/passes/composite/composite_test.go b/go/analysis/passes/composite/composite_test.go
index c55015c..952de8b 100644
--- a/go/analysis/passes/composite/composite_test.go
+++ b/go/analysis/passes/composite/composite_test.go
@@ -9,9 +9,14 @@
"golang.org/x/tools/go/analysis/analysistest"
"golang.org/x/tools/go/analysis/passes/composite"
+ "golang.org/x/tools/internal/typeparams"
)
func Test(t *testing.T) {
testdata := analysistest.TestData()
- analysistest.Run(t, testdata, composite.Analyzer, "a")
+ pkgs := []string{"a"}
+ if typeparams.Enabled {
+ pkgs = append(pkgs, "typeparams")
+ }
+ analysistest.Run(t, testdata, composite.Analyzer, pkgs...)
}
diff --git a/go/analysis/passes/composite/testdata/src/typeparams/lib/lib.go b/go/analysis/passes/composite/testdata/src/typeparams/lib/lib.go
new file mode 100644
index 0000000..9d7710d
--- /dev/null
+++ b/go/analysis/passes/composite/testdata/src/typeparams/lib/lib.go
@@ -0,0 +1,9 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package lib
+
+type Struct struct{ F int }
+type Slice []int
+type Map map[int]int
diff --git a/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go b/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go
new file mode 100644
index 0000000..dd5d57e
--- /dev/null
+++ b/go/analysis/passes/composite/testdata/src/typeparams/typeparams.go
@@ -0,0 +1,27 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package typeparams
+
+import "typeparams/lib"
+
+type localStruct struct { F int }
+
+func F[
+ T1 ~struct{ f int },
+ T2a localStruct,
+ T2b lib.Struct,
+ T3 ~[]int,
+ T4 lib.Slice,
+ T5 ~map[int]int,
+ T6 lib.Map,
+]() {
+ _ = T1{2}
+ _ = T2a{2}
+ _ = T2b{2} // want "unkeyed fields"
+ _ = T3{1,2}
+ _ = T4{1,2}
+ _ = T5{1:2}
+ _ = T6{1:2}
+}
diff --git a/internal/typeparams/normalize.go b/internal/typeparams/normalize.go
index ef3dbab..2937350 100644
--- a/internal/typeparams/normalize.go
+++ b/internal/typeparams/normalize.go
@@ -5,6 +5,7 @@
package typeparams
import (
+ "errors"
"fmt"
"go/types"
"os"
@@ -65,6 +66,44 @@
return types.NewInterfaceType(methods, embeddeds), nil
}
+var ErrEmptyTypeSet = errors.New("empty type set")
+
+// StructuralTerms returns the normalized structural type restrictions of a
+// type, if any. For types that are not type parameters, it returns term slice
+// containing a single non-tilde term holding the given type. For type
+// parameters, it returns the normalized term list of the type parameter's
+// constraint. See NormalizeInterface for more information on the normal form
+// of a constraint interface.
+//
+// StructuralTerms returns an error if the structural term list cannot be
+// computed. If the type set of typ is empty, it returns ErrEmptyTypeSet.
+func StructuralTerms(typ types.Type) ([]*Term, error) {
+ switch typ := typ.(type) {
+ case *TypeParam:
+ iface, _ := typ.Constraint().(*types.Interface)
+ if iface == nil {
+ return nil, fmt.Errorf("constraint is %T, not *types.Interface", typ)
+ }
+ tset, err := computeTermSet(iface, make(map[types.Type]*termSet), 0)
+ if err != nil {
+ return nil, err
+ }
+ if tset.terms.isEmpty() {
+ return nil, ErrEmptyTypeSet
+ }
+ if tset.terms.isAll() {
+ return nil, nil
+ }
+ var terms []*Term
+ for _, term := range tset.terms {
+ terms = append(terms, NewTerm(term.tilde, term.typ))
+ }
+ return terms, nil
+ default:
+ return []*Term{NewTerm(false, typ)}, nil
+ }
+}
+
// A termSet holds the normalized set of terms for a given type.
//
// The name termSet is intentionally distinct from 'type set': a type set is
diff --git a/internal/typeparams/typeparams_go117.go b/internal/typeparams/typeparams_go117.go
index 61e07b0..6ad3a43 100644
--- a/internal/typeparams/typeparams_go117.go
+++ b/internal/typeparams/typeparams_go117.go
@@ -164,19 +164,25 @@
return named
}
-// Term is a placeholder type, as type parameters are not supported at this Go
-// version. Its methods panic on use.
-type Term struct{}
+// Term holds information about a structural type restriction.
+type Term struct {
+ tilde bool
+ typ types.Type
+}
-func (*Term) Tilde() bool { unsupported(); return false }
-func (*Term) Type() types.Type { unsupported(); return nil }
-func (*Term) String() string { unsupported(); return "" }
-func (*Term) Underlying() types.Type { unsupported(); return nil }
+func (m *Term) Tilde() bool { return m.tilde }
+func (m *Term) Type() types.Type { return m.typ }
+func (m *Term) String() string {
+ pre := ""
+ if m.tilde {
+ pre = "~"
+ }
+ return pre + m.typ.String()
+}
// NewTerm is unsupported at this Go version, and panics.
func NewTerm(tilde bool, typ types.Type) *Term {
- unsupported()
- return nil
+ return &Term{tilde, typ}
}
// Union is a placeholder type, as type parameters are not supported at this Go