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.
This commit is contained in:
Stefan Haller 2023-11-09 10:15:25 +01:00 committed by GitHub
parent d6f215b27b
commit aec354cd0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 179 additions and 24 deletions

@ -24,11 +24,13 @@ 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 {
@ -45,6 +47,13 @@ func (v *Variable) SinglelineString() string {
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
@ -59,6 +68,14 @@ func (v *Variable) MultilineString(indent, fmtstr string) string {
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)
@ -67,7 +84,7 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
if !flags.top() && v.Addr == 0 && v.Value == "" {
if flags.includeType() && v.Type != "void" {
fmt.Fprintf(buf, "%s nil", v.Type)
fmt.Fprintf(buf, "%s nil", v.typeStr(flags))
} else {
fmt.Fprint(buf, "nil")
}
@ -83,10 +100,10 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
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)
v.writePointerTo(buf, flags)
} else {
if flags.top() && flags.newlines() && v.Children[0].Addr != 0 {
v.writePointerTo(buf)
v.writePointerTo(buf, flags)
fmt.Fprint(buf, "\n")
}
fmt.Fprint(buf, "*")
@ -103,14 +120,14 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
v.writeStructTo(buf, flags, indent, fmtstr)
} else {
if len(v.Children) == 0 {
fmt.Fprintf(buf, "%s nil", v.Type)
fmt.Fprintf(buf, "%s nil", v.typeStr(flags))
} else {
fmt.Fprintf(buf, "%s %s/%s", v.Type, v.Children[0].Value, v.Children[1].Value)
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.Type, v.Value)
fmt.Fprintf(buf, "%s(%s)", v.typeStr(flags), v.Value)
flags = flags.set(prettyIncludeType, false)
}
v.writeStructTo(buf, flags, indent, fmtstr)
@ -123,13 +140,13 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
}
if flags.includeType() {
if v.Children[0].Kind == reflect.Invalid {
fmt.Fprintf(buf, "%s ", v.Type)
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.Type, v.Children[0].Type)
fmt.Fprintf(buf, "%s(%s) ", v.typeStr(flags), v.Children[0].Type)
}
}
data := v.Children[0]
@ -145,9 +162,9 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
}
} else if data.OnlyAddr {
if strings.Contains(v.Type, "/") {
fmt.Fprintf(buf, "*(*%q)(%#x)", v.Type, v.Addr)
fmt.Fprintf(buf, "*(*%q)(%#x)", v.typeStr(flags), v.Addr)
} else {
fmt.Fprintf(buf, "*(*%s)(%#x)", v.Type, v.Addr)
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)
@ -165,11 +182,11 @@ func (v *Variable) writeTo(buf io.Writer, flags prettyFlags, indent, fmtstr stri
}
}
func (v *Variable) writePointerTo(buf io.Writer) {
func (v *Variable) writePointerTo(buf io.Writer, flags prettyFlags) {
if strings.Contains(v.Type, "/") {
fmt.Fprintf(buf, "(%q)(%#x)", v.Type, v.Children[0].Addr)
fmt.Fprintf(buf, "(%q)(%#x)", v.typeStr(flags), v.Children[0].Addr)
} else {
fmt.Fprintf(buf, "(%s)(%#x)", v.Type, v.Children[0].Addr)
fmt.Fprintf(buf, "(%s)(%#x)", v.typeStr(flags), v.Children[0].Addr)
}
}
@ -248,7 +265,7 @@ func ExtractIntValue(s string) string {
func (v *Variable) writeSliceTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
if flags.includeType() {
fmt.Fprintf(buf, "%s len: %d, cap: %d, ", v.Type, v.Len, v.Cap)
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")
@ -259,7 +276,7 @@ func (v *Variable) writeSliceTo(buf io.Writer, flags prettyFlags, indent, fmtstr
func (v *Variable) writeArrayTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
if flags.includeType() {
fmt.Fprintf(buf, "%s ", v.Type)
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
}
v.writeSliceOrArrayTo(buf, flags, indent, fmtstr)
}
@ -267,15 +284,15 @@ func (v *Variable) writeArrayTo(buf io.Writer, flags prettyFlags, 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.Type, v.Addr)
fmt.Fprintf(buf, "(*%q)(%#x)", v.typeStr(flags), v.Addr)
} else {
fmt.Fprintf(buf, "(*%s)(%#x)", v.Type, v.Addr)
fmt.Fprintf(buf, "(*%s)(%#x)", v.typeStr(flags), v.Addr)
}
return
}
if flags.includeType() {
fmt.Fprintf(buf, "%s ", v.Type)
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
}
nl := v.shouldNewlineStruct(flags.newlines())
@ -310,7 +327,7 @@ func (v *Variable) writeStructTo(buf io.Writer, flags prettyFlags, indent, fmtst
func (v *Variable) writeMapTo(buf io.Writer, flags prettyFlags, indent, fmtstr string) {
if flags.includeType() {
fmt.Fprintf(buf, "%s ", v.Type)
fmt.Fprintf(buf, "%s ", v.typeStr(flags))
}
if v.Base == 0 && len(v.Children) == 0 {
fmt.Fprintf(buf, "nil")

102
service/api/shorten_type.go Normal file

@ -0,0 +1,102 @@
package api
import (
"strings"
"unicode"
)
func ShortenType(typ string) string {
out, ok := shortenTypeEx(typ)
if !ok {
return typ
}
return out
}
func shortenTypeEx(typ string) (string, bool) {
switch {
case strings.HasPrefix(typ, "["):
for i := range typ {
if typ[i] == ']' {
sub, ok := shortenTypeEx(typ[i+1:])
return typ[:i+1] + sub, ok
}
}
return "", false
case strings.HasPrefix(typ, "*"):
sub, ok := shortenTypeEx(typ[1:])
return "*" + sub, ok
case strings.HasPrefix(typ, "map["):
depth := 1
for i := 4; i < len(typ); i++ {
switch typ[i] {
case '[':
depth++
case ']':
depth--
if depth == 0 {
key, keyok := shortenTypeEx(typ[4:i])
val, valok := shortenTypeEx(typ[i+1:])
return "map[" + key + "]" + val, keyok && valok
}
}
}
return "", false
case typ == "interface {}" || typ == "interface{}":
return typ, true
case typ == "struct {}" || typ == "struct{}":
return typ, true
default:
if containsAnonymousType(typ) {
return "", false
}
if lbrk := strings.Index(typ, "["); lbrk >= 0 {
if typ[len(typ)-1] != ']' {
return "", false
}
typ0, ok := shortenTypeEx(typ[:lbrk])
if !ok {
return "", false
}
args := strings.Split(typ[lbrk+1:len(typ)-1], ",")
for i := range args {
var ok bool
args[i], ok = shortenTypeEx(strings.TrimSpace(args[i]))
if !ok {
return "", false
}
}
return typ0 + "[" + strings.Join(args, ", ") + "]", true
}
slashnum := 0
slash := -1
for i, ch := range typ {
if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' && ch != '.' && ch != '/' && ch != '@' && ch != '%' && ch != '-' {
return "", false
}
if ch == '/' {
slash = i
slashnum++
}
}
if slashnum <= 1 || slash < 0 {
return typ, true
}
return typ[slash+1:], true
}
}
func containsAnonymousType(typ string) bool {
for _, thing := range []string{"interface {", "interface{", "struct {", "struct{", "func (", "func("} {
idx := strings.Index(typ, thing)
if idx >= 0 && idx+len(thing) < len(typ) {
ch := typ[idx+len(thing)]
if ch != '}' && ch != ')' {
return true
}
}
}
return false
}

@ -0,0 +1,36 @@
package api
import "testing"
func TestShortenType(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"long/package/path/pkg.A", "pkg.A"},
{"[]long/package/path/pkg.A", "[]pkg.A"},
{"map[long/package/path/pkg.A]long/package/path/pkg.B", "map[pkg.A]pkg.B"},
{"map[long/package/path/pkg.A]interface {}", "map[pkg.A]interface {}"},
{"map[long/package/path/pkg.A]interface{}", "map[pkg.A]interface{}"},
{"map[long/package/path/pkg.A]struct {}", "map[pkg.A]struct {}"},
{"map[long/package/path/pkg.A]struct{}", "map[pkg.A]struct{}"},
{"map[long/package/path/pkg.A]map[long/package/path/pkg.B]long/package/path/pkg.C", "map[pkg.A]map[pkg.B]pkg.C"},
{"map[long/package/path/pkg.A][]long/package/path/pkg.B", "map[pkg.A][]pkg.B"},
{"map[uint64]*github.com/aarzilli/dwarf5/dwarf.typeUnit", "map[uint64]*dwarf.typeUnit"},
{"uint8", "uint8"},
{"encoding/binary", "encoding/binary"},
{"*github.com/go-delve/delve/pkg/proc.Target", "*proc.Target"},
{"long/package/path/pkg.Parametric[long/package/path/pkg.A, map[long/package/path/pkg.B]long/package/path/pkg.A]", "pkg.Parametric[pkg.A, map[pkg.B]pkg.A]"},
{"[]long/package/path/pkg.Parametric[long/package/path/pkg.A]", "[]pkg.Parametric[pkg.A]"},
{"[24]long/package/path/pkg.A", "[24]pkg.A"},
{"chan func()", "chan func()"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := ShortenType(tt.input)
if result != tt.expected {
t.Errorf("shortenTypeEx() got = %v, want %v", result, tt.expected)
}
})
}
}

