gdharness 1.3.3 → 1.3.4

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.
Files changed (43) hide show
  1. package/build/cli.js +105 -74
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  7. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  8. package/build/godot/addons/gdharness_editor/property_values.gd +195 -221
  9. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  10. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  11. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
  13. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  14. package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
  15. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
  16. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  17. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  18. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  19. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
  20. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
  21. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  22. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  24. package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
  25. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  26. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  27. package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
  28. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  29. package/build/godot/operations/animation_reads.gd +3 -2
  30. package/build/godot/operations/declared_values.gd +157 -4
  31. package/build/godot/operations/dependencies.gd +14 -39
  32. package/build/godot/operations/editor_tools.gd +4 -3
  33. package/build/godot/operations/file_reads.gd +15 -3
  34. package/build/godot/operations/gdscript_analysis.gd +229 -75
  35. package/build/godot/operations/gdscript_authoring.gd +142 -34
  36. package/build/godot/operations/import_pipeline.gd +17 -6
  37. package/build/godot/operations/logger.gd +7 -0
  38. package/build/godot/operations/property_paths.gd +225 -0
  39. package/build/godot/operations/property_values.gd +195 -221
  40. package/build/godot/operations/reading.gd +15 -0
  41. package/build/godot/operations/same_values.gd +177 -0
  42. package/build/godot/operations/serialisation.gd +75 -3
  43. package/package.json +1 -1
@@ -9,6 +9,39 @@ const GdhLog = preload("logger.gd")
9
9
  var _log: GdhLog
10
10
  ## Kept for the call, so the project's class names are read once however many bases are walked.
11
11
  var _dependencies: GdhDependencies = null
12
+ ## The script being read, which an inner class's base is resolved against.
13
+ var _reading: String = ""
14
+
15
+
16
+ ## What one class body declares, the script's own or an inner class's.
17
+ class GdhMembers:
18
+ var declared_class_name: Variant = null
19
+ var extends_name: String = "RefCounted"
20
+ var signals: Array[Dictionary] = []
21
+ var variables: Array[Dictionary] = []
22
+ var functions: Array[Dictionary] = []
23
+ var constants: Array[Dictionary] = []
24
+ var enums: Array[Dictionary] = []
25
+ var inner_classes: Array[Dictionary] = []
26
+ ## The index of the last line the body holds.
27
+ var last: int = -1
28
+ ## An inner class's name and the line its `class` is on.
29
+ var inner_name: String = ""
30
+ var line: int = 0
31
+
32
+ ## The inner class as its entry under inner_classes, in the fields a script's own are in.
33
+ func as_inner() -> Dictionary:
34
+ return {
35
+ "name": inner_name,
36
+ "line": line,
37
+ "extends": extends_name,
38
+ "signals": signals,
39
+ "variables": variables,
40
+ "functions": functions,
41
+ "constants": constants,
42
+ "enums": enums,
43
+ "inner_classes": inner_classes,
44
+ }
12
45
 
13
46
 
14
47
  func _init(p_log: GdhLog) -> void:
@@ -41,19 +74,77 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
41
74
 
42
75
  var lines: PackedStringArray = content.split("\n")
43
76
 
44
- var declared_class_name: Variant = null
45
- var extends_name: String = "RefCounted"
46
- var signals: Array[Dictionary] = []
47
- var variables: Array[Dictionary] = []
48
- var functions: Array[Dictionary] = []
49
- var constants: Array[Dictionary] = []
50
- var enums: Array[Dictionary] = []
51
- var inner_classes: Array[String] = []
52
77
  var dependencies: Array[String] = []
78
+ _reading = full_script_path
79
+ var own: GdhMembers = _members(lines, 0, "", dependencies)
80
+ var extends_name: String = own.extends_name
81
+ var functions: Array[Dictionary] = own.functions
53
82
 
