gdharness 1.3.41 → 1.3.43

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 (107) hide show
  1. package/build/cli.js +131 -47
  2. package/build/godot/addons/auto_reload/auto_reload.gd +18 -19
  3. package/build/godot/addons/gdharness_editor/animation_reads.gd +84 -35
  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 +114 -39
  7. package/build/godot/addons/gdharness_editor/file_walk.gd +12 -5
  8. package/build/godot/addons/gdharness_editor/headless_check.gd +94 -41
  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 +37 -12
  13. package/build/godot/addons/gdharness_editor/same_values.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +136 -79
  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 +41 -28
  20. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +84 -50
  21. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +70 -40
  22. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +34 -30
  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 +181 -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 +75 -43
  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 +66 -0
  33. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +171 -62
  35. package/build/godot/addons/gdharness_editor/tools/script_paths.gd +20 -8
  36. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +11 -8
  37. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +47 -34
  38. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +77 -50
  39. package/build/godot/addons/gdharness_editor/value_shapes.gd +5 -6
  40. package/build/godot/addons/gdharness_runtime/call_errors.gd +1 -2
  41. package/build/godot/addons/gdharness_runtime/declared_values.gd +68 -32
  42. package/build/godot/addons/gdharness_runtime/error_report.gd +1 -2
  43. package/build/godot/addons/gdharness_runtime/reading.gd +37 -12
  44. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +84 -63
  45. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +153 -148
  46. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +64 -42
  47. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +184 -122
  48. package/build/godot/addons/gdharness_runtime/runtime_frames.gd +2 -2
  49. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +24 -20
  50. package/build/godot/addons/gdharness_runtime/runtime_input.gd +134 -92
  51. package/build/godot/addons/gdharness_runtime/runtime_items.gd +29 -13
  52. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +0 -1
  53. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +38 -20
  54. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +17 -13
  55. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +84 -46
  56. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +117 -79
  57. package/build/godot/addons/gdharness_runtime/runtime_says.gd +14 -17
  58. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +23 -16
  59. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +117 -66
  60. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +211 -0
  61. package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
  62. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +44 -27
  63. package/build/godot/addons/gdharness_runtime/runtime_values.gd +108 -73
  64. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +111 -84
  65. package/build/godot/addons/gdharness_runtime/runtime_words.gd +10 -7
  66. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -51
  67. package/build/godot/addons/gdharness_runtime/value_shapes.gd +5 -6
  68. package/build/godot/addons/gdharness_tests/bench.gd +4 -5
  69. package/build/godot/addons/gdharness_tests/event.gd +10 -5
  70. package/build/godot/addons/gdharness_tests/share.gd +38 -18
  71. package/build/godot/addons/gdharness_tests/suites.gd +3 -3
  72. package/build/godot/addons/gdharness_tests/wait.gd +3 -2
  73. package/build/godot/loader/loader.gd +9 -9
  74. package/build/godot/operations/animation_reads.gd +84 -35
  75. package/build/godot/operations/audio_buses.gd +48 -19
  76. package/build/godot/operations/class_cache.gd +16 -11
  77. package/build/godot/operations/classdb_queries.gd +89 -44
  78. package/build/godot/operations/declared_values.gd +68 -32
  79. package/build/godot/operations/dependencies.gd +139 -93
  80. package/build/godot/operations/editor_tools.gd +9 -10
  81. package/build/godot/operations/export_presets.gd +100 -54
  82. package/build/godot/operations/file_reads.gd +114 -39
  83. package/build/godot/operations/file_walk.gd +12 -5
  84. package/build/godot/operations/gdscript_analysis.gd +105 -66
  85. package/build/godot/operations/gdscript_authoring.gd +138 -39
  86. package/build/godot/operations/godot_operations.gd +17 -11
  87. package/build/godot/operations/import_pipeline.gd +110 -59
  88. package/build/godot/operations/input_actions.gd +63 -39
  89. package/build/godot/operations/patterns.gd +3 -1
  90. package/build/godot/operations/plugins.gd +36 -16
  91. package/build/godot/operations/project_config.gd +144 -27
  92. package/build/godot/operations/project_diagnostics.gd +24 -11
  93. package/build/godot/operations/project_file.gd +16 -16
  94. package/build/godot/operations/project_validation.gd +71 -44
  95. package/build/godot/operations/property_paths.gd +22 -12
  96. package/build/godot/operations/property_values.gd +143 -78
  97. package/build/godot/operations/reading.gd +37 -12
  98. package/build/godot/operations/rename_checks.gd +2 -2
  99. package/build/godot/operations/resource_files.gd +11 -9
  100. package/build/godot/operations/same_values.gd +4 -4
  101. package/build/godot/operations/scene_connections.gd +27 -13
  102. package/build/godot/operations/script_check.gd +52 -49
  103. package/build/godot/operations/serialisation.gd +75 -51
  104. package/build/godot/operations/setting_reads.gd +7 -6
  105. package/build/godot/operations/value_shapes.gd +5 -6
  106. package/build/godot/tests/listed.gd +321 -268
  107. 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\\]\\)]+",
