Repository: go-delve/delve
Pull request #2541 (2021-06-16 22:48:59)

service/dap: add string value of byte/rune slice as child
It is often convenient to view the string representation of a byte or rune slice when debugging.
We can display the string representation as a child of the byte slice variable. In an interactive
debugging UI, this will only get loaded when the user expands the slice.

This is implemented as a "Named" variable, so that this is shown separately from the paged index
variables.

Updates golang/vscode-go#1459

suzmue: bytes: 
<img width="608" alt="Screen Shot 2021-06-16 at 3 36 32 PM" src="https://user-images.githubusercontent.com/6634754/122304664-964d4600-ced3-11eb-861d-f21f47fd03a9.png">

paged runes:
<img width="522" alt="Screen Shot 2021-06-16 at 6 52 22 PM" src="https://user-images.githubusercontent.com/6634754/122304915-ffcd5480-ced3-11eb-8418-eb35b9dda012.png">

derekparker: @polinasok feel free to merge upon your +1.

Patch:
diff --git a/service/dap/server.go b/service/dap/server.go
index d5e1aeff..2514b3aa 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -1072,7 +1072,7 @@ func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
 			continue
 		}
 		if _, ok := bpAdded[reqString]; ok {
-			err = fmt.Errorf("Breakpoint exists at %q, line: %d, column: %d", request.Arguments.Source.Path, want.Line, want.Column)
+			err = fmt.Errorf("breakpoint exists at %q, line: %d, column: %d", request.Arguments.Source.Path, want.Line, want.Column)
 		} else {
 			got.Cond = want.Condition
 			got.HitCond = want.HitCondition
@@ -1099,7 +1099,7 @@ func (s *Server) onSetBreakpointsRequest(request *dap.SetBreakpointsRequest) {
 		var got *api.Breakpoint
 		var err error
 		if _, ok := bpAdded[reqString]; ok {
-			err = fmt.Errorf("Breakpoint exists at %q, line: %d, column: %d", request.Arguments.Source.Path, want.Line, want.Column)
+			err = fmt.Errorf("breakpoint exists at %q, line: %d, column: %d", request.Arguments.Source.Path, want.Line, want.Column)
 		} else {
 			// Create new breakpoints.
 			got, err = s.debugger.CreateBreakpoint(
@@ -1167,7 +1167,7 @@ func (s *Server) onSetFunctionBreakpointsRequest(request *dap.SetFunctionBreakpo
 			continue
 		}
 		if _, ok := bpAdded[reqString]; ok {
-			err = fmt.Errorf("Breakpoint exists at function %q", want.Name)
+			err = fmt.Errorf("breakpoint exists at function %q", want.Name)
 		} else {
 			got.Cond = want.Condition
 			got.HitCond = want.HitCondition
@@ -1707,10 +1707,22 @@ func (s *Server) onVariablesRequest(request *dap.VariablesRequest) {
 		}
 	}
 
-	children, err := s.childrenToDAPVariables(v)
-	if err != nil {
-		s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error())
-		return
+	var children []dap.Variable
+	if request.Arguments.Filter == "named" || request.Arguments.Filter == "" {
+		named, err := s.metadataToDAPVariables(v)
+		if err != nil {
+			s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error())
+			return
+		}
+		children = append(children, named...)
+	}
+	if request.Arguments.Filter == "indexed" || request.Arguments.Filter == "" {
+		indexed, err := s.childrenToDAPVariables(v)
+		if err != nil {
+			s.sendErrorResponse(request.Request, UnableToLookupVariable, "Unable to lookup variable", err.Error())
+			return
+		}
+		children = append(children, indexed...)
 	}
 	response := &dap.VariablesResponse{
 		Response: *newResponse(request.Request),
@@ -1785,6 +1797,7 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
 					Value:              key,
 					VariablesReference: keyref,
 					IndexedVariables:   getIndexedVariableCount(keyv),
+					NamedVariables:     getNamedVariableCount(keyv),
 				}
 				valvar := dap.Variable{
 					Name:               fmt.Sprintf("[val %d]", v.startIndex+kvIndex),
@@ -1793,6 +1806,7 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
 					Value:              val,
 					VariablesReference: valref,
 					IndexedVariables:   getIndexedVariableCount(valv),
+					NamedVariables:     getNamedVariableCount(valv),
 				}
 				children = append(children, keyvar, valvar)
 			} else { // At least one is a scalar
@@ -1813,9 +1827,11 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
 					}
 					kvvar.VariablesReference = keyref
 					kvvar.IndexedVariables = getIndexedVariableCount(keyv)
+					kvvar.NamedVariables = getNamedVariableCount(keyv)
 				} else if valref != 0 { // val is a type to be expanded
 					kvvar.VariablesReference = valref
 					kvvar.IndexedVariables = getIndexedVariableCount(valv)
+					kvvar.NamedVariables = getNamedVariableCount(valv)
 				}
 				children = append(children, kvvar)
 			}
