gdharness 1.2.34 → 1.2.36

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.
@@ -0,0 +1,232 @@
1
+ extends RefCounted
2
+
3
+ ## export_presets.cfg, read and written in the format the editor's Project > Export saves it in.
4
+
5
+ const Read = preload("reading.gd")
6
+ const Log = preload("logger.gd")
7
+
8
+ const PRESETS_FILE: String = "res://export_presets.cfg"
9
+ ## Where the editor names the one preset of each platform it runs on a device.
10
+ const RUNNABLE_SECTION: String = "runnable_presets"
11
+ ## The platforms the editor's exporter offers, by the name a preset gives its platform, each with
12
+ ## how the names of its files in the export templates directory begin.
13
+ const PLATFORMS: Dictionary[String, String] = {
14
+ "Windows Desktop": "windows_",
15
+ "Linux": "linux_",
16
+ "macOS": "macos",
17
+ "Android": "android_",
18
+ "iOS": "ios",
19
+ "Web": "web_",
20
+ }
21
+
22
+ var _log: Log
23
+
24
+
25
+ func _init(p_log: Log) -> void:
26
+ _log = p_log
27
+
28
+
29
+ ## The names of the project's export presets, in the order export_presets.cfg holds them.
30
+ static func names() -> Array[String]:
31
+ var named: Array[String] = []
32
+ var config: ConfigFile = ConfigFile.new()
33
+ if config.load(PRESETS_FILE) != OK:
34
+ return named
35
+ var index: int = 0
36
+ while config.has_section("preset." + str(index)):
37
+ named.append(str(config.get_value("preset." + str(index), "name", "")))
38
+ index += 1
39
+ return named
40
+
41
+
42
+ func list_export_presets(_params: Dictionary) -> Dictionary:
43
+ if not FileAccess.file_exists(PRESETS_FILE):
44
+ return {
45
+ "presets": [],
46
+ "presets_file_exists": false,
47
+ "note":
48
+ (
49
+ "No export_presets.cfg found. project_export add_preset adds a preset, as the editor's "
50
+ + "Project > Export does."
51
+ )
52
+ }
53
+ var config: ConfigFile = ConfigFile.new()
54
+ var err: Error = config.load(PRESETS_FILE)
55
+ if err != OK:
56
+ return _log.failure("Failed to parse export_presets.cfg: " + error_string(err))
57
+
58
+ var templates: String = _templates_dir()
59
+ var presets: Array[Dictionary] = []
60
+ var index: int = 0
61
+ while config.has_section("preset." + str(index)):
62
+ var section: String = "preset." + str(index)
63
+ var platform: String = str(config.get_value(section, "platform", "Unknown"))
64
+ var preset: Dictionary = {
65
+ "index": index,
66
+ "name": config.get_value(section, "name", "Unknown"),
67
+ "platform": platform,
68
+ "runnable": _runnable(config, section),
69
+ "export_path": config.get_value(section, "export_path", ""),
70
+ "export_filter": config.get_value(section, "export_filter", "all_resources"),
71
+ "include_filter": config.get_value(section, "include_filter", ""),
72
+ "exclude_filter": config.get_value(section, "exclude_filter", "")
73
+ }
74
+ if config.has_section_key(section, "custom_features"):
75
+ preset["custom_features"] = config.get_value(section, "custom_features", "")
76
+ # Left out for a platform this file names and the exporter does not know, which has no
77
+ # templates to look for.
78
+ if platform in PLATFORMS:
79
+ preset["templates_installed"] = _installed(templates, platform)
80
+ presets.append(preset)
81
+ index += 1
82
+ return {"presets": presets, "presets_file_exists": true, "total_presets": index, "templates": templates}
83
+
84
+
85
+ ## Whether the preset in [param section] is the one its platform runs. The editor names that preset
86
+ ## in a section of its own, and kept a key in the preset's section only in files saved before that.
87
+ static func _runnable(config: ConfigFile, section: String) -> bool:
88
+ var platform: String = str(config.get_value(section, "platform", ""))
89
+ if config.has_section_key(RUNNABLE_SECTION, platform):
90
+ return str(config.get_value(RUNNABLE_SECTION, platform)) == str(config.get_value(section, "name", ""))
91
+ return Read.as_bool(config.get_value(section, "runnable", false))
92
+
93
+
94
+ ## Adds an export preset to export_presets.cfg, written with the keys the editor's Project > Export
95
+ ## writes for a new one. The editor's exporter fills every platform option the file leaves out with
96
+ ## its default when it loads the file, which is what a preset fresh from that dialog holds too.
97
+ func add_export_preset(params: Dictionary) -> Dictionary:
98
+ var preset_name: String = str(params.get("preset", "")).strip_edges()
99
+ var platform: String = str(params.get("platform", ""))
100
+ var export_path: String = str(params.get("output_path", "")).trim_prefix("res://")
101
+ if preset_name.is_empty():
102
+ return _log.failure("preset is required: the name the preset is exported by")
103
+ if platform not in PLATFORMS:
104
+ return _log.failure(
105
+ "Unknown platform '%s'. The platforms are %s." % [platform, ", ".join(PLATFORMS.keys())]
106
+ )
107
+ var config: ConfigFile = ConfigFile.new()
108
+ if FileAccess.file_exists(PRESETS_FILE):
109
+ var loading: Error = config.load(PRESETS_FILE)
110
+ if loading != OK:
111
+ return _log.failure("Failed to parse export_presets.cfg: " + error_string(loading))
112
+ var index: int = 0
113
+ while config.has_section("preset." + str(index)):
114
+ var held: String = "preset." + str(index)
115
+ if str(config.get_value(held, "name", "")) == preset_name:
116
+ return _log.failure(
117
+ (
118
+ (
119
+ "There is already an export preset named '%s', for %s, exporting to '%s'. "
120
+ + "project_export run exports with it; give another name to add a second."
121
+ )
122
+ % [
123
+ preset_name,
124
+ config.get_value(held, "platform", ""),
125
+ config.get_value(held, "export_path", "")
126
+ ]
127
+ )
128
+ )
129
+ index += 1
130
+ # A platform with no preset to run takes the new one, as the editor's dialog does.
131
+ var runnable: bool = not config.has_section_key(RUNNABLE_SECTION, platform)
132
+ if runnable:
133
+ config.set_value(RUNNABLE_SECTION, platform, preset_name)
134
+
135
+ var section: String = "preset." + str(index)
136
+ var values: Dictionary = {
137
+ "name": preset_name,
138
+ "platform": platform,
139
+ "dedicated_server": false,
140
+ "custom_features": "",
141
+ "export_filter": "all_resources",
142
+ "include_filter": "",
143
+ "exclude_filter": "",
144
+ "export_path": export_path,
145
+ "patches": PackedStringArray(),
146
+ "encryption_include_filters": "",
147
+ "encryption_exclude_filters": "",
148
+ "seed": 0,
149
+ "encrypt_pck": false,
150
+ "encrypt_directory": false,
151
+ "script_export_mode": 2,
152
+ }
153
+ for key: String in values:
154
+ config.set_value(section, key, values[key])
155
+ # A section with nothing in it is not written, and the editor's loader reports a missing options
156
+ # section as an error; these two are options of every platform, empty in a new preset.
157
+ config.set_value(section + ".options", "custom_template/debug", "")
158
+ config.set_value(section + ".options", "custom_template/release", "")
159
+ var saving: Error = config.save(PRESETS_FILE)
160
+ if saving != OK:
161
+ return _log.failure("Failed to save export_presets.cfg: " + error_string(saving))
162
+
163
+ var templates: String = _templates_dir()
164
+ var installed: bool = _installed(templates, platform)
165
+ var answer: Dictionary = {
166
+ "preset": preset_name,
167
+ "platform": platform,
168
+ "index": index,
169
+ "runnable": runnable,
170
+ "export_path": export_path,
171
+ "file": PRESETS_FILE,
172
+ "templates": templates,
173
+ "templates_installed": installed,
174
+ }
175
+ if not installed:
176
+ answer["templates_note"] = (
177
+ (
178
+ "No %s export templates for this engine are at %s, and an export for %s needs them. The "
179
+ + "editor installs them in Editor > Manage Export Templates, or they can be unpacked "
180
+ + "there from the export templates archive of this engine's release."
181
+ )
182
+ % [platform, templates, platform]
183
+ )
184
+ return answer
185
+
186
+
187
+ ## Where the editor of this engine keeps its export templates: beside the executable for a
188
+ ## self-contained one, otherwise in the user's data directory, as the editor's own paths have it.
189
+ static func _templates_dir() -> String:
190
+ var version: Dictionary = Engine.get_version_info()
191
+ var named: String = "%d.%d" % [Read.as_int(version.get("major", 0)), Read.as_int(version.get("minor", 0))]
192
+ if Read.as_int(version.get("patch", 0)) != 0:
193
+ named += "." + str(Read.as_int(version.get("patch", 0)))
194
+ named += "." + str(version.get("status", ""))
195
+ if ClassDB.class_exists("CSharpScript"):
196
+ named += ".mono"
197
+ var beside: String = OS.get_executable_path().get_base_dir()
198
+ var contained: bool = _marks_self_contained(beside)
199
+ # A macOS bundle is read-only, so its editor looks for the marker beside the bundle too, and keeps
200
+ # its data there whichever of the two places had it.
201
+ if (
202
+ OS.has_feature("macos")
203
+ and beside.ends_with("MacOS")
204
+ and beside.path_join("..").simplify_path().ends_with("Contents")
205
+ ):
206
+ beside = beside.path_join("../../..").simplify_path()
207
+ contained = contained or _marks_self_contained(beside)
208
+ if contained:
209
+ return beside.path_join("editor_data").path_join("export_templates").path_join(named)
210
+ var home: String = (
211
+ "godot" if OS.get_name() in ["Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD"] else "Godot"
212
+ )
213
+ return OS.get_data_dir().path_join(home).path_join("export_templates").path_join(named)
214
+
215
+
216
+ ## Whether [param templates] holds any of [param platform]'s templates. The directory is one per
217
+ ## engine version and holds whichever platforms were installed into it, so its being there says
218
+ ## nothing about this platform.
219
+ static func _installed(templates: String, platform: String) -> bool:
220
+ if not DirAccess.dir_exists_absolute(templates):
221
+ return false
222
+ for file: String in DirAccess.get_files_at(templates):
223
+ if file.begins_with(PLATFORMS[platform]):
224
+ return true
225
+ return false
226
+
227
+
228
+ static func _marks_self_contained(directory: String) -> bool:
229
+ return (
230
+ FileAccess.file_exists(directory.path_join("._sc_"))
231
+ or FileAccess.file_exists(directory.path_join("_sc_"))
232
+ )
@@ -1,10 +1,13 @@
1
1
  extends RefCounted
