blob: 1fa54f6e8f7e5f7baa301d7885dc98fa1a558ba7 [file] [log] [blame]
// Copyright 2013 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.
// No testdata on Android.
//go:build !android
// +build !android
package pointer_test
// This test uses 'expectation' comments embedded within testdata/*.go
// files to specify the expected pointer analysis behaviour.
// See below for grammar.
import (
"bytes"
"errors"
"fmt"
"go/token"
"go/types"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
"unsafe"
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/pointer"
"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{
"testdata/a_test.go",
"testdata/another.go",
"testdata/arrayreflect.go",
"testdata/arrays.go",
"testdata/channels.go",
"testdata/chanreflect.go",
"testdata/context.go",
"testdata/conv.go",
"testdata/extended.go",
"testdata/finalizer.go",
"testdata/flow.go",
"testdata/fmtexcerpt.go",
"testdata/func.go",
"testdata/funcreflect.go",
"testdata/hello.go", // NB: causes spurious failure of HVN cross-check
"testdata/interfaces.go",
"testdata/issue9002.go",
"testdata/mapreflect.go",
"testdata/maps.go",
"testdata/panic.go",
"testdata/recur.go",
"testdata/reflect.go",
"testdata/rtti.go",
"testdata/structreflect.go",
"testdata/structs.go",
// "testdata/timer.go", // TODO(adonovan): fix broken assumptions about runtime timers
}
var raceEnabled = false
// Expectation grammar:
//
// @calls f -> g
//
// A 'calls' expectation asserts that edge (f, g) appears in the
// callgraph. f and g are notated as per Function.String(), which
// may contain spaces (e.g. promoted method in anon struct).
//
// @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 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.
//
// We use '|' because label names may contain spaces, e.g. methods
// of anonymous structs.
//
// 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 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.
//
// We use '|' because type names may contain spaces.
//
// Assertions within generic functions are treated as a union of all
// of the instantiations.
//
// @warning "regexp"
//
// A 'warning' expectation asserts that the analysis issues a
// warning that matches the regular expression within the string
// literal.
//
// @line id
//
// A line directive associates the name "id" with the current
// file:line. The string form of labels will use this id instead of
// a file:line, making @pointsto expectations more robust against
// perturbations in the source file.
// (NB, anon functions still include line numbers.)
type expectation struct {
kind string // "pointsto" | "pointstoquery" | "types" | "calls" | "warning"
filepath string
linenum int // source line number, 1-based
args []string
query string // extended query
extended []*pointer.Pointer // extended query pointer [per instantiation]
types []types.Type // for types
}
func (e *expectation) String() string {
return fmt.Sprintf("@%s[%s]", e.kind, strings.Join(e.args, " | "))
}
func (e *expectation) errorf(format string, args ...interface{}) {
fmt.Printf("%s:%d: ", e.filepath, e.linenum)
fmt.Printf(format, args...)
fmt.Println()
}
func (e *expectation) needsProbe() bool {
return e.kind == "pointsto" || e.kind == "pointstoquery" || e.kind == "types"
}
// 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
calls = append(calls, 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 {
cfg := &packages.Config{
Mode: packages.LoadAllSyntax,
Tests: true,
}
pkgs, err := packages.Load(cfg, fpath)
if err != nil {
fmt.Println(err)
return false
}
if packages.PrintErrors(pkgs) > 0 {
fmt.Println("loaded packages have errors")
return false
}
// SSA creation + building.
mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics
prog, ssaPkgs := ssautil.AllPackages(pkgs, mode)
prog.Build()
// main underlying packages.Package.
mainPpkg := pkgs[0]
mainpkg := ssaPkgs[0]
ptrmain := mainpkg // main package for the pointer analysis
if mainpkg.Func("main") == nil {
// For test programs without main, such as testdata/a_test.go,
// the package with the original code is "main [main.test]" and
// the package with the main is "main.test".
for i, pkg := range pkgs {
if pkg.ID == mainPpkg.ID+".test" {
ptrmain = ssaPkgs[i]
} else if pkg.ID == fmt.Sprintf("%s [%s.test]", mainPpkg.ID, mainPpkg.ID) {
mainpkg = ssaPkgs[i]
}
}
}
// 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.
// Exclude generic bodies as these should be dead code for pointer.
// Instance of generics are included.
probes := make(map[*ssa.CallCommon]bool)
for fn := range ssautil.AllFunctions(prog) {
if isGenericBody(fn) {
continue // skip generic bodies
}
// 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 {
call := instr.Common()
if b, ok := call.Value.(*ssa.Builtin); ok && b.Name() == "print" && len(call.Args) == 1 {
probes[instr.Common()] = true
}
}
}
}
}
}
ok := true
lineMapping := make(map[string]string) // maps "file:line" to @line tag
// Parse expectations in this input.
var exps []*expectation
re := regexp.MustCompile("// *@([a-z]*) *(.*)$")
lines := strings.Split(input, "\n")
for linenum, line := range lines {
linenum++ // make it 1-based
if matches := re.FindAllStringSubmatch(line, -1); matches != nil {
match := matches[0]
kind, rest := match[1], match[2]
e := &expectation{kind: kind, filepath: fpath, linenum: linenum}
if kind == "line" {
if rest == "" {
ok = false
e.errorf("@%s expectation requires identifier", kind)
} else {
lineMapping[fmt.Sprintf("%s:%d", fpath, linenum)] = rest
}
continue
}
if e.needsProbe() && !strings.Contains(line, "print(") {
ok = false
e.errorf("@%s expectation must follow call to print(x)", kind)
continue
}
switch kind {
case "pointsto":
e.args = split(rest, "|")
case "pointstoquery":
args := strings.SplitN(rest, " ", 2)
e.query = args[0]
e.args = split(args[1], "|")
case "types":
for _, typstr := range split(rest, "|") {
var t types.Type = types.Typ[types.Invalid] // means "..."
if typstr != "..." {
tv, err := types.Eval(prog.Fset, mainpkg.Pkg, mainPpkg.Syntax[0].Pos(), typstr)
if err != nil {
ok = false
// Don't print err since its location is bad.
e.errorf("'%s' is not a valid type: %s", typstr, err)
continue
}
t = tv.Type
}
e.types = append(e.types, t)
}
case "calls":
e.args = split(rest, "->")
// TODO(adonovan): eagerly reject the
// expectation if fn doesn't denote
// existing function, rather than fail
// the expectation after analysis.
if len(e.args) != 2 {
ok = false
e.errorf("@calls expectation wants 'caller -> callee' arguments")
continue
}
case "warning":
lit, err := strconv.Unquote(strings.TrimSpace(rest))
if err != nil {
ok = false
e.errorf("couldn't parse @warning operand: %s", err.Error())
continue
}
e.args = append(e.args, lit)
default:
ok = false
e.errorf("unknown expectation kind: %s", e)
continue
}
exps = append(exps, e)
}
}
var log bytes.Buffer
fmt.Fprintf(&log, "Input: %s\n", fpath)
// Run the analysis.
config := &pointer.Config{
Reflection: true,
BuildCallGraph: true,
Mains: []*ssa.Package{ptrmain},
Log: &log,
}
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" {
extended, err := config.AddExtendedQuery(v, e.query)
if err != nil {
panic(err)
}
e.extended = append(e.extended, extended)
}
}
if pointer.CanPoint(v.Type()) {
config.AddQuery(v)
}
}
// Print the log is there was an error or a panic.
complete := false
defer func() {
if !complete || !ok {
log.WriteTo(os.Stderr)
}
}()
result, err := pointer.Analyze(config)
if err != nil {
panic(err) // internal error in pointer analysis
}
// Check the expectations.
for _, e := range exps {
var tProbes []types.Type
var calls []*ssa.CallCommon
var ptss []pointer.PointsToSet
if e.needsProbe() {
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 {
ptss = probesPointTo(calls, result.Queries)
} else {
ptss = make([]pointer.PointsToSet, len(e.extended))
for i, p := range e.extended {
ptss[i] = p.PointsTo()
}
}
var err error
tProbes, err = probesPointToTypes(calls)
if err != nil {
ok = false
e.errorf(err.Error())
continue
}
}
switch e.kind {
case "pointsto", "pointstoquery":
if !checkPointsToExpectation(e, ptss, lineMapping, prog) {
ok = false
}
case "types":
if !checkTypesExpectation(e, ptss, tProbes) {
ok = false
}
case "calls":
if !checkCallsExpectation(prog, e, result.CallGraph) {
ok = false
}
case "warning":
if !checkWarningExpectation(prog, e, result.Warnings) {
ok = false
}
}
}
complete = true
// ok = false // debugging: uncomment to always see log
return ok
}
func labelString(l *pointer.Label, lineMapping map[string]string, prog *ssa.Program) string {
// Functions and Globals need no pos suffix,
// nor do allocations in intrinsic operations
// (for which we'll print the function name).
switch l.Value().(type) {
case nil, *ssa.Function, *ssa.Global:
return l.String()
}
str := l.String()
if pos := l.Pos(); pos != token.NoPos {
// Append the position, using a @line tag instead of a line number, if defined.
posn := prog.Fset.Position(pos)
s := fmt.Sprintf("%s:%d", posn.Filename, posn.Line)
if tag, ok := lineMapping[s]; ok {
return fmt.Sprintf("%s@%s:%d", str, tag, posn.Column)
}
str = fmt.Sprintf("%s@%s", str, posn)
}
return str
}
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
for _, g := range e.args {
if g == "..." {
exact = false
continue
}
expected[g]++
}
// Find the set of labels that the probe's
// argument (x in print(x)) may point to.
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:
ok := true
for _, count := range expected {
if count > 0 {
ok = false
e.errorf("value does not alias these expected labels: %s", join(expected))
break
}
}
for _, count := range surplus {
if count > 0 {
ok = false
e.errorf("value may additionally alias these labels: %s", join(surplus))
break
}
}
return ok
}
func checkTypesExpectation(e *expectation, ptss []pointer.PointsToSet, typs []types.Type) bool {
var expected typeutil.Map
var surplus typeutil.Map
exact := true
for _, g := range e.types {
if g == types.Typ[types.Invalid] {
exact = false
continue
}
expected.Set(g, struct{}{})
}
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 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:
ok := true
if expected.Len() > 0 {
ok = false
e.errorf("interface cannot contain these types: %s", expected.KeysString())
}
if surplus.Len() > 0 {
ok = false
e.errorf("interface may additionally contain these types: %s", surplus.KeysString())
}
return ok
}
var errOK = errors.New("OK")
func checkCallsExpectation(prog *ssa.Program, e *expectation, cg *callgraph.Graph) bool {
found := make(map[string]int)
err := callgraph.GraphVisitEdges(cg, func(edge *callgraph.Edge) error {
// Name-based matching is inefficient but it allows us to
// match functions whose names that would not appear in an
// index ("<root>") or which are not unique ("func@1.2").
if edge.Caller.Func.String() == e.args[0] {
calleeStr := edge.Callee.Func.String()
if calleeStr == e.args[1] {
return errOK // expectation satisfied; stop the search
}
found[calleeStr]++
}
return nil
})
if err == errOK {
return true
}
if len(found) == 0 {
e.errorf("didn't find any calls from %s", e.args[0])
}
e.errorf("found no call from %s to %s, but only to %s",
e.args[0], e.args[1], join(found))
return false
}
func checkWarningExpectation(prog *ssa.Program, e *expectation, warnings []pointer.Warning) bool {
// TODO(adonovan): check the position part of the warning too?
re, err := regexp.Compile(e.args[0])
if err != nil {
e.errorf("invalid regular expression in @warning expectation: %s", err.Error())
return false
}
if len(warnings) == 0 {
e.errorf("@warning %q expectation, but no warnings", e.args[0])
return false
}
for _, w := range warnings {
if re.MatchString(w.Message) {
return true
}
}
e.errorf("@warning %q expectation not satisfied; found these warnings though:", e.args[0])
for _, w := range warnings {
fmt.Printf("%s: warning: %s\n", prog.Fset.Position(w.Pos), w.Message)
}
return false
}
func TestInput(t *testing.T) {
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.Errorf("os.Getwd: %s", err)
return
}
// 'go test' does a chdir so that relative paths in
// diagnostics no longer make sense relative to the invoking
// shell's cwd. We print a special marker so that Emacs can
// make sense of them.
fmt.Fprintf(os.Stderr, "Entering directory `%s'\n", wd)
for _, filename := range inputs {
filename := filename
t.Run(filename, func(t *testing.T) {
if filename == "testdata/a_test.go" {
// For some reason this particular file is way more expensive than the others.
if unsafe.Sizeof(unsafe.Pointer(nil)) <= 4 {
t.Skip("skipping memory-intensive test on platform with small address space; https://golang.org/issue/14113")
}
if raceEnabled {
t.Skip("skipping memory-intensive test under race detector; https://golang.org/issue/14113")
}
} else {
t.Parallel()
}
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.Fatalf("couldn't get absolute path for '%s': %s", filename, err)
}
if !doOneInput(t, string(content), fpath) {
t.Fail()
}
})
}
}
// isGenericBody returns true if fn is the body of a generic function.
func isGenericBody(fn *ssa.Function) bool {
sig := fn.Signature
if typeparams.ForSignature(sig).Len() > 0 || typeparams.RecvTypeParams(sig).Len() > 0 {
return fn.Synthetic == ""
}
return false
}
// join joins the elements of multiset with " | "s.
func join(set map[string]int) string {
var buf bytes.Buffer
sep := ""
for name, count := range set {
for i := 0; i < count; i++ {
buf.WriteString(sep)
sep = " | "
buf.WriteString(name)
}
}
return buf.String()
}
// split returns the list of sep-delimited non-empty strings in s.
func split(s, sep string) (r []string) {
for _, elem := range strings.Split(s, sep) {
elem = strings.TrimSpace(elem)
if elem != "" {
r = append(r, elem)
}
}
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()
}
}