rigortype 0.4.1 → 0.4.3

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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -1
@@ -48,11 +48,13 @@ module Rigor
48
48
  # @param cache_path — the configuration's cache root; nil or empty is an inert
49
49
  # cache (every fetch computes, nothing persists).
50
50
  # @param target_ruby — parse-affecting configuration, part of the identity.
51
- def self.open(cache_path, target_ruby: nil)
51
+ # `root` names the project and working directory a run reads roles and spellings from (#1760): a
52
+ # scan cached by a run from another directory carries another role, so it is never served here.
53
+ def self.open(cache_path, target_ruby: nil, root: nil)
52
54
  return new(path: nil, header: nil) if cache_path.nil? || cache_path.to_s.empty?
53
55
 
54
56
  new(path: File.join(cache_path.to_s, FILE_NAME),
55
- header: "#{SCHEMA}:#{Rigor::VERSION}:#{target_ruby}:#{source_digest}")
57
+ header: "#{SCHEMA}:#{Rigor::VERSION}:#{target_ruby}:#{source_digest}:#{root}")
56
58
  end
57
59
 
58
60
  def self.source_digest
@@ -292,6 +292,38 @@ module Rigor
292
292
  since: "0.0.1"
293
293
  ),
294
294
 
295
+ CheckRules::RULE_DEPRECATED_RUBY2_KEYWORDS => Entry.new(
296
+ id: CheckRules::RULE_DEPRECATED_RUBY2_KEYWORDS,
297
+ summary: "A call to the ruby2_keywords family Ruby 4.1 deprecates, under an explicit target_ruby >= 4.1.",
298
+ fires_when: [
299
+ "`.rigor.yml` sets `target_ruby` explicitly to \"4.1\" or later.",
300
+ "The call reaches `Module#ruby2_keywords` (no receiver or `self` in a class / module body, a " \
301
+ "`class << self` body or a singleton method), top-level `ruby2_keywords` (at the top level, outside " \
302
+ "any block), or `Proc#ruby2_keywords` (a receiver typed `Proc`): removed in Ruby 4.4.",
303
+ "The call reaches `Hash.ruby2_keywords_hash?` or `Hash.ruby2_keywords_hash` (on `Hash` or a subclass " \
304
+ "RBS knows): removed in Ruby 4.5.",
305
+ "The same names through `send` / `__send__` with a literal Symbol."
306
+ ],
307
+ does_not_fire_when: [
308
+ "`target_ruby` is absent, is the default, is below 4.1, or is \"latest\" (which names a parser, " \
309
+ "not a runtime).",
310
+ "A project or `pre_eval:` file defines a method by the same name anywhere.",
311
+ "The call sits in a block at the top level, in an `instance_eval` / `instance_exec` block, in an " \
312
+ "instance method of a class or module, or on an explicit receiver that is not `Proc` / `Hash`.",
313
+ "The call sits in a version-guard arm that cannot run on the stated Ruby, under a guard on " \
314
+ "`RUBY_VERSION` that cannot be decided, or after a guard that returns or raises."
315
+ ],
316
+ suppression: "`# rigor:disable call.deprecated-ruby2-keywords` on the call line. To fix it instead, " \
317
+ "rewrite the delegation the flag serves to `(...)` or `(*args, **kwargs, &block)` first, then " \
318
+ "drop the `ruby2_keywords` call: removing it alone breaks keyword pass-through.",
319
+ severity_authored: :warning,
320
+ severity_by_profile: { lenient: :info, balanced: :warning, strict: :error },
321
+ # Each call names a known core method on a receiver whose class the syntax or the scope fixes, and a
322
+ # project definition of the name silences it. A file loaded only on an older Ruby is the residual.
323
+ evidence_tier: :high,
324
+ since: "0.4.2"
325
+ ),
326
+
295
327
  CheckRules::RULE_ALWAYS_RAISES => Entry.new(
296
328
  id: CheckRules::RULE_ALWAYS_RAISES,
297
329
  summary: "Call provably raises (today: Integer division-by-zero).",
@@ -552,6 +584,9 @@ module Rigor
552
584
  ],
