rigortype 0.1.3 → 0.1.5

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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +154 -33
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +26 -6
  12. data/lib/rigor/analysis/result.rb +11 -3
  13. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  14. data/lib/rigor/analysis/run_stats.rb +193 -0
  15. data/lib/rigor/analysis/runner.rb +498 -12
  16. data/lib/rigor/analysis/worker_session.rb +327 -0
  17. data/lib/rigor/builtins/imported_refinements.rb +364 -55
  18. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  19. data/lib/rigor/cache/descriptor.rb +1 -1
  20. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  21. data/lib/rigor/cache/store.rb +39 -6
  22. data/lib/rigor/cli/diff_command.rb +1 -1
  23. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  24. data/lib/rigor/cli/type_of_command.rb +1 -1
  25. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  26. data/lib/rigor/cli/type_scan_report.rb +2 -2
  27. data/lib/rigor/cli.rb +61 -3
  28. data/lib/rigor/configuration/dependencies.rb +2 -2
  29. data/lib/rigor/configuration.rb +131 -6
  30. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  31. data/lib/rigor/environment/class_registry.rb +12 -3
  32. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  33. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  34. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  35. data/lib/rigor/environment/rbs_loader.rb +194 -6
  36. data/lib/rigor/environment/reflection.rb +152 -0
  37. data/lib/rigor/environment.rb +109 -6
  38. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  39. data/lib/rigor/flow_contribution/element.rb +1 -1
  40. data/lib/rigor/flow_contribution/fact.rb +1 -1
  41. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  42. data/lib/rigor/flow_contribution/merger.rb +3 -3
  43. data/lib/rigor/flow_contribution.rb +2 -2
  44. data/lib/rigor/inference/acceptance.rb +35 -1
  45. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  46. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  47. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  48. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  49. data/lib/rigor/inference/expression_typer.rb +77 -11
  50. data/lib/rigor/inference/fallback.rb +1 -1
  51. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  52. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  53. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -41
  54. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  55. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  56. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  57. data/lib/rigor/inference/method_dispatcher/method_folding.rb +135 -0
  58. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +7 -12
  59. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  60. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -44
  61. data/lib/rigor/inference/method_dispatcher.rb +274 -5
  62. data/lib/rigor/inference/method_parameter_binder.rb +22 -14
  63. data/lib/rigor/inference/narrowing.rb +129 -12
  64. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  65. data/lib/rigor/inference/scope_indexer.rb +14 -9
  66. data/lib/rigor/inference/statement_evaluator.rb +7 -7
  67. data/lib/rigor/inference/synthetic_method.rb +86 -0
  68. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  69. data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
  70. data/lib/rigor/plugin/blueprint.rb +60 -0
  71. data/lib/rigor/plugin/io_boundary.rb +0 -2
  72. data/lib/rigor/plugin/loader.rb +5 -3
  73. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  74. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  75. data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
  76. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  77. data/lib/rigor/plugin/macro.rb +31 -0
  78. data/lib/rigor/plugin/manifest.rb +102 -10
  79. data/lib/rigor/plugin/registry.rb +43 -2
  80. data/lib/rigor/plugin/services.rb +1 -1
  81. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  82. data/lib/rigor/plugin.rb +2 -0
  83. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  84. data/lib/rigor/rbs_extended.rb +131 -32
  85. data/lib/rigor/scope.rb +25 -8
  86. data/lib/rigor/sig_gen/classification.rb +36 -0
  87. data/lib/rigor/sig_gen/generator.rb +1048 -0
  88. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  89. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  90. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  91. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  92. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  93. data/lib/rigor/sig_gen/renderer.rb +157 -0
  94. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  95. data/lib/rigor/sig_gen/write_result.rb +48 -0
  96. data/lib/rigor/sig_gen/writer.rb +530 -0
  97. data/lib/rigor/sig_gen.rb +25 -0
  98. data/lib/rigor/trinary.rb +15 -11
  99. data/lib/rigor/type/bot.rb +6 -3
  100. data/lib/rigor/type/bound_method.rb +79 -0
  101. data/lib/rigor/type/combinator.rb +207 -3
  102. data/lib/rigor/type/constant.rb +13 -0
  103. data/lib/rigor/type/hash_shape.rb +0 -2
  104. data/lib/rigor/type/integer_range.rb +7 -7
  105. data/lib/rigor/type/refined.rb +18 -12
  106. data/lib/rigor/type/top.rb +4 -3
  107. data/lib/rigor/type/union.rb +20 -1
  108. data/lib/rigor/type.rb +1 -0
  109. data/lib/rigor/type_node/generic.rb +68 -0
  110. data/lib/rigor/type_node/identifier.rb +38 -0
  111. data/lib/rigor/type_node/indexed_access.rb +41 -0
  112. data/lib/rigor/type_node/integer_literal.rb +29 -0
  113. data/lib/rigor/type_node/name_scope.rb +52 -0
  114. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  115. data/lib/rigor/type_node/string_literal.rb +32 -0
  116. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  117. data/lib/rigor/type_node/union.rb +42 -0
  118. data/lib/rigor/type_node.rb +29 -0
  119. data/lib/rigor/version.rb +1 -1
  120. data/lib/rigor.rb +2 -0
  121. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  122. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  123. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  124. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  125. data/sig/rigor/cli/diff_command.rbs +4 -0
  126. data/sig/rigor/cli/explain_command.rbs +4 -0
  127. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  128. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  129. data/sig/rigor/environment.rbs +8 -2
  130. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  131. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  132. data/sig/rigor/inference/builtins.rbs +2 -0
  133. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  134. data/sig/rigor/plugin/base.rbs +6 -0
  135. data/sig/rigor/plugin/blueprint.rbs +7 -0
  136. data/sig/rigor/plugin/fact_store.rbs +11 -0
  137. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  138. data/sig/rigor/plugin/load_error.rbs +6 -0
  139. data/sig/rigor/plugin/loader.rbs +20 -0
  140. data/sig/rigor/plugin/manifest.rbs +9 -0
  141. data/sig/rigor/plugin/registry.rbs +16 -0
  142. data/sig/rigor/plugin/services.rbs +3 -0
  143. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  144. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  145. data/sig/rigor/plugin.rbs +8 -0
  146. data/sig/rigor/scope.rbs +4 -2
  147. data/sig/rigor/type.rbs +28 -6
  148. data/sig/rigor.rbs +35 -2
  149. metadata +90 -1
