Skip to content

Commit

Permalink
Add package bicall and basic tests.
Browse files Browse the repository at this point in the history
Also: Update go-cmp.
  • Loading branch information
M. J. Fromberger committed Oct 28, 2021
1 parent 491306b commit efbebaf
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -12,6 +12,7 @@ require (
github.com/gogo/protobuf v1.3.2
github.com/golang/protobuf v1.5.2
github.com/golangci/golangci-lint v1.42.1
github.com/google/go-cmp v0.5.6
github.com/google/orderedcode v0.0.1
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.4.2
Expand Down
128 changes: 128 additions & 0 deletions tools/panic/bicall/bicall.go
@@ -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
}
101 changes: 101 additions & 0 deletions tools/panic/bicall/bicall_test.go
@@ -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
}

0 comments on commit efbebaf

Please sign in to comment.