gdharness 1.3.42 → 1.3.44

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.
Files changed (106) hide show
  1. package/build/cli.js +383 -243
  2. package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
  3. package/build/godot/addons/gdharness_editor/animation_reads.gd +55 -33
  4. package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -58
  5. package/build/godot/addons/gdharness_editor/declared_values.gd +68 -32
  6. package/build/godot/addons/gdharness_editor/file_reads.gd +83 -42
  7. package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
  8. package/build/godot/addons/gdharness_editor/headless_check.gd +68 -44
  9. package/build/godot/addons/gdharness_editor/plugin.gd +2 -3
  10. package/build/godot/addons/gdharness_editor/property_paths.gd +22 -12
  11. package/build/godot/addons/gdharness_editor/property_values.gd +143 -78
  12. package/build/godot/addons/gdharness_editor/reading.gd +51 -17
  13. package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +127 -80
  15. package/build/godot/addons/gdharness_editor/scene_uids.gd +12 -6
  16. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -51
  17. package/build/godot/addons/gdharness_editor/setting_reads.gd +7 -6
  18. package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -6
  19. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +37 -27
  20. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +79 -49
  21. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
  22. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +65 -42
  23. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +24 -22
  24. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +49 -29
  25. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +26 -21
  26. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +40 -40
  27. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +166 -107
  28. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -9
  29. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +68 -41
  30. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +35 -20
  31. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +28 -13
  32. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +12 -5
  33. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +113 -67
  34. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
  35. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
  36. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
  37. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
  38. package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
  39. package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
  40. package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
  41. package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
  42. package/build/godot/addons/gdharness_runtime/reading.gd +51 -17
  43. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
  44. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +149 -147
  45. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
  46. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
  47. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
  49. package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
  50. package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
  51. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
  52. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +34 -17
  53. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
  54. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
  55. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
  56. package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
  57. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
  58. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
  59. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +15 -15
  60. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
  61. package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
  62. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
  63. package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
  64. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
  65. package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
  66. package/build/godot/addons/gdharness_tests/bench.gd +4 -5
  67. package/build/godot/addons/gdharness_tests/event.gd +10 -5
  68. package/build/godot/addons/gdharness_tests/share.gd +38 -18
  69. package/build/godot/addons/gdharness_tests/suites.gd +3 -3
  70. package/build/godot/addons/gdharness_tests/wait.gd +3 -2
  71. package/build/godot/loader/loader.gd +9 -9
  72. package/build/godot/operations/animation_reads.gd +55 -33
  73. package/build/godot/operations/audio_buses.gd +48 -19
  74. package/build/godot/operations/class_cache.gd +16 -11
  75. package/build/godot/operations/classdb_queries.gd +89 -44
  76. package/build/godot/operations/declared_values.gd +68 -32
  77. package/build/godot/operations/dependencies.gd +190 -109
  78. package/build/godot/operations/editor_tools.gd +10 -10
  79. package/build/godot/operations/export_presets.gd +100 -54
  80. package/build/godot/operations/file_reads.gd +83 -42
  81. package/build/godot/operations/file_walk.gd +12 -5
  82. package/build/godot/operations/gdscript_analysis.gd +86 -65
  83. package/build/godot/operations/gdscript_authoring.gd +83 -36
  84. package/build/godot/operations/godot_operations.gd +17 -11
  85. package/build/godot/operations/import_pipeline.gd +110 -59
  86. package/build/godot/operations/input_actions.gd +62 -39
  87. package/build/godot/operations/patterns.gd +3 -1
  88. package/build/godot/operations/plugins.gd +36 -16
  89. package/build/godot/operations/project_config.gd +61 -32
  90. package/build/godot/operations/project_diagnostics.gd +24 -11
  91. package/build/godot/operations/project_file.gd +16 -16
  92. package/build/godot/operations/project_validation.gd +71 -44
  93. package/build/godot/operations/property_paths.gd +22 -12
  94. package/build/godot/operations/property_values.gd +143 -78
  95. package/build/godot/operations/reading.gd +51 -17
  96. package/build/godot/operations/rename_checks.gd +2 -2
  97. package/build/godot/operations/resource_files.gd +11 -9
  98. package/build/godot/operations/same_values.gd +4 -4
  99. package/build/godot/operations/scene_connections.gd +27 -13
  100. package/build/godot/operations/script_check.gd +52 -49
  101. package/build/godot/operations/serialisation.gd +75 -51
  102. package/build/godot/operations/setting_reads.gd +7 -6
  103. package/build/godot/operations/value_shapes.gd +5 -6
  104. package/build/godot/tests/listed.gd +321 -268
  105. package/build/keeper.js +43 -43
  106. package/package.json +1 -1
@@ -5,20 +5,19 @@ const GdhRead = preload("reading.gd")
5
5
  const GdhFileWalk = preload("file_walk.gd")
6
6
  const GdhLog = preload("logger.gd")
7
7
  const GdhResourceFiles = preload("resource_files.gd")