@@ -1833,6 +1849,7 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
 				Value:              cvalue,
 				VariablesReference: cvarref,
 				IndexedVariables:   getIndexedVariableCount(&v.Children[i]),
+				NamedVariables:     getNamedVariableCount(&v.Children[i]),
 			}
 		}
 	default:
@@ -1872,12 +1889,59 @@ func (s *Server) childrenToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variab
 				Value:              cvalue,
 				VariablesReference: cvarref,
 				IndexedVariables:   getIndexedVariableCount(c),
+				NamedVariables:     getNamedVariableCount(c),
 			}
 		}
 	}
 	return children, nil
 }
 
+func getNamedVariableCount(v *proc.Variable) int {
+	namedVars := 0
+	if isListOfBytesOrRunes(v) {
+		// string value of array/slice of bytes and runes.
+		namedVars += 1
+	}
+	return namedVars
+}
+
+// metadataToDAPVariables returns the DAP presentation of the referenced variable's metadata.
+// These are included as named variables
+func (s *Server) metadataToDAPVariables(v *fullyQualifiedVariable) ([]dap.Variable, error) {
+	var children []dap.Variable
+
+	if isListOfBytesOrRunes(v.Variable) {
+		// Return the string value of []byte or []rune.
+		typeName := api.PrettyTypeName(v.DwarfType)
+		loadExpr := fmt.Sprintf("string(*(*%q)(%#x))", typeName, v.Addr)
+
+		s.log.Debugf("loading %s (type %s) with %s", v.fullyQualifiedNameOrExpr, typeName, loadExpr)
+		// We know that this is an array/slice of Uint8 or Int32, so we will load up to MaxStringLen.
+		config := DefaultLoadConfig
+		config.MaxArrayValues = config.MaxStringLen
+		vLoaded, err := s.debugger.EvalVariableInScope(-1, 0, 0, loadExpr, config)
+		val := s.convertVariableToString(vLoaded)
+		if err == nil {
+			// TODO(suzmue): Add evaluate name. Using string(name) will not get the same result because the
+			// MaxArrayValues is not auto adjusted in evaluate requests like MaxStringLen is adjusted.
+			children = append(children, dap.Variable{
+				Name:  "string()",
+				Value: val,
+				Type:  "string",
+			})
+		}
+	}
+	return children, nil
+}
+
+func isListOfBytesOrRunes(v *proc.Variable) bool {
+	if len(v.Children) > 0 && (v.Kind == reflect.Array || v.Kind == reflect.Slice) {
+		childKind := v.Children[0].RealType.Common().ReflectKind
+		return childKind == reflect.Uint8 || childKind == reflect.Int32
+	}
+	return false
+}
+
 func (s *Server) getTypeIfSupported(v *proc.Variable) string {
 	if !s.clientCapabilities.supportsVariableType {
 		return ""
@@ -2147,7 +2211,7 @@ func (s *Server) onEvaluateRequest(request *dap.EvaluateRequest) {
 			opts |= showFullValue
 		}
 		exprVal, exprRef := s.convertVariableWithOpts(exprVar, fmt.Sprintf("(%s)", request.Arguments.Expression), opts)
-		response.Body = dap.EvaluateResponseBody{Result: exprVal, VariablesReference: exprRef, IndexedVariables: getIndexedVariableCount(exprVar)}
+		response.Body = dap.EvaluateResponseBody{Result: exprVal, VariablesReference: exprRef, IndexedVariables: getIndexedVariableCount(exprVar), NamedVariables: getNamedVariableCount(exprVar)}
 	}
 	s.send(response)
 }


Test patch:
diff --git a/service/dap/daptest/client.go b/service/dap/daptest/client.go
index f62a7136..3ad61ae1 100644
--- a/service/dap/daptest/client.go
+++ b/service/dap/daptest/client.go
@@ -353,6 +353,14 @@ func (c *Client) IndexedVariablesRequest(variablesReference, start, count int) {
 	c.send(request)
 }
 
+// NamedVariablesRequest sends a 'variables' request.
+func (c *Client) NamedVariablesRequest(variablesReference int) {
+	request := &dap.VariablesRequest{Request: *c.newRequest("variables")}
+	request.Arguments.VariablesReference = variablesReference
+	request.Arguments.Filter = "named"
+	c.send(request)
+}
+
 // TeriminateRequest sends a 'terminate' request.
 func (c *Client) TerminateRequest() {
 	c.send(&dap.TerminateRequest{Request: *c.newRequest("terminate")})
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index b1670435..c615d86f 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -1524,7 +1524,6 @@ func TestVariablesLoading(t *testing.T) {
 						longarr = client.ExpectVariablesResponse(t)
 						checkChildren(t, longarr, "longarr", 50)
 						checkArrayChildren(t, longarr, "longarr", 50)
-
 					}
 
 					// Slice not fully loaded based on LoadConfig.MaxArrayValues.
@@ -1789,6 +1788,89 @@ func TestVariablesLoading(t *testing.T) {
 	})
 }
 