2
2
 
3
+ const Dependencies = preload("dependencies.gd")
3
4
  const Patterns = preload("patterns.gd")
4
5
  const Read = preload("reading.gd")
5
6
  const Log = preload("logger.gd")
6
7
 
7
8
  var _log: Log
9
+ ## Kept for the call, so the project's class names are read once however many bases are walked.
10
+ var _dependencies: Dependencies = null
8
11
 
9
12
 
10
13
  func _init(p_log: Log) -> void:
@@ -136,6 +139,21 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
136
139
  # its first function would otherwise be judged against the default.
137
140
  _mark_virtuals(functions, extends_name, full_script_path)
138
141
 
142
+ # A script typed by its class_name is depended on as surely as one preloaded, and the answer
143
+ # listed only the preloads: a `var stats: Stats` left stats.gd out, while project_dependencies
144
+ # named it. The same reading as that tool's, so the two agree.
145
+ var classes_used: Array[Dictionary] = []
146
+ if _dependencies == null:
147
+ _dependencies = Dependencies.new(_log)
148
+ var declared_by: Dictionary = _dependencies.classes_declared()
149
+ for used: String in Dependencies.classes_used_in(content, declared_by):
150
+ var declaring: String = str(declared_by[used])
151
+ if declaring == full_script_path:
152
+ continue
153
+ classes_used.append({"name": used, "path": declaring})
154
+ if not declaring in dependencies:
155
+ dependencies.append(declaring)
156
+
139
157
  var answer: Dictionary = {
140
158
  "path": script_path,
141
159
  "full_path": full_script_path,
@@ -148,6 +166,7 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
148
166
  "enums": enums,
149
167
  "inner_classes": inner_classes,
150
168
  "dependencies": dependencies,
169
+ "classes_used": classes_used,
151
170
  "line_count": lines.size()
152
171
  }