@@ -49,6 +49,13 @@ module Rigor
49
49
  # miss without holding a loader instance, and the
50
50
  # instance-side {#build_env} delegates here so the
51
51
  # implementation stays single-rooted.
52
+ #
53
+ # Vendored gem stubs (`data/vendored_gem_sigs/<gem>/`) are
54
+ # loaded on top of `signature_paths` so the per-gem RBS
55
+ # bundled with Rigor itself is in scope for every analysis
56
+ # run. The gem stubs are intentionally read-only and
57
+ # appended LAST so user-supplied `signature_paths` win on
58
+ # name conflicts.
52
59
  def build_env_for(libraries:, signature_paths:)
53
60
  rbs_loader = RBS::EnvironmentLoader.new
54
61
  libraries.each do |library|
@@ -60,8 +67,32 @@ module Rigor
60
67
  path = Pathname(path) unless path.is_a?(Pathname)
61
68
  rbs_loader.add(path: path) if path.directory?
62
69
  end
70
+ vendored_gem_sig_paths.each do |path|
71
+ rbs_loader.add(path: path) if path.directory?
72
+ end
63
73
  RBS::Environment.from_loader(rbs_loader).resolve_type_names
64
74
  end
75
+
76
+ # Per-gem `data/vendored_gem_sigs/<gem>/` directories that
77
+ # ship with Rigor. Each subdirectory is one gem's RBS surface
78
+ # (the `<gem>.rbs` file is the typical content; `LICENSE.upstream`
79
+ # records provenance). Coverage is deliberately scoped to the
80
+ # native-extension and "everywhere in Rails" gems whose absence
81
+ # dominated `call.undefined-method` noise in the real-world
82
+ # survey at `docs/notes/20260515-real-world-rails-survey.md`.
83
+ VENDORED_GEM_SIGS_ROOT = File.expand_path(
84
+ "../../../data/vendored_gem_sigs",
85
+ __dir__
86
+ )
87
+ private_constant :VENDORED_GEM_SIGS_ROOT
88
+
89
+ def vendored_gem_sig_paths
90
+ return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
91
+
92
+ Dir.children(VENDORED_GEM_SIGS_ROOT).map do |gem_dir|
93
+ Pathname(File.join(VENDORED_GEM_SIGS_ROOT, gem_dir))
94
+ end
95
+ end
65
96
  end
66
97
 
67
98
  attr_reader :libraries, :signature_paths, :cache_store
@@ -122,6 +153,7 @@ module Rigor
122
153
  # it never recurses back through {#class_known?}.
123
154
  def each_known_class_name
124
155
  return enum_for(:each_known_class_name) unless block_given?
156
+ return if env.nil?
125
157
 
126
158
  env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
127
159
  env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
@@ -133,6 +165,38 @@ module Rigor
133
165
  # v0.0.9 cache `Cache::Descriptor` regression did.
134
166
  end
135
167
 