553
585
  does_not_fire_when: [
554
586
  "Override raises or preserves visibility (only reductions break substitutability).",
587
+ "The method is one Ruby makes private wherever it is defined (`initialize`, " \
588
+ "`initialize_copy` / `_dup` / `_clone`, `respond_to_missing?`), so its section is not its " \
589
+ "runtime visibility (ADR-35 WD10).",
555
590
  "The shadowed method lives on an RBS-known / third-party ancestor (RBS models only " \
556
591
  "public/private; RBS-parent visibility is a deferred follow-on).",
557
592
  "`def self.foo` singleton methods (visibility is instance-side only).",
@@ -578,6 +613,9 @@ module Rigor
578
613
  does_not_fire_when: [
579
614
  "Either side lacks an authored RBS signature (WD1 both-sides-authored gate).",
580
615
  "The override narrows or preserves the return (covariant-safe).",
616
+ "`initialize` and the `initialize_copy` / `_dup` / `_clone` hooks: `Class#new` / `dup` / " \
617
+ "`clone` reach them on the receiver's own class and discard their return, outside " \
618
+ "substitutability (ADR-35 WD10).",
581
619
  "The ancestor's return is `untyped` / `self` / an unbound generic (degrades to " \
582
620
  "`Dynamic[Top]`, which accepts everything — FP-safe).",
583
621
  "The subtype relationship between the two return types is not resolvable from loaded " \
@@ -609,6 +647,9 @@ module Rigor
609
647
  does_not_fire_when: [
610
648
  "Either side lacks an authored RBS signature (WD1 both-sides-authored gate).",
611
649
  "The override widens or preserves the parameter (contravariant-safe).",
650
+ "`initialize` and the `initialize_copy` / `_dup` / `_clone` hooks: `Class#new` / `dup` / " \
651
+ "`clone` reach them only on the receiver's own class, so no parent-typed caller passes the " \
652
+ "parent's arguments (ADR-35 WD10).",
612
653
  "Either side is overloaded (more than one method type — arm mapping is ambiguous).",
613
654
  "The ancestor's parameter is `untyped` / an unbound generic / an interface (degrades " \
614
655
  "to `Dynamic[Top]`, which is passable to anything — FP-safe).",
@@ -640,6 +681,8 @@ module Rigor
640
681
  ],
641
682
  does_not_fire_when: [
642
683
  "Later write is `nil` — the `@cache = nil` clear-idiom is allowlisted.",
684
+ "The class's RBS declares the ivar (`@var: T`, or the slot an `attr_*` member implies, own or " \
685
+ "inherited) and that type accepts both the first write and the later one.",
643
686
  "Either side is Union / Dynamic / IntegerRange / a shape-varied carrier.",
644
687
  "Writes live in different classes that happen to share an ivar name.",
645
688
  "Writes are in a singleton body — `def self.foo` or a `class << self` def — those track " \
@@ -21,6 +21,9 @@ require_relative "../environment/default_libraries"
21
21
  # lockfile paths without loading `rigor/environment` or the RBS machinery.
22
22
  require_relative "../environment/lockfile_resolver"
23
23
  require_relative "../environment/rbs_collection_discovery"
24
+ # A leaf file (it loads only the cache descriptor), unlike `dependency_source_inference`, whose walker loads the
25
+ # inference engine the boot-slimming probe exists to avoid.
26
+ require_relative "dependency_source_inference/gem_resolver"
24
27
  require_relative "shadow_harness"
25
28
 
26
29
  module Rigor
@@ -93,6 +96,7 @@ module Rigor
93
96
  def descriptor(configuration:, files:, explain:, rbs_config_entries:, template_units_digest: nil)
94
97
  Cache::Descriptor.new(
95
98
  gems: [Cache::RbsDescriptor.rbs_gem_entry],
99
+ dependencies: dependency_source_entries(configuration),
96
100
  configs: rbs_config_entries + engine_source_entries + lockfile_entries(configuration) +
97
101
  template_unit_entries(template_units_digest) + shadow_harness_entries + [
98
102
  config_entry("configuration", Marshal.dump(configuration.to_h)),
@@ -151,6 +155,14 @@ module Rigor
151
155
  [lockfile_entry("bundler.lockfile", bundler), lockfile_entry("rbs_collection.lockfile", collection)]
152
156
  end
153
157
 
158
+ # ADR-10 slice 3, issue #1814 — the `dependencies.source_inference` gems' `(name, version, mode)` rows, which the
159
+ # walker's output depends on and no lockfile digest covers when the project has none or the gem sits beside the
160
+ # bundle. Shared by this key and by {Cache::IncrementalSnapshot.fingerprint}, as the lockfiles are. A project
161
+ # that lists no gem adds no row, so its key is what it was.
162
+ def dependency_source_entries(configuration)
163
+ DependencySourceInference::GemResolver.dependency_entries(configuration.dependencies)
164
+ end
165
+
154
166
  # The one resolution of the two dependency lockfiles — `[bundler, rbs_collection]`, each a Pathname or nil
155
167
  # — shared by this key and by {Cache::IncrementalSnapshot.fingerprint} so the two caches identify the gem
156
168
  # set by the same files (issue #1532): `bundler.lockfile:` / `rbs_collection.lockfile:` and auto-detection.
@@ -180,7 +192,10 @@ module Rigor
180
192
 
181
193
  # The `rbs.libraries` config slot reconstructed from configuration alone — byte-identical to the
182
194
  # loader's `RbsDescriptor.libraries_entry(loader.libraries)` because `Environment.for_project` merges
183
- # exactly `DEFAULT_LIBRARIES + config.libraries` (uniq) into `loader.libraries`.
195
+ # exactly `DEFAULT_LIBRARIES + config.libraries` (uniq) into `loader.libraries`, plus the
196
+ # `Environment::RequiredFeatures` tokens the run's source activates (issue #1700). Those are left out
197
+ # here: reading every source file would cost the boot-slimming probe the time it exists to save, and a
198
+ # project that requires a gated feature then only misses this probe, never hits a wrong slot.
184
199
  def libraries_config_entries(configuration)
185
200
  merged = (Environment::DEFAULT_LIBRARIES + configuration.libraries.map(&:to_s)).uniq
186
201
  [Cache::RbsDescriptor.libraries_entry(merged)]
@@ -64,7 +64,7 @@ module Rigor
64
64
  # the caller loads the engine and runs the full path. Any failure declines rather than raising: the
65
65
  # probe must never turn a servable run into a crash.
66
66
  def serve(paths)
67
- files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns)
67
+ files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns, base: @configuration.exclude_base)
68
68
  key = RunCacheKey.descriptor(
69
69
  configuration: @configuration, files: files, explain: @explain,
70
70
  rbs_config_entries: RunCacheKey.libraries_config_entries(@configuration)
@@ -24,18 +24,31 @@ module Rigor
24
24
  # `class_decl_paths` (a frozen `Hash<String, String>` populated once per environment by the RBS loader;
25
25
  # ~1000-2000 entries × one `String#start_with?`).
26
26
  class RunStats
27
- attr_reader :wall_seconds, :peak_rss_bytes,
27
+ attr_reader :wall_seconds,
28
28
  :target_files,
29
29
  :rbs_classes_total, :rbs_classes_project_sig, :rbs_classes_bundled,
30
30
  :gem_walk_classes, :gem_walk_gems, :rbs_attribution_available
31
31
 
32
- def initialize(wall_seconds:, peak_rss_bytes:, # rubocop:disable Metrics/ParameterLists
32
+ # The live probe a run's summary uses: reads this process's RSS when first asked. The probe is a constant
33
+ # lambda, so the RunStats it rides on never captures the Runner (and its analysis state) through `Result`.
34
+ CURRENT_PROCESS_PEAK_RSS = Ractor.make_shareable(-> { RunStats.peak_rss_bytes })
35
+
36
+ # Issue #1529 — the peak RSS is read on the first {#peak_rss_bytes} call, not when the summary is built. A run
37
+ # that prints no summary (`--incremental` recheck, `--no-stats`) therefore never forks `ps` or reads `/proc`.
38
+ # The read stays in this process: the summary is built and printed by the process that ran the analysis.
39
+ #
40
+ # @param peak_rss_bytes — a value to report directly (specs, or a caller that already read it). Ignored when
41
+ # `peak_rss_probe:` is given.
42
+ # @param peak_rss_probe — a zero-argument callable returning the RSS in bytes, called at most once, lazily.
43
+ def initialize(wall_seconds:, # rubocop:disable Metrics/ParameterLists
33
44
  target_files:,
34
45
  rbs_classes_total:, rbs_classes_project_sig:, rbs_classes_bundled:,
35
46
  gem_walk_classes:, gem_walk_gems:,
36
- rbs_attribution_available: true)
47
+ rbs_attribution_available: true, peak_rss_bytes: nil, peak_rss_probe: nil)
37
48
  @wall_seconds = wall_seconds
38
- @peak_rss_bytes = peak_rss_bytes
49
+ @peak_rss_probe = peak_rss_probe || -> { peak_rss_bytes }
50
+ # A frozen RunStats cannot memoise into an ivar, so the first read lands in this (unfrozen) Hash.
51
+ @peak_rss_memo = {}
39
52
  @target_files = target_files
40
53
  @rbs_classes_total = rbs_classes_total
41
54
  @rbs_classes_project_sig = rbs_classes_project_sig
@@ -46,6 +59,15 @@ module Rigor
46
59
  freeze
47
60
  end
48
61
 
62
+ # The peak RSS in bytes, read once on first call and memoised (nil when no route answers). The check command
63
+ # forces the first call straight after the analysis (#1529), before `--coverage` or any output, so the figure
64
+ # covers the analysis alone. On Linux it is the kernel's VmHWM high-water mark, so a later read is never below
65
+ # the run-end value. On macOS it is `ps`'s current RSS, so the reading is the process as it stood at that
66
+ # first call, not a high-water mark.
67
+ def peak_rss_bytes
68
+ @peak_rss_memo.fetch(:value) { @peak_rss_memo[:value] = @peak_rss_probe.call }
69
+ end
70
+
49
71
  # Reports the process's resident set size in bytes. Source ordering: `/proc/self/status` (Linux — reads
50
72
  # `VmHWM:`, the peak RSS the kernel records) first; otherwise `ps -o rss= -p <pid>` (macOS / BSD —
51
73
  # reports CURRENT RSS, the closest universally-available proxy). Returns nil when neither route works
@@ -146,7 +168,7 @@ module Rigor
146
168
  end
147
169
  out.puts("#{prefix}Process")
148
170
  out.puts("#{prefix} Wall time: #{Kernel.format('%.2fs', @wall_seconds)}")
149
- out.puts("#{prefix} Memory peak: #{format_bytes(@peak_rss_bytes)}")
171
+ out.puts("#{prefix} Memory peak: #{format_bytes(peak_rss_bytes)}")
150
172
  end
151
173
 
152
174
  def to_h
@@ -159,7 +181,7 @@ module Rigor
159
181
  gem_walk_classes: @gem_walk_classes,
160
182
  gem_walk_gems: @gem_walk_gems,
161
183
  wall_seconds: @wall_seconds,
162
- peak_rss_bytes: @peak_rss_bytes
184
+ peak_rss_bytes: peak_rss_bytes
163
185
  }
164
186
  end
165
187
 
@@ -36,6 +36,8 @@ module Rigor
36
36
  # that stood down against a colliding generic arity (#610), as
37
37
  # `[path, class_name, existing_arity, incoming_arity, existing_file]` tuples. Defaults to none so
38
38
  # a caller that snapshots nothing of the kind need not say so.
39
+ # @param vendored_standdowns_snapshot — issue #1713 — reader returning what each required-feature-gated
40
+ # vendored directory left out (`Environment::RbsLoader#vendored_standdowns`). Defaults to none.
39
41
  # @param env_build_failure_snapshot — reader returning the total RBS env-build failure tuple
40
42
  # (`[error_class, first_error_line, conflicting_buffer_names]`) or nil when the env built.
41
43
  # @param definition_build_failures_snapshot — issue #696 — reader returning the per-class
@@ -55,7 +57,7 @@ module Rigor
55
57
  quarantined_signatures_snapshot:, env_build_failure_snapshot:,
56
58
  definition_build_failures_snapshot:, hkt_scan_failure_snapshot:,
57
59
  conformance_results_snapshot:, signature_standdowns_snapshot: -> { [] },
58
- pooled_run_disclosures: -> { [] })
60
+ vendored_standdowns_snapshot: -> { [] }, pooled_run_disclosures: -> { [] })
59
61
  @configuration = configuration
60
62
  @rbs_extended_reporter = rbs_extended_reporter
61
63
  @boundary_cross_reporter = boundary_cross_reporter
@@ -68,6 +70,7 @@ module Rigor
68
70
  @synthesized_namespaces_snapshot_reader = synthesized_namespaces_snapshot
69
71
  @quarantined_signatures_snapshot_reader = quarantined_signatures_snapshot
70
72
  @signature_standdowns_snapshot_reader = signature_standdowns_snapshot
73
+ @vendored_standdowns_snapshot_reader = vendored_standdowns_snapshot
71
74
  @env_build_failure_snapshot_reader = env_build_failure_snapshot
72
75
  @definition_build_failures_snapshot_reader = definition_build_failures_snapshot
73
76
  @hkt_scan_failure_snapshot_reader = hkt_scan_failure_snapshot
@@ -125,7 +128,32 @@ module Rigor
125
128
  source_family: :builtin
126
129
  )
