rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -16,6 +16,7 @@ require_relative "environment/bundle_sig_discovery"
16
16
  require_relative "environment/lockfile_resolver"
17
17
  require_relative "environment/installed_gem_set"
18
18
  require_relative "environment/rbs_collection_discovery"
19
+ require_relative "library_probe"
19
20
  require_relative "environment/project_path_gems"
20
21
  require_relative "environment/source_rbs_synthesis"
21
22
  require_relative "plugin/isolation"
@@ -49,6 +50,19 @@ module Rigor
49
50
  # `RbsCoverageReport` reports.
50
51
  GEM_OVERLAY_PLUGIN_IDS = { "activesupport" => "activesupport-core-ext" }.freeze
51
52
 
53
+ # Issue #2857 — the `RbsCoverageReport` sources each gem's bundled overlay may load under, when they
54
+ # differ from the ADR-72 `:missing` default every unlisted overlay keeps. `addressable` is the first
55
+ # exception: its overlay exists to CORRECT the `Addressable::URI.parse` signature `rbs collection`
56
+ # ships (`parse(nil)` returns nil; the collection declares `(String | URI) -> URI`), so it loads only
57
+ # under `:rbs_collection` coverage, where the deferred-signature plan composes it over the collection
58
+ # copy as an overload continuation. Under `:missing` coverage — no sigs at all — it stays unloaded:
59
+ # the overlay's partial `Addressable::URI` declaration would make every member it omits a false
60
+ # `call.undefined-method`, a worse answer than the `Dynamic` silence a sig-less gem already has.
61
+ # Frozen arrays, not Sets: `Ractor.shareable?` rejects a Set even frozen (ractor_readiness_spec).
62
+ GEM_OVERLAY_COVERAGE_SOURCES = { "addressable" => [:rbs_collection].freeze }.freeze
63
+ GEM_OVERLAY_DEFAULT_COVERAGE_SOURCES = [:missing].freeze
64
+ private_constant :GEM_OVERLAY_DEFAULT_COVERAGE_SOURCES
65
+
52
66
  attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
53
67
  :reporters, :name_scope,
54
68
  :synthetic_method_index, :project_patched_methods
@@ -118,6 +132,13 @@ module Rigor
118
132
  # stage that failed and the other stage still runs, so this getter never raises into its callers —
119
133
  # neither into a file's `analyze_body` rescue nor out of the run-owned demands, which have no rescue
120
134
  # above them at all.
135
+ # Issue #2883 — builds the RBS environment now, if it is not built yet, so typing never builds it. A no-op for an
136
+ # environment with no loader. Answers the environment.
137
+ def prepare_for_typing!
138
+ rbs_loader&.prepare_for_typing!
139
+ self
140
+ end
141
+
121
142
  def hkt_registry
122
143
  @hkt_registry_holder.fetch do
123
144
  pre_scan = pre_scan_hkt_registry
@@ -247,11 +268,14 @@ module Rigor
247
268
  end
248
269
 
249
270
  class << self
271
+ # Issue #2883 — the shared default environment is built here too, not on the first `type_of` that needs it, so
272
+ # code that types against it (`Scope.empty`, the narrowing defaults, the incremental session's summary
273
+ # evaluator) never builds it from inside typing. A non-main Ractor keeps the lazy build.
250
274
  def default
251
275
  @default ||= new(
252
276
  rbs_loader: RbsLoader.default,
253
277
  hkt_registry: Builtins::HktBuiltins.registry
254
- ).freeze
278
+ ).freeze.tap { |environment| environment.prepare_for_typing! if Ractor.main? }
255
279
  end
256
280
 
257
281
  # Builds an Environment that consults the project's local signatures and any opt-in stdlib libraries on
@@ -270,6 +294,10 @@ module Rigor
270
294
  # caching for this environment.
271
295
  # @param core_root — issue #2517 — a core signature directory loaded in place of the `rbs` gem's `core/`
272
296
  # ({Configuration#core_root}); nil (the default) loads the gem's core.
297
+ # @param eager — issue #2883 — whether the RBS environment is built here, before anything types. Typing then
298
+ # never builds it, so the core-root Frame the build reads (ADR-129 G1') is never read inside a `type_of`. The
299
+ # one caller that passes `false` is the runner's pre-cache build, which a warm result-cache hit must not pay
300
+ # for; it calls {#prepare_for_typing!} on the analysis path.
273
301
  # rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists
274
302
  def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil, # rubocop:disable Metrics/AbcSize
275
303
  plugin_registry: nil, dependency_source_index: nil,
@@ -280,7 +308,7 @@ module Rigor
280
308
  rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
281
309
  synthetic_method_index: nil, project_patched_methods: nil,
282
310
  source_files: [], locked_gems: nil, required_features: nil, core_root: nil,
283
- paths: nil, exclude_patterns: nil, exclude_base: nil)
311
+ paths: nil, exclude_patterns: nil, exclude_base: nil, eager: true)
284
312
  resolved_paths = project_signature_roots(signature_paths, root: root)
285
313
  # O4 MVP — append per-gem `sig/` directories discovered under the target project's bundler install
