From 19966e9b533a85039458eabf08228e0b85f3bf4e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 25 Oct 2018 16:44:44 -0400 Subject: [PATCH] go/types: add CheckExpr function to type-check an expression In IDE-like applications (and also in tests), there is often a need to type-check an expression as if it appeared at a particular position in the source code of an already typed-checked package. Eval was added to address this need, but did so only partially, stopping short of exposing a type-annotated expression tree. This makes it impossible to resolve an expression such as new(T).x and discover what Object x refers to. CheckExpr exposes that generality. Eval is now implemented in terms of CheckExpr. This change includes a test that demonstrates the object resolution functionality just described. Historical context: - https://go-review.googlesource.com/c/tools/+/10800 - https://codereview.appspot.com/10748044/ Change-Id: I715ba934b9fc0c9ceb61270e20c5f91f4eff20c3 Reviewed-on: https://go-review.googlesource.com/c/go/+/144677 Run-TryBot: Alan Donovan TryBot-Result: Gobot Gobot Reviewed-by: Robert Griesemer --- src/go/types/eval.go | 53 +++++++++++++-------- src/go/types/eval_test.go | 96 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 18 deletions(-) diff --git a/src/go/types/eval.go b/src/go/types/eval.go index 8d4db48a9f..51259604c9 100644 --- a/src/go/types/eval.go +++ b/src/go/types/eval.go @@ -6,6 +6,7 @@ package types import ( "fmt" + "go/ast" "go/parser" "go/token" ) @@ -16,22 +17,43 @@ import ( // complete position information relative to the provided file // set. // +// The meaning of the parameters fset, pkg, and pos is the +// same as in CheckExpr. An error is returned if expr cannot +// be parsed successfully, or the resulting expr AST cannot be +// type-checked. +func Eval(fset *token.FileSet, pkg *Package, pos token.Pos, expr string) (_ TypeAndValue, err error) { + // parse expressions + node, err := parser.ParseExprFrom(fset, "eval", expr, 0) + if err != nil { + return TypeAndValue{}, err + } + + info := &Info{ + Types: make(map[ast.Expr]TypeAndValue), + } + err = CheckExpr(fset, pkg, pos, node, info) + return info.Types[node], err +} + +// CheckExpr type checks the expression expr as if it had appeared at +// position pos of package pkg. Type information about the expression +// is recorded in info. +// // If pkg == nil, the Universe scope is used and the provided // position pos is ignored. If pkg != nil, and pos is invalid, // the package scope is used. Otherwise, pos must belong to the // package. // // An error is returned if pos is not within the package or -// if the node cannot be evaluated. +// if the node cannot be type-checked. // -// Note: Eval should not be used instead of running Check to compute -// types and values, but in addition to Check. Eval will re-evaluate -// its argument each time, and it also does not know about the context -// in which an expression is used (e.g., an assignment). Thus, top- -// level untyped constants will return an untyped type rather then the -// respective context-specific type. +// Note: Eval and CheckExpr should not be used instead of running Check +// to compute types and values, but in addition to Check, as these +// functions ignore the context in which an expression is used (e.g., an +// assignment). Thus, top-level untyped constants will return an +// untyped type rather then the respective context-specific type. // -func Eval(fset *token.FileSet, pkg *Package, pos token.Pos, expr string) (_ TypeAndValue, err error) { +func CheckExpr(fset *token.FileSet, pkg *Package, pos token.Pos, expr ast.Expr, info *Info) (err error) { // determine scope var scope *Scope if pkg == nil { @@ -56,27 +78,22 @@ func Eval(fset *token.FileSet, pkg *Package, pos token.Pos, expr string) (_ Type } // s == nil || s == pkg.scope if s == nil { - return TypeAndValue{}, fmt.Errorf("no position %s found in package %s", fset.Position(pos), pkg.name) + return fmt.Errorf("no position %s found in package %s", fset.Position(pos), pkg.name) } } } - // parse expressions - node, err := parser.ParseExprFrom(fset, "eval", expr, 0) - if err != nil { - return TypeAndValue{}, err - } - // initialize checker - check := NewChecker(nil, fset, pkg, nil) + check := NewChecker(nil, fset, pkg, info) check.scope = scope check.pos = pos defer check.handleBailout(&err) // evaluate node var x operand - check.rawExpr(&x, node, nil) + check.rawExpr(&x, expr, nil) check.processDelayed(0) // incl. all functions + check.recordUntyped() - return TypeAndValue{x.mode, x.typ, x.val}, nil + return nil } diff --git a/src/go/types/eval_test.go b/src/go/types/eval_test.go index d3b3fec66f..d940bf0e80 100644 --- a/src/go/types/eval_test.go +++ b/src/go/types/eval_test.go @@ -7,6 +7,7 @@ package types_test import ( + "fmt" "go/ast" "go/importer" "go/parser" @@ -199,3 +200,98 @@ func split(s, sep string) (string, string) { i := strings.Index(s, sep) return strings.TrimSpace(s[:i]), strings.TrimSpace(s[i+len(sep):]) } + +func TestCheckExpr(t *testing.T) { + testenv.MustHaveGoBuild(t) + + // Each comment has the form /* expr => object */: + // expr is an identifier or selector expression that is passed + // to CheckExpr at the position of the comment, and object is + // the string form of the object it denotes. + const src = ` +package p + +import "fmt" + +const c = 3.0 +type T []int +type S struct{ X int } + +func f(a int, s string) S { + /* fmt.Println => func fmt.Println(a ...interface{}) (n int, err error) */ + /* fmt.Stringer.String => func (fmt.Stringer).String() string */ + fmt.Println("calling f") + + var fmt struct{ Println int } + /* fmt => var fmt struct{Println int} */ + /* fmt.Println => field Println int */ + /* f(1, "").X => field X int */ + fmt.Println = 1 + + /* append => builtin append */ + + /* new(S).X => field X int */ + + return S{} +}` + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + + conf := Config{Importer: importer.Default()} + pkg, err := conf.Check("p", fset, []*ast.File{f}, nil) + if err != nil { + t.Fatal(err) + } + + checkExpr := func(pos token.Pos, str string) (Object, error) { + expr, err := parser.ParseExprFrom(fset, "eval", str, 0) + if err != nil { + return nil, err + } + + info := &Info{ + Uses: make(map[*ast.Ident]Object), + Selections: make(map[*ast.SelectorExpr]*Selection), + } + if err := CheckExpr(fset, pkg, pos, expr, info); err != nil { + return nil, fmt.Errorf("CheckExpr(%q) failed: %s", str, err) + } + switch expr := expr.(type) { + case *ast.Ident: + if obj, ok := info.Uses[expr]; ok { + return obj, nil + } + case *ast.SelectorExpr: + if sel, ok := info.Selections[expr]; ok { + return sel.Obj(), nil + } + if obj, ok := info.Uses[expr.Sel]; ok { + return obj, nil // qualified identifier + } + } + return nil, fmt.Errorf("no object for %s", str) + } + + for _, group := range f.Comments { + for _, comment := range group.List { + s := comment.Text + if len(s) >= 4 && strings.HasPrefix(s, "/*") && strings.HasSuffix(s, "*/") { + pos := comment.Pos() + expr, wantObj := split(s[2:len(s)-2], "=>") + obj, err := checkExpr(pos, expr) + if err != nil { + t.Errorf("%s: %s", fset.Position(pos), err) + continue + } + if obj.String() != wantObj { + t.Errorf("%s: checkExpr(%s) = %s, want %v", + fset.Position(pos), expr, obj, wantObj) + } + } + } + } +} -- 2.50.0