127
130
  end
128
- not_found + Array(pre_eval_diagnostics_from_scanner).map { |hash| diagnostic_from_hash(hash) }
131
+ scanned = Array(pre_eval_diagnostics_from_scanner).map { |hash| diagnostic_from_hash(hash) }
132
+ suppress_pre_eval(not_found + scanned)
133
+ end
134
+
135
+ # The ordinary suppression pipeline for the `pre-eval.*` rows: the `# rigor:disable` /
136
+ # `# rigor:disable-file` comments of the file a row is positioned in (a `pre_eval:` file need not be
137
+ # under `paths:`, so its comments are read here rather than by per-file analysis) and the project's
138
+ # `disable:` list. A row positioned at `.rigor.yml` has no Ruby comments (`.yml` is skipped) and answers to
139
+ # `disable:` only.
140
+ # Stream order is the output contract, so survivors are picked out of the original sequence.
141
+ def suppress_pre_eval(diagnostics)
142
+ kept = diagnostics.group_by(&:path).flat_map do |path, group|
143
+ CheckRules.filter_suppressed(
144
+ group, comments: pre_eval_comments(path), disabled_rules: @configuration.disabled_rules
145
+ )
146
+ end
147
+ kept_ids = kept.to_set(&:object_id)
148
+ diagnostics.select { |diagnostic| kept_ids.include?(diagnostic.object_id) }
149
+ end
150
+
151
+ def pre_eval_comments(path)
152
+ return [].freeze unless File.file?(path) && !path.end_with?(".rbs", ".yml", ".yaml")
153
+
154
+ Prism.parse_file(path).comments
155
+ rescue StandardError
156
+ [].freeze
129
157
  end