8
-
9
8
  ## What a reverse search reads unless told otherwise: the resource and code files, and the settings
10
- ## files that name resources, "godot" for project.godot, "cfg" for plugin.cfg and export_presets.cfg,
11
- ## and "import" for the sidecars that name import scripts and external materials.
9
+ ## files that name resources, "godot" for project.godot, "cfg" for plugin.cfg and
10
+ ## export_presets.cfg, and "import" for the sidecars that name import scripts and external
11
+ ## materials.
12
12
  const PROJECT_FILE: String = "res://project.godot"
13
13
  const DEFAULT_USAGE_KINDS: Array[String] = [
14
- "tscn", "tres", "gd", "gdshader", "gdshaderinc", "godot", "cfg", "import"
14
+ "tscn", "tres", "gd", "gdshader", "gdshaderinc", "godot", "cfg", "import",
15
15
  ]
16
- ## The files written in the shader language, whose comments are // and /* */ rather than #, which in a
17
- ## shader starts a preprocessor directive.
16
+ ## The files written in the shader language, whose comments are // and /* */ rather than #, which in
17
+ ## a shader starts a preprocessor directive.
18
18
  const SHADER_KINDS: Array[String] = ["gdshader", "gdshaderinc"]
19
19
  ## One string literal of either quote, its contents captured by the one it opened with.
20
20
  const STRING_LITERAL: String = "\"((?:[^\"\\\\]|\\\\.)*)\"|'((?:[^'\\\\]|\\\\.)*)'"
21
-
22
21
  # What a dependency reference looks like in source, and how the path is read out of each form.
23
22
  const REFERENCE_PATTERNS: Array[String] = [
24
23
  "res://[^\"'\\s\\]\\)]+",
@@ -37,28 +36,8 @@ var _files: GdhFileWalk = GdhFileWalk.new()
37
36
  ## script again made the whole-project answer quadratic in the number of scripts.
38
37
  var _classes_declared: Dictionary = {}
39
38
  var _classes_read: bool = false
40
-
41
-
42
- # Everything one dependency walk carries: the settings it was started with, and the state it
43
- # accumulates as it recurses. Kept together so the recursion passes one object rather than
44
- # six positional arguments that have to stay in the same order at every call site.
45
- class GdhDependencyWalk:
46
- var max_depth: int
47
- var include_built_in: bool
48
- var visited: Dictionary = {}
49
- var path_stack: Array[String] = []
50
- var circular_references: Array
51
- ## Whether a file the depth stops at is read for how many files it names, which the walk doing
52
- ## that reading does not do again for those.
53
- var counts_beyond: bool
54
-
55
- func _init(
56
- p_max_depth: int, p_include_built_in: bool, p_circular_references: Array, p_counts_beyond: bool = true
57
- ) -> void:
58
- max_depth = p_max_depth
59
- include_built_in = p_include_built_in
60
- circular_references = p_circular_references
61
- counts_beyond = p_counts_beyond
39
+ var _uids_declared: Dictionary = {}
40
+ var _uids_read: bool = false
62
41
 
63
42
 
64
43
  func _init(p_log: GdhLog) -> void:
@@ -88,7 +67,9 @@ func get_dependencies(params: Dictionary) -> Dictionary:
88
67
  return _log.failure(
89
68
  _files.with_nearest_files("Resource file does not exist: " + full_path, full_path)
90
69
  )
91
- var walk: GdhDependencyWalk = GdhDependencyWalk.new(max_depth, include_built_in, circular_references)
70
+ var walk: GdhDependencyWalk = GdhDependencyWalk.new(
71
+ max_depth, include_built_in, circular_references
72
+ )
92
73
  dependencies[full_path] = _analyze_resource(full_path, 0, walk)
93
74
 
94
75
  var references: int = 0
@@ -105,7 +86,7 @@ func get_dependencies(params: Dictionary) -> Dictionary:
105
86
  # `exists: false` among the entries, which a caller reading the summary never met.
106
87
  var missing_files: Array[Dictionary] = []
107
88
  for path: String in missing:
108
- missing_files.append({"path": path, "referenced_by": missing[path]})
89
+ missing_files.append({ "path": path, "referenced_by": missing[path] })
109
90
 
110
91
  return {
111
92
  "dependencies": dependencies,
@@ -119,8 +100,8 @@ func get_dependencies(params: Dictionary) -> Dictionary:
119
100
  "total_dependencies": distinct.size(),
120
101
  "total_references": references,
121
102
  "circular_count": circular_references.size(),
122
- "missing": missing_files
123
- }
103
+ "missing": missing_files,
104
+ },
124
105
  }
125
106
 
126
107
 
@@ -137,10 +118,14 @@ func missing_references() -> Array[Dictionary]:
137
118
  var naming: Array = missing.get_or_add(path, [])
138
119
  if not file in naming:
139
120
  naming.append(file)