286
314
  # root. Empty array when neither an explicit path nor auto-detection finds a bundle. Order: user
@@ -313,14 +341,17 @@ module Rigor
313
341
  )
314
342
  # O4 Layer 3 slice 2 — when `rbs collection install` has been run for the target project, parse the
315
343
  # resulting `rbs_collection.lock.yaml` and feed each gem's `<collection_path>/<name>/<version>/`
316
- # directory into `signature_paths:`. Stdlib-typed entries are skipped (already covered by
317
- # `DEFAULT_LIBRARIES`), as are gems whose RBS rigor already loads from another source —
344
+ # directory into `signature_paths:`. Stdlib-typed entries have no such directory; they load as libraries
345
+ # (issue #2946, below). Skipped too are gems whose RBS rigor already loads from another source —
318
346
  # stdlib-extracted default gems (`cgi`, `logger`, …, shipped by the collection under a `git` source)
319
347
  # and the `data/vendored_gem_sigs/` bundle (`redis`, `nokogiri`, `pg`, …). `skip_gem_names:` passes
320
348
  # both sets so the collection copy doesn't double-declare against rigor's bundled RBS (the
321
349
  # `RBS::DuplicatedDeclarationError` hazard).
322
350
  # Issue #1700 — plus the gated vendored directories this run's source activates ({#feature_tokens}).
323
- merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s) +
351
+ # Issue #2946 — plus the lockfile's `source: { type: stdlib }` entries, as libraries ({BaseLibraries.resolve}).
352
+ merged_libraries = (BaseLibraries.resolve(libraries, rbs_collection_lockfile: rbs_collection_lockfile,
353
+ rbs_collection_auto_detect: rbs_collection_auto_detect,
354
+ root: root) +
324
355
  feature_tokens(required_features, source_files, libraries)).uniq
325
356
  # Issue #2441 — plus the project's own path gems ({ProjectPathGems}), whose collection copy would
326
357
  # double-declare the project's `sig/`.
@@ -333,24 +364,32 @@ module Rigor
333
364
  auto_detect: rbs_collection_auto_detect,
334
365
  skip_gem_names: skip_gem_names
335
366
  ).map(&:to_s)
367
+ # Issue #3030 — a gem the project's `signature_paths:` already loads a copy of (a gem_rbs_collection working
368
+ # tree next to its `_test/` lockfile) loads from there alone. The coverage classification below still sees
369
+ # the whole list: the gem has RBS either way.
370
+ covered_collection_paths = collection_paths
371
+ collection_paths = RbsCollectionDiscovery.without_signature_path_copies(collection_paths, resolved_paths)
372
+ # Issue #2978 — the collection copies of the locked gems whose own `sig/` the bundle discovery loaded.
373
+ behind_gem_sig_paths = collection_paths_behind_gem_sigs(collection_paths, gem_sig_paths, locked)
336
374
  # ADR-25 — RBS signature directories contributed by loaded plugins via their manifest
337
375
  # `signature_paths:`. Resolved to absolute dirs by `Plugin::Base#signature_paths`; additive, ranked
338
376
  # below the user's explicit `signature_paths:` and above the opportunistic bundle / collection
339
377
  # discovery. A duplicate-declaration conflict degrades through the same O7 failure-memo path.
340
378
  plugin_sig_paths = plugin_registry ? plugin_registry.signature_paths.map(&:to_s) : []
341
- # ADR-72 — Gemfile.lock-gated bundled RBS overlays. For each locked gem that ships no RBS through any
342
- # resolution path (`:missing`) and has a Rigor-bundled overlay, load that overlay so its core-class
343
- # extensions resolve (e.g. `Integer#minutes` on a Rails project) — turning a systematic false
344
- # `call.undefined-method` into a no-op while a project WITHOUT the gem still sees the genuine
345
- # diagnostic. Appended last so any RBS the project already supplies wins, and skipped for a gem whose
346
- # opt-in plugin twin is REACHABLE — loaded as a plugin, or wired in by `signature_paths:` (#672) —
347
- # so the two never both declare the methods. The paths ride in `loader_signature_paths`, so the env
348
- # cache descriptor digests them for free.
379
+ # ADR-72 — Gemfile.lock-gated bundled RBS overlays. For each locked gem whose coverage source its
380
+ # overlay may load under ({GEM_OVERLAY_COVERAGE_SOURCES} — `:missing` for every overlay but
381
+ # addressable's `:rbs_collection` correction, #2857) and that has a Rigor-bundled overlay, load that
382
+ # overlay so its core-class extensions resolve (e.g. `Integer#minutes` on a Rails project) —
383
+ # turning a systematic false `call.undefined-method` into a no-op while a project WITHOUT the gem
384
+ # still sees the genuine diagnostic. Appended last so any RBS the project already supplies wins,
385
+ # and skipped for a gem whose opt-in plugin twin is REACHABLE — loaded as a plugin, or wired in by
386
+ # `signature_paths:` (#672) — so the two never both declare the methods. The paths ride in
387
+ # `loader_signature_paths`, so the env cache descriptor digests them for free.
349
388
  # Issue #2441 — the project's own path gems are neither missing RBS nor overlay-eligible: the project's
