Commit 19966e9b authored by Alan Donovan's avatar Alan Donovan

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 <adonovan@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarRobert Griesemer <gri@golang.org>
parent 5a2da562
...@@ -6,6 +6,7 @@ package types ...@@ -6,6 +6,7 @@ package types
import ( import (
"fmt" "fmt"
"go/ast"
"go/parser" "go/parser"
"go/token" "go/token"
) )
...@@ -16,22 +17,43 @@ import ( ...@@ -16,22 +17,43 @@ import (
// complete position information relative to the provided file // complete position information relative to the provided file
// set. // 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 // If pkg == nil, the Universe scope is used and the provided
// position pos is ignored. If pkg != nil, and pos is invalid, // position pos is ignored. If pkg != nil, and pos is invalid,
// the package scope is used. Otherwise, pos must belong to the // the package scope is used. Otherwise, pos must belong to the
// package. // package.
// //
// An error is returned if pos is not within the package or // 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 // Note: Eval and CheckExpr should not be used instead of running Check
// types and values, but in addition to Check. Eval will re-evaluate // to compute types and values, but in addition to Check, as these
// its argument each time, and it also does not know about the context // functions ignore the context in which an expression is used (e.g., an
// in which an expression is used (e.g., an assignment). Thus, top- // assignment). Thus, top-level untyped constants will return an
// level untyped constants will return an untyped type rather then the // untyped type rather then the respective context-specific type.
// 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 // determine scope
var scope *Scope var scope *Scope
if pkg == nil { if pkg == nil {
...@@ -56,27 +78,22 @@ func Eval(fset *token.FileSet, pkg *Package, pos token.Pos, expr string) (_ Type ...@@ -56,27 +78,22 @@ func Eval(fset *token.FileSet, pkg *Package, pos token.Pos, expr string) (_ Type
} }
// s == nil || s == pkg.scope // s == nil || s == pkg.scope
if s == nil { 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 // initialize checker
check := NewChecker(nil, fset, pkg, nil) check := NewChecker(nil, fset, pkg, info)
check.scope = scope check.scope = scope
check.pos = pos check.pos = pos
defer check.handleBailout(&err) defer check.handleBailout(&err)
// evaluate node // evaluate node
var x operand var x operand
check.rawExpr(&x, node, nil) check.rawExpr(&x, expr, nil)
check.processDelayed(0) // incl. all functions check.processDelayed(0) // incl. all functions
check.recordUntyped()
return TypeAndValue{x.mode, x.typ, x.val}, nil return nil
} }
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
package types_test package types_test
import ( import (
"fmt"
"go/ast" "go/ast"
"go/importer" "go/importer"
"go/parser" "go/parser"
...@@ -199,3 +200,98 @@ func split(s, sep string) (string, string) { ...@@ -199,3 +200,98 @@ func split(s, sep string) (string, string) {
i := strings.Index(s, sep) i := strings.Index(s, sep)
return strings.TrimSpace(s[:i]), strings.TrimSpace(s[i+len(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)
}
}
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment