gdharness 0.4.1
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/LICENSE +22 -0
- package/README.md +125 -0
- package/build/cli.js +30353 -0
- package/build/godot/addons/auto_reload/auto_reload.gd +89 -0
- package/build/godot/addons/auto_reload/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/bridge_client.gd +197 -0
- package/build/godot/addons/gdharness_editor/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +88 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +104 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +397 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +85 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +427 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +885 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +292 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +77 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +254 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +283 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +169 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +119 -0
- package/build/godot/operations/audio_buses.gd +125 -0
- package/build/godot/operations/class_cache.gd +180 -0
- package/build/godot/operations/classdb_queries.gd +252 -0
- package/build/godot/operations/dependencies.gd +293 -0
- package/build/godot/operations/file_walk.gd +55 -0
- package/build/godot/operations/gdscript_analysis.gd +288 -0
- package/build/godot/operations/gdscript_authoring.gd +419 -0
- package/build/godot/operations/godot_operations.gd +186 -0
- package/build/godot/operations/import_pipeline.gd +390 -0
- package/build/godot/operations/input_actions.gd +237 -0
- package/build/godot/operations/logger.gd +33 -0
- package/build/godot/operations/plugins.gd +159 -0
- package/build/godot/operations/project_config.gd +153 -0
- package/build/godot/operations/project_diagnostics.gd +159 -0
- package/build/godot/operations/resource_files.gd +132 -0
- package/build/godot/operations/serialisation.gd +154 -0
- package/build/index.js +29252 -0
- package/package.json +57 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
const FileWalk = preload("file_walk.gd")
|
|
4
|
+
const Log = preload("logger.gd")
|
|
5
|
+
|
|
6
|
+
# What a dependency reference looks like in source, and how the path is read out of each form.
|
|
7
|
+
const REFERENCE_PATTERNS: Array[String] = [
|
|
8
|
+
"res://[^\"'\\s\\]\\)]+",
|
|
9
|
+
'preload\\("([^"]+)"\\)',
|
|
10
|
+
'load\\("([^"]+)"\\)',
|
|
11
|
+
'ext_resource.*path="([^"]+)"',
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
var _log: Log
|
|
15
|
+
var _files: FileWalk = FileWalk.new()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Everything one dependency walk carries: the settings it was started with, and the state it
|
|
19
|
+
# accumulates as it recurses. Kept together so the recursion passes one object rather than
|
|
20
|
+
# six positional arguments that have to stay in the same order at every call site.
|
|
21
|
+
class DependencyWalk:
|
|
22
|
+
var max_depth: int
|
|
23
|
+
var include_built_in: bool
|
|
24
|
+
var visited: Dictionary = {}
|
|
25
|
+
var path_stack: Array[String] = []
|
|
26
|
+
var circular_references: Array
|
|
27
|
+
|
|
28
|
+
func _init(p_max_depth: int, p_include_built_in: bool, p_circular_references: Array) -> void:
|
|
29
|
+
max_depth = p_max_depth
|
|
30
|
+
include_built_in = p_include_built_in
|
|
31
|
+
circular_references = p_circular_references
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
func _init(p_log: Log) -> void:
|
|
35
|
+
_log = p_log
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Get dependencies for a resource with circular reference detection
|
|
39
|
+
func get_dependencies(params: Dictionary) -> Dictionary:
|
|
40
|
+
var resource_path: String = str(params.get("resource_path", ""))
|
|
41
|
+
# No depth, or a depth of zero or less, means the whole chain; the walk stops at cycles.
|
|
42
|
+
var depth: int = int(params.get("depth", 0))
|
|
43
|
+
var max_depth: int = depth if depth > 0 else 1000
|
|
44
|
+
var include_built_in: bool = bool(params.get("include_builtin", false))
|
|
45
|
+
|
|
46
|
+
_log.info(
|
|
47
|
+
(
|
|
48
|
+
"Getting dependencies"
|
|
49
|
+
+ (" for: " + resource_path if not resource_path.is_empty() else " for all resources")
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
var dependencies: Dictionary = {}
|
|
54
|
+
var circular_references: Array = []
|
|
55
|
+
var total_resources: int = 0
|
|
56
|
+
|
|
57
|
+
if not resource_path.is_empty():
|
|
58
|
+
var full_path: String = resource_path
|
|
59
|
+
if not full_path.begins_with("res://"):
|
|
60
|
+
full_path = "res://" + full_path
|
|
61
|
+
|
|
62
|
+
if not FileAccess.file_exists(full_path):
|
|
63
|
+
return _log.failure("Resource file does not exist: " + full_path)
|
|
64
|
+
|
|
65
|
+
var walk: DependencyWalk = DependencyWalk.new(max_depth, include_built_in, circular_references)
|
|
66
|
+
dependencies[full_path] = _analyze_resource(full_path, 0, walk)
|
|
67
|
+
total_resources = 1
|
|
68
|
+
else:
|
|
69
|
+
var resource_extensions: Array[String] = ["tscn", "tres", "gd", "gdshader", "shader"]
|
|
70
|
+
var all_resources: Array[String] = []
|
|
71
|
+
for ext: String in resource_extensions:
|
|
72
|
+
all_resources.append_array(_files.find_files("res://", "." + ext))
|
|
73
|
+
|
|
74
|
+
# Each root gets a fresh walk so a cycle is reported from every resource it passes
|
|
75
|
+
# through, rather than only from whichever one happened to be walked first.
|
|
76
|
+
for res_path: String in all_resources:
|
|
77
|
+
var walk: DependencyWalk = DependencyWalk.new(max_depth, include_built_in, circular_references)
|
|
78
|
+
var deps: Array[Dictionary] = _analyze_resource(res_path, 0, walk)
|
|
79
|
+
if deps.size() > 0:
|
|
80
|
+
dependencies[res_path] = deps
|
|
81
|
+
|
|
82
|
+
total_resources = all_resources.size()
|
|
83
|
+
|
|
84
|
+
var dep_count: int = 0
|
|
85
|
+
for key: String in dependencies:
|
|
86
|
+
dep_count += _count_recursive(dependencies[key])
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"dependencies": dependencies,
|
|
90
|
+
"circular_references": circular_references,
|
|
91
|
+
"summary":
|
|
92
|
+
{
|
|
93
|
+
"total_resources": total_resources,
|
|
94
|
+
"total_dependencies": dep_count,
|
|
95
|
+
"circular_count": circular_references.size()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# What refers to a resource, and how: the scenes that instance it, the scripts that extend or
|
|
101
|
+
# preload it, and for a script with a class_name, every use of that name.
|
|
102
|
+
func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
103
|
+
var resource_path: String = str(params.get("resource_path", ""))
|
|
104
|
+
var file_types: Array = params.get("file_types", ["tscn", "tres", "gd", "gdshader"])
|
|
105
|
+
|
|
106
|
+
if not resource_path.begins_with("res://"):
|
|
107
|
+
resource_path = "res://" + resource_path
|
|
108
|
+
if not FileAccess.file_exists(resource_path):
|
|
109
|
+
return _log.failure("Resource file does not exist: " + resource_path)
|
|
110
|
+
|
|
111
|
+
_log.info("Finding usages of: " + resource_path)
|
|
112
|
+
|
|
113
|
+
var class_name_declared: String = _declared_class_name(resource_path)
|
|
114
|
+
var by_path: RegEx = RegEx.new()
|
|
115
|
+
by_path.compile('"(res://)?' + _regex_escaped(resource_path.substr(6)) + '"')
|
|
116
|
+
var by_class: RegEx = null
|
|
117
|
+
if not class_name_declared.is_empty():
|
|
118
|
+
by_class = RegEx.new()
|
|
119
|
+
by_class.compile("\\b" + _regex_escaped(class_name_declared) + "\\b")
|
|
120
|
+
|
|
121
|
+
var all_files: Array[String] = []
|
|
122
|
+
for ext: Variant in file_types:
|
|
123
|
+
all_files.append_array(_files.find_files("res://", "." + str(ext)))
|
|
124
|
+
|
|
125
|
+
var usages: Array[Dictionary] = []
|
|
126
|
+
var by_kind: Dictionary = {}
|
|
127
|
+
var total: int = 0
|
|
128
|
+
|
|
129
|
+
for file_path: String in all_files:
|
|
130
|
+
if file_path == resource_path:
|
|
131
|
+
continue
|
|
132
|
+
var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
|
|
133
|
+
if not file:
|
|
134
|
+
continue
|
|
135
|
+
var lines: PackedStringArray = file.get_as_text().split("\n")
|
|
136
|
+
file.close()
|
|
137
|
+
|
|
138
|
+
var references: Array[Dictionary] = []
|
|
139
|
+
for i: int in range(lines.size()):
|
|
140
|
+
var line: String = lines[i]
|
|
141
|
+
var kind: String = ""
|
|
142
|
+
if by_path.search(line) != null:
|
|
143
|
+
kind = _path_reference_kind(line)
|
|
144
|
+
elif by_class != null and by_class.search(line) != null:
|
|
145
|
+
kind = "extends" if line.strip_edges().begins_with("extends ") else "class_name"
|
|
146
|
+
if kind.is_empty():
|
|
147
|
+
continue
|
|
148
|
+
references.append({"line": i + 1, "kind": kind, "text": line.strip_edges()})
|
|
149
|
+
by_kind[kind] = int(by_kind.get(kind, 0)) + 1
|
|
150
|
+
|
|
151
|
+
if not references.is_empty():
|
|
152
|
+
usages.append({"file": file_path, "references": references})
|
|
153
|
+
total += references.size()
|
|
154
|
+
|
|
155
|
+
var declared: Variant = null
|
|
156
|
+
if not class_name_declared.is_empty():
|
|
157
|
+
declared = class_name_declared
|
|
158
|
+
return {
|
|
159
|
+
"resource_path": resource_path,
|
|
160
|
+
"class_name": declared,
|
|
161
|
+
"usages": usages,
|
|
162
|
+
"summary":
|
|
163
|
+
{
|
|
164
|
+
"files_searched": all_files.size(),
|
|
165
|
+
"files_with_usages": usages.size(),
|
|
166
|
+
"total": total,
|
|
167
|
+
"by_kind": by_kind,
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# The class_name a script declares, or empty for a scene, a resource or a script without one.
|
|
173
|
+
func _declared_class_name(path: String) -> String:
|
|
174
|
+
if not path.ends_with(".gd"):
|
|
175
|
+
return ""
|
|
176
|
+
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
177
|
+
if not file:
|
|
178
|
+
return ""
|
|
179
|
+
var declaration: RegEx = RegEx.new()
|
|
180
|
+
declaration.compile("^class_name\\s+([A-Za-z_][A-Za-z0-9_]*)")
|
|
181
|
+
while not file.eof_reached():
|
|
182
|
+
var found: RegExMatch = declaration.search(file.get_line())
|
|
183
|
+
if found != null:
|
|
184
|
+
file.close()
|
|
185
|
+
return found.get_string(1)
|
|
186
|
+
file.close()
|
|
187
|
+
return ""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# How a line that names the resource by path uses it.
|
|
191
|
+
func _path_reference_kind(line: String) -> String:
|
|
192
|
+
var trimmed: String = line.strip_edges()
|
|
193
|
+
if trimmed.begins_with("extends "):
|
|
194
|
+
return "extends"
|
|
195
|
+
if trimmed.begins_with("[ext_resource"):
|
|
196
|
+
return "ext_resource"
|
|
197
|
+
if "preload(" in trimmed:
|
|
198
|
+
return "preload"
|
|
199
|
+
if "load(" in trimmed:
|
|
200
|
+
return "load"
|
|
201
|
+
return "path"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
func _regex_escaped(text: String) -> String:
|
|
205
|
+
var escaped: String = ""
|
|
206
|
+
for character: String in text:
|
|
207
|
+
if character in "\\^$.|?*+()[]{}/":
|
|
208
|
+
escaped += "\\"
|
|
209
|
+
escaped += character
|
|
210
|
+
return escaped
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
func _count_recursive(deps: Array[Dictionary]) -> int:
|
|
214
|
+
var count: int = deps.size()
|
|
215
|
+
for dep: Dictionary in deps:
|
|
216
|
+
if dep.has("dependencies"):
|
|
217
|
+
count += _count_recursive(dep["dependencies"])
|
|
218
|
+
return count
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
func _analyze_resource(path: String, current_depth: int, walk: DependencyWalk) -> Array[Dictionary]:
|
|
222
|
+
var deps: Array[Dictionary] = []
|
|
223
|
+
|
|
224
|
+
if current_depth >= walk.max_depth:
|
|
225
|
+
return deps
|
|
226
|
+
|
|
227
|
+
if path in walk.path_stack:
|
|
228
|
+
var cycle: Array[String] = walk.path_stack.slice(walk.path_stack.find(path))
|
|
229
|
+
cycle.append(path)
|
|
230
|
+
if not cycle in walk.circular_references:
|
|
231
|
+
walk.circular_references.append(cycle)
|
|
232
|
+
return [{"path": path, "circular": true}]
|
|
233
|
+
|
|
234
|
+
if walk.visited.has(path):
|
|
235
|
+
var cached: Array[Dictionary] = walk.visited[path]
|
|
236
|
+
return cached
|
|
237
|
+
|
|
238
|
+
walk.path_stack.append(path)
|
|
239
|
+
|
|
240
|
+
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
241
|
+
if file:
|
|
242
|
+
var content: String = file.get_as_text()
|
|
243
|
+
file.close()
|
|
244
|
+
|
|
245
|
+
for pattern: String in REFERENCE_PATTERNS:
|
|
246
|
+
var regex: RegEx = RegEx.new()
|
|
247
|
+
regex.compile(pattern)
|
|
248
|
+
for m: RegExMatch in regex.search_all(content):
|
|
249
|
+
var dep_path: String = _referenced_path(m.get_string())
|
|
250
|
+
|
|
251
|
+
if not dep_path.begins_with("res://"):
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Skip engine-internal resources unless requested. `addons/` is not one of
|
|
255
|
+
# them: it is ordinary project content, and often shipping content, so
|
|
256
|
+
# treating it as built-in dropped every dependency of anything living there.
|
|
257
|
+
if not walk.include_built_in and dep_path.begins_with("res://."):
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
if dep_path == path:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
var dep_info: Dictionary = {"path": dep_path, "exists": FileAccess.file_exists(dep_path)}
|
|
264
|
+
|
|
265
|
+
if dep_info["exists"] and current_depth + 1 < walk.max_depth:
|
|
266
|
+
var sub_deps: Array[Dictionary] = _analyze_resource(dep_path, current_depth + 1, walk)
|
|
267
|
+
if sub_deps.size() > 0:
|
|
268
|
+
dep_info["dependencies"] = sub_deps
|
|
269
|
+
|
|
270
|
+
var already_added: bool = false
|
|
271
|
+
for existing: Dictionary in deps:
|
|
272
|
+
if existing.get("path", "") == dep_path:
|
|
273
|
+
already_added = true
|
|
274
|
+
break
|
|
275
|
+
if not already_added:
|
|
276
|
+
deps.append(dep_info)
|
|
277
|
+
|
|
278
|
+
walk.path_stack.pop_back()
|
|
279
|
+
walk.visited[path] = deps
|
|
280
|
+
return deps
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# The path inside a preload(), load() or ext_resource match; a bare res:// match is the path.
|
|
284
|
+
func _referenced_path(matched: String) -> String:
|
|
285
|
+
var inner: RegEx = RegEx.new()
|
|
286
|
+
if "preload" in matched or "load" in matched:
|
|
287
|
+
inner.compile('"([^"]+)"')
|
|
288
|
+
elif "ext_resource" in matched:
|
|
289
|
+
inner.compile('path="([^"]+)"')
|
|
290
|
+
else:
|
|
291
|
+
return matched
|
|
292
|
+
var inner_match: RegExMatch = inner.search(matched)
|
|
293
|
+
return inner_match.get_string(1) if inner_match else matched
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
# Two walks rather than one because they disagree about hidden entries on purpose: a suffix
|
|
4
|
+
# walk descends into every visible directory and matches whole names, while the extension walk
|
|
5
|
+
# skips anything beginning with a dot, including .import sidecars that would otherwise answer
|
|
6
|
+
# for the resource they belong to.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Files under `path` whose name ends with `extension`, which is passed with its dot.
|
|
10
|
+
func find_files(path: String, extension: String) -> Array[String]:
|
|
11
|
+
var files: Array[String] = []
|
|
12
|
+
var dir: DirAccess = DirAccess.open(path)
|
|
13
|
+
|
|
14
|
+
if dir:
|
|
15
|
+
dir.list_dir_begin()
|
|
16
|
+
var file_name: String = dir.get_next()
|
|
17
|
+
|
|
18
|
+
while file_name != "":
|
|
19
|
+
if dir.current_is_dir() and not file_name.begins_with("."):
|
|
20
|
+
files.append_array(find_files(path + file_name + "/", extension))
|
|
21
|
+
elif file_name.ends_with(extension):
|
|
22
|
+
files.append(path + file_name)
|
|
23
|
+
|
|
24
|
+
file_name = dir.get_next()
|
|
25
|
+
|
|
26
|
+
return files
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Files under `path` whose extension is in `extensions`, which are passed without their dot.
|
|
30
|
+
func find_files_with_extensions(path: String, extensions: Array) -> Array[String]:
|
|
31
|
+
var files: Array[String] = []
|
|
32
|
+
var dir: DirAccess = DirAccess.open(path)
|
|
33
|
+
|
|
34
|
+
if dir:
|
|
35
|
+
dir.list_dir_begin()
|
|
36
|
+
var file_name: String = dir.get_next()
|
|
37
|
+
|
|
38
|
+
while file_name != "":
|
|
39
|
+
if file_name.begins_with("."):
|
|
40
|
+
file_name = dir.get_next()
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
var full_path: String = path + file_name
|
|
44
|
+
if dir.current_is_dir():
|
|
45
|
+
files.append_array(find_files_with_extensions(full_path + "/", extensions))
|
|
46
|
+
else:
|
|
47
|
+
var ext: String = file_name.get_extension().to_lower()
|
|
48
|
+
if ext in extensions:
|
|
49
|
+
files.append(full_path)
|
|
50
|
+
|
|
51
|
+
file_name = dir.get_next()
|
|
52
|
+
|
|
53
|
+
dir.list_dir_end()
|
|
54
|
+
|
|
55
|
+
return files
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
const Log = preload("logger.gd")
|
|
4
|
+
|
|
5
|
+
var _log: Log
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
func _init(p_log: Log) -> void:
|
|
9
|
+
_log = p_log
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Analyze a GDScript file and return its structure
|
|
13
|
+
func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
14
|
+
var script_path: String = str(params.get("script_path", ""))
|
|
15
|
+
|
|
16
|
+
_log.info("Analyzing GDScript: " + script_path)
|
|
17
|
+
|
|
18
|
+
var full_script_path: String = script_path
|
|
19
|
+
if not full_script_path.begins_with("res://"):
|
|
20
|
+
full_script_path = "res://" + full_script_path
|
|
21
|
+
|
|
22
|
+
if not FileAccess.file_exists(full_script_path):
|
|
23
|
+
return _log.failure("Script file does not exist: " + full_script_path)
|
|
24
|
+
|
|
25
|
+
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.READ)
|
|
26
|
+
if not file:
|
|
27
|
+
return _log.failure("Failed to open script file: " + full_script_path)
|
|
28
|
+
|
|
29
|
+
var content: String = file.get_as_text()
|
|
30
|
+
file.close()
|
|
31
|
+
|
|
32
|
+
var lines: PackedStringArray = content.split("\n")
|
|
33
|
+
|
|
34
|
+
var declared_class_name: Variant = null
|
|
35
|
+
var extends_name: String = "RefCounted"
|
|
36
|
+
var signals: Array[Dictionary] = []
|
|
37
|
+
var variables: Array[Dictionary] = []
|
|
38
|
+
var functions: Array[Dictionary] = []
|
|
39
|
+
var constants: Array[Dictionary] = []
|
|
40
|
+
var enums: Array[Dictionary] = []
|
|
41
|
+
var inner_classes: Array[String] = []
|
|
42
|
+
var dependencies: Array[String] = []
|
|
43
|
+
|
|
44
|
+
var in_multiline_string: bool = false
|
|
45
|
+
|
|
46
|
+
for i: int in range(lines.size()):
|
|
47
|
+
var stripped: String = lines[i].strip_edges()
|
|
48
|
+
|
|
49
|
+
if stripped.is_empty() or stripped.begins_with("#"):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
if '"""' in stripped or "'''" in stripped:
|
|
53
|
+
in_multiline_string = not in_multiline_string
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
if in_multiline_string:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
if stripped.begins_with("class_name "):
|
|
60
|
+
declared_class_name = stripped.substr(11).strip_edges()
|
|
61
|
+
elif stripped.begins_with("extends "):
|
|
62
|
+
extends_name = stripped.substr(8).strip_edges()
|
|
63
|
+
elif stripped.begins_with("signal "):
|
|
64
|
+
signals.append(_parse_signal(stripped, i + 1))
|
|
65
|
+
elif stripped.begins_with("const "):
|
|
66
|
+
constants.append(_parse_constant(stripped, i + 1))
|
|
67
|
+
elif stripped.begins_with("enum "):
|
|
68
|
+
enums.append(_parse_enum(stripped, i + 1))
|
|
69
|
+
elif (
|
|
70
|
+
stripped.begins_with("var ")
|
|
71
|
+
or stripped.begins_with("@export")
|
|
72
|
+
or stripped.begins_with("@onready")
|
|
73
|
+
):
|
|
74
|
+
variables.append(_parse_variable(stripped, i + 1))
|
|
75
|
+
elif stripped.begins_with("func ") or stripped.begins_with("static func "):
|
|
76
|
+
functions.append(_parse_function(stripped, i + 1))
|
|
77
|
+
elif stripped.begins_with("class "):
|
|
78
|
+
inner_classes.append(stripped.substr(6).split(":")[0].split(" ")[0].strip_edges())
|
|
79
|
+
|
|
80
|
+
if "preload(" in stripped or "load(" in stripped:
|
|
81
|
+
for dep: String in _extract_dependencies(stripped):
|
|
82
|
+
if dep not in dependencies:
|
|
83
|
+
dependencies.append(dep)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"path": script_path,
|
|
87
|
+
"full_path": full_script_path,
|
|
88
|
+
"class_name": declared_class_name,
|
|
89
|
+
"extends": extends_name,
|
|
90
|
+
"signals": signals,
|
|
91
|
+
"variables": variables,
|
|
92
|
+
"functions": functions,
|
|
93
|
+
"constants": constants,
|
|
94
|
+
"enums": enums,
|
|
95
|
+
"inner_classes": inner_classes,
|
|
96
|
+
"dependencies": dependencies,
|
|
97
|
+
"line_count": lines.size()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
func _parse_signal(line: String, line_num: int) -> Dictionary:
|
|
102
|
+
var signal_text: String = line.substr(7).strip_edges()
|
|
103
|
+
var signal_name: String = ""
|
|
104
|
+
var params: Array[Dictionary] = []
|
|
105
|
+
|
|
106
|
+
if "(" in signal_text:
|
|
107
|
+
var parts: PackedStringArray = signal_text.split("(")
|
|
108
|
+
signal_name = parts[0].strip_edges()
|
|
109
|
+
if parts.size() > 1:
|
|
110
|
+
var params_text: String = parts[1].replace(")", "").strip_edges()
|
|
111
|
+
if not params_text.is_empty():
|
|
112
|
+
for p: String in params_text.split(","):
|
|
113
|
+
params.append(_parse_param(p.strip_edges()))
|
|
114
|
+
else:
|
|
115
|
+
signal_name = signal_text
|
|
116
|
+
|
|
117
|
+
return {"name": signal_name, "params": params, "line": line_num}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
func _parse_constant(line: String, line_num: int) -> Dictionary:
|
|
121
|
+
var const_text: String = line.substr(6).strip_edges()
|
|
122
|
+
var name: String = ""
|
|
123
|
+
var value: String = ""
|
|
124
|
+
var type_hint: String = ""
|
|
125
|
+
|
|
126
|
+
if "=" in const_text:
|
|
127
|
+
var parts: PackedStringArray = const_text.split("=", true, 1)
|
|
128
|
+
var name_part: String = parts[0].strip_edges()
|
|
129
|
+
value = parts[1].strip_edges() if parts.size() > 1 else ""
|
|
130
|
+
|
|
131
|
+
if ":" in name_part:
|
|
132
|
+
var type_parts: PackedStringArray = name_part.split(":")
|
|
133
|
+
name = type_parts[0].strip_edges()
|
|
134
|
+
type_hint = type_parts[1].strip_edges()
|
|
135
|
+
else:
|
|
136
|
+
name = name_part
|
|
137
|
+
else:
|
|
138
|
+
name = const_text
|
|
139
|
+
|
|
140
|
+
return {"name": name, "value": value, "type": type_hint, "line": line_num}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
func _parse_enum(line: String, line_num: int) -> Dictionary:
|
|
144
|
+
var enum_text: String = line.substr(5).strip_edges()
|
|
145
|
+
var enum_name: String = ""
|
|
146
|
+
var values: Array[String] = []
|
|
147
|
+
|
|
148
|
+
if "{" in enum_text:
|
|
149
|
+
var parts: PackedStringArray = enum_text.split("{")
|
|
150
|
+
enum_name = parts[0].strip_edges()
|
|
151
|
+
if parts.size() > 1:
|
|
152
|
+
var values_text: String = parts[1].replace("}", "").strip_edges()
|
|
153
|
+
if not values_text.is_empty():
|
|
154
|
+
for v: String in values_text.split(","):
|
|
155
|
+
var val: String = v.strip_edges()
|
|
156
|
+
if not val.is_empty():
|
|
157
|
+
values.append(val)
|
|
158
|
+
else:
|
|
159
|
+
enum_name = enum_text
|
|
160
|
+
|
|
161
|
+
return {"name": enum_name, "values": values, "line": line_num}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
func _parse_variable(line: String, line_num: int) -> Dictionary:
|
|
165
|
+
var is_export: bool = line.begins_with("@export")
|
|
166
|
+
var is_onready: bool = "@onready" in line
|
|
167
|
+
var export_hint: String = ""
|
|
168
|
+
|
|
169
|
+
if is_export:
|
|
170
|
+
var export_match: int = line.find("@export")
|
|
171
|
+
var hint_end: int = line.find("var ")
|
|
172
|
+
if hint_end > export_match:
|
|
173
|
+
var hint_part: String = line.substr(export_match + 7, hint_end - export_match - 7).strip_edges()
|
|
174
|
+
if hint_part.begins_with("_"):
|
|
175
|
+
export_hint = hint_part.substr(1).split(" ")[0]
|
|
176
|
+
|
|
177
|
+
var var_pos: int = line.find("var ")
|
|
178
|
+
if var_pos == -1:
|
|
179
|
+
return {"name": "", "line": line_num}
|
|
180
|
+
|
|
181
|
+
var var_text: String = line.substr(var_pos + 4).strip_edges()
|
|
182
|
+
var name: String = ""
|
|
183
|
+
var type_hint: String = ""
|
|
184
|
+
var default_value: String = ""
|
|
185
|
+
|
|
186
|
+
if "=" in var_text:
|
|
187
|
+
var parts: PackedStringArray = var_text.split("=", true, 1)
|
|
188
|
+
var name_part: String = parts[0].strip_edges()
|
|
189
|
+
default_value = parts[1].strip_edges() if parts.size() > 1 else ""
|
|
190
|
+
|
|
191
|
+
if ":" in name_part:
|
|
192
|
+
var type_parts: PackedStringArray = name_part.split(":")
|
|
193
|
+
name = type_parts[0].strip_edges()
|
|
194
|
+
type_hint = type_parts[1].strip_edges()
|
|
195
|
+
else:
|
|
196
|
+
name = name_part
|
|
197
|
+
elif ":" in var_text:
|
|
198
|
+
var type_parts: PackedStringArray = var_text.split(":")
|
|
199
|
+
name = type_parts[0].strip_edges()
|
|
200
|
+
type_hint = type_parts[1].strip_edges()
|
|
201
|
+
else:
|
|
202
|
+
name = var_text.split(" ")[0].strip_edges()
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
"name": name,
|
|
206
|
+
"type": type_hint,
|
|
207
|
+
"default_value": default_value,
|
|
208
|
+
"is_export": is_export,
|
|
209
|
+
"export_hint": export_hint,
|
|
210
|
+
"is_onready": is_onready,
|
|
211
|
+
"line": line_num
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
func _parse_function(line: String, line_num: int) -> Dictionary:
|
|
216
|
+
var is_static: bool = line.begins_with("static ")
|
|
217
|
+
var func_text: String = line
|
|
218
|
+
|
|
219
|
+
if is_static:
|
|
220
|
+
func_text = line.substr(7).strip_edges()
|
|
221
|
+
|
|
222
|
+
func_text = func_text.substr(5).strip_edges()
|
|
223
|
+
|
|
224
|
+
var name: String = ""
|
|
225
|
+
var params: Array[Dictionary] = []
|
|
226
|
+
var return_type: String = ""
|
|
227
|
+
|
|
228
|
+
if "(" in func_text:
|
|
229
|
+
var paren_start: int = func_text.find("(")
|
|
230
|
+
name = func_text.substr(0, paren_start).strip_edges()
|
|
231
|
+
|
|
232
|
+
var paren_end: int = func_text.rfind(")")
|
|
233
|
+
if paren_end > paren_start:
|
|
234
|
+
var params_text: String = func_text.substr(paren_start + 1, paren_end - paren_start - 1)
|
|
235
|
+
if not params_text.is_empty():
|
|
236
|
+
for p: String in params_text.split(","):
|
|
237
|
+
params.append(_parse_param(p.strip_edges()))
|
|
238
|
+
|
|
239
|
+
var after_paren: String = func_text.substr(paren_end + 1).strip_edges()
|
|
240
|
+
if after_paren.begins_with("->"):
|
|
241
|
+
return_type = after_paren.substr(2).replace(":", "").strip_edges()
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"name": name,
|
|
245
|
+
"params": params,
|
|
246
|
+
"return_type": return_type,
|
|
247
|
+
"is_virtual": name.begins_with("_"),
|
|
248
|
+
"is_static": is_static,
|
|
249
|
+
"line": line_num
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
func _parse_param(param_text: String) -> Dictionary:
|
|
254
|
+
var name: String = ""
|
|
255
|
+
var type_hint: String = ""
|
|
256
|
+
var default_value: String = ""
|
|
257
|
+
|
|
258
|
+
if "=" in param_text:
|
|
259
|
+
var parts: PackedStringArray = param_text.split("=", true, 1)
|
|
260
|
+
var name_part: String = parts[0].strip_edges()
|
|
261
|
+
default_value = parts[1].strip_edges() if parts.size() > 1 else ""
|
|
262
|
+
|
|
263
|
+
if ":" in name_part:
|
|
264
|
+
var type_parts: PackedStringArray = name_part.split(":")
|
|
265
|
+
name = type_parts[0].strip_edges()
|
|
266
|
+
type_hint = type_parts[1].strip_edges()
|
|
267
|
+
else:
|
|
268
|
+
name = name_part
|
|
269
|
+
elif ":" in param_text:
|
|
270
|
+
var type_parts: PackedStringArray = param_text.split(":")
|
|
271
|
+
name = type_parts[0].strip_edges()
|
|
272
|
+
type_hint = type_parts[1].strip_edges()
|
|
273
|
+
else:
|
|
274
|
+
name = param_text
|
|
275
|
+
|
|
276
|
+
return {"name": name, "type": type_hint, "default": default_value}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
func _extract_dependencies(line: String) -> Array[String]:
|
|
280
|
+
var deps: Array[String] = []
|
|
281
|
+
var regex: RegEx = RegEx.new()
|
|
282
|
+
|
|
283
|
+
regex.compile("(?:preload|load)\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)")
|
|
284
|
+
|
|
285
|
+
for m: RegExMatch in regex.search_all(line):
|
|
286
|
+
deps.append(m.get_string(1))
|
|
287
|
+
|
|
288
|
+
return deps
|