350
389
  # `sig/` declares them, and an overlay (`data/gem_overlay/activesupport`) would collide with it.
351
390
  missing_gems, overlay_paths = missing_gems_and_overlay_paths(
352
391
  locked: locked.except(*project_gems), default_libraries: merged_libraries, bundle_sig_paths: gem_sig_paths,
353
- rbs_collection_paths: collection_paths, plugin_registry: plugin_registry,
392
+ rbs_collection_paths: covered_collection_paths, plugin_registry: plugin_registry,
354
393
  signature_paths: resolved_paths, bundle_root: bundle_root
355
394
  )
356
395
  loader_signature_paths = resolved_paths + plugin_sig_paths + gem_sig_paths +
@@ -374,8 +413,17 @@ module Rigor
374
413
  # Issue #610 — the plugin-contributed subset is loaded LAST and may stand down against a colliding
375
414
  # generic arity. Only the bundled plugins' own `sig/` is deferred: the user's `signature_paths:`,
376
415
  # the bundle walk and `rbs collection install` all stay authoritative.
377
- deferred_signature_paths: plugin_sig_paths,
416
+ # Issue #2974 — the ADR-72 gem overlays are Rigor-shipped too, and deferred with them: an overlay member
417
+ # the project's `sig/` also declares (`Integer#ordinalize`, `ActiveSupport::NumericWithFormat#to_fs`)
418
+ # stands down member by member (#2422) instead of failing the class's definition build.
419
+ deferred_signature_paths: plugin_sig_paths + overlay_paths,
378
420
  project_signature_paths: resolved_paths,
421
+ # Issue #2978 — a gem's collection copy loads behind the gem's own `sig/`: each member that `sig/` (or any
422
+ # other eagerly loaded signature file) declares stands down from the copy (#2422), and the rest loads.
423
+ subordinate_signature_paths: behind_gem_sig_paths,
424
+ # Issue #3030 — the collection gem directories, loaded as rbs loads a library: without the files below a
425
+ # `_`-prefixed directory (each gem's `_test/` fixtures).
426
+ collection_signature_paths: collection_paths,
379
427
  required_features: required_features || RequiredFeatures.scan(source_files, sig_paths: resolved_paths),
380
428
  core_root: core_root
381
429
  )
@@ -390,7 +438,9 @@ module Rigor
390
438
  # Issue #2517 — a selected core is built here, not lazily. The env is first demanded from inside a file's
391
439
  # analysis, under rescues that turn any raise into one `internal analyzer error` row per file; building it
392
440
  # now raises the {CoreRootError} once, before analysis, where the CLI reports it as a configuration error.
393
- loader.verify_core_root!
441
+ # Issue #2883 — the default core is built here too (`eager:`), so no `type_of` builds the environment.
442
+ # A non-main Ractor worker keeps the lazy build: the `rbs` gem's unshareable constants are unreadable there.
443
+ eager && Ractor.main? ? loader.prepare_for_typing! : loader.verify_core_root!
394
444
  # ADR-20 slice 2c + 2e — seed hkt_registry with the bundled builtins. The Environment's
395
445
  # `#hkt_registry` getter then LAZILY merges in the RBS env scan on first call so fast paths that
396
446
  # don't consult HKT (e.g. `rigor check --cache-stats --no-stats`) don't pay the eager env-build cost
@@ -414,10 +464,20 @@ module Rigor
414
464
  # The project-owned `sig/` roots a run reads: the explicit `signature_paths:` when given, else the
415
465
  # auto-detected `<root>/sig` (or none). Public so {Cache::IncrementalSnapshot} digests exactly the
416
466
  # directories the environment loads, including the auto-detected one a `nil` `signature_paths:` hides.
467
+ # The default branch is {project_signature_root_candidates} narrowed to the directories that exist.
417
468
  def project_signature_roots(signature_paths, root: Dir.pwd)
418
469
  signature_paths || default_signature_paths(root)
419
470
  end
420
471
 
472
+ # Issue #1619 — the single spelling of "the configured `signature_paths:`, else the conventional `sig`
473
+ # directory", as the CANDIDATES a run watches whether or not they exist yet. {project_signature_roots}
474
+ # narrows this to what the loader reads (a default only once it is a directory); the run slot's presence
475
+ # rows and the incremental write guard take it whole, because a `sig/` appearing mid-run must move the
476
+ # fingerprint and refuse the slot write — a question only a row for a path that need not exist can ask.
477
+ def project_signature_root_candidates(signature_paths)
478
+ signature_paths || [DEFAULT_PROJECT_SIG_DIR]
479
+ end
480
+
421
481
  private
422
482
 
423
483
  # Issue #1700 — the `RequiredFeatures` library tokens for the gated vendored directories this run asks for:
@@ -431,13 +491,31 @@ module Rigor
431
491
  # The gem names an `rbs collection` copy must not double-declare against: the library list and every
432
492
  # vendored directory except a `RequiredFeatures`-gated one. A gated directory stands down for any other
433
493
  # copy of its declarations (`GatedSignatureGuard`), so the collection's copy is kept and wins.