+// TestVariablesMetadata exposes test cases where variables contain metadata that
+// can be accessed by requesting named variables.
+func TestVariablesMetadata(t *testing.T) {
+	runTest(t, "testvariables2", func(client *daptest.Client, fixture protest.Fixture) {
+		runDebugSessionWithBPs(t, client, "launch",
+			// Launch
+			func() {
+				client.LaunchRequest("exec", fixture.Path, !stopOnEntry)
+			},
+			// Breakpoints are set within the program
+			fixture.Source, []int{},
+			[]onBreakpoint{{
+				execute:    func() {},
+				disconnect: false,
+			}, {
+				execute: func() {
+					checkStop(t, client, 1, "main.main", 368)
+
+					client.VariablesRequest(1001) // Locals
+					locals := client.ExpectVariablesResponse(t)
+
+					checkNamedChildren := func(ref int, name, typeStr string, vals []string, evaluate bool) {
+						// byteslice, request named variables
+						client.NamedVariablesRequest(ref)
+						named := client.ExpectVariablesResponse(t)
+						checkChildren(t, named, name, 1)
+						checkVarExact(t, named, 0, "string()", "", "\"tèst\"", "string", false)
+
+						client.VariablesRequest(ref)
+						all := client.ExpectVariablesResponse(t)
+						checkChildren(t, all, name, len(vals)+1)
+						checkVarExact(t, all, 0, "string()", "", "\"tèst\"", "string", false)
+						for i, v := range vals {
+							idx := fmt.Sprintf("[%d]", i)
+							evalName := fmt.Sprintf("%s[%d]", name, i)
+							if evaluate {
+								evalName = fmt.Sprintf("(%s)[%d]", name, i)
+							}
+							checkVarExact(t, all, i+1, idx, evalName, v, typeStr, false)
+						}
+					}
+
+					// byteslice
+					ref := checkVarExactIndexed(t, locals, -1, "byteslice", "byteslice", "[]uint8 len: 5, cap: 5, [116,195,168,115,116]", "[]uint8", true, 5, 1)
+					checkNamedChildren(ref, "byteslice", "uint8", []string{"116", "195", "168", "115", "116"}, false)
+
+					client.EvaluateRequest("byteslice", 0, "")
+					got := client.ExpectEvaluateResponse(t)
+					ref = checkEvalIndexed(t, got, "[]uint8 len: 5, cap: 5, [116,195,168,115,116]", hasChildren, 5, 1)
+					checkNamedChildren(ref, "byteslice", "uint8", []string{"116", "195", "168", "115", "116"}, true)
+
+					// runeslice
+					ref = checkVarExactIndexed(t, locals, -1, "runeslice", "runeslice", "[]int32 len: 4, cap: 4, [116,232,115,116]", "[]int32", true, 4, 1)
+					checkNamedChildren(ref, "runeslice", "int32", []string{"116", "232", "115", "116"}, false)
+
+					client.EvaluateRequest("runeslice", 0, "repl")
+					got = client.ExpectEvaluateResponse(t)
+					ref = checkEvalIndexed(t, got, "[]int32 len: 4, cap: 4, [116,232,115,116]", hasChildren, 4, 1)
+					checkNamedChildren(ref, "runeslice", "int32", []string{"116", "232", "115", "116"}, true)
+
+					// bytearray
+					ref = checkVarExactIndexed(t, locals, -1, "bytearray", "bytearray", "[5]uint8 [116,195,168,115,116]", "[5]uint8", true, 5, 1)
+					checkNamedChildren(ref, "bytearray", "uint8", []string{"116", "195", "168", "115", "116"}, false)
+
+					client.EvaluateRequest("bytearray", 0, "hover")
+					got = client.ExpectEvaluateResponse(t)
+					ref = checkEvalIndexed(t, got, "[5]uint8 [116,195,168,115,116]", hasChildren, 5, 1)
+					checkNamedChildren(ref, "bytearray", "uint8", []string{"116", "195", "168", "115", "116"}, true)
+
+					// runearray
+					ref = checkVarExactIndexed(t, locals, -1, "runearray", "runearray", "[4]int32 [116,232,115,116]", "[4]int32", true, 4, 1)
+					checkNamedChildren(ref, "runearray", "int32", []string{"116", "232", "115", "116"}, false)
+
+					client.EvaluateRequest("runearray", 0, "watch")
+					got = client.ExpectEvaluateResponse(t)
+					ref = checkEvalIndexed(t, got, "[4]int32 [116,232,115,116]", hasChildren, 4, 1)
+					checkNamedChildren(ref, "runearray", "int32", []string{"116", "232", "115", "116"}, true)
+				},
+				disconnect: true,
+			}})
+	})
+}
+
 // TestGlobalScopeAndVariables launches the program with showGlobalVariables
 // arg set, executes to a breakpoint in the main package and tests that global
 // package main variables got loaded. It then steps into a function