153
172
  if Read.as_bool(params.get("include_inherited", false)):
@@ -19,6 +19,7 @@ const AudioBuses = preload("audio_buses.gd")
19
19
  const ClassCache = preload("class_cache.gd")
20
20
  const ClassDbQueries = preload("classdb_queries.gd")
21
21
  const Dependencies = preload("dependencies.gd")
22
+ const ExportPresets = preload("export_presets.gd")
22
23
  const FileReads = preload("file_reads.gd")
23
24
  const GdscriptAnalysis = preload("gdscript_analysis.gd")
24
25
  const GdscriptAuthoring = preload("gdscript_authoring.gd")
@@ -31,6 +32,7 @@ const ProjectDiagnostics = preload("project_diagnostics.gd")
31
32
  const Read = preload("reading.gd")
32
33
  const RenameChecks = preload("rename_checks.gd")
33
34
  const ResourceFiles = preload("resource_files.gd")
35
+ const ScriptCheck = preload("script_check.gd")
34
36
 
35
37
  ## Printed once the answer is written, after which the engine runs the project's autoloads. Kept in
36
38
  ## step with `ANSWERED` in src/headless.ts.
@@ -179,7 +181,9 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
179
181
  "set_import_options":
180
182
  payload = ImportPipeline.new(_log).set_import_options(params)
