| // 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. |
| |
| // +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" |
| "regexp" |
| "strconv" |
| "strings" |
| "testing" |
| |
| "golang.org/x/tools/go/callgraph" |
| "golang.org/x/tools/go/loader" |
| "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" |
| ) |
| |
| 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 |
| } |
| |
| // 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. |
| // |
| // 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. |
| // |
| // @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" |
| filename 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 |
| } |
| |
| 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.filename, e.linenum) |
| fmt.Printf(format, args...) |
| fmt.Println() |
| } |
| |
| func (e *expectation) needsProbe() bool { |
| 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) { |
| for call := range probes { |
| pos := prog.Fset.Position(call.Pos()) |
| if pos.Line == e.linenum && pos.Filename == e.filename { |
| // TODO(adonovan): send this to test log (display only on failure). |
| // fmt.Printf("%s:%d: info: found probe for %s: %s\n", |
| // e.filename, e.linenum, e, p.arg0) // debugging |
| return call, queries[call.Args[0]].PointsTo() |
| } |
| } |
| return // e.g. analysis didn't reach this call |
| } |
| |
| func doOneInput(input, filename string) bool { |
| var conf loader.Config |
| |
| // Parsing. |
| f, err := conf.ParseFile(filename, input) |
| if err != nil { |
| fmt.Println(err) |
| return false |
| } |
| |
| // Create single-file main package and import its dependencies. |
| conf.CreateFromFiles("main", f) |
| iprog, err := conf.Load() |
| if err != nil { |
| fmt.Println(err) |
| return false |
| } |
| mainPkgInfo := iprog.Created[0].Pkg |
| |
| // SSA creation + building. |
| prog := ssautil.CreateProgram(iprog, ssa.SanityCheckFunctions) |
| prog.Build() |
| |
| mainpkg := prog.Package(mainPkgInfo) |
| ptrmain := mainpkg // main package for the pointer analysis |
| if mainpkg.Func("main") == nil { |
| // No main function; assume it's a test. |
| ptrmain = prog.CreateTestMainPackage(mainpkg) |
| } |
| |
| // 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 { |
| 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, filename: filename, linenum: linenum} |
| |
| if kind == "line" { |
| if rest == "" { |
| ok = false |
| e.errorf("@%s expectation requires identifier", kind) |
| } else { |
| lineMapping[fmt.Sprintf("%s:%d", filename, 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, f.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", filename) |
| |
| // Run the analysis. |
| config := &pointer.Config{ |
| Reflection: true, |
| BuildCallGraph: true, |
| 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.filename == pos.Filename && e.kind == "pointstoquery" { |
| var err error |
| e.extended, err = config.AddExtendedQuery(v, e.query) |
| if err != nil { |
| panic(err) |
| } |
| continue probeLoop |
| } |
| } |
| 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 call *ssa.CallCommon |
| var pts pointer.PointsToSet |
| var tProbe types.Type |
| if e.needsProbe() { |
| if call, pts = findProbe(prog, probes, result.Queries, e); call == nil { |
| ok = false |
| e.errorf("unreachable print() statement has expectation %s", e) |
| continue |
| } |
| if e.extended != nil { |
| pts = e.extended.PointsTo() |
| } |
| tProbe = call.Args[0].Type() |
| if !pointer.CanPoint(tProbe) { |
| ok = false |
| e.errorf("expectation on non-pointerlike operand: %s", tProbe) |
| continue |
| } |
| } |
| |
| switch e.kind { |
| case "pointsto", "pointstoquery": |
| if !checkPointsToExpectation(e, pts, lineMapping, prog) { |
| ok = false |
| } |
| |
| case "types": |
| if !checkTypesExpectation(e, pts, tProbe) { |
| 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, pts 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 _, 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, pts pointer.PointsToSet, typ 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 !pointer.CanHaveDynamicTypes(typ) { |
| e.errorf("@types expectation requires an interface- or reflect.Value-typed operand, got %s", typ) |
| 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{}{}) |
| } |
| } |
| // 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") |
| } |
| ok := true |
| |
| 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 { |
| content, err := ioutil.ReadFile(filename) |
| if err != nil { |
| t.Errorf("couldn't read file '%s': %s", filename, err) |
| continue |
| } |
| |
| if !doOneInput(string(content), filename) { |
| ok = false |
| } |
| } |
| if !ok { |
| t.Fail() |
| } |
| } |
| |
| // 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 |
| } |