rigortype 0.1.2 → 0.1.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.
- checksums.yaml +4 -4
- data/README.md +135 -31
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +113 -0
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +72 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +139 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +200 -0
- data/lib/rigor/analysis/dependency_source_inference.rb +38 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/runner.rb +206 -6
- data/lib/rigor/builtins/imported_refinements.rb +360 -55
- data/lib/rigor/cache/descriptor.rb +59 -6
- data/lib/rigor/cache/store.rb +1 -1
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/dependencies.rb +235 -0
- data/lib/rigor/configuration.rb +45 -11
- data/lib/rigor/environment.rb +47 -4
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +7 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +67 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
- data/lib/rigor/inference/method_dispatcher.rb +233 -2
- data/lib/rigor/inference/method_parameter_binder.rb +1 -3
- data/lib/rigor/inference/narrowing.rb +2 -4
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +70 -6
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +2 -2
- data/lib/rigor/plugin/manifest.rb +49 -7
- data/lib/rigor/plugin/registry.rb +11 -0
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +195 -2
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +62 -0
- data/lib/rigor/type_node/identifier.rb +30 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +29 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +6 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +3 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- metadata +58 -1
|
@@ -6,12 +6,14 @@ require_relative "../environment"
|
|
|
6
6
|
require_relative "../scope"
|
|
7
7
|
require_relative "../cache/store"
|
|
8
8
|
require_relative "../plugin"
|
|
9
|
+
require_relative "../rbs_extended/reporter"
|
|
9
10
|
require_relative "../reflection"
|
|
10
11
|
require_relative "../type/combinator"
|
|
11
12
|
require_relative "../inference/coverage_scanner"
|
|
12
13
|
require_relative "../inference/scope_indexer"
|
|
13
14
|
require_relative "../inference/method_dispatcher/file_folding"
|
|
14
15
|
require_relative "check_rules"
|
|
16
|
+
require_relative "dependency_source_inference"
|
|
15
17
|
require_relative "diagnostic"
|
|
16
18
|
require_relative "result"
|
|
17
19
|
|
|
@@ -21,7 +23,8 @@ module Rigor
|
|
|
21
23
|
RUBY_GLOB = "**/*.rb"
|
|
22
24
|
DEFAULT_CACHE_ROOT = ".rigor/cache"
|
|
23
25
|
|
|
24
|
-
attr_reader :cache_store, :plugin_registry
|
|
26
|
+
attr_reader :cache_store, :plugin_registry, :dependency_source_index,
|
|
27
|
+
:rbs_extended_reporter, :boundary_cross_reporter
|
|
25
28
|
|
|
26
29
|
# @param configuration [Rigor::Configuration]
|
|
27
30
|
# @param explain [Boolean] surface fail-soft fallback events
|
|
@@ -40,6 +43,9 @@ module Rigor
|
|
|
40
43
|
@cache_store = cache_store
|
|
41
44
|
@plugin_requirer = plugin_requirer
|
|
42
45
|
@plugin_registry = Plugin::Registry::EMPTY
|
|
46
|
+
@dependency_source_index = DependencySourceInference::Index::EMPTY
|
|
47
|
+
@rbs_extended_reporter = RbsExtended::Reporter.new
|
|
48
|
+
@boundary_cross_reporter = DependencySourceInference::BoundaryCrossReporter.new
|
|
43
49
|
end
|
|
44
50
|
|
|
45
51
|
# Walks every Ruby file under `paths`, parses it, builds a
|
|
@@ -58,22 +64,41 @@ module Rigor
|
|
|
58
64
|
return Result.new(diagnostics: [target_ruby_error]) if target_ruby_error
|
|
59
65
|
|
|
60
66
|
@plugin_registry = load_plugins
|
|
67
|
+
@dependency_source_index = DependencySourceInference::Builder.build(@configuration.dependencies)
|
|
61
68
|
environment = Environment.for_project(
|
|
62
69
|
libraries: @configuration.libraries,
|
|
63
70
|
signature_paths: @configuration.signature_paths,
|
|
64
71
|
cache_store: @cache_store,
|
|
65
|
-
plugin_registry: @plugin_registry
|
|
72
|
+
plugin_registry: @plugin_registry,
|
|
73
|
+
dependency_source_index: @dependency_source_index,
|
|
74
|
+
rbs_extended_reporter: @rbs_extended_reporter,
|
|
75
|
+
boundary_cross_reporter: @boundary_cross_reporter
|
|
66
76
|
)
|
|
67
77
|
expansion = expand_paths(paths)
|
|
68
78
|
|
|
69
|
-
diagnostics =
|
|
70
|
-
diagnostics += plugin_prepare_diagnostics
|
|
71
|
-
diagnostics += expansion.fetch(:errors)
|
|
79
|
+
diagnostics = pre_file_diagnostics(expansion)
|
|
72
80
|
diagnostics += expansion.fetch(:files).flat_map { |path| analyze_file(path, environment) }
|
|
81
|
+
diagnostics += rbs_extended_reporter_diagnostics
|
|
82
|
+
diagnostics += boundary_cross_diagnostics
|
|
73
83
|
|
|
74
84
|
Result.new(diagnostics: apply_severity_profile(diagnostics))
|
|
75
85
|
end
|
|
76
86
|
|
|
87
|
+
# Pre-file diagnostic streams that fire once per run rather
|
|
88
|
+
# than per analyzed file: plugin load / prepare envelopes,
|
|
89
|
+
# the ADR-10 dependency-source resolution surface, and the
|
|
90
|
+
# `expand_paths` errors for `paths:` entries that don't
|
|
91
|
+
# exist or aren't `.rb`. Aggregated here so `#run` stays
|
|
92
|
+
# under the ABC budget.
|
|
93
|
+
def pre_file_diagnostics(expansion)
|
|
94
|
+
plugin_load_diagnostics +
|
|
95
|
+
plugin_prepare_diagnostics +
|
|
96
|
+
dependency_source_diagnostics +
|
|
97
|
+
dependency_source_budget_diagnostics +
|
|
98
|
+
dependency_source_config_conflict_diagnostics +
|
|
99
|
+
expansion.fetch(:errors)
|
|
100
|
+
end
|
|
101
|
+
|
|
77
102
|
# `target_ruby` flows through to Prism's `version:` option.
|
|
78
103
|
# Prism enforces the supported range and raises
|
|
79
104
|
# `ArgumentError` for versions it does not recognise. Run a
|
|
@@ -207,6 +232,181 @@ module Rigor
|
|
|
207
232
|
end
|
|
208
233
|
end
|
|
209
234
|
|
|
235
|
+
# ADR-10 § "Diagnostic prefix family" — surfaces gems
|
|
236
|
+
# listed in `dependencies.source_inference` that RubyGems
|
|
237
|
+
# could not resolve. The run continues; the gem simply
|
|
238
|
+
# contributes nothing this session, mirroring the
|
|
239
|
+
# plugin-load error envelope. Authored `:warning` because
|
|
240
|
+
# an unresolvable gem usually means a typo or a missing
|
|
241
|
+
# `bundle install` rather than a project-blocking problem;
|
|
242
|
+
# the severity profile still re-stamps it.
|
|
243
|
+
def dependency_source_diagnostics
|
|
244
|
+
@dependency_source_index.unresolvable.map do |entry|
|
|
245
|
+
Diagnostic.new(
|
|
246
|
+
path: ".rigor.yml",
|
|
247
|
+
line: 1,
|
|
248
|
+
column: 1,
|
|
249
|
+
message: "dependencies.source_inference[].gem #{entry.gem_name.inspect} could not be " \
|
|
250
|
+
"resolved (#{entry.reason}); skipping",
|
|
251
|
+
severity: :warning,
|
|
252
|
+
rule: "dynamic.dependency-source.gem-not-found",
|
|
253
|
+
source_family: :builtin
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# ADR-10 § "Budget interaction" / slice 4 — emits one
|
|
259
|
+
# `:warning` per gem whose Walker run hit the
|
|
260
|
+
# `dependencies.budget_per_gem` cap. The cap is a Walker-
|
|
261
|
+
# side guard rail (slice 4 picks the (α) semantics from
|
|
262
|
+
# ADR-10 WD4: harvesting stops, the dispatcher behaves
|
|
263
|
+
# exactly as before for unrecorded methods). The
|
|
264
|
+
# diagnostic names the gem and points the user at the
|
|
265
|
+
# three remediations: ship RBS, reduce `mode:` from
|
|
266
|
+
# `full` to `when_missing`, or de-list the gem.
|
|
267
|
+
# ADR-10 § "config-conflict diagnostic" / 5d — surfaces
|
|
268
|
+
# `Configuration::Dependencies` warnings accumulated
|
|
269
|
+
# during `from_h` deduplication of the `includes:`-chain
|
|
270
|
+
# source_inference array. Each warning describes a
|
|
271
|
+
# per-gem mode conflict that the merge resolved
|
|
272
|
+
# right-wins; the user sees one diagnostic per conflict.
|
|
273
|
+
# `:warning` matches the user's "warn but don't block"
|
|
274
|
+
# preference per the design discussion.
|
|
275
|
+
def dependency_source_config_conflict_diagnostics
|
|
276
|
+
@configuration.dependencies.warnings.map do |message|
|
|
277
|
+
Diagnostic.new(
|
|
278
|
+
path: ".rigor.yml",
|
|
279
|
+
line: 1,
|
|
280
|
+
column: 1,
|
|
281
|
+
message: message,
|
|
282
|
+
severity: :warning,
|
|
283
|
+
rule: "dynamic.dependency-source.config-conflict",
|
|
284
|
+
source_family: :builtin
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def dependency_source_budget_diagnostics
|
|
290
|
+
budget = @configuration.dependencies.budget_per_gem
|
|
291
|
+
@dependency_source_index.budget_exceeded.map do |gem_name|
|
|
292
|
+
Diagnostic.new(
|
|
293
|
+
path: ".rigor.yml",
|
|
294
|
+
line: 1,
|
|
295
|
+
column: 1,
|
|
296
|
+
message: "dependencies.source_inference[].gem #{gem_name.inspect} exceeded the per-gem " \
|
|
297
|
+
"catalog cap (#{budget} method definitions); the remaining methods fall back " \
|
|
298
|
+
"to the existing RBS-or-Dynamic[top] boundary. Ship RBS for the gem, set " \
|
|
299
|
+
"`mode: when_missing` instead of `full`, or de-list the gem.",
|
|
300
|
+
severity: :warning,
|
|
301
|
+
rule: "dynamic.dependency-source.budget-exceeded",
|
|
302
|
+
source_family: :builtin
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# ADR-13 slice 3b — drains the per-run
|
|
308
|
+
# {RbsExtended::Reporter} into one diagnostic per accumulated
|
|
309
|
+
# event:
|
|
310
|
+
#
|
|
311
|
+
# - `dynamic.rbs-extended.unresolved` for every annotation
|
|
312
|
+
# payload the parser could not turn into a {Rigor::Type}.
|
|
313
|
+
# Surfaces typos and references to plugin-supplied names
|
|
314
|
+
# the project did not enable.
|
|
315
|
+
# - `dynamic.shape.lossy-projection` for every shape-projection
|
|
316
|
+
# type function (`pick_of`, …) applied to a carrier that
|
|
317
|
+
# loses precision (anything other than `HashShape` / `Tuple`).
|
|
318
|
+
#
|
|
319
|
+
# Both are authored `:info`; the severity profile re-stamps
|
|
320
|
+
# them per project taste. Path / line / column come from the
|
|
321
|
+
# annotation's `RBS::Location` when available, falling back
|
|
322
|
+
# to `.rigor.yml`-style file-level attribution otherwise.
|
|
323
|
+
def rbs_extended_reporter_diagnostics
|
|
324
|
+
return [] if @rbs_extended_reporter.empty?
|
|
325
|
+
|
|
326
|
+
unresolved = @rbs_extended_reporter.unresolved_payloads.map do |entry|
|
|
327
|
+
build_reporter_diagnostic(
|
|
328
|
+
entry.source_location,
|
|
329
|
+
rule: "dynamic.rbs-extended.unresolved",
|
|
330
|
+
message: "`RBS::Extended` directive payload could not be resolved: " \
|
|
331
|
+
"#{entry.payload.inspect}. Check for typos or enable a plugin " \
|
|
332
|
+
"that contributes the referenced type vocabulary."
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
lossy = @rbs_extended_reporter.lossy_projections.map do |entry|
|
|
337
|
+
build_reporter_diagnostic(
|
|
338
|
+
entry.source_location,
|
|
339
|
+
rule: "dynamic.shape.lossy-projection",
|
|
340
|
+
message: "Shape projection `#{entry.head}` applied to a carrier without a " \
|
|
341
|
+
"literal shape; the projection degrades to the input type. Author " \
|
|
342
|
+
"a `HashShape` / `Tuple` carrier or accept the unchanged result."
|
|
343
|
+
)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
unresolved + lossy
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# ADR-10 slice 5c — drains the per-run
|
|
350
|
+
# {DependencySourceInference::BoundaryCrossReporter} into
|
|
351
|
+
# `dynamic.dependency-source.boundary-cross` `:info`
|
|
352
|
+
# diagnostics. Each event flags a call site where RBS
|
|
353
|
+
# dispatch produced a concrete answer AND a `mode: :full`
|
|
354
|
+
# opt-in gem's source catalog ALSO contains an entry for
|
|
355
|
+
# the same `(class_name, method_name)` — i.e., both
|
|
356
|
+
# contracts have an opinion. RBS still wins on the
|
|
357
|
+
# dispatch result; the diagnostic is purely advisory so
|
|
358
|
+
# the user can verify the two contracts haven't drifted.
|
|
359
|
+
#
|
|
360
|
+
# Severity profile re-stamps the rule per project taste.
|
|
361
|
+
# The diagnostic carries no `path` / `line` / `column`
|
|
362
|
+
# because the crossing is per-method-per-gem, not
|
|
363
|
+
# per-call-site — the diagnostic anchors at `.rigor.yml`
|
|
364
|
+
# like the other `dependency-source.*` diagnostics that
|
|
365
|
+
# report on opt-in configuration.
|
|
366
|
+
def boundary_cross_diagnostics
|
|
367
|
+
return [] if @boundary_cross_reporter.empty?
|
|
368
|
+
|
|
369
|
+
@boundary_cross_reporter.entries.map do |entry|
|
|
370
|
+
Diagnostic.new(
|
|
371
|
+
path: ".rigor.yml", line: 1, column: 1,
|
|
372
|
+
message: "`#{entry.class_name}##{entry.method_name}` is contributed by both " \
|
|
373
|
+
"RBS (#{entry.rbs_display}) and the `mode: :full` opt-in gem " \
|
|
374
|
+
"`#{entry.gem_name}`. RBS wins on dispatch; verify the gem source " \
|
|
375
|
+
"has not drifted from its RBS contract.",
|
|
376
|
+
severity: :info,
|
|
377
|
+
rule: "dynamic.dependency-source.boundary-cross",
|
|
378
|
+
source_family: :builtin
|
|
379
|
+
)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def build_reporter_diagnostic(source_location, rule:, message:)
|
|
384
|
+
path, line, column = location_fields(source_location)
|
|
385
|
+
Diagnostic.new(
|
|
386
|
+
path: path, line: line, column: column,
|
|
387
|
+
message: message, severity: :info, rule: rule, source_family: :builtin
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def location_fields(source_location)
|
|
392
|
+
return [".rigor.yml", 1, 1] if source_location.nil?
|
|
393
|
+
|
|
394
|
+
path = location_path(source_location)
|
|
395
|
+
line = source_location.respond_to?(:start_line) ? source_location.start_line : 1
|
|
396
|
+
column = source_location.respond_to?(:start_column) ? source_location.start_column + 1 : 1
|
|
397
|
+
[path, line, column]
|
|
398
|
+
rescue StandardError
|
|
399
|
+
[".rigor.yml", 1, 1]
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def location_path(source_location)
|
|
403
|
+
buffer = source_location.respond_to?(:buffer) ? source_location.buffer : nil
|
|
404
|
+
return ".rigor.yml" if buffer.nil? || !buffer.respond_to?(:name)
|
|
405
|
+
|
|
406
|
+
name = buffer.name.to_s
|
|
407
|
+
name.empty? ? ".rigor.yml" : name
|
|
408
|
+
end
|
|
409
|
+
|
|
210
410
|
# ADR-9 slice 3 — invokes every loaded plugin's `#prepare`
|
|
211
411
|
# hook once per run, after the loader's `#init` pass and
|
|
212
412
|
# before per-file iteration. Plugins publish facts here
|
|
@@ -364,7 +564,7 @@ module Rigor
|
|
|
364
564
|
parse_result = Prism.parse_file(path, version: @configuration.target_ruby)
|
|
365
565
|
return parse_diagnostics(path, parse_result) unless parse_result.errors.empty?
|
|
366
566
|
|
|
367
|
-
scope = Scope.empty(environment: environment)
|
|
567
|
+
scope = Scope.empty(environment: environment, source_path: path)
|
|
368
568
|
index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
|
|
369
569
|
diagnostics = CheckRules.diagnose(
|
|
370
570
|
path: path,
|