168
+ # Returns a frozen `Hash<String, String>` mapping each loaded
169
+ # class / module name (top-level prefixed) to the file path of
170
+ # its FIRST declaration's RBS source. Used by
171
+ # {Rigor::Analysis::RunStats} to attribute the type universe
172
+ # between "project sig/" (paths under the configured
173
+ # `signature_paths`) and "bundled" (everything else — RBS
174
+ # core, stdlib libraries, gem-bundled RBS). Each value is a
175
+ # frozen `String` so the whole result is `Ractor.shareable?`
176
+ # — the Phase 4b worker pool ships a snapshot back to the
177
+ # coordinator on the first `:prepare` message.
178
+ def class_decl_paths
179
+ return {}.freeze if env.nil?
180
+
181
+ result = {}
182
+ env.class_decls.each do |rbs_name, entry|
183
+ decl = entry.primary_decl
184
+ next if decl.nil?
185
+
186
+ location = decl.location
187
+ next if location.nil?
188
+
189
+ buffer = location.buffer
190
+ name = buffer.respond_to?(:name) ? buffer.name : nil
191
+ next if name.nil?
192
+
193
+ result[rbs_name.to_s.dup.freeze] = name.to_s.dup.freeze
194
+ end
195
+ result.freeze
196
+ rescue ::RBS::BaseError
197
+ {}.freeze
198
+ end
199
+
136
200
  # @return [RBS::Definition, nil] the resolved instance definition
137
201
  # for `class_name`, or nil when the class is unknown or its
138
202
  # definition cannot be built (RBS may raise on broken hierarchies;
@@ -250,6 +314,8 @@ module Rigor
250
314
  # materialises the constant-type table; ordinary callers
251
315
  # should keep using {#constant_type} for point lookups.
252
316
  def constant_names
317
+ return [] if env.nil?
318
+
253
319
  env.constant_decls.keys.map(&:to_s)
254
320
  rescue ::RBS::BaseError
255
321
  []
@@ -262,6 +328,7 @@ module Rigor
262
328
  # back into the cache when `cache_store` is set).
263
329
  def each_constant_decl
264
330
  return enum_for(:each_constant_decl) unless block_given?
331
+ return if env.nil?
265
332
 
266
333
  env.constant_decls.each do |rbs_name, entry|
267
334
  yield rbs_name.to_s, entry
@@ -299,26 +366,109 @@ module Rigor
299
366
  nil
300
367
  end
301
368
 
369
+ # ADR-15 Phase 4b.x — eagerly drives every cached
370
+ # producer so a subsequent worker Ractor can serve all
371
+ # of its RBS queries from the Marshal blob on disk
372
+ # without ever calling `RBS::EnvironmentLoader.new`.
373
+ # The loader path that calls `EnvironmentLoader.new`
374
+ # transitively reads a chain of non-`Ractor.shareable?`
375
+ # module constants
376
+ # (`RBS::EnvironmentLoader::DEFAULT_CORE_ROOT`,
377
+ # `RBS::Repository::DEFAULT_STDLIB_ROOT`,
378
+ # `Gem::Requirement::DefaultRequirement`, …) and trips
379
+ # `Ractor::IsolationError`. Pre-warming the cache on
380
+ # the main Ractor and letting workers consult ONLY the
381
+ # Marshal-loaded blob sidesteps the whole chain.
382
+ #
383
+ # No-op when `cache_store` is nil — without a Store the
384
+ # worker has no choice but to build env via the loader,
385
+ # so the caller MUST ensure pool mode runs with caching
386
+ # enabled. Returns `self` so the call chains cleanly
387
+ # from the `Runner` pre-spawn hook.
388
+ def prewarm
389
+ return self if cache_store.nil?
390
+
391
+ env
392
+ known_class_names_set
393
+ constant_type_table
394
+ type_param_names_table
395
+ ancestor_names_table
396
+ instance_definitions_table
397
+ singleton_definitions_table
398
+ self
399
+ end
400
+
401
+ # ADR-15 Phase 2b — return the loader's read-only
402
+ # query surface as a frozen, `Ractor.shareable?`
403
+ # {Reflection} value object. Built lazily on first
404
+ # access; the loader memoises so repeated calls return
405
+ # the same instance.
406
+ #
407
+ # The Reflection consumes the loader's already-warmed
408
+ # cache producers (or, when no `cache_store` is set,
409
+ # eagerly walks the env). Once constructed, the
410
+ # Reflection carries the derived tables independently
411
+ # and never re-consults the loader — making it safe to
412
+ # share across Ractors while the loader stays per-
413
+ # process / per-Ractor for write-path operations.
414
+ def reflection
415
+ @state[:reflection] ||= begin
416
+ require_relative "reflection"
417
+ Environment::Reflection.new(
418
+ known_class_names: known_class_names_set,
419
+ instance_definitions: instance_definitions_table,
420
+ singleton_definitions: singleton_definitions_table,
421
+ type_param_names: type_param_names_table,
422
+ constant_types: constant_type_table,
423
+ ancestor_names: ancestor_names_table
424
+ )
425
+ end
426
+ end
427
+
302
428
  private
