gdharness 1.2.35 → 1.2.36
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 +166 -34
- package/build/godot/addons/gdharness_runtime/call_errors.gd +7 -2
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +9 -5
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +33 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +12 -4
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +7 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +113 -3
- package/build/godot/operations/classdb_queries.gd +22 -0
- package/build/godot/operations/dependencies.gd +90 -20
- package/build/godot/operations/gdscript_analysis.gd +19 -0
- package/build/godot/operations/godot_operations.gd +2 -1
- package/build/godot/operations/rename_checks.gd +0 -19
- package/build/godot/operations/script_check.gd +98 -0
- package/build/index.js +163 -33
- package/package.json +1 -1
|
@@ -26,8 +26,13 @@ const LOOKS_APART: int = 9
|
|
|
26
26
|
## two looks and a wait for 32 never sees it.
|
|
27
27
|
const COMPARES: Array[String] = ["==", "!=", ">=", "<=", ">", "<"]
|
|
28
28
|
|
|
29
|
+
## The most signal watches kept at once; arming one past this lets the oldest go.
|
|
30
|
+
const MOST_WATCHES: int = 16
|
|
31
|
+
|
|
29
32
|
var _host: Node
|
|
30
33
|
var _values: Values
|
|
34
|
+
var _watches: Dictionary[int, Watch] = {}
|
|
35
|
+
var _next_watch: int = 1
|
|
31
36
|
|
|
32
37
|
|
|
33
38
|
func _init(host: Node, values: Values) -> void:
|
|
@@ -56,13 +61,16 @@ func wait_frames(params: Dictionary) -> Dictionary:
|
|
|
56
61
|
return {"type": "waited", "frames": frames, "elapsed_ms": Time.get_ticks_msec() - started}
|
|
57
62
|
|
|
58
63
|
|
|
59
|
-
## Waits for a signal to fire, and answers with what it carried, or that the time ran out.
|
|
64
|
+
## Waits for a signal to fire, and answers with what it carried, or that the time ran out. Given a
|
|
65
|
+
## watch, collects that instead: see [method watch_signal].
|
|
60
66
|
func wait_signal(params: Dictionary) -> Dictionary:
|
|
61
67
|
var node_path: String = str(params.get("path", ""))
|
|
62
68
|
var signal_name: String = str(params.get("signal", ""))
|
|
63
69
|
var timeout_ms: int = Read.as_int(params.get("timeout_ms", 5000), 5000)
|
|
64
70
|
if timeout_ms < 1 or timeout_ms > CEILING_MSEC:
|
|
65
71
|
return _out_of_range("timeout_ms", timeout_ms, 1, CEILING_MSEC)
|
|
72
|
+
if params.has("watch"):
|
|
73
|
+
return await _collect(Read.as_int(params["watch"], 0), timeout_ms)
|
|
66
74
|
if node_path.is_empty() or signal_name.is_empty():
|
|
67
75
|
return {"type": "error", "message": "Node path and signal name required"}
|
|
68
76
|
|
|
@@ -84,7 +92,7 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
84
92
|
if not catcher.fired and is_instance_valid(node) and node.is_connected(signal_name, callable):
|
|
85
93
|
node.disconnect(signal_name, callable)
|
|
86
94
|
|
|
87
|
-
|
|
95
|
+
var answer: Dictionary = {
|
|
88
96
|
"type": "signal",
|
|
89
97
|
"path": node_path,
|
|
90
98
|
"signal": signal_name,
|
|
@@ -92,6 +100,90 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
92
100
|
"args": _values.serialize(catcher.args),
|
|
93
101
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
94
102
|
}
|
|
103
|
+
if not catcher.fired:
|
|
104
|
+
answer["note"] = (
|
|
105
|
+
"If a call meant to set the signal off was sent beside this wait, it may have run only after"
|
|
106
|
+
+ " the wait answered, as a client can run its calls in turn: arm runtime_wait watch first,"
|
|
107
|
+
+ " make the call, then wait with signal and that watch"
|
|
108
|
+
)
|
|
109
|
+
return answer
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## Starts recording a signal and answers at once with the watch to collect it by.
|
|
113
|
+
##
|
|
114
|
+
## A wait sent beside the call meant to set its signal off does not see it when the client runs the
|
|
115
|
+
## two in turn: Claude Code ran a key press only after the signal wait beside it had answered, so the
|
|
116
|
+
## wait ran out and the press landed after it. A watch is armed before the call and collected after.
|
|
117
|
+
func watch_signal(params: Dictionary) -> Dictionary:
|
|
118
|
+
var node_path: String = str(params.get("path", ""))
|
|
119
|
+
var signal_name: String = str(params.get("signal", ""))
|
|
120
|
+
if node_path.is_empty() or signal_name.is_empty():
|
|
121
|
+
return {"type": "error", "message": "Node path and signal name required"}
|
|
122
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
123
|
+
if standing.has("message"):
|
|
124
|
+
return standing
|
|
125
|
+
var node: Node = standing["node"]
|
|
126
|
+
if not node.has_signal(signal_name):
|
|
127
|
+
return {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
|
|
128
|
+
var watch: Watch = Watch.new(node, node_path, signal_name)
|
|
129
|
+
var listening: Error = node.connect(signal_name, watch.catcher._on_fired)
|
|
130
|
+
if listening != OK:
|
|
131
|
+
return {"type": "error", "message": "Could not listen to signal: " + signal_name}
|
|
132
|
+
while _watches.size() >= MOST_WATCHES:
|
|
133
|
+
var oldest: int = _watches.keys().min()
|
|
134
|
+
_let_go(oldest)
|
|
135
|
+
var id: int = _next_watch
|
|
136
|
+
_next_watch += 1
|
|
137
|
+
_watches[id] = watch
|
|
138
|
+
return {"type": "watching", "watch": id, "path": node_path, "signal": signal_name}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
## Answers at once with the first emission recorded since watch [param id] was armed, otherwise
|
|
142
|
+
## waits for the next until the time runs out. The watch goes either way.
|
|
143
|
+
func _collect(id: int, timeout_ms: int) -> Dictionary:
|
|
144
|
+
if not _watches.has(id):
|
|
145
|
+
return {
|
|
146
|
+
"type": "error",
|
|
147
|
+
"message":
|
|
148
|
+
(
|
|
149
|
+
(
|
|
150
|
+
"No watch %d is armed in this game: it was collected already, %d armed after it let it"
|
|
151
|
+
+ " go, or the game started again since"
|
|
152
|
+
)
|
|
153
|
+
% [id, MOST_WATCHES]
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
var watch: Watch = _watches[id]
|
|
157
|
+
var started: int = Time.get_ticks_msec()
|
|
158
|
+
var before: bool = watch.catcher.fired
|
|
159
|
+
while not watch.catcher.fired and Time.get_ticks_msec() - started < timeout_ms:
|
|
160
|
+
await _host.get_tree().process_frame
|
|
161
|
+
_let_go(id)
|
|
162
|
+
var answer: Dictionary = {
|
|
163
|
+
"type": "signal",
|
|
164
|
+
"path": watch.node_path,
|
|
165
|
+
"signal": watch.signal_name,
|
|
166
|
+
"watch": id,
|
|
167
|
+
"fired": watch.catcher.fired,
|
|
168
|
+
"times": watch.catcher.times,
|
|
169
|
+
"args": _values.serialize(watch.catcher.args),
|
|
170
|
+
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
171
|
+
}
|
|
172
|
+
if before:
|
|
173
|
+
answer["fired_before_wait"] = true
|
|
174
|
+
return answer
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
func _let_go(id: int) -> void:
|
|
178
|
+
var watch: Watch = _watches.get(id)
|
|
179
|
+
var _gone: bool = _watches.erase(id)
|
|
180
|
+
if watch == null:
|
|
181
|
+
return
|
|
182
|
+
var held: Object = watch.node.get_ref()
|
|
183
|
+
var node: Node = held as Node
|
|
184
|
+
var callable: Callable = watch.catcher._on_fired
|
|
185
|
+
if node != null and node.is_connected(watch.signal_name, callable):
|
|
186
|
+
node.disconnect(watch.signal_name, callable)
|
|
95
187
|
|
|
96
188
|
|
|
97
189
|
## Waits until a property reads as the value given, or until something under the path says what
|
|
@@ -492,8 +584,26 @@ func _anything_says(node_path: String, said: String, include_hidden: bool) -> bo
|
|
|
492
584
|
class SignalCatcher:
|
|
493
585
|
extends RefCounted
|
|
494
586
|
var fired: bool = false
|
|
587
|
+
## How many times it fired; [member args] holds what the first carried.
|
|
588
|
+
var times: int = 0
|
|
495
589
|
var args: Array = []
|
|
496
590
|
|
|
497
591
|
func _on_fired(...carried: Array) -> void:
|
|
592
|
+
if not fired:
|
|
593
|
+
args = carried
|
|
498
594
|
fired = true
|
|
499
|
-
|
|
595
|
+
times += 1
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
## A signal recorded from when it was armed until it is collected.
|
|
599
|
+
class Watch:
|
|
600
|
+
extends RefCounted
|
|
601
|
+
var node: WeakRef
|
|
602
|
+
var node_path: String
|
|
603
|
+
var signal_name: String
|
|
604
|
+
var catcher: SignalCatcher = SignalCatcher.new()
|
|
605
|
+
|
|
606
|
+
func _init(watched: Node, path: String, named: String) -> void:
|
|
607
|
+
node = weakref(watched)
|
|
608
|
+
node_path = path
|
|
609
|
+
signal_name = named
|
|
@@ -21,6 +21,17 @@ const CATEGORY_BASES: Dictionary = {
|
|
|
21
21
|
"visual": ["VisualInstance3D"],
|
|
22
22
|
"animation": ["AnimationMixer"],
|
|
23
23
|
}
|
|
24
|
+
## The engine's MethodFlags, each by the name an answer gives it.
|
|
25
|
+
const METHOD_FLAG_NAMES: Dictionary[int, String] = {
|
|
26
|
+
METHOD_FLAG_NORMAL: "normal",
|
|
27
|
+
METHOD_FLAG_EDITOR: "editor",
|
|
28
|
+
METHOD_FLAG_CONST: "const",
|
|
29
|
+
METHOD_FLAG_VIRTUAL: "virtual",
|
|
30
|
+
METHOD_FLAG_VARARG: "vararg",
|
|
31
|
+
METHOD_FLAG_STATIC: "static",
|
|
32
|
+
METHOD_FLAG_OBJECT_CORE: "object_core",
|
|
33
|
+
METHOD_FLAG_VIRTUAL_REQUIRED: "virtual_required",
|
|
34
|
+
}
|
|
24
35
|
|
|
25
36
|
var _log: Log
|
|
26
37
|
var _values: Serialisation = Serialisation.new()
|
|
@@ -333,6 +344,7 @@ func _methods_of(class_name_str: String, include_inherited: bool) -> Array[Dicti
|
|
|
333
344
|
"class_name": return_info.get("class_name", "")
|
|
334
345
|
},
|
|
335
346
|
"flags": m.get("flags", 0),
|
|
347
|
+
"flag_names": _flag_names(Read.as_int(m.get("flags", 0))),
|
|
336
348
|
# As values, tagged the way every other answer tags one: JSON writes a Vector2i it is
|
|
337
349
|
# handed as its text, so TileMapLayer.set_cell's default atlas coords read "(-1, -1)".
|
|
338
350
|
"default_args": _values.serialize_value(m.get("default_args", []))
|
|
@@ -343,6 +355,16 @@ func _methods_of(class_name_str: String, include_inherited: bool) -> Array[Dicti
|
|
|
343
355
|
return methods
|
|
344
356
|
|
|
345
357
|
|
|
358
|
+
# The MethodFlags set in [param flags] by name, beside the number the engine reports, which reads as
|
|
359
|
+
# nothing without its enum: 1 is normal, and a const method is 5.
|
|
360
|
+
static func _flag_names(flags: int) -> Array[String]:
|
|
361
|
+
var named: Array[String] = []
|
|
362
|
+
for flag: int in METHOD_FLAG_NAMES:
|
|
363
|
+
if flags & flag:
|
|
364
|
+
named.append(METHOD_FLAG_NAMES[flag])
|
|
365
|
+
return named
|
|
366
|
+
|
|
367
|
+
|
|
346
368
|
# What [param info]'s type is called: its class for an object of one, and otherwise the engine's own
|
|
347
369
|
# name for the type, beside the number the engine reports, which reads as nothing without its enum.
|
|
348
370
|
# Nil is a value that can be anything when the usage says so, and [param nil_name] otherwise: void
|
|
@@ -23,6 +23,11 @@ const REFERENCE_PATTERNS: Array[String] = [
|
|
|
23
23
|
|
|
24
24
|
var _log: Log
|
|
25
25
|
var _files: FileWalk = FileWalk.new()
|
|
26
|
+
## The project's class names and the scripts declaring them, read the first time a script is walked
|
|
27
|
+
## and kept for the call: walking every resource starts a walk per root, and each reading every
|
|
28
|
+
## script again made the whole-project answer quadratic in the number of scripts.
|
|
29
|
+
var _classes_declared: Dictionary = {}
|
|
30
|
+
var _classes_read: bool = false
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
# Everything one dependency walk carries: the settings it was started with, and the state it
|
|
@@ -91,12 +96,14 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
91
96
|
|
|
92
97
|
total_resources = all_resources.size()
|
|
93
98
|
|
|
94
|
-
var
|
|
99
|
+
var references: int = 0
|
|
100
|
+
var distinct: Dictionary = {}
|
|
95
101
|
for key: String in dependencies:
|
|
96
102
|
# Through a typed local: what comes out of a Dictionary is Variant, and a project that
|
|
97
103
|
# errors on handing one to a typed parameter will not compile this script at all.
|
|
98
104
|
var walked: Array[Dictionary] = dependencies[key]
|
|
99
|
-
|
|
105
|
+
references += _count_recursive(walked)
|
|
106
|
+
_gather_paths(walked, key, distinct)
|
|
100
107
|
|
|
101
108
|
return {
|
|
102
109
|
"dependencies": dependencies,
|
|
@@ -104,7 +111,10 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
104
111
|
"summary":
|
|
105
112
|
{
|
|
106
113
|
"total_resources": total_resources,
|
|
107
|
-
|
|
114
|
+
# The files depended on, each once. The tree names a file again under everything that
|
|
115
|
+
# loads it, and counting its entries answered 11 for a scene with six dependencies.
|
|
116
|
+
"total_dependencies": distinct.size(),
|
|
117
|
+
"total_references": references,
|
|
108
118
|
"circular_count": circular_references.size()
|
|
109
119
|
}
|
|
110
120
|
}
|
|
@@ -332,9 +342,24 @@ func _regex_escaped(text: String) -> String:
|
|
|
332
342
|
return escaped
|
|
333
343
|
|
|
334
344
|
|
|
345
|
+
# [param root] is left out: a cycle leads back to it, and it is not one of its own dependencies.
|
|
346
|
+
func _gather_paths(deps: Array[Dictionary], root: String, into: Dictionary) -> void:
|
|
347
|
+
for dep: Dictionary in deps:
|
|
348
|
+
var path: String = str(dep.get("path", ""))
|
|
349
|
+
if path != root:
|
|
350
|
+
into[path] = true
|
|
351
|
+
if dep.has("dependencies"):
|
|
352
|
+
var deeper: Array[Dictionary] = dep["dependencies"]
|
|
353
|
+
_gather_paths(deeper, root, into)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# A circular entry is not counted: it marks that the entry holding it was reached again, which that
|
|
357
|
+
# entry already counts as one reference.
|
|
335
358
|
func _count_recursive(deps: Array[Dictionary]) -> int:
|
|
336
|
-
var count: int =
|
|
359
|
+
var count: int = 0
|
|
337
360
|
for dep: Dictionary in deps:
|
|
361
|
+
if not dep.get("circular", false):
|
|
362
|
+
count += 1
|
|
338
363
|
if dep.has("dependencies"):
|
|
339
364
|
var deeper: Array[Dictionary] = dep["dependencies"]
|
|
340
365
|
count += _count_recursive(deeper)
|
|
@@ -390,29 +415,74 @@ func _analyze_resource(path: String, current_depth: int, walk: DependencyWalk) -
|
|
|
390
415
|
if not walk.include_built_in and dep_path.begins_with("res://."):
|
|
391
416
|
continue
|
|
392
417
|
|
|
393
|
-
|
|
394
|
-
continue
|
|
395
|
-
|
|
396
|
-
var dep_info: Dictionary = {"path": dep_path, "exists": FileAccess.file_exists(dep_path)}
|
|
397
|
-
|
|
398
|
-
if dep_info["exists"] and current_depth + 1 < walk.max_depth:
|
|
399
|
-
var sub_deps: Array[Dictionary] = _analyze_resource(dep_path, current_depth + 1, walk)
|
|
400
|
-
if sub_deps.size() > 0:
|
|
401
|
-
dep_info["dependencies"] = sub_deps
|
|
418
|
+
_depend(deps, path, dep_path, "", current_depth, walk)
|
|
402
419
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
420
|
+
# A script uses another by its class_name as surely as by a preload, and only the reverse walk
|
|
421
|
+
# looked for that: asked what a script depends on, the answer left out the script it typed a
|
|
422
|
+
# variable with, while asking what used that script named this one.
|
|
423
|
+
if path.get_extension() == "gd":
|
|
424
|
+
var classes: Dictionary = classes_declared()
|
|
425
|
+
for used: String in classes_used_in(content, classes):
|
|
426
|
+
_depend(deps, path, str(classes[used]), used, current_depth, walk)
|
|
410
427
|
|
|
411
428
|
walk.path_stack.pop_back()
|
|
412
429
|
walk.visited[path] = {"deps": deps, "remaining": remaining}
|
|
413
430
|
return deps
|
|
414
431
|
|
|
415
432
|
|
|
433
|
+
# Adds [param dep_path] to [param deps], what [param path] depends on, walked as far as the depth left
|
|
434
|
+
# allows, once however many times it is named. [param by_class] is the class name it was named by,
|
|
435
|
+
# or empty for a path.
|
|
436
|
+
func _depend(
|
|
437
|
+
deps: Array[Dictionary],
|
|
438
|
+
path: String,
|
|
439
|
+
dep_path: String,
|
|
440
|
+
by_class: String,
|
|
441
|
+
current_depth: int,
|
|
442
|
+
walk: DependencyWalk
|
|
443
|
+
) -> void:
|
|
444
|
+
if dep_path == path:
|
|
445
|
+
return
|
|
446
|
+
for existing: Dictionary in deps:
|
|
447
|
+
if existing.get("path", "") == dep_path:
|
|
448
|
+
return
|
|
449
|
+
var dep_info: Dictionary = {"path": dep_path, "exists": FileAccess.file_exists(dep_path)}
|
|
450
|
+
if not by_class.is_empty():
|
|
451
|
+
dep_info["by_class"] = by_class
|
|
452
|
+
if dep_info["exists"] and current_depth + 1 < walk.max_depth:
|
|
453
|
+
var sub_deps: Array[Dictionary] = _analyze_resource(dep_path, current_depth + 1, walk)
|
|
454
|
+
if sub_deps.size() > 0:
|
|
455
|
+
dep_info["dependencies"] = sub_deps
|
|
456
|
+
deps.append(dep_info)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
## Every class_name the project's scripts declare, with the script declaring it.
|
|
460
|
+
func classes_declared() -> Dictionary:
|
|
461
|
+
if _classes_read:
|
|
462
|
+
return _classes_declared
|
|
463
|
+
_classes_read = true
|
|
464
|
+
for script: String in _files.find_files("res://", ".gd"):
|
|
465
|
+
var declared: String = _declared_class_name(script)
|
|
466
|
+
if not declared.is_empty():
|
|
467
|
+
_classes_declared[declared] = script
|
|
468
|
+
return _classes_declared
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
## The names in [param classes] that the code in [param content] uses, in the order first used: words
|
|
472
|
+
## outside strings and comments, so a class named in prose or in a message is not a use of it.
|
|
473
|
+
static func classes_used_in(content: String, classes: Dictionary) -> Array[String]:
|
|
474
|
+
var used: Array[String] = []
|
|
475
|
+
if classes.is_empty():
|
|
476
|
+
return used
|
|
477
|
+
var word: RegEx = Patterns.compiled("[A-Za-z_][A-Za-z0-9_]*")
|
|
478
|
+
for line: String in content.split("\n"):
|
|
479
|
+
for m: RegExMatch in word.search_all(_code_of(line)):
|
|
480
|
+
var name: String = m.get_string()
|
|
481
|
+
if classes.has(name) and not name in used:
|
|
482
|
+
used.append(name)
|
|
483
|
+
return used
|
|
484
|
+
|
|
485
|
+
|
|
416
486
|
# [param written] as the res:// path the engine would load from [param from]: a uid:// by what the
|
|
417
487
|
# project's uid cache says it names, and a relative path against the referring file's directory.
|
|
418
488
|
# Both were passed over as not being res:// paths, so a script's `preload("sibling.gd")` was no
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
+
const Dependencies = preload("dependencies.gd")
|
|
3
4
|
const Patterns = preload("patterns.gd")
|
|
4
5
|
const Read = preload("reading.gd")
|
|
5
6
|
const Log = preload("logger.gd")
|
|
6
7
|
|
|
7
8
|
var _log: Log
|
|
9
|
+
## Kept for the call, so the project's class names are read once however many bases are walked.
|
|
10
|
+
var _dependencies: Dependencies = null
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
func _init(p_log: Log) -> void:
|
|
@@ -136,6 +139,21 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
136
139
|
# its first function would otherwise be judged against the default.
|
|
137
140
|
_mark_virtuals(functions, extends_name, full_script_path)
|
|
138
141
|
|
|
142
|
+
# A script typed by its class_name is depended on as surely as one preloaded, and the answer
|
|
143
|
+
# listed only the preloads: a `var stats: Stats` left stats.gd out, while project_dependencies
|
|
144
|
+
# named it. The same reading as that tool's, so the two agree.
|
|
145
|
+
var classes_used: Array[Dictionary] = []
|
|
146
|
+
if _dependencies == null:
|
|
147
|
+
_dependencies = Dependencies.new(_log)
|
|
148
|
+
var declared_by: Dictionary = _dependencies.classes_declared()
|
|
149
|
+
for used: String in Dependencies.classes_used_in(content, declared_by):
|
|
150
|
+
var declaring: String = str(declared_by[used])
|
|
151
|
+
if declaring == full_script_path:
|
|
152
|
+
continue
|
|
153
|
+
classes_used.append({"name": used, "path": declaring})
|
|
154
|
+
if not declaring in dependencies:
|
|
155
|
+
dependencies.append(declaring)
|
|
156
|
+
|
|
139
157
|
var answer: Dictionary = {
|
|
140
158
|
"path": script_path,
|
|
141
159
|
"full_path": full_script_path,
|
|
@@ -148,6 +166,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
148
166
|
"enums": enums,
|
|
149
167
|
"inner_classes": inner_classes,
|
|
150
168
|
"dependencies": dependencies,
|
|
169
|
+
"classes_used": classes_used,
|
|
151
170
|
"line_count": lines.size()
|
|
152
171
|
}
|
|
153
172
|
if Read.as_bool(params.get("include_inherited", false)):
|
|
@@ -32,6 +32,7 @@ const ProjectDiagnostics = preload("project_diagnostics.gd")
|
|
|
32
32
|
const Read = preload("reading.gd")
|
|
33
33
|
const RenameChecks = preload("rename_checks.gd")
|
|
34
34
|
const ResourceFiles = preload("resource_files.gd")
|
|
35
|
+
const ScriptCheck = preload("script_check.gd")
|
|
35
36
|
|
|
36
37
|
## Printed once the answer is written, after which the engine runs the project's autoloads. Kept in
|
|
37
38
|
## step with `ANSWERED` in src/headless.ts.
|
|
@@ -218,7 +219,7 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
218
219
|
"names_taken":
|
|
219
220
|
payload = RenameChecks.new(_log).names_taken(params)
|
|
220
221
|
"check_scripts":
|
|
221
|
-
payload =
|
|
222
|
+
payload = ScriptCheck.new(_log).check_scripts(params)
|
|
222
223
|
|
|
223
224
|
# Plugins and input
|
|
224
225
|
"list_plugins":
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
-
const GdscriptAuthoring = preload("gdscript_authoring.gd")
|
|
4
3
|
const Log = preload("logger.gd")
|
|
5
4
|
|
|
6
5
|
var _log: Log
|
|
@@ -63,21 +62,3 @@ static func _named(entries: Array[Dictionary], name: String) -> bool:
|
|
|
63
62
|
if str(entry.get("name", "")) == name:
|
|
64
63
|
return true
|
|
65
64
|
return false
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# Whether each script compiles in a fresh engine, which reads the class cache as it starts and so
|
|
69
|
-
# resolves every class the cache lists. The engine's reasons go to stderr and come back with the
|
|
70
|
-
# answer. It names the scripts it checked rather than counting them, since a caller told one was
|
|
71
|
-
# checked could not say which.
|
|
72
|
-
func check_scripts(params: Dictionary) -> Dictionary:
|
|
73
|
-
var paths: Array = params.get("paths", [])
|
|
74
|
-
var checked: Array[String] = []
|
|
75
|
-
var failed: Array[String] = []
|
|
76
|
-
for value: Variant in paths:
|
|
77
|
-
var path: String = str(value)
|
|
78
|
-
checked.append(path)
|
|
79
|
-
var script: Script = ResourceLoader.load(path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
80
|
-
if not GdscriptAuthoring._parses(script):
|
|
81
|
-
_log.error("Does not compile: " + path)
|
|
82
|
-
failed.append(path)
|
|
83
|
-
return {"checked": checked, "failed": failed}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Compiles scripts the way the engine does when it loads them, and answers what it raised.
|
|
4
|
+
##
|
|
5
|
+
## The engine reports a script that does not compile as errors raised from GDScript::reload, each
|
|
6
|
+
## naming the file and line, and they reach a Logger as well as stderr. Measured on 4.7.2: a script
|
|
7
|
+
## whose dependency does not compile reports the dependency's errors at the dependency's path and one
|
|
8
|
+
## of its own at line 0, "Compile Error: Failed to compile depended scripts", and is still handed
|
|
9
|
+
## back as a script that can_instantiate() calls instantiable. Warnings are not among them: the engine
|
|
10
|
+
## sends those only to an editor or a debugger.
|
|
11
|
+
##
|
|
12
|
+
## A script that failed stays failed in the engine's script cache, which no ResourceLoader cache mode
|
|
13
|
+
## reaches, and every script loaded after it that depends on it compiles in silence, reload() and all.
|
|
14
|
+
## So a run ends at the first script that raised anything, and the rest go to a fresh engine.
|
|
15
|
+
|
|
16
|
+
const Read = preload("reading.gd")
|
|
17
|
+
const Log = preload("logger.gd")
|
|
18
|
+
|
|
19
|
+
## Where the engine raises a script's compile errors from.
|
|
20
|
+
const RELOAD: String = "GDScript::reload"
|
|
21
|
+
|
|
22
|
+
var _log: Log
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Every compile error the engine raises while it is attached, in order.
|
|
26
|
+
class Caught:
|
|
27
|
+
extends Logger
|
|
28
|
+
|
|
29
|
+
var raised: Array[Dictionary] = []
|
|
30
|
+
|
|
31
|
+
func _log_message(_message: String, _error: bool) -> void:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
# The engine's own signature for a Logger, eight parameters and not this project's to shape.
|
|
35
|
+
# gdlint:ignore = function-arguments-number
|
|
36
|
+
func _log_error(
|
|
37
|
+
function: String,
|
|
38
|
+
file: String,
|
|
39
|
+
line: int,
|
|
40
|
+
code: String,
|
|
41
|
+
rationale: String,
|
|
42
|
+
_editor_notify: bool,
|
|
43
|
+
_error_type: int,
|
|
44
|
+
_script_backtraces: Array[ScriptBacktrace],
|
|
45
|
+
) -> void:
|
|
46
|
+
if function != RELOAD:
|
|
47
|
+
return
|
|
48
|
+
raised.append(
|
|
49
|
+
{"path": file, "line": line, "message": rationale if not rationale.is_empty() else code}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
func _init(p_log: Log) -> void:
|
|
54
|
+
_log = p_log
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Whether the scripts in [code]paths[/code] compile in a fresh engine, which reads the class cache
|
|
58
|
+
## as it starts and so resolves every class the cache lists, up to and including the first that
|
|
59
|
+
## raised an error. [code]checked[/code] names the scripts read, in order, and [code]failed[/code]
|
|
60
|
+
## those that did not compile; [code]scripts[/code] holds each failed one with the errors raised in
|
|
61
|
+
## its own file, and the other scripts that failed to compile while it was loaded. Read past the
|
|
62
|
+
## resource cache, so a script loaded for an earlier one is compiled again for its own entry.
|
|
63
|
+
func check_scripts(params: Dictionary) -> Dictionary:
|
|
64
|
+
var asked: Array = params.get("paths", [])
|
|
65
|
+
var caught: Caught = Caught.new()
|
|
66
|
+
OS.add_logger(caught)
|
|
67
|
+
var checked: Array[String] = []
|
|
68
|
+
var failed: Array[String] = []
|
|
69
|
+
var scripts: Array[Dictionary] = []
|
|
70
|
+
for entry: Variant in asked:
|
|
71
|
+
var path: String = str(entry)
|
|
72
|
+
checked.append(path)
|
|
73
|
+
caught.raised.clear()
|
|
74
|
+
var loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
75
|
+
var errors: Array[Dictionary] = []
|
|
76
|
+
var others: Array[String] = []
|
|
77
|
+
for error: Dictionary in caught.raised:
|
|
78
|
+
var at: String = str(error["path"])
|
|
79
|
+
if at != path:
|
|
80
|
+
if not at in others:
|
|
81
|
+
others.append(at)
|
|
82
|
+
continue
|
|
83
|
+
var found: Dictionary = {"line": Read.as_int(error["line"]), "message": str(error["message"])}
|
|
84
|
+
if not found in errors:
|
|
85
|
+
errors.append(found)
|
|
86
|
+
if loaded == null and errors.is_empty():
|
|
87
|
+
errors.append({"line": 0, "message": "The engine could not load it as a script."})
|
|
88
|
+
if errors.is_empty() and others.is_empty():
|
|
89
|
+
continue
|
|
90
|
+
if not errors.is_empty():
|
|
91
|
+
failed.append(path)
|
|
92
|
+
var one: Dictionary = {"path": path, "errors": errors}
|
|
93
|
+
if not others.is_empty():
|
|
94
|
+
one["dependencies_with_errors"] = others
|
|
95
|
+
scripts.append(one)
|
|
96
|
+
break
|
|
97
|
+
OS.remove_logger(caught)
|
|
98
|
+
return {"checked": checked, "failed": failed, "scripts": scripts}
|