@@ -39,28 +38,6 @@ var _classes_declared: Dictionary = {}
39
38
  var _classes_read: bool = false
40
39
 
41
40
 
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
62
-
63
-
64
41
  func _init(p_log: GdhLog) -> void:
65
42
  _log = p_log
66
43
 
@@ -88,7 +65,9 @@ func get_dependencies(params: Dictionary) -> Dictionary:
88
65
  return _log.failure(
89
66
  _files.with_nearest_files("Resource file does not exist: " + full_path, full_path)
90
67
  )
91
- var walk: GdhDependencyWalk = GdhDependencyWalk.new(max_depth, include_built_in, circular_references)
68
+ var walk: GdhDependencyWalk = GdhDependencyWalk.new(
69
+ max_depth, include_built_in, circular_references
70
+ )
92
71
  dependencies[full_path] = _analyze_resource(full_path, 0, walk)
93
72
 
94
73
  var references: int = 0
@@ -105,7 +84,7 @@ func get_dependencies(params: Dictionary) -> Dictionary:
105
84
  # `exists: false` among the entries, which a caller reading the summary never met.
106
85
  var missing_files: Array[Dictionary] = []
107
86
  for path: String in missing:
108
- missing_files.append({"path": path, "referenced_by": missing[path]})
87
+ missing_files.append({ "path": path, "referenced_by": missing[path] })
109
88
 
110
89
  return {
111
90
  "dependencies": dependencies,
@@ -119,8 +98,8 @@ func get_dependencies(params: Dictionary) -> Dictionary:
119
98
  "total_dependencies": distinct.size(),
120
99
  "total_references": references,
121
100
  "circular_count": circular_references.size(),
122
- "missing": missing_files
123
- }
101
+ "missing": missing_files,
102
+ },
124
103
  }
125
104
 
126
105
 
@@ -137,10 +116,14 @@ func missing_references() -> Array[Dictionary]:
137
116
  var naming: Array = missing.get_or_add(path, [])
138
117
  if not file in naming:
139
118
  naming.append(file)
140
- for file: String in _files.find_files_with_extensions("res://", GdhResourceFiles.TEXT_RESOURCE_KINDS):
119
+ for file: String in _files.find_files_with_extensions(
120
+ "res://", GdhResourceFiles.TEXT_RESOURCE_KINDS
121
+ ):
141
122
  var content: String = FileAccess.get_file_as_string(file)
142
123
  var moved: Dictionary = _moved_by_uid(content)
143
- for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
124
+ for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(
125
+ content
126
+ ):
144
127
  var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line.get_string())
145
128
  if at == null or moved.has(at.get_string(1)):
146
129
  continue
@@ -162,7 +145,7 @@ func missing_references() -> Array[Dictionary]:
162
145
  for path: String in paths:
163
146
  var naming: Array = missing[path]
164
147
  naming.sort()
165
- found.append({"path": path, "referenced_by": naming})
148
+ found.append({ "path": path, "referenced_by": naming })
166
149
  return found
167
150
 
168
151
 
@@ -183,12 +166,16 @@ func _loaded_and_missing(script: String) -> Array[String]:
183
166
  continue
184
167
  var calls: Dictionary = {}