83
+ # After the walk, because it needs the base the walk reads, and a file that declared one after
84
+ # its first function would otherwise be judged against the default.
85
+ _mark_virtuals(functions, extends_name, full_script_path)
86
+
87
+ # A script typed by its class_name is depended on as surely as one preloaded, and the answer
88
+ # listed only the preloads: a `var stats: Stats` left stats.gd out, while project_dependencies
89
+ # named it. The same reading as that tool's, so the two agree.
90
+ var classes_used: Array[Dictionary] = []
91
+ if _dependencies == null:
92
+ _dependencies = GdhDependencies.new(_log)
93
+ var declared_by: Dictionary = _dependencies.classes_declared()
94
+ for used: String in GdhDependencies.classes_used_in(content, declared_by):
95
+ var declaring: String = str(declared_by[used])
96
+ if declaring == full_script_path:
97
+ continue
98
+ classes_used.append({"name": used, "path": declaring})
99
+ if not declaring in dependencies:
100
+ dependencies.append(declaring)
101
+
102
+ # An autoload is named the way a class is, and its script is one this one needs to compile, but
103
+ # the project loads it rather than this script, so it is answered apart from dependencies.
104
+ var global_autoloads: Dictionary = {}
105
+ for property: Dictionary in ProjectSettings.get_property_list():
106
+ var setting: String = str(property.get("name", ""))
107
+ var value: String = (
108
+ str(ProjectSettings.get_setting(setting)) if setting.begins_with("autoload/") else ""
109
+ )
110
+ if value.begins_with("*"):
111
+ global_autoloads[setting.trim_prefix("autoload/")] = value.trim_prefix("*")
112
+ var autoloads_used: Array[Dictionary] = []
113
+ for used: String in GdhDependencies.classes_used_in(content, global_autoloads):
114
+ if str(global_autoloads[used]) != full_script_path:
115
+ autoloads_used.append({"name": used, "path": str(global_autoloads[used])})
116
+
117
+ var answer: Dictionary = {
118
+ "path": script_path,
119
+ "full_path": full_script_path,
120
+ "class_name": own.declared_class_name,
121
+ "extends": extends_name,
122
+ "signals": own.signals,
123
+ "variables": own.variables,
124
+ "functions": functions,
125
+ "constants": own.constants,
126
+ "enums": own.enums,
127
+ "inner_classes": own.inner_classes,
128
+ "dependencies": dependencies,
129
+ "classes_used": classes_used,
130
+ "autoloads_used": autoloads_used,
131
+ "line_count": lines.size() - 1 if content.is_empty() or content.ends_with("\n") else lines.size()
132
+ }
133
+ if GdhRead.as_bool(params.get("include_inherited", false)):
134
+ _add_inherited(answer, [full_script_path])
135
+ return answer
136
+
137
+
138
+ ## What one class body declares: the script's own at the left margin, or an inner class's at the
139
+ ## indent [param margin] its body is written at, read from line [param first] to the first line
140
+ ## less indented than that. [param dependencies] collects what every line loads, bodies included.
141
+ func _members(
142
+ lines: PackedStringArray, first: int, margin: String, dependencies: Array[String]
143
+ ) -> GdhMembers:
144
+ var own: GdhMembers = GdhMembers.new()
54
145
  var in_multiline_string: bool = false
55
146
 
56
- var i: int = -1
147
+ var i: int = first - 1
57
148
  while i + 1 < lines.size():
58
149
  i += 1
59
150
  # The comment off first, so nothing downstream has to know about them. It was reaching the
@@ -72,6 +163,12 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
72
163
  if stripped.is_empty():
73
164
  continue
74
165
 
166
+ # A line back at a shallower indent closes an inner class's body, and is the outer body's
167
+ # to read.
168
+ if not raw.begins_with(margin):
169
+ own.last = i - 1
170
+ return own
171
+
75
172
  # An odd count opens a string that runs on; an even one is a string that closed on the line
76
173
  # it opened on. Toggling on any count lost `const HELP := """Usage"""`, and everything after
77
174
  # it up to the next triple quote, with the state inverted from there to the end of the file.
@@ -79,10 +176,11 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
79
176
  if _triple_quotes(stripped) % 2 == 1:
