rigortype 0.1.4 → 0.1.6

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -56
  3. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  4. data/lib/rigor/analysis/check_rules.rb +11 -1
  5. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  6. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  8. data/lib/rigor/analysis/fact_store.rb +15 -3
  9. data/lib/rigor/analysis/project_scan.rb +39 -0
  10. data/lib/rigor/analysis/result.rb +11 -3
  11. data/lib/rigor/analysis/run_stats.rb +193 -0
  12. data/lib/rigor/analysis/runner.rb +681 -19
  13. data/lib/rigor/analysis/worker_session.rb +339 -0
  14. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  15. data/lib/rigor/builtins/imported_refinements.rb +6 -2
  16. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  17. data/lib/rigor/builtins/static_return_refinements.rb +120 -0
  18. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  19. data/lib/rigor/cache/store.rb +72 -9
  20. data/lib/rigor/cli/lsp_command.rb +129 -0
  21. data/lib/rigor/cli/type_of_command.rb +44 -5
  22. data/lib/rigor/cli.rb +122 -10
  23. data/lib/rigor/configuration.rb +168 -7
  24. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  25. data/lib/rigor/environment/class_registry.rb +12 -3
  26. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  27. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  28. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  29. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  30. data/lib/rigor/environment/rbs_loader.rb +238 -7
  31. data/lib/rigor/environment/reflection.rb +152 -0
  32. data/lib/rigor/environment/reporters.rb +40 -0
  33. data/lib/rigor/environment.rb +179 -10
  34. data/lib/rigor/inference/acceptance.rb +83 -4
  35. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  36. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  37. data/lib/rigor/inference/expression_typer.rb +59 -2
  38. data/lib/rigor/inference/hkt_body.rb +171 -0
  39. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  40. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  41. data/lib/rigor/inference/hkt_registry.rb +223 -0
  42. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  43. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
  44. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  45. data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
  46. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +126 -31
  47. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  48. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
  49. data/lib/rigor/inference/method_dispatcher.rb +282 -6
  50. data/lib/rigor/inference/method_parameter_binder.rb +21 -11
  51. data/lib/rigor/inference/narrowing.rb +127 -8
  52. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  53. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  54. data/lib/rigor/inference/scope_indexer.rb +156 -12
  55. data/lib/rigor/inference/statement_evaluator.rb +106 -6
  56. data/lib/rigor/inference/synthetic_method.rb +86 -0
  57. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  58. data/lib/rigor/inference/synthetic_method_scanner.rb +599 -0
  59. data/lib/rigor/language_server/buffer_table.rb +63 -0
  60. data/lib/rigor/language_server/completion_provider.rb +438 -0
  61. data/lib/rigor/language_server/debouncer.rb +86 -0
  62. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  63. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  64. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  65. data/lib/rigor/language_server/hover_provider.rb +74 -0
  66. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  67. data/lib/rigor/language_server/loop.rb +71 -0
  68. data/lib/rigor/language_server/project_context.rb +145 -0
  69. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  70. data/lib/rigor/language_server/server.rb +384 -0
  71. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  72. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  73. data/lib/rigor/language_server/uri.rb +40 -0
  74. data/lib/rigor/language_server.rb +29 -0
  75. data/lib/rigor/plugin/base.rb +63 -0
  76. data/lib/rigor/plugin/blueprint.rb +60 -0
  77. data/lib/rigor/plugin/loader.rb +3 -1
  78. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  79. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  80. data/lib/rigor/plugin/macro/heredoc_template.rb +315 -0
  81. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  82. data/lib/rigor/plugin/macro.rb +31 -0
  83. data/lib/rigor/plugin/manifest.rb +127 -9
  84. data/lib/rigor/plugin/registry.rb +51 -2
  85. data/lib/rigor/plugin.rb +1 -0
  86. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  87. data/lib/rigor/rbs_extended.rb +82 -2
  88. data/lib/rigor/sig_gen/generator.rb +12 -3
  89. data/lib/rigor/trinary.rb +15 -11
  90. data/lib/rigor/type/app.rb +107 -0
  91. data/lib/rigor/type/bot.rb +6 -3
  92. data/lib/rigor/type/combinator.rb +12 -1
  93. data/lib/rigor/type/integer_range.rb +7 -7
  94. data/lib/rigor/type/refined.rb +18 -12
  95. data/lib/rigor/type/top.rb +4 -3
  96. data/lib/rigor/type.rb +1 -0
  97. data/lib/rigor/type_node/generic.rb +7 -1
  98. data/lib/rigor/type_node/identifier.rb +9 -1
  99. data/lib/rigor/type_node/string_literal.rb +4 -1
  100. data/lib/rigor/version.rb +1 -1
  101. data/sig/rigor/environment.rbs +11 -4
  102. data/sig/rigor/inference.rbs +2 -0
  103. data/sig/rigor/plugin/blueprint.rbs +7 -0
  104. data/sig/rigor/plugin/manifest.rbs +1 -1
  105. data/sig/rigor/plugin/registry.rbs +14 -1
  106. data/sig/rigor.rbs +37 -2
  107. metadata +92 -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