@@ -1947,7 +2029,7 @@ func checkBreakpoints(t *testing.T, client *daptest.Client, bps []Breakpoint, br
 	}
 	for i, bp := range breakpoints {
 		if bps[i].line < 0 && !bps[i].verified {
-			if bp.Verified != bps[i].verified || !strings.Contains(bp.Message, bps[i].msgPrefix) {
+			if bp.Verified != bps[i].verified || !stringContainsCaseInsensitive(bp.Message, bps[i].msgPrefix) {
 				t.Errorf("got breakpoints[%d] = %#v, \nwant %#v", i, bp, bps[i])
 			}
 			continue
@@ -1990,7 +2072,7 @@ func TestSetBreakpoint(t *testing.T) {
 
 					// Set another breakpoint inside the loop in loop(), twice to trigger error
 					client.SetBreakpointsRequest(fixture.Source, []int{8, 8})
-					expectSetBreakpointsResponse(t, client, []Breakpoint{{8, fixture.Source, true, ""}, {-1, "", false, "Breakpoint exists"}})
+					expectSetBreakpointsResponse(t, client, []Breakpoint{{8, fixture.Source, true, ""}, {-1, "", false, "breakpoint exists"}})
 
 					// Continue into the loop
 					client.ContinueRequest(1)
@@ -2037,6 +2119,10 @@ func TestSetBreakpoint(t *testing.T) {
 	})
 }
 
+func stringContainsCaseInsensitive(got, want string) bool {
+	return strings.Contains(strings.ToLower(got), strings.ToLower(want))
+}
+
 // TestSetFunctionBreakpoints is inspired by service/test.TestClientServer_FindLocations.
 func TestSetFunctionBreakpoints(t *testing.T) {
 	runTest(t, "locationsprog", func(client *daptest.Client, fixture protest.Fixture) {
@@ -2066,7 +2152,7 @@ func TestSetFunctionBreakpoints(t *testing.T) {
 						}
 						for i, bp := range got.Body.Breakpoints {
 							if bps[i].line < 0 && !bps[i].verified {
-								if bp.Verified != bps[i].verified || !strings.Contains(bp.Message, bps[i].errMsg) {
+								if bp.Verified != bps[i].verified || !stringContainsCaseInsensitive(bp.Message, bps[i].errMsg) {
 									t.Errorf("got breakpoints[%d] = %#v, \nwant %#v", i, bp, bps[i])
 								}
 								continue
@@ -2213,7 +2299,7 @@ func TestSetFunctionBreakpoints(t *testing.T) {
 					client.SetFunctionBreakpointsRequest([]dap.FunctionBreakpoint{
 						{Name: "SomeType.String"}, {Name: "(*SomeType).String"},
 					})
-					expectSetFunctionBreakpointsResponse([]Breakpoint{{14, filepath.Base(fixture.Source), true, ""}, {-1, "", false, "Breakpoint exists"}})
+					expectSetFunctionBreakpointsResponse([]Breakpoint{{14, filepath.Base(fixture.Source), true, ""}, {-1, "", false, "breakpoint exists"}})
 
 					// Set two breakpoints at SomeType.String and SomeType.SomeFunction.
 					client.SetFunctionBreakpointsRequest([]dap.FunctionBreakpoint{
@@ -3701,7 +3787,7 @@ func TestLaunchDebugRequest(t *testing.T) {
 		// BinaryInfo.Close(), but it appears that it is still in use (by Windows?)
 		// shortly after. gobuild.Remove has a delay to address this, but
 		// to avoid any test flakiness we guard against this failure here as well.
-		if runtime.GOOS != "windows" || !strings.Contains(rmErr, "Access is denied") {
+		if runtime.GOOS != "windows" || !stringContainsCaseInsensitive(rmErr, "Access is denied") {
 			t.Fatalf("Binary removal failure:\n%s\n", rmErr)
 		}
 	} else {
@@ -4014,7 +4100,7 @@ func (h *helperForSetVariable) failSetVariable0(ref int, name, value, wantErrInf
 		h.c.ExpectStoppedEvent(h.t)
 	}
 	resp := h.c.ExpectErrorResponse(h.t)
-	if got := resp.Body.Error.Format; !strings.Contains(got, wantErrInfo) {
+	if got := resp.Body.Error.Format; !stringContainsCaseInsensitive(got, wantErrInfo) {
 		h.t.Errorf("got %#v, want error string containing %v", got, wantErrInfo)
 	}
 }