140
- for file: String in _files.find_files_with_extensions("res://", GdhResourceFiles.TEXT_RESOURCE_KINDS):
121
+ for file: String in _files.find_files_with_extensions(
122
+ "res://", GdhResourceFiles.TEXT_RESOURCE_KINDS
123
+ ):
141
124
  var content: String = FileAccess.get_file_as_string(file)
142
125
  var moved: Dictionary = _moved_by_uid(content)
143
- for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
126
+ for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(
127
+ content
128
+ ):
144
129
  var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line.get_string())
145
130
  if at == null or moved.has(at.get_string(1)):
146
131
  continue
@@ -162,7 +147,7 @@ func missing_references() -> Array[Dictionary]:
162
147
  for path: String in paths:
163
148
  var naming: Array = missing[path]
164
149
  naming.sort()
165
- found.append({"path": path, "referenced_by": naming})
150
+ found.append({ "path": path, "referenced_by": naming })
166
151
  return found
167
152
 
168
153
 
@@ -183,12 +168,16 @@ func _loaded_and_missing(script: String) -> Array[String]:
183
168
  continue
184
169
  var calls: Dictionary = {}
185
170
  for loading: RegExMatch in (
186
- GdhPatterns.compiled("(?:(?<![\\w.])|ResourceLoader\\.)load\\(\\s*(?=[\"'])").search_all(code)
171
+ GdhPatterns
172
+ . compiled("(?:(?<![\\w.])|ResourceLoader\\.)load\\(\\s*(?=[\"'])")
173
+ . search_all(code)
187
174
  ):
188
175
  calls[loading.get_end()] = true
189
176
  for literal: RegExMatch in GdhPatterns.compiled(STRING_LITERAL).search_all(code):
190
177
  var single: String = literal.get_string(2)
191
- var path: String = _resolved(literal.get_string(1) if single.is_empty() else single, script)
178
+ var path: String = _resolved(
179
+ literal.get_string(1) if single.is_empty() else single, script
180
+ )
192
181
  if calls.has(literal.get_start()):
193
182
  loaded.append(path)
194
183
  else:
@@ -200,7 +189,11 @@ func _loaded_and_missing(script: String) -> Array[String]:
200
189
  if not named.has(path) and not missing.has(path):
201
190
  missing.append(path)
202
191
  continue
203
- if not path.begins_with("res://") or path.begins_with("res://.") or path.get_extension().is_empty():
192
+ if (
193
+ not path.begins_with("res://")
194
+ or path.begins_with("res://.")
195
+ or path.get_extension().is_empty()
196
+ ):
204
197
  continue
205
198
  if not named.has(path) and not FileAccess.file_exists(path) and not missing.has(path):
206
199
  missing.append(path)
@@ -239,7 +232,11 @@ static func missing_line(entry: Dictionary) -> String:
239
232
  var naming: PackedStringArray = PackedStringArray(referring)
240
233
  return (
241
234
  "%s does not exist, and %s %s to it"
242
- % [str(entry.get("path", "")), ", ".join(naming), "refers" if naming.size() == 1 else "refer"]
235
+ % [
236
+ str(entry.get("path", "")),
237
+ ", ".join(naming),
238
+ "refers" if naming.size() == 1 else "refer",
239
+ ]
243
240
  )
244
241
 
245
242
 
@@ -276,7 +273,7 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
276
273
  by_autoload = GdhPatterns.compiled("\\b" + _regex_escaped(autoload_named) + "\\b")
277
274
  var target: Dictionary = {
278
275
  "path": resource_path,
279
- "uid": str(GdhResourceFiles.uid_of(resource_path).get("uid", "")) if exists else ""
276
+ "uid": str(GdhResourceFiles.uid_of(resource_path).get("uid", "")) if exists else "",
280
277
  }
281
278
 
282
279
  var all_files: Array[String] = _usage_files(file_types)
@@ -296,9 +293,9 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
296
293
  var text: String = file.get_as_text()
297
294
  file.close()
298
295
  var lines: PackedStringArray = text.split("\n")
299
- # A class is used by name only in a script's code. In a scene or a resource the same word is a
300
- # node's name or a caption, and each was counted as a use of the class; a shader has no use
301
- # for one at all.
296
+ # A class is used by name only in a script's code. In a scene or a resource the same word is
297
+ # a node's name or a caption, and each was counted as a use of the class; a shader has no
298
+ # use for one at all.
302
299
  var code: bool = file_path.get_extension() == "gd"
303
300
  # In a shader # opens a directive, so an #include was answered as a comment, and its
304
301
  # comments are // and /* */.
@@ -326,9 +323,13 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
326
323
  var trimmed: String = line.strip_edges()
327
324
  var kind: String = ""
328
325
  var extra: Dictionary = {}
329
- var loaded: String = _loaded_by_uid(line) if trimmed.begins_with("[ext_resource") else ""
326
+ var loaded: String = (
327
+ _loaded_by_uid(line) if trimmed.begins_with("[ext_resource") else ""
328
+ )
330
329
  var written: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line)
