blob: 871b78156bb8b946099d76b12b337b973b7f5b60 [file] [log] [blame]
// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
// This file contains the code to process sources, to be able to deduct the
// original types.
package stack
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"math"
"os"
"strconv"
"strings"
)
// Private stuff.
// cacheAST is a cache of parsed Go sources.
type cacheAST struct {
files map[string][]byte
parsed map[string]*parsedFile
}
// augmentGoroutine processes source files to improve call to be more
// descriptive.
//
// It modifies the routine.
func (c *cacheAST) augmentGoroutine(g *Goroutine) error {
var err error
for i, call := range g.Stack.Calls {
// Only load the AST if there's an argument to process.
if len(call.Args.Values) == 0 {
continue
}
if err1 := c.loadFile(g.Stack.Calls[i].LocalSrcPath); err1 != nil {
//log.Printf("%s", err)
err = err1
}
if p := c.parsed[call.LocalSrcPath]; p != nil {
f, err1 := p.getFuncAST(call.Func.Name, call.Line)
if err1 != nil {
err = err1
continue
}
if f != nil {
augmentCall(&g.Stack.Calls[i], f)
}
}
}
return err
}
// loadFile loads a Go source file and parses the AST tree.
func (c *cacheAST) loadFile(fileName string) error {
if fileName == "" {
return nil
}
if _, ok := c.parsed[fileName]; ok {
return nil
}
// Do not attempt to parse the same file twice.
c.parsed[fileName] = nil
if !strings.HasSuffix(fileName, ".go") {
// Ignore C and assembly.
return fmt.Errorf("cannot load non-go file %q", fileName)
}
/* #nosec G304 */
src, err := os.ReadFile(fileName)
if err != nil {
return err
}
c.files[fileName] = src
fset := token.NewFileSet()
parsed, err := parser.ParseFile(fset, fileName, src, 0)
if err != nil {
return fmt.Errorf("failed to parse %w", err)
}
c.parsed[fileName] = &parsedFile{
lineToByteOffset: lineToByteOffsets(src),
parsed: parsed,
}
return nil
}
// lineToByteOffsets extract the line number into raw file offset.
//
// Inserts a dummy 0 at offset 0 so line offsets can be 1 based.
func lineToByteOffsets(src []byte) []int {
offsets := []int{0, 0}
for offset := 0; offset < len(src); {
n := bytes.IndexByte(src[offset:], '\n')
if n == -1 {
break
}
offset += n + 1
offsets = append(offsets, offset)
}
return offsets
}
// parsedFile is a processed Go source file.
type parsedFile struct {
lineToByteOffset []int
parsed *ast.File
}
// getFuncAST gets the callee site function AST representation for the code
// inside the function f at line l.
func (p *parsedFile) getFuncAST(f string, l int) (d *ast.FuncDecl, err error) {
if len(p.lineToByteOffset) <= l {
// The line number in the stack trace line does not exist in the file. That
// can only mean that the sources on disk do not match the sources used to
// build the binary.
return nil, fmt.Errorf("line %d is over line count of %d", l, len(p.lineToByteOffset)-1)
}
// Walk the AST to find the lineToByteOffset that fits the line number.
var lastFunc *ast.FuncDecl
// Inspect() goes depth first. This means for example that a function like:
// func a() {
// b := func() {}
// c()
// }
//
// Were we are looking at the c() call can return confused values. It is
// important to look at the actual ast.Node hierarchy.
ast.Inspect(p.parsed, func(n ast.Node) bool {
if d != nil {
return false
}
if n == nil {
return true
}
if int(n.Pos()) >= p.lineToByteOffset[l] {
// We are expecting a ast.CallExpr node. It can be harder to figure out
// when there are multiple calls on a single line, as the stack trace
// doesn't have file byte offset information, only line based.
// gofmt will always format to one function call per line but there can
// be edge cases, like:
// a = A{Foo(), Bar()}
d = lastFunc
//p.processNode(call, n)
return false
} else if f, ok := n.(*ast.FuncDecl); ok {
lastFunc = f
}
return true
})
return
}
func name(n ast.Node) string {
switch t := n.(type) {
case *ast.InterfaceType:
return "interface{}"
case *ast.Ident:
return t.Name
case *ast.SelectorExpr:
return t.Sel.Name
case *ast.StarExpr:
return "*" + name(t.X)
case *ast.BasicLit:
return t.Value
case *ast.Ellipsis:
return "..."
default:
return "<unknown>"
}
}
// fieldToType returns the type name and whether if it's an ellipsis.
func fieldToType(f *ast.Field) (string, bool) {
switch arg := f.Type.(type) {
case *ast.ArrayType:
if arg.Len != nil {
// Array.
return "[" + name(arg.Len) + "]" + name(arg.Elt), false
}
// Slice.
return "[]" + name(arg.Elt), false
case *ast.Ellipsis:
return name(arg.Elt), true
case *ast.FuncType:
// Do not print the function signature to not overload the trace.
return "func", false
case *ast.Ident:
return arg.Name, false
case *ast.InterfaceType:
return "interface{}", false
case *ast.SelectorExpr:
return arg.Sel.Name, false
case *ast.StarExpr:
return "*" + name(arg.X), false
case *ast.MapType:
return fmt.Sprintf("map[%s]%s", name(arg.Key), name(arg.Value)), false
case *ast.ChanType:
return fmt.Sprintf("chan %s", name(arg.Value)), false
default:
// TODO(maruel): Implement anything missing.
return "<unknown>", false
}
}
// extractArgumentsType returns the name of the type of each input argument.
func extractArgumentsType(f *ast.FuncDecl) ([]string, bool) {
var fields []*ast.Field
if f.Recv != nil {
if len(f.Recv.List) != 1 {
panic("Expect only one receiver; please fix panicparse's code")
}
// If it is an object receiver (vs a pointer receiver), its address is not
// printed in the stack trace so it needs to be ignored.
if _, ok := f.Recv.List[0].Type.(*ast.StarExpr); ok {
fields = append(fields, f.Recv.List[0])
}
}
var types []string
ellipsis := false
for _, arg := range append(fields, f.Type.Params.List...) {
// Assert that ellipsis is only set on the last item of fields?
var t string
t, ellipsis = fieldToType(arg)
mult := len(arg.Names)
if mult == 0 {
mult = 1
}
for i := 0; i < mult; i++ {
types = append(types, t)
}
}
return types, ellipsis
}
// augmentCall walks the function and populate call accordingly.
func augmentCall(call *Call, f *ast.FuncDecl) {
flatArgs := make([]*Arg, 0, len(call.Args.Values))
call.Args.walk(func(arg *Arg) {
flatArgs = append(flatArgs, arg)
})
pop := func() *Arg {
if len(flatArgs) == 0 {
return nil
}
a := flatArgs[0]
flatArgs = flatArgs[1:]
return a
}
popFmt := func(fmtFn func(v uint64) string) string {
a := pop()
if a == nil {
return "<nil>"
}
if a.IsOffsetTooLarge {
return "_"
}
return fmtFn(a.Value)
}
popName := func() string {
a := pop()
if a == nil {
return "<nil>"
}
if len(a.Name) != 0 {
return a.Name
}
if a.IsOffsetTooLarge {
return "_"
}
return fmt.Sprintf("0x%x", a.Value)
}
types, extra := extractArgumentsType(f)
for i := 0; len(flatArgs) != 0; i++ {
var t string
if i >= len(types) {
if !extra {
// These are unexpected value! Print them as hex.
call.Args.Processed = append(call.Args.Processed, popName())
continue
}
t = types[len(types)-1]
} else {
t = types[i]
}
var str string
switch t {
case "float32":
str = popFmt(func(v uint64) string {
f := float64(math.Float32frombits(uint32(v)))
return strconv.FormatFloat(f, 'g', -1, 32)
})
case "float64":
str = popFmt(func(v uint64) string {
f := math.Float64frombits(v)
return strconv.FormatFloat(f, 'g', -1, 64)
})
case "int":
str = popFmt(func(v uint64) string {
return strconv.FormatInt(int64(int(v)), 10)
})
case "int8":
str = popFmt(func(v uint64) string {
return strconv.FormatInt(int64(int8(v)), 10)
})
case "int16":
str = popFmt(func(v uint64) string {
return strconv.FormatInt(int64(int16(v)), 10)
})
case "int32":
str = popFmt(func(v uint64) string {
return strconv.FormatInt(int64(int32(v)), 10)
})
case "int64":
str = popFmt(func(v uint64) string {
return strconv.FormatInt(int64(v), 10)
})
case "uint", "uint8", "uint16", "uint32", "uint64":
str = popFmt(func(v uint64) string {
return strconv.FormatUint(v, 10)
})
case "bool":
str = popFmt(func(v uint64) string {
if v == 0 {
return "false"
}
return "true"
})
case "string":
name := popName()
lenStr := popFmt(func(v uint64) string {
return strconv.FormatUint(v, 10)
})
str = fmt.Sprintf("%s(%s, len=%s)", t, name, lenStr)
default:
if strings.HasPrefix(t, "*") {
str = fmt.Sprintf("%s(%s)", t, popName())
} else if strings.HasPrefix(t, "[]") {
name := popName()
lenStr := popFmt(func(v uint64) string {
return strconv.FormatUint(v, 10)
})
capStr := popFmt(func(v uint64) string {
return strconv.FormatUint(v, 10)
})
str = fmt.Sprintf("%s(%s len=%s cap=%s)", t, name, lenStr, capStr)
} else {
if i < len(call.Args.Values) && call.Args.Values[i].IsAggregate {
// If top-level argument is an aggregate-type, include each
// of its sub-arguments.
v := &call.Args.Values[i].Fields
var fields []string
v.walk(func(arg *Arg) {
fields = append(fields, popName())
})
if v.Elided {
fields = append(fields, "...")
}
str = fmt.Sprintf("%s{%s}", t, strings.Join(fields, ", "))
} else {
// Assumes it's an interface. For now, discard the object
// value, which is probably not a good idea.
str = fmt.Sprintf("%s(%s)", t, popName())
pop()
}
}
}
call.Args.Processed = append(call.Args.Processed, str)
}
}