gdharness 1.3.44 → 1.3.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +300 -291
- package/build/godot/addons/gdharness_editor/setting_reads.gd +42 -1
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +13 -0
- package/build/godot/operations/dependencies.gd +103 -69
- package/build/godot/operations/gdscript_analysis.gd +45 -8
- package/build/godot/operations/patterns.gd +6 -0
- package/build/godot/operations/project_validation.gd +3 -4
- package/build/godot/operations/setting_reads.gd +42 -1
- package/package.json +1 -1
|
@@ -62,7 +62,7 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
|
|
|
62
62
|
var value: Variant = ProjectSettings.get_setting(name)
|
|
63
63
|
var described: Dictionary = {
|
|
64
64
|
"setting": name,
|
|
65
|
-
"type":
|
|
65
|
+
"type": type_named(kind, value),
|
|
66
66
|
"value": values.serialize_value(value),
|
|
67
67
|
}
|
|
68
68
|
# As a single get says it, so a family read can be told apart into what the project sets and
|
|
@@ -81,6 +81,47 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
|
|
|
81
81
|
return { "prefix": prefix, "count": found.size(), "settings": found }
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
## The type [param kind] names, with what [param value] holds when it is a typed array or
|
|
85
|
+
## dictionary, as project.godot writes it: a `Dictionary[String, bool]` was answered as Dictionary,
|
|
86
|
+
## which hides what its keys and values must be.
|
|
87
|
+
static func type_named(kind: int, value: Variant) -> String:
|
|
88
|
+
if kind == TYPE_ARRAY and value is Array:
|
|
89
|
+
var list: Array = value
|
|
90
|
+
if list.is_typed():
|
|
91
|
+
var element: String = _held(
|
|
92
|
+
list.get_typed_builtin(), list.get_typed_class_name(), list.get_typed_script()
|
|
93
|
+
)
|
|
94
|
+
return "Array[%s]" % element
|
|
95
|
+
elif kind == TYPE_DICTIONARY and value is Dictionary:
|
|
96
|
+
var table: Dictionary = value
|
|
97
|
+
if table.is_typed():
|
|
98
|
+
var key: String = _held(
|
|
99
|
+
table.get_typed_key_builtin(),
|
|
100
|
+
table.get_typed_key_class_name(),
|
|
101
|
+
table.get_typed_key_script()
|
|
102
|
+
)
|
|
103
|
+
var held: String = _held(
|
|
104
|
+
table.get_typed_value_builtin(),
|
|
105
|
+
table.get_typed_value_class_name(),
|
|
106
|
+
table.get_typed_value_script()
|
|
107
|
+
)
|
|
108
|
+
return "Dictionary[%s, %s]" % [key, held]
|
|
109
|
+
return type_string(kind)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## One side of a typed collection: a built-in type, a class, or a script by its class_name.
|
|
113
|
+
static func _held(builtin: int, named: StringName, script: Variant) -> String:
|
|
114
|
+
if builtin == TYPE_NIL:
|
|
115
|
+
return "Variant"
|
|
116
|
+
if builtin != TYPE_OBJECT:
|
|
117
|
+
return type_string(builtin)
|
|
118
|
+
if script is Script:
|
|
119
|
+
var declared: Script = script
|
|
120
|
+
if not declared.get_global_name().is_empty():
|
|
121
|
+
return declared.get_global_name()
|
|
122
|
+
return str(named)
|
|
123
|
+
|
|
124
|
+
|
|
84
125
|
## The setting [param setting_path] overrides for builds with one feature, as
|
|
85
126
|
## `application/run/max_fps.mobile` does `application/run/max_fps`, or "" for a plain setting.
|
|
86
127
|
static func override_base(setting_path: String) -> String:
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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]:
|
|
@@ -108,3 +108,9 @@ static func without_comment(line: String) -> String:
|
|
|
108
108
|
if ch == '"' or ch == "'":
|
|
109
109
|
quote = ch
|
|
110
110
|
return line.strip_edges()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
## How many triple quotes of either kind [param line] holds. An odd count opens or closes a string
|
|
114
|
+
## that runs across lines.
|
|
115
|
+
static func triple_quotes(line: String) -> int:
|
|
116
|
+
return line.count('"""') + line.count("'''")
|
|
@@ -196,9 +196,9 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
196
196
|
var scripts_checked: int = 0
|
|
197
197
|
var script_issues: Array[Dictionary] = []
|
|
198
198
|
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
for script_path: String in script_files
|
|
199
|
+
# Every script, as health reads them: cut at the first hundred in walk order, the count sat
|
|
200
|
+
# beside a compile check of all 803 scripts of a game and read as how many were compiled.
|
|
201
|
+
for script_path: String in script_files:
|
|
202
202
|
scripts_checked += 1
|
|
203
203
|
var file: FileAccess = FileAccess.open(script_path, FileAccess.READ)
|
|
204
204
|
if file:
|
|
@@ -292,7 +292,6 @@ func validate_project(params: Dictionary) -> Dictionary:
|
|
|
292
292
|
"warnings": warnings,
|
|
293
293
|
"checks_performed": checks_performed,
|
|
294
294
|
"scripts_checked": scripts_checked,
|
|
295
|
-
# Said beside the count, so a project of four hundred scripts is not read as checked whole.
|
|
296
295
|
"scripts_total": script_files.size(),
|
|
297
296
|
"issue_count": issues.size(),
|
|
298
297
|
"warning_count": warnings.size(),
|
|
@@ -62,7 +62,7 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
|
|
|
62
62
|
var value: Variant = ProjectSettings.get_setting(name)
|
|
63
63
|
var described: Dictionary = {
|
|
64
64
|
"setting": name,
|
|
65
|
-
"type":
|
|
65
|
+
"type": type_named(kind, value),
|
|
66
66
|
"value": values.serialize_value(value),
|
|
67
67
|
}
|
|
68
68
|
# As a single get says it, so a family read can be told apart into what the project sets and
|
|
@@ -81,6 +81,47 @@ static func under(prefix: String, values: GdhSerialisation) -> Dictionary:
|
|
|
81
81
|
return { "prefix": prefix, "count": found.size(), "settings": found }
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
## The type [param kind] names, with what [param value] holds when it is a typed array or
|
|
85
|
+
## dictionary, as project.godot writes it: a `Dictionary[String, bool]` was answered as Dictionary,
|
|
86
|
+
## which hides what its keys and values must be.
|
|
87
|
+
static func type_named(kind: int, value: Variant) -> String:
|
|
88
|
+
if kind == TYPE_ARRAY and value is Array:
|
|
89
|
+
var list: Array = value
|
|
90
|
+
if list.is_typed():
|
|
91
|
+
var element: String = _held(
|
|
92
|
+
list.get_typed_builtin(), list.get_typed_class_name(), list.get_typed_script()
|
|
93
|
+
)
|
|
94
|
+
return "Array[%s]" % element
|
|
95
|
+
elif kind == TYPE_DICTIONARY and value is Dictionary:
|
|
96
|
+
var table: Dictionary = value
|
|
97
|
+
if table.is_typed():
|
|
98
|
+
var key: String = _held(
|
|
99
|
+
table.get_typed_key_builtin(),
|
|
100
|
+
table.get_typed_key_class_name(),
|
|
101
|
+
table.get_typed_key_script()
|
|
102
|
+
)
|
|
103
|
+
var held: String = _held(
|
|
104
|
+
table.get_typed_value_builtin(),
|
|
105
|
+
table.get_typed_value_class_name(),
|
|
106
|
+
table.get_typed_value_script()
|
|
107
|
+
)
|
|
108
|
+
return "Dictionary[%s, %s]" % [key, held]
|
|
109
|
+
return type_string(kind)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## One side of a typed collection: a built-in type, a class, or a script by its class_name.
|
|
113
|
+
static func _held(builtin: int, named: StringName, script: Variant) -> String:
|
|
114
|
+
if builtin == TYPE_NIL:
|
|
115
|
+
return "Variant"
|
|
116
|
+
if builtin != TYPE_OBJECT:
|
|
117
|
+
return type_string(builtin)
|
|
118
|
+
if script is Script:
|
|
119
|
+
var declared: Script = script
|
|
120
|
+
if not declared.get_global_name().is_empty():
|
|
121
|
+
return declared.get_global_name()
|
|
122
|
+
return str(named)
|
|
123
|
+
|
|
124
|
+
|
|
84
125
|
## The setting [param setting_path] overrides for builds with one feature, as
|
|
85
126
|
## `application/run/max_fps.mobile` does `application/run/max_fps`, or "" for a plain setting.
|
|
86
127
|
static func override_base(setting_path: String) -> String:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.45",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|