331
- var written_path: String = "" if written == null else _resolved(written.get_string(1), file_path)
330
+ var written_path: String = (
331
+ "" if written == null else _resolved(written.get_string(1), file_path)
332
+ )
332
333
  # A mention in a comment is not a use, and both were answered as one. The question a
333
334
  # reverse walk is asked is whether anything still uses a class, and a project that
334
335
  # documents itself names its collaborators in `##` links: four references to one class,
@@ -339,10 +340,14 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
339
340
  kind = "doc"
340
341
  elif (code and trimmed.begins_with("#")) or name_in_comment:
341
342
  kind = "comment"
342
- elif not shader_lines.is_empty() and not _names_target(shader_lines[i], file_path, target):
343
+ elif (
344
+ not shader_lines.is_empty()
345
+ and not _names_target(shader_lines[i], file_path, target)
346
+ ):
343
347
  kind = "comment"
344
348
  elif by_its_path and not loaded.is_empty() and loaded != target["path"]:
345
- # The engine loads the file the uid names, so the path written here is used by nothing.
349
+ # The engine loads the file the uid names, so the path written here is used by
350
+ # nothing.
346
351
  kind = "stale_path"
347
352
  extra["loads"] = loaded
348
353
  elif by_its_path:
@@ -362,7 +367,7 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
362
367
  if GdhPatterns.without_annotations(line).begins_with("extends ")
363
368
  else "class_name"
364
369
  )
365
- var found: Dictionary = {"line": i + 1, "kind": kind, "text": trimmed}
370
+ var found: Dictionary = { "line": i + 1, "kind": kind, "text": trimmed }
366
371
  found.merge(extra)
367
372
  references.append(found)
368
373
  by_kind[kind] = GdhRead.as_int(by_kind.get(kind, 0)) + 1
@@ -370,13 +375,16 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
370
375
  in_code += 1
371
376
 
372
377
  if not references.is_empty():
373
- usages.append({"file": file_path, "references": references})
378
+ usages.append({ "file": file_path, "references": references })
374
379
  total += references.size()
375
380
 
376
381
  if not exists and usages.is_empty():
377
382
  return _log.failure(
378
383
  _files.with_nearest_files(
379
- "Resource file does not exist: %s, and nothing in the project refers to it" % resource_path,
384
+ (
385
+ "Resource file does not exist: %s, and nothing in the project refers to it"
386
+ % resource_path
387
+ ),
380
388
  resource_path
381
389
  )
382
390
  )
@@ -413,7 +421,9 @@ func _declared_class_name(path: String) -> String:
413
421
  return ""
414
422
  while not file.eof_reached():
415
423
  # Annotations first, since `@abstract class_name X` is one line and this read it as none.
416
- var found: RegExMatch = GdhPatterns.declared_class(GdhPatterns.without_annotations(file.get_line()))
424
+ var found: RegExMatch = GdhPatterns.declared_class(
425
+ GdhPatterns.without_annotations(file.get_line())
426
+ )
417
427
  if found != null:
418
428
  file.close()
419
429
  return found.get_string(1)
@@ -443,11 +453,11 @@ func _usage_files(kinds: Array) -> Array[String]:
443
453
  return found
444
454
 
445
455
 
446
- # Whether [param line], in [param file_path], names the target by path or by UID. Every string on the
447
- # line is read as a path the way the engine reads it: a res:// path as it is, a uid:// by what it
448
- # names, and anything else relative to the file it is in, which is how `preload("sibling.gd")` and a
449
- # plugin.cfg's `script="plugin.gd"` name their files. Only the quoted form of the project path was
450
- # looked for, so neither of those was found, and nor was a scene named by its UID.
456
+ # Whether [param line], in [param file_path], names the target by path or by UID. Every string on
457
+ # the line is read as a path the way the engine reads it: a res:// path as it is, a uid:// by what
458
+ # it names, and anything else relative to the file it is in, which is how `preload("sibling.gd")`
459
+ # and a plugin.cfg's `script="plugin.gd"` name their files. Only the quoted form of the project path
460
+ # was looked for, so neither of those was found, and nor was a scene named by its UID.
451
461
  func _names_target(line: String, file_path: String, target: Dictionary) -> bool:
452
462
  var path: String = target["path"]
453
463
  var uid: String = target["uid"]
@@ -566,15 +576,17 @@ func _count_recursive(deps: Array[Dictionary]) -> int:
566
576
  return count
567
577
 
568
578
 
569
- func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk) -> Array[Dictionary]:
579
+ func _analyze_resource(
580
+ path: String, current_depth: int, walk: GdhDependencyWalk
581
+ ) -> Array[Dictionary]:
570
582
  var deps: Array[Dictionary] = []
571
583
 
572
584
  if current_depth >= walk.max_depth:
573
585
  return deps
574
586
 