130
158
 
131
159
  def diagnostic_from_hash(hash)
@@ -437,11 +465,23 @@ module Rigor
437
465
  standdowns.map { |entry| build_rbs_plugin_signature_stood_down_diagnostic(entry) }
438
466
  end
439
467
 
440
- # The two `:info` notices that close the `rbs.coverage.*` ladder, in this order: the namespace
441
- # synthesis first, then the stand-down (#610) — the outcome that AVOIDED a definition-build failure
442
- # and so the quietest row on it. One method so the runner's assembly reads them as one slot.
468
+ # Issue #1713 — the other outcome that avoided a definition-build failure: a required-feature-gated
469
+ # vendored directory (`data/vendored_gem_sigs/prime/`) left out what another signature source already
470
+ # declares — a member, a whole type, or, when nothing narrower built, the directory. One `:info` per
471
+ # directory, naming the declarations that stood down and the files that displaced them.
472
+ def rbs_vendored_signature_stood_down_diagnostics
473
+ standdowns = vendored_standdowns_snapshot
474
+ return [] if standdowns.nil? || standdowns.empty?
475
+
476
+ standdowns.map { |entry| build_rbs_vendored_signature_stood_down_diagnostic(entry) }
477
+ end
478
+
479
+ # The `:info` notices that close the `rbs.coverage.*` ladder, in this order: the namespace synthesis
480
+ # first, then the stand-downs (#610, #1713) — the outcomes that AVOIDED a definition-build failure and so
481
+ # the quietest rows on it. One method so the runner's assembly reads them as one slot.
443
482
  def rbs_coverage_notice_diagnostics