185
168
  for loading: RegExMatch in (
186
- GdhPatterns.compiled("(?:(?<![\\w.])|ResourceLoader\\.)load\\(\\s*(?=[\"'])").search_all(code)
169
+ GdhPatterns
170
+ . compiled("(?:(?<![\\w.])|ResourceLoader\\.)load\\(\\s*(?=[\"'])")
171
+ . search_all(code)
187
172
  ):
188
173
  calls[loading.get_end()] = true
189
174
  for literal: RegExMatch in GdhPatterns.compiled(STRING_LITERAL).search_all(code):
190
175
  var single: String = literal.get_string(2)
191
- var path: String = _resolved(literal.get_string(1) if single.is_empty() else single, script)
176
+ var path: String = _resolved(
177
+ literal.get_string(1) if single.is_empty() else single, script
178
+ )
192
179
  if calls.has(literal.get_start()):
193
180
  loaded.append(path)
194
181
  else:
@@ -200,7 +187,11 @@ func _loaded_and_missing(script: String) -> Array[String]:
200
187
  if not named.has(path) and not missing.has(path):
201
188
  missing.append(path)
202
189
  continue
203
- if not path.begins_with("res://") or path.begins_with("res://.") or path.get_extension().is_empty():
190
+ if (
191
+ not path.begins_with("res://")
192
+ or path.begins_with("res://.")
193
+ or path.get_extension().is_empty()
194
+ ):
204
195
  continue
205
196
  if not named.has(path) and not FileAccess.file_exists(path) and not missing.has(path):
206
197
  missing.append(path)
@@ -239,7 +230,11 @@ static func missing_line(entry: Dictionary) -> String:
239
230
  var naming: PackedStringArray = PackedStringArray(referring)
240
231
  return (
241
232
  "%s does not exist, and %s %s to it"
242
- % [str(entry.get("path", "")), ", ".join(naming), "refers" if naming.size() == 1 else "refer"]
233
+ % [
234
+ str(entry.get("path", "")),
235
+ ", ".join(naming),
236
+ "refers" if naming.size() == 1 else "refer",
237
+ ]
243
238
  )
244
239
 
245
240
 
@@ -276,7 +271,7 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
276
271
  by_autoload = GdhPatterns.compiled("\\b" + _regex_escaped(autoload_named) + "\\b")
277
272
  var target: Dictionary = {
278
273
  "path": resource_path,
279
- "uid": str(GdhResourceFiles.uid_of(resource_path).get("uid", "")) if exists else ""
274
+ "uid": str(GdhResourceFiles.uid_of(resource_path).get("uid", "")) if exists else "",
280
275
  }
281
276
 
282
277
  var all_files: Array[String] = _usage_files(file_types)
@@ -296,9 +291,9 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
296
291
  var text: String = file.get_as_text()
297
292
  file.close()
298
293
  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.
294
+ # A class is used by name only in a script's code. In a scene or a resource the same word is
295
+ # a node's name or a caption, and each was counted as a use of the class; a shader has no
296
+ # use for one at all.
302
297
  var code: bool = file_path.get_extension() == "gd"
303
298
  # In a shader # opens a directive, so an #include was answered as a comment, and its
304
299
  # comments are // and /* */.
@@ -326,9 +321,13 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
326
321
  var trimmed: String = line.strip_edges()
327
322
  var kind: String = ""
328
323
  var extra: Dictionary = {}
329
- var loaded: String = _loaded_by_uid(line) if trimmed.begins_with("[ext_resource") else ""
324
+ var loaded: String = (
325
+ _loaded_by_uid(line) if trimmed.begins_with("[ext_resource") else ""
326
+ )
330
327
  var written: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line)
331
- var written_path: String = "" if written == null else _resolved(written.get_string(1), file_path)
328
+ var written_path: String = (
329
+ "" if written == null else _resolved(written.get_string(1), file_path)
330
+ )
332
331
  # A mention in a comment is not a use, and both were answered as one. The question a
333
332
  # reverse walk is asked is whether anything still uses a class, and a project that
334
333
  # documents itself names its collaborators in `##` links: four references to one class,
@@ -339,10 +338,14 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
339
338
  kind = "doc"
340
339
  elif (code and trimmed.begins_with("#")) or name_in_comment:
341
340
  kind = "comment"