80
177
  in_multiline_string = true
81
178
 
82
- # Only a line at the left margin declares one of the script's own members. Indented, it is a
179
+ # Only a line at this body's own margin declares one of its members. Deeper, it is a
83
180
  # function's local or an inner class's member, and each was answered as the script's own:
84
181
  # every `var` in every body, and an inner class's `extends` taken for the script's base.
85
- if raw.begins_with(" ") or raw.begins_with("\t"):
182
+ var indented: String = raw.substr(margin.length())
183
+ if indented.begins_with(" ") or indented.begins_with("\t"):
86
184
  _collect_dependencies(stripped, dependencies)
87
185
  continue
88
186
 
@@ -113,86 +211,75 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
113
211
  header = GdhPatterns.without_annotations(stripped)
114
212
  var declaration: RegExMatch = GdhPatterns.declared_class(header)
115
213
  if declaration != null:
116
- declared_class_name = declaration.get_string(1)
214
+ own.declared_class_name = declaration.get_string(1)
117
215
  # `class_name X extends Y` is also one line, and taking the rest of it as the name
118
216
  # reported `Blade extends Node2D` as the class while `extends` kept its default of
119
217
  # `RefCounted`: the name unusable and the base flatly wrong.
120
218
  if not declaration.get_string(2).is_empty():
121
- extends_name = declaration.get_string(2)
219
+ own.extends_name = declaration.get_string(2)
122
220
  elif header.begins_with("extends "):
123
- extends_name = header.substr(8).strip_edges()
221
+ own.extends_name = header.substr(8).strip_edges()
124
222
  elif header.begins_with("signal "):
125
223
  # `at`, not `i`: a wrapped declaration starts where it starts, and `i` is now sitting on
126
224
  # the line that closed it.
127
- signals.append(_parse_signal(header, at + 1))
225
+ own.signals.append(_parse_signal(header, at + 1))
128
226
  elif header.begins_with("const "):
129
- constants.append(_parse_constant(header, at + 1))
227
+ own.constants.append(_parse_constant(header, at + 1))
130
228
  elif header.begins_with("enum "):
131
- enums.append(_parse_enum(header, at + 1))
132
- elif header.begins_with("var "):
229
+ own.enums.append(_parse_enum(header, at + 1))
230
+ elif header.begins_with("var ") or header.begins_with("static var "):
133
231
  # The whole line here, not the header: which annotations a variable carries is the
134
232
  # answer rather than noise in front of it, and `@export_range(0, 1)` is the hint.
135
- variables.append(_parse_variable(stripped, at + 1))
233
+ own.variables.append(_parse_variable(stripped, at + 1))
136
234
  elif header.begins_with("func ") or header.begins_with("static func "):
137
- functions.append(_parse_function(header, at + 1, stripped))
235
+ own.functions.append(_parse_function(header, at + 1, stripped))
138
236
  elif header.begins_with("class "):
139
- inner_classes.append(header.substr(6).split(":")[0].split(" ")[0].strip_edges())
237
+ _collect_dependencies(stripped, dependencies)
238
+ var inner: GdhMembers = _inner_class(lines, i, margin, header, dependencies)
239
+ own.inner_classes.append(inner.as_inner())
240
+ i = inner.last
241
+ continue
140
242
 
141
243
  _collect_dependencies(stripped, dependencies)
142
244
 
