gdharness 1.3.0 → 1.3.2
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 +322 -96
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -19
- package/build/godot/addons/gdharness_editor/bridge_client.gd +8 -8
- package/build/godot/addons/gdharness_editor/declared_values.gd +116 -0
- package/build/godot/addons/gdharness_editor/declared_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +65 -25
- package/build/godot/addons/gdharness_editor/headless_check.gd +7 -7
- package/build/godot/addons/gdharness_editor/plugin.gd +6 -6
- package/build/godot/addons/gdharness_editor/property_values.gd +222 -59
- package/build/godot/addons/gdharness_editor/reading.gd +65 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +28 -23
- package/build/godot/addons/gdharness_editor/serialisation.gd +49 -94
- package/build/godot/addons/gdharness_editor/tool_executor.gd +24 -24
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +52 -41
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +4 -4
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +14 -14
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +175 -74
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +17 -17
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +14 -12
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +7 -7
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +88 -84
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +4 -4
- package/build/godot/addons/gdharness_editor/value_shapes.gd +118 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +65 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +18 -18
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +29 -29
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +21 -18
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +86 -80
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +65 -64
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +6 -6
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +8 -8
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +6 -6
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -10
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +7 -7
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +41 -41
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +3 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +33 -33
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +20 -20
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +52 -25
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +56 -50
- package/build/godot/addons/gdharness_runtime/serialisation.gd +49 -94
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +118 -0
- package/build/godot/addons/gdharness_runtime/value_shapes.gd.uid +1 -0
- package/build/godot/operations/animation_reads.gd +7 -19
- package/build/godot/operations/audio_buses.gd +10 -10
- package/build/godot/operations/class_cache.gd +16 -16
- package/build/godot/operations/classdb_queries.gd +25 -15
- package/build/godot/operations/declared_values.gd +116 -0
- package/build/godot/operations/dependencies.gd +141 -33
- package/build/godot/operations/editor_tools.gd +12 -12
- package/build/godot/operations/export_presets.gd +14 -14
- package/build/godot/operations/file_reads.gd +65 -25
- package/build/godot/operations/file_walk.gd +34 -0
- package/build/godot/operations/gdscript_analysis.gd +27 -22
- package/build/godot/operations/gdscript_authoring.gd +122 -17
- package/build/godot/operations/godot_operations.gd +66 -63
- package/build/godot/operations/import_pipeline.gd +59 -20
- package/build/godot/operations/input_actions.gd +90 -18
- package/build/godot/operations/logger.gd +1 -1
- package/build/godot/operations/plugins.gd +8 -8
- package/build/godot/operations/project_config.gd +83 -20
- package/build/godot/operations/project_diagnostics.gd +36 -11
- package/build/godot/operations/property_values.gd +222 -59
- package/build/godot/operations/reading.gd +65 -0
- package/build/godot/operations/rename_checks.gd +3 -3
- package/build/godot/operations/resource_files.gd +9 -6
- package/build/godot/operations/script_check.gd +23 -7
- package/build/godot/operations/serialisation.gd +49 -94
- package/build/godot/operations/value_shapes.gd +118 -0
- package/package.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
## What a property declares beyond its type: the values of its enum and the range it is exported
|
|
5
|
+
## with, for taking a value by name and for saying when a value written lies outside them.
|
|
6
|
+
|
|
7
|
+
const GdhRead = preload("reading.gd")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## The values an enum hint names, by name, from its hint string: "SWORD:0,SHIELD:1" as a script
|
|
11
|
+
## declares one, or "Add,Subtract" as an engine property does, each name then taking the number
|
|
12
|
+
## after the one before it.
|
|
13
|
+
static func enum_values(hint_string: String) -> Dictionary:
|
|
14
|
+
var values: Dictionary = {}
|
|
15
|
+
var next: int = 0
|
|
16
|
+
for entry: String in hint_string.split(",", false):
|
|
17
|
+
var name: String = entry.get_slice(":", 0).strip_edges()
|
|
18
|
+
var number: String = entry.get_slice(":", 1).strip_edges() if ":" in entry else ""
|
|
19
|
+
var value: int = number.to_int() if number.is_valid_int() else next
|
|
20
|
+
values[name] = value
|
|
21
|
+
next = value + 1
|
|
22
|
+
return values
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## The values of the enum [param declared] declares, by name, spelt as the script of [param target]
|
|
26
|
+
## spells them when it declares that enum. The engine's hint capitalises each name, "SWORD" as
|
|
27
|
+
## "Sword", and a refusal listing those named values the script never spells.
|
|
28
|
+
static func enum_of(target: Object, declared: Dictionary) -> Dictionary:
|
|
29
|
+
var hinted: Dictionary = enum_values(str(declared.get("hint_string", "")))
|
|
30
|
+
var attached: Variant = target.get_script()
|
|
31
|
+
if not attached is Script:
|
|
32
|
+
return hinted
|
|
33
|
+
var script: Script = attached
|
|
34
|
+
for constant: Variant in script.get_script_constant_map().values():
|
|
35
|
+
if not constant is Dictionary:
|
|
36
|
+
continue
|
|
37
|
+
var values: Dictionary = constant
|
|
38
|
+
if values.size() != hinted.size():
|
|
39
|
+
continue
|
|
40
|
+
var spelt: Dictionary = {}
|
|
41
|
+
for name: Variant in values:
|
|
42
|
+
if hinted.get(str(name).capitalize()) == values[name]:
|
|
43
|
+
spelt[str(name)] = values[name]
|
|
44
|
+
if spelt.size() == hinted.size():
|
|
45
|
+
return spelt
|
|
46
|
+
return hinted
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## The number [param word] names among [param choices], matched as written and then ignoring case,
|
|
50
|
+
## or null when it names none.
|
|
51
|
+
static func enum_named(choices: Dictionary, word: String) -> Variant:
|
|
52
|
+
var wanted: String = word.strip_edges()
|
|
53
|
+
if choices.has(wanted):
|
|
54
|
+
return choices[wanted]
|
|
55
|
+
for name: String in choices:
|
|
56
|
+
if name.to_lower() == wanted.to_lower():
|
|
57
|
+
return choices[name]
|
|
58
|
+
return null
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
static func enum_refusal(holder: String, property: String, word: String, choices: Dictionary) -> String:
|
|
62
|
+
var said: String = (
|
|
63
|
+
"%s.%s is an enum, and %s is none of its values: send %s by name or by number"
|
|
64
|
+
% [holder, property, word, _listed(choices)]
|
|
65
|
+
)
|
|
66
|
+
var close: PackedStringArray = GdhRead.nearest(word, PackedStringArray(choices.keys()))
|
|
67
|
+
if close.is_empty():
|
|
68
|
+
return said + "."
|
|
69
|
+
return said + ". The nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## Why [param value], shown as [param shown], lies outside what [param declared] allows, an export
|
|
73
|
+
## range or an enum's values, or "" when it does not. A range marked or_greater or or_less is open
|
|
74
|
+
## on that side.
|
|
75
|
+
static func outside(target: Object, declared: Dictionary, value: Variant, shown: String) -> String:
|
|
76
|
+
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
77
|
+
var hint_string: String = str(declared.get("hint_string", ""))
|
|
78
|
+
var property: String = str(declared.get("name", ""))
|
|
79
|
+
if typeof(value) != TYPE_INT and typeof(value) != TYPE_FLOAT:
|
|
80
|
+
return ""
|
|
81
|
+
var number: float = GdhRead.as_float(value)
|
|
82
|
+
if hint == PROPERTY_HINT_ENUM and typeof(value) == TYPE_INT:
|
|
83
|
+
var choices: Dictionary = enum_of(target, declared)
|
|
84
|
+
if choices.is_empty() or choices.values().has(value):
|
|
85
|
+
return ""
|
|
86
|
+
return "%d is none of the values %s declares: %s." % [value, property, _listed(choices)]
|
|
87
|
+
if hint != PROPERTY_HINT_RANGE:
|
|
88
|
+
return ""
|
|
89
|
+
var parts: PackedStringArray = []
|
|
90
|
+
for part: String in hint_string.split(","):
|
|
91
|
+
var _added: bool = parts.append(part.strip_edges())
|
|
92
|
+
if parts.size() < 2 or not parts[0].is_valid_float() or not parts[1].is_valid_float():
|
|
93
|
+
return ""
|
|
94
|
+
var low: float = parts[0].to_float()
|
|
95
|
+
var high: float = parts[1].to_float()
|
|
96
|
+
if (number <= high or parts.has("or_greater")) and (number >= low or parts.has("or_less")):
|
|
97
|
+
return ""
|
|
98
|
+
return (
|
|
99
|
+
(
|
|
100
|
+
"%s is outside the range %s declares, %s to %s, which the inspector holds it to; the "
|
|
101
|
+
+ "engine stores it as given."
|
|
102
|
+
)
|
|
103
|
+
% [shown, property, _bound(low), _bound(high)]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
static func _listed(choices: Dictionary) -> String:
|
|
108
|
+
var listed: PackedStringArray = []
|
|
109
|
+
for name: String in choices:
|
|
110
|
+
var _added: bool = listed.append("%s (%d)" % [name, choices[name]])
|
|
111
|
+
return ", ".join(listed)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
## A range's end as the script wrote it: the engine's hint gives @export_range(0, 10) as 0.0 and 10.0.
|
|
115
|
+
static func _bound(end: float) -> String:
|
|
116
|
+
return str(int(end)) if end == floorf(end) and absf(end) < 1e15 else str(end)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
3
|
+
const GdhPatterns = preload("patterns.gd")
|
|
4
|
+
const GdhRead = preload("reading.gd")
|
|
5
|
+
const GdhFileWalk = preload("file_walk.gd")
|
|
6
|
+
const GdhLog = preload("logger.gd")
|
|
7
|
+
const GdhResourceFiles = preload("resource_files.gd")
|
|
8
8
|
|
|
9
9
|
## What a reverse search reads unless told otherwise: the resource and code files, and the settings
|
|
10
10
|
## files that name resources, "godot" for project.godot, "cfg" for plugin.cfg and export_presets.cfg,
|
|
@@ -21,8 +21,8 @@ const REFERENCE_PATTERNS: Array[String] = [
|
|
|
21
21
|
'ext_resource.*path="([^"]+)"',
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
-
var _log:
|
|
25
|
-
var _files:
|
|
24
|
+
var _log: GdhLog
|
|
25
|
+
var _files: GdhFileWalk = GdhFileWalk.new()
|
|
26
26
|
## The project's class names and the scripts declaring them, read the first time a script is walked
|
|
27
27
|
## and kept for the call: walking every resource starts a walk per root, and each reading every
|
|
28
28
|
## script again made the whole-project answer quadratic in the number of scripts.
|
|
@@ -33,7 +33,7 @@ var _classes_read: bool = false
|
|
|
33
33
|
# Everything one dependency walk carries: the settings it was started with, and the state it
|
|
34
34
|
# accumulates as it recurses. Kept together so the recursion passes one object rather than
|
|
35
35
|
# six positional arguments that have to stay in the same order at every call site.
|
|
36
|
-
class
|
|
36
|
+
class GdhDependencyWalk:
|
|
37
37
|
var max_depth: int
|
|
38
38
|
var include_built_in: bool
|
|
39
39
|
var visited: Dictionary = {}
|
|
@@ -46,7 +46,7 @@ class DependencyWalk:
|
|
|
46
46
|
circular_references = p_circular_references
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
func _init(p_log:
|
|
49
|
+
func _init(p_log: GdhLog) -> void:
|
|
50
50
|
_log = p_log
|
|
51
51
|
|
|
52
52
|
|
|
@@ -54,9 +54,9 @@ func _init(p_log: Log) -> void:
|
|
|
54
54
|
func get_dependencies(params: Dictionary) -> Dictionary:
|
|
55
55
|
var resource_path: String = str(params.get("resource_path", ""))
|
|
56
56
|
# No depth, or a depth of zero or less, means the whole chain; the walk stops at cycles.
|
|
57
|
-
var depth: int =
|
|
57
|
+
var depth: int = GdhRead.as_int(params.get("depth", 0))
|
|
58
58
|
var max_depth: int = depth if depth > 0 else 1000
|
|
59
|
-
var include_built_in: bool =
|
|
59
|
+
var include_built_in: bool = GdhRead.as_bool(params.get("include_builtin", false))
|
|
60
60
|
|
|
61
61
|
_log.info(
|
|
62
62
|
(
|
|
@@ -75,9 +75,11 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
75
75
|
full_path = "res://" + full_path
|
|
76
76
|
|
|
77
77
|
if not FileAccess.file_exists(full_path):
|
|
78
|
-
return _log.failure(
|
|
78
|
+
return _log.failure(
|
|
79
|
+
_files.with_nearest_files("Resource file does not exist: " + full_path, full_path)
|
|
80
|
+
)
|
|
79
81
|
|
|
80
|
-
var walk:
|
|
82
|
+
var walk: GdhDependencyWalk = GdhDependencyWalk.new(max_depth, include_built_in, circular_references)
|
|
81
83
|
dependencies[full_path] = _analyze_resource(full_path, 0, walk)
|
|
82
84
|
total_resources = 1
|
|
83
85
|
else:
|
|
@@ -89,7 +91,9 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
89
91
|
# Each root gets a fresh walk so a cycle is reported from every resource it passes
|
|
90
92
|
# through, rather than only from whichever one happened to be walked first.
|
|
91
93
|
for res_path: String in all_resources:
|
|
92
|
-
var walk:
|
|
94
|
+
var walk: GdhDependencyWalk = GdhDependencyWalk.new(
|
|
95
|
+
max_depth, include_built_in, circular_references
|
|
96
|
+
)
|
|
93
97
|
var deps: Array[Dictionary] = _analyze_resource(res_path, 0, walk)
|
|
94
98
|
if deps.size() > 0:
|
|
95
99
|
dependencies[res_path] = deps
|
|
@@ -98,12 +102,19 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
98
102
|
|
|
99
103
|
var references: int = 0
|
|
100
104
|
var distinct: Dictionary = {}
|
|
105
|
+
var missing: Dictionary = {}
|
|
101
106
|
for key: String in dependencies:
|
|
102
107
|
# Through a typed local: what comes out of a Dictionary is Variant, and a project that
|
|
103
108
|
# errors on handing one to a typed parameter will not compile this script at all.
|
|
104
109
|
var walked: Array[Dictionary] = dependencies[key]
|
|
105
110
|
references += _count_recursive(walked)
|
|
106
111
|
_gather_paths(walked, key, distinct)
|
|
112
|
+
_gather_missing(walked, key, missing)
|
|
113
|
+
# Said in the summary as well as in the tree, where a file that is not there was one
|
|
114
|
+
# `exists: false` among the entries, which a caller reading the summary never met.
|
|
115
|
+
var missing_files: Array[Dictionary] = []
|
|
116
|
+
for path: String in missing:
|
|
117
|
+
missing_files.append({"path": path, "referenced_by": missing[path]})
|
|
107
118
|
|
|
108
119
|
return {
|
|
109
120
|
"dependencies": dependencies,
|
|
@@ -115,11 +126,54 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
115
126
|
# loads it, and counting its entries answered 11 for a scene with six dependencies.
|
|
116
127
|
"total_dependencies": distinct.size(),
|
|
117
128
|
"total_references": references,
|
|
118
|
-
"circular_count": circular_references.size()
|
|
129
|
+
"circular_count": circular_references.size(),
|
|
130
|
+
"missing": missing_files
|
|
119
131
|
}
|
|
120
132
|
}
|
|
121
133
|
|
|
122
134
|
|
|
135
|
+
## Every file a scene or resource of the project loads through an ext_resource that is not there,
|
|
136
|
+
## by its path or its uid, each with the files loading it. Only those: a script naming a path in
|
|
137
|
+
## its code may be asking whether the file exists, and the shipped addons check for files they
|
|
138
|
+
## write, so every project was told its addons referred to files that are missing. A script's
|
|
139
|
+
## preloads are the compile's to judge.
|
|
140
|
+
func missing_references() -> Array[Dictionary]:
|
|
141
|
+
var missing: Dictionary = {}
|
|
142
|
+
for file: String in _files.find_files_with_extensions("res://", GdhResourceFiles.TEXT_RESOURCE_KINDS):
|
|
143
|
+
var content: String = FileAccess.get_file_as_string(file)
|
|
144
|
+
var moved: Dictionary = _moved_by_uid(content)
|
|
145
|
+
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
146
|
+
var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line.get_string())
|
|
147
|
+
if at == null or moved.has(at.get_string(1)):
|
|
148
|
+
continue
|
|
149
|
+
var path: String = _resolved(at.get_string(1), file)
|
|
150
|
+
if FileAccess.file_exists(path):
|
|
151
|
+
continue
|
|
152
|
+
var naming: Array = missing.get_or_add(path, [])
|
|
153
|
+
if not file in naming:
|
|
154
|
+
naming.append(file)
|
|
155
|
+
# Sorted, since the walk meets files in the order the filesystem lists them, which differs
|
|
156
|
+
# between platforms.
|
|
157
|
+
var paths: Array = missing.keys()
|
|
158
|
+
paths.sort()
|
|
159
|
+
var found: Array[Dictionary] = []
|
|
160
|
+
for path: String in paths:
|
|
161
|
+
var naming: Array = missing[path]
|
|
162
|
+
naming.sort()
|
|
163
|
+
found.append({"path": path, "referenced_by": naming})
|
|
164
|
+
return found
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## One line for [param entry], one of [method missing_references].
|
|
168
|
+
static func missing_line(entry: Dictionary) -> String:
|
|
169
|
+
var referring: Array = entry.get("referenced_by", [])
|
|
170
|
+
var naming: PackedStringArray = PackedStringArray(referring)
|
|
171
|
+
return (
|
|
172
|
+
"%s does not exist, and %s %s to it"
|
|
173
|
+
% [str(entry.get("path", "")), ", ".join(naming), "refers" if naming.size() == 1 else "refer"]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
123
177
|
# What refers to a resource, and how: the scenes that instance it, the scripts that extend or
|
|
124
178
|
# preload it, and for a script with a class_name, every use of that name.
|
|
125
179
|
func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
@@ -128,17 +182,20 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
128
182
|
|
|
129
183
|
if not resource_path.begins_with("res://"):
|
|
130
184
|
resource_path = "res://" + resource_path
|
|
131
|
-
|
|
132
|
-
|
|
185
|
+
# A file that is gone is searched for all the same, since what still refers to it is what a
|
|
186
|
+
# caller asks about one, and the forward walk of a scene had just named it missing. Refused only
|
|
187
|
+
# when nothing refers to it either, which is a path written wrong.
|
|
188
|
+
var exists: bool = FileAccess.file_exists(resource_path)
|
|
133
189
|
|
|
134
190
|
_log.info("Finding usages of: " + resource_path)
|
|
135
191
|
|
|
136
192
|
var class_name_declared: String = _declared_class_name(resource_path)
|
|
137
193
|
var by_class: RegEx = null
|
|
138
194
|
if not class_name_declared.is_empty():
|
|
139
|
-
by_class =
|
|
195
|
+
by_class = GdhPatterns.compiled("\\b" + _regex_escaped(class_name_declared) + "\\b")
|
|
140
196
|
var target: Dictionary = {
|
|
141
|
-
"path": resource_path,
|
|
197
|
+
"path": resource_path,
|
|
198
|
+
"uid": str(GdhResourceFiles.uid_of(resource_path).get("uid", "")) if exists else ""
|
|
142
199
|
}
|
|
143
200
|
|
|
144
201
|
var all_files: Array[String] = _usage_files(file_types)
|
|
@@ -189,10 +246,12 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
189
246
|
kind = _path_reference_kind(line, file_path)
|
|
190
247
|
else:
|
|
191
248
|
kind = (
|
|
192
|
-
"extends"
|
|
249
|
+
"extends"
|
|
250
|
+
if GdhPatterns.without_annotations(line).begins_with("extends ")
|
|
251
|
+
else "class_name"
|
|
193
252
|
)
|
|
194
253
|
references.append({"line": i + 1, "kind": kind, "text": trimmed})
|
|
195
|
-
by_kind[kind] =
|
|
254
|
+
by_kind[kind] = GdhRead.as_int(by_kind.get(kind, 0)) + 1
|
|
196
255
|
if kind != "doc" and kind != "comment":
|
|
197
256
|
in_code += 1
|
|
198
257
|
|
|
@@ -200,11 +259,19 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
200
259
|
usages.append({"file": file_path, "references": references})
|
|
201
260
|
total += references.size()
|
|
202
261
|
|
|
262
|
+
if not exists and usages.is_empty():
|
|
263
|
+
return _log.failure(
|
|
264
|
+
_files.with_nearest_files(
|
|
265
|
+
"Resource file does not exist: %s, and nothing in the project refers to it" % resource_path,
|
|
266
|
+
resource_path
|
|
267
|
+
)
|
|
268
|
+
)
|
|
203
269
|
var declared: Variant = null
|
|
204
270
|
if not class_name_declared.is_empty():
|
|
205
271
|
declared = class_name_declared
|
|
206
272
|
return {
|
|
207
273
|
"resource_path": resource_path,
|
|
274
|
+
"exists": exists,
|
|
208
275
|
"class_name": declared,
|
|
209
276
|
"usages": usages,
|
|
210
277
|
"summary":
|
|
@@ -229,7 +296,7 @@ func _declared_class_name(path: String) -> String:
|
|
|
229
296
|
return ""
|
|
230
297
|
while not file.eof_reached():
|
|
231
298
|
# Annotations first, since `@abstract class_name X` is one line and this read it as none.
|
|
232
|
-
var found: RegExMatch =
|
|
299
|
+
var found: RegExMatch = GdhPatterns.declared_class(GdhPatterns.without_annotations(file.get_line()))
|
|
233
300
|
if found != null:
|
|
234
301
|
file.close()
|
|
235
302
|
return found.get_string(1)
|
|
@@ -282,14 +349,14 @@ func _names_target(line: String, file_path: String, target: Dictionary) -> bool:
|
|
|
282
349
|
# The contents of each string literal on [param line], of either quote.
|
|
283
350
|
static func _string_literals(line: String) -> Array[String]:
|
|
284
351
|
var literals: Array[String] = []
|
|
285
|
-
for m: RegExMatch in
|
|
352
|
+
for m: RegExMatch in GdhPatterns.compiled(STRING_LITERAL).search_all(line):
|
|
286
353
|
literals.append(m.get_string(1) if not m.get_string(1).is_empty() else m.get_string(2))
|
|
287
354
|
return literals
|
|
288
355
|
|
|
289
356
|
|
|
290
357
|
# [param line] with its strings emptied and its comment taken off: the part a class can be used in.
|
|
291
358
|
static func _code_of(line: String) -> String:
|
|
292
|
-
var bare: String =
|
|
359
|
+
var bare: String = GdhPatterns.compiled(STRING_LITERAL).sub(line, '""', true)
|
|
293
360
|
var mark: int = bare.find("#")
|
|
294
361
|
return bare if mark == -1 else bare.substr(0, mark)
|
|
295
362
|
|
|
@@ -299,13 +366,13 @@ static func _code_of(line: String) -> String:
|
|
|
299
366
|
static func _code_lines(content: String) -> String:
|
|
300
367
|
var kept: Array[String] = []
|
|
301
368
|
for line: String in content.split("\n"):
|
|
302
|
-
kept.append(
|
|
369
|
+
kept.append(GdhPatterns.without_comment(line))
|
|
303
370
|
return "\n".join(kept)
|
|
304
371
|
|
|
305
372
|
|
|
306
373
|
# The comment on [param line], strings emptied first so a # inside one does not start it.
|
|
307
374
|
static func _comment_of(line: String) -> String:
|
|
308
|
-
var bare: String =
|
|
375
|
+
var bare: String = GdhPatterns.compiled(STRING_LITERAL).sub(line, '""', true)
|
|
309
376
|
var mark: int = bare.find("#")
|
|
310
377
|
return "" if mark == -1 else bare.substr(mark)
|
|
311
378
|
|
|
@@ -321,7 +388,7 @@ func _path_reference_kind(line: String, file_path: String) -> String:
|
|
|
321
388
|
return "export_preset"
|
|
322
389
|
if file_path.ends_with(".import"):
|
|
323
390
|
return "import"
|
|
324
|
-
var trimmed: String =
|
|
391
|
+
var trimmed: String = GdhPatterns.without_annotations(line)
|
|
325
392
|
if trimmed.begins_with("extends "):
|
|
326
393
|
return "extends"
|
|
327
394
|
if trimmed.begins_with("[ext_resource"):
|
|
@@ -353,6 +420,20 @@ func _gather_paths(deps: Array[Dictionary], root: String, into: Dictionary) -> v
|
|
|
353
420
|
_gather_paths(deeper, root, into)
|
|
354
421
|
|
|
355
422
|
|
|
423
|
+
# Each file in [param deps] that does not exist, into [param into] with the files naming it, once
|
|
424
|
+
# each however often the tree reaches them; [param parent] names the entries of [param deps].
|
|
425
|
+
func _gather_missing(deps: Array[Dictionary], parent: String, into: Dictionary) -> void:
|
|
426
|
+
for dep: Dictionary in deps:
|
|
427
|
+
var path: String = str(dep.get("path", ""))
|
|
428
|
+
if not dep.get("exists", true):
|
|
429
|
+
var naming: Array = into.get_or_add(path, [])
|
|
430
|
+
if not parent in naming:
|
|
431
|
+
naming.append(parent)
|
|
432
|
+
if dep.has("dependencies"):
|
|
433
|
+
var deeper: Array[Dictionary] = dep["dependencies"]
|
|
434
|
+
_gather_missing(deeper, path, into)
|
|
435
|
+
|
|
436
|
+
|
|
356
437
|
# A circular entry is not counted: it marks that the entry holding it was reached again, which that
|
|
357
438
|
# entry already counts as one reference.
|
|
358
439
|
func _count_recursive(deps: Array[Dictionary]) -> int:
|
|
@@ -366,7 +447,7 @@ func _count_recursive(deps: Array[Dictionary]) -> int:
|
|
|
366
447
|
return count
|
|
367
448
|
|
|
368
449
|
|
|
369
|
-
func _analyze_resource(path: String, current_depth: int, walk:
|
|
450
|
+
func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk) -> Array[Dictionary]:
|
|
370
451
|
var deps: Array[Dictionary] = []
|
|
371
452
|
|
|
372
453
|
if current_depth >= walk.max_depth:
|
|
@@ -385,7 +466,7 @@ func _analyze_resource(path: String, current_depth: int, walk: DependencyWalk) -
|
|
|
385
466
|
var remaining: int = walk.max_depth - current_depth
|
|
386
467
|
if walk.visited.has(path):
|
|
387
468
|
var cached: Dictionary = walk.visited[path]
|
|
388
|
-
if
|
|
469
|
+
if GdhRead.as_int(cached["remaining"]) == remaining:
|
|
389
470
|
var cached_deps: Array[Dictionary] = cached["deps"]
|
|
390
471
|
return cached_deps
|
|
391
472
|
|
|
@@ -400,14 +481,20 @@ func _analyze_resource(path: String, current_depth: int, walk: DependencyWalk) -
|
|
|
400
481
|
# about `["res://addons/x/plugin.cfg"]` was a dependency of every project with the addon.
|
|
401
482
|
if path.get_extension() == "gd":
|
|
402
483
|
content = _code_lines(content)
|
|
484
|
+
var moved: Dictionary = _moved_by_uid(content)
|
|
403
485
|
|
|
404
486
|
for pattern: String in REFERENCE_PATTERNS:
|
|
405
|
-
var regex: RegEx =
|
|
487
|
+
var regex: RegEx = GdhPatterns.compiled(pattern)
|
|
406
488
|
for m: RegExMatch in regex.search_all(content):
|
|
407
489
|
var dep_path: String = _resolved(_referenced_path(m.get_string()), path)
|
|
490
|
+
dep_path = str(moved.get(dep_path, dep_path))
|
|
408
491
|
|
|
409
492
|
if not dep_path.begins_with("res://"):
|
|
410
493
|
continue
|
|
494
|
+
# A literal naming a directory, or the start of a path a script builds as it runs
|
|
495
|
+
# ("res://levels/" + name), names no file, and walked as one it was a missing dependency.
|
|
496
|
+
if dep_path.get_extension().is_empty():
|
|
497
|
+
continue
|
|
411
498
|
|
|
412
499
|
# Skip engine-internal resources unless requested. `addons/` is not one of
|
|
413
500
|
# them: it is ordinary project content, and often shipping content, so
|
|
@@ -439,7 +526,7 @@ func _depend(
|
|
|
439
526
|
dep_path: String,
|
|
440
527
|
by_class: String,
|
|
441
528
|
current_depth: int,
|
|
442
|
-
walk:
|
|
529
|
+
walk: GdhDependencyWalk
|
|
443
530
|
) -> void:
|
|
444
531
|
if dep_path == path:
|
|
445
532
|
return
|
|
@@ -474,7 +561,7 @@ static func classes_used_in(content: String, classes: Dictionary) -> Array[Strin
|
|
|
474
561
|
var used: Array[String] = []
|
|
475
562
|
if classes.is_empty():
|
|
476
563
|
return used
|
|
477
|
-
var word: RegEx =
|
|
564
|
+
var word: RegEx = GdhPatterns.compiled("[A-Za-z_][A-Za-z0-9_]*")
|
|
478
565
|
for line: String in content.split("\n"):
|
|
479
566
|
for m: RegExMatch in word.search_all(_code_of(line)):
|
|
480
567
|
var name: String = m.get_string()
|
|
@@ -499,6 +586,27 @@ static func _resolved(written: String, from: String) -> String:
|
|
|
499
586
|
|
|
500
587
|
|
|
501
588
|
# The path inside a preload(), load() or ext_resource match; a bare res:// match is the path.
|
|
589
|
+
# The ext_resource paths in [param content] that nothing is at, each with the file its uid names
|
|
590
|
+
# instead. The engine loads an ext_resource by its uid first, so a file moved outside the editor
|
|
591
|
+
# still loads, and the walk called it missing; every pattern reads the old path, the bare res://
|
|
592
|
+
# one included, so the paths are mapped before any of them runs.
|
|
593
|
+
static func _moved_by_uid(content: String) -> Dictionary:
|
|
594
|
+
var moved: Dictionary = {}
|
|
595
|
+
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
596
|
+
var entry: String = line.get_string()
|
|
597
|
+
var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(entry)
|
|
598
|
+
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
599
|
+
if at == null or uid == null or FileAccess.file_exists(at.get_string(1)):
|
|
600
|
+
continue
|
|
601
|
+
var id: int = ResourceUID.text_to_id(uid.get_string(1))
|
|
602
|
+
if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
|
|
603
|
+
continue
|
|
604
|
+
var found: String = ResourceUID.get_id_path(id)
|
|
605
|
+
if FileAccess.file_exists(found):
|
|
606
|
+
moved[at.get_string(1)] = found
|
|
607
|
+
return moved
|
|
608
|
+
|
|
609
|
+
|
|
502
610
|
func _referenced_path(matched: String) -> String:
|
|
503
611
|
var expression: String = ""
|
|
504
612
|
if "preload" in matched or "load" in matched:
|
|
@@ -507,6 +615,6 @@ func _referenced_path(matched: String) -> String:
|
|
|
507
615
|
expression = 'path="([^"]+)"'
|
|
508
616
|
else:
|
|
509
617
|
return matched
|
|
510
|
-
var inner: RegEx =
|
|
618
|
+
var inner: RegEx = GdhPatterns.compiled(expression)
|
|
511
619
|
var inner_match: RegExMatch = inner.search(matched)
|
|
512
620
|
return inner_match.get_string(1) if inner_match else matched
|
|
@@ -7,10 +7,10 @@ extends RefCounted
|
|
|
7
7
|
## made here passes the same checks and answers in the same shape. What the engine does that an
|
|
8
8
|
## editor does not, running every script, is put back in scene_file.gd and headless_check.gd.
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
10
|
+
const GdhAnimationTools = preload("../addons/gdharness_editor/tools/animation_tools.gd")
|
|
11
|
+
const GdhResourceTools = preload("../addons/gdharness_editor/tools/resource_tools.gd")
|
|
12
|
+
const GdhSceneTools = preload("../addons/gdharness_editor/tools/scene_tools.gd")
|
|
13
|
+
const GdhSceneFile = preload("../addons/gdharness_editor/scene_file.gd")
|
|
14
14
|
|
|
15
15
|
const SCENE_COMMANDS: Array[String] = [
|
|
16
16
|
"create_scene",
|
|
@@ -51,18 +51,18 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
51
51
|
var arguments: Dictionary = given
|
|
52
52
|
var tools: Node = null
|
|
53
53
|
if command in SCENE_COMMANDS:
|
|
54
|
-
tools =
|
|
54
|
+
tools = GdhSceneTools.new()
|
|
55
55
|
elif command in RESOURCE_COMMANDS:
|
|
56
|
-
tools =
|
|
56
|
+
tools = GdhResourceTools.new()
|
|
57
57
|
elif command in ANIMATION_COMMANDS:
|
|
58
|
-
tools =
|
|
58
|
+
tools = GdhAnimationTools.new()
|
|
59
59
|
else:
|
|
60
60
|
return {"ok": false, "error": "No editor tool runs headless as " + command}
|
|
61
61
|
var answer: Variant = tools.call(command, arguments)
|
|
62
62
|
tools.free()
|
|
63
63
|
if answer is Dictionary:
|
|
64
64
|
var answered: Dictionary = answer
|
|
65
|
-
if not
|
|
65
|
+
if not GdhSceneFile.by_path.is_empty():
|
|
66
66
|
answered["namedByPath"] = (
|
|
67
67
|
(
|
|
68
68
|
"%s %s named by path rather than by uid. A uid in a file's own header is resolved "
|
|
@@ -72,10 +72,10 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
72
72
|
+ "it name %s by uid."
|
|
73
73
|
)
|
|
74
74
|
% [
|
|
75
|
-
", ".join(
|
|
76
|
-
"is" if
|
|
77
|
-
"it" if
|
|
78
|
-
"it" if
|
|
75
|
+
", ".join(GdhSceneFile.by_path),
|
|
76
|
+
"is" if GdhSceneFile.by_path.size() == 1 else "are",
|
|
77
|
+
"it" if GdhSceneFile.by_path.size() == 1 else "them",
|
|
78
|
+
"it" if GdhSceneFile.by_path.size() == 1 else "them",
|
|
79
79
|
]
|
|
80
80
|
)
|
|
81
81
|
return answered
|
|
@@ -2,8 +2,8 @@ extends RefCounted
|
|
|
2
2
|
|
|
3
3
|
## export_presets.cfg, read and written in the format the editor's Project > Export saves it in.
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const GdhRead = preload("reading.gd")
|
|
6
|
+
const GdhLog = preload("logger.gd")
|
|
7
7
|
|
|
8
8
|
const PRESETS_FILE: String = "res://export_presets.cfg"
|
|
9
9
|
## Where the editor names the one preset of each platform it runs on a device.
|
|
@@ -19,10 +19,10 @@ const PLATFORMS: Dictionary[String, String] = {
|
|
|
19
19
|
"Web": "web_",
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
var _log:
|
|
22
|
+
var _log: GdhLog
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
func _init(p_log:
|
|
25
|
+
func _init(p_log: GdhLog) -> void:
|
|
26
26
|
_log = p_log
|
|
27
27
|
|
|
28
28
|
|
|
@@ -88,7 +88,7 @@ static func _runnable(config: ConfigFile, section: String) -> bool:
|
|
|
88
88
|
var platform: String = str(config.get_value(section, "platform", ""))
|
|
89
89
|
if config.has_section_key(RUNNABLE_SECTION, platform):
|
|
90
90
|
return str(config.get_value(RUNNABLE_SECTION, platform)) == str(config.get_value(section, "name", ""))
|
|
91
|
-
return
|
|
91
|
+
return GdhRead.as_bool(config.get_value(section, "runnable", false))
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
## Adds an export preset to export_presets.cfg, written with the keys the editor's Project > Export
|
|
@@ -113,17 +113,15 @@ func add_export_preset(params: Dictionary) -> Dictionary:
|
|
|
113
113
|
while config.has_section("preset." + str(index)):
|
|
114
114
|
var held: String = "preset." + str(index)
|
|
115
115
|
if str(config.get_value(held, "name", "")) == preset_name:
|
|
116
|
+
var path: String = str(config.get_value(held, "export_path", ""))
|
|
117
|
+
var to: String = "with no export path set" if path.is_empty() else "exporting to '%s'" % path
|
|
116
118
|
return _log.failure(
|
|
117
119
|
(
|
|
118
120
|
(
|
|
119
|
-
"There is already an export preset named '%s', for %s,
|
|
121
|
+
"There is already an export preset named '%s', for %s, %s. "
|
|
120
122
|
+ "project_export run exports with it; give another name to add a second."
|
|
121
123
|
)
|
|
122
|
-
% [
|
|
123
|
-
preset_name,
|
|
124
|
-
config.get_value(held, "platform", ""),
|
|
125
|
-
config.get_value(held, "export_path", "")
|
|
126
|
-
]
|
|
124
|
+
% [preset_name, config.get_value(held, "platform", ""), to]
|
|
127
125
|
)
|
|
128
126
|
)
|
|
129
127
|
index += 1
|
|
@@ -188,9 +186,11 @@ func add_export_preset(params: Dictionary) -> Dictionary:
|
|
|
188
186
|
## self-contained one, otherwise in the user's data directory, as the editor's own paths have it.
|
|
189
187
|
static func _templates_dir() -> String:
|
|
190
188
|
var version: Dictionary = Engine.get_version_info()
|
|
191
|
-
var named: String =
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
var named: String = (
|
|
190
|
+
"%d.%d" % [GdhRead.as_int(version.get("major", 0)), GdhRead.as_int(version.get("minor", 0))]
|
|
191
|
+
)
|
|
192
|
+
if GdhRead.as_int(version.get("patch", 0)) != 0:
|
|
193
|
+
named += "." + str(GdhRead.as_int(version.get("patch", 0)))
|
|
194
194
|
named += "." + str(version.get("status", ""))
|
|
195
195
|
if ClassDB.class_exists("CSharpScript"):
|
|
196
196
|
named += ".mono"
|