From da3a30b5e1f6ffc78e853c43cf4ebb70644a0232 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 15 Jul 2013 18:09:18 -0400 Subject: [PATCH] go.tools/ssa: move pure-AST functions to importer package. This slightly simplifies the PathEnclosingInterval spec (and some uncommitted client code of mine). R=gri CC=golang-dev https://golang.org/cl/11305043 --- ssa/source_ast.go => importer/source.go | 35 ++- importer/source_test.go | 278 ++++++++++++++++++++++++ ssa/promote.go | 4 +- ssa/source.go | 38 ---- ssa/source_test.go | 270 +---------------------- 5 files changed, 313 insertions(+), 312 deletions(-) rename ssa/source_ast.go => importer/source.go (94%) create mode 100644 importer/source_test.go diff --git a/ssa/source_ast.go b/importer/source.go similarity index 94% rename from ssa/source_ast.go rename to importer/source.go index 0d946f1e..326d5d25 100644 --- a/ssa/source_ast.go +++ b/importer/source.go @@ -1,11 +1,7 @@ -package ssa +package importer // This file defines utilities for working with source positions. -// It has no dependencies on ssa or go/types. -// TODO(adonovan): move it somewhere more general, -// e.g. go.tools/importer? - import ( "fmt" "go/ast" @@ -623,3 +619,32 @@ func NodeDescription(n ast.Node) string { } panic(fmt.Sprintf("unexpected node type: %T", n)) } + +// TODO(adonovan): make this a method: func (*token.File) Contains(token.Pos) +func tokenFileContainsPos(f *token.File, pos token.Pos) bool { + p := int(pos) + base := f.Base() + return base <= p && p < base+f.Size() +} + +// PathEnclosingInterval returns the PackageInfo and ast.Node that +// contain source interval [start, end), and all the node's ancestors +// up to the AST root. It searches all ast.Files of all packages in the +// Importer imp. exact is defined as for standalone +// PathEnclosingInterval. +// +// The result is (nil, nil, false) if not found. +// +func (imp *Importer) PathEnclosingInterval(start, end token.Pos) (pkg *PackageInfo, path []ast.Node, exact bool) { + for _, info := range imp.Packages { + for _, f := range info.Files { + if !tokenFileContainsPos(imp.Fset.File(f.Package), start) { + continue + } + if path, exact := PathEnclosingInterval(f, start, end); path != nil { + return info, path, exact + } + } + } + return nil, nil, false +} diff --git a/importer/source_test.go b/importer/source_test.go new file mode 100644 index 00000000..8493425d --- /dev/null +++ b/importer/source_test.go @@ -0,0 +1,278 @@ +package importer_test + +// This file defines tests of source utilities. + +// TODO(adonovan): exhaustive tests that run over the whole input +// tree, not just handcrafted examples. + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/token" + "strings" + "testing" + + "code.google.com/p/go.tools/importer" + "code.google.com/p/go.tools/ssa" +) + +// pathToString returns a string containing the concrete types of the +// nodes in path. +func pathToString(path []ast.Node) string { + var buf bytes.Buffer + fmt.Fprint(&buf, "[") + for i, n := range path { + if i > 0 { + fmt.Fprint(&buf, " ") + } + fmt.Fprint(&buf, strings.TrimPrefix(fmt.Sprintf("%T", n), "*ast.")) + } + fmt.Fprint(&buf, "]") + return buf.String() +} + +// findInterval parses input and returns the [start, end) positions of +// the first occurrence of substr in input. f==nil indicates failure; +// an error has already been reported in that case. +// +func findInterval(t *testing.T, fset *token.FileSet, input, substr string) (f *ast.File, start, end token.Pos) { + f, err := parser.ParseFile(fset, "", input, parser.DeclarationErrors) + if err != nil { + t.Errorf("parse error: %s", err) + return + } + + i := strings.Index(input, substr) + if i < 0 { + t.Errorf("%q is not a substring of input", substr) + f = nil + return + } + + filePos := fset.File(f.Package) + return f, filePos.Pos(i), filePos.Pos(i + len(substr)) +} + +// Common input for following tests. +const input = ` +// Hello. +package main +import "fmt" +func f() {} +func main() { + z := (x + y) // add them + f() // NB: ExprStmt and its CallExpr have same Pos/End +} +` + +func TestPathEnclosingInterval_Exact(t *testing.T) { + // For the exact tests, we check that a substring is mapped to + // the canonical string for the node it denotes. + tests := []struct { + substr string // first occurrence of this string indicates interval + node string // complete text of expected containing node + }{ + {"package", + input[11 : len(input)-1]}, + {"\npack", + input[11 : len(input)-1]}, + {"main", + "main"}, + {"import", + "import \"fmt\""}, + {"\"fmt\"", + "\"fmt\""}, + {"\nfunc f() {}\n", + "func f() {}"}, + {"x ", + "x"}, + {" y", + "y"}, + {"z", + "z"}, + {" + ", + "x + y"}, + {" :=", + "z := (x + y)"}, + {"x + y", + "x + y"}, + {"(x + y)", + "(x + y)"}, + {" (x + y) ", + "(x + y)"}, + {" (x + y) // add", + "(x + y)"}, + {"func", + "func f() {}"}, + {"func f() {}", + "func f() {}"}, + {"\nfun", + "func f() {}"}, + {" f", + "f"}, + } + for _, test := range tests { + f, start, end := findInterval(t, new(token.FileSet), input, test.substr) + if f == nil { + continue + } + + path, exact := importer.PathEnclosingInterval(f, start, end) + if !exact { + t.Errorf("PathEnclosingInterval(%q) not exact", test.substr) + continue + } + + if len(path) == 0 { + if test.node != "" { + t.Errorf("PathEnclosingInterval(%q).path: got [], want %q", + test.substr, test.node) + } + continue + } + + if got := input[path[0].Pos():path[0].End()]; got != test.node { + t.Errorf("PathEnclosingInterval(%q): got %q, want %q (path was %s)", + test.substr, got, test.node, pathToString(path)) + continue + } + } +} + +func TestPathEnclosingInterval_Paths(t *testing.T) { + // For these tests, we check only the path of the enclosing + // node, but not its complete text because it's often quite + // large when !exact. + tests := []struct { + substr string // first occurrence of this string indicates interval + path string // the pathToString(),exact of the expected path + }{ + {"// add", + "[BlockStmt FuncDecl File],false"}, + {"(x + y", + "[ParenExpr AssignStmt BlockStmt FuncDecl File],false"}, + {"x +", + "[BinaryExpr ParenExpr AssignStmt BlockStmt FuncDecl File],false"}, + {"z := (x", + "[AssignStmt BlockStmt FuncDecl File],false"}, + {"func f", + "[FuncDecl File],false"}, + {"func f()", + "[FuncDecl File],false"}, + {" f()", + "[FuncDecl File],false"}, + {"() {}", + "[FuncDecl File],false"}, + {"// Hello", + "[File],false"}, + {" f", + "[Ident FuncDecl File],true"}, + {"func ", + "[FuncDecl File],true"}, + {"mai", + "[Ident File],true"}, + {"f() // NB", + "[CallExpr ExprStmt BlockStmt FuncDecl File],true"}, + } + for _, test := range tests { + f, start, end := findInterval(t, new(token.FileSet), input, test.substr) + if f == nil { + continue + } + + path, exact := importer.PathEnclosingInterval(f, start, end) + if got := fmt.Sprintf("%s,%v", pathToString(path), exact); got != test.path { + t.Errorf("PathEnclosingInterval(%q): got %q, want %q", + test.substr, got, test.path) + continue + } + } +} + +// -------- Tests of source.go ----------------------------------------- + +func TestEnclosingFunction(t *testing.T) { + tests := []struct { + input string // the input file + substr string // first occurrence of this string denotes interval + fn string // name of expected containing function + }{ + // We use distinctive numbers as syntactic landmarks. + + // Ordinary function: + {`package main + func f() { println(1003) }`, + "100", "main.f"}, + // Methods: + {`package main + type T int + func (t T) f() { println(200) }`, + "200", "(main.T).f"}, + // Function literal: + {`package main + func f() { println(func() { print(300) }) }`, + "300", "func@2.24"}, + // Doubly nested + {`package main + func f() { println(func() { print(func() { print(350) })})}`, + "350", "func@2.39"}, + // Implicit init for package-level var initializer. + {"package main; var a = 400", "400", "main.init"}, + // No code for constants: + {"package main; const a = 500", "500", "(none)"}, + // Explicit init() + {"package main; func init() { println(600) }", "600", "main.init"}, + // Multiple explicit init functions: + {`package main + func init() { println("foo") } + func init() { println(800) }`, + "800", "main.init"}, + // init() containing FuncLit. + {`package main + func init() { println(func(){print(900)}) }`, + "900", "func@2.27"}, + } + for _, test := range tests { + imp := importer.New(new(importer.Context)) // (NB: no Loader) + f, start, end := findInterval(t, imp.Fset, test.input, test.substr) + if f == nil { + continue + } + path, exact := importer.PathEnclosingInterval(f, start, end) + if !exact { + t.Errorf("EnclosingFunction(%q) not exact", test.substr) + continue + } + info, err := imp.CreateSourcePackage("main", []*ast.File{f}) + if err != nil { + t.Error(err.Error()) + continue + } + + prog := ssa.NewProgram(imp.Fset, 0) + prog.CreatePackages(imp) + pkg := prog.Package(info.Pkg) + pkg.Build() + + name := "(none)" + fn := ssa.EnclosingFunction(pkg, path) + if fn != nil { + name = fn.String() + } + + if name != test.fn { + t.Errorf("EnclosingFunction(%q in %q) got %s, want %s", + test.substr, test.input, name, test.fn) + continue + } + + // While we're here: test HasEnclosingFunction. + if has := ssa.HasEnclosingFunction(pkg, path); has != (fn != nil) { + t.Errorf("HasEnclosingFunction(%q in %q) got %v, want %v", + test.substr, test.input, has, fn != nil) + continue + } + } +} diff --git a/ssa/promote.go b/ssa/promote.go index df382edf..f7b563a5 100644 --- a/ssa/promote.go +++ b/ssa/promote.go @@ -9,7 +9,7 @@ package ssa // - bound method wrappers, for uncalled obj.Method closures. // - indirection wrappers, for calls to T-methods on a *T receiver. -// TODO(adonovan): rename to methods.go. +// TODO(adonovan): rename to wrappers.go when promotion logic has evaporated. import ( "code.google.com/p/go.tools/go/types" @@ -127,6 +127,8 @@ func (p *Program) MethodSet(typ types.Type) MethodSet { // buildMethodSet computes the concrete method set for type typ. // It is the implementation of Program.MethodSet. // +// TODO(adonovan): use go/types.MethodSet(typ) when it's ready. +// // EXCLUSIVE_LOCKS_REQUIRED(meth.Prog.methodsMu) // func buildMethodSet(prog *Program, typ types.Type) MethodSet { diff --git a/ssa/source.go b/ssa/source.go index d97ecd58..ddbe378c 100644 --- a/ssa/source.go +++ b/ssa/source.go @@ -7,46 +7,8 @@ import ( "go/token" "code.google.com/p/go.tools/go/types" - "code.google.com/p/go.tools/importer" ) -// TODO(adonovan): make this a method: func (*token.File) Contains(token.Pos) -func tokenFileContainsPos(f *token.File, pos token.Pos) bool { - p := int(pos) - base := f.Base() - return base <= p && p < base+f.Size() -} - -// PathEnclosingInterval returns the Package and ast.Node that -// contain source interval [start, end), and all the node's ancestors -// up to the AST root. It searches all files of all packages in the -// program prog. exact is defined as for standalone -// PathEnclosingInterval. -// -// imp provides ASTs for the program's packages. -// -// pkg may be nil if no SSA package has yet been created for the found -// package. Call prog.CreatePackages(imp) to avoid this. -// -// The result is (nil, nil, false) if not found. -// -func (prog *Program) PathEnclosingInterval(imp *importer.Importer, start, end token.Pos) (pkg *Package, path []ast.Node, exact bool) { - for importPath, info := range imp.Packages { - for _, f := range info.Files { - if !tokenFileContainsPos(imp.Fset.File(f.Package), start) { - continue - } - if path, exact := PathEnclosingInterval(f, start, end); path != nil { - // TODO(adonovan): return info in lieu - // of pkg; remove Prog as a parameter; - // move to importer. - return prog.PackagesByPath[importPath], path, exact - } - } - } - return nil, nil, false -} - // EnclosingFunction returns the function that contains the syntax // node denoted by path. // diff --git a/ssa/source_test.go b/ssa/source_test.go index 552bad31..8ed82b20 100644 --- a/ssa/source_test.go +++ b/ssa/source_test.go @@ -1,18 +1,13 @@ package ssa_test -// This file defines tests of the source and source_ast utilities. - -// TODO(adonovan): exhaustive tests that run over the whole input -// tree, not just handcrafted examples. +// This file defines tests of source-level debugging utilities. import ( - "bytes" "fmt" "go/ast" "go/parser" "go/token" "regexp" - "strings" "testing" "code.google.com/p/go.tools/go/exact" @@ -21,267 +16,6 @@ import ( "code.google.com/p/go.tools/ssa" ) -// -------- Tests of source_ast.go ------------------------------------- - -// pathToString returns a string containing the concrete types of the -// nodes in path. -func pathToString(path []ast.Node) string { - var buf bytes.Buffer - fmt.Fprint(&buf, "[") - for i, n := range path { - if i > 0 { - fmt.Fprint(&buf, " ") - } - fmt.Fprint(&buf, strings.TrimPrefix(fmt.Sprintf("%T", n), "*ast.")) - } - fmt.Fprint(&buf, "]") - return buf.String() -} - -// findInterval parses input and returns the [start, end) positions of -// the first occurrence of substr in input. f==nil indicates failure; -// an error has already been reported in that case. -// -func findInterval(t *testing.T, fset *token.FileSet, input, substr string) (f *ast.File, start, end token.Pos) { - f, err := parser.ParseFile(fset, "", input, parser.DeclarationErrors) - if err != nil { - t.Errorf("parse error: %s", err) - return - } - - i := strings.Index(input, substr) - if i < 0 { - t.Errorf("%q is not a substring of input", substr) - f = nil - return - } - - filePos := fset.File(f.Package) - return f, filePos.Pos(i), filePos.Pos(i + len(substr)) -} - -// Common input for following tests. -const input = ` -// Hello. -package main -import "fmt" -func f() {} -func main() { - z := (x + y) // add them - f() // NB: ExprStmt and its CallExpr have same Pos/End -} -` - -func TestPathEnclosingInterval_Exact(t *testing.T) { - // For the exact tests, we check that a substring is mapped to - // the canonical string for the node it denotes. - tests := []struct { - substr string // first occurrence of this string indicates interval - node string // complete text of expected containing node - }{ - {"package", - input[11 : len(input)-1]}, - {"\npack", - input[11 : len(input)-1]}, - {"main", - "main"}, - {"import", - "import \"fmt\""}, - {"\"fmt\"", - "\"fmt\""}, - {"\nfunc f() {}\n", - "func f() {}"}, - {"x ", - "x"}, - {" y", - "y"}, - {"z", - "z"}, - {" + ", - "x + y"}, - {" :=", - "z := (x + y)"}, - {"x + y", - "x + y"}, - {"(x + y)", - "(x + y)"}, - {" (x + y) ", - "(x + y)"}, - {" (x + y) // add", - "(x + y)"}, - {"func", - "func f() {}"}, - {"func f() {}", - "func f() {}"}, - {"\nfun", - "func f() {}"}, - {" f", - "f"}, - } - for _, test := range tests { - f, start, end := findInterval(t, new(token.FileSet), input, test.substr) - if f == nil { - continue - } - - path, exact := ssa.PathEnclosingInterval(f, start, end) - if !exact { - t.Errorf("PathEnclosingInterval(%q) not exact", test.substr) - continue - } - - if len(path) == 0 { - if test.node != "" { - t.Errorf("PathEnclosingInterval(%q).path: got [], want %q", - test.substr, test.node) - } - continue - } - - if got := input[path[0].Pos():path[0].End()]; got != test.node { - t.Errorf("PathEnclosingInterval(%q): got %q, want %q (path was %s)", - test.substr, got, test.node, pathToString(path)) - continue - } - } -} - -func TestPathEnclosingInterval_Paths(t *testing.T) { - // For these tests, we check only the path of the enclosing - // node, but not its complete text because it's often quite - // large when !exact. - tests := []struct { - substr string // first occurrence of this string indicates interval - path string // the pathToString(),exact of the expected path - }{ - {"// add", - "[BlockStmt FuncDecl File],false"}, - {"(x + y", - "[ParenExpr AssignStmt BlockStmt FuncDecl File],false"}, - {"x +", - "[BinaryExpr ParenExpr AssignStmt BlockStmt FuncDecl File],false"}, - {"z := (x", - "[AssignStmt BlockStmt FuncDecl File],false"}, - {"func f", - "[FuncDecl File],false"}, - {"func f()", - "[FuncDecl File],false"}, - {" f()", - "[FuncDecl File],false"}, - {"() {}", - "[FuncDecl File],false"}, - {"// Hello", - "[File],false"}, - {" f", - "[Ident FuncDecl File],true"}, - {"func ", - "[FuncDecl File],true"}, - {"mai", - "[Ident File],true"}, - {"f() // NB", - "[CallExpr ExprStmt BlockStmt FuncDecl File],true"}, - } - for _, test := range tests { - f, start, end := findInterval(t, new(token.FileSet), input, test.substr) - if f == nil { - continue - } - - path, exact := ssa.PathEnclosingInterval(f, start, end) - if got := fmt.Sprintf("%s,%v", pathToString(path), exact); got != test.path { - t.Errorf("PathEnclosingInterval(%q): got %q, want %q", - test.substr, got, test.path) - continue - } - } -} - -// -------- Tests of source.go ----------------------------------------- - -func TestEnclosingFunction(t *testing.T) { - tests := []struct { - input string // the input file - substr string // first occurrence of this string denotes interval - fn string // name of expected containing function - }{ - // We use distinctive numbers as syntactic landmarks. - - // Ordinary function: - {`package main - func f() { println(1003) }`, - "100", "main.f"}, - // Methods: - {`package main - type T int - func (t T) f() { println(200) }`, - "200", "(main.T).f"}, - // Function literal: - {`package main - func f() { println(func() { print(300) }) }`, - "300", "func@2.24"}, - // Doubly nested - {`package main - func f() { println(func() { print(func() { print(350) })})}`, - "350", "func@2.39"}, - // Implicit init for package-level var initializer. - {"package main; var a = 400", "400", "main.init"}, - // No code for constants: - {"package main; const a = 500", "500", "(none)"}, - // Explicit init() - {"package main; func init() { println(600) }", "600", "main.init"}, - // Multiple explicit init functions: - {`package main - func init() { println("foo") } - func init() { println(800) }`, - "800", "main.init"}, - // init() containing FuncLit. - {`package main - func init() { println(func(){print(900)}) }`, - "900", "func@2.27"}, - } - for _, test := range tests { - imp := importer.New(new(importer.Context)) // (NB: no Loader) - f, start, end := findInterval(t, imp.Fset, test.input, test.substr) - if f == nil { - continue - } - path, exact := ssa.PathEnclosingInterval(f, start, end) - if !exact { - t.Errorf("EnclosingFunction(%q) not exact", test.substr) - continue - } - info, err := imp.CreateSourcePackage("main", []*ast.File{f}) - if err != nil { - t.Error(err.Error()) - continue - } - - prog := ssa.NewProgram(imp.Fset, 0) - prog.CreatePackages(imp) - pkg := prog.Package(info.Pkg) - pkg.Build() - - name := "(none)" - fn := ssa.EnclosingFunction(pkg, path) - if fn != nil { - name = fn.String() - } - - if name != test.fn { - t.Errorf("EnclosingFunction(%q in %q) got %s, want %s", - test.substr, test.input, name, test.fn) - continue - } - - // While we're here: test HasEnclosingFunction. - if has := ssa.HasEnclosingFunction(pkg, path); has != (fn != nil) { - t.Errorf("HasEnclosingFunction(%q in %q) got %v, want %v", - test.substr, test.input, has, fn != nil) - continue - } - } -} - func TestObjValueLookup(t *testing.T) { imp := importer.New(new(importer.Context)) // (uses GCImporter) f, err := parser.ParseFile(imp.Fset, "testdata/objlookup.go", nil, parser.DeclarationErrors|parser.ParseComments) @@ -348,7 +82,7 @@ func TestObjValueLookup(t *testing.T) { // The result varies based on the specific Ident. for _, id := range ids { if obj, ok := info.ObjectOf(id).(*types.Var); ok { - ref, _ := ssa.PathEnclosingInterval(f, id.Pos(), id.Pos()) + ref, _ := importer.PathEnclosingInterval(f, id.Pos(), id.Pos()) pos := imp.Fset.Position(id.Pos()) exp := expectations[fmt.Sprintf("%s:%d", id.Name, pos.Line)] if exp == "" {