mirror of
https://github.com/golang/go
synced 2024-11-19 03:24:40 -07:00
7ce958b4a5
(caused by overlapping pending CLs at commit time). R=crawshaw TBR=crawshaw CC=golang-dev https://golang.org/cl/12820048
568 lines
15 KiB
Go
568 lines
15 KiB
Go
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"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/ioutil"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"code.google.com/p/go.tools/go/types"
|
|
"code.google.com/p/go.tools/go/types/typemap"
|
|
"code.google.com/p/go.tools/importer"
|
|
"code.google.com/p/go.tools/pointer"
|
|
"code.google.com/p/go.tools/ssa"
|
|
)
|
|
|
|
var inputs = []string{
|
|
// Currently debugging:
|
|
// "testdata/tmp.go",
|
|
|
|
// Working:
|
|
"testdata/another.go",
|
|
"testdata/arrays.go",
|
|
"testdata/channels.go",
|
|
"testdata/context.go",
|
|
"testdata/conv.go",
|
|
"testdata/flow.go",
|
|
"testdata/fmtexcerpt.go",
|
|
"testdata/func.go",
|
|
"testdata/hello.go",
|
|
"testdata/interfaces.go",
|
|
"testdata/maps.go",
|
|
"testdata/panic.go",
|
|
"testdata/recur.go",
|
|
"testdata/structs.go",
|
|
"testdata/a_test.go",
|
|
|
|
// TODO(adonovan): get these tests (of reflection) passing.
|
|
// (The tests are mostly sound since they were used for a
|
|
// previous implementation.)
|
|
// "testdata/funcreflect.go",
|
|
// "testdata/arrayreflect.go",
|
|
// "testdata/chanreflect.go",
|
|
// "testdata/finalizer.go",
|
|
// "testdata/reflect.go",
|
|
// "testdata/mapreflect.go",
|
|
// "testdata/structreflect.go",
|
|
}
|
|
|
|
// 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, @concrete, below.
|
|
//
|
|
// @concrete t | u | v
|
|
//
|
|
// A 'concrete' 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.
|
|
//
|
|
// A 'concrete' 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 concrete 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" | "concrete" | "calls" | "warning"
|
|
filename string
|
|
linenum int // source line number, 1-based
|
|
args []string
|
|
types []types.Type // for concrete
|
|
}
|
|
|
|
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 == "concrete"
|
|
}
|
|
|
|
// A record of a call to the built-in print() function. Used for testing.
|
|
type probe struct {
|
|
instr *ssa.CallCommon
|
|
arg0 pointer.Pointer // first argument to print
|
|
}
|
|
|
|
// Find probe (call to print(x)) of same source
|
|
// file/line as expectation.
|
|
func findProbe(prog *ssa.Program, probes []probe, e *expectation) *probe {
|
|
for _, p := range probes {
|
|
pos := prog.Fset.Position(p.instr.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 &p
|
|
}
|
|
}
|
|
return nil // e.g. analysis didn't reach this call
|
|
}
|
|
|
|
func doOneInput(input, filename string) bool {
|
|
impctx := &importer.Config{Loader: importer.MakeGoBuildLoader(nil)}
|
|
imp := importer.New(impctx)
|
|
|
|
// Parsing.
|
|
f, err := parser.ParseFile(imp.Fset, filename, input, parser.DeclarationErrors)
|
|
if err != nil {
|
|
// TODO(adonovan): err is a scanner error list;
|
|
// display all errors not just first?
|
|
fmt.Println(err.Error())
|
|
return false
|
|
}
|
|
|
|
// Type checking.
|
|
info := imp.CreateSourcePackage("main", []*ast.File{f})
|
|
if info.Err != nil {
|
|
fmt.Println(info.Err.Error())
|
|
return false
|
|
}
|
|
|
|
// SSA creation + building.
|
|
prog := ssa.NewProgram(imp.Fset, ssa.SanityCheckFunctions)
|
|
for _, info := range imp.Packages {
|
|
prog.CreatePackage(info)
|
|
}
|
|
prog.BuildAll()
|
|
|
|
mainpkg := prog.Package(info.Pkg)
|
|
ptrmain := mainpkg // main package for the pointer analysis
|
|
if mainpkg.Func("main") == nil {
|
|
// No main function; assume it's a test.
|
|
mainpkg.CreateTestMainFunction()
|
|
fmt.Printf("%s: synthesized testmain package for test.\n", imp.Fset.Position(f.Package))
|
|
}
|
|
|
|
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 "concrete":
|
|
for _, typstr := range split(rest, "|") {
|
|
var t types.Type = types.Typ[types.Invalid] // means "..."
|
|
if typstr != "..." {
|
|
texpr, err := parser.ParseExpr(typstr)
|
|
if err != nil {
|
|
ok = false
|
|
// Don't print err since its location is bad.
|
|
e.errorf("'%s' is not a valid type", typstr)
|
|
continue
|
|
}
|
|
t, _, err = types.EvalNode(imp.Fset, texpr, mainpkg.Object, mainpkg.Object.Scope())
|
|
if err != nil {
|
|
ok = false
|
|
// TODO Don't print err since its location is bad.
|
|
e.errorf("'%s' is not a valid type: %s", typstr, err)
|
|
continue
|
|
}
|
|
}
|
|
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 probes []probe
|
|
var warnings []string
|
|
var log bytes.Buffer
|
|
|
|
callgraph := make(pointer.CallGraph)
|
|
|
|
// Run the analysis.
|
|
config := &pointer.Config{
|
|
Mains: []*ssa.Package{ptrmain},
|
|
Log: &log,
|
|
Print: func(site *ssa.CallCommon, p pointer.Pointer) {
|
|
probes = append(probes, probe{site, p})
|
|
},
|
|
Call: callgraph.AddEdge,
|
|
Warn: func(pos token.Pos, format string, args ...interface{}) {
|
|
msg := fmt.Sprintf(format, args...)
|
|
fmt.Printf("%s: warning: %s\n", prog.Fset.Position(pos), msg)
|
|
warnings = append(warnings, msg)
|
|
},
|
|
}
|
|
pointer.Analyze(config)
|
|
|
|
// Print the log is there was an error or a panic.
|
|
complete := false
|
|
defer func() {
|
|
if !complete || !ok {
|
|
log.WriteTo(os.Stderr)
|
|
}
|
|
}()
|
|
|
|
// Check the expectations.
|
|
for _, e := range exps {
|
|
var pr *probe
|
|
if e.needsProbe() {
|
|
if pr = findProbe(prog, probes, e); pr == nil {
|
|
ok = false
|
|
e.errorf("unreachable print() statement has expectation %s", e)
|
|
continue
|
|
}
|
|
if pr.arg0 == nil {
|
|
ok = false
|
|
e.errorf("expectation on non-pointerlike operand: %s", pr.instr.Args[0].Type())
|
|
continue
|
|
}
|
|
}
|
|
|
|
switch e.kind {
|
|
case "pointsto":
|
|
if !checkPointsToExpectation(e, pr, lineMapping, prog) {
|
|
ok = false
|
|
}
|
|
|
|
case "concrete":
|
|
if !checkConcreteExpectation(e, pr) {
|
|
ok = false
|
|
}
|
|
|
|
case "calls":
|
|
if !checkCallsExpectation(prog, e, callgraph) {
|
|
ok = false
|
|
}
|
|
|
|
case "warning":
|
|
if !checkWarningExpectation(prog, e, 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.
|
|
switch l.Value.(type) {
|
|
case *ssa.Function, *ssa.Global:
|
|
return l.String()
|
|
}
|
|
|
|
str := l.String()
|
|
if pos := l.Value.Pos(); pos != 0 {
|
|
// Append the position, using a @line tag instead of a line number, if defined.
|
|
posn := prog.Fset.Position(l.Value.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, pr *probe, lineMapping map[string]string, prog *ssa.Program) bool {
|
|
expected := make(map[string]struct{})
|
|
surplus := make(map[string]struct{})
|
|
exact := true
|
|
for _, g := range e.args {
|
|
if g == "..." {
|
|
exact = false
|
|
continue
|
|
}
|
|
expected[g] = struct{}{}
|
|
}
|
|
// Find the set of labels that the probe's
|
|
// argument (x in print(x)) may point to.
|
|
for _, label := range pr.arg0.PointsTo().Labels() {
|
|
name := labelString(label, lineMapping, prog)
|
|
if _, ok := expected[name]; ok {
|
|
delete(expected, name)
|
|
} else if exact {
|
|
surplus[name] = struct{}{}
|
|
}
|
|
}
|
|
// Report set difference:
|
|
ok := true
|
|
if len(expected) > 0 {
|
|
ok = false
|
|
e.errorf("value does not alias these expected labels: %s", join(expected))
|
|
}
|
|
if len(surplus) > 0 {
|
|
ok = false
|
|
e.errorf("value may additionally alias these labels: %s", join(surplus))
|
|
}
|
|
return ok
|
|
}
|
|
|
|
// underlying returns the underlying type of typ. Copied from go/types.
|
|
func underlyingType(typ types.Type) types.Type {
|
|
if typ, ok := typ.(*types.Named); ok {
|
|
return typ.Underlying() // underlying types are never NamedTypes
|
|
}
|
|
if typ == nil {
|
|
panic("underlying(nil)")
|
|
}
|
|
return typ
|
|
}
|
|
|
|
func checkConcreteExpectation(e *expectation, pr *probe) bool {
|
|
var expected typemap.M
|
|
var surplus typemap.M
|
|
exact := true
|
|
for _, g := range e.types {
|
|
if g == types.Typ[types.Invalid] {
|
|
exact = false
|
|
continue
|
|
}
|
|
expected.Set(g, struct{}{})
|
|
}
|
|
|
|
switch t := underlyingType(pr.instr.Args[0].Type()).(type) {
|
|
case *types.Interface:
|
|
// ok
|
|
default:
|
|
e.errorf("@concrete expectation requires an interface-typed operand, got %s", t)
|
|
return false
|
|
}
|
|
|
|
// Find the set of concrete types that the probe's
|
|
// argument (x in print(x)) may contain.
|
|
for _, conc := range pr.arg0.PointsTo().ConcreteTypes().Keys() {
|
|
if expected.At(conc) != nil {
|
|
expected.Delete(conc)
|
|
} else if exact {
|
|
surplus.Set(conc, struct{}{})
|
|
}
|
|
}
|
|
// Report set difference:
|
|
ok := true
|
|
if expected.Len() > 0 {
|
|
ok = false
|
|
e.errorf("interface cannot contain these concrete types: %s", expected.KeysString())
|
|
}
|
|
if surplus.Len() > 0 {
|
|
ok = false
|
|
e.errorf("interface may additionally contain these concrete types: %s", surplus.KeysString())
|
|
}
|
|
return ok
|
|
return false
|
|
}
|
|
|
|
func checkCallsExpectation(prog *ssa.Program, e *expectation, callgraph pointer.CallGraph) bool {
|
|
// TODO(adonovan): this is inefficient and not robust against
|
|
// typos. Better to convert strings to *Functions during
|
|
// expectation parsing (somehow).
|
|
for caller, callees := range callgraph {
|
|
if caller.Func().String() == e.args[0] {
|
|
found := make(map[string]struct{})
|
|
for callee := range callees {
|
|
s := callee.Func().String()
|
|
found[s] = struct{}{}
|
|
if s == e.args[1] {
|
|
return true // expectation satisfied
|
|
}
|
|
}
|
|
e.errorf("found no call from %s to %s, but only to %s",
|
|
e.args[0], e.args[1], join(found))
|
|
return false
|
|
}
|
|
}
|
|
e.errorf("didn't find any calls from %s", e.args[0])
|
|
return false
|
|
}
|
|
|
|
func checkWarningExpectation(prog *ssa.Program, e *expectation, warnings []string) 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 %s expectation, but no warnings", strconv.Quote(e.args[0]))
|
|
return false
|
|
}
|
|
|
|
for _, warning := range warnings {
|
|
if re.MatchString(warning) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
e.errorf("@warning %s expectation not satised; found these warnings though:", strconv.Quote(e.args[0]))
|
|
for _, warning := range warnings {
|
|
fmt.Println("\t", warning)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestInput(t *testing.T) {
|
|
ok := true
|
|
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Errorf("os.Getwd: %s", err.Error())
|
|
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.Error())
|
|
continue
|
|
}
|
|
|
|
if !doOneInput(string(content), filename) {
|
|
ok = false
|
|
}
|
|
}
|
|
if !ok {
|
|
t.Fail()
|
|
}
|
|
}
|
|
|
|
// join joins the elements of set with " | "s.
|
|
func join(set map[string]struct{}) string {
|
|
var buf bytes.Buffer
|
|
sep := ""
|
|
for name := range set {
|
|
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
|
|
}
|