494
+ #
495
+ # Issue #2971 — a `libraries:` name that is not an RBS library (`foogem`, collection-installed) is dropped by
496
+ # the loader's `has_library?` filter, so it declares nothing to double-declare against; it stays out of the
497
+ # skip set so the collection's copy still loads. Issue #2978 — that holds for a gem whose own `sig/` the bundle
498
+ # loaded too: its collection copy loads behind that `sig/` ({.collection_paths_behind_gem_sigs}).
434
499
  def collection_skip_gem_names(library_names)
435
- library_names + RbsLoader.vendored_gem_names.reject { |name| RequiredFeatures.gated_dir?(name) }
500
+ LibraryProbe.loadable(library_names) + RbsLoader.vendored_gem_names.reject do |name|
501
+ RequiredFeatures.gated_dir?(name)
502
+ end
503
+ end
504
+
505
+ # Issue #2978 — the `collection_paths` (`<collection>/<name>/<version>/`) of the locked gems whose own `sig/` is
506
+ # among `gem_sig_paths` ({BundleSigDiscovery.locked_names_of}, keyed by name and version).
507
+ def collection_paths_behind_gem_sigs(collection_paths, gem_sig_paths, locked)
508
+ return [] if collection_paths.empty? || gem_sig_paths.empty?
509
+
510
+ names = BundleSigDiscovery.locked_names_of(gem_sig_paths.map { |dir| Pathname.new(dir) }, locked).to_set
511
+ collection_paths.select { |path| names.include?(Pathname.new(path).parent.basename.to_s) }
436
512
  end
437
513
 
438
514
  def default_signature_paths(root)
439
- sig = Pathname(root) / DEFAULT_PROJECT_SIG_DIR
440
- sig.directory? ? [sig] : []
515
+ project_signature_root_candidates(nil).filter_map do |candidate|
516
+ path = Pathname(root) / candidate.to_s
517
+ path if path.directory?
518
+ end
441
519
  end
442
520
 
443
521
  # ADR-82 WD9 — the resolved bundle root for the missing-gem constant index, doubling (ADR-90) as the
@@ -460,9 +538,12 @@ module Rigor
460
538
  bundle_root
461
539
  end
462
540
 
463
- # The `:missing` classification (locked gems with no RBS through any resolution path), computed once
464
- # and consumed twice: as `[gem_name, version, variants]` entries for the ADR-82 WD9 constant-ownership
465
- # index, and as the eligible-gem set for the ADR-72 overlay resolution. Returns `[entries, overlay_paths]`.
541
+ # The per-gem RBS coverage classification (locked gems against every resolution path), computed
542
+ # once and consumed twice: its `:missing` rows become `[gem_name, version, variants]` entries for
543
+ # the ADR-82 WD9 constant-ownership index, and the full row set feeds the ADR-72 overlay resolution,
544
+ # which admits a gem when its row's source is one the gem's overlay may load under
545
+ # ({GEM_OVERLAY_COVERAGE_SOURCES} — `:missing` by default, `:rbs_collection` for addressable).
546
+ # Returns `[entries, overlay_paths]`.
466
547
  def missing_gems_and_overlay_paths(locked:, default_libraries:, bundle_sig_paths:,
467
548
  rbs_collection_paths:, plugin_registry:, signature_paths: [],
468
549
  bundle_root: nil)
@@ -470,10 +551,10 @@ module Rigor
470
551
  return unlocked_missing_gems(default_libraries, bundle_sig_paths, rbs_collection_paths, bundle_root)
471
552
  end
472
553
 
473
- rows = missing_rows(locked, default_libraries, bundle_sig_paths, rbs_collection_paths)
554
+ coverage = coverage_rows(locked, default_libraries, bundle_sig_paths, rbs_collection_paths)
555
+ rows = coverage.select { |row| row.source == :missing }
474
556
  overlays = gem_overlay_paths(
475
- missing_gem_names: rows.map(&:gem_name), plugin_registry: plugin_registry,
476
- signature_paths: signature_paths
557
+ coverage: coverage, plugin_registry: plugin_registry, signature_paths: signature_paths
477
558
  )
478
559
  [index_entries(rows, locked), overlays]
479
560
  end
@@ -500,14 +581,20 @@ module Rigor
500
581
  end
501
582
 
502
583
  def missing_rows(gems, default_libraries, bundle_sig_paths, rbs_collection_paths)
584
+ coverage_rows(gems, default_libraries, bundle_sig_paths, rbs_collection_paths)
585
+ .select { |row| row.source == :missing }
586
+ end
587
+
588
+ def coverage_rows(gems, default_libraries, bundle_sig_paths, rbs_collection_paths)
503
589
  RbsCoverageReport.classify(
504
590
  locked_gems: gems, default_libraries: default_libraries,
505
591
  bundle_sig_paths: bundle_sig_paths, rbs_collection_paths: rbs_collection_paths
506
- ).select { |row| row.source == :missing }
592
+ )
507
593
  end
508
594
 
509
595
  # ADR-72 — resolve the bundled RBS overlay directories to load for this project. A gem is eligible when
