gdharness 1.3.44 → 1.3.46

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.
@@ -92,6 +92,19 @@ func connect_signal(args: Dictionary) -> Dictionary:
92
92
  if unbinds > 0:
93
93
  callable = callable.unbind(unbinds)
94
94
  var method_found: bool = target.has_method(method_name)
95
+ if not method_found and (not binds.is_empty() or unbinds > 0):
96
+ var named_to: String = str(target.name) if target == scene.root else target_node_path
97
+ return scene.refuse(
98
+ (
99
+ (
100
+ "%s has no method %s, and the engine connects a signal that binds or unbinds "
101
+ + "arguments only to a method its target has, so the scene would load without "
102
+ + "the connection. Add the method to the target's script first, or connect "
103
+ + "without binds and unbinds."
104
+ )
105
+ % [named_to, method_name]
106
+ )
107
+ )
95
108
  var misfit: String = _misfit(source, signal_name, callable, flags)
96
109
  if not before.is_empty():
97
110
  var held: Callable = before["callable"]
@@ -102,6 +115,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
102
115
 
103
116
  var from: String = scene.path_of(source)
104
117
  var to: String = scene.path_of(target)
118
+ var _replaced: Dictionary = scene.drop_unmade(from, signal_name, to, method_name)
105
119
  var saved: Dictionary = {}
106
120
  var written: Dictionary = scene.write(
107
121
  func(loaded: Node) -> String:
@@ -464,7 +478,11 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
464
478
  # A connection that was never there saved the scene unchanged and answered as removed, so a
465
479
  # typo in either name read as success.
466
480
  var held: Dictionary = _saved(scene.root, from, signal_name, to, method_name)
467
- if held.is_empty():
481
+ # One the engine could not make is in the file alone, and the write drops it.
482
+ var unmade: Dictionary = scene.drop_unmade(from, signal_name, to, method_name)
483
+ if held.is_empty() and not unmade.is_empty():
484
+ held = { "flags": unmade["flags"], "binds": unmade["binds"], "unbinds": unmade["unbinds"] }
485
+ elif held.is_empty():
468
486
  # The root's path is ".", which before ".method" read as "..method".
469
487
  var said_from: String = str(source.name) if source == scene.root else from
470
488
  var said_to: String = str(target.name) if target == scene.root else to
@@ -492,8 +510,9 @@ func disconnect_signal(args: Dictionary) -> Dictionary:
492
510
  )
493
511
  )
494
512
  )
495
- var callable: Callable = held["callable"]
496
- source.disconnect(signal_name, callable)
513
+ if held.has("callable"):
514
+ var callable: Callable = held["callable"]
515
+ source.disconnect(signal_name, callable)
497
516
 