@ -2589,7 +2589,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
}
return s.variableHandles.create(&fullyQualifiedVariable{v, qualifiedNameOrExpr, false /*not a scope*/, 0})
}
value = api.ConvertVar(v).SinglelineString()
value = api.ConvertVar(v).SinglelineStringWithShortTypes()
if v.Unreadable != nil {
return value, 0
}
@ -2603,7 +2603,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
// TODO(polina): Get *proc.Variable object from debugger instead. Export a function to set v.loaded to false
// and call v.loadValue gain with a different load config. It's more efficient, and it's guaranteed to keep
// working with generics.
value = api.ConvertVar(v).SinglelineString()
value = api.ConvertVar(v).SinglelineStringWithShortTypes()
typeName := api.PrettyTypeName(v.DwarfType)
loadExpr := fmt.Sprintf("*(*%q)(%#x)", typeName, v.Addr)
s.config.log.Debugf("loading %s (type %s) with %s", qualifiedNameOrExpr, typeName, loadExpr)
@ -2617,7 +2617,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
} else {
v.Children = vLoaded.Children
v.Value = vLoaded.Value
value = api.ConvertVar(v).SinglelineString()
value = api.ConvertVar(v).SinglelineStringWithShortTypes()
}
return value
}
@ -2649,7 +2649,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr
} else {
cLoaded.Name = v.Children[0].Name // otherwise, this will be the pointer expression
v.Children = []proc.Variable{*cLoaded}
value = api.ConvertVar(v).SinglelineString()
value = api.ConvertVar(v).SinglelineStringWithShortTypes()
}
} else {
value = reloadVariable(v, qualifiedNameOrExpr)

@ -2502,7 +2502,7 @@ func TestGlobalScopeAndVariables(t *testing.T) {
client.VariablesRequest(globalsScope)
globals := client.ExpectVariablesResponse(t)
checkChildren(t, globals, "Globals", 1)
ref := checkVarExact(t, globals, 0, "SomeVar", "github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeVar", "github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeType {X: 0}", "github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeType", hasChildren)
ref := checkVarExact(t, globals, 0, "SomeVar", "github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeVar", "pkg.SomeType {X: 0}", "github.com/go-delve/delve/_fixtures/internal/dir0/pkg.SomeType", hasChildren)
if ref > 0 {
client.VariablesRequest(ref)