143
- # After the loop, because it needs the base the loop reads, and a file that declared one after
144
- # its first function would otherwise be judged against the default.
145
- _mark_virtuals(functions, extends_name, full_script_path)
146
-
147
- # A script typed by its class_name is depended on as surely as one preloaded, and the answer
148
- # listed only the preloads: a `var stats: Stats` left stats.gd out, while project_dependencies
149
- # named it. The same reading as that tool's, so the two agree.
150
- var classes_used: Array[Dictionary] = []
151
- if _dependencies == null:
152
- _dependencies = GdhDependencies.new(_log)
153
- var declared_by: Dictionary = _dependencies.classes_declared()
154
- for used: String in GdhDependencies.classes_used_in(content, declared_by):
155
- var declaring: String = str(declared_by[used])
156
- if declaring == full_script_path:
245
+ own.last = lines.size() - 1
246
+ return own
247
+
248
+
249
+ ## The inner class whose `class` line is [param at], [param header] with its annotations and
250
+ ## comment off, read with the members its body declares. Answered as a name alone, its members
251
+ ## were in no answer at all.
252
+ func _inner_class(
253
+ lines: PackedStringArray, at: int, margin: String, header: String, dependencies: Array[String]
254
+ ) -> GdhMembers:
255
+ var signature: String = _up_to(header.substr(6), ":").strip_edges()
256
+ var based: String = ""
257
+ if " extends " in signature:
258
+ based = signature.get_slice(" extends ", 1).strip_edges()
259
+ # The body is at the indent of its first line, whatever this file indents with.
260
+ var body_margin: String = ""
261
+ for next: int in range(at + 1, lines.size()):
262
+ if GdhPatterns.without_comment(lines[next]).is_empty():
157
263
  continue
158
- classes_used.append({"name": used, "path": declaring})
159
- if not declaring in dependencies:
160
- dependencies.append(declaring)
161
-
162
- # An autoload is named the way a class is, and its script is one this one needs to compile, but
163
- # the project loads it rather than this script, so it is answered apart from dependencies.
164
- var global_autoloads: Dictionary = {}
165
- for property: Dictionary in ProjectSettings.get_property_list():
166
- var setting: String = str(property.get("name", ""))
167
- var value: String = (
168
- str(ProjectSettings.get_setting(setting)) if setting.begins_with("autoload/") else ""
169
- )
170
- if value.begins_with("*"):
171
- global_autoloads[setting.trim_prefix("autoload/")] = value.trim_prefix("*")
172
- var autoloads_used: Array[Dictionary] = []
173
- for used: String in GdhDependencies.classes_used_in(content, global_autoloads):
174
- if str(global_autoloads[used]) != full_script_path:
175
- autoloads_used.append({"name": used, "path": str(global_autoloads[used])})
176
-
177
- var answer: Dictionary = {
178
- "path": script_path,
179
- "full_path": full_script_path,
180
- "class_name": declared_class_name,
181
- "extends": extends_name,
182
- "signals": signals,
183
- "variables": variables,
184
- "functions": functions,
185
- "constants": constants,
186
- "enums": enums,
187
- "inner_classes": inner_classes,
188
- "dependencies": dependencies,
189
- "classes_used": classes_used,
190
- "autoloads_used": autoloads_used,
191
- "line_count": lines.size() - 1 if content.is_empty() or content.ends_with("\n") else lines.size()
192
- }
193
- if GdhRead.as_bool(params.get("include_inherited", false)):
194
- _add_inherited(answer, [full_script_path])
195
- return answer
264
+ var indent: String = _indentation(lines[next])
265
+ if indent.length() > margin.length() and lines[next].begins_with(margin):
266
+ body_margin = indent
267
+ break
268
+ var body: GdhMembers = (
269
+ GdhMembers.new() if body_margin.is_empty() else _members(lines, at + 1, body_margin, dependencies)
270
+ )
271
+ if body_margin.is_empty():
272
+ body.last = at
273
+ if not based.is_empty():
274
+ body.extends_name = based
275
+ body.inner_name = signature.get_slice(" ", 0)
276
+ body.line = at + 1
277
+ _mark_virtuals(body.functions, body.extends_name, _reading)
278
+ return body
279
+
280
+
281
+ static func _indentation(line: String) -> String:
282
+ return line.substr(0, line.length() - line.strip_edges(true, false).length())
196
283
 
197
284
 
198
285
  # Which of these functions the engine will call, asked of the engine.
@@ -340,7 +427,67 @@ func _parse_constant(line: String, line_num: int) -> Dictionary:
340
427
  else:
341
428
  name = const_text
342
429
 
