gdharness 0.13.12 → 0.13.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js
CHANGED
|
@@ -7557,6 +7557,41 @@ function cachedClasses(projectPath) {
|
|
|
7557
7557
|
}
|
|
7558
7558
|
return listed;
|
|
7559
7559
|
}
|
|
7560
|
+
function missingMemberIn(message) {
|
|
7561
|
+
const found = /The (method|property) "([^"(]+)(?:\(\))?" is not present on the inferred type "([^"]+)"/.exec(message);
|
|
7562
|
+
if (found === null) {
|
|
7563
|
+
return null;
|
|
7564
|
+
}
|
|
7565
|
+
return {
|
|
7566
|
+
kind: found[1] === "property" ? "property" : "method",
|
|
7567
|
+
member: found[2] ?? "",
|
|
7568
|
+
type: found[3] ?? ""
|
|
7569
|
+
};
|
|
7570
|
+
}
|
|
7571
|
+
function declaresMember(source, missing) {
|
|
7572
|
+
const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7573
|
+
const declaration = missing.kind === "method" ? new RegExp(String.raw`^\s*(?:static\s+)?func\s+${name}\s*\(`, "m") : new RegExp(String.raw`^\s*(?:static\s+)?(?:@export\s+)?(?:var|const)\s+${name}\b`, "m");
|
|
7574
|
+
return declaration.test(source);
|
|
7575
|
+
}
|
|
7576
|
+
function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
7577
|
+
const read = new Map;
|
|
7578
|
+
const found = [];
|
|
7579
|
+
for (const message of messages) {
|
|
7580
|
+
const missing = missingMemberIn(message);
|
|
7581
|
+
const declaredIn = missing === null ? undefined : classes.get(missing.type);
|
|
7582
|
+
if (missing === null || declaredIn === undefined) {
|
|
7583
|
+
continue;
|
|
7584
|
+
}
|
|
7585
|
+
if (!read.has(declaredIn)) {
|
|
7586
|
+
read.set(declaredIn, sourceOf(declaredIn));
|
|
7587
|
+
}
|
|
7588
|
+
const source = read.get(declaredIn) ?? null;
|
|
7589
|
+
if (source !== null && declaresMember(source, missing)) {
|
|
7590
|
+
found.push({ ...missing, declaredIn });
|
|
7591
|
+
}
|
|
7592
|
+
}
|
|
7593
|
+
return found;
|
|
7594
|
+
}
|
|
7560
7595
|
function cacheWrittenAt(projectPath) {
|
|
7561
7596
|
const cache = join6(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
7562
7597
|
try {
|
|
@@ -7621,6 +7656,36 @@ function reason2(stdout, stderr) {
|
|
|
7621
7656
|
}
|
|
7622
7657
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
7623
7658
|
}
|
|
7659
|
+
async function runImport(godotPath, projectPath) {
|
|
7660
|
+
const logDir = mkdtempSync2(join7(tmpdir3(), "gdharness-import-"));
|
|
7661
|
+
try {
|
|
7662
|
+
const { stderr } = await run3(godotPath, [
|
|
7663
|
+
"--headless",
|
|
7664
|
+
"--log-file",
|
|
7665
|
+
join7(logDir, "engine.log"),
|
|
7666
|
+
"--path",
|
|
7667
|
+
projectPath,
|
|
7668
|
+
"--import"
|
|
7669
|
+
]);
|
|
7670
|
+
return { ok: true, messages: problems2(stderr) };
|
|
7671
|
+
} catch (error) {
|
|
7672
|
+
if (error instanceof Error && "stdout" in error && "stderr" in error) {
|
|
7673
|
+
const failed = error;
|
|
7674
|
+
return {
|
|
7675
|
+
ok: false,
|
|
7676
|
+
message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason2(failed.stdout, failed.stderr)}`,
|
|
7677
|
+
messages: problems2(failed.stderr)
|
|
7678
|
+
};
|
|
7679
|
+
}
|
|
7680
|
+
return {
|
|
7681
|
+
ok: false,
|
|
7682
|
+
message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
7683
|
+
messages: []
|
|
7684
|
+
};
|
|
7685
|
+
} finally {
|
|
7686
|
+
discard(logDir);
|
|
7687
|
+
}
|
|
7688
|
+
}
|
|
7624
7689
|
async function runOperation2(engine, operation, params, projectPath) {
|
|
7625
7690
|
const paramsDir = mkdtempSync2(join7(tmpdir3(), "gdharness-params-"));
|
|
7626
7691
|
const paramsFile = join7(paramsDir, `${operation}.json`);
|
|
@@ -15475,7 +15540,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15475
15540
|
requires: []
|
|
15476
15541
|
},
|
|
15477
15542
|
uid: { summary: "the UID of one file", requires: ["resourcePath"] },
|
|
15478
|
-
refresh_uids: {
|
|
15543
|
+
refresh_uids: {
|
|
15544
|
+
summary: "import the project so every script and shader has its .uid sidecar, and name under uidsCreated the ones this made and under stillWithoutUid the ones the engine would not import: it writes no scene and no script, so a project whose sidecars are all present is left untouched",
|
|
15545
|
+
requires: []
|
|
15546
|
+
},
|
|
15479
15547
|
refresh_classes: {
|
|
15480
15548
|
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
|
|
15481
15549
|
requires: []
|
|
@@ -15716,7 +15784,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15716
15784
|
},
|
|
15717
15785
|
{
|
|
15718
15786
|
name: "script_diagnostics",
|
|
15719
|
-
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running.",
|
|
15787
|
+
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Any diagnostic saying a member is not present on an inferred type is checked against the file the class cache points at, and one the file contradicts is named under contradictedByTheFile with staleAnalysis saying what to do: the language server hands dependents the type it analysed at startup, so a method added to an existing class_name is reported missing at every caller until the editor restarts.",
|
|
15720
15788
|
parameters: {
|
|
15721
15789
|
projectPath: PROJECT_PATH,
|
|
15722
15790
|
scriptPath: SCRIPT_PATH
|
|
@@ -16223,7 +16291,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16223
16291
|
});
|
|
16224
16292
|
|
|
16225
16293
|
// src/headless-operations.ts
|
|
16226
|
-
var HEADLESS_OPERATIONS, EDITOR_READS;
|
|
16294
|
+
var HEADLESS_OPERATIONS, ENGINE_PASSES, EDITOR_READS;
|
|
16227
16295
|
var init_headless_operations = __esm(() => {
|
|
16228
16296
|
HEADLESS_OPERATIONS = dictionary({
|
|
16229
16297
|
project_settings: dictionary({
|
|
@@ -16245,7 +16313,6 @@ var init_headless_operations = __esm(() => {
|
|
|
16245
16313
|
set_options: "set_import_options",
|
|
16246
16314
|
reimport: "reimport_resource",
|
|
16247
16315
|
uid: "get_uid",
|
|
16248
|
-
refresh_uids: "resave_resources",
|
|
16249
16316
|
refresh_classes: "refresh_class_cache"
|
|
16250
16317
|
}),
|
|
16251
16318
|
project_export: dictionary({ list: "list_export_presets" }),
|
|
@@ -16257,6 +16324,9 @@ var init_headless_operations = __esm(() => {
|
|
|
16257
16324
|
inheritance: "inspect_inheritance"
|
|
16258
16325
|
})
|
|
16259
16326
|
});
|
|
16327
|
+
ENGINE_PASSES = dictionary({
|
|
16328
|
+
project_import: dictionary({ refresh_uids: "import" })
|
|
16329
|
+
});
|
|
16260
16330
|
EDITOR_READS = dictionary({
|
|
16261
16331
|
project_settings: dictionary({ get: "get_project_setting" })
|
|
16262
16332
|
});
|
|
@@ -36049,6 +36119,33 @@ function projectStructure(projectPath) {
|
|
|
36049
36119
|
visit(projectPath);
|
|
36050
36120
|
return structure;
|
|
36051
36121
|
}
|
|
36122
|
+
function scriptsWithoutUid(projectPath) {
|
|
36123
|
+
const found = [];
|
|
36124
|
+
const visit = (directory, prefix) => {
|
|
36125
|
+
if (steppedOver(directory)) {
|
|
36126
|
+
return;
|
|
36127
|
+
}
|
|
36128
|
+
for (const entry of readdirSync4(directory, { withFileTypes: true })) {
|
|
36129
|
+
if (skipped(entry.name)) {
|
|
36130
|
+
continue;
|
|
36131
|
+
}
|
|
36132
|
+
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
36133
|
+
if (entry.isDirectory()) {
|
|
36134
|
+
visit(join13(directory, entry.name), spelled);
|
|
36135
|
+
continue;
|
|
36136
|
+
}
|
|
36137
|
+
if (!entry.isFile()) {
|
|
36138
|
+
continue;
|
|
36139
|
+
}
|
|
36140
|
+
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
36141
|
+
if (CARRIES_A_UID.has(extension) && !existsSync11(join13(directory, `${entry.name}.uid`))) {
|
|
36142
|
+
found.push(spelled);
|
|
36143
|
+
}
|
|
36144
|
+
}
|
|
36145
|
+
};
|
|
36146
|
+
visit(projectPath, "");
|
|
36147
|
+
return found.sort();
|
|
36148
|
+
}
|
|
36052
36149
|
function searchProject(projectPath, options) {
|
|
36053
36150
|
const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
36054
36151
|
const result = {
|
|
@@ -36108,10 +36205,11 @@ function searchProject(projectPath, options) {
|
|
|
36108
36205
|
visit(projectPath);
|
|
36109
36206
|
return result;
|
|
36110
36207
|
}
|
|
36111
|
-
var SKIPPED, ASSET_EXTENSIONS;
|
|
36208
|
+
var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
|
|
36112
36209
|
var init_project_scan = __esm(() => {
|
|
36113
36210
|
SKIPPED = new Set(["node_modules"]);
|
|
36114
36211
|
ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
36212
|
+
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
36115
36213
|
});
|
|
36116
36214
|
|
|
36117
36215
|
// src/runtime-client.ts
|
|
@@ -37218,6 +37316,9 @@ class GodotServer {
|
|
|
37218
37316
|
return { ok: true, op };
|
|
37219
37317
|
}
|
|
37220
37318
|
async dispatch(tool, op, args) {
|
|
37319
|
+
if (ENGINE_PASSES[tool]?.[op] !== undefined) {
|
|
37320
|
+
return await this.handleRefreshUids(args);
|
|
37321
|
+
}
|
|
37221
37322
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
37222
37323
|
if (headless !== undefined) {
|
|
37223
37324
|
const { from: asked, ...answerable } = args;
|
|
@@ -37588,6 +37689,33 @@ class GodotServer {
|
|
|
37588
37689
|
const { op: _op, projectPath: _projectPath, ...params } = contained.value;
|
|
37589
37690
|
return this.answer(await this.operation(operation, params, project.value.path));
|
|
37590
37691
|
}
|
|
37692
|
+
async handleRefreshUids(args) {
|
|
37693
|
+
const project = this.project(args);
|
|
37694
|
+
if (!project.ok) {
|
|
37695
|
+
return project.response;
|
|
37696
|
+
}
|
|
37697
|
+
const engine = await this.engine();
|
|
37698
|
+
if (!engine.ok) {
|
|
37699
|
+
return this.createErrorResponse("No Godot executable found.");
|
|
37700
|
+
}
|
|
37701
|
+
const projectPath = project.value.path;
|
|
37702
|
+
const before = scriptsWithoutUid(projectPath);
|
|
37703
|
+
const imported = await runImport(engine.value, projectPath);
|
|
37704
|
+
if (!imported.ok) {
|
|
37705
|
+
return this.answer(imported);
|
|
37706
|
+
}
|
|
37707
|
+
const after = scriptsWithoutUid(projectPath);
|
|
37708
|
+
const given = before.filter((script) => !after.includes(script));
|
|
37709
|
+
return this.answer({
|
|
37710
|
+
ok: true,
|
|
37711
|
+
messages: imported.messages,
|
|
37712
|
+
payload: {
|
|
37713
|
+
uidsCreated: given,
|
|
37714
|
+
stillWithoutUid: after,
|
|
37715
|
+
note: after.length > 0 ? `${after.length} still have no .uid, which is the engine declining to import them rather than this op skipping them. No scene was written.` : "No scene was written: this reads the project and imports it, and does not resave anything."
|
|
37716
|
+
}
|
|
37717
|
+
});
|
|
37718
|
+
}
|
|
37591
37719
|
async operation(operation, params, projectPath) {
|
|
37592
37720
|
const engine = await this.engine();
|
|
37593
37721
|
if (!engine.ok) {
|
|
@@ -37922,14 +38050,44 @@ class GodotServer {
|
|
|
37922
38050
|
const severity = asParams(entry)["severity"];
|
|
37923
38051
|
return severity === 1 || severity === "error" || severity === "ERROR";
|
|
37924
38052
|
}).length;
|
|
38053
|
+
const contradicted = this.contradictedByTheFile(diagnostics, args);
|
|
37925
38054
|
return this.jsonTextResponse(markIfStale({
|
|
37926
38055
|
scriptPath: readString(args, "scriptPath"),
|
|
37927
38056
|
clean: errors === 0,
|
|
37928
38057
|
errors,
|
|
37929
38058
|
warnings: diagnostics.length - errors,
|
|
37930
|
-
diagnostics
|
|
38059
|
+
diagnostics,
|
|
38060
|
+
...contradicted.length === 0 ? {} : {
|
|
38061
|
+
contradictedByTheFile: contradicted,
|
|
38062
|
+
staleAnalysis: `The editor is reporting against an older copy of ${contradicted.length === 1 ? "a type" : "some types"} named above. Each member listed is declared in the file the class cache points at, so those diagnostics are wrong however the code is written. editor_launch restart clears it; editor_rescan and project_import refresh_classes do not, because neither reaches the analysed types the language server built at startup.`
|
|
38063
|
+
}
|
|
37931
38064
|
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
37932
38065
|
}
|
|
38066
|
+
contradictedByTheFile(diagnostics, args) {
|
|
38067
|
+
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
38068
|
+
if (!messages.some((message) => missingMemberIn(message) !== null)) {
|
|
38069
|
+
return [];
|
|
38070
|
+
}
|
|
38071
|
+
const project = this.project(args);
|
|
38072
|
+
if (!project.ok) {
|
|
38073
|
+
return [];
|
|
38074
|
+
}
|
|
38075
|
+
const classes = cachedClasses(project.value.path);
|
|
38076
|
+
if (classes === null) {
|
|
38077
|
+
return [];
|
|
38078
|
+
}
|
|
38079
|
+
return contradictedDiagnostics(messages, classes, (resourcePath) => {
|
|
38080
|
+
const contained = resolveWithinProject(project.value.path, resourcePath);
|
|
38081
|
+
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
38082
|
+
return null;
|
|
38083
|
+
}
|
|
38084
|
+
try {
|
|
38085
|
+
return readFileSync15(contained.absolutePath, "utf8");
|
|
38086
|
+
} catch {
|
|
38087
|
+
return null;
|
|
38088
|
+
}
|
|
38089
|
+
});
|
|
38090
|
+
}
|
|
37933
38091
|
async handleLSP(toolName, args) {
|
|
37934
38092
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
37935
38093
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
@@ -40347,7 +40505,13 @@ function namedType(declared) {
|
|
|
40347
40505
|
return "any";
|
|
40348
40506
|
}
|
|
40349
40507
|
function headlessSection() {
|
|
40350
|
-
const
|
|
40508
|
+
const everyOp = new Map;
|
|
40509
|
+
for (const table of [HEADLESS_OPERATIONS, ENGINE_PASSES]) {
|
|
40510
|
+
for (const [tool, ops] of Object.entries(table)) {
|
|
40511
|
+
everyOp.set(tool, [...everyOp.get(tool) ?? [], ...Object.keys(ops)]);
|
|
40512
|
+
}
|
|
40513
|
+
}
|
|
40514
|
+
const byTool = [...everyOp].map(([tool, ops]) => `\`${tool}\` (${ops.join(", ")})`).sort();
|
|
40351
40515
|
return [
|
|
40352
40516
|
"## What a gate can call",
|
|
40353
40517
|
"",
|
|
@@ -40360,7 +40524,8 @@ function headlessSection() {
|
|
|
40360
40524
|
"Needing nothing is not the same as changing nothing, and a gate wants both. Most of these",
|
|
40361
40525
|
"write to the project because that is what they are for: `script_edit create` writes a file,",
|
|
40362
40526
|
"`project_settings set` rewrites `project.godot`, `project_import reimport` and `refresh_classes`",
|
|
40363
|
-
"write under `.godot
|
|
40527
|
+
"write under `.godot`, and `project_import refresh_uids` writes a `.uid` beside each script that",
|
|
40528
|
+
"had none. The reading ops are the ones to gate on, and which is which is in each",
|
|
40364
40529
|
"op's own summary below rather than assumed from this list.",
|
|
40365
40530
|
"",
|
|
40366
40531
|
"Everything else needs the editor (`editor_*`, `debug_*`, `scene_*`, `resource_*`) or a running",
|
|
@@ -108,8 +108,6 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
108
108
|
# Resource files
|
|
109
109
|
"get_uid":
|
|
110
110
|
payload = ResourceFiles.new(_log).get_uid(params)
|
|
111
|
-
"resave_resources":
|
|
112
|
-
payload = ResourceFiles.new(_log).resave_resources(params)
|
|
113
111
|
"refresh_class_cache":
|
|
114
112
|
payload = ClassCache.new(_log).refresh_class_cache(params)
|
|
115
113
|
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
-
const FileWalk = preload("file_walk.gd")
|
|
4
3
|
const Log = preload("logger.gd")
|
|
5
4
|
|
|
6
5
|
var _log: Log
|
|
7
|
-
var _files: FileWalk = FileWalk.new()
|
|
8
6
|
|
|
9
7
|
|
|
10
8
|
func _init(p_log: Log) -> void:
|
|
@@ -41,92 +39,3 @@ func get_uid(params: Dictionary) -> Dictionary:
|
|
|
41
39
|
return {
|
|
42
40
|
"file": file_path, "absolute_path": absolute_path, "uid": uid_content.strip_edges(), "exists": true
|
|
43
41
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# Resave every scene, script and shader so that UID references are current.
|
|
47
|
-
func resave_resources(_params: Dictionary) -> Dictionary:
|
|
48
|
-
_log.info("Resaving all resources to update UID references...")
|
|
49
|
-
var project_path: String = "res://"
|
|
50
|
-
|
|
51
|
-
var scenes: Array[String] = _files.find_files(project_path, ".tscn")
|
|
52
|
-
_log.debug("Found " + str(scenes.size()) + " scenes")
|
|
53
|
-
|
|
54
|
-
# Resave each scene
|
|
55
|
-
var success_count: int = 0
|
|
56
|
-
var error_count: int = 0
|
|
57
|
-
|
|
58
|
-
for scene_path: String in scenes:
|
|
59
|
-
_log.debug("Processing scene: " + scene_path)
|
|
60
|
-
|
|
61
|
-
if not FileAccess.file_exists(scene_path):
|
|
62
|
-
_log.error("Scene file does not exist at: " + scene_path)
|
|
63
|
-
error_count += 1
|
|
64
|
-
continue
|
|
65
|
-
|
|
66
|
-
var scene: Resource = load(scene_path)
|
|
67
|
-
if not scene:
|
|
68
|
-
error_count += 1
|
|
69
|
-
_log.error("Failed to load: " + scene_path)
|
|
70
|
-
continue
|
|
71
|
-
|
|
72
|
-
var error: Error = ResourceSaver.save(scene, scene_path)
|
|
73
|
-
if error != OK:
|
|
74
|
-
error_count += 1
|
|
75
|
-
_log.error("Failed to save: " + scene_path + ", error: " + str(error))
|
|
76
|
-
continue
|
|
77
|
-
|
|
78
|
-
success_count += 1
|
|
79
|
-
if _log.debug_mode:
|
|
80
|
-
_log.debug("Scene saved successfully: " + scene_path)
|
|
81
|
-
var file_check_after: bool = FileAccess.file_exists(scene_path)
|
|
82
|
-
_log.debug("File exists check after save: " + str(file_check_after))
|
|
83
|
-
if not file_check_after:
|
|
84
|
-
_log.error("File reported as saved but does not exist at: " + scene_path)
|
|
85
|
-
|
|
86
|
-
# A UID sits beside every script and shader as a .uid file. Outside the editor the save
|
|
87
|
-
# below answers OK and writes no sidecar, so what comes back is what was resaved and how
|
|
88
|
-
# many are still without one, rather than a count of UIDs this made.
|
|
89
|
-
var scripts: Array[String] = (
|
|
90
|
-
_files.find_files(project_path, ".gd")
|
|
91
|
-
+ _files.find_files(project_path, ".shader")
|
|
92
|
-
+ _files.find_files(project_path, ".gdshader")
|
|
93
|
-
)
|
|
94
|
-
_log.debug("Found " + str(scripts.size()) + " scripts/shaders")
|
|
95
|
-
|
|
96
|
-
var missing_uids: int = 0
|
|
97
|
-
var resaved_scripts: int = 0
|
|
98
|
-
|
|
99
|
-
for script_path: String in scripts:
|
|
100
|
-
_log.debug("Checking UID for: " + script_path)
|
|
101
|
-
var uid_path: String = script_path + ".uid"
|
|
102
|
-
|
|
103
|
-
var f: FileAccess = FileAccess.open(uid_path, FileAccess.READ)
|
|
104
|
-
if f:
|
|
105
|
-
_log.debug("UID file already exists for: " + script_path)
|
|
106
|
-
continue
|
|
107
|
-
|
|
108
|
-
missing_uids += 1
|
|
109
|
-
_log.debug("Missing UID file for: " + script_path + ", resaving...")
|
|
110
|
-
|
|
111
|
-
var res: Resource = load(script_path)
|
|
112
|
-
if not res:
|
|
113
|
-
_log.error("Failed to load resource: " + script_path)
|
|
114
|
-
continue
|
|
115
|
-
|
|
116
|
-
var error: Error = ResourceSaver.save(res, script_path)
|
|
117
|
-
if error != OK:
|
|
118
|
-
_log.error("Failed to resave: " + script_path + ", error: " + str(error))
|
|
119
|
-
continue
|
|
120
|
-
|
|
121
|
-
resaved_scripts += 1
|
|
122
|
-
_log.debug("Resaved: " + script_path)
|
|
123
|
-
|
|
124
|
-
_log.info("Resave operation complete")
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
"scenes_processed": scenes.size(),
|
|
128
|
-
"scenes_saved": success_count,
|
|
129
|
-
"scenes_with_errors": error_count,
|
|
130
|
-
"scripts_missing_uids": missing_uids,
|
|
131
|
-
"scripts_resaved": resaved_scripts
|
|
132
|
-
}
|
package/build/index.js
CHANGED
|
@@ -24817,6 +24817,41 @@ function cachedClasses(projectPath) {
|
|
|
24817
24817
|
}
|
|
24818
24818
|
return listed;
|
|
24819
24819
|
}
|
|
24820
|
+
function missingMemberIn(message) {
|
|
24821
|
+
const found = /The (method|property) "([^"(]+)(?:\(\))?" is not present on the inferred type "([^"]+)"/.exec(message);
|
|
24822
|
+
if (found === null) {
|
|
24823
|
+
return null;
|
|
24824
|
+
}
|
|
24825
|
+
return {
|
|
24826
|
+
kind: found[1] === "property" ? "property" : "method",
|
|
24827
|
+
member: found[2] ?? "",
|
|
24828
|
+
type: found[3] ?? ""
|
|
24829
|
+
};
|
|
24830
|
+
}
|
|
24831
|
+
function declaresMember(source, missing) {
|
|
24832
|
+
const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24833
|
+
const declaration = missing.kind === "method" ? new RegExp(String.raw`^\s*(?:static\s+)?func\s+${name}\s*\(`, "m") : new RegExp(String.raw`^\s*(?:static\s+)?(?:@export\s+)?(?:var|const)\s+${name}\b`, "m");
|
|
24834
|
+
return declaration.test(source);
|
|
24835
|
+
}
|
|
24836
|
+
function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
24837
|
+
const read = new Map;
|
|
24838
|
+
const found = [];
|
|
24839
|
+
for (const message of messages) {
|
|
24840
|
+
const missing = missingMemberIn(message);
|
|
24841
|
+
const declaredIn = missing === null ? undefined : classes.get(missing.type);
|
|
24842
|
+
if (missing === null || declaredIn === undefined) {
|
|
24843
|
+
continue;
|
|
24844
|
+
}
|
|
24845
|
+
if (!read.has(declaredIn)) {
|
|
24846
|
+
read.set(declaredIn, sourceOf(declaredIn));
|
|
24847
|
+
}
|
|
24848
|
+
const source = read.get(declaredIn) ?? null;
|
|
24849
|
+
if (source !== null && declaresMember(source, missing)) {
|
|
24850
|
+
found.push({ ...missing, declaredIn });
|
|
24851
|
+
}
|
|
24852
|
+
}
|
|
24853
|
+
return found;
|
|
24854
|
+
}
|
|
24820
24855
|
function cacheWrittenAt(projectPath) {
|
|
24821
24856
|
const cache = join3(projectPath, ".godot", "global_script_class_cache.cfg");
|
|
24822
24857
|
try {
|
|
@@ -26390,6 +26425,36 @@ function reason(stdout, stderr) {
|
|
|
26390
26425
|
}
|
|
26391
26426
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
26392
26427
|
}
|
|
26428
|
+
async function runImport(godotPath, projectPath) {
|
|
26429
|
+
const logDir = mkdtempSync(join6(tmpdir2(), "gdharness-import-"));
|
|
26430
|
+
try {
|
|
26431
|
+
const { stderr } = await run2(godotPath, [
|
|
26432
|
+
"--headless",
|
|
26433
|
+
"--log-file",
|
|
26434
|
+
join6(logDir, "engine.log"),
|
|
26435
|
+
"--path",
|
|
26436
|
+
projectPath,
|
|
26437
|
+
"--import"
|
|
26438
|
+
]);
|
|
26439
|
+
return { ok: true, messages: problems(stderr) };
|
|
26440
|
+
} catch (error) {
|
|
26441
|
+
if (error instanceof Error && "stdout" in error && "stderr" in error) {
|
|
26442
|
+
const failed = error;
|
|
26443
|
+
return {
|
|
26444
|
+
ok: false,
|
|
26445
|
+
message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
|
|
26446
|
+
messages: problems(failed.stderr)
|
|
26447
|
+
};
|
|
26448
|
+
}
|
|
26449
|
+
return {
|
|
26450
|
+
ok: false,
|
|
26451
|
+
message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
26452
|
+
messages: []
|
|
26453
|
+
};
|
|
26454
|
+
} finally {
|
|
26455
|
+
discard(logDir);
|
|
26456
|
+
}
|
|
26457
|
+
}
|
|
26393
26458
|
async function runOperation(engine, operation, params, projectPath) {
|
|
26394
26459
|
const paramsDir = mkdtempSync(join6(tmpdir2(), "gdharness-params-"));
|
|
26395
26460
|
const paramsFile = join6(paramsDir, `${operation}.json`);
|
|
@@ -26459,7 +26524,6 @@ var HEADLESS_OPERATIONS = dictionary({
|
|
|
26459
26524
|
set_options: "set_import_options",
|
|
26460
26525
|
reimport: "reimport_resource",
|
|
26461
26526
|
uid: "get_uid",
|
|
26462
|
-
refresh_uids: "resave_resources",
|
|
26463
26527
|
refresh_classes: "refresh_class_cache"
|
|
26464
26528
|
}),
|
|
26465
26529
|
project_export: dictionary({ list: "list_export_presets" }),
|
|
@@ -26471,6 +26535,9 @@ var HEADLESS_OPERATIONS = dictionary({
|
|
|
26471
26535
|
inheritance: "inspect_inheritance"
|
|
26472
26536
|
})
|
|
26473
26537
|
});
|
|
26538
|
+
var ENGINE_PASSES = dictionary({
|
|
26539
|
+
project_import: dictionary({ refresh_uids: "import" })
|
|
26540
|
+
});
|
|
26474
26541
|
var EDITOR_READS = dictionary({
|
|
26475
26542
|
project_settings: dictionary({ get: "get_project_setting" })
|
|
26476
26543
|
});
|
|
@@ -27408,6 +27475,34 @@ function projectStructure(projectPath) {
|
|
|
27408
27475
|
visit(projectPath);
|
|
27409
27476
|
return structure;
|
|
27410
27477
|
}
|
|
27478
|
+
var CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
27479
|
+
function scriptsWithoutUid(projectPath) {
|
|
27480
|
+
const found = [];
|
|
27481
|
+
const visit = (directory, prefix) => {
|
|
27482
|
+
if (steppedOver(directory)) {
|
|
27483
|
+
return;
|
|
27484
|
+
}
|
|
27485
|
+
for (const entry of readdirSync3(directory, { withFileTypes: true })) {
|
|
27486
|
+
if (skipped(entry.name)) {
|
|
27487
|
+
continue;
|
|
27488
|
+
}
|
|
27489
|
+
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
27490
|
+
if (entry.isDirectory()) {
|
|
27491
|
+
visit(join7(directory, entry.name), spelled);
|
|
27492
|
+
continue;
|
|
27493
|
+
}
|
|
27494
|
+
if (!entry.isFile()) {
|
|
27495
|
+
continue;
|
|
27496
|
+
}
|
|
27497
|
+
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
27498
|
+
if (CARRIES_A_UID.has(extension) && !existsSync6(join7(directory, `${entry.name}.uid`))) {
|
|
27499
|
+
found.push(spelled);
|
|
27500
|
+
}
|
|
27501
|
+
}
|
|
27502
|
+
};
|
|
27503
|
+
visit(projectPath, "");
|
|
27504
|
+
return found.sort();
|
|
27505
|
+
}
|
|
27411
27506
|
function searchProject(projectPath, options) {
|
|
27412
27507
|
const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
27413
27508
|
const result = {
|
|
@@ -28562,7 +28657,10 @@ var TOOL_SPECS = [
|
|
|
28562
28657
|
requires: []
|
|
28563
28658
|
},
|
|
28564
28659
|
uid: { summary: "the UID of one file", requires: ["resourcePath"] },
|
|
28565
|
-
refresh_uids: {
|
|
28660
|
+
refresh_uids: {
|
|
28661
|
+
summary: "import the project so every script and shader has its .uid sidecar, and name under uidsCreated the ones this made and under stillWithoutUid the ones the engine would not import: it writes no scene and no script, so a project whose sidecars are all present is left untouched",
|
|
28662
|
+
requires: []
|
|
28663
|
+
},
|
|
28566
28664
|
refresh_classes: {
|
|
28567
28665
|
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
|
|
28568
28666
|
requires: []
|
|
@@ -28803,7 +28901,7 @@ var TOOL_SPECS = [
|
|
|
28803
28901
|
},
|
|
28804
28902
|
{
|
|
28805
28903
|
name: "script_diagnostics",
|
|
28806
|
-
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running.",
|
|
28904
|
+
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Any diagnostic saying a member is not present on an inferred type is checked against the file the class cache points at, and one the file contradicts is named under contradictedByTheFile with staleAnalysis saying what to do: the language server hands dependents the type it analysed at startup, so a method added to an existing class_name is reported missing at every caller until the editor restarts.",
|
|
28807
28905
|
parameters: {
|
|
28808
28906
|
projectPath: PROJECT_PATH,
|
|
28809
28907
|
scriptPath: SCRIPT_PATH
|
|
@@ -29970,6 +30068,9 @@ class GodotServer {
|
|
|
29970
30068
|
return { ok: true, op };
|
|
29971
30069
|
}
|
|
29972
30070
|
async dispatch(tool, op, args) {
|
|
30071
|
+
if (ENGINE_PASSES[tool]?.[op] !== undefined) {
|
|
30072
|
+
return await this.handleRefreshUids(args);
|
|
30073
|
+
}
|
|
29973
30074
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
29974
30075
|
if (headless !== undefined) {
|
|
29975
30076
|
const { from: asked, ...answerable } = args;
|
|
@@ -30340,6 +30441,33 @@ class GodotServer {
|
|
|
30340
30441
|
const { op: _op, projectPath: _projectPath, ...params } = contained.value;
|
|
30341
30442
|
return this.answer(await this.operation(operation, params, project.value.path));
|
|
30342
30443
|
}
|
|
30444
|
+
async handleRefreshUids(args) {
|
|
30445
|
+
const project = this.project(args);
|
|
30446
|
+
if (!project.ok) {
|
|
30447
|
+
return project.response;
|
|
30448
|
+
}
|
|
30449
|
+
const engine = await this.engine();
|
|
30450
|
+
if (!engine.ok) {
|
|
30451
|
+
return this.createErrorResponse("No Godot executable found.");
|
|
30452
|
+
}
|
|
30453
|
+
const projectPath = project.value.path;
|
|
30454
|
+
const before = scriptsWithoutUid(projectPath);
|
|
30455
|
+
const imported = await runImport(engine.value, projectPath);
|
|
30456
|
+
if (!imported.ok) {
|
|
30457
|
+
return this.answer(imported);
|
|
30458
|
+
}
|
|
30459
|
+
const after = scriptsWithoutUid(projectPath);
|
|
30460
|
+
const given = before.filter((script) => !after.includes(script));
|
|
30461
|
+
return this.answer({
|
|
30462
|
+
ok: true,
|
|
30463
|
+
messages: imported.messages,
|
|
30464
|
+
payload: {
|
|
30465
|
+
uidsCreated: given,
|
|
30466
|
+
stillWithoutUid: after,
|
|
30467
|
+
note: after.length > 0 ? `${after.length} still have no .uid, which is the engine declining to import them rather than this op skipping them. No scene was written.` : "No scene was written: this reads the project and imports it, and does not resave anything."
|
|
30468
|
+
}
|
|
30469
|
+
});
|
|
30470
|
+
}
|
|
30343
30471
|
async operation(operation, params, projectPath) {
|
|
30344
30472
|
const engine = await this.engine();
|
|
30345
30473
|
if (!engine.ok) {
|
|
@@ -30674,14 +30802,44 @@ class GodotServer {
|
|
|
30674
30802
|
const severity = asParams(entry)["severity"];
|
|
30675
30803
|
return severity === 1 || severity === "error" || severity === "ERROR";
|
|
30676
30804
|
}).length;
|
|
30805
|
+
const contradicted = this.contradictedByTheFile(diagnostics, args);
|
|
30677
30806
|
return this.jsonTextResponse(markIfStale({
|
|
30678
30807
|
scriptPath: readString(args, "scriptPath"),
|
|
30679
30808
|
clean: errors === 0,
|
|
30680
30809
|
errors,
|
|
30681
30810
|
warnings: diagnostics.length - errors,
|
|
30682
|
-
diagnostics
|
|
30811
|
+
diagnostics,
|
|
30812
|
+
...contradicted.length === 0 ? {} : {
|
|
30813
|
+
contradictedByTheFile: contradicted,
|
|
30814
|
+
staleAnalysis: `The editor is reporting against an older copy of ${contradicted.length === 1 ? "a type" : "some types"} named above. Each member listed is declared in the file the class cache points at, so those diagnostics are wrong however the code is written. editor_launch restart clears it; editor_rescan and project_import refresh_classes do not, because neither reaches the analysed types the language server built at startup.`
|
|
30815
|
+
}
|
|
30683
30816
|
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
30684
30817
|
}
|
|
30818
|
+
contradictedByTheFile(diagnostics, args) {
|
|
30819
|
+
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
30820
|
+
if (!messages.some((message) => missingMemberIn(message) !== null)) {
|
|
30821
|
+
return [];
|
|
30822
|
+
}
|
|
30823
|
+
const project = this.project(args);
|
|
30824
|
+
if (!project.ok) {
|
|
30825
|
+
return [];
|
|
30826
|
+
}
|
|
30827
|
+
const classes = cachedClasses(project.value.path);
|
|
30828
|
+
if (classes === null) {
|
|
30829
|
+
return [];
|
|
30830
|
+
}
|
|
30831
|
+
return contradictedDiagnostics(messages, classes, (resourcePath) => {
|
|
30832
|
+
const contained = resolveWithinProject(project.value.path, resourcePath);
|
|
30833
|
+
if (!contained.ok || !existsSync9(contained.absolutePath)) {
|
|
30834
|
+
return null;
|
|
30835
|
+
}
|
|
30836
|
+
try {
|
|
30837
|
+
return readFileSync11(contained.absolutePath, "utf8");
|
|
30838
|
+
} catch {
|
|
30839
|
+
return null;
|
|
30840
|
+
}
|
|
30841
|
+
});
|
|
30842
|
+
}
|
|
30685
30843
|
async handleLSP(toolName, args) {
|
|
30686
30844
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
30687
30845
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.14",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|