181
183
  "list_export_presets":
182
- payload = ImportPipeline.new(_log).list_export_presets(params)
184
+ payload = ExportPresets.new(_log).list_export_presets(params)
185
+ "add_export_preset":
186
+ payload = ExportPresets.new(_log).add_export_preset(params)
183
187
  "validate_project":
184
188
  payload = ImportPipeline.new(_log).validate_project(params)
185
189
 
@@ -215,7 +219,7 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
215
219
  "names_taken":
216
220
  payload = RenameChecks.new(_log).names_taken(params)
217
221
  "check_scripts":
218
- payload = RenameChecks.new(_log).check_scripts(params)
222
+ payload = ScriptCheck.new(_log).check_scripts(params)
219
223
 
220
224
  # Plugins and input
221
225
  "list_plugins":
@@ -1,5 +1,6 @@
1
1
  extends RefCounted
2
2
 
3
+ const ExportPresets = preload("export_presets.gd")
3
4
  const Read = preload("reading.gd")
4
5
  const Serialisation = preload("serialisation.gd")
5
6
  const FileWalk = preload("file_walk.gd")
@@ -319,63 +320,6 @@ static func _all_ints(held: Variant, keys: bool) -> bool:
319
320
  return true
320
321
 
321
322
 
322
- # The names of the project's export presets, in the order export_presets.cfg holds them.
323
- static func _preset_names() -> Array[String]:
324
- var names: Array[String] = []
325
- var config: ConfigFile = ConfigFile.new()
326
- if config.load("res://export_presets.cfg") != OK:
327
- return names
328
- var index: int = 0
329
- while config.has_section("preset." + str(index)):
330
- names.append(str(config.get_value("preset." + str(index), "name", "")))
331
- index += 1
332
- return names
333
-
334
-
335
- # List export presets
336
- func list_export_presets(_params: Dictionary) -> Dictionary:
337
- _log.info("Listing export presets")
338
-
339
- var presets_file: String = "res://export_presets.cfg"
340
-
341
- if not FileAccess.file_exists(presets_file):
342
- return {
343
- "presets": [],
344
- "presets_file_exists": false,
345
- "note": "No export_presets.cfg found. Configure export presets in the Godot editor."
346
- }
347
-
348
- var config: ConfigFile = ConfigFile.new()
349
- var err: Error = config.load(presets_file)
350
-
351
- if err != OK:
352
- return _log.failure("Failed to parse export_presets.cfg: " + str(err))
353
-
354
- # Export presets are stored as [preset.0], [preset.1], etc.
355
- var presets: Array[Dictionary] = []
356
- var preset_idx: int = 0
357
- while config.has_section("preset." + str(preset_idx)):
358
- var section: String = "preset." + str(preset_idx)
359
- var preset: Dictionary = {
360
- "index": preset_idx,
361
- "name": config.get_value(section, "name", "Unknown"),
362
- "platform": config.get_value(section, "platform", "Unknown"),
363
- "runnable": config.get_value(section, "runnable", false),
364
- "export_path": config.get_value(section, "export_path", ""),
365
- "export_filter": config.get_value(section, "export_filter", "all_resources"),
366
- "include_filter": config.get_value(section, "include_filter", ""),
367
- "exclude_filter": config.get_value(section, "exclude_filter", "")
368
- }
369
-
370
- if config.has_section_key(section, "custom_features"):
371
- preset["custom_features"] = config.get_value(section, "custom_features", "")
372
-
373
- presets.append(preset)
374
- preset_idx += 1
375
-
376
- return {"presets": presets, "presets_file_exists": true, "total_presets": preset_idx}
377
-
378
-
379
323
  # Validate project for export