303
429
 
304
430
  def constant_type_table
305
431
  @constant_type_table ||= begin
306
432
  require_relative "../cache/rbs_constant_table"
307
- Cache::RbsConstantTable.fetch(loader: self, store: cache_store)
433
+ fetch_or_compute_producer(Cache::RbsConstantTable)
308
434
  end
309
435
  end
310
436
 
311
437
  def known_class_names_set
312
438
  @known_class_names_set ||= begin
313
439
  require_relative "../cache/rbs_known_class_names"
314
- Cache::RbsKnownClassNames.fetch(loader: self, store: cache_store)
440
+ fetch_or_compute_producer(Cache::RbsKnownClassNames)
315
441
  end
316
442
  end
317
443
 
318
444
  def type_param_names_table
319
445
  @type_param_names_table ||= begin
320
446
  require_relative "../cache/rbs_class_type_param_names"
321
- Cache::RbsClassTypeParamNames.fetch(loader: self, store: cache_store)
447
+ fetch_or_compute_producer(Cache::RbsClassTypeParamNames)
448
+ end
449
+ end
450
+
451
+ # ADR-15 Phase 2b — the `Reflection` build path
452
+ # consumes these tables even when `cache_store` is nil
453
+ # (e.g. tests that build a `Reflection` without a
454
+ # persistent cache). The helper routes through the
455
+ # producer's `.fetch` when a store IS available, and
456
+ # falls back to the producer's `.compute` otherwise.
457
+ def fetch_or_compute_producer(producer)
458
+ return producer.fetch(loader: self, store: cache_store) if cache_store
459
+
460
+ producer.send(:compute, self)
461
+ end
462
+
463
+ # ADR-15 Phase 2b — `Hash<String, Array<String>>` of
464
+ # normalised ancestor chains per class. Consumes the
465
+ # existing `RbsClassAncestorTable` producer when
466
+ # `cache_store` is set; falls back to the producer's
467
+ # `compute` otherwise. Used by {#reflection}.
468
+ def ancestor_names_table
469
+ @ancestor_names_table ||= begin
470
+ require_relative "../cache/rbs_class_ancestor_table"
471
+ fetch_or_compute_producer(Cache::RbsClassAncestorTable)
322
472
  end
323
473
  end
324
474
 
@@ -332,6 +482,8 @@ module Rigor
332
482
  end
333
483
 
334
484
  def translate_constant_decl(rbs_name)
485
+ return nil if env.nil?
486
+
335
487
  entry = env.constant_decls[rbs_name]
336
488
  return nil unless entry
337
489
 
@@ -339,8 +491,41 @@ module Rigor
339
491
  translated unless translated.is_a?(Type::Bot)
340
492
  end
341
493
 
494
+ # The RBS environment for this loader. Memoised both on
495
+ # success AND on failure: when the env build raises
496
+ # (typically `RBS::DuplicatedDeclarationError` because a
497
+ # `signature_paths:` entry redeclares a constant or class
498
+ # already shipped by stdlib RBS), retrying on every
499
+ # subsequent `env` call would re-parse and re-resolve the
500
+ # whole sig set per AST node touched during analysis,
501
+ # multiplying per-file analysis cost by ~100x. Failures
502
+ # short-circuit to `nil` here and are surfaced to the user
503
+ # via `warn_about_env_build_failure_once` so the broken
504
+ # `signature_paths:` entry is identifiable.
342
505
  def env
343
- @state[:env] ||= cache_store ? cached_env : build_env
506
+ return @state[:env] if @state[:env_loaded]
507
+
508
+ @state[:env_loaded] = true
509
+ @state[:env] = cache_store ? cached_env : build_env
510
+ rescue ::RBS::BaseError => e
511
+ warn_about_env_build_failure_once(e)
512
+ @state[:env] = nil
513
+ end
514
+
515
+ def warn_about_env_build_failure_once(error)
516
+ return if @state[:env_build_warned]
517
+
518
+ @state[:env_build_warned] = true
519
+ first_line = error.message.to_s.lines.first.to_s.strip
520
+ warn(
521
+ "rigor: RBS environment build failed: #{error.class}: #{first_line}\n " \
522
+ "Likely cause: a `signature_paths:` entry redeclares a constant or class\n " \
523
+ "already shipped by Rigor's bundled RBS (Ruby core / stdlib / gem-bundled\n " \
524
+ "RBS / `data/vendored_gem_sigs/`). Rigor will continue analyzing with no\n " \
525
+ "RBS env in scope, so most type-of queries will return `Dynamic[top]` and\n " \
526
+ "most rule diagnostics will not fire. Remove the conflicting `.rbs` from\n " \
527
+ "your `signature_paths:` to restore type coverage."
528
+ )
344
529
  end