575
- # Reused only when it was walked with as much depth left as now. A resource first reached deep in
576
- # the walk has a list cut short at the limit, and handing that to a nearer reach answered with a
577
- # chain that stopped where nothing stopped it.
587
+ # Reused only when it was walked with as much depth left as now. A resource first reached deep
588
+ # in the walk has a list cut short at the limit, and handing that to a nearer reach answered
589
+ # with a chain that stopped where nothing stopped it.
578
590
  var remaining: int = walk.max_depth - current_depth
579
591
  if walk.visited.has(path):
580
592
  var cached: Dictionary = walk.visited[path]
@@ -589,8 +601,9 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
589
601
  var content: String = file.get_as_text()
590
602
  file.close()
591
603
  # A script's comments name paths as examples, and read as references they were answered as
592
- # dependencies, one of a file that does not exist: the shipped serialisation module's comment
593
- # about `["res://addons/x/plugin.cfg"]` was a dependency of every project with the addon.
604
+ # dependencies, one of a file that does not exist: the shipped serialisation module's
605
+ # comment about `["res://addons/x/plugin.cfg"]` was a dependency of every project with the
606
+ # addon.
594
607
  if path.get_extension() == "gd":
595
608
  content = _code_lines(content)
596
609
  elif path.get_extension() in SHADER_KINDS:
@@ -604,8 +617,8 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
604
617
  var written: String = _resolved(_referenced_path(m.get_string()), path)
605
618
  var dep_path: String = str(moved.get(written, written))
606
619
 
607
- # A uid the project does not know names no file, and loading it fails: passed over as
608
- # no res:// path, a script's load of one went unsaid while the game printed
620
+ # A uid the project does not know names no file, and loading it fails: passed over
621
+ # as no res:// path, a script's load of one went unsaid while the game printed
609
622
  # "Unrecognized UID" at that line.
610
623
  if dep_path.begins_with("uid://"):
611
624
  _depend(deps, path, dep_path, {}, current_depth, walk)
@@ -613,7 +626,8 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
613
626
  if not dep_path.begins_with("res://"):
614
627
  continue
615
628
  # A literal naming a directory, or the start of a path a script builds as it runs
616
- # ("res://levels/" + name), names no file, and walked as one it was a missing dependency.
629
+ # ("res://levels/" + name), names no file, and walked as one it was a missing
630
+ # dependency.
617
631
  if dep_path.get_extension().is_empty():
618
632
  continue
619
633
 
@@ -623,32 +637,34 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
623
637
  if not walk.include_built_in and dep_path.begins_with("res://."):
624
638
  continue
625
639
 
626
- var named_by: Dictionary = {"stale_path": written} if dep_path != written else {}
640
+ var named_by: Dictionary = { "stale_path": written } if dep_path != written else {}
627
641
  if unknown.has(dep_path):
628
642
  named_by["unknown_uid"] = unknown[dep_path]
629
643
  _depend(deps, path, dep_path, named_by, current_depth, walk)
630
644
 
631
- # A script uses another by its class_name as surely as by a preload, and only the reverse walk
632
- # looked for that: asked what a script depends on, the answer left out the script it typed a
633
- # variable with, while asking what used that script named this one. An autoload's global
634
- # name is the same kind of use, of the script registered under it.
645
+ # A script uses another by its class_name as surely as by a preload, and only the reverse
646
+ # walk looked for that: asked what a script depends on, the answer left out the script it
647
+ # typed a variable with, while asking what used that script named this one. An autoload's
648
+ # global name is the same kind of use, of the script registered under it.
635
649
  if path.get_extension() == "gd":
636
650
  var classes: Dictionary = classes_declared()
637
651
  for used: String in classes_used_in(content, classes):
638
- _depend(deps, path, str(classes[used]), {"by_class": used}, current_depth, walk)
652
+ _depend(deps, path, str(classes[used]), { "by_class": used }, current_depth, walk)
639
653
  var autoloads: Dictionary = autoloads_declared()
640
654
  for used: String in classes_used_in(content, autoloads):
641
- _depend(deps, path, str(autoloads[used]), {"by_autoload": used}, current_depth, walk)
655
+ _depend(
656
+ deps, path, str(autoloads[used]), { "by_autoload": used }, current_depth, walk
657
+ )
642
658
 
643
659
  walk.path_stack.pop_back()
644
- walk.visited[path] = {"deps": deps, "remaining": remaining}
660
+ walk.visited[path] = { "deps": deps, "remaining": remaining }
645
661
  return deps
646
662
 
647
663
 
