gdharness 1.3.43 → 1.3.45
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 +607 -511
- package/build/godot/addons/gdharness_editor/reading.gd +14 -5
- package/build/godot/addons/gdharness_editor/scene_file.gd +18 -1
- package/build/godot/addons/gdharness_editor/setting_reads.gd +42 -1
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +31 -12
- package/build/godot/addons/gdharness_runtime/reading.gd +14 -5
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +13 -0
- package/build/godot/operations/dependencies.gd +154 -85
- package/build/godot/operations/editor_tools.gd +5 -4
- package/build/godot/operations/gdscript_analysis.gd +45 -8
- package/build/godot/operations/patterns.gd +6 -0
- package/build/godot/operations/project_config.gd +8 -3
- package/build/godot/operations/project_validation.gd +3 -4
- package/build/godot/operations/reading.gd +14 -5
- package/build/godot/operations/setting_reads.gd +42 -1
- package/build/keeper.js +43 -43
- package/package.json +1 -1
|
@@ -312,16 +312,25 @@ static func _bare(word: String) -> String:
|
|
|
312
312
|
return word.to_lower().replace("_", "").replace(" ", "")
|
|
313
313
|
|
|
314
314
|
|
|
315
|
-
## The signals [param object] has that a caller can connect, its own class's first:
|
|
316
|
-
##
|
|
317
|
-
##
|
|
315
|
+
## The signals [param object] has that a caller can connect, its own class's first: its script's,
|
|
316
|
+
## as declared, then the engine's from the class down to Object. The engine lists its own from
|
|
317
|
+
## Object down, which puts a Button's pressed after thirty a Control has, and its script's ahead of
|
|
318
|
+
## those, so reversing the whole list put a game's own two signals after twenty-four of the
|
|
319
|
+
## engine's. Names starting with an underscore are the engine's own.
|
|
318
320
|
static func signals_of(object: Object) -> Array:
|
|
321
|
+
var declared: Array = []
|
|
322
|
+
var script: Script = object.get_script()
|
|
323
|
+
if script != null:
|
|
324
|
+
for entry: Dictionary in script.get_script_signal_list():
|
|
325
|
+
var name: String = str(entry["name"])
|
|
326
|
+
if not name.begins_with("_") and not name in declared:
|
|
327
|
+
declared.append(name)
|
|
319
328
|
var names: Array = []
|
|
320
329
|
for entry: Dictionary in object.get_signal_list():
|
|
321
330
|
var name: String = str(entry["name"])
|
|
322
|
-
if not name.begins_with("_"):
|
|
331
|
+
if not name.begins_with("_") and not name in declared:
|
|
323
332
|
names.push_front(name)
|
|
324
|
-
return names
|
|
333
|
+
return declared + names
|
|
325
334
|
|
|
326
335
|
|
|
327
336
|
## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
|
|
@@ -526,10 +526,27 @@ func unusable_name(parent: Node, child_name: String, moving: Node = null) -> Str
|
|
|
526
526
|
if taken != null and taken != moving and taken.get_parent() == parent:
|
|
527
527
|
# The root by its name, since its path is ".", and ". already has a child" read as a typo.
|
|
528
528
|
var holder: String = "The root, %s," % parent.name if parent == root else path_of(parent)
|
|
529
|
-
return
|
|
529
|
+
return (
|
|
530
|
+
"%s already has a child named %s; %s is free there."
|
|
531
|
+
% [holder, child_name, _free_name(parent, child_name)]
|
|
532
|
+
)
|
|
530
533
|
return ""
|
|
531
534
|
|
|
532
535
|
|
|
536
|
+
## The first name after [param taken] that no child of [param parent] has, numbered as Godot numbers
|
|
537
|
+
## a duplicate: Crate2 after Crate, Crate4 after Crate3 when that is taken too. The refusal alone
|
|
538
|
+
## left a caller guessing at a name that might be taken as well.
|
|
539
|
+
static func _free_name(parent: Node, taken: String) -> String:
|
|
540
|
+
var digits: int = 0
|
|
541
|
+
while digits < taken.length() and taken[taken.length() - 1 - digits] in "0123456789":
|
|
542
|
+
digits += 1
|
|
543
|
+
var base: String = taken.left(taken.length() - digits)
|
|
544
|
+
var at: int = 2 if digits == 0 else taken.right(digits).to_int() + 1
|
|
545
|
+
while parent.has_node(NodePath(base + str(at))):
|
|
546
|
+
at += 1
|
|
547
|
+
return base + str(at)
|
|
548
|
+
|
|
549
|
+
|
|
533
550
|
## The properties this scene's own file sets on [param node], each with the value the file gives it.
|
|
534
551
|
func set_here(node: Node) -> Dictionary:
|
|
535
552
|
return set_at(path_of(node))
|
|
@@ -62,7 +62,7 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
|
|
|
62
62
|
var value: Variant = ProjectSettings.get_setting(name)
|
|
63
63
|
var described: Dictionary = {
|
|
64
64
|
"setting": name,
|
|
65
|
-
"type":
|
|
65
|
+
"type": type_named(kind, value),
|
|
66
66
|
"value": values.serialize_value(value),
|
|
67
67
|
}
|
|
68
68
|
# As a single get says it, so a family read can be told apart into what the project sets and
|
|
@@ -81,6 +81,47 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
|
|
|
81
81
|
return { "prefix": prefix, "count": found.size(), "settings": found }
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
## The type [param kind] names, with what [param value] holds when it is a typed array or
|
|
85
|
+
## dictionary, as project.godot writes it: a `Dictionary[String, bool]` was answered as Dictionary,
|
|
86
|
+
## which hides what its keys and values must be.
|
|
87
|
+
static func type_named(kind: int, value: Variant) -> String:
|
|
88
|
+
if kind == TYPE_ARRAY and value is Array:
|
|
89
|
+
var list: Array = value
|
|
90
|
+
if list.is_typed():
|
|
91
|
+
var element: String = _held(
|
|
92
|
+
list.get_typed_builtin(), list.get_typed_class_name(), list.get_typed_script()
|
|
93
|
+
)
|
|
94
|
+
return "Array[%s]" % element
|
|
95
|
+
elif kind == TYPE_DICTIONARY and value is Dictionary:
|
|
96
|
+
var table: Dictionary = value
|
|
97
|
+
if table.is_typed():
|
|
98
|
+
var key: String = _held(
|
|
99
|
+
table.get_typed_key_builtin(),
|
|
100
|
+
table.get_typed_key_class_name(),
|
|
101
|
+
table.get_typed_key_script()
|
|
102
|
+
)
|
|
103
|
+
var held: String = _held(
|
|
104
|
+
table.get_typed_value_builtin(),
|
|
105
|
+
table.get_typed_value_class_name(),
|
|
106
|
+
table.get_typed_value_script()
|
|
107
|
+
)
|
|
108
|
+
return "Dictionary[%s, %s]" % [key, held]
|
|
109
|
+
return type_string(kind)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## One side of a typed collection: a built-in type, a class, or a script by its class_name.
|
|
113
|
+
static func _held(builtin: int, named: StringName, script: Variant) -> String:
|
|
114
|
+
if builtin == TYPE_NIL:
|
|
115
|
+
return "Variant"
|
|
116
|
+
if builtin != TYPE_OBJECT:
|
|
117
|
+
return type_string(builtin)
|
|
118
|
+
if script is Script:
|
|
119
|
+
var declared: Script = script
|
|
120
|
+
if not declared.get_global_name().is_empty():
|
|
121
|
+
return declared.get_global_name()
|
|
122
|
+
return str(named)
|
|
123
|
+
|
|
124
|
+
|
|
84
125
|
## The setting [param setting_path] overrides for builds with one feature, as
|
|
85
126
|
## `application/run/max_fps.mobile` does `application/run/max_fps`, or "" for a plain setting.
|
|
86
127
|
static func override_base(setting_path: String) -> String:
|
|
@@ -51,7 +51,7 @@ func reload_script(args: Dictionary) -> Dictionary:
|
|
|
51
51
|
var asked: String = str(args.get("scriptPath", ""))
|
|
52
52
|
if asked.is_empty():
|
|
53
53
|
return { "ok": false, "error": "scriptPath is required" }
|
|
54
|
-
var path: String = _spelled_as_on_disk(asked)
|
|
54
|
+
var path: String = _spelled_as_on_disk(asked, {})
|
|
55
55
|
if not ResourceLoader.exists(path):
|
|
56
56
|
return { "ok": false, "error": "No script at " + path }
|
|
57
57
|
|
|
@@ -151,8 +151,9 @@ func scripts_behind(args: Dictionary) -> Dictionary:
|
|
|
151
151
|
unsaved = EditorInterface.get_script_editor().get_unsaved_files()
|
|
152
152
|
var behind: Array[String] = []
|
|
153
153
|
var edited: Array[String] = []
|
|
154
|
+
var listings: Dictionary = {}
|
|
154
155
|
for entry: Variant in asked:
|
|
155
|
-
var path: String = _spelled_as_on_disk(str(entry))
|
|
156
|
+
var path: String = _spelled_as_on_disk(str(entry), listings)
|
|
156
157
|
var script: GDScript = ResourceLoader.get_cached_ref(path) as GDScript
|
|
157
158
|
if script == null:
|
|
158
159
|
continue
|
|
@@ -196,28 +197,46 @@ func _reread_open_tabs(path: String, answer: Dictionary) -> void:
|
|
|
196
197
|
## `res://core/rules.gd`, but the editor holds its copy under the spelling on disk, and a load by
|
|
197
198
|
## another spelling misses that copy and compiles a new one: reloading that answers with a fresh
|
|
198
199
|
## compile's members and leaves the copy the editor holds as it was.
|
|
199
|
-
|
|
200
|
+
##
|
|
201
|
+
## [param listings] keeps each directory's entries for the length of one call that spells many
|
|
202
|
+
## paths. Listed again for every path, a check of the 416 classes of one project listed a folder of
|
|
203
|
+
## eight hundred files twice per class, and held an editor's main thread for close to a minute on a
|
|
204
|
+
## CI runner, with every request behind it waiting.
|
|
205
|
+
static func _spelled_as_on_disk(path: String, listings: Dictionary) -> String:
|
|
200
206
|
var local: String = ProjectSettings.localize_path(path)
|
|
201
207
|
if not local.begins_with("res://"):
|
|
202
208
|
return local
|
|
203
209
|
var spelled: String = "res://"
|
|
204
210
|
for step: String in local.trim_prefix("res://").split("/", false):
|
|
205
|
-
|
|
206
|
-
|
|
211
|
+
if not listings.has(spelled):
|
|
212
|
+
listings[spelled] = _entries_by_spelling(spelled)
|
|
213
|
+
var entries: Variant = listings[spelled]
|
|
214
|
+
if entries == null:
|
|
207
215
|
return local
|
|
208
|
-
var
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
found = entry
|
|
212
|
-
break
|
|
213
|
-
if found.is_empty() and entry.nocasecmp_to(step) == 0:
|
|
214
|
-
found = entry
|
|
216
|
+
var exact: Dictionary = entries[0]
|
|
217
|
+
var folded: Dictionary = entries[1]
|
|
218
|
+
var found: String = step if exact.has(step) else str(folded.get(step.to_lower(), ""))
|
|
215
219
|
if found.is_empty():
|
|
216
220
|
return local
|
|
217
221
|
spelled = spelled.path_join(found)
|
|
218
222
|
return spelled
|
|
219
223
|
|
|
220
224
|
|
|
225
|
+
## The entries of [param directory] by their own spelling and by their spelling in lower case, the
|
|
226
|
+
## first of those that fold together winning, or null for a directory that cannot be opened.
|
|
227
|
+
static func _entries_by_spelling(directory: String) -> Variant:
|
|
228
|
+
var listing: DirAccess = DirAccess.open(directory)
|
|
229
|
+
if listing == null:
|
|
230
|
+
return null
|
|
231
|
+
var exact: Dictionary = {}
|
|
232
|
+
var folded: Dictionary = {}
|
|
233
|
+
for entry: String in listing.get_directories() + listing.get_files():
|
|
234
|
+
exact[entry] = true
|
|
235
|
+
if not folded.has(entry.to_lower()):
|
|
236
|
+
folded[entry.to_lower()] = entry
|
|
237
|
+
return [exact, folded]
|
|
238
|
+
|
|
239
|
+
|
|
221
240
|
func _method_names(script: GDScript) -> Array[String]:
|
|
222
241
|
var names: Array[String] = []
|
|
223
242
|
for entry: Dictionary in script.get_script_method_list():
|
|
@@ -312,16 +312,25 @@ static func _bare(word: String) -> String:
|
|
|
312
312
|
return word.to_lower().replace("_", "").replace(" ", "")
|
|
313
313
|
|
|
314
314
|
|
|
315
|
-
## The signals [param object] has that a caller can connect, its own class's first:
|
|
316
|
-
##
|
|
317
|
-
##
|
|
315
|
+
## The signals [param object] has that a caller can connect, its own class's first: its script's,
|
|
316
|
+
## as declared, then the engine's from the class down to Object. The engine lists its own from
|
|
317
|
+
## Object down, which puts a Button's pressed after thirty a Control has, and its script's ahead of
|
|
318
|
+
## those, so reversing the whole list put a game's own two signals after twenty-four of the
|
|
319
|
+
## engine's. Names starting with an underscore are the engine's own.
|
|
318
320
|
static func signals_of(object: Object) -> Array:
|
|
321
|
+
var declared: Array = []
|
|
322
|
+
var script: Script = object.get_script()
|
|
323
|
+
if script != null:
|
|
324
|
+
for entry: Dictionary in script.get_script_signal_list():
|
|
325
|
+
var name: String = str(entry["name"])
|
|
326
|
+
if not name.begins_with("_") and not name in declared:
|
|
327
|
+
declared.append(name)
|
|
319
328
|
var names: Array = []
|
|
320
329
|
for entry: Dictionary in object.get_signal_list():
|
|
321
330
|
var name: String = str(entry["name"])
|
|
322
|
-
if not name.begins_with("_"):
|
|
331
|
+
if not name.begins_with("_") and not name in declared:
|
|
323
332
|
names.push_front(name)
|
|
324
|
-
return names
|
|
333
|
+
return declared + names
|
|
325
334
|
|
|
326
335
|
|
|
327
336
|
## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
|
|
@@ -395,6 +395,19 @@ static func class_at(root: Node, path: String) -> Dictionary:
|
|
|
395
395
|
"type": "error",
|
|
396
396
|
"message": "No script is at %s to call a static function of." % script_path,
|
|
397
397
|
}
|
|
398
|
+
# A script that did not compile loads all the same, with no methods, and the call answered that
|
|
399
|
+
# it had no such method, which reads as a misspelt name. An abstract script can be instantiated
|
|
400
|
+
# as far as this asks, so it is not taken for one.
|
|
401
|
+
var compiled: Script = loaded as Script
|
|
402
|
+
if not compiled.can_instantiate():
|
|
403
|
+
return {
|
|
404
|
+
"type": "error",
|
|
405
|
+
"message":
|
|
406
|
+
(
|
|
407
|
+
"%s did not compile, so none of its functions can be called; script_diagnostics on "
|
|
408
|
+
+ "it gives the error and its line."
|
|
409
|
+
) % script_path,
|
|
410
|
+
}
|
|
398
411
|
return { "object": loaded, "node": root }
|
|
399
412
|
|
|
400
413
|
|
|
@@ -36,6 +36,8 @@ var _files: GdhFileWalk = GdhFileWalk.new()
|
|
|
36
36
|
## script again made the whole-project answer quadratic in the number of scripts.
|
|
37
37
|
var _classes_declared: Dictionary = {}
|
|
38
38
|
var _classes_read: bool = false
|
|
39
|
+
var _uids_declared: Dictionary = {}
|
|
40
|
+
var _uids_read: bool = false
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
func _init(p_log: GdhLog) -> void:
|
|
@@ -158,8 +160,7 @@ func missing_references() -> Array[Dictionary]:
|
|
|
158
160
|
func _loaded_and_missing(script: String) -> Array[String]:
|
|
159
161
|
var loaded: Array[String] = []
|
|
160
162
|
var named: Dictionary = {}
|
|
161
|
-
for
|
|
162
|
-
var code: String = GdhPatterns.without_comment(line)
|
|
163
|
+
for code: String in code_by_line(FileAccess.get_file_as_string(script)):
|
|
163
164
|
if not code.contains("load("):
|
|
164
165
|
for literal: String in _string_literals(code):
|
|
165
166
|
named[_resolved(literal, script)] = true
|
|
@@ -492,10 +493,32 @@ static func _code_of(line: String) -> String:
|
|
|
492
493
|
# [param content] with each line's comment taken off and its strings kept, which is where the paths
|
|
493
494
|
# a script loads are written.
|
|
494
495
|
static func _code_lines(content: String) -> String:
|
|
495
|
-
|
|
496
|
+
return "\n".join(code_by_line(content))
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
## Each line of [param content] as code: its comment off, and the text of a string running across
|
|
500
|
+
## lines left out. Read as code, a test suite template's example
|
|
501
|
+
## `load("res://folder/my_class.gd")` was a call of a file that does not exist, which made the
|
|
502
|
+
## project invalid.
|
|
503
|
+
static func code_by_line(content: String) -> Array[String]:
|
|
504
|
+
var lines: Array[String] = []
|
|
505
|
+
var in_string: bool = false
|
|
496
506
|
for line: String in content.split("\n"):
|
|
497
|
-
|
|
498
|
-
|
|
507
|
+
if in_string:
|
|
508
|
+
if GdhPatterns.triple_quotes(line) % 2 == 1:
|
|
509
|
+
in_string = false
|
|
510
|
+
lines.append("")
|
|
511
|
+
continue
|
|
512
|
+
var code: String = GdhPatterns.without_comment(line)
|
|
513
|
+
if GdhPatterns.triple_quotes(code) % 2 == 1:
|
|
514
|
+
in_string = true
|
|
515
|
+
var opened: int = code.find('"""')
|
|
516
|
+
var apostrophes: int = code.find("'''")
|
|
517
|
+
if opened < 0 or (apostrophes >= 0 and apostrophes < opened):
|
|
518
|
+
opened = apostrophes
|
|
519
|
+
code = code.substr(0, opened)
|
|
520
|
+
lines.append(code)
|
|
521
|
+
return lines
|
|
499
522
|
|
|
500
523
|
|
|
501
524
|
# The comment on [param line], strings emptied first so a # inside one does not start it.
|
|
@@ -578,21 +601,72 @@ func _analyze_resource(
|
|
|
578
601
|
path: String, current_depth: int, walk: GdhDependencyWalk
|
|
579
602
|
) -> Array[Dictionary]:
|
|
580
603
|
var deps: Array[Dictionary] = []
|
|
581
|
-
|
|
582
604
|
if current_depth >= walk.max_depth:
|
|
583
605
|
return deps
|
|
584
606
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
607
|
+
walk.path_stack.append(path)
|
|
608
|
+
var cut: bool = false
|
|
609
|
+
for named: Dictionary in _references(path, walk):
|
|
610
|
+
var dep_info: Dictionary = named.duplicate()
|
|
611
|
+
cut = _depend(dep_info, current_depth, walk) or cut
|
|
612
|
+
deps.append(dep_info)
|
|
613
|
+
walk.path_stack.pop_back()
|
|
614
|
+
|
|
588
615
|
var remaining: int = walk.max_depth - current_depth
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
return cached_deps
|
|
616
|
+
var shown: Dictionary = walk.shown.get(path, {})
|
|
617
|
+
if shown.is_empty() or remaining > GdhRead.as_int(shown["remaining"]):
|
|
618
|
+
walk.shown[path] = { "remaining": remaining, "cut": cut }
|
|
619
|
+
return deps
|
|
594
620
|
|
|
595
|
-
|
|
621
|
+
|
|
622
|
+
# Walks [param dep_info], one entry of what the file at [param current_depth] names, and answers
|
|
623
|
+
# whether the depth cut short what it shows. Its dependencies are listed at its first entry, which
|
|
624
|
+
# every later one says is above it: listed in full under every file loading it, a game of a
|
|
625
|
+
# hundred scripts typed by class_name answered half a megabyte at depth six and nothing at all
|
|
626
|
+
# unlimited, the tree growing about sixfold a level. A list the depth cut short is listed again
|
|
627
|
+
# where more depth is left, since pointing a nearer reach at it answered a chain that stopped
|
|
628
|
+
# where nothing stopped it. A list is recorded once walked, so what a later entry points at is
|
|
629
|
+
# always above it.
|
|
630
|
+
func _depend(dep_info: Dictionary, current_depth: int, walk: GdhDependencyWalk) -> bool:
|
|
631
|
+
var dep_path: String = dep_info["path"]
|
|
632
|
+
# Marked where the reference closing the cycle is. Marked inside the file reached again, the
|
|
633
|
+
# tree answered that file as a dependency of itself.
|
|
634
|
+
if dep_path in walk.path_stack:
|
|
635
|
+
var cycle: Array[String] = walk.path_stack.slice(walk.path_stack.find(dep_path))
|
|
636
|
+
cycle.append(dep_path)
|
|
637
|
+
if not cycle in walk.circular_references:
|
|
638
|
+
walk.circular_references.append(cycle)
|
|
639
|
+
dep_info["circular"] = true
|
|
640
|
+
return false
|
|
641
|
+
if not dep_info["exists"]:
|
|
642
|
+
return false
|
|
643
|
+
var named: int = _references(dep_path, walk).size()
|
|
644
|
+
if named == 0:
|
|
645
|
+
return false
|
|
646
|
+
if current_depth + 1 >= walk.max_depth:
|
|
647
|
+
# A file the depth stopped at read the same as one naming nothing, and a missing file past
|
|
648
|
+
# it went unsaid.
|
|
649
|
+
dep_info["beyond_depth"] = named
|
|
650
|
+
return true
|
|
651
|
+
var shown: Dictionary = walk.shown.get(dep_path, {})
|
|
652
|
+
if not shown.is_empty():
|
|
653
|
+
var shown_cut: bool = shown["cut"]
|
|
654
|
+
var remaining: int = walk.max_depth - current_depth - 1
|
|
655
|
+
if not shown_cut or remaining <= GdhRead.as_int(shown["remaining"]):
|
|
656
|
+
dep_info["dependencies_listed_above"] = true
|
|
657
|
+
return shown_cut
|
|
658
|
+
dep_info["dependencies"] = _analyze_resource(dep_path, current_depth + 1, walk)
|
|
659
|
+
var walked: Dictionary = walk.shown[dep_path]
|
|
660
|
+
return walked["cut"]
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
## What [param path] names, each file once, as the entries the tree lists it by; read once a walk.
|
|
664
|
+
func _references(path: String, walk: GdhDependencyWalk) -> Array[Dictionary]:
|
|
665
|
+
if walk.references.has(path):
|
|
666
|
+
var known: Array[Dictionary] = walk.references[path]
|
|
667
|
+
return known
|
|
668
|
+
var deps: Array[Dictionary] = []
|
|
669
|
+
walk.references[path] = deps
|
|
596
670
|
|
|
597
671
|
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
598
672
|
if file:
|
|
@@ -619,7 +693,7 @@ func _analyze_resource(
|
|
|
619
693
|
# as no res:// path, a script's load of one went unsaid while the game printed
|
|
620
694
|
# "Unrecognized UID" at that line.
|
|
621
695
|
if dep_path.begins_with("uid://"):
|
|
622
|
-
|
|
696
|
+
_name(deps, path, dep_path, {})
|
|
623
697
|
continue
|
|
624
698
|
if not dep_path.begins_with("res://"):
|
|
625
699
|
continue
|
|
@@ -638,7 +712,7 @@ func _analyze_resource(
|
|
|
638
712
|
var named_by: Dictionary = { "stale_path": written } if dep_path != written else {}
|
|
639
713
|
if unknown.has(dep_path):
|
|
640
714
|
named_by["unknown_uid"] = unknown[dep_path]
|
|
641
|
-
|
|
715
|
+
_name(deps, path, dep_path, named_by)
|
|
642
716
|
|
|
643
717
|
# A script uses another by its class_name as surely as by a preload, and only the reverse
|
|
644
718
|
# walk looked for that: asked what a script depends on, the answer left out the script it
|
|
@@ -647,30 +721,18 @@ func _analyze_resource(
|
|
|
647
721
|
if path.get_extension() == "gd":
|
|
648
722
|
var classes: Dictionary = classes_declared()
|
|
649
723
|
for used: String in classes_used_in(content, classes):
|
|
650
|
-
|
|
724
|
+
_name(deps, path, str(classes[used]), { "by_class": used })
|
|
651
725
|
var autoloads: Dictionary = autoloads_declared()
|
|
652
726
|
for used: String in classes_used_in(content, autoloads):
|
|
653
|
-
|
|
654
|
-
deps, path, str(autoloads[used]), { "by_autoload": used }, current_depth, walk
|
|
655
|
-
)
|
|
656
|
-
|
|
657
|
-
walk.path_stack.pop_back()
|
|
658
|
-
walk.visited[path] = { "deps": deps, "remaining": remaining }
|
|
727
|
+
_name(deps, path, str(autoloads[used]), { "by_autoload": used })
|
|
659
728
|
return deps
|
|
660
729
|
|
|
661
730
|
|
|
662
|
-
# Adds [param dep_path] to [param deps], what [param path] depends on,
|
|
663
|
-
#
|
|
664
|
-
#
|
|
665
|
-
#
|
|
666
|
-
func
|
|
667
|
-
deps: Array[Dictionary],
|
|
668
|
-
path: String,
|
|
669
|
-
dep_path: String,
|
|
670
|
-
named_by: Dictionary,
|
|
671
|
-
current_depth: int,
|
|
672
|
-
walk: GdhDependencyWalk
|
|
673
|
-
) -> void:
|
|
731
|
+
# Adds [param dep_path] to [param deps], what [param path] depends on, once however many times it
|
|
732
|
+
# is named. [param named_by] is the class or autoload name it was named by, as by_class or
|
|
733
|
+
# by_autoload, and the path written when the engine loads another, as stale_path; empty for a path
|
|
734
|
+
# that is the file.
|
|
735
|
+
func _name(deps: Array[Dictionary], path: String, dep_path: String, named_by: Dictionary) -> void:
|
|
674
736
|
if dep_path == path:
|
|
675
737
|
return
|
|
676
738
|
for existing: Dictionary in deps:
|
|
@@ -683,33 +745,12 @@ func _depend(
|
|
|
683
745
|
"exists": not dep_path.begins_with("uid://") and FileAccess.file_exists(dep_path),
|
|
684
746
|
}
|
|
685
747
|
dep_info.merge(named_by)
|
|
686
|
-
# Marked where the reference closing the cycle is. Marked inside the file reached again, the
|
|
687
|
-
# tree answered that file as a dependency of itself.
|
|
688
|
-
if dep_path in walk.path_stack:
|
|
689
|
-
var cycle: Array[String] = walk.path_stack.slice(walk.path_stack.find(dep_path))
|
|
690
|
-
cycle.append(dep_path)
|
|
691
|
-
if not cycle in walk.circular_references:
|
|
692
|
-
walk.circular_references.append(cycle)
|
|
693
|
-
dep_info["circular"] = true
|
|
694
|
-
deps.append(dep_info)
|
|
695
|
-
return
|
|
696
|
-
if dep_info["exists"] and current_depth + 1 < walk.max_depth:
|
|
697
|
-
var sub_deps: Array[Dictionary] = _analyze_resource(dep_path, current_depth + 1, walk)
|
|
698
|
-
if sub_deps.size() > 0:
|
|
699
|
-
dep_info["dependencies"] = sub_deps
|
|
700
|
-
elif dep_info["exists"] and walk.counts_beyond:
|
|
701
|
-
# A file the depth stopped at read the same as one naming nothing, and a missing file past
|
|
702
|
-
# it went unsaid.
|
|
703
|
-
var beyond: GdhDependencyWalk = GdhDependencyWalk.new(1, walk.include_built_in, [], false)
|
|
704
|
-
var named: int = _analyze_resource(dep_path, 0, beyond).size()
|
|
705
|
-
if named > 0:
|
|
706
|
-
dep_info["beyond_depth"] = named
|
|
707
748
|
deps.append(dep_info)
|
|
708
749
|
|
|
709
750
|
|
|
710
751
|
## Every autoload the project registers as a global name, with its script. One that is not global
|
|
711
752
|
## is no name code can use.
|
|
712
|
-
|
|
753
|
+
func autoloads_declared() -> Dictionary:
|
|
713
754
|
var autoloads: Dictionary = {}
|
|
714
755
|
for property: Dictionary in ProjectSettings.get_property_list():
|
|
715
756
|
var setting: String = str(property.get("name", ""))
|
|
@@ -762,27 +803,63 @@ static func classes_used_in(content: String, classes: Dictionary) -> Array[Strin
|
|
|
762
803
|
return used
|
|
763
804
|
|
|
764
805
|
|
|
765
|
-
# [param written] as the res:// path the engine would load from [param from]: a uid:// by
|
|
766
|
-
#
|
|
767
|
-
#
|
|
768
|
-
|
|
769
|
-
static func _resolved(written: String, from: String) -> String:
|
|
806
|
+
# [param written] as the res:// path the engine would load from [param from]: a uid:// by the file
|
|
807
|
+
# it names, and a relative path against the referring file's directory. Both were passed over as not
|
|
808
|
+
# being res:// paths, so a script's `preload("sibling.gd")` was no dependency at all.
|
|
809
|
+
func _resolved(written: String, from: String) -> String:
|
|
770
810
|
if written.begins_with("res://"):
|
|
771
811
|
return written
|
|
772
812
|
if written.begins_with("uid://"):
|
|
773
|
-
var
|
|
774
|
-
return
|
|
813
|
+
var found: String = _uid_path(written)
|
|
814
|
+
return written if found.is_empty() else found
|
|
775
815
|
if written.contains("://"):
|
|
776
816
|
return written
|
|
777
817
|
return from.get_base_dir().path_join(written).simplify_path()
|
|
778
818
|
|
|
779
819
|
|
|
820
|
+
# The file the uid:// [param text] names, or empty when nothing does: the engine's uid cache first,
|
|
821
|
+
# then the uids the project's files declare. Only an import writes the cache, so on a fresh clone
|
|
822
|
+
# every uid was answered as naming no file, and a scene moved outside the editor was missing under
|
|
823
|
+
# its old path, although the first import loads it by its uid. A cached path with no file there
|
|
824
|
+
# gives way to a file declaring the uid, and is the answer when none does.
|
|
825
|
+
func _uid_path(text: String) -> String:
|
|
826
|
+
var id: int = ResourceUID.text_to_id(text)
|
|
827
|
+
if id == ResourceUID.INVALID_ID:
|
|
828
|
+
return ""
|
|
829
|
+
var cached: String = ResourceUID.get_id_path(id) if ResourceUID.has_id(id) else ""
|
|
830
|
+
if FileAccess.file_exists(cached):
|
|
831
|
+
return cached
|
|
832
|
+
return str(_files_by_uid().get(id, cached))
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
# Every file of the project that declares a uid, by its uid's id: in a .uid sidecar, an import
|
|
836
|
+
# sidecar or the header of a text resource, as GdhResourceFiles reads them. Read once for the call,
|
|
837
|
+
# the first time the cache does not know a uid.
|
|
838
|
+
func _files_by_uid() -> Dictionary:
|
|
839
|
+
if _uids_read:
|
|
840
|
+
return _uids_declared
|
|
841
|
+
_uids_read = true
|
|
842
|
+
var declaring: Dictionary = {}
|
|
843
|
+
for file: String in _files.find_files_with_extensions(
|
|
844
|
+
"res://", ["uid", "import"] + GdhResourceFiles.TEXT_RESOURCE_KINDS
|
|
845
|
+
):
|
|
846
|
+
var sidecar: bool = file.get_extension() in ["uid", "import"]
|
|
847
|
+
declaring[file.get_basename() if sidecar else file] = true
|
|
848
|
+
for file: String in declaring:
|
|
849
|
+
if not FileAccess.file_exists(file):
|
|
850
|
+
continue
|
|
851
|
+
var id: int = ResourceUID.text_to_id(str(GdhResourceFiles.uid_of(file).get("uid", "")))
|
|
852
|
+
if id != ResourceUID.INVALID_ID:
|
|
853
|
+
_uids_declared[id] = file
|
|
854
|
+
return _uids_declared
|
|
855
|
+
|
|
856
|
+
|
|
780
857
|
# The ext_resource paths in [param content] whose uid leads elsewhere, each with the file the uid
|
|
781
858
|
# names. The engine loads an ext_resource by its uid whenever the uid is known, whatever the path
|
|
782
859
|
# says, so a file moved outside the editor still loads, and the walk called it missing; every
|
|
783
860
|
# pattern reads the old path, the bare res:// one included, so the paths are mapped before any of
|
|
784
861
|
# them runs.
|
|
785
|
-
|
|
862
|
+
func _moved_by_uid(content: String) -> Dictionary:
|
|
786
863
|
var moved: Dictionary = {}
|
|
787
864
|
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
788
865
|
var found: String = _loaded_by_uid(line.get_string())
|
|
@@ -795,7 +872,7 @@ static func _moved_by_uid(content: String) -> Dictionary:
|
|
|
795
872
|
# The ext_resource paths in [param content], read from [param from], whose uid names no file the
|
|
796
873
|
# project has, each with that uid. The engine warns that the uid is invalid and loads the path, and
|
|
797
874
|
# the walk followed the path saying nothing, so an export was the first to mention it.
|
|
798
|
-
|
|
875
|
+
func _unknown_uids(content: String, from: String) -> Dictionary:
|
|
799
876
|
var unknown: Dictionary = {}
|
|
800
877
|
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
801
878
|
var uid: String = _unknown_uid_of(line.get_string())
|
|
@@ -806,21 +883,18 @@ static func _unknown_uids(content: String, from: String) -> Dictionary:
|
|
|
806
883
|
|
|
807
884
|
|
|
808
885
|
# The uid an ext_resource [param entry] carries when it names no file the project has, or empty.
|
|
809
|
-
|
|
886
|
+
func _unknown_uid_of(entry: String) -> String:
|
|
810
887
|
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
811
888
|
return "" if uid == null or not _loaded_by_uid(entry).is_empty() else uid.get_string(1)
|
|
812
889
|
|
|
813
890
|
|
|
814
891
|
# The file an ext_resource [param entry] is loaded from by its uid, or empty when it carries none
|
|
815
892
|
# the project knows, and the engine falls back to the path.
|
|
816
|
-
|
|
893
|
+
func _loaded_by_uid(entry: String) -> String:
|
|
817
894
|
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
818
895
|
if uid == null:
|
|
819
896
|
return ""
|
|
820
|
-
var
|
|
821
|
-
if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
|
|
822
|
-
return ""
|
|
823
|
-
var found: String = ResourceUID.get_id_path(id)
|
|
897
|
+
var found: String = _uid_path(uid.get_string(1))
|
|
824
898
|
return found if FileAccess.file_exists(found) else ""
|
|
825
899
|
|
|
826
900
|
|
|
@@ -877,23 +951,18 @@ class GdhDependencyWalk:
|
|
|
877
951
|
|
|
878
952
|
var include_built_in: bool
|
|
879
953
|
|
|
880
|
-
|
|
954
|
+
## What each file read names, by its path.
|
|
955
|
+
var references: Dictionary = {}
|
|
956
|
+
|
|
957
|
+
## For each file whose dependencies the tree lists, the depth left where they are listed and
|
|
958
|
+
## whether the depth cut that list short.
|
|
959
|
+
var shown: Dictionary = {}
|
|
881
960
|
|
|
882
961
|
var path_stack: Array[String] = []
|
|
883
962
|
|
|
884
963
|
var circular_references: Array
|
|
885
964
|
|
|
886
|
-
|
|
887
|
-
## that reading does not do again for those.
|
|
888
|
-
var counts_beyond: bool
|
|
889
|
-
|
|
890
|
-
func _init(
|
|
891
|
-
p_max_depth: int,
|
|
892
|
-
p_include_built_in: bool,
|
|
893
|
-
p_circular_references: Array,
|
|
894
|
-
p_counts_beyond: bool = true
|
|
895
|
-
) -> void:
|
|
965
|
+
func _init(p_max_depth: int, p_include_built_in: bool, p_circular_references: Array) -> void:
|
|
896
966
|
max_depth = p_max_depth
|
|
897
967
|
include_built_in = p_include_built_in
|
|
898
968
|
circular_references = p_circular_references
|
|
899
|
-
counts_beyond = p_counts_beyond
|
|
@@ -85,15 +85,16 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
85
85
|
answered["namedByPath"] = (
|
|
86
86
|
(
|
|
87
87
|
"What this saved names %s by path rather than by uid, which loads the same. A "
|
|
88
|
-
+ "
|
|
89
|
-
+ "
|
|
90
|
-
+ "
|
|
91
|
-
+ "
|
|
88
|
+
+ "reference by uid is resolved through the project's uid cache, which holds "
|
|
89
|
+
+ "none for %s yet, as with a file made since the project was last scanned, so "
|
|
90
|
+
+ "naming %s by uid now would warn on every load. project_import refresh_uids "
|
|
91
|
+
+ "scans the project, and writes after it name %s by uid."
|
|
92
92
|
)
|
|
93
93
|
% [
|
|
94
94
|
", ".join(GdhSceneUids.by_path),
|
|
95
95
|
"it" if GdhSceneUids.by_path.size() == 1 else "them",
|
|
96
96
|
"it" if GdhSceneUids.by_path.size() == 1 else "them",
|
|
97
|
+
"it" if GdhSceneUids.by_path.size() == 1 else "them",
|
|
97
98
|
]
|
|
98
99
|
)
|
|
99
100
|
return answered
|