+ ).freeze
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
@@ -87,7 +118,20 @@ module Rigor
87
118
  @libraries = libraries.map(&:to_s).freeze
88
119
  @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
89
120
  @cache_store = cache_store
90
- @state = { env: nil, builder: nil }
121
+ # Per-loader memoization bucket. Held as a single
122
+ # mutable Hash so the loader instance itself can be
123
+ # `.freeze`d (per ADR-15 reflection-facade contract)
124
+ # without losing the lazy-memo behaviour. Slot names
125
+ # currently consulted: `:env`, `:env_loaded`,
126
+ # `:env_build_warned`, `:builder`, `:reflection`,
127
+ # `:instance_definitions_table`,
128
+ # `:singleton_definitions_table`. Constructed via
129
+ # `Hash.new` (NOT a `{ ... }` literal) so Rigor's
130
+ # `HashShape` narrowing doesn't infer a fixed key set
131
+ # from the initial state and fold post-initial slot
132
+ # reads (e.g. `@state[:env_loaded]`) to a constant
133
+ # `nil`.
134
+ @state = Hash.new # rubocop:disable Style/EmptyLiteral
91
135
  @instance_definition_cache = {}
92
136
  @singleton_definition_cache = {}
93
137
  @class_known_cache = {}
@@ -122,6 +166,7 @@ module Rigor
122
166
  # it never recurses back through {#class_known?}.
123
167
  def each_known_class_name
124
168
  return enum_for(:each_known_class_name) unless block_given?
169
+ return if env.nil?
125
170
 
126
171
  env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
127
172
  env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
@@ -133,6 +178,68 @@ module Rigor
133
178
  # v0.0.9 cache `Cache::Descriptor` regression did.
134
179
  end
135
180
 
181
+ # ADR-20 slice 2e — iterates over every `%a{...}`
182
+ # annotation attached to a class- or module-level
183
+ # declaration in the loaded RBS environment, yielding
184
+ # `(annotation_string, source_location)` pairs. Used by
185
+ # {Rigor::Inference::HktRegistry.scan_rbs_loader} to
186
+ # find `rigor:v1:hkt_register` / `rigor:v1:hkt_define`
187
+ # directives in user-authored overlays and merge them
188
+ # into the per-`Environment` HKT registry. Yields nothing
189
+ # when the env failed to build (fail-soft, same shape as
190
+ # {#each_known_class_name}).
191
+ def each_class_decl_annotation
192
+ return enum_for(:each_class_decl_annotation) unless block_given?
193
+ return if env.nil?
194
+
195
+ env.class_decls.each_value do |entry|
196
+ entry.each_decl do |decl|
197
+ next unless decl.respond_to?(:annotations)
198
+
199
+ decl.annotations.each { |a| yield a.string, a.location }
200
+ end
201
+ end
202
+ rescue ::RBS::BaseError, ::Ractor::IsolationError
203
+ # fail-soft: matches each_known_class_name's policy.
204
+ # Ractor::IsolationError surfaces when the scan is
205
+ # invoked from a non-main Ractor pool worker before
206
+ # ADR-15's full deep-freeze migration completes — the
207
+ # worker falls back to the base (builtins-only)
208
+ # registry rather than crashing.
209
+ end
210
+
211
+ # Returns a frozen `Hash<String, String>` mapping each loaded
212
+ # class / module name (top-level prefixed) to the file path of
213
+ # its FIRST declaration's RBS source. Used by
214
+ # {Rigor::Analysis::RunStats} to attribute the type universe
215
+ # between "project sig/" (paths under the configured
216
+ # `signature_paths`) and "bundled" (everything else — RBS
217
+ # core, stdlib libraries, gem-bundled RBS). Each value is a
218
+ # frozen `String` so the whole result is `Ractor.shareable?`
219
+ # — the Phase 4b worker pool ships a snapshot back to the
220
+ # coordinator on the first `:prepare` message.
221
+ def class_decl_paths
222
+ return {}.freeze if env.nil?
223
+
224
+ result = {}
225
+ env.class_decls.each do |rbs_name, entry|
226
+ decl = entry.primary_decl
227
+ next if decl.nil?
228
+
229
+ location = decl.location
230
+ next if location.nil?
231
+
232
+ buffer = location.buffer
233
+ name = buffer.respond_to?(:name) ? buffer.name : nil
234
+ next if name.nil?
235
+
236
+ result[rbs_name.to_s.dup.freeze] = name.to_s.dup.freeze
237
+ end
238
+ result.freeze
239
+ rescue ::RBS::BaseError
240
+ {}.freeze
241
+ end
242
+
136
243
  # @return [RBS::Definition, nil] the resolved instance definition