342
- elif not shader_lines.is_empty() and not _names_target(shader_lines[i], file_path, target):
341
+ elif (
342
+ not shader_lines.is_empty()
343
+ and not _names_target(shader_lines[i], file_path, target)
344
+ ):
343
345
  kind = "comment"
344
346
  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.
347
+ # The engine loads the file the uid names, so the path written here is used by
348
+ # nothing.
346
349
  kind = "stale_path"
347
350
  extra["loads"] = loaded
348
351
  elif by_its_path:
@@ -362,7 +365,7 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
362
365
  if GdhPatterns.without_annotations(line).begins_with("extends ")
363
366
  else "class_name"
364
367
  )
365
- var found: Dictionary = {"line": i + 1, "kind": kind, "text": trimmed}
368
+ var found: Dictionary = { "line": i + 1, "kind": kind, "text": trimmed }
366
369
  found.merge(extra)
367
370
  references.append(found)
368
371
  by_kind[kind] = GdhRead.as_int(by_kind.get(kind, 0)) + 1
@@ -370,13 +373,16 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
370
373
  in_code += 1
371
374
 
372
375
  if not references.is_empty():
373
- usages.append({"file": file_path, "references": references})
376
+ usages.append({ "file": file_path, "references": references })
374
377
  total += references.size()
375
378
 
376
379
  if not exists and usages.is_empty():
377
380
  return _log.failure(
378
381
  _files.with_nearest_files(
379
- "Resource file does not exist: %s, and nothing in the project refers to it" % resource_path,
382
+ (
383
+ "Resource file does not exist: %s, and nothing in the project refers to it"
384
+ % resource_path
385
+ ),
380
386
  resource_path
381
387
  )
382
388
  )
@@ -413,7 +419,9 @@ func _declared_class_name(path: String) -> String:
413
419
  return ""
414
420
  while not file.eof_reached():
415
421
  # 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()))
422
+ var found: RegExMatch = GdhPatterns.declared_class(
423
+ GdhPatterns.without_annotations(file.get_line())
424
+ )
417
425
  if found != null:
418
426
  file.close()
419
427
  return found.get_string(1)
@@ -443,11 +451,11 @@ func _usage_files(kinds: Array) -> Array[String]:
443
451
  return found
444
452
 
445
453
 
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.
454
+ # Whether [param line], in [param file_path], names the target by path or by UID. Every string on
455
+ # the line is read as a path the way the engine reads it: a res:// path as it is, a uid:// by what
456
+ # it names, and anything else relative to the file it is in, which is how `preload("sibling.gd")`
457
+ # and a plugin.cfg's `script="plugin.gd"` name their files. Only the quoted form of the project path
458
+ # was looked for, so neither of those was found, and nor was a scene named by its UID.
451
459
  func _names_target(line: String, file_path: String, target: Dictionary) -> bool:
452
460
  var path: String = target["path"]
453
461
  var uid: String = target["uid"]
@@ -566,15 +574,17 @@ func _count_recursive(deps: Array[Dictionary]) -> int:
566
574
  return count
567
575
 
568
576
 
569
- func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk) -> Array[Dictionary]:
577
+ func _analyze_resource(
578
+ path: String, current_depth: int, walk: GdhDependencyWalk
579
+ ) -> Array[Dictionary]:
570
580
  var deps: Array[Dictionary] = []
571
581
 
572
582
  if current_depth >= walk.max_depth:
573
583
  return deps
574
584
 
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.
585
+ # Reused only when it was walked with as much depth left as now. A resource first reached deep
586
+ # in the walk has a list cut short at the limit, and handing that to a nearer reach answered
587
+ # with a chain that stopped where nothing stopped it.
578
588
  var remaining: int = walk.max_depth - current_depth
579
589
  if walk.visited.has(path):
580
590
  var cached: Dictionary = walk.visited[path]
@@ -589,8 +599,9 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
589
599
  var content: String = file.get_as_text()
590
600
  file.close()
591
601
  # 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.
602
+ # dependencies, one of a file that does not exist: the shipped serialisation module's
603
+ # comment about `["res://addons/x/plugin.cfg"]` was a dependency of every project with the
604
+ # addon.
594
605
  if path.get_extension() == "gd":