380
324
  func validate_project(params: Dictionary) -> Dictionary:
381
325
  var preset_name: String = str(params.get("preset", ""))
@@ -429,7 +373,7 @@ func validate_project(params: Dictionary) -> Dictionary:
429
373
  "warning",
430
374
  "export_presets",
431
375
  "No export presets configured",
432
- "Configure export presets in Godot editor: Project > Export",
376
+ "Add a preset with project_export add_preset, or in the editor: Project > Export",
433
377
  include_suggestions
434
378
  )
435
379
  )
@@ -438,7 +382,7 @@ func validate_project(params: Dictionary) -> Dictionary:
438
382
  # does not exist answered valid, as though it had been checked.
439
383
  if not preset_name.is_empty():
440
384
  checks_performed.append("export_preset")
441
- var named: Array[String] = _preset_names()
385
+ var named: Array[String] = ExportPresets.names()
442
386
  if preset_name not in named:
443
387
  issues.append(
444
388
  _finding(
@@ -453,7 +397,7 @@ func validate_project(params: Dictionary) -> Dictionary:
453
397
  else ": the presets are " + ", ".join(named)
454
398
  )
455
399
  ),
456
- "Pass one of the preset names, or add the preset in the editor: Project > Export",
400
+ "Pass one of the preset names, or add the preset with project_export add_preset",
457
401
  include_suggestions
458
402
  )
459
403
  )
@@ -148,7 +148,9 @@ func get_project_health(params: Dictionary) -> Dictionary:
148
148
  recommendations.append("Address critical issues to improve project stability")
149
149
  # A project still being made has no presets yet, which is no fault in it.
150
150
  if "config" in check_categories and not FileAccess.file_exists("res://export_presets.cfg"):
151
- recommendations.append("No export presets are configured: add them before exporting")
151
+ recommendations.append(
152
+ "No export presets are configured: project_export add_preset adds one before exporting"
153
+ )
152
154
 
153
155
  # Every detail costs points, so each is an issue; `issues` was always empty beside them, and a
154
156
  # category said passed next to the failure it had just found.
@@ -1,6 +1,5 @@
1
1
  extends RefCounted
2
2
 
3
- const GdscriptAuthoring = preload("gdscript_authoring.gd")
4
3
  const Log = preload("logger.gd")
5
4
 
6
5
  var _log: Log
@@ -63,21 +62,3 @@ static func _named(entries: Array[Dictionary], name: String) -> bool:
63
62
  if str(entry.get("name", "")) == name:
64
63
  return true
65
64
  return false
