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
|
@@ -10,9 +10,9 @@ extends RefCounted
|
|
|
10
10
|
## without making a node. Loading the file still does what loading does in the game: static
|
|
11
11
|
## initialisers run, and a resource with a script is made with it.
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
13
|
+
const GdhPropertyValues = preload("property_values.gd")
|
|
14
|
+
const GdhRead = preload("reading.gd")
|
|
15
|
+
const GdhSerialisation = preload("serialisation.gd")
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf and
|
|
@@ -42,13 +42,43 @@ static func scene_node(scene_path: String, node_path: String) -> Dictionary:
|
|
|
42
42
|
return {"ok": true, "name": found["name"], "type": found["type"], "properties": found["properties"]}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
##
|
|
46
|
-
##
|
|
47
|
-
|
|
45
|
+
## The node at [param node_path] in [param scene_path] as [method scene_node] answers it, when it is
|
|
46
|
+
## a [param kind]; otherwise a refusal saying what the scene has there and where its nodes of that
|
|
47
|
+
## kind are, so the caller has the path to pass. The file is read once, because every load prints
|
|
48
|
+
## the engine's warnings about it again.
|
|
49
|
+
static func scene_node_of_kind(scene_path: String, node_path: String, kind: String) -> Dictionary:
|
|
48
50
|
var laid: Dictionary = _laid(scene_path)
|
|
49
51
|
if not laid.get("ok", false):
|
|
50
|
-
return
|
|
52
|
+
return laid
|
|
51
53
|
var nodes: Dictionary = laid["nodes"]
|
|
54
|
+
var path: String = _joined(".", node_path)
|
|
55
|
+
var said: String = "%s not found at: %s" % [kind, node_path]
|
|
56
|
+
var found: String = ""
|
|
57
|
+
if nodes.has(path):
|
|
58
|
+
found = str(nodes[path]["type"])
|
|
59
|
+
if ClassDB.class_exists(found) and ClassDB.is_parent_class(found, kind):
|
|
60
|
+
return {
|
|
61
|
+
"ok": true,
|
|
62
|
+
"name": nodes[path]["name"],
|
|
63
|
+
"type": found,
|
|
64
|
+
"properties": nodes[path]["properties"]
|
|
65
|
+
}
|
|
66
|
+
else:
|
|
67
|
+
var hinted: String = said
|
|
68
|
+
if not nodes.has(_joined(".", node_path.get_slice("/", 0))):
|
|
69
|
+
hinted = GdhRead.from_the_root(said, node_path, str(nodes["."]["name"]))
|
|
70
|
+
said = hinted if hinted != said else _missing_in(nodes, said, node_path)
|
|
71
|
+
var held: PackedStringArray = []
|
|
72
|
+
for at: Variant in nodes:
|
|
73
|
+
var type: String = str(nodes[at]["type"])
|
|
74
|
+
if ClassDB.class_exists(type) and ClassDB.is_parent_class(type, kind):
|
|
75
|
+
var _held: bool = held.append(str(at))
|
|
76
|
+
return {"ok": false, "error": GdhRead.not_of_kind(said, node_path, found, kind, held)}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## [param said], the refusal for [param node_path], which the scene laid as [param nodes] does not
|
|
80
|
+
## have, with where the path leaves the scene as [method GdhRead.missing_below] says it.
|
|
81
|
+
static func _missing_in(nodes: Dictionary, said: String, node_path: String) -> String:
|
|
52
82
|
var parts: PackedStringArray = node_path.trim_prefix("./").split("/", false)
|
|
53
83
|
var at: String = "."
|
|
54
84
|
var reached: PackedStringArray = []
|
|
@@ -64,7 +94,7 @@ static func missing_from(said: String, scene_path: String, node_path: String) ->
|
|
|
64
94
|
var children: Array[String] = nodes[at]["children"]
|
|
65
95
|
for child: String in children:
|
|
66
96
|
var _named: bool = names.append(str(nodes[child]["name"]))
|
|
67
|
-
return
|
|
97
|
+
return GdhRead.missing_below(said, str(nodes["."]["name"]), reached, parts[reached.size()], names)
|
|
68
98
|
|
|
69
99
|
|
|
70
100
|
## Every node of [param scene_path] as {ok, nodes}, keyed by path, or a refusal.
|
|
@@ -77,15 +107,14 @@ static func _laid(scene_path: String) -> Dictionary:
|
|
|
77
107
|
if loaded == null:
|
|
78
108
|
return {"ok": false, "error": "Failed to load %s as a scene" % scene_path}
|
|
79
109
|
if not loaded is PackedScene:
|
|
80
|
-
var
|
|
81
|
-
var sort: String = ("an " if kind.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
110
|
+
var sort: String = GdhRead.with_article(loaded.get_class())
|
|
82
111
|
return {
|
|
83
112
|
"ok": false,
|
|
84
113
|
"error": "%s is %s, not a scene: resource_edit read reads its properties." % [scene_path, sort]
|
|
85
114
|
}
|
|
86
115
|
var packed: PackedScene = loaded
|
|
87
116
|
var nodes: Dictionary = {}
|
|
88
|
-
var refused: String = _lay(packed.get_state(), ".", nodes, [scene_path])
|
|
117
|
+
var refused: String = _lay(packed.get_state(), ".", nodes, [scene_path], packed)
|
|
89
118
|
if not refused.is_empty():
|
|
90
119
|
return {"ok": false, "error": refused}
|
|
91
120
|
if not nodes.has("."):
|
|
@@ -99,8 +128,11 @@ static func _laid(scene_path: String) -> Dictionary:
|
|
|
99
128
|
##
|
|
100
129
|
## [param within] is the files being laid, outermost first, so a scene that instances one of them
|
|
101
130
|
## is refused rather than laid forever; the engine refuses to load such a scene, so this is only
|
|
102
|
-
## reached through a state built some other way.
|
|
103
|
-
|
|
131
|
+
## reached through a state built some other way. [param loaded] is the scene [param state] is read
|
|
132
|
+
## from, when the caller has it loaded already.
|
|
133
|
+
static func _lay(
|
|
134
|
+
state: SceneState, prefix: String, nodes: Dictionary, within: Array, loaded: PackedScene = null
|
|
135
|
+
) -> String:
|
|
104
136
|
var base: SceneState = state.get_base_scene_state()
|
|
105
137
|
if base != null and not nodes.has(prefix):
|
|
106
138
|
var refused: String = _lay(base, prefix, nodes, within)
|
|
@@ -164,7 +196,7 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
164
196
|
# Only the scene being read and the scenes it inherits share its root, the owner whose editable
|
|
165
197
|
# instances a write here sets; a file instanced below makes its own owner's editable.
|
|
166
198
|
if prefix == ".":
|
|
167
|
-
for editable: String in _editable_in(state.get_path()):
|
|
199
|
+
for editable: String in _editable_in(state.get_path(), loaded):
|
|
168
200
|
var at: String = _joined(prefix, editable)
|
|
169
201
|
var marked: Dictionary = nodes.get(at, {})
|
|
170
202
|
if marked.has("editableChildren"):
|
|
@@ -173,10 +205,13 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
173
205
|
|
|
174
206
|
|
|
175
207
|
## The paths of the instances [param scene_path] gives editable children, from its root. SceneState
|
|
176
|
-
## does not give them to a script, so they are read off the scene's bundle
|
|
177
|
-
|
|
208
|
+
## does not give them to a script, so they are read off the scene's bundle: [param given_scene]'s,
|
|
209
|
+
## when the caller has the file loaded, since loading it again prints its warnings again.
|
|
210
|
+
static func _editable_in(scene_path: String, given_scene: PackedScene = null) -> PackedStringArray:
|
|
178
211
|
var found: PackedStringArray = []
|
|
179
|
-
var loaded: Resource =
|
|
212
|
+
var loaded: Resource = given_scene
|
|
213
|
+
if loaded == null:
|
|
214
|
+
loaded = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
180
215
|
if not loaded is PackedScene:
|
|
181
216
|
return found
|
|
182
217
|
var given: Variant = loaded.get("_bundled")
|
|
@@ -236,7 +271,7 @@ static func _by_id(nodes: Dictionary, owner: String, id: int) -> String:
|
|
|
236
271
|
if path == owner or not (owner == "." or path.begins_with(owner + "/")):
|
|
237
272
|
continue
|
|
238
273
|
var entry: Dictionary = nodes[path]
|
|
239
|
-
if
|
|
274
|
+
if GdhRead.as_int(entry.get("uniqueId", -1), -1) != id:
|
|
240
275
|
continue
|
|
241
276
|
var above: String = "." if not path.contains("/") else path.get_base_dir()
|
|
242
277
|
if _owner_of(nodes, above, owner) == owner:
|
|
@@ -311,7 +346,7 @@ static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
|
311
346
|
if script != null:
|
|
312
347
|
for declared: Dictionary in script.get_script_property_list():
|
|
313
348
|
var property: String = str(declared.get("name", ""))
|
|
314
|
-
if not property.is_empty() and
|
|
349
|
+
if not property.is_empty() and GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
315
350
|
scripted[property] = script.get_property_default_value(property)
|
|
316
351
|
var values: Dictionary = {}
|
|
317
352
|
if ClassDB.class_exists(type) and ClassDB.can_instantiate(type):
|
|
@@ -321,7 +356,7 @@ static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
|
321
356
|
bare.set(str(property), given[property])
|
|
322
357
|
for declared: Dictionary in bare.get_property_list():
|
|
323
358
|
var property: String = str(declared.get("name", ""))
|
|
324
|
-
if not property.is_empty() and
|
|
359
|
+
if not property.is_empty() and GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
325
360
|
values[property] = bare.get(property)
|
|
326
361
|
if bare is Node:
|
|
327
362
|
bare.free()
|
|
@@ -330,7 +365,7 @@ static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
|
330
365
|
# Given last, which keeps what a bare node cannot hold: the script itself, and a path to a node
|
|
331
366
|
# for a property that holds the node once it is in a tree.
|
|
332
367
|
values.merge(given, true)
|
|
333
|
-
var serialiser:
|
|
368
|
+
var serialiser: GdhSerialisation = GdhSerialisation.new()
|
|
334
369
|
var answered: Dictionary = {}
|
|
335
370
|
for property: Variant in values:
|
|
336
371
|
answered[property] = serialiser.serialize_value(values[property])
|
|
@@ -369,13 +404,13 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
369
404
|
if script != null:
|
|
370
405
|
for declared: Dictionary in script.get_script_property_list():
|
|
371
406
|
scripted[str(declared.get("name", ""))] = true
|
|
372
|
-
var serialiser:
|
|
407
|
+
var serialiser: GdhSerialisation = GdhSerialisation.new()
|
|
373
408
|
var properties: Dictionary = {}
|
|
374
409
|
for p: Dictionary in loaded.get_property_list():
|
|
375
410
|
var property: String = str(p.get("name", ""))
|
|
376
411
|
if property.is_empty() or property == "script":
|
|
377
412
|
continue
|
|
378
|
-
if not (
|
|
413
|
+
if not (GdhRead.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
379
414
|
continue
|
|
380
415
|
var value: Variant = loaded.get(property)
|
|
381
416
|
if not include_defaults:
|
|
@@ -384,7 +419,7 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
384
419
|
if scripted.has(property)
|
|
385
420
|
else ClassDB.class_get_property_default_value(loaded.get_class(), property)
|
|
386
421
|
)
|
|
387
|
-
if
|
|
422
|
+
if GdhPropertyValues.same(value, default):
|
|
388
423
|
continue
|
|
389
424
|
properties[property] = serialiser.serialize_value(value)
|
|
390
425
|
var answer: Dictionary = {
|
|
@@ -392,6 +427,11 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
392
427
|
}
|
|
393
428
|
if script != null:
|
|
394
429
|
answer["script"] = script.resource_path
|
|
430
|
+
# resourceType is the engine class under the script, which for a class the project declares
|
|
431
|
+
# is Resource whatever create was given.
|
|
432
|
+
var declared_class: String = str(script.get_global_name())
|
|
433
|
+
if not declared_class.is_empty():
|
|
434
|
+
answer["scriptClass"] = declared_class
|
|
395
435
|
return answer
|
|
396
436
|
|
|
397
437
|
|
|
@@ -412,7 +452,7 @@ static func _kept_in(value: Variant, file: String, id: String, seen: Dictionary)
|
|
|
412
452
|
if held.resource_scene_unique_id == id:
|
|
413
453
|
return held
|
|
414
454
|
for p: Dictionary in held.get_property_list():
|
|
415
|
-
if
|
|
455
|
+
if GdhRead.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
416
456
|
var found: Resource = _kept_in(held.get(str(p.get("name", ""))), file, id, seen)
|
|
417
457
|
if found != null:
|
|
418
458
|
return found
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
+
const GdhRead = preload("reading.gd")
|
|
4
|
+
|
|
3
5
|
# Two walks rather than one because they disagree about hidden entries on purpose: a suffix
|
|
4
6
|
# walk descends into every visible directory and matches whole names, while the extension walk
|
|
5
7
|
# skips anything beginning with a dot, including .import sidecars that would otherwise answer
|
|
@@ -30,6 +32,38 @@ static func missing_reason(path: String) -> String:
|
|
|
30
32
|
return "" if FileAccess.file_exists(path) else path + " does not exist"
|
|
31
33
|
|
|
32
34
|
|
|
35
|
+
## [param said], the refusal for [param path], a res:// path nothing is at, with the files of its
|
|
36
|
+
## extension whose names are nearest its own, or every one when there are few and none is near, so
|
|
37
|
+
## the caller has a path to send instead.
|
|
38
|
+
func with_nearest_files(said: String, path: String) -> String:
|
|
39
|
+
var extension: String = path.get_extension()
|
|
40
|
+
if extension.is_empty():
|
|
41
|
+
return said
|
|
42
|
+
var files: Array[String] = find_files_with_extensions("res://", [extension])
|
|
43
|
+
# The walk lists a directory in the order the filesystem keeps it, which differs between
|
|
44
|
+
# platforms, so the same project was answered with its files in another order on Linux.
|
|
45
|
+
files.sort()
|
|
46
|
+
if files.is_empty():
|
|
47
|
+
return "%s. The project holds no .%s file." % [said, extension]
|
|
48
|
+
var names: PackedStringArray = []
|
|
49
|
+
for file: String in files:
|
|
50
|
+
var _named: bool = names.append(file.get_file().get_basename())
|
|
51
|
+
var close: PackedStringArray = GdhRead.nearest(path.get_file().get_basename(), names)
|
|
52
|
+
var offered: PackedStringArray = []
|
|
53
|
+
for file: String in files:
|
|
54
|
+
if file.get_file().get_basename() in close:
|
|
55
|
+
var _offered: bool = offered.append(file)
|
|
56
|
+
if not offered.is_empty():
|
|
57
|
+
var one: bool = offered.size() == 1
|
|
58
|
+
return (
|
|
59
|
+
"%s. The nearest .%s %s %s."
|
|
60
|
+
% [said, extension, "file is" if one else "files are", ", ".join(offered)]
|
|
61
|
+
)
|
|
62
|
+
if files.size() <= GdhRead.NAMED_CHILDREN:
|
|
63
|
+
return "%s. The project's .%s files are %s." % [said, extension, ", ".join(files)]
|
|
64
|
+
return "%s. None of the project's %d .%s files is named like it." % [said, files.size(), extension]
|
|
65
|
+
|
|
66
|
+
|
|
33
67
|
# A directory the engine steps over, marker file and all.
|
|
34
68
|
func is_stepped_over(path: String) -> bool:
|
|
35
69
|
return FileAccess.file_exists(path + ".gdignore")
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
3
|
+
const GdhDependencies = preload("dependencies.gd")
|
|
4
|
+
const GdhFileWalk = preload("file_walk.gd")
|
|
5
|
+
const GdhPatterns = preload("patterns.gd")
|
|
6
|
+
const GdhRead = preload("reading.gd")
|
|
7
|
+
const GdhLog = preload("logger.gd")
|
|
7
8
|
|
|
8
|
-
var _log:
|
|
9
|
+
var _log: GdhLog
|
|
9
10
|
## Kept for the call, so the project's class names are read once however many bases are walked.
|
|
10
|
-
var _dependencies:
|
|
11
|
+
var _dependencies: GdhDependencies = null
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
func _init(p_log:
|
|
14
|
+
func _init(p_log: GdhLog) -> void:
|
|
14
15
|
_log = p_log
|
|
15
16
|
|
|
16
17
|
|
|
@@ -25,7 +26,11 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
25
26
|
full_script_path = "res://" + full_script_path
|
|
26
27
|
|
|
27
28
|
if not FileAccess.file_exists(full_script_path):
|
|
28
|
-
return _log.failure(
|
|
29
|
+
return _log.failure(
|
|
30
|
+
GdhFileWalk.new().with_nearest_files(
|
|
31
|
+
"Script file does not exist: " + full_script_path, full_script_path
|
|
32
|
+
)
|
|
33
|
+
)
|
|
29
34
|
|
|
30
35
|
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.READ)
|
|
31
36
|
if not file:
|
|
@@ -62,7 +67,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
62
67
|
if _triple_quotes(raw) % 2 == 1:
|
|
63
68
|
in_multiline_string = false
|
|
64
69
|
continue
|
|
65
|
-
var stripped: String =
|
|
70
|
+
var stripped: String = GdhPatterns.without_comment(raw)
|
|
66
71
|
|
|
67
72
|
if stripped.is_empty():
|
|
68
73
|
continue
|
|
@@ -85,7 +90,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
85
90
|
# on the line with them off. `@abstract class_name X` was reported as no declared class,
|
|
86
91
|
# and `@abstract func x()` and `@warning_ignore("...") func x()` as no function at all:
|
|
87
92
|
# gdUnit4 alone declares 248 methods that way and this answered with none of them.
|
|
88
|
-
var header: String =
|
|
93
|
+
var header: String = GdhPatterns.without_annotations(stripped)
|
|
89
94
|
|
|
90
95
|
var at: int = i
|
|
91
96
|
# A declaration wrapped across lines is one declaration, and reading the first line alone
|
|
@@ -104,9 +109,9 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
104
109
|
if _can_wrap(header):
|
|
105
110
|
while _bracket_depth(stripped) > 0 and i + 1 < lines.size():
|
|
106
111
|
i += 1
|
|
107
|
-
stripped = (stripped + " " +
|
|
108
|
-
header =
|
|
109
|
-
var declaration: RegExMatch =
|
|
112
|
+
stripped = (stripped + " " + GdhPatterns.without_comment(lines[i])).strip_edges()
|
|
113
|
+
header = GdhPatterns.without_annotations(stripped)
|
|
114
|
+
var declaration: RegExMatch = GdhPatterns.declared_class(header)
|
|
110
115
|
if declaration != null:
|
|
111
116
|
declared_class_name = declaration.get_string(1)
|
|
112
117
|
# `class_name X extends Y` is also one line, and taking the rest of it as the name
|
|
@@ -144,9 +149,9 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
144
149
|
# named it. The same reading as that tool's, so the two agree.
|
|
145
150
|
var classes_used: Array[Dictionary] = []
|
|
146
151
|
if _dependencies == null:
|
|
147
|
-
_dependencies =
|
|
152
|
+
_dependencies = GdhDependencies.new(_log)
|
|
148
153
|
var declared_by: Dictionary = _dependencies.classes_declared()
|
|
149
|
-
for used: String in
|
|
154
|
+
for used: String in GdhDependencies.classes_used_in(content, declared_by):
|
|
150
155
|
var declaring: String = str(declared_by[used])
|
|
151
156
|
if declaring == full_script_path:
|
|
152
157
|
continue
|
|
@@ -165,7 +170,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
165
170
|
if value.begins_with("*"):
|
|
166
171
|
global_autoloads[setting.trim_prefix("autoload/")] = value.trim_prefix("*")
|
|
167
172
|
var autoloads_used: Array[Dictionary] = []
|
|
168
|
-
for used: String in
|
|
173
|
+
for used: String in GdhDependencies.classes_used_in(content, global_autoloads):
|
|
169
174
|
if str(global_autoloads[used]) != full_script_path:
|
|
170
175
|
autoloads_used.append({"name": used, "path": str(global_autoloads[used])})
|
|
171
176
|
|
|
@@ -183,9 +188,9 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
|
|
|
183
188
|
"dependencies": dependencies,
|
|
184
189
|
"classes_used": classes_used,
|
|
185
190
|
"autoloads_used": autoloads_used,
|
|
186
|
-
"line_count": lines.size()
|
|
191
|
+
"line_count": lines.size() - 1 if content.is_empty() or content.ends_with("\n") else lines.size()
|
|
187
192
|
}
|
|
188
|
-
if
|
|
193
|
+
if GdhRead.as_bool(params.get("include_inherited", false)):
|
|
189
194
|
_add_inherited(answer, [full_script_path])
|
|
190
195
|
return answer
|
|
191
196
|
|
|
@@ -202,7 +207,7 @@ func _mark_virtuals(functions: Array[Dictionary], extends_name: String, from_pat
|
|
|
202
207
|
var virtuals: Dictionary = {}
|
|
203
208
|
if not native.is_empty():
|
|
204
209
|
for m: Dictionary in ClassDB.class_get_method_list(native, false):
|
|
205
|
-
var flags: int =
|
|
210
|
+
var flags: int = GdhRead.as_int(m.get("flags", 0))
|
|
206
211
|
if flags & METHOD_FLAG_VIRTUAL:
|
|
207
212
|
virtuals[str(m.get("name", ""))] = true
|
|
208
213
|
for one: Dictionary in functions:
|
|
@@ -366,7 +371,7 @@ func _parse_variable(line: String, line_num: int) -> Dictionary:
|
|
|
366
371
|
|
|
367
372
|
# From the annotations themselves rather than from where they sit. `@onready @export var x` put
|
|
368
373
|
# the export second and so read as not exported, and a hint with a space in it was cut at it.
|
|
369
|
-
for one: String in
|
|
374
|
+
for one: String in GdhPatterns.annotations_on(line):
|
|
370
375
|
if one.begins_with("export"):
|
|
371
376
|
is_export = true
|
|
372
377
|
var after: String = one.substr(6)
|
|
@@ -457,7 +462,7 @@ func _parse_function(line: String, line_num: int, annotated: String = "") -> Dic
|
|
|
457
462
|
"is_static": is_static,
|
|
458
463
|
# Said rather than left to be inferred: an abstract method has no body, so a caller that
|
|
459
464
|
# saw only the declaration would otherwise take it for one whose body it failed to read.
|
|
460
|
-
"is_abstract": "abstract" in
|
|
465
|
+
"is_abstract": "abstract" in GdhPatterns.annotations_on(annotated),
|
|
461
466
|
"line": line_num
|
|
462
467
|
}
|
|
463
468
|
|
|
@@ -624,7 +629,7 @@ static func _triple_quotes(line: String) -> int:
|
|
|
624
629
|
|
|
625
630
|
func _extract_dependencies(line: String) -> Array[String]:
|
|
626
631
|
var deps: Array[String] = []
|
|
627
|
-
var regex: RegEx =
|
|
632
|
+
var regex: RegEx = GdhPatterns.compiled("(?:preload|load)\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)")
|
|
628
633
|
|
|
629
634
|
for m: RegExMatch in regex.search_all(line):
|
|
630
635
|
deps.append(m.get_string(1))
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
3
|
+
const GdhFileWalk = preload("file_walk.gd")
|
|
4
|
+
const GdhRead = preload("reading.gd")
|
|
5
|
+
const GdhLog = preload("logger.gd")
|
|
6
|
+
const GdhPatterns = preload("patterns.gd")
|
|
7
|
+
const GdhScriptCheck = preload("script_check.gd")
|
|
6
8
|
|
|
7
9
|
const MODIFICATION_TYPES: Array[String] = ["add_constant", "add_variable", "add_signal", "add_function"]
|
|
8
10
|
const BLANKS_AROUND_FUNCTIONS: int = 2
|
|
9
11
|
|
|
10
|
-
var _log:
|
|
12
|
+
var _log: GdhLog
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
func _init(p_log:
|
|
15
|
+
func _init(p_log: GdhLog) -> void:
|
|
14
16
|
_log = p_log
|
|
15
17
|
|
|
16
18
|
|
|
@@ -56,6 +58,13 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
56
58
|
"Script file already exists: " + full_script_path + ". script_edit modify adds to it."
|
|
57
59
|
)
|
|
58
60
|
|
|
61
|
+
# Content is left to the parse below: two scripts that name each other are written one at a time,
|
|
62
|
+
# and the first does not compile until the second exists.
|
|
63
|
+
if content.is_empty():
|
|
64
|
+
var header: String = _header_refusal(cls_name_param, extends_class)
|
|
65
|
+
if not header.is_empty():
|
|
66
|
+
return _log.failure(header + " Nothing was written.")
|
|
67
|
+
|
|
59
68
|
var dir: DirAccess = DirAccess.open("res://")
|
|
60
69
|
var script_dir: String = full_script_path.get_base_dir()
|
|
61
70
|
if script_dir != "res://" and not dir.dir_exists(script_dir.substr(6)):
|
|
@@ -111,7 +120,11 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
111
120
|
full_script_path = "res://" + full_script_path
|
|
112
121
|
|
|
113
122
|
if not FileAccess.file_exists(full_script_path):
|
|
114
|
-
return _log.failure(
|
|
123
|
+
return _log.failure(
|
|
124
|
+
GdhFileWalk.new().with_nearest_files(
|
|
125
|
+
"Script file does not exist: " + full_script_path, full_script_path
|
|
126
|
+
)
|
|
127
|
+
)
|
|
115
128
|
|
|
116
129
|
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.READ)
|
|
117
130
|
if not file:
|
|
@@ -122,6 +135,8 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
122
135
|
|
|
123
136
|
var lines: Array[String] = []
|
|
124
137
|
lines.assign(original_content.split("\n"))
|
|
138
|
+
var before: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
139
|
+
var parsed_before: bool = _parses(before)
|
|
125
140
|
|
|
126
141
|
# All of them checked before any is made, so a call is made whole or not at all: one skipped for a
|
|
127
142
|
# missing name used to leave the rest written and the answer saying success over an empty list.
|
|
@@ -166,16 +181,99 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
166
181
|
if not rewrote:
|
|
167
182
|
return _log.failure("Failed to write to script file: " + full_script_path)
|
|
168
183
|
|
|
184
|
+
var caught: GdhScriptCheck.GdhCaught = GdhScriptCheck.GdhCaught.new()
|
|
185
|
+
OS.add_logger(caught)
|
|
169
186
|
var made: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
187
|
+
OS.remove_logger(caught)
|
|
188
|
+
var parses: bool = _parses(made)
|
|
189
|
+
# The names checked above are the script's own, and a member the base class declares, or a body
|
|
190
|
+
# that does not compile, is found only by the engine; a script left broken by an addition breaks
|
|
191
|
+
# every scene that uses it, so it goes back as it was. One already broken is written, as the
|
|
192
|
+
# addition can be what mends it.
|
|
193
|
+
if parsed_before and not parses:
|
|
194
|
+
file = FileAccess.open(full_script_path, FileAccess.WRITE)
|
|
195
|
+
var restored: bool = file != null and file.store_string(original_content)
|
|
196
|
+
if file != null:
|
|
197
|
+
file.close()
|
|
198
|
+
if not restored:
|
|
199
|
+
return _log.failure(
|
|
200
|
+
"%s would not parse with these additions, and could not be put back as it was" % script_path
|
|
201
|
+
)
|
|
202
|
+
return _log.failure(
|
|
203
|
+
(
|
|
204
|
+
"Nothing was changed: %s parses as it is and would not with these additions: %s"
|
|
205
|
+
% [script_path, _reasons(caught.raised, full_script_path)]
|
|
206
|
+
)
|
|
207
|
+
)
|
|
170
208
|
return {
|
|
171
209
|
"success": true,
|
|
172
210
|
"script_path": script_path,
|
|
173
211
|
"modifications_applied": modifications_applied,
|
|
174
212
|
"total_modifications": modifications_applied.size(),
|
|
175
|
-
"parses":
|
|
213
|
+
"parses": parses,
|
|
176
214
|
}
|
|
177
215
|
|
|
178
216
|
|
|
217
|
+
# The engine's errors in [param raised] for [param path], each with its line, as one sentence part.
|
|
218
|
+
static func _reasons(raised: Array[Dictionary], path: String) -> String:
|
|
219
|
+
var reasons: PackedStringArray = []
|
|
220
|
+
for error: Dictionary in raised:
|
|
221
|
+
var reason: String = "line %d, %s" % [GdhRead.as_int(error["line"]), str(error["message"])]
|
|
222
|
+
if str(error["path"]) == path and not reason in reasons:
|
|
223
|
+
var _added: bool = reasons.append(reason)
|
|
224
|
+
if reasons.is_empty():
|
|
225
|
+
return "the engine would not load it as a script."
|
|
226
|
+
var last: String = reasons[-1]
|
|
227
|
+
return "; ".join(reasons) + ("" if last.ends_with(".") or last.ends_with(".)") else ".")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# Why a script with [param named] as its class_name and extending [param base] cannot be written,
|
|
231
|
+
# or "". A class_name another script holds does not compile ("hides a global script class"), and nor
|
|
232
|
+
# does a base no class answers to; both were written and answered as made. Classes are read off the
|
|
233
|
+
# scripts as well as the class list, which a fresh clone has not written yet.
|
|
234
|
+
static func _header_refusal(named: String, base: String) -> String:
|
|
235
|
+
var declared: Dictionary = _declared_classes()
|
|
236
|
+
if not named.is_empty():
|
|
237
|
+
if ClassDB.class_exists(named):
|
|
238
|
+
return "class_name %s is the name of an engine class, which a script cannot take." % named
|
|
239
|
+
if declared.has(named):
|
|
240
|
+
return (
|
|
241
|
+
(
|
|
242
|
+
"class_name %s is taken: %s declares it, and a second script of that name does not "
|
|
243
|
+
+ "compile. Give this one another class_name, or script_edit rename the first."
|
|
244
|
+
)
|
|
245
|
+
% [named, declared[named]]
|
|
246
|
+
)
|
|
247
|
+
if base.begins_with('"') or base.begins_with("'") or "." in base:
|
|
248
|
+
return ""
|
|
249
|
+
if ClassDB.class_exists(base) or declared.has(base):
|
|
250
|
+
return ""
|
|
251
|
+
var said: String = (
|
|
252
|
+
"extends names %s, which is neither an engine class nor a class_name this project declares" % base
|
|
253
|
+
)
|
|
254
|
+
var close: PackedStringArray = GdhRead.nearest(base, PackedStringArray(declared.keys()))
|
|
255
|
+
if close.is_empty():
|
|
256
|
+
close = GdhRead.nearest(base, ClassDB.get_class_list())
|
|
257
|
+
if close.is_empty():
|
|
258
|
+
return said + "."
|
|
259
|
+
return said + "; the nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# The class_name each script of the project declares, by name, with its path.
|
|
263
|
+
static func _declared_classes() -> Dictionary:
|
|
264
|
+
var declared: Dictionary = {}
|
|
265
|
+
for entry: Dictionary in ProjectSettings.get_global_class_list():
|
|
266
|
+
var path: String = str(entry.get("path", ""))
|
|
267
|
+
if FileAccess.file_exists(path):
|
|
268
|
+
declared[str(entry.get("class", ""))] = path
|
|
269
|
+
for path: String in GdhFileWalk.new().find_files("res://", ".gd"):
|
|
270
|
+
for line: String in FileAccess.get_file_as_string(path).split("\n"):
|
|
271
|
+
var found: RegExMatch = GdhPatterns.declared_class(GdhPatterns.without_annotations(line))
|
|
272
|
+
if found != null and not declared.has(found.get_string(1)):
|
|
273
|
+
declared[found.get_string(1)] = path
|
|
274
|
+
return declared
|
|
275
|
+
|
|
276
|
+
|
|
179
277
|
# Every reason [param modifications] cannot be made as asked, each naming the entry by position.
|
|
180
278
|
#
|
|
181
279
|
# A name the script already declares is one of them, as is one the call adds twice. GDScript gives a
|
|
@@ -231,14 +329,14 @@ static func _modification_problems(modifications: Array, declared: Dictionary) -
|
|
|
231
329
|
# one: variables, constants, signals, functions, enums and inner classes, which share one namespace.
|
|
232
330
|
static func _declared(lines: Array[String]) -> Dictionary:
|
|
233
331
|
var found: Dictionary = {}
|
|
234
|
-
var declaration: RegEx =
|
|
332
|
+
var declaration: RegEx = GdhPatterns.compiled(
|
|
235
333
|
"^(?:static\\s+)?(var|const|signal|func|enum|class)\\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
236
334
|
)
|
|
237
335
|
for i: int in range(lines.size()):
|
|
238
336
|
var raw: String = lines[i]
|
|
239
337
|
if raw.is_empty() or raw[0] == "\t" or raw[0] == " ":
|
|
240
338
|
continue
|
|
241
|
-
var matched: RegExMatch = declaration.search(
|
|
339
|
+
var matched: RegExMatch = declaration.search(GdhPatterns.without_annotations(raw))
|
|
242
340
|
if matched != null and not found.has(matched.get_string(2)):
|
|
243
341
|
found[matched.get_string(2)] = {"kind": matched.get_string(1), "line": i + 1}
|
|
244
342
|
return found
|
|
@@ -286,7 +384,7 @@ func _add_constant(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
|
|
|
286
384
|
var const_line: String = "const %s: %s = %s" % [str(mod.get("name", "")), declared_type, value]
|
|
287
385
|
var after: int = 0
|
|
288
386
|
for i: int in range(lines.size()):
|
|
289
|
-
var line: String =
|
|
387
|
+
var line: String = GdhPatterns.without_annotations(lines[i])
|
|
290
388
|
if (
|
|
291
389
|
line.begins_with("extends ")
|
|
292
390
|
or line.begins_with("class_name ")
|
|
@@ -310,9 +408,9 @@ func _add_variable(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
|
|
|
310
408
|
var var_name: String = str(mod.get("name", ""))
|
|
311
409
|
var var_type: String = str(mod.get("varType", ""))
|
|
312
410
|
var default_value: String = str(mod.get("defaultValue", ""))
|
|
313
|
-
var is_export: bool =
|
|
411
|
+
var is_export: bool = GdhRead.as_bool(mod.get("isExport", false))
|
|
314
412
|
var export_hint: String = str(mod.get("exportHint", ""))
|
|
315
|
-
var is_onready: bool =
|
|
413
|
+
var is_onready: bool = GdhRead.as_bool(mod.get("isOnready", false))
|
|
316
414
|
|
|
317
415
|
var var_line: String = ""
|
|
318
416
|
|
|
@@ -341,6 +439,11 @@ func _add_variable(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
|
|
|
341
439
|
var_line += ": Variant"
|
|
342
440
|
|
|
343
441
|
var insert_line: int = _variable_insertion_point(lines)
|
|
442
|
+
# The first variable below the header or the signals is set apart from them by a blank line, as
|
|
443
|
+
# the style guide and gdformat lay a script out.
|
|
444
|
+
var above: String = lines[insert_line - 1].strip_edges() if insert_line > 0 else ""
|
|
445
|
+
if not above.is_empty() and not GdhPatterns.without_annotations(above).begins_with("var "):
|
|
446
|
+
return insert_line + 1 if _insert(lines, insert_line, ["", var_line], placed) else -1
|
|
344
447
|
return insert_line if _insert(lines, insert_line, [var_line], placed) else -1
|
|
345
448
|
|
|
346
449
|
|
|
@@ -433,7 +536,7 @@ func _variable_insertion_point(lines: Array[String]) -> int:
|
|
|
433
536
|
# takes the new line above the `class_name`, where it does not parse; one that declares
|
|
434
537
|
# `extends` first takes it between the two, which does not parse either. Stripping also
|
|
435
538
|
# turns `@export var x` into `var x`, which is why the annotation names are gone below.
|
|
436
|
-
var line: String =
|
|
539
|
+
var line: String = GdhPatterns.without_annotations(lines[i])
|
|
437
540
|
if line.begins_with("extends ") or line.begins_with("class_name "):
|
|
438
541
|
after_header = i + 1
|
|
439
542
|
elif line.begins_with("signal "):
|
|
@@ -443,7 +546,7 @@ func _variable_insertion_point(lines: Array[String]) -> int:
|
|
|
443
546
|
break
|
|
444
547
|
|
|
445
548
|
for i: int in range(after_header, before_func):
|
|
446
|
-
if
|
|
549
|
+
if GdhPatterns.without_annotations(lines[i]).begins_with("var "):
|
|
447
550
|
after_header = i + 1
|
|
448
551
|
|
|
449
552
|
return after_header
|
|
@@ -455,7 +558,7 @@ func _signal_insertion_point(lines: Array[String]) -> int:
|
|
|
455
558
|
for i: int in range(lines.size()):
|
|
456
559
|
# As above: the annotations come off before the line is read, so an abstract class has a
|
|
457
560
|
# header to find and `@export var x` reads as the var it is.
|
|
458
|
-
var line: String =
|
|
561
|
+
var line: String = GdhPatterns.without_annotations(lines[i])
|
|
459
562
|
if line.begins_with("extends ") or line.begins_with("class_name "):
|
|
460
563
|
after_header = i + 1
|
|
461
564
|
elif line.begins_with("signal "):
|
|
@@ -485,7 +588,7 @@ func _line_after_function(lines: Array[String], prefix: String) -> int:
|
|
|
485
588
|
# ends of this. An annotated `_ready` was never found, so `after_ready` placed the new
|
|
486
589
|
# function at the end of the file, and an annotated function after `_ready` was not the
|
|
487
590
|
# boundary it is, so the new one went in after whichever later function was plain.
|
|
488
|
-
var line: String =
|
|
591
|
+
var line: String = GdhPatterns.without_annotations(lines[i])
|
|
489
592
|
if line.begins_with(prefix):
|
|
490
593
|
inside = true
|
|
491
594
|
elif inside and (line.begins_with("func ") or line.begins_with("static func ")):
|
|
@@ -502,7 +605,9 @@ static func _start_of_declaration(lines: Array[String], at: int) -> int:
|
|
|
502
605
|
var start: int = at
|
|
503
606
|
while start > 0 and _indentation(lines[start - 1]) == indent:
|
|
504
607
|
var above: String = lines[start - 1].strip_edges()
|
|
505
|
-
var annotation_only: bool =
|
|
608
|
+
var annotation_only: bool = (
|
|
609
|
+
above.begins_with("@") and GdhPatterns.without_annotations(above).is_empty()
|
|
610
|
+
)
|
|
506
611
|
if not above.begins_with("#") and not annotation_only:
|
|
507
612
|
break
|
|
508
613
|
start -= 1
|