343
- return {"name": name, "value": value, "type": type_hint, "line": line_num}
430
+ var constant: Dictionary = {"name": name, "value": value, "type": type_hint, "line": line_num}
431
+ # A constant has its value's type whether or not it is written with :=.
432
+ if type_hint.is_empty():
433
+ _infer(constant, value)
434
+ return constant
435
+
436
+
437
+ ## Gives [param declared] the type GDScript infers from [param value], marked inferred, when the
438
+ ## value is a literal or a built-in type's constructor: `const MAX_STACK := 99` read as having no
439
+ ## type though the engine holds an int. Any other expression is left without one, since only the
440
+ ## engine's analyser knows what it gives.
441
+ static func _infer(declared: Dictionary, value: String) -> void:
442
+ # A variable with a setter or getter ends its line with the colon that opens them.
443
+ var text: String = value.strip_edges().trim_suffix(":").strip_edges()
444
+ var type: String = ""
445
+ if text == "true" or text == "false":
446
+ type = "bool"
447
+ elif text.is_valid_int() or text.is_valid_hex_number(true):
448
+ type = "int"
449
+ elif text.is_valid_float():
450
+ type = "float"
451
+ elif text.begins_with('&"') and text.ends_with('"'):
452
+ type = "StringName"
453
+ elif text.begins_with('^"') and text.ends_with('"'):
454
+ type = "NodePath"
455
+ elif text.length() > 1 and text[0] in ['"', "'"] and text.ends_with(text[0]):
456
+ type = "String"
457
+ elif _whole(text, "[", "]"):
458
+ type = "Array"
459
+ elif _whole(text, "{", "}"):
460
+ type = "Dictionary"
461
+ else:
462
+ var called: String = text.get_slice("(", 0).strip_edges()
463
+ if _whole(text.substr(called.length()), "(", ")") and _built_in_types().has(called):
464
+ type = called
465
+ if not type.is_empty():
466
+ declared["type"] = type
467
+ declared["inferred"] = true
468
+
469
+
470
+ ## Whether [param text] is one bracketed whole, its first [param opening] closed by its last
471
+ ## character, so `[1, 2]` is a list and `[1, 2].size()` is not.
472
+ static func _whole(text: String, opening: String, closing: String) -> bool:
473
+ if not text.begins_with(opening) or not text.ends_with(closing):
474
+ return false
475
+ var depth: int = 0
476
+ for index: int in text.length():
477
+ if text[index] == opening:
478
+ depth += 1
479
+ elif text[index] == closing:
480
+ depth -= 1
481
+ if depth == 0:
482
+ return index == text.length() - 1
483
+ return false
484
+
485
+
486
+ static func _built_in_types() -> Dictionary:
487
+ var names: Dictionary = {}
488
+ for type: int in TYPE_MAX:
489
+ names[type_string(type)] = true
490
+ return names
344
491
 
345
492
 
346
493
  func _parse_enum(line: String, line_num: int) -> Dictionary:
@@ -412,15 +559,22 @@ func _parse_variable(line: String, line_num: int) -> Dictionary:
412
559
  if "=" in var_text:
413
560
  stated_default = default_value
414
561
 
415
- return {
562
+ var variable: Dictionary = {
416
563
  "name": name,
417
564
  "type": type_hint,
418
565
  "default_value": stated_default,
419
566
  "is_export": is_export,
420
567
  "export_hint": export_hint,
421
568
  "is_onready": is_onready,
569
+ # One per class rather than per instance, which a caller reading a counter needs to know.
570
+ "is_static": GdhPatterns.without_annotations(line).begins_with("static "),
422
571
  "line": line_num
423
572
  }
573
+ # Only := types a variable by its value; one written with = alone holds any type.
574
+ var walrus: bool = "=" in var_text and var_text.split("=", true, 1)[0].strip_edges().ends_with(":")
575
+ if walrus and type_hint.is_empty():
576
+ _infer(variable, default_value)
577
+ return variable
424
578
 
425
579
 
426
580
  func _parse_function(line: String, line_num: int, annotated: String = "") -> Dictionary: