Handle linesStartAt1 in DAP

The DAP initialize request has a "linesStartAt1" option, where the
client can indicate that it prefers whether line numbers be 0-based or
1-based.

This patch implements this.  I audited all the line-related code in
the DAP implementation.

Note that while a similar option exists for column numbers, gdb
doesn't handle these yet, so nothing is done here.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=32468
(cherry picked from commit 8ac42dbf50)
This commit is contained in:
Tom Tromey
2024-12-16 10:44:55 -07:00
parent 2efabd1d9b
commit 093a1b6b81
8 changed files with 119 additions and 20 deletions

View File

@@ -21,7 +21,7 @@ from typing import Optional, Sequence
import gdb import gdb
from .server import capability, request, send_event from .server import capability, export_line, import_line, request, send_event
from .sources import make_source from .sources import make_source
from .startup import ( from .startup import (
DAPException, DAPException,
@@ -128,7 +128,7 @@ def _breakpoint_descriptor(bp):
result.update( result.update(
{ {
"source": make_source(filename), "source": make_source(filename),
"line": line, "line": export_line(line),
} }
) )
@@ -281,7 +281,7 @@ def _rewrite_src_breakpoint(
): ):
return { return {
"source": source["path"], "source": source["path"],
"line": line, "line": import_line(line),
"condition": condition, "condition": condition,
"hitCondition": hitCondition, "hitCondition": hitCondition,
"logMessage": logMessage, "logMessage": logMessage,

View File

@@ -21,7 +21,7 @@ import gdb
from .frames import dap_frame_generator from .frames import dap_frame_generator
from .modules import module_id from .modules import module_id
from .scopes import symbol_value from .scopes import symbol_value
from .server import capability, request from .server import capability, export_line, request
from .sources import make_source from .sources import make_source
from .startup import in_gdb_thread from .startup import in_gdb_thread
from .state import set_thread from .state import set_thread
@@ -84,10 +84,13 @@ def _backtrace(thread_id, levels, startFrame, stack_format):
"column": 0, "column": 0,
"instructionPointerReference": hex(pc), "instructionPointerReference": hex(pc),
} }
line = current_frame.line() line = export_line(current_frame.line())
if line is not None: if line is not None:
newframe["line"] = line newframe["line"] = line
if stack_format["line"]: if stack_format["line"]:
# Unclear whether export_line should be called
# here, but since it's just for users we pick the
# gdb representation.
name += ", line " + str(line) name += ", line " + str(line)
objfile = gdb.current_progspace().objfile_for_address(pc) objfile = gdb.current_progspace().objfile_for_address(pc)
if objfile is not None: if objfile is not None:

View File

@@ -15,7 +15,7 @@
import gdb import gdb
from .server import capability, request from .server import capability, export_line, request
from .sources import make_source from .sources import make_source
@@ -53,7 +53,7 @@ class _BlockTracker:
sal = gdb.find_pc_line(pc) sal = gdb.find_pc_line(pc)
if sal.symtab is not None: if sal.symtab is not None:
if sal.line != 0: if sal.line != 0:
result["line"] = sal.line result["line"] = export_line(sal.line)
if sal.symtab.filename is not None: if sal.symtab.filename is not None:
# The spec says this can be omitted in some # The spec says this can be omitted in some
# situations, but it's a little simpler to just always # situations, but it's a little simpler to just always

View File

@@ -16,7 +16,7 @@
# This is deprecated in 3.9, but required in older versions. # This is deprecated in 3.9, but required in older versions.
from typing import Optional from typing import Optional
from .server import capability, request from .server import capability, export_line, import_line, request
from .sources import decode_source from .sources import decode_source
from .startup import exec_mi_and_log from .startup import exec_mi_and_log
@@ -31,12 +31,15 @@ from .startup import exec_mi_and_log
@request("breakpointLocations", expect_stopped=False) @request("breakpointLocations", expect_stopped=False)
@capability("supportsBreakpointLocationsRequest") @capability("supportsBreakpointLocationsRequest")
def breakpoint_locations(*, source, line: int, endLine: Optional[int] = None, **extra): def breakpoint_locations(*, source, line: int, endLine: Optional[int] = None, **extra):
line = import_line(line)
if endLine is None: if endLine is None:
endLine = line endLine = line
else:
endLine = import_line(endLine)
filename = decode_source(source) filename = decode_source(source)
lines = set() lines = set()
for entry in exec_mi_and_log("-symbol-list-lines", filename)["lines"]: for entry in exec_mi_and_log("-symbol-list-lines", filename)["lines"]:
this_line = entry["line"] this_line = entry["line"]
if this_line >= line and this_line <= endLine: if this_line >= line and this_line <= endLine:
lines.add(this_line) lines.add(export_line(this_line))
return {"breakpoints": [{"line": x} for x in sorted(lines)]} return {"breakpoints": [{"line": x} for x in sorted(lines)]}

View File

@@ -17,7 +17,7 @@ import gdb
from .frames import frame_for_id from .frames import frame_for_id
from .globalvars import get_global_scope from .globalvars import get_global_scope
from .server import request from .server import export_line, request
from .sources import make_source from .sources import make_source
from .startup import in_gdb_thread from .startup import in_gdb_thread
from .varref import BaseReference from .varref import BaseReference
@@ -92,7 +92,7 @@ class _ScopeReference(BaseReference):
result["namedVariables"] = self.child_count() result["namedVariables"] = self.child_count()
frame = frame_for_id(self.frameId) frame = frame_for_id(self.frameId)
if frame.line() is not None: if frame.line() is not None:
result["line"] = frame.line() result["line"] = export_line(frame.line())
filename = frame.filename() filename = frame.filename()
if filename is not None: if filename is not None:
result["source"] = make_source(filename) result["source"] = make_source(filename)

View File

@@ -46,6 +46,10 @@ _commands = {}
# The global server. # The global server.
_server = None _server = None
# This is set by the initialize request and is used when rewriting
# line numbers.
_lines_start_at_1 = False
class DeferredRequest: class DeferredRequest:
"""If a DAP request function returns a deferred request, no """If a DAP request function returns a deferred request, no
@@ -571,15 +575,15 @@ def capability(name, value=True):
return wrap return wrap
def client_bool_capability(name): def client_bool_capability(name, default=False):
"""Return the value of a boolean client capability. """Return the value of a boolean client capability.
If the capability was not specified, or did not have boolean type, If the capability was not specified, or did not have boolean type,
False is returned.""" DEFAULT is returned. DEFAULT defaults to False."""
global _server global _server
if name in _server.config and isinstance(_server.config[name], bool): if name in _server.config and isinstance(_server.config[name], bool):
return _server.config[name] return _server.config[name]
return False return default
@request("initialize", on_dap_thread=True) @request("initialize", on_dap_thread=True)
@@ -587,6 +591,8 @@ def initialize(**args):
global _server, _capabilities global _server, _capabilities
_server.config = args _server.config = args
_server.send_event_later("initialized") _server.send_event_later("initialized")
global _lines_start_at_1
_lines_start_at_1 = client_bool_capability("linesStartAt1", True)
return _capabilities.copy() return _capabilities.copy()
@@ -690,3 +696,27 @@ def send_gdb_with_response(fn):
if isinstance(val, (Exception, KeyboardInterrupt)): if isinstance(val, (Exception, KeyboardInterrupt)):
raise val raise val
return val return val
def export_line(line):
"""Rewrite LINE according to client capability.
This applies the linesStartAt1 capability as needed,
when sending a line number from gdb to the client."""
global _lines_start_at_1
if not _lines_start_at_1:
# In gdb, lines start at 1, so we only need to change this if
# the client starts at 0.
line = line - 1
return line
def import_line(line):
"""Rewrite LINE according to client capability.
This applies the linesStartAt1 capability as needed,
when the client sends a line number to gdb."""
global _lines_start_at_1
if not _lines_start_at_1:
# In gdb, lines start at 1, so we only need to change this if
# the client starts at 0.
line = line + 1
return line

View File

@@ -0,0 +1,60 @@
# Copyright 2024 Free Software Foundation, Inc.
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Test breakpoints when lines start at zero.
require allow_dap_tests
load_lib dap-support.exp
standard_testfile basic-dap.c
if {[build_executable ${testfile}.exp $testfile $srcfile] == -1} {
return
}
if {[dap_initialize {linesStartAt1 [l false]}] == ""} {
return
}
set launch_id [dap_launch $testfile]
# We told gdb that lines start at 0, so subtract one.
set line [expr {[gdb_get_line_number "BREAK"] - 1}]
set obj [dap_check_request_and_response "set breakpoint by line number" \
setBreakpoints \
[format {o source [o path [%s]] breakpoints [a [o line [i %d]]]} \
[list s $srcfile] $line]]
set line_bpno [dap_get_breakpoint_number $obj]
dap_check_request_and_response "configurationDone" configurationDone
dap_check_response "launch response" launch $launch_id
dap_wait_for_event_and_check "inferior started" thread "body reason" started
dap_wait_for_event_and_check "stopped at line breakpoint" stopped \
"body reason" breakpoint \
"body hitBreakpointIds" $line_bpno \
"body allThreadsStopped" true
set bt [lindex [dap_check_request_and_response "backtrace" stackTrace \
{o threadId [i 1]}] \
0]
set stop_line [dict get [lindex [dict get $bt body stackFrames] 0] line]
gdb_assert {$stop_line == $line} "stop line is 0-based"
dap_shutdown

View File

@@ -272,16 +272,19 @@ proc dap_check_request_and_response {name command {obj {}}} {
# Start gdb, send a DAP initialization request and return the # Start gdb, send a DAP initialization request and return the
# response. This approach lets the caller check the feature list, if # response. This approach lets the caller check the feature list, if
# desired. Returns the empty string on failure. NAME is used as the # desired. Returns the empty string on failure. NAME is used as the
# test name. # test name. EXTRA are other settings to pass via the "initialize"
proc dap_initialize {{name "initialize"}} { # request.
proc dap_initialize {{name "initialize"} {extra ""}} {
if {[dap_gdb_start]} { if {[dap_gdb_start]} {
return "" return ""
} }
return [dap_check_request_and_response $name initialize \ return [dap_check_request_and_response $name initialize \
{o clientID [s "gdb testsuite"] \ [format {o clientID [s "gdb testsuite"] \
supportsVariableType [l true] \ supportsVariableType [l true] \
supportsVariablePaging [l true] \ supportsVariablePaging [l true] \
supportsMemoryReferences [l true]}] supportsMemoryReferences [l true] \
%s} \
$extra]]
} }
# Send a launch request specifying FILE as the program to use for the # Send a launch request specifying FILE as the program to use for the