345
530
 
346
531
  def cached_env
@@ -362,7 +547,7 @@ module Rigor
362
547
  def instance_definitions_table
363
548
  @state[:instance_definitions_table] ||= begin
364
549
  require_relative "../cache/rbs_instance_definitions"
365
- Cache::RbsInstanceDefinitions.fetch(loader: self, store: cache_store)
550
+ fetch_or_compute_producer(Cache::RbsInstanceDefinitions)
366
551
  end
367
552
  end
368
553
 
@@ -373,7 +558,7 @@ module Rigor
373
558
  def singleton_definitions_table
374
559
  @state[:singleton_definitions_table] ||= begin
375
560
  require_relative "../cache/rbs_instance_definitions"
376
- Cache::RbsSingletonDefinitions.fetch(loader: self, store: cache_store)
561
+ fetch_or_compute_producer(Cache::RbsSingletonDefinitions)
377
562
  end
378
563
  end
379
564
 
@@ -398,6 +583,7 @@ module Rigor
398
583
  def build_instance_definition(class_name)
399
584
  rbs_name = parse_type_name(class_name)
400
585
  return nil unless rbs_name
586
+ return nil if env.nil?
401
587
  return nil unless env.class_decls.key?(rbs_name)
402
588
 
403
589
  builder.build_instance(rbs_name)
@@ -408,6 +594,7 @@ module Rigor
408
594
  def build_singleton_definition(class_name)
409
595
  rbs_name = parse_type_name(class_name)
410
596
  return nil unless rbs_name
597
+ return nil if env.nil?
411
598
  return nil unless env.class_decls.key?(rbs_name)
412
599
 
413
600
  builder.build_singleton(rbs_name)
@@ -428,6 +615,7 @@ module Rigor
428
615
  def compute_class_known(name)
429
616
  rbs_name = parse_type_name(name)
430
617
  return false unless rbs_name
618
+ return false if env.nil?
431
619
 
432
620
  # `RBS::Environment#class_decls` after `resolve_type_names`