444
- rbs_synthesized_namespace_diagnostics + rbs_plugin_signature_stood_down_diagnostics
483
+ rbs_synthesized_namespace_diagnostics + rbs_plugin_signature_stood_down_diagnostics +
484
+ rbs_vendored_signature_stood_down_diagnostics
445
485
  end
446
486
 
447
487
  # Maps the per-run `rigor:v1:conforms-to` scan results into diagnostics (spec: `rbs-extended.md` §
@@ -769,6 +809,69 @@ module Rigor
769
809
  )
770
810
  end
771
811
 
812
+ def build_rbs_vendored_signature_stood_down_diagnostic(entry)
813
+ dir, declarations, whole = entry
814
+ Diagnostic.new(
815
+ path: ".rigor.yml",
816
+ line: 1,
817
+ column: 1,
818
+ message: vendored_standdown_message(dir, declarations, whole),
819
+ severity: :info,
820
+ rule: "rbs.coverage.vendored-signature-stood-down",
821
+ source_family: :builtin
822
+ )
823
+ end
824
+
825
+ def vendored_standdown_message(dir, declarations, whole)
826
+ subject = "Rigor's vendored `#{dir}` signatures (loaded because the project requires `#{dir}` or lists " \
827
+ "it under `libraries:`)"
828
+ return vendored_whole_standdown_message(subject, dir, declarations) if whole
829
+
830
+ vendored_partial_standdown_message(subject, declarations)
831
+ end
832
+
833
+ # One sentence per kind of stand-down present ({Environment::GatedSignaturePlan.standdowns}' reasons).
834
+ VENDORED_STANDDOWN_SENTENCES = {
835
+ "member" => "left out %<count>d method declaration(s) another signature source also makes, since two " \
836
+ "declarations of one method fail the class's definition build, and uses the other " \
837
+ "source's: %<list>s.",
838
+ "type" => "stood %<count>d type(s) down whole, keeping only the types nested in them, because another " \
839
+ "source declares each with a different kind (class or module) or generic arity, or the type " \
840
+ "failed to build beside that source's declaration: %<list>s.",
841
+ "open" => "kept %<count>d type(s) without their superclass, because each failed to build beside the " \
842
+ "other signatures although no other source declares it; calls they no longer answer read " \
843
+ "untyped rather than undefined: %<list>s."
844
+ }.freeze
845
+ private_constant :VENDORED_STANDDOWN_SENTENCES
846
+
847
+ def vendored_partial_standdown_message(subject, declarations)
848
+ sentences = VENDORED_STANDDOWN_SENTENCES.filter_map do |reason, template|
849
+ group = declarations.select { |entry| (entry[2] || "member") == reason }
850
+ next if group.empty?
851
+
852
+ format(template, count: group.size, list: sampled(vendored_standdown_labels(group), 5))
853
+ end
854
+ "#{subject} loaded without some of their declarations. Rigor #{sentences.join(' Rigor ')} The rest of the " \
855
+ "vendored signatures still load. Nothing to fix unless the other declaration is an outdated copy; " \
856
+ "removing it brings the vendored one back."
857
+ end
858
+
859
+ def vendored_whole_standdown_message(subject, dir, declarations)
860
+ labels = vendored_standdown_labels(declarations)
861
+ clashes = labels.empty? ? "" : " Declarations it clashed with: #{sampled(labels, 5)}."
862
+ "#{subject} stood down " \
863
+ "entirely: a type they declare failed to build beside the other signature sources even with the " \
864
+ "clashing declarations left out, so calls into `#{dir}`'s API resolve only against what those " \
865
+ "sources declare.#{clashes}"
866
+ end
867
+
868
+ def vendored_standdown_labels(declarations)
869
+ declarations.map do |declaration, cause_file|
870
+ cause = cause_file.nil? ? "" : " (#{relative_signature_path(cause_file.to_s)})"
871
+ "`#{declaration}`#{cause}"
872
+ end
873
+ end
874
+
772
875
  def displacing_source_phrase(existing_file)
773
876
  return "another loaded signature source" if existing_file.nil?
774
877
 
@@ -1196,6 +1299,10 @@ module Rigor
1196
1299
  @signature_standdowns_snapshot_reader.call
1197
1300
  end
1198
1301
 
1302
+ def vendored_standdowns_snapshot
1303
+ @vendored_standdowns_snapshot_reader.call
1304
+ end
1305
+
1199
1306
  def env_build_failure_snapshot
1200
1307
  @env_build_failure_snapshot_reader.call
1201
1308
  end
@@ -45,7 +45,8 @@ module Rigor
45
45
  snapshots:, plugin_registry:, dependency_source_index:,
46
46
  synthetic_method_index:, project_patched_methods:,