510
- # it is classified `:missing` (see {missing_rows}) and its conflicting opt-in plugin twin (if
596
+ # its coverage row's source is one the gem's overlay may load under ({GEM_OVERLAY_COVERAGE_SOURCES};
597
+ # `:missing` for every overlay the map does not name) and its conflicting opt-in plugin twin (if
511
598
  # any) is NOT already reachable. Returns `[Pathname]`, deterministically ordered, or `[]` when no
512
599
  # eligible gem.
513
600
  #
@@ -521,20 +608,29 @@ module Rigor
521
608
  # all); `RbsLoader.bundled_overlay_twin_signatures_loaded?` answers the filesystem half, beside the
522
609
  # overlay-side answer to the same question, and carries the reasoning for why it asks what an entry
523
610
  # LOADS rather than what it looks like.
524
- def gem_overlay_paths(missing_gem_names:, plugin_registry:, signature_paths: [])
525
- return [] if missing_gem_names.empty?
611
+ def gem_overlay_paths(coverage:, plugin_registry:, signature_paths: [])
612
+ return [] if coverage.empty?
526
613
 
527
614
  loaded_ids = plugin_registry ? plugin_registry.ids.to_set : Set.new
528
- eligible = missing_gem_names.reject do |gem_name|
529
- plugin_id = GEM_OVERLAY_PLUGIN_IDS[gem_name]
530
- next false unless plugin_id
615
+ eligible = coverage.filter_map do |row|
616
+ next unless overlay_coverage_source?(row.gem_name, row.source)
617
+
618
+ plugin_id = GEM_OVERLAY_PLUGIN_IDS[row.gem_name]
619
+ next if plugin_id &&
620
+ (loaded_ids.include?(plugin_id) ||
621
+ RbsLoader.bundled_overlay_twin_signatures_loaded?(plugin_id, signature_paths))
531
622
 
532
- loaded_ids.include?(plugin_id) ||
533
- RbsLoader.bundled_overlay_twin_signatures_loaded?(plugin_id, signature_paths)
534
- end.sort
623
+ row.gem_name
624
+ end.uniq.sort
535
625
  RbsLoader.gem_overlay_sig_paths(eligible)
536
626
  end
537
627
 
628
+ # Whether `source` — a `RbsCoverageReport` source — is one `gem_name`'s bundled overlay may load
629
+ # under: the gem's {GEM_OVERLAY_COVERAGE_SOURCES} entry when it has one, else ADR-72's `:missing`.
630
+ def overlay_coverage_source?(gem_name, source)
631
+ (GEM_OVERLAY_COVERAGE_SOURCES[gem_name] || GEM_OVERLAY_DEFAULT_COVERAGE_SOURCES).include?(source)
632
+ end
633
+
538
634
  # ADR-32 WD4 + WD5 — for each project source file, invoke every plugin-registered synthesizer once and
539
635
  # collect non-nil returns. The returned array is `[[virtual_filename, rbs_source], ...]`; the loader
540
636
  # threads it through to `RbsLoader.new(virtual_rbs: ...)`.
@@ -3,6 +3,7 @@
3
3
  require_relative "../type"
4
4
  require_relative "range_constant"
5
5
  require_relative "content_join"
6
+ require_relative "../typing_state"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -75,8 +76,16 @@ module Rigor
75
76
  }.freeze
76
77
  private_constant :TYPE_HANDLERS
77
78
 
78
- UNREAD_SHAPE_REFUSED_KEY = :__rigor_acceptance_unread_shape_refused__
79
- private_constant :UNREAD_SHAPE_REFUSED_KEY
79
+ # ADR-129 WD1 — a Frame: {#refusing_unread} installs the kind of unread shape that answers `no` for one block
80
+ # and restores the previous one on the way out. It changes answers (`unread_shape_maybe` reads it), and the
81
+ # installed value is a Symbol (`:all`, `:array`) or nil.
82
+ UNREAD_SHAPE_REFUSED = TypingState.frame(
83
+ :acceptance_unread_shape_refused,
84
+ reason: "installed around a block by refusing_unread; read by unread_shape_maybe, which answers no for " \
85
+ "the refused kind of unread shape instead of maybe",
86
+ witness: "spec/fixtures/typing_state_witnesses/acceptance_unread_shape_refused.rb"
87
+ )
88
+ private_constant :UNREAD_SHAPE_REFUSED
80
89
 
81
90
  # Whether a `maybe` for `param` against `arg` rests on a shape that could not read its argument: a record
82
91
  # against a `Hash` with a gradual arm ({#gradual_hash?}) or an open `HashShape`, or a tuple against an
@@ -120,18 +129,14 @@ module Rigor
120
129
  # calls below the one asked (a tuple element, a `Hash` value, a union member), and those calls go
121
130
  # through each carrier's `Type#accepts`. It is set only for the duration of the block. `kind` is `:all`
122
131
  # (records and tuples) or `:array` (tuples only).
123
- def refusing_unread(kind)
124
- previous = Thread.current[UNREAD_SHAPE_REFUSED_KEY]
125
- Thread.current[UNREAD_SHAPE_REFUSED_KEY] = kind
126
- yield
127
- ensure
128
- Thread.current[UNREAD_SHAPE_REFUSED_KEY] = previous
132
+ def refusing_unread(kind, &)
133
+ TypingState.current.with(UNREAD_SHAPE_REFUSED, kind, &)
129
134
  end