648
- # Adds [param dep_path] to [param deps], what [param path] depends on, walked as far as the depth left
649
- # allows, once however many times it is named. [param named_by] is the class or autoload name it was
650
- # named by, as by_class or by_autoload, and the path written when the engine loads another, as
651
- # stale_path; empty for a path that is the file.
664
+ # Adds [param dep_path] to [param deps], what [param path] depends on, walked as far as the depth
665
+ # left allows, once however many times it is named. [param named_by] is the class or autoload name
666
+ # it was named by, as by_class or by_autoload, and the path written when the engine loads another,
667
+ # as stale_path; empty for a path that is the file.
652
668
  func _depend(
653
669
  deps: Array[Dictionary],
654
670
  path: String,
@@ -662,14 +678,15 @@ func _depend(
662
678
  for existing: Dictionary in deps:
663
679
  if existing.get("path", "") == dep_path:
664
680
  return
665
- # A uid still written as one is one the project does not know, and asked about, the engine prints
666
- # an error for it.
681
+ # A uid still written as one is one the project does not know, and asked about, the engine
682
+ # prints an error for it.
667
683
  var dep_info: Dictionary = {
668
- "path": dep_path, "exists": not dep_path.begins_with("uid://") and FileAccess.file_exists(dep_path)
684
+ "path": dep_path,
685
+ "exists": not dep_path.begins_with("uid://") and FileAccess.file_exists(dep_path),
669
686
  }
670
687
  dep_info.merge(named_by)
671
- # Marked where the reference closing the cycle is. Marked inside the file reached again, the tree
672
- # answered that file as a dependency of itself.
688
+ # Marked where the reference closing the cycle is. Marked inside the file reached again, the
689
+ # tree answered that file as a dependency of itself.
673
690
  if dep_path in walk.path_stack:
674
691
  var cycle: Array[String] = walk.path_stack.slice(walk.path_stack.find(dep_path))
675
692
  cycle.append(dep_path)
@@ -683,8 +700,8 @@ func _depend(
683
700
  if sub_deps.size() > 0:
684
701
  dep_info["dependencies"] = sub_deps
685
702
  elif dep_info["exists"] and walk.counts_beyond:
686
- # A file the depth stopped at read the same as one naming nothing, and a missing file past it
687
- # went unsaid.
703
+ # A file the depth stopped at read the same as one naming nothing, and a missing file past
704
+ # it went unsaid.
688
705
  var beyond: GdhDependencyWalk = GdhDependencyWalk.new(1, walk.include_built_in, [], false)
689
706
  var named: int = _analyze_resource(dep_path, 0, beyond).size()
690
707
  if named > 0:
@@ -694,7 +711,7 @@ func _depend(
694
711
 
695
712
  ## Every autoload the project registers as a global name, with its script. One that is not global
696
713
  ## is no name code can use.
697
- static func autoloads_declared() -> Dictionary:
714
+ func autoloads_declared() -> Dictionary:
698
715
  var autoloads: Dictionary = {}
699
716
  for property: Dictionary in ProjectSettings.get_property_list():
700
717
  var setting: String = str(property.get("name", ""))
@@ -728,12 +745,12 @@ func list_script_classes(_params: Dictionary) -> Dictionary:
728
745
  names.sort()
729
746
  var classes: Array[Dictionary] = []
730
747
  for name: Variant in names:
731
- classes.append({"name": str(name), "path": str(declared[name])})
732
- return {"classes": classes, "count": classes.size()}
748
+ classes.append({ "name": str(name), "path": str(declared[name]) })
749
+ return { "classes": classes, "count": classes.size() }
733
750
 
734
751
 
735
- ## The names in [param classes] that the code in [param content] uses, in the order first used: words
736
- ## outside strings and comments, so a class named in prose or in a message is not a use of it.
752
+ ## The names in [param classes] that the code in [param content] uses, in the order first used:
753
+ ## words outside strings and comments, so a class named in prose or in a message is not a use of it.
737
754
  static func classes_used_in(content: String, classes: Dictionary) -> Array[String]:
738
755
  var used: Array[String] = []
739
756
  if classes.is_empty():
@@ -747,26 +764,63 @@ static func classes_used_in(content: String, classes: Dictionary) -> Array[Strin
747
764
  return used
748
765
 
749
766
 
750
- # [param written] as the res:// path the engine would load from [param from]: a uid:// by what the
751
- # project's uid cache says it names, and a relative path against the referring file's directory.
752
- # Both were passed over as not being res:// paths, so a script's `preload("sibling.gd")` was no
753
- # dependency at all.
754
- static func _resolved(written: String, from: String) -> String:
767
+ # [param written] as the res:// path the engine would load from [param from]: a uid:// by the file
768
+ # it names, and a relative path against the referring file's directory. Both were passed over as not
769
+ # being res:// paths, so a script's `preload("sibling.gd")` was no dependency at all.
770
+ func _resolved(written: String, from: String) -> String:
755
771
  if written.begins_with("res://"):
756
772
  return written
757
773
  if written.begins_with("uid://"):
758
- var id: int = ResourceUID.text_to_id(written)
759
- return ResourceUID.get_id_path(id) if ResourceUID.has_id(id) else written
774
+ var found: String = _uid_path(written)
775
+ return written if found.is_empty() else found
760
776
  if written.contains("://"):
761
777
  return written
762
778
  return from.get_base_dir().path_join(written).simplify_path()
763
779
 
764
780
 
781
+ # The file the uid:// [param text] names, or empty when nothing does: the engine's uid cache first,
782
+ # then the uids the project's files declare. Only an import writes the cache, so on a fresh clone
783
+ # every uid was answered as naming no file, and a scene moved outside the editor was missing under
784
+ # its old path, although the first import loads it by its uid. A cached path with no file there
785
+ # gives way to a file declaring the uid, and is the answer when none does.
786
+ func _uid_path(text: String) -> String:
787
+ var id: int = ResourceUID.text_to_id(text)
788
+ if id == ResourceUID.INVALID_ID:
789
+ return ""
790
+ var cached: String = ResourceUID.get_id_path(id) if ResourceUID.has_id(id) else ""
791
+ if FileAccess.file_exists(cached):
792
+ return cached
793
+ return str(_files_by_uid().get(id, cached))
794
+
795
+
796
+ # Every file of the project that declares a uid, by its uid's id: in a .uid sidecar, an import
797
+ # sidecar or the header of a text resource, as GdhResourceFiles reads them. Read once for the call,
798
+ # the first time the cache does not know a uid.
799
+ func _files_by_uid() -> Dictionary:
800
+ if _uids_read:
801
+ return _uids_declared
802
+ _uids_read = true
803
+ var declaring: Dictionary = {}
804
+ for file: String in _files.find_files_with_extensions(
805
+ "res://", ["uid", "import"] + GdhResourceFiles.TEXT_RESOURCE_KINDS
806
+ ):
807
+ var sidecar: bool = file.get_extension() in ["uid", "import"]
808
+ declaring[file.get_basename() if sidecar else file] = true
809
+ for file: String in declaring:
810
+ if not FileAccess.file_exists(file):
811
+ continue
812
+ var id: int = ResourceUID.text_to_id(str(GdhResourceFiles.uid_of(file).get("uid", "")))
813
+ if id != ResourceUID.INVALID_ID:
814
+ _uids_declared[id] = file
815
+ return _uids_declared
816
+
817
+
765
818
  # The ext_resource paths in [param content] whose uid leads elsewhere, each with the file the uid
766
819
  # names. The engine loads an ext_resource by its uid whenever the uid is known, whatever the path
767
- # says, so a file moved outside the editor still loads, and the walk called it missing; every pattern
768
- # reads the old path, the bare res:// one included, so the paths are mapped before any of them runs.
769
- static func _moved_by_uid(content: String) -> Dictionary:
820
+ # says, so a file moved outside the editor still loads, and the walk called it missing; every
821
+ # pattern reads the old path, the bare res:// one included, so the paths are mapped before any of
822
+ # them runs.
823
+ func _moved_by_uid(content: String) -> Dictionary:
770
824
  var moved: Dictionary = {}
771
825
  for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
772
826
  var found: String = _loaded_by_uid(line.get_string())
@@ -779,7 +833,7 @@ static func _moved_by_uid(content: String) -> Dictionary:
779
833
  # The ext_resource paths in [param content], read from [param from], whose uid names no file the
780
834
  # project has, each with that uid. The engine warns that the uid is invalid and loads the path, and
781
835
  # the walk followed the path saying nothing, so an export was the first to mention it.
782
- static func _unknown_uids(content: String, from: String) -> Dictionary:
836
+ func _unknown_uids(content: String, from: String) -> Dictionary:
783
837
  var unknown: Dictionary = {}
784
838
  for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
785
839
  var uid: String = _unknown_uid_of(line.get_string())
@@ -790,21 +844,18 @@ static func _unknown_uids(content: String, from: String) -> Dictionary:
790
844
 
791
845
 
792
846
  # The uid an ext_resource [param entry] carries when it names no file the project has, or empty.
793
- static func _unknown_uid_of(entry: String) -> String:
847
+ func _unknown_uid_of(entry: String) -> String:
794
848
  var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
795
849
  return "" if uid == null or not _loaded_by_uid(entry).is_empty() else uid.get_string(1)
796
850
 
797
851
 
798
852
  # The file an ext_resource [param entry] is loaded from by its uid, or empty when it carries none
799
853
  # the project knows, and the engine falls back to the path.
800
- static func _loaded_by_uid(entry: String) -> String:
854
+ func _loaded_by_uid(entry: String) -> String:
801
855
  var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
802
856
  if uid == null:
803
857
  return ""
804
- var id: int = ResourceUID.text_to_id(uid.get_string(1))
805
- if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
806
- return ""
807
- var found: String = ResourceUID.get_id_path(id)
858
+ var found: String = _uid_path(uid.get_string(1))
808
859
  return found if FileAccess.file_exists(found) else ""
809
860
 
810
861
 
@@ -838,8 +889,8 @@ static func _shader_code(content: String) -> String:
838
889
  return "".join(PackedStringArray(kept))
839
890
 
840
891
 
841
- # The path inside a preload(), load(), #include or ext_resource match; a bare res:// match is the path.
842
- # An ext_resource is read first, since its type or its path can hold the word load.
892
+ # The path inside a preload(), load(), #include or ext_resource match; a bare res:// match is the
893
+ # path. An ext_resource is read first, since its type or its path can hold the word load.
843
894
  func _referenced_path(matched: String) -> String:
844
895
  var expression: String = ""
845
896
  if matched.begins_with("ext_resource"):
@@ -851,3 +902,33 @@ func _referenced_path(matched: String) -> String:
851
902
  var inner: RegEx = GdhPatterns.compiled(expression)
852
903
  var inner_match: RegExMatch = inner.search(matched)
853
904
  return inner_match.get_string(1) if inner_match else matched
905
+
906
+
907
+ # Everything one dependency walk carries: the settings it was started with, and the state it
908
+ # accumulates as it recurses. Kept together so the recursion passes one object rather than
909
+ # six positional arguments that have to stay in the same order at every call site.
910
+ class GdhDependencyWalk:
911
+ var max_depth: int
912
+
913
+ var include_built_in: bool
914
+
915
+ var visited: Dictionary = {}
916
+
917
+ var path_stack: Array[String] = []
918
+
919
+ var circular_references: Array
920
+
921
+ ## Whether a file the depth stops at is read for how many files it names, which the walk doing
922
+ ## that reading does not do again for those.
923
+ var counts_beyond: bool
924
+
925
+ func _init(
926
+ p_max_depth: int,
927
+ p_include_built_in: bool,
928
+ p_circular_references: Array,
929
+ p_counts_beyond: bool = true
930
+ ) -> void:
931
+ max_depth = p_max_depth
932
+ include_built_in = p_include_built_in
933
+ circular_references = p_circular_references
934
+ counts_beyond = p_counts_beyond
@@ -1,5 +1,4 @@
1
1
  extends RefCounted
2
-
3
2
  ## The editor addon's scene and resource tools, run by a headless engine for a project no editor
4
3
  ## serves.
5
4
  ##
@@ -16,7 +15,6 @@ const GdhSignalTools = preload("../addons/gdharness_editor/tools/signal_tools.gd
16
15
  const GdhTilesetTools = preload("../addons/gdharness_editor/tools/tileset_tools.gd")
17
16
  const GdhSceneFile = preload("../addons/gdharness_editor/scene_file.gd")
18
17
  const GdhSceneUids = preload("../addons/gdharness_editor/scene_uids.gd")
19
-
20
18
  const SCENE_COMMANDS: Array[String] = [
21
19
  "create_scene",
22
20
  "save_scene",
@@ -58,7 +56,7 @@ static func run(params: Dictionary) -> Dictionary:
58
56
  var command: String = str(params.get("command", ""))
59
57
  var given: Variant = params.get("arguments", {})
60
58
  if not given is Dictionary:
61
- return {"ok": false, "error": "arguments must be an object."}
59
+ return { "ok": false, "error": "arguments must be an object." }
62
60
  var arguments: Dictionary = given
63
61
  var tools: Node = null
64
62
  if command in SCENE_COMMANDS:
@@ -76,7 +74,7 @@ static func run(params: Dictionary) -> Dictionary:
76
74
  elif command in ANIMATION_STATE_COMMANDS:
77
75
  tools = GdhAnimationStates.new()
78
76
  else:
79
- return {"ok": false, "error": "No editor tool runs headless as " + command}
77
+ return { "ok": false, "error": "No editor tool runs headless as " + command }
80
78
  var answer: Variant = tools.call(command, arguments)
81
79
  tools.free()
82
80
  if answer is Dictionary:
@@ -86,19 +84,21 @@ static func run(params: Dictionary) -> Dictionary:
86
84
  if not GdhSceneUids.by_path.is_empty():
87
85
  answered["namedByPath"] = (
88
86
  (
89
- "What this saved names %s by path rather than by uid, which loads the same. A uid in a "
90
- + "file's own header is resolved through the project's uid cache, which holds none for "
91
- + "%s yet, as with a file made since the project was last scanned, so a uid written "
92
- + "now would warn on every load. project_import refresh_uids scans the project, and "
93
- + "writes after it name %s by uid."
87
+ "What this saved names %s by path rather than by uid, which loads the same. A "
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."
94
92
  )
95
93
  % [
96
94
  ", ".join(GdhSceneUids.by_path),
97
95
  "it" if GdhSceneUids.by_path.size() == 1 else "them",
98
96
  "it" if GdhSceneUids.by_path.size() == 1 else "them",
97
+ "it" if GdhSceneUids.by_path.size() == 1 else "them",
99
98
  ]
100
99
  )
101
100
  return answered
102
101
  return {
103
- "ok": false, "error": "%s answered %s rather than an object." % [command, type_string(typeof(answer))]
102
+ "ok": false,
103
+ "error": "%s answered %s rather than an object." % [command, type_string(typeof(answer))],
104
104
  }