47
47
  analyze_file:, project_scope_seed: -> { {} }, record_dependencies: false,
48
- restored_run_level_rows: nil, template_units: -> { TemplateUnits.empty })
48
+ restored_run_level_rows: nil, template_units: -> { TemplateUnits.empty },
49
+ feature_sources: -> { {} }, plugin_prepare: -> { [[], []] })
49
50
  @configuration = configuration
50
51
  @cache_store = cache_store
51
52
  @explain = explain
@@ -67,6 +68,7 @@ module Rigor
67
68
  @collected_run_disclosures = {}
68
69
  @collect_stats = collect_stats
69
70
  @buffer = buffer
71
+ @feature_sources = feature_sources
70
72
  @environment_override = environment_override
71
73
  @rbs_extended_reporter = rbs_extended_reporter
72
74
  @boundary_cross_reporter = boundary_cross_reporter
@@ -82,6 +84,10 @@ module Rigor
82
84
  # inherits the same compiled bytes.
83
85
  @template_units_reader = template_units
84
86
  @analyze_file = analyze_file
87
+ # Issue #1541 — `() -> [rows, disclosure records]`, a `#prepare` run on a throwaway registry. Read
88
+ # only by {#serve_empty_closure}: a pooled run's `#prepare` happens inside its workers, and an
89
+ # empty closure dispatches none.
90
+ @plugin_prepare = plugin_prepare
85
91
  # Issues #796 / #794 — the previous full run's run-level rows, handed down by
86
92
  # {Analysis::IncrementalSession} on a NARROWED run only (a recheck's closure, a
87
93
  # `--verify-incremental` partition). A full run leaves this nil and derives everything itself.
@@ -144,10 +150,8 @@ module Rigor
144
150
  # (`expansion.fetch(:files)`), independent of any `analyze_only` narrowing of `files`; see
145
151
  # {#serve_empty_closure} for what an empty `files` does with it.
146
152
  def analyze_files(files, environment: nil, project_files: nil)
147
- if files.empty?
148
- serve_empty_closure(environment, project_files)
149
- return []
150
- end
153
+ return serve_empty_closure(environment, project_files) if files.empty?
154
+
151
155
  # Issue #784 / #793 — `files` is what this run ANALYSES; `source_files` is what its environment
152
156
  # is BUILT over, and the two are the same only on a full run. A narrowed run (`analyze_only`: an
153
157
  # incremental closure, a `--verify-incremental` partition) used to build its environment over the
@@ -197,7 +201,16 @@ module Rigor
197
201
  # whether anyone could have demanded anything at all: with no files, nobody could have, and an
198
202
  # empty project keeps paying no env build. Keyed on the project's files rather than on
199
203
  # `analyze_only`, because a recheck over an EMPTY project narrows to `Set[]`, which is non-nil.
204
+ #
205
+ # Issue #1541 — and, in pool mode, its `prepare` rows. A pooled run's `#prepare` happens inside the
206
+ # workers and its rows reach the stream through their first payload, so the coordinator-side
207
+ # pre-pass skips it ({ProjectPrePasses#run}) and {DiagnosticAggregator#pre_file_diagnostics} omits
208
+ # the cached rows; with no worker dispatched the row appeared nowhere, and a plugin whose `#prepare`
209
+ # raises turned a red project green on a nothing-changed `--incremental --workers N` recheck. They
210
+ # are returned as the run's per-file stream head, where the pool path puts them (`.rigor.yml`).
211
+ # Sequential, whose pre-pass already ran `#prepare`, returns no rows here.
200
212
  def serve_empty_closure(environment, project_files)
213
+ prepare_rows = pool_mode? ? pooled_prepare_rows : []
201
214
  env = environment || @environment_override
202
215
  # Resolved over the project's OWN file list — not `[]`, which would drop every plugin-synthesized
203
216
  # virtual RBS (`Environment.collect_virtual_rbs` short-circuits on an empty list) and scan a
@@ -208,23 +221,40 @@ module Rigor
208
221
 
209
222
  env = resolve_sequential_environment(source_files: project_files)
210
223
  end
211
- snapshot_project_signature_state(signature_state_environment(resolve))
224
+ snapshot_project_signature_state(signature_state_environment(resolve, project_files))
212
225
  snapshot_effect_annotation_carrier(
213
226
  annotation_carrier_environment(resolve, in_hand: !env.nil?)&.rbs_loader
214
227
  )
215
228
  # `env` rather than `resolve.call`: read the demand the snapshots above just made when they built
216
229
  # an environment, and force nothing when they did not.
217
230
  record_definition_build_failures(env&.rbs_loader&.definition_build_failures)
218
- return if replayed_restored_run_level_rows?
231
+ record_hkt_scan_failure(hkt_scan_outcome(resolve.call)) unless replayed_restored_run_level_rows?
232
+ prepare_rows
233
+ end
234
+
235
+ # Issue #1541 — the pooled empty closure's `#prepare`: the rows go to the caller, the run-scoped
236
+ # registrations into the table the workers' payloads would have filled.
237
+ def pooled_prepare_rows
238
+ rows, records = @plugin_prepare.call
239
+ merge_run_disclosures(records)
240
+ Array(rows)
241
+ end
219
242
 
