pointer: Adds unit tests for pointer with type parameters.
Adds tests for pointer that test pointer flows through type instantiations.
Updates pointer_test.go to support one probe per instantiation. This is due to there being one copy of a Function per instantiation.
Updates golang/go#48525
Updates golang/go#52504
Change-Id: Ia750cd37ddf1aaf55342ff8464f12e96e7e1030f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/402276
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
Reviewed-by: Guodong Li <guodongli@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Guodong Li <guodongli@google.com>
diff --git a/go/pointer/example_test.go b/go/pointer/example_test.go
index 6e02092..00017df 100644
--- a/go/pointer/example_test.go
+++ b/go/pointer/example_test.go
@@ -61,7 +61,7 @@
}
// Create SSA-form program representation.
- prog := ssautil.CreateProgram(iprog, 0)
+ prog := ssautil.CreateProgram(iprog, ssa.InstantiateGenerics)
mainPkg := prog.Package(iprog.Created[0].Pkg)
// Build SSA code for bodies of all functions in the whole program.
diff --git a/go/pointer/pointer_test.go b/go/pointer/pointer_test.go
index 49065c4..47074f6 100644
--- a/go/pointer/pointer_test.go
+++ b/go/pointer/pointer_test.go
@@ -34,6 +34,7 @@
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
"golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/typeparams"
)
var inputs = []string{
@@ -75,39 +76,45 @@
//
// @pointsto a | b | c
//
-// A 'pointsto' expectation asserts that the points-to set of its
-// operand contains exactly the set of labels {a,b,c} notated as per
-// labelString.
+// A 'pointsto' expectation asserts that the points-to set of its
+// operand contains exactly the set of labels {a,b,c} notated as per
+// labelString.
//
-// A 'pointsto' expectation must appear on the same line as a
-// print(x) statement; the expectation's operand is x.
+// A 'pointsto' expectation must appear on the same line as a
+// print(x) statement; the expectation's operand is x.
//
-// If one of the strings is "...", the expectation asserts that the
-// points-to set at least the other labels.
+// If one of the strings is "...", the expectation asserts that the
+// points-to set at least the other labels.
//
-// We use '|' because label names may contain spaces, e.g. methods
-// of anonymous structs.
+// We use '|' because label names may contain spaces, e.g. methods
+// of anonymous structs.
//
-// From a theoretical perspective, concrete types in interfaces are
-// labels too, but they are represented differently and so have a
-// different expectation, @types, below.
+// Assertions within generic functions are treated as a union of all
+// of the instantiations.
+//
+// From a theoretical perspective, concrete types in interfaces are
+// labels too, but they are represented differently and so have a
+// different expectation, @types, below.
//
// @types t | u | v
//
-// A 'types' expectation asserts that the set of possible dynamic
-// types of its interface operand is exactly {t,u,v}, notated per
-// go/types.Type.String(). In other words, it asserts that the type
-// component of the interface may point to that set of concrete type
-// literals. It also works for reflect.Value, though the types
-// needn't be concrete in that case.
+// A 'types' expectation asserts that the set of possible dynamic
+// types of its interface operand is exactly {t,u,v}, notated per
+// go/types.Type.String(). In other words, it asserts that the type
+// component of the interface may point to that set of concrete type
+// literals. It also works for reflect.Value, though the types
+// needn't be concrete in that case.
//
-// A 'types' expectation must appear on the same line as a
-// print(x) statement; the expectation's operand is x.
+// A 'types' expectation must appear on the same line as a
+// print(x) statement; the expectation's operand is x.
//
-// If one of the strings is "...", the expectation asserts that the
-// interface's type may point to at least the other types.
+// If one of the strings is "...", the expectation asserts that the
+// interface's type may point to at least the other types.
//
-// We use '|' because type names may contain spaces.
+// We use '|' because type names may contain spaces.
+//
+// Assertions within generic functions are treated as a union of all
+// of the instantiations.
//
// @warning "regexp"
//
@@ -127,9 +134,9 @@
filepath string
linenum int // source line number, 1-based
args []string
- query string // extended query
- extended *pointer.Pointer // extended query pointer
- types []types.Type // for types
+ query string // extended query
+ extended []*pointer.Pointer // extended query pointer [per instantiation]
+ types []types.Type // for types
}
func (e *expectation) String() string {
@@ -146,18 +153,43 @@
return e.kind == "pointsto" || e.kind == "pointstoquery" || e.kind == "types"
}
-// Find probe (call to print(x)) of same source file/line as expectation.
-func findProbe(prog *ssa.Program, probes map[*ssa.CallCommon]bool, queries map[ssa.Value]pointer.Pointer, e *expectation) (site *ssa.CallCommon, pts pointer.PointsToSet) {
+// Find probes (call to print(x)) of same source file/line as expectation.
+//
+// May match multiple calls for different instantiations.
+func findProbes(prog *ssa.Program, probes map[*ssa.CallCommon]bool, e *expectation) []*ssa.CallCommon {
+ var calls []*ssa.CallCommon
for call := range probes {
pos := prog.Fset.Position(call.Pos())
if pos.Line == e.linenum && pos.Filename == e.filepath {
// TODO(adonovan): send this to test log (display only on failure).
// fmt.Printf("%s:%d: info: found probe for %s: %s\n",
// e.filepath, e.linenum, e, p.arg0) // debugging
- return call, queries[call.Args[0]].PointsTo()
+ calls = append(calls, call)
}
}
- return // e.g. analysis didn't reach this call
+ return calls
+}
+
+// Find points to sets of probes (call to print(x)).
+func probesPointTo(calls []*ssa.CallCommon, queries map[ssa.Value]pointer.Pointer) []pointer.PointsToSet {
+ ptss := make([]pointer.PointsToSet, len(calls))
+ for i, call := range calls {
+ ptss[i] = queries[call.Args[0]].PointsTo()
+ }
+ return ptss
+}
+
+// Find the types of the probes (call to print(x)).
+// Returns an error if type of the probe cannot point.
+func probesPointToTypes(calls []*ssa.CallCommon) ([]types.Type, error) {
+ tProbes := make([]types.Type, len(calls))
+ for i, call := range calls {
+ tProbes[i] = call.Args[0].Type()
+ if !pointer.CanPoint(tProbes[i]) {
+ return nil, fmt.Errorf("expectation on non-pointerlike operand: %s", tProbes[i])
+ }
+ }
+ return tProbes, nil
}
func doOneInput(t *testing.T, input, fpath string) bool {
@@ -176,7 +208,8 @@
}
// SSA creation + building.
- prog, ssaPkgs := ssautil.AllPackages(pkgs, ssa.SanityCheckFunctions)
+ mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics
+ prog, ssaPkgs := ssautil.AllPackages(pkgs, mode)
prog.Build()
// main underlying packages.Package.
@@ -196,12 +229,19 @@
}
}
+ // files in mainPpkg.
+ mainFiles := make(map[*token.File]bool)
+ for _, syn := range mainPpkg.Syntax {
+ mainFiles[prog.Fset.File(syn.Pos())] = true
+ }
+
// Find all calls to the built-in print(x). Analytically,
// print is a no-op, but it's a convenient hook for testing
// the PTS of an expression, so our tests use it.
probes := make(map[*ssa.CallCommon]bool)
for fn := range ssautil.AllFunctions(prog) {
- if fn.Pkg == mainpkg {
+ // TODO(taking): Switch to a more principled check like fn.declaredPackage() == mainPkg if _Origin is exported.
+ if fn.Pkg == mainpkg || (fn.Pkg == nil && mainFiles[prog.Fset.File(fn.Pos())]) {
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
if instr, ok := instr.(ssa.CallInstruction); ok {
@@ -310,18 +350,16 @@
Mains: []*ssa.Package{ptrmain},
Log: &log,
}
-probeLoop:
for probe := range probes {
v := probe.Args[0]
pos := prog.Fset.Position(probe.Pos())
for _, e := range exps {
if e.linenum == pos.Line && e.filepath == pos.Filename && e.kind == "pointstoquery" {
- var err error
- e.extended, err = config.AddExtendedQuery(v, e.query)
+ extended, err := config.AddExtendedQuery(v, e.query)
if err != nil {
panic(err)
}
- continue probeLoop
+ e.extended = append(e.extended, extended)
}
}
if pointer.CanPoint(v.Type()) {
@@ -344,34 +382,42 @@
// Check the expectations.
for _, e := range exps {
- var call *ssa.CallCommon
- var pts pointer.PointsToSet
- var tProbe types.Type
+ var tProbes []types.Type
+ var calls []*ssa.CallCommon
+ var ptss []pointer.PointsToSet
if e.needsProbe() {
- if call, pts = findProbe(prog, probes, result.Queries, e); call == nil {
+ calls = findProbes(prog, probes, e)
+ if len(calls) == 0 {
ok = false
e.errorf("unreachable print() statement has expectation %s", e)
continue
}
- if e.extended != nil {
- pts = e.extended.PointsTo()
+ if e.extended == nil {
+ ptss = probesPointTo(calls, result.Queries)
+ } else {
+ ptss = make([]pointer.PointsToSet, len(e.extended))
+ for i, p := range e.extended {
+ ptss[i] = p.PointsTo()
+ }
}
- tProbe = call.Args[0].Type()
- if !pointer.CanPoint(tProbe) {
+
+ var err error
+ tProbes, err = probesPointToTypes(calls)
+ if err != nil {
ok = false
- e.errorf("expectation on non-pointerlike operand: %s", tProbe)
+ e.errorf(err.Error())
continue
}
}
switch e.kind {
case "pointsto", "pointstoquery":
- if !checkPointsToExpectation(e, pts, lineMapping, prog) {
+ if !checkPointsToExpectation(e, ptss, lineMapping, prog) {
ok = false
}
case "types":
- if !checkTypesExpectation(e, pts, tProbe) {
+ if !checkTypesExpectation(e, ptss, tProbes) {
ok = false
}
@@ -416,7 +462,7 @@
return str
}
-func checkPointsToExpectation(e *expectation, pts pointer.PointsToSet, lineMapping map[string]string, prog *ssa.Program) bool {
+func checkPointsToExpectation(e *expectation, ptss []pointer.PointsToSet, lineMapping map[string]string, prog *ssa.Program) bool {
expected := make(map[string]int)
surplus := make(map[string]int)
exact := true
@@ -429,12 +475,14 @@
}
// Find the set of labels that the probe's
// argument (x in print(x)) may point to.
- for _, label := range pts.Labels() {
- name := labelString(label, lineMapping, prog)
- if expected[name] > 0 {
- expected[name]--
- } else if exact {
- surplus[name]++
+ for _, pts := range ptss { // treat ptss as union of points-to sets.
+ for _, label := range pts.Labels() {
+ name := labelString(label, lineMapping, prog)
+ if expected[name] > 0 {
+ expected[name]--
+ } else if exact {
+ surplus[name]++
+ }
}
}
// Report multiset difference:
@@ -456,7 +504,7 @@
return ok
}
-func checkTypesExpectation(e *expectation, pts pointer.PointsToSet, typ types.Type) bool {
+func checkTypesExpectation(e *expectation, ptss []pointer.PointsToSet, typs []types.Type) bool {
var expected typeutil.Map
var surplus typeutil.Map
exact := true
@@ -468,18 +516,26 @@
expected.Set(g, struct{}{})
}
- if !pointer.CanHaveDynamicTypes(typ) {
- e.errorf("@types expectation requires an interface- or reflect.Value-typed operand, got %s", typ)
+ if len(typs) != len(ptss) {
+ e.errorf("@types expectation internal error differing number of types(%d) and points to sets (%d)", len(typs), len(ptss))
return false
}
// Find the set of types that the probe's
// argument (x in print(x)) may contain.
- for _, T := range pts.DynamicTypes().Keys() {
- if expected.At(T) != nil {
- expected.Delete(T)
- } else if exact {
- surplus.Set(T, struct{}{})
+ for i := range ptss {
+ var Ts []types.Type
+ if pointer.CanHaveDynamicTypes(typs[i]) {
+ Ts = ptss[i].DynamicTypes().Keys()
+ } else {
+ Ts = append(Ts, typs[i]) // static type
+ }
+ for _, T := range Ts {
+ if expected.At(T) != nil {
+ expected.Delete(T)
+ } else if exact {
+ surplus.Set(T, struct{}{})
+ }
}
}
// Report set difference:
@@ -615,3 +671,34 @@
}
return
}
+
+func TestTypeParam(t *testing.T) {
+ if !typeparams.Enabled {
+ t.Skip("TestTypeParamInput requires type parameters")
+ }
+ // Based on TestInput. Keep this up to date with that.
+ filename := "testdata/typeparams.go"
+
+ if testing.Short() {
+ t.Skip("skipping in short mode; this test requires tons of memory; https://golang.org/issue/14113")
+ }
+
+ wd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("os.Getwd: %s", err)
+ }
+ fmt.Fprintf(os.Stderr, "Entering directory `%s'\n", wd)
+
+ content, err := ioutil.ReadFile(filename)
+ if err != nil {
+ t.Fatalf("couldn't read file '%s': %s", filename, err)
+ }
+ fpath, err := filepath.Abs(filename)
+ if err != nil {
+ t.Errorf("couldn't get absolute path for '%s': %s", filename, err)
+ }
+
+ if !doOneInput(t, string(content), fpath) {
+ t.Fail()
+ }
+}
diff --git a/go/pointer/stdlib_test.go b/go/pointer/stdlib_test.go
index 3ba42a1..978cfb8 100644
--- a/go/pointer/stdlib_test.go
+++ b/go/pointer/stdlib_test.go
@@ -46,7 +46,7 @@
}
// Create SSA packages.
- prog, _ := ssautil.AllPackages(pkgs, 0)
+ prog, _ := ssautil.AllPackages(pkgs, ssa.InstantiateGenerics)
prog.Build()
numPkgs := len(prog.AllPackages())
diff --git a/go/pointer/testdata/typeparams.go b/go/pointer/testdata/typeparams.go
new file mode 100644
index 0000000..ddd0a74
--- /dev/null
+++ b/go/pointer/testdata/typeparams.go
@@ -0,0 +1,68 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "os"
+)
+
+type S[T any] struct{ t T }
+
+var theSint S[int]
+var theSbool S[bool]
+
+func (s *S[T]) String() string {
+ print(s) // @pointsto command-line-arguments.theSbool | command-line-arguments.theSint
+ return ""
+}
+
+func Type[T any]() {
+ var x *T
+ print(x) // @types *int | *bool
+}
+
+func Caller[T any]() {
+ var s *S[T]
+ _ = s.String()
+}
+
+var a int
+var b bool
+
+type t[T any] struct {
+ a *map[string]chan *T
+}
+
+func fn[T any](a *T) {
+ m := make(map[string]chan *T)
+ m[""] = make(chan *T, 1)
+ m[""] <- a
+ x := []t[T]{t[T]{a: &m}}
+ print(x) // @pointstoquery <-(*x[i].a)[key] command-line-arguments.a | command-line-arguments.b
+}
+
+func main() {
+ // os.Args is considered intrinsically allocated,
+ // but may also be set explicitly (e.g. on Windows), hence '...'.
+ print(os.Args) // @pointsto <command-line args> | ...
+ fmt.Println("Hello!", &theSint)
+ fmt.Println("World!", &theSbool)
+
+ Type[int]() // call
+ f := Type[bool] // call through a variable
+ _ = Type[string] // not called so will not appear in Type's print.
+ f()
+
+ Caller[int]()
+ Caller[bool]()
+
+ fn(&a)
+ fn(&b)
+}
+
+// @calls (*fmt.pp).handleMethods -> (*command-line-arguments.S[int]).String[[int]]
+// @calls (*fmt.pp).handleMethods -> (*command-line-arguments.S[bool]).String[[bool]]
+// @calls command-line-arguments.Caller[[int]] -> (*command-line-arguments.S[int]).String[[int]]
+// @calls command-line-arguments.Caller[[bool]] -> (*command-line-arguments.S[bool]).String[[bool]]