130
135
 
131
136
  # The `maybe` a shape of `kind` (`:hash` or `:array`) answers for an argument it cannot read, or `no`
132
137
  # inside {#refusing_unread} for that kind.
133
138
  def unread_shape_maybe(mode, reason, kind)
134
- refused = Thread.current[UNREAD_SHAPE_REFUSED_KEY]
139
+ refused = TypingState.current[UNREAD_SHAPE_REFUSED]
135
140
  if refused == :all || refused == kind
136
141
  return Type::AcceptsResult.no(mode: mode, reasons: "#{reason} (refused as evidence)")
137
142
  end
@@ -427,6 +432,19 @@ module Rigor
427
432
  CLASS_OBJECT_NOMINALS = %w[Module Class Object Kernel BasicObject].freeze
428
433
  private_constant :CLASS_OBJECT_NOMINALS
429
434
 
435
+ # Issue #2855 — whether `type` is an unparameterised `Class` or `Module` nominal: a class object whose
436
+ # identity inference lost (`const_get` kept by `is_a?(Class)`, an `is_a?(Module)` guard). `Acceptance` still
437
+ # answers `no` for it against a `Singleton` (it is a supertype, not a subtype); `call.argument-type-mismatch`
438
+ # reads it to withhold that verdict. `Object`, `BasicObject` and `Kernel` are left out: they mean "any value",
439
+ # and `Object.new` against `singleton(X)` is a real mismatch.
440
+ CLASS_OR_MODULE_NOMINALS = %w[Class Module].freeze
441
+ private_constant :CLASS_OR_MODULE_NOMINALS
442
+
443
+ def class_or_module_nominal?(type)
444
+ type.is_a?(Type::Nominal) && type.type_args.empty? && CLASS_OR_MODULE_NOMINALS.include?(type.class_name)
445
+ end
446
+ public :class_or_module_nominal?
447
+
430
448
  def accepts_nominal_from_singleton(self_type, other_type, mode)
431
449
  if CLASS_OBJECT_NOMINALS.include?(self_type.class_name)
