Files
tekton/addons/godot_ai/utils/log_backtrace.gd
T

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"))