595
606
  content = _code_lines(content)
596
607
  elif path.get_extension() in SHADER_KINDS:
@@ -604,8 +615,8 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
604
615
  var written: String = _resolved(_referenced_path(m.get_string()), path)
605
616
  var dep_path: String = str(moved.get(written, written))
606
617
 
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
618
+ # A uid the project does not know names no file, and loading it fails: passed over
619
+ # as no res:// path, a script's load of one went unsaid while the game printed
609
620
  # "Unrecognized UID" at that line.
610
621
  if dep_path.begins_with("uid://"):
611
622
  _depend(deps, path, dep_path, {}, current_depth, walk)
@@ -613,7 +624,8 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
613
624
  if not dep_path.begins_with("res://"):
614
625
  continue
615
626
  # 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.
627
+ # ("res://levels/" + name), names no file, and walked as one it was a missing
628
+ # dependency.
617
629
  if dep_path.get_extension().is_empty():
618
630
  continue
619
631
 
@@ -623,32 +635,34 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
623
635
  if not walk.include_built_in and dep_path.begins_with("res://."):
624
636
  continue
625
637
 
626
- var named_by: Dictionary = {"stale_path": written} if dep_path != written else {}
638
+ var named_by: Dictionary = { "stale_path": written } if dep_path != written else {}
627
639
  if unknown.has(dep_path):
628
640
  named_by["unknown_uid"] = unknown[dep_path]
629
641
  _depend(deps, path, dep_path, named_by, current_depth, walk)
630
642
 
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.
643
+ # A script uses another by its class_name as surely as by a preload, and only the reverse
644
+ # walk looked for that: asked what a script depends on, the answer left out the script it
645
+ # typed a variable with, while asking what used that script named this one. An autoload's
646
+ # global name is the same kind of use, of the script registered under it.
635
647
  if path.get_extension() == "gd":
636
648
  var classes: Dictionary = classes_declared()
637
649
  for used: String in classes_used_in(content, classes):
638
- _depend(deps, path, str(classes[used]), {"by_class": used}, current_depth, walk)
650
+ _depend(deps, path, str(classes[used]), { "by_class": used }, current_depth, walk)
639
651
  var autoloads: Dictionary = autoloads_declared()
640
652
  for used: String in classes_used_in(content, autoloads):
641
- _depend(deps, path, str(autoloads[used]), {"by_autoload": used}, current_depth, walk)
653
+ _depend(
654
+ deps, path, str(autoloads[used]), { "by_autoload": used }, current_depth, walk
655
+ )
642
656
 
643
657
  walk.path_stack.pop_back()
644
- walk.visited[path] = {"deps": deps, "remaining": remaining}
658
+ walk.visited[path] = { "deps": deps, "remaining": remaining }
645
659
  return deps
646
660
 
