rigortype 0.1.14 → 0.1.16
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 +10 -2
- data/exe/rigor +19 -0
- data/lib/rigor/analysis/check_rules.rb +428 -6
- data/lib/rigor/analysis/diagnostic.rb +55 -3
- data/lib/rigor/analysis/rule_catalog.rb +80 -0
- data/lib/rigor/analysis/runner.rb +71 -2
- data/lib/rigor/analysis/worker_session.rb +3 -2
- data/lib/rigor/cache/descriptor.rb +6 -2
- data/lib/rigor/cli/plugin_command.rb +245 -0
- data/lib/rigor/cli/plugins_command.rb +51 -4
- data/lib/rigor/cli/plugins_renderer.rb +86 -1
- data/lib/rigor/cli.rb +143 -5
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/environment/rbs_loader.rb +259 -1
- data/lib/rigor/environment.rb +8 -2
- data/lib/rigor/inference/budget_trace.rb +137 -0
- data/lib/rigor/inference/expression_typer.rb +9 -2
- data/lib/rigor/inference/hkt_reducer.rb +2 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +81 -14
- data/lib/rigor/inference/method_dispatcher.rb +57 -10
- data/lib/rigor/inference/precision_scanner.rb +60 -1
- data/lib/rigor/inference/scope_indexer.rb +184 -27
- data/lib/rigor/inference/statement_evaluator.rb +13 -8
- data/lib/rigor/inference/synthetic_method_index.rb +23 -4
- data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
- data/lib/rigor/plugin/additional_initializer.rb +108 -0
- data/lib/rigor/plugin/base.rb +321 -2
- data/lib/rigor/plugin/box.rb +64 -0
- data/lib/rigor/plugin/inflector.rb +121 -0
- data/lib/rigor/plugin/isolation.rb +191 -0
- data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
- data/lib/rigor/plugin/macro.rb +1 -0
- data/lib/rigor/plugin/manifest.rb +120 -23
- data/lib/rigor/plugin/node_context.rb +62 -0
- data/lib/rigor/plugin/registry.rb +10 -0
- data/lib/rigor/plugin.rb +3 -0
- data/lib/rigor/scope.rb +27 -1
- data/lib/rigor/sig_gen/generator.rb +2 -3
- data/lib/rigor/sig_gen/observation_collector.rb +2 -2
- data/lib/rigor/source/literals.rb +118 -0
- data/lib/rigor/source/node_walker.rb +26 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +71 -5
- data/lib/rigor/type/combinator.rb +6 -1
- data/lib/rigor/type/union.rb +65 -1
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
- data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +36 -31
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +6 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
- data/sig/rigor/plugin/access_denied_error.rbs +3 -1
- data/sig/rigor/plugin/base.rbs +58 -3
- data/sig/rigor/plugin/io_boundary.rbs +3 -0
- data/sig/rigor/plugin/manifest.rbs +31 -1
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor/source.rbs +12 -0
- data/sig/rigor.rbs +5 -0
- data/skills/rigor-plugin-author/SKILL.md +33 -9
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +65 -26
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +213 -80
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
- data/skills/rigor-project-init/SKILL.md +72 -7
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +233 -19
- metadata +53 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
|
@@ -13,7 +13,7 @@ module Rigor
|
|
|
13
13
|
# tooling (SKILLs, CI, editor integrations) while text is
|
|
14
14
|
# for interactive inspection. Rows are printed in the order
|
|
15
15
|
# the loader resolved them.
|
|
16
|
-
class PluginsRenderer
|
|
16
|
+
class PluginsRenderer # rubocop:disable Metrics/ClassLength
|
|
17
17
|
def initialize(rows:, configuration_path:)
|
|
18
18
|
@rows = rows
|
|
19
19
|
@configuration_path = configuration_path
|
|
@@ -42,8 +42,74 @@ module Rigor
|
|
|
42
42
|
)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# ADR-37 § "Machine-readable capability catalogue" — the focused
|
|
46
|
+
# per-plugin extension-protocol dump. Only loaded plugins appear
|
|
47
|
+
# (a plugin that failed to load contributes no capabilities), and
|
|
48
|
+
# each carries only the gate values an agent enumerates to learn
|
|
49
|
+
# what the plugin does: node-rule node types, dynamic-return
|
|
50
|
+
# receivers, type-specifier methods, and produced / consumed facts.
|
|
51
|
+
def capabilities_json
|
|
52
|
+
JSON.pretty_generate(
|
|
53
|
+
{
|
|
54
|
+
"configuration" => @configuration_path,
|
|
55
|
+
"capabilities" => loaded_rows.map { |row| capabilities_json_for(row) }
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def capabilities_text
|
|
61
|
+
lines = ["Plugin capability catalogue (ADR-37 narrow extension protocols)", ""]
|
|
62
|
+
loaded = loaded_rows
|
|
63
|
+
if loaded.empty?
|
|
64
|
+
lines << " (no plugins loaded)"
|
|
65
|
+
else
|
|
66
|
+
loaded.each_with_index do |row, index|
|
|
67
|
+
lines.concat(capability_lines(row))
|
|
68
|
+
lines << "" unless index == loaded.size - 1
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
lines.join("\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
45
74
|
private
|
|
46
75
|
|
|
76
|
+
def loaded_rows
|
|
77
|
+
@rows.select { |r| r[:status] == :loaded }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def capability_lines(row)
|
|
81
|
+
lines = [" #{row[:id]} v#{row[:version]} (#{row[:gem]})"]
|
|
82
|
+
capability_surfaces(row).each { |surface| lines << " #{surface}" }
|
|
83
|
+
lines << " (no narrow extension protocols declared)" if lines.size == 1
|
|
84
|
+
lines
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The non-empty capability surfaces for a plugin, each as a
|
|
88
|
+
# `label: a, b, c` string. Data-driven so the catalogue stays a
|
|
89
|
+
# single source of truth shared between the text and JSON views.
|
|
90
|
+
def capability_surfaces(row)
|
|
91
|
+
[
|
|
92
|
+
["node_rule", row[:node_rule_types]],
|
|
93
|
+
["dynamic_return receivers", row[:dynamic_return_receivers]],
|
|
94
|
+
["type_specifier methods", row[:type_specifier_methods]],
|
|
95
|
+
["produces", row[:produces]],
|
|
96
|
+
["consumes", row[:consumes]]
|
|
97
|
+
].filter_map { |label, values| "#{label}: #{values.join(', ')}" if values.any? }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def capabilities_json_for(row)
|
|
101
|
+
{
|
|
102
|
+
"id" => row[:id],
|
|
103
|
+
"gem" => row[:gem],
|
|
104
|
+
"version" => row[:version],
|
|
105
|
+
"node_rule_types" => row[:node_rule_types],
|
|
106
|
+
"dynamic_return_receivers" => row[:dynamic_return_receivers],
|
|
107
|
+
"type_specifier_methods" => row[:type_specifier_methods],
|
|
108
|
+
"produces" => row[:produces],
|
|
109
|
+
"consumes" => row[:consumes]
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
47
113
|
def header
|
|
48
114
|
loaded = @rows.count { |r| r[:status] == :loaded }
|
|
49
115
|
errored = @rows.count { |r| r[:status] == :load_error }
|
|
@@ -99,6 +165,22 @@ module Rigor
|
|
|
99
165
|
lines << " owns_receivers: #{row[:owns_receivers].join(', ')}" if row[:owns_receivers].any?
|
|
100
166
|
lines << " produces: #{row[:produces].join(', ')}" if row[:produces].any?
|
|
101
167
|
lines << " consumes: #{row[:consumes].join(', ')}" if row[:consumes].any?
|
|
168
|
+
lines.concat(narrow_protocol_lines(row))
|
|
169
|
+
lines
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# ADR-37 narrow extension protocols (node_rule / dynamic_return /
|
|
173
|
+
# type_specifier). Surfaced in the full report alongside the
|
|
174
|
+
# declarative surfaces; `--capabilities` is the focused view.
|
|
175
|
+
def narrow_protocol_lines(row)
|
|
176
|
+
lines = []
|
|
177
|
+
lines << " node_rule: #{row[:node_rule_types].join(', ')}" if row[:node_rule_types].any?
|
|
178
|
+
if row[:dynamic_return_receivers].any?
|
|
179
|
+
lines << " dynamic_return receivers: #{row[:dynamic_return_receivers].join(', ')}"
|
|
180
|
+
end
|
|
181
|
+
if row[:type_specifier_methods].any?
|
|
182
|
+
lines << " type_specifier methods: #{row[:type_specifier_methods].join(', ')}"
|
|
183
|
+
end
|
|
102
184
|
lines
|
|
103
185
|
end
|
|
104
186
|
|
|
@@ -157,6 +239,9 @@ module Rigor
|
|
|
157
239
|
"hkt_definitions" => row[:hkt_definitions],
|
|
158
240
|
"protocol_contracts" => row[:protocol_contracts],
|
|
159
241
|
"source_rbs_synthesizer" => row[:source_rbs_synthesizer],
|
|
242
|
+
"node_rule_types" => row[:node_rule_types],
|
|
243
|
+
"dynamic_return_receivers" => row[:dynamic_return_receivers],
|
|
244
|
+
"type_specifier_methods" => row[:type_specifier_methods],
|
|
160
245
|
"load_error" => row[:load_error]
|
|
161
246
|
}
|
|
162
247
|
end
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Rigor
|
|
|
33
33
|
"triage" => :run_triage,
|
|
34
34
|
"coverage" => :run_coverage,
|
|
35
35
|
"plugins" => :run_plugins,
|
|
36
|
+
"plugin" => :run_plugin,
|
|
36
37
|
"playground" => :run_playground,
|
|
37
38
|
"skill" => :run_skill
|
|
38
39
|
}.freeze
|
|
@@ -77,11 +78,7 @@ module Rigor
|
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def run_check
|
|
80
|
-
|
|
81
|
-
require_relative "analysis/buffer_binding"
|
|
82
|
-
require_relative "analysis/baseline"
|
|
83
|
-
require_relative "cache/store"
|
|
84
|
-
|
|
81
|
+
load_check_dependencies
|
|
85
82
|
options = parse_check_options
|
|
86
83
|
buffer = resolve_buffer_binding(options)
|
|
87
84
|
return EXIT_USAGE if buffer == :usage_error
|
|
@@ -99,6 +96,7 @@ module Rigor
|
|
|
99
96
|
|
|
100
97
|
write_result(result, options.fetch(:format))
|
|
101
98
|
write_run_stats(result.stats) if result.stats
|
|
99
|
+
write_trace_appendices
|
|
102
100
|
write_cache_stats(cache_root, runner.cache_store) if options.fetch(:cache_stats)
|
|
103
101
|
|
|
104
102
|
exit_code = result.success? ? 0 : 1
|
|
@@ -401,6 +399,139 @@ module Rigor
|
|
|
401
399
|
stats.format(@err)
|
|
402
400
|
end
|
|
403
401
|
|
|
402
|
+
# Opt-in developer diagnostics printed after the run: the
|
|
403
|
+
# inference-cutoff trace (RIGOR_BUDGET_TRACE) and the heap-attribution
|
|
404
|
+
# profile (RIGOR_HEAP_PROFILE). Each gates itself, so this is a no-op
|
|
405
|
+
# on a normal run.
|
|
406
|
+
def write_trace_appendices
|
|
407
|
+
write_budget_trace
|
|
408
|
+
write_heap_profile
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Dumps the opt-in inference-cutoff counters (RIGOR_BUDGET_TRACE).
|
|
412
|
+
# These are the hard-coded "budget" guards that silently degrade
|
|
413
|
+
# to `Dynamic[top]` / a fallback bound — counting them shows where
|
|
414
|
+
# inference actually stopped. Process-global counters: meaningful
|
|
415
|
+
# only on a single-process run (`--workers 0`), since they do not
|
|
416
|
+
# cross fork boundaries.
|
|
417
|
+
def write_budget_trace
|
|
418
|
+
return unless Inference::BudgetTrace.enabled?
|
|
419
|
+
|
|
420
|
+
counts = Inference::BudgetTrace.snapshot
|
|
421
|
+
@err.puts("")
|
|
422
|
+
@err.puts("Inference cutoffs (RIGOR_BUDGET_TRACE; --workers 0 for an exact count)")
|
|
423
|
+
@err.puts(" recursion-guard hits: #{counts[Inference::BudgetTrace::RECURSION_GUARD]}")
|
|
424
|
+
@err.puts(" ancestor-walk-limit hits: #{counts[Inference::BudgetTrace::ANCESTOR_WALK_LIMIT]}")
|
|
425
|
+
@err.puts(" hkt-fuel-exhausted hits: #{counts[Inference::BudgetTrace::HKT_FUEL_EXHAUSTED]}")
|
|
426
|
+
write_budget_distributions
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Dumps the read-only size distributions (ADR-41 Slice 2a). These
|
|
430
|
+
# observe how large unions actually get, with no cap enforced — the
|
|
431
|
+
# data the `union_size` budget default should be chosen from. The
|
|
432
|
+
# `over` thresholds bracket the TypeProf prior (10) and Rigor's spec
|
|
433
|
+
# default (24).
|
|
434
|
+
def write_budget_distributions
|
|
435
|
+
summary = Inference::BudgetTrace.summarize(Inference::BudgetTrace::UNION_ARITY, over: [10, 24, 40])
|
|
436
|
+
pct = summary[:percentiles]
|
|
437
|
+
@err.puts(" union arity: n=#{summary[:count]} max=#{summary[:max]} " \
|
|
438
|
+
"p50=#{pct[:p50]} p90=#{pct[:p90]} p99=#{pct[:p99]}")
|
|
439
|
+
over = summary[:over]
|
|
440
|
+
@err.puts(" unions ≥10: #{over[10]} ≥24: #{over[24]} ≥40: #{over[40]}")
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Dumps a live-heap class breakdown (RIGOR_HEAP_PROFILE) — retained
|
|
444
|
+
# objects by class after a forced GC, ranked by total memsize. The
|
|
445
|
+
# tool for attributing where the analyzer's resident memory goes
|
|
446
|
+
# (ADR-41 Slice 2b): it answers whether the heap is type carriers,
|
|
447
|
+
# RBS objects, Prism nodes, or fact-store Hashes/Strings. Walking the
|
|
448
|
+
# whole heap is slow — a dev probe, not a normal diagnostic. Run
|
|
449
|
+
# single-process (`--workers 0`) so the parent heap is the analysis
|
|
450
|
+
# heap; the gem is required lazily so a normal run never loads it.
|
|
451
|
+
def write_heap_profile
|
|
452
|
+
return if ENV["RIGOR_HEAP_PROFILE"].to_s.empty?
|
|
453
|
+
|
|
454
|
+
by_class, total = tally_live_heap
|
|
455
|
+
@err.puts("")
|
|
456
|
+
@err.puts("Heap profile (RIGOR_HEAP_PROFILE; live objects after GC, by class)")
|
|
457
|
+
@err.puts(" total tracked: #{heap_mb(total)} across #{by_class.size} classes")
|
|
458
|
+
by_class.sort_by { |_, (_, bytes)| -bytes }.first(30).each do |name, (count, bytes)|
|
|
459
|
+
@err.puts(" #{heap_mb(bytes).rjust(10)} #{count.to_s.rjust(9)} obj #{name}")
|
|
460
|
+
end
|
|
461
|
+
write_string_allocation_sites
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Loads the analysis-path dependencies lazily (so non-check commands
|
|
465
|
+
# stay light) and starts heap-allocation tracing if requested, before
|
|
466
|
+
# any analysis object is allocated.
|
|
467
|
+
def load_check_dependencies
|
|
468
|
+
require_relative "analysis/runner"
|
|
469
|
+
require_relative "analysis/buffer_binding"
|
|
470
|
+
require_relative "analysis/baseline"
|
|
471
|
+
require_relative "cache/store"
|
|
472
|
+
start_heap_trace_if_requested
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# Starts allocation tracing (RIGOR_HEAP_TRACE) as early as possible so
|
|
476
|
+
# the heap profile can attribute retained Strings to their allocation
|
|
477
|
+
# `file:line`. Very high overhead — run on a small file subset only.
|
|
478
|
+
def start_heap_trace_if_requested
|
|
479
|
+
return if ENV["RIGOR_HEAP_TRACE"].to_s.empty?
|
|
480
|
+
|
|
481
|
+
require "objspace"
|
|
482
|
+
ObjectSpace.trace_object_allocations_start
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# When RIGOR_HEAP_TRACE is on, groups the live String objects by their
|
|
486
|
+
# allocation site (`sourcefile:sourceline`) and prints the top sites by
|
|
487
|
+
# count — pinpointing which engine code retains the millions of strings
|
|
488
|
+
# that dominate the large-app heap (ADR-41 Slice 2b). Strings allocated
|
|
489
|
+
# before tracing started report `(pre-trace)`.
|
|
490
|
+
def write_string_allocation_sites
|
|
491
|
+
return if ENV["RIGOR_HEAP_TRACE"].to_s.empty?
|
|
492
|
+
|
|
493
|
+
by_site = Hash.new(0)
|
|
494
|
+
ObjectSpace.each_object(String) do |str|
|
|
495
|
+
file = ObjectSpace.allocation_sourcefile(str)
|
|
496
|
+
line = ObjectSpace.allocation_sourceline(str)
|
|
497
|
+
by_site[file ? "#{file}:#{line}" : "(pre-trace)"] += 1
|
|
498
|
+
end
|
|
499
|
+
@err.puts("")
|
|
500
|
+
@err.puts(" String allocation sites (top 25 by live count)")
|
|
501
|
+
by_site.sort_by { |_, n| -n }.first(25).each do |site, n|
|
|
502
|
+
@err.puts(" #{n.to_s.rjust(9)} #{site}")
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Walks the whole live heap (after a forced GC) and tallies
|
|
507
|
+
# `{class_name => [count, memsize]}` plus the grand total. Returns
|
|
508
|
+
# `[by_class, total]`. Slow — a dev probe only.
|
|
509
|
+
def tally_live_heap
|
|
510
|
+
require "objspace"
|
|
511
|
+
GC.start
|
|
512
|
+
by_class = Hash.new { |h, k| h[k] = [0, 0] }
|
|
513
|
+
total = 0
|
|
514
|
+
ObjectSpace.each_object do |obj|
|
|
515
|
+
size = ObjectSpace.memsize_of(obj)
|
|
516
|
+
entry = by_class[heap_class_name(obj)]
|
|
517
|
+
entry[0] += 1
|
|
518
|
+
entry[1] += size
|
|
519
|
+
total += size
|
|
520
|
+
end
|
|
521
|
+
[by_class, total]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def heap_class_name(obj)
|
|
525
|
+
klass = Object.instance_method(:class).bind_call(obj)
|
|
526
|
+
klass.name || klass.inspect
|
|
527
|
+
rescue StandardError
|
|
528
|
+
"(unknown)"
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def heap_mb(bytes)
|
|
532
|
+
Kernel.format("%.1f MB", bytes / 1_048_576.0)
|
|
533
|
+
end
|
|
534
|
+
|
|
404
535
|
def write_cache_stats(cache_root, runtime_store)
|
|
405
536
|
inv = Cache::Store.disk_inventory(root: cache_root)
|
|
406
537
|
|
|
@@ -642,6 +773,12 @@ module Rigor
|
|
|
642
773
|
CLI::SkillCommand.new(argv: @argv, out: @out, err: @err).run
|
|
643
774
|
end
|
|
644
775
|
|
|
776
|
+
def run_plugin
|
|
777
|
+
require_relative "cli/plugin_command"
|
|
778
|
+
|
|
779
|
+
CLI::PluginCommand.new(argv: @argv, out: @out, err: @err).run
|
|
780
|
+
end
|
|
781
|
+
|
|
645
782
|
def write_result(result, format)
|
|
646
783
|
case format
|
|
647
784
|
when "json"
|
|
@@ -688,6 +825,7 @@ module Rigor
|
|
|
688
825
|
triage Summarise diagnostics: distribution, hotspots, hints (ADR-23)
|
|
689
826
|
coverage Report type-precision coverage (precise vs Dynamic ratio)
|
|
690
827
|
plugins Report activation status of every configured plugin
|
|
828
|
+
plugin Browse bundled plugin source as worked examples (list/path/print/root)
|
|
691
829
|
playground Start the browser playground (requires rigor-playground gem)
|
|
692
830
|
skill List or print bundled Agent Skills (rigor-project-init, ...)
|
|
693
831
|
version Print the Rigor version
|
|
@@ -51,6 +51,9 @@ module Rigor
|
|
|
51
51
|
"dump.type" => :info,
|
|
52
52
|
"def.return-type-mismatch" => :warning,
|
|
53
53
|
"def.method-visibility-mismatch" => :warning,
|
|
54
|
+
"def.override-visibility-reduced" => :off,
|
|
55
|
+
"def.override-return-widened" => :off,
|
|
56
|
+
"def.override-param-narrowed" => :off,
|
|
54
57
|
"def.ivar-write-mismatch" => :warning
|
|
55
58
|
}.freeze,
|
|
56
59
|
balanced: {
|
|
@@ -67,6 +70,9 @@ module Rigor
|
|
|
67
70
|
"dump.type" => :info,
|
|
68
71
|
"def.return-type-mismatch" => :warning,
|
|
69
72
|
"def.method-visibility-mismatch" => :error,
|
|
73
|
+
"def.override-visibility-reduced" => :warning,
|
|
74
|
+
"def.override-return-widened" => :warning,
|
|
75
|
+
"def.override-param-narrowed" => :warning,
|
|
70
76
|
"def.ivar-write-mismatch" => :warning
|
|
71
77
|
}.freeze,
|
|
72
78
|
strict: {
|
|
@@ -83,6 +89,9 @@ module Rigor
|
|
|
83
89
|
"dump.type" => :error,
|
|
84
90
|
"def.return-type-mismatch" => :error,
|
|
85
91
|
"def.method-visibility-mismatch" => :error,
|
|
92
|
+
"def.override-visibility-reduced" => :error,
|
|
93
|
+
"def.override-return-widened" => :error,
|
|
94
|
+
"def.override-param-narrowed" => :error,
|
|
86
95
|
"def.ivar-write-mismatch" => :error
|
|
87
96
|
}.freeze
|
|
88
97
|
}.freeze
|
|
@@ -30,6 +30,24 @@ module Rigor
|
|
|
30
30
|
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
31
31
|
# rubocop:disable Metrics/ClassLength
|
|
32
32
|
class RbsLoader
|
|
33
|
+
# Buffer name stamped on the `module` declarations synthesized by
|
|
34
|
+
# {.synthesize_missing_namespaces}. Re-read off the built env by
|
|
35
|
+
# {#synthesized_namespaces} so the analysis layer can surface an
|
|
36
|
+
# `:info` diagnostic naming the project's malformed-RBS namespaces
|
|
37
|
+
# — robust across the marshalled env cache, since the sentinel
|
|
38
|
+
# rides along on each synthetic declaration's location.
|
|
39
|
+
SYNTHETIC_NAMESPACE_BUFFER = "(rigor: synthesized namespaces)"
|
|
40
|
+
|
|
41
|
+
# Buffer name stamped on the stub `class` / `module` declarations
|
|
42
|
+
# synthesized by {.stub_missing_referenced_types} for types the
|
|
43
|
+
# project's RBS references but no loaded signature declares.
|
|
44
|
+
# {#synthesized_stub_types} reads them back off the built env (so
|
|
45
|
+
# the answer survives the marshalled env cache), and
|
|
46
|
+
# {#synthesized_type_names} folds them together with the
|
|
47
|
+
# namespace stubs into the set {MethodDispatcher} resolves to
|
|
48
|
+
# `Dynamic[Top]` (no false `call.undefined-method`).
|
|
49
|
+
SYNTHETIC_STUB_BUFFER = "(rigor: synthesized stub types)"
|
|
50
|
+
|
|
33
51
|
class << self
|
|
34
52
|
def default
|
|
35
53
|
@default ||= new.freeze
|
|
@@ -72,7 +90,172 @@ module Rigor
|
|
|
72
90
|
end
|
|
73
91
|
env = RBS::Environment.from_loader(rbs_loader)
|
|
74
92
|
add_virtual_rbs(env, virtual_rbs)
|
|
75
|
-
env
|
|
93
|
+
synthesize_missing_namespaces(env)
|
|
94
|
+
resolved = env.resolve_type_names
|
|
95
|
+
stub_missing_referenced_types(env, resolved, project_sig_files(signature_paths))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# ADR-5 robustness, second tier. A project `signature_paths:`
|
|
99
|
+
# RBS that *references* a type no loaded signature declares —
|
|
100
|
+
# `def x: () -> DRb::DRbServer` when the `drb` RBS is not
|
|
101
|
+
# available, or a stale reference to its own removed
|
|
102
|
+
# `Textbringer::EditorError` — makes
|
|
103
|
+
# `RBS::DefinitionBuilder#build_instance` raise
|
|
104
|
+
# `NoTypeFoundError`, and (per RBS's all-or-nothing per-class
|
|
105
|
+
# build) that single unresolved reference takes down EVERY
|
|
106
|
+
# method on the class, not just the one signature. Observed on
|
|
107
|
+
# shugo/textbringer: one `DRb::DRbServer` reference left the
|
|
108
|
+
# whole `Textbringer::Commands` module — including its
|
|
109
|
+
# 186-call-site `define_command` DSL — resolving as
|
|
110
|
+
# `Dynamic[Top]`.
|
|
111
|
+
#
|
|
112
|
+
# We synthesize an empty stub for each such referenced-but-
|
|
113
|
+
# undeclared type so the rest of the class builds. A leaf type
|
|
114
|
+
# is stubbed as `class`, its enclosing namespaces as `module`.
|
|
115
|
+
# Stubbed types carry no methods, so a call against a value of
|
|
116
|
+
# a stubbed type would otherwise mis-fire `call.undefined-method`;
|
|
117
|
+
# {MethodDispatcher} consults {#synthesized_type_names} and
|
|
118
|
+
# resolves such calls to `Dynamic[Top]` instead (the same
|
|
119
|
+
# no-false-positive contract as the dependency-source tier).
|
|
120
|
+
#
|
|
121
|
+
# Detection re-uses RBS's own builder (correct by construction):
|
|
122
|
+
# build every PROJECT class and read the missing name out of the
|
|
123
|
+
# raised error. Bounded to `signature_paths` classes (stdlib /
|
|
124
|
+
# vendored RBS is well-formed) and to {MAX_STUB_PASSES}
|
|
125
|
+
# iterations — a fresh stub can expose a deeper reference the
|
|
126
|
+
# first build error hid, but empty stubs reference nothing, so
|
|
127
|
+
# the fixpoint converges quickly.
|
|
128
|
+
MAX_STUB_PASSES = 5
|
|
129
|
+
|
|
130
|
+
def stub_missing_referenced_types(base_env, resolved, project_files)
|
|
131
|
+
return resolved if project_files.empty?
|
|
132
|
+
|
|
133
|
+
MAX_STUB_PASSES.times do
|
|
134
|
+
missing = unresolved_referenced_types(resolved, project_files)
|
|
135
|
+
break if missing.empty?
|
|
136
|
+
|
|
137
|
+
append_stub_declarations(base_env, missing)
|
|
138
|
+
resolved = base_env.resolve_type_names
|
|
139
|
+
end
|
|
140
|
+
resolved
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Robustness (ADR-5): a project whose RBS declares qualified
|
|
144
|
+
# names (`class Foo::Bar`) without ever declaring the enclosing
|
|
145
|
+
# namespace (`module Foo`) is invalid by upstream RBS rules —
|
|
146
|
+
# `RBS::DefinitionBuilder#build_instance` raises
|
|
147
|
+
# `NoTypeFoundError: Could not find ::Foo`, which the loader's
|
|
148
|
+
# fail-soft rescue turns into a silent dispatch miss (every
|
|
149
|
+
# method on every such class degrades to `Dynamic[Top]`). This
|
|
150
|
+
# is a common authoring mistake (e.g. shugo/textbringer ships a
|
|
151
|
+
# `sig/` that `rbs validate` itself rejects). Rather than let an
|
|
152
|
+
# otherwise-usable signature set contribute nothing, synthesize
|
|
153
|
+
# an empty `module` declaration for each undeclared enclosing
|
|
154
|
+
# namespace so the definitions build. We only ever add names
|
|
155
|
+
# that are absent — a genuinely-declared namespace (module or
|
|
156
|
+
# class, here or in a loaded gem) is left untouched.
|
|
157
|
+
def synthesize_missing_namespaces(env)
|
|
158
|
+
missing = collect_missing_namespaces(env)
|
|
159
|
+
return if missing.empty?
|
|
160
|
+
|
|
161
|
+
source = missing.map { |name| "module #{name}\nend\n" }.join
|
|
162
|
+
buffer = ::RBS::Buffer.new(name: SYNTHETIC_NAMESPACE_BUFFER, content: source)
|
|
163
|
+
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
164
|
+
env.add_source(::RBS::Source::RBS.new(buffer, directives || [], decls || []))
|
|
165
|
+
rescue ::RBS::BaseError
|
|
166
|
+
# Fail-soft: synthesis is an opportunistic uplift, never a
|
|
167
|
+
# hard requirement. A parse failure here just leaves the env
|
|
168
|
+
# as it was (dispatch misses on the affected classes).
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Returns the `::`-stripped names of every enclosing namespace
|
|
173
|
+
# that some declaration references but no declaration defines,
|
|
174
|
+
# shallowest-first so the synthesized source declares `Foo`
|
|
175
|
+
# before `Foo::Bar`.
|
|
176
|
+
def collect_missing_namespaces(env)
|
|
177
|
+
declared = env.class_decls.keys.to_set
|
|
178
|
+
missing = {}
|
|
179
|
+
env.class_decls.each_key do |type_name|
|
|
180
|
+
path = type_name.namespace.path
|
|
181
|
+
path.each_index do |i|
|
|
182
|
+
prefix = path[0..i]
|
|
183
|
+
full = ::RBS::TypeName.parse("::#{prefix.join('::')}")
|
|
184
|
+
missing[prefix.join("::")] = prefix.length unless declared.include?(full)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
missing.sort_by { |_name, depth| depth }.map(&:first)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The absolute paths of every `.rbs` file under the project's
|
|
191
|
+
# `signature_paths:` (NOT vendored / stdlib RBS — those are
|
|
192
|
+
# well-formed, so attempting to build them would only waste
|
|
193
|
+
# time). Used to scope the referenced-type build sweep.
|
|
194
|
+
def project_sig_files(signature_paths)
|
|
195
|
+
signature_paths.flat_map do |path|
|
|
196
|
+
path = Pathname(path) unless path.is_a?(Pathname)
|
|
197
|
+
next [] unless path.directory?
|
|
198
|
+
|
|
199
|
+
Dir.glob(path.join("**", "*.rbs")).map { |p| File.expand_path(p) }
|
|
200
|
+
end.to_set
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Builds every project class (instance + singleton side) and
|
|
204
|
+
# returns the `::`-stripped names of the types whose absence
|
|
205
|
+
# raised `NoTypeFoundError`. Only the FIRST missing reference
|
|
206
|
+
# per class surfaces per build, which is why the caller loops.
|
|
207
|
+
def unresolved_referenced_types(env, project_files)
|
|
208
|
+
builder = ::RBS::DefinitionBuilder.new(env: env)
|
|
209
|
+
missing = []
|
|
210
|
+
env.class_decls.each do |type_name, entry|
|
|
211
|
+
next unless project_entry?(entry, project_files)
|
|
212
|
+
|
|
213
|
+
%i[build_instance build_singleton].each do |build|
|
|
214
|
+
builder.public_send(build, type_name)
|
|
215
|
+
rescue ::RBS::NoTypeFoundError => e
|
|
216
|
+
name = e.message[/Could not find (\S+)/, 1]
|
|
217
|
+
missing << name.sub(/\A::/, "") if name
|
|
218
|
+
rescue ::RBS::BaseError
|
|
219
|
+
# Other build failures (duplicate decl, mixin cycle, ...)
|
|
220
|
+
# are not ours to repair here — leave them fail-soft.
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
missing.uniq
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# True when a `class_decls` entry was declared in one of the
|
|
227
|
+
# project's own signature files (by declaration location), so
|
|
228
|
+
# the sweep skips the bundled stdlib / vendored universe.
|
|
229
|
+
def project_entry?(entry, project_files)
|
|
230
|
+
decl = entry.respond_to?(:primary_decl) ? entry.primary_decl : nil
|
|
231
|
+
location = decl&.location
|
|
232
|
+
buffer_name = location&.buffer&.name
|
|
233
|
+
return false unless buffer_name
|
|
234
|
+
|
|
235
|
+
project_files.include?(File.expand_path(buffer_name.to_s))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Adds empty stub declarations for the missing referenced types
|
|
239
|
+
# (and any enclosing namespace they need) to the pre-resolve
|
|
240
|
+
# env, tagged with {SYNTHETIC_STUB_BUFFER}. A name that is a
|
|
241
|
+
# prefix of another name is declared `module` (it is a
|
|
242
|
+
# namespace); a leaf is declared `class` (referenced types
|
|
243
|
+
# appear in instance position far more often than as mixins).
|
|
244
|
+
def append_stub_declarations(base_env, missing)
|
|
245
|
+
names = missing.to_set
|
|
246
|
+
missing.each do |name|
|
|
247
|
+
parts = name.split("::")
|
|
248
|
+
(1...parts.length).each { |i| names << parts[0, i].join("::") }
|
|
249
|
+
end
|
|
250
|
+
source = names.sort_by { |n| n.count(":") }.map do |name|
|
|
251
|
+
keyword = names.any? { |other| other != name && other.start_with?("#{name}::") } ? "module" : "class"
|
|
252
|
+
"#{keyword} #{name}\nend\n"
|
|
253
|
+
end.join
|
|
254
|
+
buffer = ::RBS::Buffer.new(name: SYNTHETIC_STUB_BUFFER, content: source)
|
|
255
|
+
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
256
|
+
base_env.add_source(::RBS::Source::RBS.new(buffer, directives || [], decls || []))
|
|
257
|
+
rescue ::RBS::BaseError
|
|
258
|
+
nil
|
|
76
259
|
end
|
|
77
260
|
|
|
78
261
|
# ADR-32 WD4 — merge synthesised-from-source RBS strings
|
|
@@ -192,6 +375,40 @@ module Rigor
|
|
|
192
375
|
@hierarchy = RbsHierarchy.new(self)
|
|
193
376
|
end
|
|
194
377
|
|
|
378
|
+
# The enclosing namespaces {.synthesize_missing_namespaces} had to
|
|
379
|
+
# invent because the project's `signature_paths:` RBS declared
|
|
380
|
+
# qualified names (`class Foo::Bar`) without ever declaring `Foo`.
|
|
381
|
+
# Recovered by scanning the built env for class/module entries
|
|
382
|
+
# whose every declaration originated from the synthetic buffer, so
|
|
383
|
+
# the answer survives the marshalled-env cache (where no build-time
|
|
384
|
+
# collector would). Returns `::`-stripped names, shallowest-first.
|
|
385
|
+
# Empty for a well-formed sig set (the common case) and whenever
|
|
386
|
+
# the env failed to build.
|
|
387
|
+
def synthesized_namespaces
|
|
388
|
+
names_synthesized_in(SYNTHETIC_NAMESPACE_BUFFER)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# The referenced-but-undeclared types
|
|
392
|
+
# {.stub_missing_referenced_types} stubbed so the project classes
|
|
393
|
+
# that mention them could build (e.g. an unavailable
|
|
394
|
+
# `DRb::DRbServer`, or a stale `Textbringer::EditorError`).
|
|
395
|
+
# Recovered off the built env like {#synthesized_namespaces}, so
|
|
396
|
+
# it survives the marshalled-env cache.
|
|
397
|
+
def synthesized_stub_types
|
|
398
|
+
names_synthesized_in(SYNTHETIC_STUB_BUFFER)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Every type name Rigor invented to make an otherwise-inert /
|
|
402
|
+
# unbuildable project signature set resolve — both the namespace
|
|
403
|
+
# stubs and the referenced-type stubs. {MethodDispatcher} resolves
|
|
404
|
+
# a call whose receiver is one of these (and that no real
|
|
405
|
+
# signature answered) to `Dynamic[Top]`, so the empty stub never
|
|
406
|
+
# mis-fires `call.undefined-method`. Memoised; empty (and cheap)
|
|
407
|
+
# for the common well-formed sig set.
|
|
408
|
+
def synthesized_type_names
|
|
409
|
+
@state[:synthesized_type_names] ||= (synthesized_namespaces + synthesized_stub_types).to_set
|
|
410
|
+
end
|
|
411
|
+
|
|
195
412
|
# Returns true when an RBS class or module declaration with the given
|
|
196
413
|
# name is loaded. Accepts unprefixed or top-level-prefixed names
|
|
197
414
|
# ("Integer" or "::Integer"). Memoized per-name (positive and
|
|
@@ -546,6 +763,47 @@ module Rigor
|
|
|
546
763
|
|
|
547
764
|
private
|
|
548
765
|
|
|
766
|
+
# The `::`-stripped names of every class/module entry whose
|
|
767
|
+
# declarations ALL originated from the given sentinel buffer —
|
|
768
|
+
# i.e. names Rigor synthesized, not names the project declared.
|
|
769
|
+
# Reads off the built env so the answer survives the marshalled
|
|
770
|
+
# env cache; shallowest-first. Empty when the env failed to build.
|
|
771
|
+
def names_synthesized_in(buffer_name)
|
|
772
|
+
e = env
|
|
773
|
+
return [] if e.nil?
|
|
774
|
+
|
|
775
|
+
names = e.class_decls.filter_map do |type_name, entry|
|
|
776
|
+
decls = entry_declarations(entry)
|
|
777
|
+
next if decls.empty?
|
|
778
|
+
next unless decls.all? { |decl| synthetic_decl?(decl, buffer_name) }
|
|
779
|
+
|
|
780
|
+
type_name.to_s.sub(/\A::/, "")
|
|
781
|
+
end
|
|
782
|
+
names.sort_by { |name| name.count("::") }
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
# Collects the AST declaration nodes behind a `class_decls`
|
|
786
|
+
# entry. RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl`;
|
|
787
|
+
# the older single-`decl` shape is handled defensively so the
|
|
788
|
+
# loader survives an rbs-gem minor bump.
|
|
789
|
+
def entry_declarations(entry)
|
|
790
|
+
if entry.respond_to?(:each_decl)
|
|
791
|
+
[].tap { |acc| entry.each_decl { |decl| acc << decl } }
|
|
792
|
+
elsif entry.respond_to?(:decl)
|
|
793
|
+
[entry.decl]
|
|
794
|
+
else
|
|
795
|
+
[]
|
|
796
|
+
end
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# True when an AST declaration was emitted into `buffer_name`
|
|
800
|
+
# (one of the synthetic-source sentinels) — identified by the
|
|
801
|
+
# buffer name on its location.
|
|
802
|
+
def synthetic_decl?(decl, buffer_name)
|
|
803
|
+
location = decl.respond_to?(:location) ? decl.location : nil
|
|
804
|
+
location&.buffer&.name.to_s == buffer_name
|
|
805
|
+
end
|
|
806
|
+
|
|
549
807
|
def constant_type_table
|
|
550
808
|
@constant_type_table ||= begin
|
|
551
809
|
require_relative "../cache/rbs_constant_table"
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -512,8 +512,14 @@ module Rigor
|
|
|
512
512
|
# presence check without materialising a type carrier.
|
|
513
513
|
def class_known?(name)
|
|
514
514
|
return true if class_registry.nominal_for_name(name)
|
|
515
|
-
|
|
516
|
-
|
|
515
|
+
return true if class_known_in_rbs?(name)
|
|
516
|
+
|
|
517
|
+
# ADR-36 nested-class emission — a variant subclass the
|
|
518
|
+
# substrate synthesised (e.g. `Shape::Circle` from a
|
|
519
|
+
# `variants do variant Circle, Float end` block) is a real
|
|
520
|
+
# class for resolution purposes even though no RBS / source
|
|
521
|
+
# declares it.
|
|
522
|
+
@synthetic_method_index&.knows_class?(name) || false
|
|
517
523
|
end
|
|
518
524
|
|
|
519
525
|
# ADR-15 Phase 2b — returns the loader's read-only,
|