137
244
  # for `class_name`, or nil when the class is unknown or its
138
245
  # definition cannot be built (RBS may raise on broken hierarchies;
@@ -250,6 +357,8 @@ module Rigor
250
357
  # materialises the constant-type table; ordinary callers
251
358
  # should keep using {#constant_type} for point lookups.
252
359
  def constant_names
360
+ return [] if env.nil?
361
+
253
362
  env.constant_decls.keys.map(&:to_s)
254
363
  rescue ::RBS::BaseError
255
364
  []
@@ -262,6 +371,7 @@ module Rigor
262
371
  # back into the cache when `cache_store` is set).
263
372
  def each_constant_decl
264
373
  return enum_for(:each_constant_decl) unless block_given?
374
+ return if env.nil?
265
375
 
266
376
  env.constant_decls.each do |rbs_name, entry|
267
377
  yield rbs_name.to_s, entry
@@ -299,26 +409,109 @@ module Rigor
299
409
  nil
300
410
  end
301
411
 
412
+ # ADR-15 Phase 4b.x — eagerly drives every cached
413
+ # producer so a subsequent worker Ractor can serve all
414
+ # of its RBS queries from the Marshal blob on disk
415
+ # without ever calling `RBS::EnvironmentLoader.new`.
416
+ # The loader path that calls `EnvironmentLoader.new`
417
+ # transitively reads a chain of non-`Ractor.shareable?`
418
+ # module constants
419
+ # (`RBS::EnvironmentLoader::DEFAULT_CORE_ROOT`,
420
+ # `RBS::Repository::DEFAULT_STDLIB_ROOT`,
421
+ # `Gem::Requirement::DefaultRequirement`, …) and trips
422
+ # `Ractor::IsolationError`. Pre-warming the cache on
423
+ # the main Ractor and letting workers consult ONLY the
424
+ # Marshal-loaded blob sidesteps the whole chain.
425
+ #
426
+ # No-op when `cache_store` is nil — without a Store the
427
+ # worker has no choice but to build env via the loader,
428
+ # so the caller MUST ensure pool mode runs with caching
429
+ # enabled. Returns `self` so the call chains cleanly
430
+ # from the `Runner` pre-spawn hook.
431
+ def prewarm
432
+ return self if cache_store.nil?
433
+
434
+ env
435
+ known_class_names_set
436
+ constant_type_table
437
+ type_param_names_table
438
+ ancestor_names_table
439
+ instance_definitions_table
440
+ singleton_definitions_table
441
+ self
442
+ end
443
+
444
+ # ADR-15 Phase 2b — return the loader's read-only
445
+ # query surface as a frozen, `Ractor.shareable?`
446
+ # {Reflection} value object. Built lazily on first
447
+ # access; the loader memoises so repeated calls return
448
+ # the same instance.
449
+ #
450
+ # The Reflection consumes the loader's already-warmed
451
+ # cache producers (or, when no `cache_store` is set,
452
+ # eagerly walks the env). Once constructed, the
453
+ # Reflection carries the derived tables independently
454
+ # and never re-consults the loader — making it safe to
455
+ # share across Ractors while the loader stays per-
456
+ # process / per-Ractor for write-path operations.
457
+ def reflection
458
+ @state[:reflection] ||= begin
459
+ require_relative "reflection"
460
+ Environment::Reflection.new(
461
+ known_class_names: known_class_names_set,
462
+ instance_definitions: instance_definitions_table,
463
+ singleton_definitions: singleton_definitions_table,
464
+ type_param_names: type_param_names_table,
465
+ constant_types: constant_type_table,
466
+ ancestor_names: ancestor_names_table
467
+ )
468
+ end
469
+ end
470
+
302
471
  private