433
621
  # holds entries for both classes AND modules; the gem unifies
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Environment
5
+ # Frozen, `Ractor.shareable?` read-only RBS query facade.
6
+ # [ADR-15](../../../docs/adr/15-ractor-concurrency.md)
7
+ # Phase 2b extracts the read-only surface of {RbsLoader}
8
+ # into this carrier so future Ractor-isolated workers
9
+ # can share one Reflection across the pool while keeping
10
+ # the per-Ractor mutable accelerator state (per-process
11
+ # memo Hashes) where it belongs.
12
+ #
13
+ # Backing tables (all frozen at construction):
14
+ #
15
+ # - `known_class_names` — `Set<String>` of every
16
+ # class / module / alias name in the loaded RBS
17
+ # environment. Top-level prefixed (`"::Hash"`); plain
18
+ # queries normalise via {#normalise}.
19
+ # - `instance_definitions` —
20
+ # `Hash<String, RBS::Definition>` keyed on
21
+ # `RBS::TypeName#to_s` (top-level prefixed).
22
+ # - `singleton_definitions` — same shape, singleton side.
23
+ # - `type_param_names` —
24
+ # `Hash<String, Array<Symbol>>` of declared type
25
+ # parameters per class.
26
+ # - `constant_types` — `Hash<String, Rigor::Type>` of
27
+ # translated constant declarations.
28
+ # - `ancestor_names` — `Hash<String, Array<String>>` of
29
+ # normalised ancestor chains per class.
30
+ #
31
+ # Each `Reflection` instance is `frozen?` at construction
32
+ # — every cached table is frozen, `self` is frozen.
33
+ # **NOT** `Ractor.shareable?`: the `instance_definitions`
34
+ # / `singleton_definitions` tables hold upstream
35
+ # `RBS::Definition` objects that transitively reference
36
+ # `RBS::Location` (C-extension state that
37
+ # `Ractor.make_shareable` rejects).
38
+ #
39
+ # The Ractor worker pool (ADR-15 Phase 4) sidesteps this
40
+ # by having each worker build ITS OWN `Reflection` from
41
+ # the shared `Cache::Store`. The cross-Ractor sharing
42
+ # point is the Store's on-disk + in-process memo layer,
43
+ # NOT the Reflection itself. Each Reflection is a per-
44
+ # Ractor immutable read-side view; this carrier exists
45
+ # to GUARANTEE the per-worker view never mutates after
46
+ # construction.
47
+ #
48
+ # If a future RBS release makes `RBS::Location`
49
+ # Ractor-shareable, swapping the `freeze` call below for
50
+ # `Ractor.make_shareable(self)` makes the whole carrier
51
+ # cross-Ractor-shareable in one line. Until then, the
52
+ # frozen-read-only contract is the deliverable.
53
+ class Reflection
54
+ attr_reader :known_class_names, :instance_definitions, :singleton_definitions,
55
+ :type_param_names, :constant_types, :ancestor_names
56
+
57
+ def initialize(known_class_names:, instance_definitions:, singleton_definitions:,
58
+ type_param_names:, constant_types:, ancestor_names:)
59
+ @known_class_names = freeze_set(known_class_names)
60
+ @instance_definitions = freeze_hash(instance_definitions)
61
+ @singleton_definitions = freeze_hash(singleton_definitions)
62
+ @type_param_names = freeze_hash(type_param_names)
63
+ @constant_types = freeze_hash(constant_types)
64
+ @ancestor_names = freeze_hash(ancestor_names)
65
+ freeze
66
+ end
67
+
68
+ def class_known?(name)
69
+ @known_class_names.include?(rooted(name))
70
+ end
71
+
72
+ def instance_definition(name)
73
+ @instance_definitions[rooted(name)]
74
+ end
75
+
76
+ def singleton_definition(name)
77
+ @singleton_definitions[rooted(name)]
78
+ end
79
+
80
+ def class_type_param_names(name)
81
+ @type_param_names.fetch(unrooted(name), [])
82
+ end
83
+
84
+ def constant_type(name)
85
+ @constant_types[rooted(name)]
86
+ end
87
+
88
+ # Three-valued `(lhs, rhs)` relation:
89
+ # `:equal` / `:subclass` / `:superclass` / `:disjoint` /
90
+ # `:unknown`. Mirrors {RbsHierarchy#class_ordering}'s
91
+ # contract; the Reflection's frozen ancestor table
92
+ # supports the same queries without any in-process
93
+ # mutation.
94
+ def class_ordering(lhs, rhs)
95
+ lhs = unrooted(lhs)
96
+ rhs = unrooted(rhs)
97
+ return :equal if lhs == rhs
98
+
99
+ lhs_ancestors = @ancestor_names[lhs]
100
+ rhs_ancestors = @ancestor_names[rhs]
101
+ return :unknown if lhs_ancestors.nil? || rhs_ancestors.nil? || lhs_ancestors.empty? || rhs_ancestors.empty?
102
+
103
+ if lhs_ancestors.include?(rhs)
104
+ :subclass
105
+ elsif rhs_ancestors.include?(lhs)
106
+ :superclass
107
+ else
108
+ :disjoint
109
+ end
110
+ end
111
+
112
+ # Yields every known class / module / alias name in
113
+ # the loader's canonical rooted form (`"::Hash"`).
114
+ def each_known_class_name(&)
115
+ @known_class_names.each(&)
116
+ end
117
+
118
+ private
119
+
120
+ # The cached tables use mixed key conventions inherited
121
+ # from the underlying RBS::TypeName surface: the
122
+ # name-set / definition tables / constant table store
123
+ # rooted `"::Foo"` keys; the type-param / ancestor
124
+ # tables store unrooted `"Foo"`. Reflection's queries
125
+ # normalise per-lookup so callers can pass either form.
126
+ def rooted(name)
127
+ s = name.to_s
128
+ s.start_with?("::") ? s : "::#{s}"
129
+ end
130
+
131
+ def unrooted(name)
132
+ name.to_s.delete_prefix("::")
133
+ end
134
+
135
+ def freeze_set(value)
136
+ return value if value.is_a?(Set) && value.frozen?
137
+
138
+ case value
139
+ when Set then value.dup.freeze
140
+ when Array, Hash then Set.new(value).freeze
141
+ else raise ArgumentError, "expected Set / Array / Hash, got #{value.class}"
142
+ end
143
+ end
144
+
145
+ def freeze_hash(value)
146
+ return value if value.frozen?
147
+
148
+ value.dup.freeze
149
+ end
150
+ end
151
+ end
152
+ end
@@ -2,6 +2,14 @@
2
2
 
3
3
  require_relative "environment/class_registry"
4
4
  require_relative "environment/rbs_loader"
5
+ require_relative "environment/reflection"
6
+ require_relative "environment/bundle_sig_discovery"
7
+ require_relative "environment/lockfile_resolver"
8
+ require_relative "environment/rbs_collection_discovery"
9
+ require_relative "environment/rbs_coverage_report"
10
+ require_relative "inference/synthetic_method_index"
11
+ require_relative "type_node/name_scope"
12
+ require_relative "type_node/resolver_chain"
5
13
 
6
14
  module Rigor
7
15
  # The engine's view of the type universe outside the current scope.
@@ -38,10 +46,19 @@ module Rigor
38
46
  pathname optparse json yaml fileutils tempfile tmpdir
39
47
  stringio forwardable digest securerandom
40
48
  uri logger date