66
-
67
-
68
- # Whether each script compiles in a fresh engine, which reads the class cache as it starts and so
69
- # resolves every class the cache lists. The engine's reasons go to stderr and come back with the
70
- # answer. It names the scripts it checked rather than counting them, since a caller told one was
71
- # checked could not say which.
72
- func check_scripts(params: Dictionary) -> Dictionary:
73
- var paths: Array = params.get("paths", [])
74
- var checked: Array[String] = []
75
- var failed: Array[String] = []
76
- for value: Variant in paths:
77
- var path: String = str(value)
78
- checked.append(path)
79
- var script: Script = ResourceLoader.load(path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
80
- if not GdscriptAuthoring._parses(script):
81
- _log.error("Does not compile: " + path)
82
- failed.append(path)
83
- return {"checked": checked, "failed": failed}
@@ -0,0 +1,98 @@
1
+ extends RefCounted
2
+
3
+ ## Compiles scripts the way the engine does when it loads them, and answers what it raised.
4
+ ##
5
+ ## The engine reports a script that does not compile as errors raised from GDScript::reload, each
6
+ ## naming the file and line, and they reach a Logger as well as stderr. Measured on 4.7.2: a script
7
+ ## whose dependency does not compile reports the dependency's errors at the dependency's path and one
8
+ ## of its own at line 0, "Compile Error: Failed to compile depended scripts", and is still handed
9
+ ## back as a script that can_instantiate() calls instantiable. Warnings are not among them: the engine
10
+ ## sends those only to an editor or a debugger.
11
+ ##
12
+ ## A script that failed stays failed in the engine's script cache, which no ResourceLoader cache mode
13
+ ## reaches, and every script loaded after it that depends on it compiles in silence, reload() and all.
14
+ ## So a run ends at the first script that raised anything, and the rest go to a fresh engine.
15
+
16
+ const Read = preload("reading.gd")
17
+ const Log = preload("logger.gd")
18
+
19
+ ## Where the engine raises a script's compile errors from.
20
+ const RELOAD: String = "GDScript::reload"
21
+
22
+ var _log: Log
23
+
24
+
25
+ ## Every compile error the engine raises while it is attached, in order.
26
+ class Caught:
27
+ extends Logger
28
+
29
+ var raised: Array[Dictionary] = []
30
+
31
+ func _log_message(_message: String, _error: bool) -> void:
32
+ pass
33
+
34
+ # The engine's own signature for a Logger, eight parameters and not this project's to shape.
35
+ # gdlint:ignore = function-arguments-number
36
+ func _log_error(
37
+ function: String,
38
+ file: String,
39
+ line: int,
40
+ code: String,
41
+ rationale: String,
42
+ _editor_notify: bool,
43
+ _error_type: int,
44
+ _script_backtraces: Array[ScriptBacktrace],
45
+ ) -> void:
46
+ if function != RELOAD:
47
+ return
48
+ raised.append(
49
+ {"path": file, "line": line, "message": rationale if not rationale.is_empty() else code}
50
+ )
51
+
52
+
53
+ func _init(p_log: Log) -> void:
54
+ _log = p_log
55
+
56
+
57
+ ## Whether the scripts in [code]paths[/code] compile in a fresh engine, which reads the class cache
58
+ ## as it starts and so resolves every class the cache lists, up to and including the first that
59
+ ## raised an error. [code]checked[/code] names the scripts read, in order, and [code]failed[/code]
60
+ ## those that did not compile; [code]scripts[/code] holds each failed one with the errors raised in
61
+ ## its own file, and the other scripts that failed to compile while it was loaded. Read past the
62
+ ## resource cache, so a script loaded for an earlier one is compiled again for its own entry.
63
+ func check_scripts(params: Dictionary) -> Dictionary:
64
+ var asked: Array = params.get("paths", [])
65
+ var caught: Caught = Caught.new()
66
+ OS.add_logger(caught)
67
+ var checked: Array[String] = []
68
+ var failed: Array[String] = []
69
+ var scripts: Array[Dictionary] = []
70
+ for entry: Variant in asked:
71
+ var path: String = str(entry)
72
+ checked.append(path)
73
+ caught.raised.clear()
74
+ var loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
75
+ var errors: Array[Dictionary] = []
76
+ var others: Array[String] = []
77
+ for error: Dictionary in caught.raised:
78
+ var at: String = str(error["path"])
79
+ if at != path:
80
+ if not at in others:
81
+ others.append(at)
82
+ continue
83
+ var found: Dictionary = {"line": Read.as_int(error["line"]), "message": str(error["message"])}
84
+ if not found in errors:
85
+ errors.append(found)
86
+ if loaded == null and errors.is_empty():
87
+ errors.append({"line": 0, "message": "The engine could not load it as a script."})
88
+ if errors.is_empty() and others.is_empty():
89
+ continue
90
+ if not errors.is_empty():
91
+ failed.append(path)
92
+ var one: Dictionary = {"path": path, "errors": errors}
93
+ if not others.is_empty():
94
+ one["dependencies_with_errors"] = others
95
+ scripts.append(one)
96
+ break
97
+ OS.remove_logger(caught)
98
+ return {"checked": checked, "failed": failed, "scripts": scripts}