647
661
 
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.
662
+ # Adds [param dep_path] to [param deps], what [param path] depends on, walked as far as the depth
663
+ # left allows, once however many times it is named. [param named_by] is the class or autoload name
664
+ # it was named by, as by_class or by_autoload, and the path written when the engine loads another,
665
+ # as stale_path; empty for a path that is the file.
652
666
  func _depend(
653
667
  deps: Array[Dictionary],
654
668
  path: String,
@@ -662,14 +676,15 @@ func _depend(
662
676
  for existing: Dictionary in deps:
663
677
  if existing.get("path", "") == dep_path:
664
678
  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.
679
+ # A uid still written as one is one the project does not know, and asked about, the engine
680
+ # prints an error for it.
667
681
  var dep_info: Dictionary = {
668
- "path": dep_path, "exists": not dep_path.begins_with("uid://") and FileAccess.file_exists(dep_path)
682
+ "path": dep_path,
683
+ "exists": not dep_path.begins_with("uid://") and FileAccess.file_exists(dep_path),
669
684
  }
670
685
  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.
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.
673
688
  if dep_path in walk.path_stack:
674
689
  var cycle: Array[String] = walk.path_stack.slice(walk.path_stack.find(dep_path))
675
690
  cycle.append(dep_path)
@@ -683,8 +698,8 @@ func _depend(
683
698
  if sub_deps.size() > 0:
684
699
  dep_info["dependencies"] = sub_deps
685
700
  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.
701
+ # A file the depth stopped at read the same as one naming nothing, and a missing file past
702
+ # it went unsaid.
688
703
  var beyond: GdhDependencyWalk = GdhDependencyWalk.new(1, walk.include_built_in, [], false)
689
704
  var named: int = _analyze_resource(dep_path, 0, beyond).size()
690
705
  if named > 0:
@@ -728,12 +743,12 @@ func list_script_classes(_params: Dictionary) -> Dictionary:
728
743
  names.sort()
729
744
  var classes: Array[Dictionary] = []
730
745
  for name: Variant in names:
731
- classes.append({"name": str(name), "path": str(declared[name])})
732
- return {"classes": classes, "count": classes.size()}
746
+ classes.append({ "name": str(name), "path": str(declared[name]) })
747
+ return { "classes": classes, "count": classes.size() }
733
748
 
734
749
 
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.
750
+ ## The names in [param classes] that the code in [param content] uses, in the order first used:
751
+ ## words outside strings and comments, so a class named in prose or in a message is not a use of it.
737
752
  static func classes_used_in(content: String, classes: Dictionary) -> Array[String]:
738
753
  var used: Array[String] = []
739
754
  if classes.is_empty():
@@ -764,8 +779,9 @@ static func _resolved(written: String, from: String) -> String:
764
779
 
765
780
  # The ext_resource paths in [param content] whose uid leads elsewhere, each with the file the uid
766
781
  # 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.
782
+ # says, so a file moved outside the editor still loads, and the walk called it missing; every
783
+ # pattern reads the old path, the bare res:// one included, so the paths are mapped before any of
784
+ # them runs.
769
785
  static func _moved_by_uid(content: String) -> Dictionary:
770
786
  var moved: Dictionary = {}
771
787
  for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
@@ -838,8 +854,8 @@ static func _shader_code(content: String) -> String:
838
854
  return "".join(PackedStringArray(kept))
839
855
 
840
856
 
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.
857
+ # The path inside a preload(), load(), #include or ext_resource match; a bare res:// match is the
858
+ # path. An ext_resource is read first, since its type or its path can hold the word load.
843
859
  func _referenced_path(matched: String) -> String:
844
860
  var expression: String = ""
845
861
  if matched.begins_with("ext_resource"):
@@ -851,3 +867,33 @@ func _referenced_path(matched: String) -> String:
851
867
  var inner: RegEx = GdhPatterns.compiled(expression)
852
868
  var inner_match: RegExMatch = inner.search(matched)
853
869
  return inner_match.get_string(1) if inner_match else matched
870
+
871
+
872
+ # Everything one dependency walk carries: the settings it was started with, and the state it
873
+ # accumulates as it recurses. Kept together so the recursion passes one object rather than
874
+ # six positional arguments that have to stay in the same order at every call site.
875
+ class GdhDependencyWalk:
876
+ var max_depth: int
877
+
878
+ var include_built_in: bool
879
+
880
+ var visited: Dictionary = {}
881
+
882
+ var path_stack: Array[String] = []
883
+
884
+ var circular_references: Array
885
+
886
+ ## Whether a file the depth stops at is read for how many files it names, which the walk doing
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:
896
+ max_depth = p_max_depth
897
+ include_built_in = p_include_built_in
898
+ circular_references = p_circular_references
899
+ 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,11 +84,11 @@ 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
+ + "uid in a file's own header is resolved through the project's uid cache, "
89
+ + "which holds none for %s yet, as with a file made since the project was last "
90
+ + "scanned, so a uid written now would warn on every load. project_import "
91
+ + "refresh_uids scans the project, and writes after it name %s by uid."
94
92
  )
95
93
  % [
96
94
  ", ".join(GdhSceneUids.by_path),
@@ -100,5 +98,6 @@ static func run(params: Dictionary) -> Dictionary:
100
98
  )
101
99
  return answered
102
100
  return {
103
- "ok": false, "error": "%s answered %s rather than an object." % [command, type_string(typeof(answer))]
101
+ "ok": false,
102
+ "error": "%s answered %s rather than an object." % [command, type_string(typeof(answer))],
104
103
  }