114 lines
3.8 KiB
GDScript
114 lines
3.8 KiB
GDScript
@tool
|
|
class_name McpLogBacktrace
|
|
extends RefCounted
|
|
|
|
## Helpers for interpreting Godot's `_log_error` virtual arguments.
|
|
## (Named `McpLogBacktrace`, not `ScriptBacktrace`: Godot ships a built-in
|
|
## `ScriptBacktrace` class — the type of `script_backtraces[i]` entries
|
|
## — so class_name'ing ours the same would collide. Verified against
|
|
## the engine's `--doctool` output in 4.6.)
|
|
##
|
|
## Both `editor_logger.gd` and `game_logger.gd` need to:
|
|
## - Map `error_type` (0=ERROR, 1=WARNING, 2=SCRIPT, 3=SHADER) to a
|
|
## two-bucket "error" / "warn" string so callers can filter without
|
|
## consulting the enum.
|
|
## - Fall back to `code` when `rationale` is empty — single-arg
|
|
## `push_error("msg")` leaves rationale empty and stuffs the user's
|
|
## string into `code`; without the fallback the user message is
|
|
## silently lost. The two-arg form `push_error(code, rationale)`
|
|
## populates both and rationale wins.
|
|
## - Remap the source location to the first frame of `script_backtraces[0]`
|
|
## when present. `push_error` / `push_warning` always report
|
|
## `file=core/variant/variant_utility.cpp`; the actual user GDScript
|
|
## caller is in the backtrace.
|
|
##
|
|
## Centralising the rules keeps the next push_error semantics shift
|
|
## (already happened once between 4.5 and 4.6, see PR #78) a one-place
|
|
## fix instead of a two-place hunt.
|
|
|
|
|
|
## Coalesce the per-virtual-arg shape Godot hands `_log_error` into a
|
|
## flat record. Always walks `script_backtraces` for the first non-empty
|
|
## frame; loggers that need to filter by source path call this first and
|
|
## then check the resolved `path` field.
|
|
##
|
|
## Returns: `{level, message, path, line, function, details}`
|
|
## - `level`: "error" or "warn" (warn iff `error_type == 1`).
|
|
## - `message`: `rationale` when non-empty, else `code`.
|
|
## - `path` / `line` / `function`: first backtrace frame when one is
|
|
## available; otherwise the original `file` / `line` / `function`.
|
|
## - `details`: original `_log_error` fields plus the first non-empty
|
|
## backtrace as frames, mirroring the debugger Errors tab context.
|
|
const ERROR_TYPE_NAMES := {
|
|
0: "error",
|
|
1: "warning",
|
|
2: "script",
|
|
3: "shader",
|
|
}
|
|
|
|
|
|
static func resolve_error(
|
|
function: String,
|
|
file: String,
|
|
line: int,
|
|
code: String,
|
|
rationale: String,
|
|
error_type: int,
|
|
script_backtraces: Array,
|
|
) -> Dictionary:
|
|
var src_file := file
|
|
var src_line := line
|
|
var src_function := function
|
|
var frames: Array[Dictionary] = []
|
|
## First non-empty frame wins, not just `script_backtraces[0]` —
|
|
## chained errors can leave the leading entry empty with the actual
|
|
## user frame in `script_backtraces[1]`.
|
|
for bt in script_backtraces:
|
|
if bt != null and bt.get_frame_count() > 0:
|
|
frames = _frames_from_backtrace(bt)
|
|
src_file = str(frames[0].get("path", ""))
|
|
src_line = int(frames[0].get("line", 0))
|
|
src_function = str(frames[0].get("function", ""))
|
|
break
|
|
var message := rationale if not rationale.is_empty() else code
|
|
return {
|
|
"level": "warn" if error_type == 1 else "error",
|
|
"message": message,
|
|
"path": src_file,
|
|
"line": src_line,
|
|
"function": src_function,
|
|
"details": {
|
|
"message": message,
|
|
"code": code,
|
|
"rationale": rationale,
|
|
"error_type": error_type,
|
|
"error_type_name": _error_type_name(error_type),
|
|
"source": {
|
|
"path": file,
|
|
"line": line,
|
|
"function": function,
|
|
},
|
|
"resolved": {
|
|
"path": src_file,
|
|
"line": src_line,
|
|
"function": src_function,
|
|
},
|
|
"frames": frames,
|
|
},
|
|
}
|
|
|
|
|
|
static func _frames_from_backtrace(bt) -> Array[Dictionary]:
|
|
var frames: Array[Dictionary] = []
|
|
for i in bt.get_frame_count():
|
|
frames.append({
|
|
"path": bt.get_frame_file(i),
|
|
"line": bt.get_frame_line(i),
|
|
"function": bt.get_frame_function(i),
|
|
})
|
|
return frames
|
|
|
|
|
|
static func _error_type_name(error_type: int) -> String:
|
|
return str(ERROR_TYPE_NAMES.get(error_type, "unknown"))
|