Skip to content

Commit 19ce116

Browse files
authored
service/dap: allow expression evaluation in log messages (#2747)
From the DAP spec: If this attribute exists and is non-empty, the backend must not 'break' (stop) but log the message instead. Expressions within {} are interpolated. This change parses the log messages and stores the parsed values as a format string and list of expressions to evaluate and get the string value of. Updates golang/vscode-go#123
1 parent f8deab8 commit 19ce116

File tree

2 files changed

+185
-20
lines changed

2 files changed

+185
-20
lines changed

service/dap/server.go

Lines changed: 106 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1355,9 +1355,10 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(
13551355
} else {
13561356
got.Cond = want.condition
13571357
got.HitCond = want.hitCondition
1358-
got.Tracepoint = want.logMessage != ""
1359-
got.UserData = want.logMessage
1360-
err = s.debugger.AmendBreakpoint(got)
1358+
err = setLogMessage(got, want.logMessage)
1359+
if err == nil {
1360+
err = s.debugger.AmendBreakpoint(got)
1361+
}
13611362
}
13621363
createdBps[want.name] = struct{}{}
13631364
s.updateBreakpointsResponse(breakpoints, i, err, got)
@@ -1380,19 +1381,20 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(
13801381
if _, ok := createdBps[want.name]; ok {
13811382
err = fmt.Errorf("breakpoint already exists")
13821383
} else {
1383-
// Create new breakpoints.
1384-
got, err = s.debugger.CreateBreakpoint(
1385-
&api.Breakpoint{
1386-
Name: want.name,
1387-
File: wantLoc.file,
1388-
Line: wantLoc.line,
1389-
Addr: wantLoc.addr,
1390-
Addrs: wantLoc.addrs,
1391-
Cond: want.condition,
1392-
HitCond: want.hitCondition,
1393-
Tracepoint: want.logMessage != "",
1394-
UserData: want.logMessage,
1395-
})
1384+
bp := &api.Breakpoint{
1385+
Name: want.name,
1386+
File: wantLoc.file,
1387+
Line: wantLoc.line,
1388+
Addr: wantLoc.addr,
1389+
Addrs: wantLoc.addrs,
1390+
Cond: want.condition,
1391+
HitCond: want.hitCondition,
1392+
}
1393+
err = setLogMessage(bp, want.logMessage)
1394+
if err == nil {
1395+
// Create new breakpoints.
1396+
got, err = s.debugger.CreateBreakpoint(bp)
1397+
}
13961398
}
13971399
}
13981400
createdBps[want.name] = struct{}{}
@@ -1401,6 +1403,18 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(
14011403
return breakpoints
14021404
}
14031405

1406+
func setLogMessage(bp *api.Breakpoint, msg string) error {
1407+
tracepoint, userdata, err := parseLogPoint(msg)
1408+
if err != nil {
1409+
return err
1410+
}
1411+
bp.Tracepoint = tracepoint
1412+
if userdata != nil {
1413+
bp.UserData = *userdata
1414+
}
1415+
return nil
1416+
}
1417+
14041418
func (s *Session) updateBreakpointsResponse(breakpoints []dap.Breakpoint, i int, err error, got *api.Breakpoint) {
14051419
breakpoints[i].Verified = (err == nil)
14061420
if err != nil {
@@ -3601,8 +3615,8 @@ func (s *Session) logBreakpointMessage(bp *api.Breakpoint, goid int) bool {
36013615
if !bp.Tracepoint {
36023616
return false
36033617
}
3604-
// TODO(suzmue): allow evaluate expressions within log points.
3605-
if msg, ok := bp.UserData.(string); ok {
3618+
if lMsg, ok := bp.UserData.(logMessage); ok {
3619+
msg := lMsg.evaluate(s, goid)
36063620
s.send(&dap.OutputEvent{
36073621
Event: *newEvent("output"),
36083622
Body: dap.OutputEventBody{
@@ -3618,6 +3632,19 @@ func (s *Session) logBreakpointMessage(bp *api.Breakpoint, goid int) bool {
36183632
return true
36193633
}
36203634

3635+
func (msg *logMessage) evaluate(s *Session, goid int) string {
3636+
evaluated := make([]interface{}, len(msg.args))
3637+
for i := range msg.args {
3638+
exprVar, err := s.debugger.EvalVariableInScope(goid, 0, 0, msg.args[i], DefaultLoadConfig)
3639+
if err != nil {
3640+
evaluated[i] = fmt.Sprintf("{eval err: %e}", err)
3641+
continue
3642+
}
3643+
evaluated[i] = s.convertVariableToString(exprVar)
3644+
}
3645+
return fmt.Sprintf(msg.format, evaluated...)
3646+
}
3647+
36213648
func (s *Session) toClientPath(path string) string {
36223649
if len(s.args.substitutePathServerToClient) == 0 {
36233650
return path
@@ -3639,3 +3666,64 @@ func (s *Session) toServerPath(path string) string {
36393666
}
36403667
return serverPath
36413668
}
3669+
3670+
type logMessage struct {
3671+
format string
3672+
args []string
3673+
}
3674+
3675+
// parseLogPoint parses a log message according to the DAP spec:
3676+
// "Expressions within {} are interpolated."
3677+
func parseLogPoint(msg string) (bool, *logMessage, error) {
3678+
// Note: All braces *must* come in pairs, even those within an
3679+
// expression to be interpolated.
3680+
// TODO(suzmue): support individual braces in string values in
3681+
// eval expressions.
3682+
var args []string
3683+
3684+
var isArg bool
3685+
var formatSlice, argSlice []rune
3686+
braceCount := 0
3687+
for _, r := range msg {
3688+
if isArg {
3689+
switch r {
3690+
case '}':
3691+
if braceCount--; braceCount == 0 {
3692+
argStr := strings.TrimSpace(string(argSlice))
3693+
if len(argStr) == 0 {
3694+
return false, nil, fmt.Errorf("empty evaluation string")
3695+
}
3696+
args = append(args, argStr)
3697+
formatSlice = append(formatSlice, '%', 's')
3698+
isArg = false
3699+
continue
3700+
}
3701+
case '{':
3702+
braceCount += 1
3703+
}
3704+
argSlice = append(argSlice, r)
3705+
continue
3706+
}
3707+
3708+
switch r {
3709+
case '}':
3710+
return false, nil, fmt.Errorf("invalid log point format, unexpected '}'")
3711+
case '{':
3712+
if braceCount++; braceCount == 1 {
3713+
isArg, argSlice = true, []rune{}
3714+
continue
3715+
}
3716+
}
3717+
formatSlice = append(formatSlice, r)
3718+
}
3719+
if isArg {
3720+
return false, nil, fmt.Errorf("invalid log point format")
3721+
}
3722+
if len(formatSlice) == 0 {
3723+
return false, nil, nil
3724+
}
3725+
return true, &logMessage{
3726+
format: string(formatSlice),
3727+
args: args,
3728+
}, nil
3729+
}

service/dap/server_test.go

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3126,7 +3126,7 @@ func TestLogPoints(t *testing.T) {
31263126
execute: func() {
31273127
checkStop(t, client, 1, "main.main", 23)
31283128
bps := []int{6, 25, 27, 16}
3129-
logMessages := map[int]string{6: "in callme!", 16: "in callme2!"}
3129+
logMessages := map[int]string{6: "{i*2}: in callme!", 16: "in callme2!"}
31303130
client.SetBreakpointsRequestWithArgs(fixture.Source, bps, nil, nil, logMessages)
31313131
client.ExpectSetBreakpointsResponse(t)
31323132

@@ -3142,7 +3142,7 @@ func TestLogPoints(t *testing.T) {
31423142

31433143
client.ContinueRequest(1)
31443144
client.ExpectContinueResponse(t)
3145-
checkLogMessage(t, client.ExpectOutputEvent(t), 1, "in callme!", fixture.Source, 6)
3145+
checkLogMessage(t, client.ExpectOutputEvent(t), 1, fmt.Sprintf("%d: in callme!", i*2), fixture.Source, 6)
31463146
}
31473147
se := client.ExpectStoppedEvent(t)
31483148
if se.Body.Reason != "breakpoint" || se.Body.ThreadId != 1 {
@@ -6528,6 +6528,83 @@ func TestBadlyFormattedMessageToServer(t *testing.T) {
65286528
})
65296529
}
65306530

6531+
func TestParseLogPoint(t *testing.T) {
6532+
tests := []struct {
6533+
name string
6534+
msg string
6535+
wantTracepoint bool
6536+
wantFormat string
6537+
wantArgs []string
6538+
wantErr bool
6539+
}{
6540+
// Test simple log messages.
6541+
{name: "simple string", msg: "hello, world!", wantTracepoint: true, wantFormat: "hello, world!"},
6542+
{name: "empty string", msg: "", wantTracepoint: false, wantErr: false},
6543+
// Test parse eval expressions.
6544+
{
6545+
name: "simple eval",
6546+
msg: "{x}",
6547+
wantTracepoint: true,
6548+
wantFormat: "%s",
6549+
wantArgs: []string{"x"},
6550+
},
6551+
{
6552+
name: "type cast",
6553+
msg: "hello {string(x)}",
6554+
wantTracepoint: true,
6555+
wantFormat: "hello %s",
6556+
wantArgs: []string{"string(x)"},
6557+
},
6558+
{
6559+
name: "multiple eval",
6560+
msg: "{x} {y} {z}",
6561+
wantTracepoint: true,
6562+
wantFormat: "%s %s %s",
6563+
wantArgs: []string{"x", "y", "z"},
6564+
},
6565+
{
6566+
name: "eval expressions contain braces",
6567+
msg: "{interface{}(x)} {myType{y}} {[]myType{{z}}}",
6568+
wantTracepoint: true,
6569+
wantFormat: "%s %s %s",
6570+
wantArgs: []string{"interface{}(x)", "myType{y}", "[]myType{{z}}"},
6571+
},
6572+
// Test parse errors.
6573+
{name: "empty evaluation", msg: "{}", wantErr: true},
6574+
{name: "empty space evaluation", msg: "{ \n}", wantErr: true},
6575+
{name: "open brace missing closed", msg: "{", wantErr: true},
6576+
{name: "closed brace missing open", msg: "}", wantErr: true},
6577+
{name: "open brace in expression", msg: `{m["{"]}`, wantErr: true},
6578+
{name: "closed brace in expression", msg: `{m["}"]}`, wantErr: true},
6579+
}
6580+
for _, tt := range tests {
6581+
t.Run(tt.name, func(t *testing.T) {
6582+
gotTracepoint, gotLogMessage, err := parseLogPoint(tt.msg)
6583+
if gotTracepoint != tt.wantTracepoint {
6584+
t.Errorf("parseLogPoint() tracepoint = %v, wantTracepoint %v", gotTracepoint, tt.wantTracepoint)
6585+
return
6586+
}
6587+
if (err != nil) != tt.wantErr {
6588+
t.Errorf("parseLogPoint() error = %v, wantErr %v", err, tt.wantErr)
6589+
return
6590+
}
6591+
if !tt.wantTracepoint {
6592+
return
6593+
}
6594+
if gotLogMessage == nil {
6595+
t.Errorf("parseLogPoint() gotLogMessage = nil, want log message")
6596+
return
6597+
}
6598+
if gotLogMessage.format != tt.wantFormat {
6599+
t.Errorf("parseLogPoint() gotFormat = %v, want %v", gotLogMessage.format, tt.wantFormat)
6600+
}
6601+
if !reflect.DeepEqual(gotLogMessage.args, tt.wantArgs) {
6602+
t.Errorf("parseLogPoint() gotArgs = %v, want %v", gotLogMessage.args, tt.wantArgs)
6603+
}
6604+
})
6605+
}
6606+
}
6607+
65316608
func TestDisassemble(t *testing.T) {
65326609
runTest(t, "increment", func(client *daptest.Client, fixture protest.Fixture) {
65336610
runDebugSessionWithBPs(t, client, "launch",

0 commit comments

Comments
 (0)