From 074820e17b39fd2279bcad5e07f9734aac976428 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Sun, 10 Oct 2021 15:12:32 -0400 Subject: [PATCH] go/analysis/passes/usesgenerics: a new analysis to detect generic code Add a new "usesgenerics" analyzer to report uses of features related to generic programming. This analyzer may be used to temporarily guard other analyzers until they may be updated to support generic features. Along the way, update the typeparams API to return the Info.Instances map, rather than provide indirect access, so that it can be iterated. Fixes golang/go#48790 Change-Id: Ia3555524beff6e19f0b9101582205e26e757c8da Reviewed-on: https://go-review.googlesource.com/c/tools/+/355009 Trust: Robert Findley Run-TryBot: Robert Findley gopls-CI: kokoro TryBot-Result: Go Bot Reviewed-by: Tim King --- .../passes/usesgenerics/testdata/src/a/a.go | 9 + .../passes/usesgenerics/testdata/src/b/b.go | 7 + .../passes/usesgenerics/testdata/src/c/c.go | 13 ++ .../passes/usesgenerics/testdata/src/d/d.go | 13 ++ .../passes/usesgenerics/usesgenerics.go | 166 ++++++++++++++++++ .../passes/usesgenerics/usesgenerics_test.go | 21 +++ .../lsp/analysis/infertypeargs/run_go118.go | 8 +- internal/lsp/source/identifier.go | 4 +- internal/typeparams/typeparams_go117.go | 13 +- internal/typeparams/typeparams_go118.go | 15 +- 10 files changed, 252 insertions(+), 17 deletions(-) create mode 100644 go/analysis/passes/usesgenerics/testdata/src/a/a.go create mode 100644 go/analysis/passes/usesgenerics/testdata/src/b/b.go create mode 100644 go/analysis/passes/usesgenerics/testdata/src/c/c.go create mode 100644 go/analysis/passes/usesgenerics/testdata/src/d/d.go create mode 100644 go/analysis/passes/usesgenerics/usesgenerics.go create mode 100644 go/analysis/passes/usesgenerics/usesgenerics_test.go diff --git a/go/analysis/passes/usesgenerics/testdata/src/a/a.go b/go/analysis/passes/usesgenerics/testdata/src/a/a.go new file mode 100644 index 0000000000..a6dd88819c --- /dev/null +++ b/go/analysis/passes/usesgenerics/testdata/src/a/a.go @@ -0,0 +1,9 @@ +// want package:`features{typeDecl,funcDecl,funcInstance}` + +package a + +type T[P any] int + +func F[P any]() {} + +var _ = F[int] diff --git a/go/analysis/passes/usesgenerics/testdata/src/b/b.go b/go/analysis/passes/usesgenerics/testdata/src/b/b.go new file mode 100644 index 0000000000..81c2810f00 --- /dev/null +++ b/go/analysis/passes/usesgenerics/testdata/src/b/b.go @@ -0,0 +1,7 @@ +// want package:`features{typeSet}` + +package b + +type Constraint interface { + ~int | string +} diff --git a/go/analysis/passes/usesgenerics/testdata/src/c/c.go b/go/analysis/passes/usesgenerics/testdata/src/c/c.go new file mode 100644 index 0000000000..f07499e84d --- /dev/null +++ b/go/analysis/passes/usesgenerics/testdata/src/c/c.go @@ -0,0 +1,13 @@ +// want package:`features{typeDecl,funcDecl,typeSet,typeInstance,funcInstance}` + +// Features funcDecl, typeSet, and funcInstance come from imported packages "a" +// and "b". These features are not directly present in "c". + +package c + +import ( + "a" + "b" +) + +type T[P b.Constraint] a.T[P] diff --git a/go/analysis/passes/usesgenerics/testdata/src/d/d.go b/go/analysis/passes/usesgenerics/testdata/src/d/d.go new file mode 100644 index 0000000000..a06c77651c --- /dev/null +++ b/go/analysis/passes/usesgenerics/testdata/src/d/d.go @@ -0,0 +1,13 @@ +// want package:`features{typeSet}` + +package d + +type myInt int + +func _() { + // Sanity check that we can both detect local types and interfaces with + // embedded defined types. + type constraint interface { + myInt + } +} diff --git a/go/analysis/passes/usesgenerics/usesgenerics.go b/go/analysis/passes/usesgenerics/usesgenerics.go new file mode 100644 index 0000000000..73b53fbefa --- /dev/null +++ b/go/analysis/passes/usesgenerics/usesgenerics.go @@ -0,0 +1,166 @@ +// 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 usesgenerics defines an Analyzer that checks for usage of generic +// features added in Go 1.18. +package usesgenerics + +import ( + "go/ast" + "go/types" + "reflect" + "strings" + + "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" +) + +var Analyzer = &analysis.Analyzer{ + Name: "usesgenerics", + Doc: Doc, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, + ResultType: reflect.TypeOf((*Result)(nil)), + FactTypes: []analysis.Fact{new(featuresFact)}, +} + +const Doc = `detect whether a package uses generics features + +The usesgenerics analysis reports whether a package directly or transitively +uses certain features associated with generic programming in Go.` + +// Result is the usesgenerics analyzer result type. The Direct field records +// features used directly by the package being analyzed (i.e. contained in the +// package source code). The Transitive field records any features used by the +// package or any of its transitive imports. +type Result struct { + Direct, Transitive Features +} + +// Features is a set of flags reporting which features of generic Go code a +// package uses, or 0. +type Features int + +const ( + // GenericTypeDecls indicates whether the package declares types with type + // parameters. + GenericTypeDecls Features = 1 << iota + + // GenericFuncDecls indicates whether the package declares functions with + // type parameters. + GenericFuncDecls + + // EmbeddedTypeSets indicates whether the package declares interfaces that + // contain structural type restrictions, i.e. are not fully described by + // their method sets. + EmbeddedTypeSets + + // TypeInstantiation indicates whether the package instantiates any generic + // types. + TypeInstantiation + + // FuncInstantiation indicates whether the package instantiates any generic + // functions. + FuncInstantiation +) + +func (f Features) String() string { + var feats []string + if f&GenericTypeDecls != 0 { + feats = append(feats, "typeDecl") + } + if f&GenericFuncDecls != 0 { + feats = append(feats, "funcDecl") + } + if f&EmbeddedTypeSets != 0 { + feats = append(feats, "typeSet") + } + if f&TypeInstantiation != 0 { + feats = append(feats, "typeInstance") + } + if f&FuncInstantiation != 0 { + feats = append(feats, "funcInstance") + } + return "features{" + strings.Join(feats, ",") + "}" +} + +type featuresFact struct { + Features Features +} + +func (f *featuresFact) AFact() {} +func (f *featuresFact) String() string { return f.Features.String() } + +func run(pass *analysis.Pass) (interface{}, error) { + direct := directFeatures(pass) + + transitive := direct | importedTransitiveFeatures(pass) + if transitive != 0 { + pass.ExportPackageFact(&featuresFact{transitive}) + } + + return &Result{ + Direct: direct, + Transitive: transitive, + }, nil +} + +// directFeatures computes which generic features are used directly by the +// package being analyzed. +func directFeatures(pass *analysis.Pass) Features { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.FuncType)(nil), + (*ast.InterfaceType)(nil), + (*ast.ImportSpec)(nil), + (*ast.TypeSpec)(nil), + } + + var direct Features + + inspect.Preorder(nodeFilter, func(node ast.Node) { + switch n := node.(type) { + case *ast.FuncType: + if tparams := typeparams.ForFuncType(n); tparams != nil { + direct |= GenericFuncDecls + } + case *ast.InterfaceType: + tv := pass.TypesInfo.Types[n] + if iface, _ := tv.Type.(*types.Interface); iface != nil && !typeparams.IsMethodSet(iface) { + direct |= EmbeddedTypeSets + } + case *ast.TypeSpec: + if tparams := typeparams.ForTypeSpec(n); tparams != nil { + direct |= GenericTypeDecls + } + } + }) + + instances := typeparams.GetInstances(pass.TypesInfo) + for _, inst := range instances { + switch inst.Type.(type) { + case *types.Named: + direct |= TypeInstantiation + case *types.Signature: + direct |= FuncInstantiation + } + } + return direct +} + +// importedTransitiveFeatures computes features that are used transitively via +// imports. +func importedTransitiveFeatures(pass *analysis.Pass) Features { + var feats Features + for _, imp := range pass.Pkg.Imports() { + var importedFact featuresFact + if pass.ImportPackageFact(imp, &importedFact) { + feats |= importedFact.Features + } + } + return feats +} diff --git a/go/analysis/passes/usesgenerics/usesgenerics_test.go b/go/analysis/passes/usesgenerics/usesgenerics_test.go new file mode 100644 index 0000000000..3dcff240d4 --- /dev/null +++ b/go/analysis/passes/usesgenerics/usesgenerics_test.go @@ -0,0 +1,21 @@ +// 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 usesgenerics_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/passes/usesgenerics" + "golang.org/x/tools/internal/typeparams" +) + +func Test(t *testing.T) { + if !typeparams.Enabled { + t.Skip("type parameters are not enabled at this Go version") + } + testdata := analysistest.TestData() + analysistest.Run(t, testdata, usesgenerics.Analyzer, "a", "b", "c", "d") +} diff --git a/internal/lsp/analysis/infertypeargs/run_go118.go b/internal/lsp/analysis/infertypeargs/run_go118.go index 96654c0011..1b767e76d0 100644 --- a/internal/lsp/analysis/infertypeargs/run_go118.go +++ b/internal/lsp/analysis/infertypeargs/run_go118.go @@ -33,10 +33,11 @@ func run(pass *analysis.Pass) (interface{}, error) { } // Confirm that instantiation actually occurred at this ident. - _, instance := typeparams.GetInstance(pass.TypesInfo, ident) - if instance == nil { + idata, ok := typeparams.GetInstances(pass.TypesInfo)[ident] + if !ok { return // something went wrong, but fail open } + instance := idata.Type // Start removing argument expressions from the right, and check if we can // still infer the call expression. @@ -62,7 +63,8 @@ func run(pass *analysis.Pass) (interface{}, error) { // Most likely inference failed. break } - _, newInstance := typeparams.GetInstance(info, ident) + newIData := typeparams.GetInstances(info)[ident] + newInstance := newIData.Type if !types.Identical(instance, newInstance) { // The inferred result type does not match the original result type, so // this simplification is not valid. diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go index 140a9d2cb0..f8fddac1ec 100644 --- a/internal/lsp/source/identifier.go +++ b/internal/lsp/source/identifier.go @@ -354,8 +354,8 @@ func fullNode(snapshot Snapshot, obj types.Object, pkg Package) (ast.Decl, error // // If no such signature exists, it returns nil. func inferredSignature(info *types.Info, id *ast.Ident) *types.Signature { - _, typ := typeparams.GetInstance(info, id) - sig, _ := typ.(*types.Signature) + inst := typeparams.GetInstances(info)[id] + sig, _ := inst.Type.(*types.Signature) return sig } diff --git a/internal/typeparams/typeparams_go117.go b/internal/typeparams/typeparams_go117.go index d22899d29e..5a536526c5 100644 --- a/internal/typeparams/typeparams_go117.go +++ b/internal/typeparams/typeparams_go117.go @@ -186,9 +186,16 @@ func NewUnion(terms []*Term) *Union { // InitInstanceInfo is a noop at this Go version. func InitInstanceInfo(*types.Info) {} -// GetInstance returns nothing, as type parameters are not supported at this Go -// version. -func GetInstance(*types.Info, *ast.Ident) (*TypeList, types.Type) { return nil, nil } +// Instance is a placeholder type, as type parameters are not supported at this +// Go version. +type Instance struct { + TypeArgs *TypeList + Type types.Type +} + +// GetInstances returns a nil map, as type parameters are not supported at this +// Go version. +func GetInstances(info *types.Info) map[*ast.Ident]Instance { return nil } // Context is a placeholder type, as type parameters are not supported at // this Go version. diff --git a/internal/typeparams/typeparams_go118.go b/internal/typeparams/typeparams_go118.go index a252183411..e3c48221dd 100644 --- a/internal/typeparams/typeparams_go118.go +++ b/internal/typeparams/typeparams_go118.go @@ -169,15 +169,12 @@ func InitInstanceInfo(info *types.Info) { info.Instances = make(map[*ast.Ident]types.Instance) } -// GetInstance extracts information about the instantiation occurring at the -// identifier id. id should be the identifier denoting a parameterized type or -// function in an instantiation expression or function call. -func GetInstance(info *types.Info, id *ast.Ident) (*TypeList, types.Type) { - if info.Instances != nil { - inf := info.Instances[id] - return inf.TypeArgs, inf.Type - } - return nil, nil +// Instance is an alias for types.Instance. +type Instance = types.Instance + +// GetInstances returns info.Instances. +func GetInstances(info *types.Info) map[*ast.Ident]Instance { + return info.Instances } // Context is an alias for types.Context.