220
- record_hkt_scan_failure(hkt_scan_outcome(resolve.call))
243
+ # Issue #794 — the signature-state snapshot's own gates, asked BEFORE an environment exists. Every
244
+ # answer is loader-free (the configuration, the plugin registry and, issue #1713, the required-feature
245
+ # scan), and with no gate open {#snapshot_project_signature_state} writes the same empty state a nil
246
+ # environment gives it.
247
+ def signature_state_environment(resolve, project_files)
248
+ gated = plugin_signature_paths? || project_signature_paths? || vendored_features_requested?(project_files)
249
+ gated ? resolve.call : nil
221
250
  end
222
251
 
223
- # Issue #794 — the signature-state snapshot's own gates, asked BEFORE an environment exists. Both
224
- # answers are loader-free (the configuration and the plugin registry), and with neither gate open
225
- # {#snapshot_project_signature_state} writes the same empty state a nil environment gives it.
226
- def signature_state_environment(resolve)
227
- plugin_signature_paths? || project_signature_paths? ? resolve.call : nil
252
+ # Issue #1713 — whether this run activates a required-feature-gated vendored directory, whose stand-down
253
+ # {#snapshot_project_signature_state} reports: the scan every build of this run gates on, or the
254
+ # feature named under `libraries:`.
255
+ def vendored_features_requested?(project_files)
256
+ features = project_files.nil? || project_files.empty? ? [] : required_features_for(project_files)
257
+ !Environment::RequiredFeatures.tokens(features, @configuration.libraries).empty?
228
258
  end
229
259
 
230
260
  # Issue #794 — the same, for the #441 carrier. The carrier is a filter over the loader's
@@ -344,6 +374,10 @@ module Rigor
344
374
  # no such plugin reads nothing off its loader here, as before.
345
375
  @snapshots.signature_standdowns =
346
376
  plugin_signature_paths? ? signature_standdowns_for(environment&.rbs_loader) : []
377
+ # Issue #1713 — NOT behind the `signature_paths:` gate either: what displaces a vendored declaration may
378
+ # be a gem's or a collection's copy. The loader answers without building its env when no gated
379
+ # directory is active.
380
+ @snapshots.vendored_standdowns = vendored_standdowns_for(environment&.rbs_loader)
347
381
  unless project_signature_paths?
348
382
  @snapshots.synthesized_namespaces = []
349
383
  @snapshots.quarantined_signatures = []
@@ -459,7 +493,22 @@ module Rigor
459
493
  **ProjectEnvironment.dependency_discovery_options(@configuration),
460
494
  synthetic_method_index: synthetic_method_index,
461
495
  project_patched_methods: project_patched_methods,
462
- source_files: source_files
496
+ source_files: source_files,
497
+ required_features: required_features_for(source_files)
498
+ )
499
+ end
500
+
501
+ # Issue #1700 — the {Environment::RequiredFeatures} this run's environments gate on: the configured paths
502
+ # plus `source_files`, whichever files the run checks, with an editor buffer or an in-memory source read
503
+ # in place of its file. Taken once per file list and handed to every build (the sequential one, the
504
+ # pool's prewarm and each worker), so they agree and a pool does not re-read the project per worker.
505
+ def required_features_for(source_files)
506
+ key = source_files.map(&:to_s)
507
+ return @required_features if @required_features_key == key
508
+
509
+ @required_features_key = key
510
+ @required_features = Ractor.make_shareable(
511
+ Environment::RequiredFeatures.for_configuration(@configuration, key, sources: @feature_sources.call)
463
512
  )
464
513
  end
465
514
 
@@ -575,9 +624,10 @@ module Rigor
575
624
  # a frozen Array<String>; cheaply shareable. Issue #793 — it IS the full project now
576
625
  # (`source_files`), not the analyzed subset this comment always described.
577
626
  shareable_source_files = source_files.map { |path| path.to_s.dup.freeze }.freeze
627
+ features = required_features_for(source_files)
578
628
 
579
629
  pool = Array.new(@workers) do
580
- Ractor.new(configuration, cache_root, blueprints, explain, shareable_source_files, locked_gems) do |configuration, cache_root, blueprints, explain, shareable_source_files, locked_gems| # rubocop:disable Layout/LineLength
630
+ Ractor.new(configuration, cache_root, blueprints, explain, shareable_source_files, locked_gems, features) do |configuration, cache_root, blueprints, explain, shareable_source_files, locked_gems, features| # rubocop:disable Layout/LineLength
581
631
  cache_store = cache_root ? Rigor::Cache::Store.new(root: cache_root) : nil
582
632
  session = Rigor::Analysis::WorkerSession.new(
583
633
  configuration: configuration,
@@ -585,7 +635,8 @@ module Rigor
585
635
  plugin_blueprints: blueprints,
586
636
  explain: explain,
587
637
  source_files: shareable_source_files,
588
- locked_gems: locked_gems
638
+ locked_gems: locked_gems,
639
+ required_features: features
589
640
  )
590
641
  main = Ractor.main
591
642
  main.send([:prepare, session.prepare_diagnostics])
@@ -667,8 +718,9 @@ module Rigor
667
718
  # Issue #1051, known exception: run-scoped disclosures a plugin registered from `#prepare` are NOT