49
+ pp delegate observable abbrev find tsort singleton
50
+ shellwords benchmark base64 did_you_mean
51
+ monitor mutex_m timeout
52
+ open3 erb etc ipaddr bigdecimal bigdecimal-math
53
+ prettyprint random-formatter time open-uri resolv
54
+ csv pstore objspace io-console cgi cgi-escape
55
+ strscan
41
56
  prism rbs
42
57
  ].freeze
43
58
 
44
- attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index
59
+ attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
60
+ :rbs_extended_reporter, :boundary_cross_reporter, :name_scope,
61
+ :synthetic_method_index
45
62
 
46
63
  # @param class_registry [Rigor::Environment::ClassRegistry]
47
64
  # @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
@@ -63,11 +80,17 @@ module Rigor
63
80
  # When nil (the default), no dep-source contribution
64
81
  # participates and the dispatcher tier is a no-op.
65
82
  def initialize(class_registry: ClassRegistry.default, rbs_loader: nil,
66
- plugin_registry: nil, dependency_source_index: nil)
83
+ plugin_registry: nil, dependency_source_index: nil,
84
+ rbs_extended_reporter: nil, boundary_cross_reporter: nil,
85
+ synthetic_method_index: nil)
67
86
  @class_registry = class_registry
68
87
  @rbs_loader = rbs_loader
69
88
  @plugin_registry = plugin_registry
70
89
  @dependency_source_index = dependency_source_index
90
+ @rbs_extended_reporter = rbs_extended_reporter
91
+ @boundary_cross_reporter = boundary_cross_reporter
92
+ @synthetic_method_index = synthetic_method_index || Inference::SyntheticMethodIndex::EMPTY
93
+ @name_scope = build_name_scope
71
94
  freeze
72
95
  end
73
96
 
@@ -97,21 +120,69 @@ module Rigor
97
120
  # reflection artefacts) consult the cache. Pass `nil` (the
98
121
  # default) to skip caching for this environment.
99
122
  # @return [Rigor::Environment]
100
- def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil, # rubocop:disable Metrics/ParameterLists
101
- plugin_registry: nil, dependency_source_index: nil)
123
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
124
+ def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil,
125
+ plugin_registry: nil, dependency_source_index: nil,
126
+ rbs_extended_reporter: nil, boundary_cross_reporter: nil,
127
+ bundler_bundle_path: nil, bundler_auto_detect: false,
128
+ bundler_lockfile: nil,
129
+ rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
130
+ synthetic_method_index: nil)
102
131
  resolved_paths = signature_paths || default_signature_paths(root)
132
+ # O4 MVP — append per-gem `sig/` directories discovered
133
+ # under the target project's bundler install root. Empty
134
+ # array when neither an explicit path nor auto-detection
135
+ # finds a bundle. Order: user `signature_paths:` win first
136
+ # (semantic precedence inside `RbsLoader.build_env_for`);
137
+ # gem-shipped sigs append last so user overrides stay
138
+ # authoritative.
139
+ #
140
+ # O4 Layer 3 — when a Gemfile.lock is available (explicit
141
+ # `bundler_lockfile:` or auto-detected next to the project
142
+ # root), use the locked gem set to filter the discovered
143
+ # `sig/` directories. Stale gems in the bundle install
144
+ # tree (out-of-band installs, version drift after a
145
+ # `bundle update`) are silently dropped so only gems the
146
+ # project actually declares contribute RBS.
147
+ locked = LockfileResolver.locked_gems(
148
+ lockfile_path: bundler_lockfile,
149
+ project_root: root,
150
+ auto_detect: bundler_auto_detect
151
+ )
152
+ gem_sig_paths = BundleSigDiscovery.discover(
153
+ bundle_path: bundler_bundle_path,
154
+ project_root: root,
155
+ auto_detect: bundler_auto_detect,
156
+ locked_gems: locked.empty? ? nil : locked
157
+ ).map(&:to_s)
158
+ # O4 Layer 3 slice 2 — when `rbs collection install`
159
+ # has been run for the target project, parse the
160
+ # resulting `rbs_collection.lock.yaml` and feed each
161
+ # gem's `<collection_path>/<name>/<version>/` directory
162
+ # into `signature_paths:`. Stdlib-typed entries are
163
+ # skipped (already covered by `DEFAULT_LIBRARIES`).
164
+ collection_paths = RbsCollectionDiscovery.discover(
165
+ lockfile_path: rbs_collection_lockfile,
166
+ project_root: root,
167
+ auto_detect: rbs_collection_auto_detect
168
+ ).map(&:to_s)
169
+ loader_signature_paths = resolved_paths + gem_sig_paths + collection_paths
103
170
  merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
104
171
  loader = RbsLoader.new(
105
172
  libraries: merged_libraries,
106
- signature_paths: resolved_paths,
173
+ signature_paths: loader_signature_paths,
107
174
  cache_store: cache_store
108
175
  )
