gdb/dap/startup.py \
gdb/dap/state.py \
gdb/dap/threads.py \
+ gdb/dap/typecheck.py \
gdb/dap/varref.py \
gdb/function/__init__.py \
gdb/function/as_string.py \
import gdb
import os
+# These are deprecated in 3.9, but required in older versions.
+from typing import Optional, Sequence
+
from .server import request, capability
from .startup import send_gdb_with_response, in_gdb_thread
return _set_breakpoints_callback(kind, specs, gdb.Breakpoint)
+# FIXME we do not specify a type for 'source'.
+# FIXME 'breakpoints' is really a list[SourceBreakpoint].
@request("setBreakpoints")
-def set_breakpoint(*, source, breakpoints=(), **args):
+def set_breakpoint(*, source, breakpoints: Sequence = (), **args):
if "path" not in source:
result = []
else:
@request("setFunctionBreakpoints")
@capability("supportsFunctionBreakpoints")
-def set_fn_breakpoint(*, breakpoints, **args):
+def set_fn_breakpoint(*, breakpoints: Sequence, **args):
specs = []
for bp in breakpoints:
specs.append(
@request("setInstructionBreakpoints")
@capability("supportsInstructionBreakpoints")
-def set_insn_breakpoints(*, breakpoints, offset=None, **args):
+def set_insn_breakpoints(
+ *, breakpoints: Sequence, offset: Optional[int] = None, **args
+):
specs = []
for bp in breakpoints:
# There's no way to set an explicit address breakpoint
@request("setExceptionBreakpoints")
@capability("supportsExceptionFilterOptions")
-@capability("exceptionBreakpointFilters", ({
- "filter": "assert",
- "label": "Ada assertions",
- "supportsCondition": True,
-}, {
- "filter": "exception",
- "label": "Ada exceptions",
- "supportsCondition": True,
-}))
-def set_exception_breakpoints(*, filters, filterOptions=(), **args):
+@capability(
+ "exceptionBreakpointFilters",
+ (
+ {
+ "filter": "assert",
+ "label": "Ada assertions",
+ "supportsCondition": True,
+ },
+ {
+ "filter": "exception",
+ "label": "Ada exceptions",
+ "supportsCondition": True,
+ },
+ ),
+)
+def set_exception_breakpoints(
+ *, filters: Sequence[str], filterOptions: Sequence = (), **args
+):
# Convert the 'filters' to the filter-options style.
options = [{"filterId": filter} for filter in filters]
options.extend(filterOptions)
@request("stackTrace")
@capability("supportsDelayedStackTraceLoading")
-def stacktrace(*, levels=0, startFrame=0, threadId, **extra):
+def stacktrace(*, levels: int = 0, startFrame: int = 0, threadId: int, **extra):
return send_gdb_with_response(lambda: _backtrace(threadId, levels, startFrame))
@request("disassemble")
@capability("supportsDisassembleRequest")
def disassemble(
- *, memoryReference, offset=0, instructionOffset=0, instructionCount, **extra
+ *,
+ memoryReference: str,
+ offset: int = 0,
+ instructionOffset: int = 0,
+ instructionCount: int,
+ **extra
):
pc = int(memoryReference, 0) + offset
return send_gdb_with_response(
import gdb
import gdb.printing
+# This is deprecated in 3.9, but required in older versions.
+from typing import Optional
+
from .frames import frame_for_id
from .server import request
from .startup import send_gdb_with_response, in_gdb_thread
# FIXME supportsVariableType handling
@request("evaluate")
-def eval_request(*, expression, frameId=None, context="variables", **args):
+def eval_request(
+ *,
+ expression: str,
+ frameId: Optional[int] = None,
+ context: str = "variables",
+ **args,
+):
if context in ("watch", "variables"):
# These seem to be expression-like.
return send_gdb_with_response(lambda: _evaluate(expression, frameId))
@request("variables")
# Note that we ignore the 'filter' field. That seems to be
# specific to javascript.
-def variables(*, variablesReference, start=0, count=0, **args):
+def variables(*, variablesReference: int, start: int = 0, count: int = 0, **args):
result = send_gdb_with_response(
lambda: _variables(variablesReference, start, count)
)
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import gdb
+
+# These are deprecated in 3.9, but required in older versions.
+from typing import Mapping, Optional, Sequence
+
from .events import ExecutionInvoker
from .server import request, capability
from .startup import send_gdb, send_gdb_with_response, in_gdb_thread
# from implementations. Any additions or changes here should be
# documented in the gdb manual.
@request("launch")
-def launch(*, program=None, args=(), env=None, **extra):
+def launch(
+ *,
+ program: Optional[str] = None,
+ args: Sequence[str] = (),
+ env: Optional[Mapping[str, str]] = None,
+ **extra,
+):
if program is not None:
global _program
_program = program
@request("attach")
-def attach(*, pid, **args):
+def attach(*, pid: int, **args):
# Ensure configurationDone does not try to run.
global _program
_program = None
@request("readMemory")
@capability("supportsReadMemoryRequest")
-def read_memory(*, memoryReference, offset=0, count, **extra):
+def read_memory(*, memoryReference: str, offset: int = 0, count: int, **extra):
addr = int(memoryReference, 0) + offset
buf = send_gdb_with_response(lambda: _read_memory(addr, count))
return {
@request("writeMemory")
@capability("supportsWriteMemoryRequest")
-def write_memory(*, memoryReference, offset=0, data, **extra):
+def write_memory(*, memoryReference: str, offset: int = 0, data: str, **extra):
addr = int(memoryReference, 0) + offset
send_gdb_with_response(lambda: _write_memory(addr, data))
return {}
@request("next")
-def next(*, threadId, singleThread=False, granularity="statement", **args):
+def next(
+ *, threadId: int, singleThread: bool = False, granularity: str = "statement", **args
+):
send_gdb(lambda: _handle_thread_step(threadId, singleThread))
cmd = "next"
if granularity == "instruction":
@capability("supportsSteppingGranularity")
@capability("supportsSingleThreadExecutionRequests")
@request("stepIn")
-def step_in(*, threadId, singleThread=False, granularity="statement", **args):
+def step_in(
+ *, threadId: int, singleThread: bool = False, granularity: str = "statement", **args
+):
send_gdb(lambda: _handle_thread_step(threadId, singleThread))
cmd = "step"
if granularity == "instruction":
@request("stepOut")
-def step_out(*, threadId, singleThread=False):
+def step_out(*, threadId: int, singleThread: bool = False):
send_gdb(lambda: _handle_thread_step(threadId, singleThread))
send_gdb(ExecutionInvoker("finish", StopKinds.STEP))
@request("continue")
-def continue_request(*, threadId, singleThread=False, **args):
+def continue_request(*, threadId: int, singleThread: bool = False, **args):
locked = send_gdb_with_response(lambda: _handle_thread_step(threadId, singleThread))
send_gdb(ExecutionInvoker("continue", None))
return {"allThreadsContinued": not locked}
@request("scopes")
-def scopes(*, frameId, **extra):
+def scopes(*, frameId: int, **extra):
scopes = send_gdb_with_response(lambda: _get_scope(frameId))
return {"scopes": scopes}
log_stack,
send_gdb_with_response,
)
+from .typecheck import type_check
# Map capability names to values.
def wrap(func):
global _commands
# All requests must run in the DAP thread.
- func = in_dap_thread(func)
+ # Also type-check the calls.
+ func = in_dap_thread(type_check(func))
_commands[name] = func
return func
@request("disconnect")
@capability("supportTerminateDebuggee")
-def disconnect(*, terminateDebuggee=False, **args):
+def disconnect(*, terminateDebuggee: bool = False, **args):
if terminateDebuggee:
terminate()
_server.shutdown()
--- /dev/null
+# Copyright 2023 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/>.
+
+# A simple runtime type checker.
+
+import collections.abc
+import functools
+import typing
+
+
+# 'isinstance' won't work in general for type variables, so we
+# implement the subset that is needed by DAP.
+def _check_instance(value, typevar):
+ base = typing.get_origin(typevar)
+ if base is None:
+ return isinstance(value, typevar)
+ arg_types = typing.get_args(typevar)
+ if base == collections.abc.Mapping or base == typing.Mapping:
+ if not isinstance(value, collections.abc.Mapping):
+ return False
+ assert len(arg_types) == 2
+ (keytype, valuetype) = arg_types
+ return all(
+ _check_instance(k, keytype) and _check_instance(v, valuetype)
+ for k, v in value.items()
+ )
+ elif base == collections.abc.Sequence or base == typing.Sequence:
+ # In some places we simply use 'Sequence' without arguments.
+ if not isinstance(value, base):
+ return False
+ if len(arg_types) == 0:
+ return True
+ assert len(arg_types) == 1
+ arg_type = arg_types[0]
+ return all(_check_instance(item, arg_type) for item in value)
+ elif base == typing.Union:
+ return any(_check_instance(value, arg_type) for arg_type in arg_types)
+ raise TypeError("unsupported type variable '" + str(typevar) + "'")
+
+
+def type_check(func):
+ """A decorator that checks FUNC's argument types at runtime."""
+
+ # The type checker relies on 'typing.get_origin', which was added
+ # in Python 3.8. (It also relies on 'typing.get_args', but that
+ # was added at the same time.)
+ if not hasattr(typing, "get_origin"):
+ return func
+
+ hints = typing.get_type_hints(func)
+ # We don't check the return type, but we allow it in case someone
+ # wants to use it on a function definition.
+ if "return" in hints:
+ del hints["return"]
+
+ # Note that keyword-only is fine for our purposes, because this is
+ # only used for DAP requests, and those are always called this
+ # way.
+ @functools.wraps(func)
+ def check_arguments(**kwargs):
+ for key in hints:
+ # The argument might not be passed in; we could type-check
+ # any default value here, but it seems fine to just rely
+ # on the code being correct -- the main goal of this
+ # checking is to verify JSON coming from the client.
+ if key in kwargs and not _check_instance(kwargs[key], hints[key]):
+ raise TypeError(
+ "value for '"
+ + key
+ + "' does not have expected type '"
+ + str(hints[key])
+ + "'"
+ )
+ return func(**kwargs)
+
+ return check_arguments
--- /dev/null
+# Copyright 2023 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 the DAP type-checker.
+
+require allow_dap_tests allow_python_tests
+
+load_lib dap-support.exp
+load_lib gdb-python.exp
+
+clean_restart
+
+set remote_python_file \
+ [gdb_remote_download host ${srcdir}/${subdir}/${gdb_test_file_name}.py]
+gdb_test_no_output "source ${remote_python_file}" "load python file"
+
+gdb_test "python check_everything()" OK "type checker"
--- /dev/null
+# Copyright 2023 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 the type checker.
+
+import typing
+from gdb.dap.typecheck import type_check
+
+
+# A wrapper to call a function that should cause a type-checking
+# failure. Returns if the exception was seen. Throws an exception if
+# a TypeError was not seen.
+def should_fail(func, **args):
+ try:
+ func(**args)
+ except TypeError:
+ return
+ raise RuntimeError("function failed to throw TypeError")
+
+
+# Also specify a return type to make sure return types do not confuse
+# the checker.
+@type_check
+def simple_types(*, b: bool, s: str, i: int = 23) -> int:
+ return i
+
+
+def check_simple():
+ simple_types(b=True, s="DEI", i=97)
+ # Check the absence of a defaulted argument.
+ simple_types(b=True, s="DEI")
+ simple_types(b=False, s="DEI", i=97)
+ should_fail(simple_types, b=97, s="DEI", i=97)
+ should_fail(simple_types, b=True, s=None, i=97)
+ should_fail(simple_types, b=True, s="DEI", i={})
+
+
+@type_check
+def sequence_type(*, s: typing.Sequence[str]):
+ pass
+
+
+def check_sequence():
+ sequence_type(s=("hi", "out", "there"))
+ sequence_type(s=["hi", "out", "there"])
+ sequence_type(s=())
+ sequence_type(s=[])
+ should_fail(sequence_type, s=23)
+ should_fail(sequence_type, s=["hi", 97])
+
+
+@type_check
+def map_type(*, m: typing.Mapping[str, int]):
+ pass
+
+
+def check_map():
+ map_type(m={})
+ map_type(m={"dei": 23})
+ should_fail(map_type, m=[1, 2, 3])
+ should_fail(map_type, m=None)
+ should_fail(map_type, m={"dei": "string"})
+
+
+@type_check
+def opt_type(*, u: typing.Optional[int]):
+ pass
+
+
+def check_opt():
+ opt_type(u=23)
+ opt_type(u=None)
+ should_fail(opt_type, u="string")
+
+
+def check_everything():
+ # Older versions of Python can't really implement this.
+ if hasattr(typing, "get_origin"):
+ # Just let any exception propagate and end up in the log.
+ check_simple()
+ check_sequence()
+ check_map()
+ check_opt()
+ print("OK")