668
719
  # recovered here — nor, since #1060, a positioned `#emit_once` batch registered there, which rides
669
720
  # the same table. There is no session to drain — this backend builds none on the coordinator — and
670
- # `#prepare` deliberately does not run on the coordinator-side registry under pool mode, so a
671
- # prepare-time disclosure lives only inside the Ractor that died with it. A disclosure registered
721
+ # `#prepare` does not run on the coordinator-side registry under pool mode (only an empty closure
722
+ # runs it, on a throwaway registry; see {#serve_empty_closure}), so a prepare-time disclosure lives
723
+ # only inside the Ractor that died with it. A disclosure registered
672
724
  # from `#diagnostics_for_file` IS recovered, because the re-analysis below runs the coordinator-side
673
725
  # plugin instances that {DiagnosticAggregator#plugin_run_disclosure_diagnostics} reads directly.
674
726
  # Left as is: recovering the rest means running `#prepare` on the coordinator registry, which would
@@ -713,7 +765,8 @@ module Rigor
713
765
  project_scope_seed: project_scope_seed,
714
766
  source_files: source_files,
715
767
  record_dependencies: @record_dependencies,
716
- template_units: template_units
768
+ template_units: template_units,
769
+ required_features: required_features_for(source_files)
717
770
  )
718
771
  # Force the full RBS load on the parent so children copy-on-write inherit a warm Environment
719
772
  # rather than each rebuilding it after the fork.
@@ -890,6 +943,7 @@ module Rigor
890
943
  plugin_registry: plugin_registry,
891
944
  source_files: source_files,
892
945
  locked_gems: locked_gems,
946
+ required_features: required_features_for(source_files),
893
947
  **ProjectEnvironment.dependency_discovery_options(@configuration)
894
948
  )
895
949
  warm_env.rbs_loader&.prewarm
@@ -1094,6 +1148,10 @@ module Rigor
1094
1148
  loader.signature_standdowns
1095
1149
  end
1096
1150
 
1151
+ def vendored_standdowns_for(loader)
1152
+ loader.respond_to?(:vendored_standdowns) ? loader.vendored_standdowns : []
1153
+ end
1154
+
1097
1155
  def plugin_registry
1098
1156
  @plugin_registry_reader.call
1099
1157
  end
@@ -59,7 +59,7 @@ module Rigor
59
59
  :unpositioned_mixins, :discovered_class_sources, :constant_values, :constant_sources, :constant_writes,
60
60
  :discovered_method_visibilities, :discovered_methods, :discovered_parameter_envelopes, :data_member_layouts,
61
61
  :struct_member_layouts, :discovered_deferred_ranges, :discovered_refinements, :siblings,
62
- :discovered_global_write_census
62
+ :discovered_global_write_census, :singleton_class_sources
63
63
  )
64
64
 
65
65
  # Internal: drives every EAGER project-wide pre-pass — the ones whose products feed the RBS
@@ -175,6 +175,8 @@ module Rigor
175
175
  # object's `extend`s reach `Narrowing` from a sibling file the way its `include`s already do.
176
176
  discovered_extends: def_index.fetch(:extends), unpositioned_mixins: def_index.fetch(:unpositioned_mixins),
177
177
  discovered_class_sources: def_index.fetch(:class_sources),
178
+ # Issue #1752 — the files opening `class << Const`, for the class-declaration diff alone.
179
+ singleton_class_sources: def_index.fetch(:singleton_class_sources, {}),
178
180
  # Issue #644 — the cross-file value-constant publication table and its write attribution.
179
181
  constant_values: def_index.fetch(:constant_values), constant_sources: def_index.fetch(:constant_sources),
180
182
  constant_writes: def_index.fetch(:constant_writes),
@@ -251,6 +253,24 @@ module Rigor
251
253
  registry
252
254
  end
253
255
 
256
+ # Issue #1541 — what a pooled run's workers would have produced from `#prepare`, for the coordinator's
257
+ # empty-closure branch, which dispatches none: the `#prepare` error rows and the run-scoped
258
+ # registrations (`#disclose_once` / `#emit_once`) made there. It runs on a THROWAWAY registry, so
259
+ # nothing `#prepare` records on a plugin instance (a trust refusal, say) reaches the coordinator-side
260
+ # instances {DiagnosticAggregator} reads — a pooled cold run never shows those either.
261
+ # Returns the rows first, then the disclosure records.
262
+ def prepare_throwaway_registry
263
+ registry = load_plugins
264
+ rows = plugin_prepare_diagnostics(registry)
265
+ records = registry.plugins.flat_map do |plugin|
266
+ plugin_id = safe_plugin_id(plugin)
267
+ Array(plugin.run_disclosure_records).map { |record| record.merge(plugin_id: plugin_id) }
268
+ rescue StandardError
269
+ []
270
+ end
271
+ [rows, records]
272
+ end
273
+
254
274
  # Returns the per-run shared `Plugin::FactStore` instance. All loaded plugins share this store
255
275
  # through their respective `Plugin::Services` (the same instance is threaded by
256
276
  # `Plugin::Loader.load`). Returns `nil` when no plugins are loaded.