109
176
  new(
110
177
  rbs_loader: loader,
111
178
  plugin_registry: plugin_registry,
112
- dependency_source_index: dependency_source_index
179
+ dependency_source_index: dependency_source_index,
180
+ rbs_extended_reporter: rbs_extended_reporter,
181
+ boundary_cross_reporter: boundary_cross_reporter,
182
+ synthetic_method_index: synthetic_method_index
113
183
  )
114
184
  end
185
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
115
186
 
116
187
  private
117
188
 
@@ -175,6 +246,17 @@ module Rigor
175
246
  class_known_in_rbs?(name)
176
247
  end
177
248
 
249
+ # ADR-15 Phase 2b — returns the loader's read-only,
250
+ # `Ractor.shareable?` query surface as a frozen
251
+ # {Environment::Reflection}. Built lazily on first
252
+ # access; subsequent calls return the same instance.
253
+ # Returns `nil` when the environment carries no RBS
254
+ # loader (test-only `Environment.new` without
255
+ # `rbs_loader:`).
256
+ def reflection
257
+ @rbs_loader&.reflection
258
+ end
259
+
178
260
  # Compares two class/module names using analyzer-owned class data.
179
261
  # Returns `:equal`, `:subclass`, `:superclass`, `:disjoint`, or
180
262
  # `:unknown`. The static registry handles built-ins cheaply; the RBS
@@ -204,5 +286,26 @@ module Rigor
204
286
  def normalize_class_name(name)
205
287
  name.to_s.delete_prefix("::")
206
288
  end
289
+
290
+ # ADR-13 slice 3b — composes the per-run plugin-supplied
291
+ # {Rigor::TypeNode::ResolverChain} into a single
292
+ # {Rigor::TypeNode::NameScope} that the RBS::Extended
293
+ # directive parser threads down to the
294
+ # {Rigor::Builtins::ImportedRefinements::Resolver}. Returns
295
+ # `nil` when no plugin contributes a type-node resolver so
296
+ # the parser short-circuits the chain consultation and
297
+ # behaves bit-for-bit like the v0.1.0 → v0.1.3 default.
298
+ def build_name_scope
299
+ return nil if @plugin_registry.nil? || @plugin_registry.empty?
300
+
301
+ resolvers = @plugin_registry.type_node_resolvers
302
+ return nil if resolvers.empty?
303
+
304
+ TypeNode::NameScope.new(
305
+ resolver: TypeNode::ResolverChain.new(resolvers),
306
+ class_context: nil,
307
+ type_alias_table: {}
308
+ )
309
+ end
207
310
  end
208
311
  end
@@ -28,8 +28,8 @@ module Rigor
28
28
  lower_tier_contradiction
29
29
  ].freeze
30
30
 
31
- Conflict = Data.define(:target, :edge, :kind, :reason, :provenances, :message) do
32
- def initialize(target:, edge:, kind:, reason:, provenances:, message:) # rubocop:disable Metrics/ParameterLists
31
+ class Conflict < Data.define(:target, :edge, :kind, :reason, :provenances, :message)
32
+ def initialize(target:, edge:, kind:, reason:, provenances:, message:)
33
33
  unless CONFLICT_VALID_REASONS.include?(reason)
34
34
  raise ArgumentError,
35
35
  "FlowContribution::Conflict reason must be one of " \
@@ -28,7 +28,7 @@ module Rigor
28
28
  role
29
29
  ].freeze
30
30
 
31
- Element = Data.define(:target, :edge, :kind, :payload, :provenance) do
31
+ class Element < Data.define(:target, :edge, :kind, :payload, :provenance)
32
32
  def initialize(target:, edge:, kind:, payload:, provenance:)
33
33
  unless ELEMENT_VALID_EDGES.include?(edge)
34
34
  raise ArgumentError,
@@ -55,7 +55,7 @@ module Rigor
55
55
  # land in the same merge bucket.
56
56
  FACT_VALID_TARGET_KINDS = %i[parameter self].freeze
57
57
 
58
- Fact = Data.define(:target_kind, :target_name, :type, :negative) do
58
+ class Fact < Data.define(:target_kind, :target_name, :type, :negative)
59
59
  def initialize(target_kind:, target_name:, type:, negative: false)
60
60
  unless FACT_VALID_TARGET_KINDS.include?(target_kind)
61
61
  raise ArgumentError,
@@ -42,7 +42,7 @@ module Rigor
42
42
  !@conflicts.empty?
43
43
  end
44
44
 
45
- def empty? # rubocop:disable Metrics/CyclomaticComplexity
45
+ def empty?
46
46
  @return_type.nil? && @truthy_facts.empty? && @falsey_facts.empty? &&
47
47
  @post_return_facts.empty? && @mutations.empty? && @invalidations.empty? &&
48
48
  @exceptional.nil? && @role_conformance.empty?