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.
- package/build/cli.js +587 -478
- package/build/godot/addons/gdharness_editor/animation_reads.gd +8 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +190 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd +42 -1
- package/build/godot/addons/gdharness_editor/tool_executor.gd +4 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +89 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +61 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +21 -2
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +18 -5
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +22 -3
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +7 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +9 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +29 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +13 -0
- package/build/godot/operations/animation_reads.gd +8 -0
- package/build/godot/operations/dependencies.gd +103 -69
- package/build/godot/operations/editor_tools.gd +2 -0
- package/build/godot/operations/gdscript_analysis.gd +45 -8
- package/build/godot/operations/patterns.gd +33 -2
- package/build/godot/operations/project_diagnostics.gd +2 -1
- package/build/godot/operations/project_validation.gd +7 -6
- package/build/godot/operations/setting_reads.gd +42 -1
- package/build/keeper.js +3 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
500
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
665
|
-
#
|
|
666
|
-
#
|
|
667
|
-
#
|
|
668
|
-
func
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
|
830
|
-
return
|
|
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
|
|
112
|
+
return i
|
|
108
113
|
if ch == '"' or ch == "'":
|
|
109
114
|
quote = ch
|
|
110
|
-
return
|
|
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
|
|
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
|
-
#
|
|
200
|
-
#
|
|
201
|
-
for script_path: String in script_files
|
|
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
|
-
|
|
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
|
|
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(),
|