303
472
 
304
473
  def constant_type_table
305
474
  @constant_type_table ||= begin
306
475
  require_relative "../cache/rbs_constant_table"
307
- Cache::RbsConstantTable.fetch(loader: self, store: cache_store)
476
+ fetch_or_compute_producer(Cache::RbsConstantTable)
308
477
  end
309
478
  end
310
479
 
311
480
  def known_class_names_set
312
481
  @known_class_names_set ||= begin
313
482
  require_relative "../cache/rbs_known_class_names"
314
- Cache::RbsKnownClassNames.fetch(loader: self, store: cache_store)
483
+ fetch_or_compute_producer(Cache::RbsKnownClassNames)
315
484
  end
316
485
  end
317
486
 
318
487
  def type_param_names_table
319
488
  @type_param_names_table ||= begin
320
489
  require_relative "../cache/rbs_class_type_param_names"
321
- Cache::RbsClassTypeParamNames.fetch(loader: self, store: cache_store)
490
+ fetch_or_compute_producer(Cache::RbsClassTypeParamNames)
491
+ end
492
+ end
493
+
494
+ # ADR-15 Phase 2b — the `Reflection` build path
495
+ # consumes these tables even when `cache_store` is nil
496
+ # (e.g. tests that build a `Reflection` without a
497
+ # persistent cache). The helper routes through the
498
+ # producer's `.fetch` when a store IS available, and
499
+ # falls back to the producer's `.compute` otherwise.
500
+ def fetch_or_compute_producer(producer)
501
+ return producer.fetch(loader: self, store: cache_store) if cache_store
502
+
503
+ producer.send(:compute, self)
504
+ end
505
+
506
+ # ADR-15 Phase 2b — `Hash<String, Array<String>>` of
507
+ # normalised ancestor chains per class. Consumes the
508
+ # existing `RbsClassAncestorTable` producer when
509
+ # `cache_store` is set; falls back to the producer's
510
+ # `compute` otherwise. Used by {#reflection}.
511
+ def ancestor_names_table
512
+ @ancestor_names_table ||= begin
513
+ require_relative "../cache/rbs_class_ancestor_table"
514
+ fetch_or_compute_producer(Cache::RbsClassAncestorTable)
322
515
  end
323
516
  end
324
517
 
@@ -332,6 +525,8 @@ module Rigor
332
525
  end
333
526
 
334
527
  def translate_constant_decl(rbs_name)
528
+ return nil if env.nil?
529
+
335
530
  entry = env.constant_decls[rbs_name]
336
531
  return nil unless entry
337
532
 
@@ -339,8 +534,41 @@ module Rigor
339
534
  translated unless translated.is_a?(Type::Bot)
340
535
  end
341
536
 
537
+ # The RBS environment for this loader. Memoised both on
538
+ # success AND on failure: when the env build raises
539
+ # (typically `RBS::DuplicatedDeclarationError` because a
540
+ # `signature_paths:` entry redeclares a constant or class
541
+ # already shipped by stdlib RBS), retrying on every
542
+ # subsequent `env` call would re-parse and re-resolve the
543
+ # whole sig set per AST node touched during analysis,
544
+ # multiplying per-file analysis cost by ~100x. Failures
545
+ # short-circuit to `nil` here and are surfaced to the user
546
+ # via `warn_about_env_build_failure_once` so the broken
547
+ # `signature_paths:` entry is identifiable.
342
548
  def env
343
- @state[:env] ||= cache_store ? cached_env : build_env
549
+ return @state[:env] if @state[:env_loaded]
550
+
551
+ @state[:env_loaded] = true
552
+ @state[:env] = cache_store ? cached_env : build_env
553
+ rescue ::RBS::BaseError => e
554
+ warn_about_env_build_failure_once(e)
555
+ @state[:env] = nil
556
+ end
557
+
558
+ def warn_about_env_build_failure_once(error)
559
+ return if @state[:env_build_warned]
560
+
561
+ @state[:env_build_warned] = true
562
+ first_line = error.message.to_s.lines.first.to_s.strip
563
+ warn(
564
+ "rigor: RBS environment build failed: #{error.class}: #{first_line}\n " \
565
+ "Likely cause: a `signature_paths:` entry redeclares a constant or class\n " \
566
+ "already shipped by Rigor's bundled RBS (Ruby core / stdlib / gem-bundled\n " \
567
+ "RBS / `data/vendored_gem_sigs/`). Rigor will continue analyzing with no\n " \
568
+ "RBS env in scope, so most type-of queries will return `Dynamic[top]` and\n " \
569
+ "most rule diagnostics will not fire. Remove the conflicting `.rbs` from\n " \
570
+ "your `signature_paths:` to restore type coverage."
571
+ )
344
572
  end