498
517
  var written: Dictionary = scene.write(
499
518
  func(loaded: Node) -> String:
@@ -303,6 +303,13 @@ func set_tilemap_cells(args: Dictionary) -> Dictionary:
303
303
  wanted.append([at, source_id, atlas_coords, alternative])
304
304
  if source_id == -1:
305
305
  erased += 1
306
+ # An erased cell leaves the layer's data at its next update, which a layer outside the tree
307
+ # never has, so the pack wrote it as a cell naming no tile, where the editor's save leaves it
308
+ # out.
309
+ if layered != null:
310
+ layered.update_internals()
311
+ else:
312
+ single.update_internals()
306
313
 
307
314
  var at_path: String = scene.path_of(node)
308
315
  var written: Dictionary = scene.write(
@@ -62,6 +62,15 @@ func set_property(params: Dictionary) -> Dictionary:
62
62
  "type": "error",
63
63
  "message": GdhPaths.nothing_there(holder, named, str(reached["called"])),
64
64
  }
65
+ if GdhPaths.is_constant(holder, named):
66
+ return {
67
+ "type": "error",
68
+ "message":
69
+ (
70
+ "%s:%s is a constant its script declares, and a constant cannot be set"
71
+ % [reached["called"], named]
72
+ ),
73
+ }
65
74
  var old_value: Variant = GdhPaths.read_under(holder, named)
66
75
  var slot: Dictionary = GdhValues.slot_declared(holder, named)
67
76
  var declared: int = slot["type"]
@@ -503,7 +503,7 @@ static func can_read(holder: Variant, named: String) -> bool:
503
503
  var object: Object = holder
504
504
  if not method.is_empty():
505
505
  return call_refused(object, method, "", args.size()).is_empty()
506
- return _has_property(object, named)
506
+ return _has_property(object, named) or not constant_of(object, named).is_empty()
507
507
  return false
508
508
 
509
509
 
@@ -876,7 +876,7 @@ static func read_under(holder: Variant, named: String) -> Variant:
876
876
  if not method.is_empty():
877
877
  var args: Array = arguments_of(named)["args"]
878
878
  return object.callv(method, args)
879
- return object.get(named)
879
+ return constant_of(object, named)[0] if is_constant(object, named) else object.get(named)
880
880
 
881
881
 
882
882
  ## Where [param named] lands in a list of [param size], or -1 for a step that is not in it.
@@ -1030,3 +1030,30 @@ static func _has_property(holder: Object, named: String) -> bool:
1030
1030
  if str(entry["name"]) == named:
1031
1031
  return true
1032
1032
  return false
1033
+
1034
+
1035
+ ## The constant [param named] that [param holder]'s script or a script it extends declares, in a
1036
+ ## list of one, or an empty list. A constant is no property, so the property list never names it,
1037
+ ## and `get` answers null for one.
1038
+ static func constant_of(holder: Variant, named: String) -> Array:
1039
+ if not holder is Object:
1040
+ return []
1041
+ var object: Object = holder
1042
+ var attached: Variant = object.get_script()
1043
+ if not attached is Script:
1044
+ return []
1045
+ var script: Script = attached
1046
+ while script != null:
1047
+ var constants: Dictionary = script.get_script_constant_map()
1048
+ if constants.has(named):
1049
+ return [constants[named]]
1050
+ script = script.get_base_script()
1051
+ return []
1052
+
1053
+
1054
+ ## Whether [param named] reaches a constant on [param holder] rather than a property.
1055
+ static func is_constant(holder: Variant, named: String) -> bool:
1056
+ if not holder is Object:
1057
+ return false
1058
+ var object: Object = holder
1059
+ return not _has_property(object, named) and not constant_of(object, named).is_empty()
@@ -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
 
@@ -11,6 +11,11 @@ const LOOP_MODE_NAMES: Dictionary[int, String] = {
11
11
  Animation.LOOP_LINEAR: "linear",
12
12
  Animation.LOOP_PINGPONG: "pingpong",
13
13
  }
14
+ const UPDATE_MODE_NAMES: Dictionary[int, String] = {
15
+ Animation.UPDATE_CONTINUOUS: "continuous",
16
+ Animation.UPDATE_DISCRETE: "discrete",
17
+ Animation.UPDATE_CAPTURE: "capture",
18
+ }
14
19
  const TRACK_TYPE_NAMES: Dictionary[int, String] = {
15
20
  Animation.TYPE_VALUE: "property",
16
21
  Animation.TYPE_POSITION_3D: "position_3d",
@@ -166,6 +171,9 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
166
171
  var property: String = str(path.get_concatenated_subnames())
167
172
  if not property.is_empty():
168
173
  shaped["property"] = property
174
+ if kind == Animation.TYPE_VALUE:
175
+ var mode: int = anim.value_track_get_update_mode(track)
176
+ shaped["update"] = UPDATE_MODE_NAMES.get(mode, str(mode))
169
177
  var keyframes: Array[Dictionary] = []
170
178
  var methods: Dictionary = {}
171
179
  for key: int in anim.track_get_key_count(track):
@@ -160,8 +160,7 @@ func missing_references() -> Array[Dictionary]:
160
160
  func _loaded_and_missing(script: String) -> Array[String]:
161
161
  var loaded: Array[String] = []
162
162
  var named: Dictionary = {}
163
- for line: String in FileAccess.get_file_as_string(script).split("\n"):
164
- var code: String = GdhPatterns.without_comment(line)
163
+ for code: String in code_by_line(FileAccess.get_file_as_string(script)):
165
164
  if not code.contains("load("):
166
165
  for literal: String in _string_literals(code):
167
166
  named[_resolved(literal, script)] = true
@@ -494,10 +493,32 @@ static func _code_of(line: String) -> String:
494
493
  # [param content] with each line's comment taken off and its strings kept, which is where the paths
495
494
  # a script loads are written.
496
495
  static func _code_lines(content: String) -> String:
497
- var kept: Array[String] = []
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
498
506
  for line: String in content.split("\n"):
499
- kept.append(GdhPatterns.without_comment(line))
500
- return "\n".join(kept)
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
501
522
 
502
523
 
503
524
  # The comment on [param line], strings emptied first so a # inside one does not start it.
@@ -580,21 +601,72 @@ func _analyze_resource(
580
601
  path: String, current_depth: int, walk: GdhDependencyWalk
581
602
  ) -> Array[Dictionary]:
582
603
  var deps: Array[Dictionary] = []
583
-
584
604
  if current_depth >= walk.max_depth:
585
605
  return deps
586
606
 
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.
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
+
590
615
  var remaining: int = walk.max_depth - current_depth
591
- if walk.visited.has(path):
592
- var cached: Dictionary = walk.visited[path]
593
- if GdhRead.as_int(cached["remaining"]) == remaining:
594
- var cached_deps: Array[Dictionary] = cached["deps"]
595
- 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
596
620
 
597
- walk.path_stack.append(path)
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
598
670
 
599
671
  var file: FileAccess = FileAccess.open(path, FileAccess.READ)
600
672
  if file:
@@ -621,7 +693,7 @@ func _analyze_resource(
621
693
  # as no res:// path, a script's load of one went unsaid while the game printed
622
694
  # "Unrecognized UID" at that line.
623
695
  if dep_path.begins_with("uid://"):
624
- _depend(deps, path, dep_path, {}, current_depth, walk)
696
+ _name(deps, path, dep_path, {})
625
697
  continue
626
698
  if not dep_path.begins_with("res://"):
627
699
  continue
@@ -640,7 +712,7 @@ func _analyze_resource(
640
712
  var named_by: Dictionary = { "stale_path": written } if dep_path != written else {}
641
713
  if unknown.has(dep_path):
642
714
  named_by["unknown_uid"] = unknown[dep_path]
643
- _depend(deps, path, dep_path, named_by, current_depth, walk)
715
+ _name(deps, path, dep_path, named_by)
644
716
 
645
717
  # A script uses another by its class_name as surely as by a preload, and only the reverse
646
718
  # walk looked for that: asked what a script depends on, the answer left out the script it
@@ -649,30 +721,18 @@ func _analyze_resource(
649
721
  if path.get_extension() == "gd":
650
722
  var classes: Dictionary = classes_declared()
651
723
  for used: String in classes_used_in(content, classes):
652
- _depend(deps, path, str(classes[used]), { "by_class": used }, current_depth, walk)
724
+ _name(deps, path, str(classes[used]), { "by_class": used })
653
725
  var autoloads: Dictionary = autoloads_declared()
654
726
  for used: String in classes_used_in(content, autoloads):
655
- _depend(
656
- deps, path, str(autoloads[used]), { "by_autoload": used }, current_depth, walk
657
- )
658
-
659
- walk.path_stack.pop_back()
660
- walk.visited[path] = { "deps": deps, "remaining": remaining }
727
+ _name(deps, path, str(autoloads[used]), { "by_autoload": used })
661
728
  return deps
662
729
 
663
730
 
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.
668
- func _depend(
669
- deps: Array[Dictionary],
670
- path: String,
671
- dep_path: String,
672
- named_by: Dictionary,
673
- current_depth: int,
674
- walk: GdhDependencyWalk
675
- ) -> 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:
676
736
  if dep_path == path:
677
737
  return
678
738
  for existing: Dictionary in deps:
@@ -685,27 +745,6 @@ func _depend(
685
745
  "exists": not dep_path.begins_with("uid://") and FileAccess.file_exists(dep_path),
686
746
  }
687
747
  dep_info.merge(named_by)
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.
690
- if dep_path in walk.path_stack:
691
- var cycle: Array[String] = walk.path_stack.slice(walk.path_stack.find(dep_path))
692
- cycle.append(dep_path)
693
- if not cycle in walk.circular_references:
694
- walk.circular_references.append(cycle)
695
- dep_info["circular"] = true
696
- deps.append(dep_info)
697
- return
698
- if dep_info["exists"] and current_depth + 1 < walk.max_depth:
699
- var sub_deps: Array[Dictionary] = _analyze_resource(dep_path, current_depth + 1, walk)
700
- if sub_deps.size() > 0:
701
- dep_info["dependencies"] = sub_deps
702
- elif dep_info["exists"] and walk.counts_beyond:
703
- # A file the depth stopped at read the same as one naming nothing, and a missing file past
704
- # it went unsaid.
705
- var beyond: GdhDependencyWalk = GdhDependencyWalk.new(1, walk.include_built_in, [], false)
706
- var named: int = _analyze_resource(dep_path, 0, beyond).size()
707
- if named > 0:
708
- dep_info["beyond_depth"] = named
709
748
  deps.append(dep_info)
710
749
 
711
750
 
@@ -912,23 +951,18 @@ class GdhDependencyWalk:
912
951
 
913
952
  var include_built_in: bool
914
953
 
915
- var visited: Dictionary = {}
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 = {}
916
960
 
917
961
  var path_stack: Array[String] = []
918
962
 
919
963
  var circular_references: Array
920
964
 
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:
965
+ func _init(p_max_depth: int, p_include_built_in: bool, p_circular_references: Array) -> void:
931
966
  max_depth = p_max_depth
932
967
  include_built_in = p_include_built_in
933
968
  circular_references = p_circular_references
934
- counts_beyond = p_counts_beyond
@@ -81,6 +81,8 @@ static func run(params: Dictionary) -> Dictionary:
81
81
  var answered: Dictionary = answer
82
82
  if not GdhSceneFile.also_saved.is_empty():
83
83
  answered["alsoSaved"] = GdhSceneFile.also_saved.duplicate(true)
84
+ if not GdhSceneFile.unmade_note.is_empty():
85
+ answered["unmadeConnections"] = GdhSceneFile.unmade_note
84
86
  if not GdhSceneUids.by_path.is_empty():
85
87
  answered["namedByPath"] = (
86
88
  (
@@ -56,6 +56,25 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
56
56
  # its first function would otherwise be judged against the default.
57
57
  _mark_virtuals(functions, extends_name, full_script_path)
58
58
 
59
+ # The engine loads a base named by its path and a constant handed to load or preload as surely
60
+ # as a path written in the call, and both were left out while project_dependencies named them: a
61
+ # script extending "frame.gd" did not depend on it, nor one calling load(TYPE_CASE).
62
+ for written: String in _based_on_paths(extends_name, own.inner_classes):
63
+ if not written in dependencies:
64
+ dependencies.append(written)
65
+ var held: Dictionary = {}
66
+ for constant: Dictionary in own.constants:
67
+ held[str(constant.get("name", ""))] = _unquoted(str(constant.get("value", "")))
68
+ for code: String in GdhDependencies.code_by_line(content):
69
+ for loading: RegExMatch in (
70
+ GdhPatterns
71
+ . compiled("(?<![\\w.])(?:preload|load)\\s*\\(\\s*([A-Za-z_]\\w*)\\s*\\)")
72
+ . search_all(code)
73
+ ):
74
+ var written: String = str(held.get(loading.get_string(1), ""))
75
+ if not written.is_empty() and not written in dependencies:
76
+ dependencies.append(written)
77
+
59
78
  # A script typed by its class_name is depended on as surely as one preloaded, and the answer
60
79
  # listed only the preloads: a `var stats: Stats` left stats.gd out, while project_dependencies
61
80
  # named it. The same reading as that tool's, so the two agree.
@@ -131,7 +150,7 @@ func _members(
131
150
  # Inside a string the raw line is counted, because a `#` in it is text and taking it for a
132
151
  # comment would cut off the quotes that close the string.
133
152
  if in_multiline_string:
134
- if _triple_quotes(raw) % 2 == 1:
153
+ if GdhPatterns.triple_quotes(raw) % 2 == 1:
135
154
  in_multiline_string = false
136
155
  continue
137
156
  # A `##` block documents what it sits directly above: the class when that is its class_name
@@ -165,7 +184,7 @@ func _members(
165
184
  # it opened on. Toggling on any count lost `const HELP := """Usage"""`, and everything after
166
185
  # it up to the next triple quote, with the state inverted from there to the end of the file.
167
186
  # The line that opens one is still read: it can be the declaration the string belongs to.
168
- if _triple_quotes(stripped) % 2 == 1:
187
+ if GdhPatterns.triple_quotes(stripped) % 2 == 1:
169
188
  in_multiline_string = true
170
189
 
171
190
  # Only a line at this body's own margin declares one of its members. Deeper, it is a
@@ -207,7 +226,7 @@ func _members(
207
226
  while i + 1 < lines.size():
208
227
  i += 1
209
228
  stripped += "\n" + lines[i]
210
- if _triple_quotes(lines[i]) % 2 == 1:
229
+ if GdhPatterns.triple_quotes(lines[i]) % 2 == 1:
211
230
  break
212
231
  in_multiline_string = false
213
232
  header = GdhPatterns.without_annotations(stripped)
@@ -821,13 +840,31 @@ func _collect_dependencies(line: String, into: Array[String]) -> void:
821
840
  into.append(dep)
822
841
 
823
842
 
824
- # How many triple quotes of either kind [param line] holds.
825
- static func _declares_variable(header: String) -> bool:
826
- return header.begins_with("var ") or header.begins_with("static var ")
843
+ ## The paths a script and its inner classes extend by, as written: [param extends_name] the
844
+ ## script's own extends, and [param inner_classes] its inner classes as answered.
845
+ static func _based_on_paths(extends_name: String, inner_classes: Array) -> Array[String]:
846
+ var paths: Array[String] = []
847
+ var written: String = _unquoted(extends_name)
848
+ if not written.is_empty():
849
+ paths.append(written)
850
+ for inner: Dictionary in inner_classes:
851
+ var deeper: Array = inner.get("inner_classes", [])
852
+ for path: String in _based_on_paths(str(inner.get("extends", "")), deeper):
853
+ if not path in paths:
854
+ paths.append(path)
855
+ return paths
856
+
857
+
858
+ ## The text of [param literal], a string literal as written; empty for anything else.
859
+ static func _unquoted(literal: String) -> String:
860
+ var text: String = literal.strip_edges()
861
+ if text.length() >= 2 and text[0] in ["\"", "'"] and text[-1] == text[0]:
862
+ return text.substr(1, text.length() - 2)
863
+ return ""
827
864
 
828
865
 
829
- static func _triple_quotes(line: String) -> int:
830
- return line.count('"""') + line.count("'''")
866
+ static func _declares_variable(header: String) -> bool:
867
+ return header.begins_with("var ") or header.begins_with("static var ")
831
868
 
832
869
 
833
870
  func _extract_dependencies(line: String) -> Array[String]:
@@ -96,6 +96,11 @@ static func without_annotations(line: String) -> String:
96
96
  # brace opens before the `#` and closes on a later line. Held by a case, since the argument for the
97
97
  # difference is not visible from the code and reads like an oversight beside its neighbours.
98
98
  static func without_comment(line: String) -> String:
99
+ var start: int = _comment_start(line)
100
+ return (line if start == -1 else line.substr(0, start)).strip_edges()
101
+
102
+
103
+ static func _comment_start(line: String) -> int:
99
104
  var quote: String = ""
100
105
  for i: int in range(line.length()):
101
106
  var ch: String = line[i]
@@ -104,7 +109,33 @@ static func without_comment(line: String) -> String:
104
109
  quote = ""
105
110
  continue
106
111
  if ch == "#":
107
- return line.substr(0, i).strip_edges()
112
+ return i
108
113
  if ch == '"' or ch == "'":
109
114
  quote = ch
110
- return line.strip_edges()
115
+ return -1
116
+
117
+
118
+ ## The code in front of each TODO or FIXME comment in [param content], one entry a comment.
119
+ ##
120
+ ## Read from the comments alone, as the editor highlights them, so `#TODO` counts and the same
121
+ ## words inside a string or a multi-line string do not.
122
+ static func noted_work(content: String) -> Array[String]:
123
+ var noted: Array[String] = []
124
+ var in_string: bool = false
125
+ for line: String in content.split("\n"):
126
+ if in_string:
127
+ in_string = triple_quotes(line) % 2 == 0
128
+ continue
129
+ var start: int = _comment_start(line)
130
+ var code: String = line if start == -1 else line.substr(0, start)
131
+ if triple_quotes(code) % 2 == 1:
132
+ in_string = true
133
+ elif start != -1 and compiled("^#+\\s*(?:TODO|FIXME)\\b").search(line.substr(start)):
134
+ noted.append(code.strip_edges())
135
+ return noted
136
+
137
+
138
+ ## How many triple quotes of either kind [param line] holds. An odd count opens or closes a string
139
+ ## that runs across lines.
140
+ static func triple_quotes(line: String) -> int:
141
+ return line.count('"""') + line.count("'''")
@@ -5,6 +5,7 @@ const GdhFileWalk = preload("file_walk.gd")
5
5
  const GdhImportPipeline = preload("import_pipeline.gd")
6
6
  const GdhInputActions = preload("input_actions.gd")
7
7
  const GdhLog = preload("logger.gd")
8
+ const GdhPatterns = preload("patterns.gd")
8
9
  const GdhRead = preload("reading.gd")
9
10
  const GdhSceneConnections = preload("scene_connections.gd")
10
11
  const GdhScriptCheck = preload("script_check.gd")
@@ -87,7 +88,7 @@ func get_project_health(params: Dictionary) -> Dictionary:
87
88
  var content: String = file.get_as_text()
88
89
  file.close()
89
90
 
90
- if "# TODO" in content or "# FIXME" in content:
91
+ if not GdhPatterns.noted_work(content).is_empty():
91
92
  todo_count += 1
92
93
 
93
94
  # Work noted for later, which no script fails on, so said rather than failed. A count of
@@ -8,6 +8,7 @@ const GdhFileWalk = preload("file_walk.gd")
8
8
  const GdhImportPipeline = preload("import_pipeline.gd")
9
9
  const GdhInputActions = preload("input_actions.gd")
10
10
  const GdhLog = preload("logger.gd")
11
+ const GdhPatterns = preload("patterns.gd")
11
12
  const GdhRead = preload("reading.gd")
12
13
  const GdhSceneConnections = preload("scene_connections.gd")
13
14
  const GdhScriptCheck = preload("script_check.gd")
@@ -196,20 +197,21 @@ func validate_project(params: Dictionary) -> Dictionary:
196
197
  var scripts_checked: int = 0
197
198
  var script_issues: Array[Dictionary] = []
198
199
 
199
- # A hundred scripts is enough to say whether the project is tidy without a large one
200
- # turning validation into a full read of its source tree.
201
- for script_path: String in script_files.slice(0, 100):
200
+ # Every script, as health reads them: cut at the first hundred in walk order, the count sat
201
+ # beside a compile check of all 803 scripts of a game and read as how many were compiled.
202
+ for script_path: String in script_files:
202
203
  scripts_checked += 1
203
204
  var file: FileAccess = FileAccess.open(script_path, FileAccess.READ)
204
205
  if file:
205
206
  var content: String = file.get_as_text()
206
207
  file.close()
207
208
 
208
- if "# TODO" in content or "# FIXME" in content:
209
+ var noted: Array[String] = GdhPatterns.noted_work(content)
210
+ if not noted.is_empty():
209
211
  script_issues.append(
210
212
  { "path": script_path, "issue": "Contains TODO/FIXME comments" }
211
213
  )
212
- if "pass # TODO" in content:
214
+ if "pass" in noted:
213
215
  script_issues.append(
214
216
  { "path": script_path, "issue": "Contains unimplemented functions" }
215
217
  )
@@ -292,7 +294,6 @@ func validate_project(params: Dictionary) -> Dictionary:
292
294
  "warnings": warnings,
293
295
  "checks_performed": checks_performed,
294
296
  "scripts_checked": scripts_checked,
295
- # Said beside the count, so a project of four hundred scripts is not read as checked whole.
296
297
  "scripts_total": script_files.size(),
297
298
  "issue_count": issues.size(),
298
299
  "warning_count": warnings.size(),