delve/service/api/prettyprint.go
Stefan Haller aec354cd0b
Shorten variable types (#3535)
* Add ShortenType function

Taken from

  https://github.com/aarzilli/gdlv/blob/master/internal/prettyprint/short.go

with kind permission by @aarzilli.

* Shorten type names in variable values

The variables view in VS Code is a lot easier to read if long type names are
shortened in much the same way as we shorten them for functions in the call
stack view.

We only shorten them in the value strings; the Type field of dap.Variable is
kept as is. Since this only appears in a tooltip, it isn't a problem to have the
full type visible there.
2023-11-09 10:15:25 +01:00

627 lines
16 KiB
Go

package api
import (
"bytes"
"fmt"
"io"
"math"
"reflect"
"strconv"
"strings"
"text/tabwriter"
)
const (
// strings longer than this will cause slices, arrays and structs to be printed on multiple lines when newlines is enabled
maxShortStringLen = 7
// string used for one indentation level (when printing on multiple lines)
indentString = "\t"
)
type prettyFlags uint8
const (
prettyTop prettyFlags = 1 << iota
prettyNewlines
prettyIncludeType
prettyShortenType
)
func (flags prettyFlags) top() bool { return flags&prettyTop != 0 }
func (flags prettyFlags) includeType() bool { return flags&prettyIncludeType != 0 }
func (flags prettyFlags) newlines() bool { return flags&prettyNewlines != 0 }
func (flags prettyFlags) shortenType() bool { return flags&prettyShortenType != 0 }
func (flags prettyFlags) set(flag prettyFlags, v bool) prettyFlags {
if v {
return flags | flag
} else {
return flags &^ flag
}
}
// SinglelineString returns a representation of v on a single line.
func (v *Variable) SinglelineString() string {
var buf bytes.Buffer
v.writeTo(&buf, prettyTop|prettyIncludeType, "", "")
return buf.String()
}
// SinglelineStringWithShortTypes returns a representation of v on a single line, with types shortened.
func (v *Variable) SinglelineStringWithShortTypes() string {
var buf bytes.Buffer
v.writeTo(&buf, prettyTop|prettyIncludeType|prettyShortenType, "", "")
return buf.String()
}
// SinglelineStringFormatted returns a representation of v on a single line, using the format specified by fmtstr.
func (v *Variable) SinglelineStringFormatted(fmtstr string) string {
var buf bytes.Buffer
v.writeTo(&buf, prettyTop|prettyIncludeType, "", fmtstr)
return buf.String()
}
// MultilineString returns a representation of v on multiple lines.
func (v *Variable) MultilineString(indent, fmtstr string) string {
var buf bytes.Buffer
v.writeTo(&buf, prettyTop|prettyNewlines|prettyIncludeType, indent, fmtstr)
return buf.String()
}
func (v *Variable) typeStr(flags prettyFlags) string {
if flags.shortenType() {
return ShortenType(v.Type)
}
return v.Type
}
func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
if v.Unreadable != "" {
fmt.Fprintf(buf, "(unreadable %s)", v.Unreadable)
return
}
if !flags.top() && v.Addr == 0 && v.Value == "" {
if flags.includeType() && v.Type != "void" {
fmt.Fprintf(buf, "%s nil", v.typeStr(flags))
} else {
fmt.Fprint(buf, "nil")
}
return
}
switch v.Kind {
case reflect.Slice:
v.writeSliceTo(buf, flags, indent, fmtstr)
case reflect.Array:
v.writeArrayTo(buf, flags, indent, fmtstr)
case reflect.Ptr:
if v.Type == "" || len(v.Children) == 0 {
fmt.Fprint(buf, "nil")
} else if v.Children[0].OnlyAddr && v.Children[0].Addr != 0 {
v.writePointerTo(buf, flags)
} else {
if flags.top() && flags.newlines() && v.Children[0].Addr != 0 {
v.writePointerTo(buf, flags)
fmt.Fprint(buf, "\n")
}
fmt.Fprint(buf, "*")
v.Children[0].writeTo(buf, flags.set(prettyTop, false), indent, fmtstr)
}
case reflect.UnsafePointer:
if len(v.Children) == 0 {
fmt.Fprintf(buf, "unsafe.Pointer(nil)")
} else {
fmt.Fprintf(buf, "unsafe.Pointer(%#x)", v.Children[0].Addr)
}
case reflect.Chan:
if flags.newlines() {
v.writeStructTo(buf, flags, indent, fmtstr)
} else {
if len(v.Children) == 0 {
fmt.Fprintf(buf, "%s nil", v.typeStr(flags))
} else {
fmt.Fprintf(buf, "%s %s/%s", v.typeStr(flags), v.Children[0].Value, v.Children[1].Value)
}
}
case reflect.Struct:
if v.Value != "" {
fmt.Fprintf(buf, "%s(%s)", v.typeStr(flags), v.Value)
flags = flags.set(prettyIncludeType, false)
}
v.writeStructTo(buf, flags, indent, fmtstr)
case reflect.Interface:
if v.Addr == 0 {
// an escaped interface variable that points to nil, this shouldn't
// happen in normal code but can happen if the variable is out of scope.
fmt.Fprintf(buf, "nil")
return
}
if flags.includeType() {
if v.Children[0].Kind == reflect.Invalid {
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
if v.Children[0].Addr == 0 {
fmt.Fprint(buf, "nil")
return
}
} else {
fmt.Fprintf(buf, "%s(%s) ", v.typeStr(flags), v.Children[0].Type)
}
}
data := v.Children[0]
if data.Kind == reflect.Ptr {
if len(data.Children) == 0 {
fmt.Fprint(buf, "...")
} else if data.Children[0].Addr == 0 {
fmt.Fprint(buf, "nil")
} else if data.Children[0].OnlyAddr {
fmt.Fprintf(buf, "0x%x", v.Children[0].Addr)
} else {
v.Children[0].writeTo(buf, flags.set(prettyTop, false).set(prettyIncludeType, !flags.includeType()), indent, fmtstr)
}
} else if data.OnlyAddr {
if strings.Contains(v.Type, "/") {
fmt.Fprintf(buf, "*(*%q)(%#x)", v.typeStr(flags), v.Addr)
} else {
fmt.Fprintf(buf, "*(*%s)(%#x)", v.typeStr(flags), v.Addr)
}
} else {
v.Children[0].writeTo(buf, flags.set(prettyTop, false).set(prettyIncludeType, !flags.includeType()), indent, fmtstr)
}
case reflect.Map:
v.writeMapTo(buf, flags, indent, fmtstr)
case reflect.Func:
if v.Value == "" {
fmt.Fprint(buf, "nil")
} else {
fmt.Fprintf(buf, "%s", v.Value)
}
default:
v.writeBasicType(buf, fmtstr)
}
}
func (v *Variable) writePointerTo(buf io.Writer, flags prettyFlags) {
if strings.Contains(v.Type, "/") {
fmt.Fprintf(buf, "(%q)(%#x)", v.typeStr(flags), v.Children[0].Addr)
} else {
fmt.Fprintf(buf, "(%s)(%#x)", v.typeStr(flags), v.Children[0].Addr)
}
}
func (v *Variable) writeBasicType(buf io.Writer, fmtstr string) {
if v.Value == "" && v.Kind != reflect.String {
fmt.Fprintf(buf, "(unknown %s)", v.Kind)
return
}
switch v.Kind {
case reflect.Bool:
if fmtstr == "" {
buf.Write([]byte(v.Value))
return
}
var b bool = v.Value == "true"
fmt.Fprintf(buf, fmtstr, b)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if fmtstr == "" {
buf.Write([]byte(v.Value))
return
}
n, _ := strconv.ParseInt(ExtractIntValue(v.Value), 10, 64)
fmt.Fprintf(buf, fmtstr, n)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
if fmtstr == "" {
buf.Write([]byte(v.Value))
return
}
n, _ := strconv.ParseUint(ExtractIntValue(v.Value), 10, 64)
fmt.Fprintf(buf, fmtstr, n)
case reflect.Float32, reflect.Float64:
if fmtstr == "" {
buf.Write([]byte(v.Value))
return
}
x, _ := strconv.ParseFloat(v.Value, 64)
fmt.Fprintf(buf, fmtstr, x)
case reflect.Complex64, reflect.Complex128:
if fmtstr == "" {
fmt.Fprintf(buf, "(%s + %si)", v.Children[0].Value, v.Children[1].Value)
return
}
real, _ := strconv.ParseFloat(v.Children[0].Value, 64)
imag, _ := strconv.ParseFloat(v.Children[1].Value, 64)
var x complex128 = complex(real, imag)
fmt.Fprintf(buf, fmtstr, x)
case reflect.String:
if fmtstr == "" {
s := v.Value
if len(s) != int(v.Len) {
s = fmt.Sprintf("%s...+%d more", s, int(v.Len)-len(s))
}
fmt.Fprintf(buf, "%q", s)
return
}
fmt.Fprintf(buf, fmtstr, v.Value)
}
}
func ExtractIntValue(s string) string {
if s == "" || s[len(s)-1] != ')' {
return s
}
open := strings.LastIndex(s, "(")
if open < 0 {
return s
}
return s[open+1 : len(s)-1]
}
func (v *Variable) writeSliceTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
if flags.includeType() {
fmt.Fprintf(buf, "%s len: %d, cap: %d, ", v.typeStr(flags), v.Len, v.Cap)
}
if v.Base == 0 && len(v.Children) == 0 {
fmt.Fprintf(buf, "nil")
return
}
v.writeSliceOrArrayTo(buf, flags, indent, fmtstr)
}
func (v *Variable) writeArrayTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
if flags.includeType() {
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
}
v.writeSliceOrArrayTo(buf, flags, indent, fmtstr)
}
func (v *Variable) writeStructTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
if int(v.Len) != len(v.Children) && len(v.Children) == 0 {
if strings.Contains(v.Type, "/") {
fmt.Fprintf(buf, "(*%q)(%#x)", v.typeStr(flags), v.Addr)
} else {
fmt.Fprintf(buf, "(*%s)(%#x)", v.typeStr(flags), v.Addr)
}
return
}
if flags.includeType() {
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
}
nl := v.shouldNewlineStruct(flags.newlines())
fmt.Fprint(buf, "{")
for i := range v.Children {
if nl {
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
}
fmt.Fprintf(buf, "%s: ", v.Children[i].Name)
v.Children[i].writeTo(buf, prettyIncludeType.set(prettyNewlines, nl), indent+indentString, fmtstr)
if i != len(v.Children)-1 || nl {
fmt.Fprint(buf, ",")
if !nl {
fmt.Fprint(buf, " ")
}
}
}
if len(v.Children) != int(v.Len) {
if nl {
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
} else {
fmt.Fprint(buf, ",")
}
fmt.Fprintf(buf, "...+%d more", int(v.Len)-len(v.Children))
}
fmt.Fprint(buf, "}")
}
func (v *Variable) writeMapTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
if flags.includeType() {
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
}
if v.Base == 0 && len(v.Children) == 0 {
fmt.Fprintf(buf, "nil")
return
}
nl := flags.newlines() && (len(v.Children) > 0)
fmt.Fprint(buf, "[")
for i := 0; i < len(v.Children); i += 2 {
key := &v.Children[i]
value := &v.Children[i+1]
if nl {
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
}
key.writeTo(buf, 0, indent+indentString, fmtstr)
fmt.Fprint(buf, ": ")
value.writeTo(buf, prettyFlags(0).set(prettyNewlines, nl), indent+indentString, fmtstr)
if i != len(v.Children)-1 || nl {
fmt.Fprint(buf, ", ")
}
}
if len(v.Children)/2 != int(v.Len) {
if len(v.Children) != 0 {
if nl {
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
} else {
fmt.Fprint(buf, ",")
}
fmt.Fprintf(buf, "...+%d more", int(v.Len)-(len(v.Children)/2))
} else {
fmt.Fprint(buf, "...")
}
}
if nl {
fmt.Fprintf(buf, "\n%s", indent)
}
fmt.Fprint(buf, "]")
}
func (v *Variable) shouldNewlineArray(newlines bool) bool {
if !newlines || len(v.Children) == 0 {
return false
}
kind, hasptr := (&v.Children[0]).recursiveKind()
switch kind {
case reflect.Slice, reflect.Array, reflect.Struct, reflect.Map, reflect.Interface:
return true
case reflect.String:
if hasptr {
return true
}
for i := range v.Children {
if len(v.Children[i].Value) > maxShortStringLen {
return true
}
}
return false
default:
return false
}
}
func (v *Variable) recursiveKind() (reflect.Kind, bool) {
hasptr := false
var kind reflect.Kind
for {
kind = v.Kind
if kind == reflect.Ptr {
hasptr = true
if len(v.Children) == 0 {
return kind, hasptr
}
v = &(v.Children[0])
} else {
break
}
}
return kind, hasptr
}
func (v *Variable) shouldNewlineStruct(newlines bool) bool {
if !newlines || len(v.Children) == 0 {
return false
}
for i := range v.Children {
kind, hasptr := (&v.Children[i]).recursiveKind()
switch kind {
case reflect.Slice, reflect.Array, reflect.Struct, reflect.Map, reflect.Interface:
return true
case reflect.String:
if hasptr {
return true
}
if len(v.Children[i].Value) > maxShortStringLen {
return true
}
}
}
return false
}
func (v *Variable) writeSliceOrArrayTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
nl := v.shouldNewlineArray(flags.newlines())
fmt.Fprint(buf, "[")
for i := range v.Children {
if nl {
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
}
v.Children[i].writeTo(buf, prettyFlags(0).set(prettyNewlines, nl), indent+indentString, fmtstr)
if i != len(v.Children)-1 || nl {
fmt.Fprint(buf, ",")
}
}
if len(v.Children) != int(v.Len) {
if len(v.Children) != 0 {
if nl {
fmt.Fprintf(buf, "\n%s%s", indent, indentString)
} else {
fmt.Fprint(buf, ",")
}
fmt.Fprintf(buf, "...+%d more", int(v.Len)-len(v.Children))
} else {
fmt.Fprint(buf, "...")
}
}
if nl {
fmt.Fprintf(buf, "\n%s", indent)
}
fmt.Fprint(buf, "]")
}
// PrettyExamineMemory examine the memory and format data
//
// `format` specifies the data format (or data type), `size` specifies size of each data,
// like 4byte integer, 1byte character, etc. `count` specifies the number of values.
func PrettyExamineMemory(address uintptr, memArea []byte, isLittleEndian bool, format byte, size int) string {
var (
cols int
colFormat string
colBytes = size
addrLen int
addrFmt string
)
// Different versions of golang output differently about '#'.
// See https://ci.appveyor.com/project/derekparker/delve-facy3/builds/30179356.
switch format {
case 'b':
cols = 4 // Avoid emitting rows that are too long when using binary format
colFormat = fmt.Sprintf("%%0%db", colBytes*8)
case 'o':
cols = 8
colFormat = fmt.Sprintf("0%%0%do", colBytes*3) // Always keep one leading zero for octal.
case 'd':
cols = 8
colFormat = fmt.Sprintf("%%0%dd", colBytes*3)
case 'x':
cols = 8
colFormat = fmt.Sprintf("0x%%0%dx", colBytes*2) // Always keep one leading '0x' for hex.
default:
return fmt.Sprintf("not supported format %q\n", string(format))
}
colFormat += "\t"
l := len(memArea)
rows := l / (cols * colBytes)
if l%(cols*colBytes) != 0 {
rows++
}
// Avoid the lens of two adjacent address are different, so always use the last addr's len to format.
if l != 0 {
addrLen = len(fmt.Sprintf("%x", uint64(address)+uint64(l)))
}
addrFmt = "0x%0" + strconv.Itoa(addrLen) + "x:\t"
var b strings.Builder
w := tabwriter.NewWriter(&b, 0, 0, 3, ' ', 0)
for i := 0; i < rows; i++ {
fmt.Fprintf(w, addrFmt, address)
for j := 0; j < cols; j++ {
offset := i*(cols*colBytes) + j*colBytes
if offset+colBytes <= len(memArea) {
n := byteArrayToUInt64(memArea[offset:offset+colBytes], isLittleEndian)
fmt.Fprintf(w, colFormat, n)
}
}
fmt.Fprintln(w, "")
address += uintptr(cols * colBytes)
}
w.Flush()
return b.String()
}
func byteArrayToUInt64(buf []byte, isLittleEndian bool) uint64 {
var n uint64
if isLittleEndian {
for i := len(buf) - 1; i >= 0; i-- {
n = n<<8 + uint64(buf[i])
}
} else {
for i := 0; i < len(buf); i++ {
n = n<<8 + uint64(buf[i])
}
}
return n
}
const stacktraceTruncatedMessage = "(truncated)"
func digits(n int) int {
if n <= 0 {
return 1
}
return int(math.Floor(math.Log10(float64(n)))) + 1
}
func PrintStack(formatPath func(string) string, out io.Writer, stack []Stackframe, ind string, offsets bool, include func(Stackframe) bool) {
if len(stack) == 0 {
return
}
extranl := offsets
for i := range stack {
if extranl {
break
}
extranl = extranl || (len(stack[i].Defers) > 0) || (len(stack[i].Arguments) > 0) || (len(stack[i].Locals) > 0)
}
d := digits(len(stack) - 1)
fmtstr := "%s%" + strconv.Itoa(d) + "d 0x%016x in %s\n"
s := ind + strings.Repeat(" ", d+2+len(ind))
for i := range stack {
if !include(stack[i]) {
continue
}
if stack[i].Err != "" {
fmt.Fprintf(out, "%serror: %s\n", s, stack[i].Err)
continue
}
fmt.Fprintf(out, fmtstr, ind, i, stack[i].PC, stack[i].Function.Name())
fmt.Fprintf(out, "%sat %s:%d\n", s, formatPath(stack[i].File), stack[i].Line)
if offsets {
fmt.Fprintf(out, "%sframe: %+#x frame pointer %+#x\n", s, stack[i].FrameOffset, stack[i].FramePointerOffset)
}
for j, d := range stack[i].Defers {
deferHeader := fmt.Sprintf("%s defer %d: ", s, j+1)
s2 := strings.Repeat(" ", len(deferHeader))
if d.Unreadable != "" {
fmt.Fprintf(out, "%s(unreadable defer: %s)\n", deferHeader, d.Unreadable)
continue
}
fmt.Fprintf(out, "%s%#016x in %s\n", deferHeader, d.DeferredLoc.PC, d.DeferredLoc.Function.Name())
fmt.Fprintf(out, "%sat %s:%d\n", s2, formatPath(d.DeferredLoc.File), d.DeferredLoc.Line)
fmt.Fprintf(out, "%sdeferred by %s at %s:%d\n", s2, d.DeferLoc.Function.Name(), formatPath(d.DeferLoc.File), d.DeferLoc.Line)
}
for j := range stack[i].Arguments {
fmt.Fprintf(out, "%s %s = %s\n", s, stack[i].Arguments[j].Name, stack[i].Arguments[j].SinglelineString())
}
for j := range stack[i].Locals {
fmt.Fprintf(out, "%s %s = %s\n", s, stack[i].Locals[j].Name, stack[i].Locals[j].SinglelineString())
}
if extranl {
fmt.Fprintln(out)
}
}
if len(stack) > 0 && !stack[len(stack)-1].Bottom {
fmt.Fprintf(out, "%s"+stacktraceTruncatedMessage+"\n", ind)
}
}