345
573
 
346
574
  def cached_env
@@ -362,7 +590,7 @@ module Rigor
362
590
  def instance_definitions_table
363
591
  @state[:instance_definitions_table] ||= begin
364
592
  require_relative "../cache/rbs_instance_definitions"
365
- Cache::RbsInstanceDefinitions.fetch(loader: self, store: cache_store)
593
+ fetch_or_compute_producer(Cache::RbsInstanceDefinitions)
366
594
  end
367
595
  end
368
596
 
@@ -373,7 +601,7 @@ module Rigor
373
601
  def singleton_definitions_table
374
602
  @state[:singleton_definitions_table] ||= begin
375
603
  require_relative "../cache/rbs_instance_definitions"
376
- Cache::RbsSingletonDefinitions.fetch(loader: self, store: cache_store)
604
+ fetch_or_compute_producer(Cache::RbsSingletonDefinitions)
377
605
  end
378
606
  end
379
607
 
@@ -398,6 +626,7 @@ module Rigor
398
626
  def build_instance_definition(class_name)
399
627
  rbs_name = parse_type_name(class_name)
400
628
  return nil unless rbs_name
629
+ return nil if env.nil?
401
630
  return nil unless env.class_decls.key?(rbs_name)
402
631
 
403
632
  builder.build_instance(rbs_name)
@@ -408,6 +637,7 @@ module Rigor
408
637
  def build_singleton_definition(class_name)
409
638
  rbs_name = parse_type_name(class_name)
410
639
  return nil unless rbs_name
640
+ return nil if env.nil?
411
641
  return nil unless env.class_decls.key?(rbs_name)
412
642
 
413
643
  builder.build_singleton(rbs_name)
@@ -428,6 +658,7 @@ module Rigor
428
658
  def compute_class_known(name)
429
659
  rbs_name = parse_type_name(name)
430
660
  return false unless rbs_name
661
+ return false if env.nil?
431
662
 
432
663
  # `RBS::Environment#class_decls` after `resolve_type_names`
433
664
  # 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
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Environment
5
+ # Mutable container for the per-run analysis reporters
6
+ # ({Rigor::RbsExtended::Reporter} and
7
+ # {Rigor::Analysis::DependencySourceInference::BoundaryCrossReporter}).
8
+ # Held by {Environment} as a single attr; the reporters can be
9
+ # swapped through {Environment#attach_reporters!} so long-lived
10
+ # integrations (the LSP `ProjectContext`, future editor-mode
11
+ # daemons) can share one Environment across many `Runner.run`
12
+ # calls without each call's diagnostic events accumulating into
13
+ # a single reporter pair.
14
+ #
15
+ # Per-publish reset is the contract: at the start of every
16
+ # `Runner.run` in sequential mode, the runner stamps the
17
+ # environment's `Reporters` slot with the runner's own
18
+ # freshly-built reporter pair. Dispatchers / `RbsExtended`
19
+ # consumers continue to write through
20
+ # `environment.rbs_extended_reporter` /
21
+ # `environment.boundary_cross_reporter` — the lookup just hops
22
+ # through the `Reporters` slot rather than reading a frozen
23
+ # ivar.
24
+ #
25
+ # Construction default is `nil` on both slots so existing
26
+ # callers that don't care about reporters (project-default
27
+ # `Environment.default`, test scopes that don't drive
28
+ # dispatch) keep their current behaviour: reporter lookups
29
+ # return nil, and the consumer sites short-circuit on
30
+ # `reporter.nil?`.
31
+ class Reporters
32
+ attr_accessor :rbs_extended, :boundary_cross
33
+
34
+ def initialize(rbs_extended: nil, boundary_cross: nil)
35
+ @rbs_extended = rbs_extended
36
+ @boundary_cross = boundary_cross
37
+ end
38
+ end
39
+ end
40
+ end