432
450
  return Type::AcceptsResult.yes(
@@ -850,11 +868,14 @@ module Rigor
850
868
  # carrier whose value set is visibly apart from `removed` — a `Constant` (compare values directly),
851
869
  # another `Difference` with the same removed value (already exhibits the disjointness), an
852
870
  # `IntegerRange` that skips an Integer removed value, a `Tuple` whose fixed arity differs from a
853
- # removed `Tuple`, or a `HashShape` with a required key a closed removed `HashShape` cannot hold. The
854
- # last three cover the empty witnesses of `non-zero-int`, `non-empty-array[T]` and
855
- # `non-empty-hash[K, V]`. Any other carrier — a Nominal above all — is no proof and the difference
856
- # rejects it under gradual mode, even where its values happen to exclude `removed`. A Union or
857
- # Intersection argument never reaches this rule whole: `accepts` splits it into members first.
871
+ # removed `Tuple`, a `HashShape` with a required key a closed removed `HashShape` cannot hold, or a
872
+ # `Refined` whose predicate recogniser rejects the removed `Constant`. The range, tuple and shape
873
+ # arms cover the empty witnesses of `non-zero-int`, `non-empty-array[T]` and
874
+ # `non-empty-hash[K, V]`; the `Refined` arm is what lets `non-empty-string` accept
875
+ # `decimal-int-string`, which can never hold `""`. Any other carrier — a Nominal above all — is no
876
+ # proof and the difference rejects it under gradual mode, even where its values happen to exclude
877
+ # `removed`. A Union or Intersection argument never reaches this rule whole: `accepts` splits it
878
+ # into members first.
858
879
  def accepts_difference(self_type, other_type, mode)
859
880
  base_result = accepts(self_type.base, other_type, mode: mode)
860
881
  return base_result if base_result.no?
@@ -884,16 +905,30 @@ module Rigor
884
905
  # `removed`, the meet is too.
885
906
  other_type.members.any? { |m| provably_disjoint_from_removed?(m, removed) }
886
907
  when Type::IntegerRange
887
- removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && !other_type.covers?(removed.value)
908
+ integer_range_excludes_removed?(other_type, removed)
888
909
  when Type::Tuple
889
910
  # A Tuple's arity is fixed, so it shares no inhabitant with a Tuple of another arity — the
890
911
  # zero-arity witness `non-empty-array[T]` removes included.
891
912
  removed.is_a?(Type::Tuple) && removed.elements.size != other_type.elements.size
892
913
  when Type::HashShape
893
914
  hash_shape_excludes_removed?(other_type, removed)
915
+ when Type::Refined
916
+ refined_excludes_removed?(other_type, removed)
894
917
  end
895
918
  end
896
919
 
920
+ def integer_range_excludes_removed?(range, removed)
921
+ removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && !range.covers?(removed.value)
922
+ end
923
+
924
+ # A predicate refinement excludes the removed value when its recogniser rejects the removed
925
+ # constant outright — `decimal-int-string` can never hold `""`. `matches?` answers nil for a
926
+ # predicate the registry does not know, and nil is no proof, so the comparison is `== false`
927
+ # rather than `!`.
928
+ def refined_excludes_removed?(refined, removed)
929
+ removed.is_a?(Type::Constant) && refined.matches?(removed.value) == false
930
+ end
931
+
897
932
  # Every inhabitant of `shape` holds each of its required keys, and a closed `removed` shape holds no
898
933
  # key outside its pairs, so one required key `removed` does not list makes the two disjoint. Against
899
934
  # the closed `{}` witness `non-empty-hash[K, V]` removes, that is any required key; an optional key
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../source/node_children"
6
+ require_relative "../closure_escape_analyzer"
7
+ require_relative "../remover_holder_scan"
8
+
9
+ module Rigor
10
+ module Inference
11
+ module AliasMutationFloor
12
+ # Issue #2335 — the *foreign* locals of a frame body ({AliasMutationFloor}): those whose object may have come
13
+ # from elsewhere, so they may be another local's object. A method parameter, a block or lambda parameter (but one
14
+ # of a catalogued iterator over literals that read nothing, whose parameters are those literals' own objects), a
15
+ # local any target node binds, and a local written anything but a new object.
16
+ module ForeignLocals
17
+ FRESH_VALUES = [
18
+ Prism::ArrayNode, Prism::HashNode, Prism::StringNode, Prism::InterpolatedStringNode, Prism::XStringNode,
19
+ Prism::InterpolatedXStringNode, Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode,
20
+ Prism::SymbolNode, Prism::InterpolatedSymbolNode, Prism::NilNode, Prism::TrueNode, Prism::FalseNode,
21
+ Prism::RangeNode, Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode
22
+ ].freeze
23
+ # `+"x"`, `"x".dup`, `String.new`: a new String.
24
+ FRESH_STRING_CALLS = %i[+@ dup clone].to_set.freeze
25
+ LITERAL_PARTS = [Prism::ArrayNode, Prism::HashNode, Prism::KeywordHashNode, Prism::AssocNode,
26
+ *FRESH_VALUES].freeze
27
+ private_constant :FRESH_VALUES, :FRESH_STRING_CALLS, :LITERAL_PARTS
28
+
29
+ module_function
30
+
31
+ # `[foreign, entry]`: the foreign locals, and the method parameters the body never rebinds, each frozen.
32
+ def collect(body, parameters)
33
+ foreign = Set.new
34
+ written = Set.new
35
+ collect_foreign(body, foreign, written)
36
+ finish(parameters, foreign, written)
37
+ end
38
+
39
+ # `[foreign, entry]` from the body's own `foreign` and `written` sets and the method's `parameters`.
40
+ def finish(parameters, foreign, written)
41
+ entry = Set.new
42
+ collect_parameters(parameters, entry)
43
+ [foreign.merge(entry).freeze, (entry - written).freeze]
44
+ end
45
+
46
+ def collect_parameters(node, names)
47
+ return if node.nil?
48
+
49
+ names << node.name if node.class.name.end_with?("ParameterNode") && node.respond_to?(:name) && node.name
50
+ node.rigor_each_child { |child| collect_parameters(child, names) }
51
+ end
52
+
53
+ # `foreign` gains every block or lambda parameter and every local a write binds to an object from elsewhere (an
54
+ # operator write's value is a call result); `written` every local any write or target binds. A nested def is
55
+ # not entered.
56
+ def collect_foreign(node, foreign, written, literal_blocks = Set.new.compare_by_identity)
57
+ return if node.is_a?(Prism::DefNode)
58
+
59
+ visit(node, foreign, written, literal_blocks)
60
+ node.rigor_each_child { |child| collect_foreign(child, foreign, written, literal_blocks) }
61
+ end
62
+
63
+ # {.collect_foreign}'s step for one node, which the site collector's walk also takes
64
+ # ({AliasMutationFloor::SiteCollector}).
65
+ def visit(node, foreign, written, literal_blocks)
66
+ case node
67
+ when Prism::CallNode then literal_blocks << node.block if literal_iterator_block?(node)
68
+ when Prism::BlockNode, Prism::LambdaNode
69
+ collect_parameters(node.parameters, foreign) unless literal_blocks.include?(node)
70
+ when Prism::LocalVariableTargetNode, Prism::LocalVariableOperatorWriteNode
71
+ foreign << written_name(node, written)
72
+ when Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode
73
+ name = written_name(node, written)
74
+ foreign << name unless fresh_value?(node.value)
75
+ end
76
+ end
77
+
78
+ # A catalogued iterator over literals that read nothing (`[[1], [2]].each { |x| }`,
79
+ # `[1].each_with_object({}) { |x, h| }`): its block's parameters are those literals' own new objects. Any other
80
+ # block's parameters may be any object.
81
+ def literal_iterator_block?(call_node)
82
+ return false unless call_node.block.is_a?(Prism::BlockNode)
83
+ return false unless ClosureEscapeAnalyzer.iterator_name?(call_node.name)
84
+
85
+ [call_node.receiver, *call_node.arguments&.arguments].all? do |operand|
86
+ !operand.nil? && literal_only?(operand)
87
+ end
88
+ end
89
+
90
+ def literal_only?(node)
91
+ return false unless LITERAL_PARTS.any? { |klass| node.is_a?(klass) }
92
+
93
+ node.rigor_each_child { |child| return false unless literal_only?(child) }
94
+ true
95
+ end
96
+
97
+ def written_name(node, written)
98
+ written << node.name
99
+ node.name
100
+ end
101
+
102
+ # A value that is a new object, so the local it binds holds no other holder's object.
103
+ def fresh_value?(value)
104
+ value = RemoverHolderScan.unparenthesised(value)
105
+ return true if FRESH_VALUES.any? { |klass| value.is_a?(klass) }
106
+ return false unless value.is_a?(Prism::CallNode)
107
+ return true if RemoverHolderScan.fresh_value?(value)
108
+
109
+ receiver = value.receiver
110
+ (FRESH_STRING_CALLS.include?(value.name) &&
111
+ (receiver.is_a?(Prism::StringNode) || receiver.is_a?(Prism::InterpolatedStringNode))) ||
112
+ (value.name == :new && receiver.is_a?(Prism::ConstantReadNode) && receiver.name == :String)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../typing_state"
4
+ require_relative "../../memo_audit"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module AliasMutationFloor
9
+ # ADR-129 WD1 — the floor's four run-scoped Memos, each a pure function of the key it holds, and their accessors.
10
+ # Each accessor's block computes the value: on a miss it is stored, on a hit {MemoAudit.check} compares it with
11
+ # the stored one under `RIGOR_MEMO_AUDIT=1`, and while an audit recomputation runs ({MemoAudit.suspended?}) it
12
+ # answers without reading or writing the memo.
13
+ module Memos
14
+ FACTS = TypingState.memo(
15
+ :alias_mutation_floor_facts,
16
+ run_scoped: true, audit: "alias floor facts",
17
+ reason: "keyed by the body node; the facts are a pure function of the body and its parameters"
18
+ )
19
+ IVAR = TypingState.memo(
20
+ :alias_mutation_floor_ivar_mutators,
21
+ run_scoped: true, audit: "alias floor ivar mutator",
22
+ reason: "keyed by def node; whether its body mutates an ivar is a pure function of the body"
23
+ )
24
+ FLOOR = TypingState.memo(
25
+ :alias_mutation_floor_callee_floors,
26
+ run_scoped: true, audit: "alias floor callee floor",
27
+ reason: "keyed by the literal shape; the callee block floor is a pure function of it"
28
+ )
29
+ STORE = TypingState.memo(
30
+ :alias_mutation_floor_unknown_stores,
31
+ run_scoped: true, audit: "alias floor unknown store",
32
+ reason: "keyed by call node, removed-keys mode and type, every input of the widened store"
33
+ )
34
+ private_constant :FACTS, :IVAR, :FLOOR, :STORE
35
+
36
+ module_function
37
+
38
+ # The facts of `body`. A hit is audited without the facts' per-read memos, which fill as reads are answered.
39
+ def facts(body, &)
40
+ return yield if MemoAudit.enabled? && MemoAudit.suspended?
41
+
42
+ table = TypingState.current.weak_key_map(FACTS)
43
+ facts = table[body]
44
+ return table[body] = MemoAudit.key_audit("alias floor facts", &) if facts.nil?
45
+
46
+ MemoAudit.check("alias floor facts", audited(facts), body) { audited(yield) } if MemoAudit.enabled?
47
+ facts
48
+ end
49
+
50
+ def ivar_mutator(def_node, &)
51
+ return yield if MemoAudit.enabled? && MemoAudit.suspended?
52
+
53
+ table = TypingState.current.weak_key_map(IVAR)
54
+ cached = table[def_node]
55
+ return table[def_node] = MemoAudit.key_audit("alias floor ivar mutator", &) if cached.nil?
56
+
57
+ MemoAudit.check("alias floor ivar mutator", cached, def_node, &)
58
+ end
59
+
60
+ def callee_floor(type, &)
61
+ return yield if MemoAudit.enabled? && MemoAudit.suspended?
62
+
63
+ state = TypingState.current
64
+ table = (state[FLOOR] ||= {})
65
+ return table[type] = MemoAudit.key_audit("alias floor callee floor", &) unless table.key?(type)
66
+
67
+ MemoAudit.check("alias floor callee floor", table[type], type, &)
68
+ end
69
+
70
+ def unknown_store(call_node, removed_keys, type, &)
71
+ return yield if MemoAudit.enabled? && MemoAudit.suspended?
72
+
73
+ state = TypingState.current
74
+ table = (state[STORE] ||= {}.compare_by_identity)
75
+ per_mode = ((table[call_node] ||= {})[removed_keys] ||= {})
76
+ return per_mode[type] = MemoAudit.key_audit("alias floor unknown store", &) unless per_mode.key?(type)
77
+
78
+ MemoAudit.check("alias floor unknown store", per_mode[type], call_node, type, &)
79
+ end
80
+
81
+ def audited(facts) = facts.with(memo: nil, named_memo: nil)
82
+ end
83
+ end
84
+ end
85
+ end