Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Also: Update go-cmp.
- Loading branch information
M. J. Fromberger
committed
Oct 28, 2021
1 parent
491306b
commit efbebaf
Showing
3 changed files
with
230 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
// Package bicall locates calls of built-in functions in Go source. | ||
package bicall | ||
|
||
import ( | ||
"fmt" | ||
"go/ast" | ||
"go/parser" | ||
"go/token" | ||
"io" | ||
"strings" | ||
) | ||
|
||
// See https://golang.org/ref/spec#Built-in_functions | ||
var isBuiltin = map[string]bool{ | ||
"append": true, | ||
"cap": true, | ||
"close": true, | ||
"complex": true, | ||
"copy": true, | ||
"delete": true, | ||
"imag": true, | ||
"len": true, | ||
"make": true, | ||
"new": true, | ||
"panic": true, | ||
"print": true, | ||
"println": true, | ||
"real": true, | ||
"recover": true, | ||
} | ||
|
||
// Call represents a call to a built-in function in a source program. | ||
type Call struct { | ||
Name string // the name of the built-in function | ||
Call *ast.CallExpr // the call expression in the AST | ||
Site token.Position // the location of the call | ||
Path []ast.Node // the AST path to the call | ||
Comments []string // comments attributed to the call site by the parser | ||
} | ||
|
||
// flattenComments extracts the text of the given comment groups, and removes | ||
// leading and trailing whitespace from them. | ||
func flattenComments(cgs []*ast.CommentGroup) (out []string) { | ||
for _, cg := range cgs { | ||
out = append(out, strings.TrimSuffix(cg.Text(), "\n")) | ||
} | ||
return | ||
} | ||
|
||
// Parse parses the contents of r as a Go source file, and calls f for each | ||
// call expression targeting a built-in function that occurs in the resulting | ||
// AST. Location information about each call is attributed to the specified | ||
// filename. | ||
// | ||
// If f reports an error, traversal stops and that error is reported to the | ||
// caller of Parse. | ||
func Parse(r io.Reader, filename string, f func(Call) error) error { | ||
fset := token.NewFileSet() | ||
file, err := parser.ParseFile(fset, filename, r, parser.ParseComments) | ||
if err != nil { | ||
return fmt.Errorf("parsing %q: %w", filename, err) | ||
} | ||
cmap := ast.NewCommentMap(fset, file, file.Comments) | ||
|
||
var path []ast.Node | ||
|
||
// Find the comments associated with node. If the node does not have its own | ||
// comments, scan upward for a statement containing the node. | ||
commentsFor := func(node ast.Node) []string { | ||
if cgs := cmap[node]; cgs != nil { | ||
return flattenComments(cgs) | ||
} | ||
for i := len(path) - 2; i >= 0; i-- { | ||
if _, ok := path[i].(ast.Stmt); !ok { | ||
continue | ||
} | ||
if cgs := cmap[path[i]]; cgs != nil { | ||
return flattenComments(cgs) | ||
} else { | ||
break | ||
} | ||
} | ||
return nil | ||
} | ||
v := &visitor{ | ||
visit: func(node ast.Node) error { | ||
if node == nil { | ||
path = path[:len(path)-1] | ||
return nil | ||
} | ||
path = append(path, node) | ||
if call, ok := node.(*ast.CallExpr); ok { | ||
id, ok := call.Fun.(*ast.Ident) | ||
if !ok || !isBuiltin[id.Name] { | ||
return nil | ||
} | ||
|
||
if err := f(Call{ | ||
Name: id.Name, | ||
Call: call, | ||
Site: fset.Position(call.Pos()), | ||
Path: path, | ||
Comments: commentsFor(node), | ||
}); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
}, | ||
} | ||
ast.Walk(v, file) | ||
return v.err | ||
} | ||
|
||
type visitor struct { | ||
err error | ||
visit func(ast.Node) error | ||
} | ||
|
||
func (v *visitor) Visit(node ast.Node) ast.Visitor { | ||
if v.err == nil { | ||
v.err = v.visit(node) | ||
} | ||
if v.err != nil { | ||
return nil | ||
} | ||
return v | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package bicall_test | ||
|
||
import ( | ||
"log" | ||
"sort" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/tendermint/tendermint/tools/panic/bicall" | ||
) | ||
|
||
func TestParse(t *testing.T) { | ||
want := mustFindNeedles(testInput) | ||
sortByLocation(want) | ||
|
||
var got []needle | ||
err := bicall.Parse(strings.NewReader(testInput), "testinput.go", func(c bicall.Call) error { | ||
got = append(got, needle{ | ||
Name: c.Name, | ||
Line: c.Site.Line, | ||
Col: c.Site.Column - 1, | ||
}) | ||
t.Logf("Found call site for %q at %v", c.Name, c.Site) | ||
|
||
// Verify that the indicator comment shows up attributed to the site. | ||
tag := "@" + c.Name | ||
if len(c.Comments) != 1 || c.Comments[0] != tag { | ||
t.Errorf("Wrong comment at %v: got %+q, want [%q]", c.Site, c.Comments, tag) | ||
} | ||
return nil | ||
}) | ||
if err != nil { | ||
t.Fatalf("Parse unexpectedly failed: %v", err) | ||
} | ||
sortByLocation(got) | ||
|
||
if diff := cmp.Diff(want, got); diff != "" { | ||
t.Errorf("Call site mismatch: (-want, +got)\n%s", diff) | ||
} | ||
} | ||
|
||
// sortByLocation permutes ns in-place to be ordered by line and column. | ||
// The specific ordering rule is not important; we just need a consistent order | ||
// for comparison of test results. | ||
func sortByLocation(ns []needle) { | ||
sort.Slice(ns, func(i, j int) bool { | ||
if ns[i].Line == ns[j].Line { | ||
return ns[i].Col < ns[j].Col | ||
} | ||
return ns[i].Line < ns[j].Line | ||
}) | ||
} | ||
|
||
// To add call sites to the test, include a trailing line comment having the | ||
// form "//@name", where "name" is a built-in function name. | ||
// The first offset of that name on the line prior to the comment will become | ||
// an expected call site for that function. | ||
const testInput = ` | ||
package testinput | ||
func main() { | ||
defer func() { | ||
x := recover() //@recover | ||
if x != nil { | ||
println("whoa") //@println | ||
} | ||
}() | ||
ip := new(int) //@new | ||
*ip = 3 + copy([]byte{}, "") //@copy | ||
panic(fmt.Sprintf("ip=%p", ip)) //@panic | ||
} | ||
` | ||
|
||
// A needle is a name at a location in the source, that is expected to be | ||
// located in a scan of the input for built-in calls. | ||
// N.B. Fields are exported to allow comparison by the cmp package. | ||
type needle struct { | ||
Name string | ||
Line, Col int | ||
} | ||
|
||
func mustFindNeedles(src string) []needle { | ||
var needles []needle | ||
for i, raw := range strings.Split(src, "\n") { | ||
tag := strings.SplitN(raw, "//@", 2) | ||
if len(tag) == 1 { | ||
continue // no needle on this line | ||
} | ||
name := strings.TrimSpace(tag[1]) | ||
col := strings.Index(tag[0], name) | ||
if col < 0 { | ||
log.Panicf("No match for %q on line %d of test input", name, i+1) | ||
} | ||
needles = append(needles, needle{ | ||
Name: name, | ||
Line: i + 1, | ||
Col: col, | ||
}) | ||
} | ||
return needles | ||
} |