rigortype 0.3.5 → 0.3.7

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 (266) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/builtins/ruby_core/date.yml +6 -3
  4. data/data/builtins/ruby_core/enumerable.yml +1 -0
  5. data/data/builtins/ruby_core/exception.yml +2 -1
  6. data/data/builtins/ruby_core/file.yml +28 -14
  7. data/data/builtins/ruby_core/hash.yml +5 -2
  8. data/data/builtins/ruby_core/io.yml +33 -16
  9. data/data/builtins/ruby_core/random.yml +2 -1
  10. data/data/builtins/ruby_core/re.yml +6 -3
  11. data/data/builtins/ruby_core/struct.yml +2 -0
  12. data/data/builtins/ruby_core/time.yml +2 -1
  13. data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
  14. data/docs/handbook/02-everyday-types.md +1 -1
  15. data/docs/handbook/04-tuples-and-shapes.md +1 -1
  16. data/docs/handbook/08-understanding-errors.md +1 -1
  17. data/docs/handbook/09-plugins.md +2 -2
  18. data/docs/handbook/10-sorbet.md +1 -1
  19. data/docs/handbook/README.md +2 -2
  20. data/docs/handbook/appendix-go.md +1 -1
  21. data/docs/handbook/appendix-java-csharp.md +2 -2
  22. data/docs/handbook/appendix-mypy.md +1 -1
  23. data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
  24. data/docs/handbook/appendix-rust.md +1 -1
  25. data/docs/handbook/appendix-type-theory.md +2 -2
  26. data/docs/handbook/appendix-typescript.md +4 -4
  27. data/docs/manual/02-cli-reference.md +29 -16
  28. data/docs/manual/03-configuration.md +1 -1
  29. data/docs/manual/04-diagnostics.md +12 -0
  30. data/docs/manual/05-inspecting-types.md +20 -8
  31. data/docs/manual/07-plugins.md +4 -0
  32. data/docs/manual/08-skills.md +1 -1
  33. data/docs/manual/11-ci.md +1 -1
  34. data/docs/manual/12-caching.md +1 -1
  35. data/docs/manual/15-type-protection-coverage.md +1 -1
  36. data/docs/manual/18-removing-dead-code.md +13 -4
  37. data/docs/manual/19-effect-labels.md +19 -12
  38. data/docs/manual/README.md +2 -2
  39. data/docs/manual/plugins/rigor-actionmailer.md +4 -4
  40. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  41. data/docs/manual/plugins/rigor-activejob.md +3 -3
  42. data/docs/manual/plugins/rigor-activerecord.md +78 -5
  43. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  44. data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
  45. data/docs/manual/plugins/rigor-rspec.md +6 -2
  46. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  47. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  48. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  49. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  50. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  51. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  52. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  53. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  54. data/lib/rigor/analysis/check_rules.rb +383 -48
  55. data/lib/rigor/analysis/crash_signature.rb +116 -0
  56. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  57. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  58. data/lib/rigor/analysis/diagnostic.rb +17 -6
  59. data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
  60. data/lib/rigor/analysis/incremental.rb +28 -0
  61. data/lib/rigor/analysis/incremental_session.rb +44 -9
  62. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  63. data/lib/rigor/analysis/reachability/graph.rb +44 -16
  64. data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
  65. data/lib/rigor/analysis/reachability/scan.rb +27 -7
  66. data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
  67. data/lib/rigor/analysis/result.rb +32 -0
  68. data/lib/rigor/analysis/run_cache_key.rb +53 -1
  69. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
  70. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
  71. data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
  72. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  73. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
  74. data/lib/rigor/analysis/runner.rb +298 -38
  75. data/lib/rigor/analysis/worker_session.rb +10 -1
  76. data/lib/rigor/bleeding_edge.rb +7 -6
  77. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  78. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  79. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  80. data/lib/rigor/cache/descriptor.rb +42 -2
  81. data/lib/rigor/cache/file_digest.rb +20 -2
  82. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  83. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
  84. data/lib/rigor/cache/store.rb +10 -1
  85. data/lib/rigor/cli/check_command.rb +67 -48
  86. data/lib/rigor/cli/coverage_command.rb +28 -24
  87. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  88. data/lib/rigor/cli/coverage_scan.rb +47 -6
  89. data/lib/rigor/cli/doc_links.rb +100 -0
  90. data/lib/rigor/cli/docs_command.rb +32 -2
  91. data/lib/rigor/cli/effects_command.rb +27 -2
  92. data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
  93. data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
  94. data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
  95. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  96. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  97. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  98. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  99. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  100. data/lib/rigor/cli/sig_gen_command.rb +18 -0
  101. data/lib/rigor/cli/type_of_command.rb +188 -41
  102. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  103. data/lib/rigor/cli/type_scan_command.rb +10 -1
  104. data/lib/rigor/cli/unused_command.rb +65 -16
  105. data/lib/rigor/cli.rb +22 -9
  106. data/lib/rigor/configuration.rb +7 -4
  107. data/lib/rigor/effects/definition_lines.rb +100 -0
  108. data/lib/rigor/effects/envelope_check.rb +18 -1
  109. data/lib/rigor/effects/liskov_check.rb +17 -8
  110. data/lib/rigor/effects/plugin_facts.rb +1 -1
  111. data/lib/rigor/effects/scanner.rb +2 -3
  112. data/lib/rigor/effects/signature_sources.rb +13 -2
  113. data/lib/rigor/effects/snapshot.rb +53 -21
  114. data/lib/rigor/effects/snapshot_diff.rb +26 -3
  115. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  116. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  117. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  118. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  119. data/lib/rigor/environment/rbs_loader.rb +525 -46
  120. data/lib/rigor/environment.rb +38 -15
  121. data/lib/rigor/inference/acceptance.rb +172 -20
  122. data/lib/rigor/inference/captured_locals.rb +62 -0
  123. data/lib/rigor/inference/content_join.rb +347 -0
  124. data/lib/rigor/inference/def_handle.rb +15 -3
  125. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  126. data/lib/rigor/inference/expression_typer.rb +1020 -141
  127. data/lib/rigor/inference/fork_map.rb +6 -1
  128. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  129. data/lib/rigor/inference/hkt_registry.rb +46 -0
  130. data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
  131. data/lib/rigor/inference/index_write_widening.rb +48 -0
  132. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  133. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  134. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  135. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  136. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  137. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  138. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  139. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  140. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  141. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  142. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  143. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  144. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  145. data/lib/rigor/inference/mutation_widening.rb +244 -174
  146. data/lib/rigor/inference/narrowing.rb +331 -27
  147. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  148. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  149. data/lib/rigor/inference/precision_scanner.rb +5 -2
  150. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  151. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  152. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  153. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  154. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  155. data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
  156. data/lib/rigor/inference/version_guard.rb +229 -0
  157. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  158. data/lib/rigor/plugin/base.rb +3 -2
  159. data/lib/rigor/plugin/inflector.rb +14 -5
  160. data/lib/rigor/plugin/io_boundary.rb +107 -4
  161. data/lib/rigor/plugin/loader.rb +14 -0
  162. data/lib/rigor/protection/analysis_guard.rb +60 -0
  163. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  164. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  165. data/lib/rigor/protection/discovery_seed.rb +1 -1
  166. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  167. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  168. data/lib/rigor/protection/mutator.rb +121 -18
  169. data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
  170. data/lib/rigor/reflection.rb +182 -30
  171. data/lib/rigor/scope/discovery_index.rb +39 -1
  172. data/lib/rigor/scope.rb +385 -4
  173. data/lib/rigor/sig_gen/classification.rb +12 -1
  174. data/lib/rigor/sig_gen/generator.rb +254 -5
  175. data/lib/rigor/sig_gen/renderer.rb +1 -6
  176. data/lib/rigor/sig_gen/writer.rb +3 -0
  177. data/lib/rigor/source/constant_path.rb +79 -2
  178. data/lib/rigor/triage/catalogue.rb +1 -1
  179. data/lib/rigor/type/combinator.rb +10 -0
  180. data/lib/rigor/type/maybe.rb +47 -0
  181. data/lib/rigor/type/refined.rb +1 -2
  182. data/lib/rigor/type/result.rb +53 -0
  183. data/lib/rigor/type.rb +2 -0
  184. data/lib/rigor/version.rb +1 -1
  185. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  186. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  187. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  188. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  189. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  190. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  191. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  192. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  193. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  194. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  195. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  196. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  197. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  198. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  199. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  200. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  201. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  202. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  203. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  204. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  205. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  206. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  207. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  208. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  209. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  210. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  211. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  212. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  213. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  214. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  215. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  216. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  217. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  218. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  219. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  220. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  221. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  222. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  223. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  224. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  225. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  226. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  227. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  228. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  229. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  230. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  231. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  232. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  233. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  234. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  235. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  236. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  237. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  238. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  239. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  240. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  241. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  242. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  243. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  244. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  245. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  246. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  247. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  248. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  249. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  250. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  251. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  252. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  253. data/sig/rigor/analysis/baseline.rbs +1 -1
  254. data/sig/rigor/environment.rbs +3 -0
  255. data/sig/rigor/inference.rbs +4 -1
  256. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  257. data/sig/rigor/reflection.rbs +3 -1
  258. data/sig/rigor/scope.rbs +25 -1
  259. data/sig/rigor/type.rbs +95 -1
  260. data/sig/rigor.rbs +16 -0
  261. data/skills/rigor-ci-setup/SKILL.md +2 -2
  262. data/skills/rigor-editor-setup/SKILL.md +2 -2
  263. data/skills/rigor-mcp-setup/SKILL.md +2 -2
  264. data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
  265. data/skills/rigor-plugin-review/SKILL.md +3 -3
  266. metadata +73 -3
@@ -38,9 +38,9 @@ module Rigor
38
38
  # ADR-87 WD4 boot-slimming probe can read it without loading the inference engine.
39
39
 
40
40
  # ADR-72 — a Gemfile.lock gem name mapped to the opt-in plugin id that ships the SAME core-ext RBS. When
41
- # that plugin is loaded the auto-overlay for the gem stands down, so the two never both declare the
42
- # methods (which would raise a duplicate-declaration error). Keyed on the gem name `RbsCoverageReport`
43
- # reports.
41
+ # that plugin's signatures are reachable the auto-overlay for the gem stands down, so the two never both
42
+ # declare the methods (which would raise a duplicate-declaration error). Keyed on the gem name
43
+ # `RbsCoverageReport` reports.
44
44
  GEM_OVERLAY_PLUGIN_IDS = { "activesupport" => "activesupport-core-ext" }.freeze
45
45
 
46
46
  attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
@@ -190,7 +190,7 @@ module Rigor
190
190
  # {Environment::RbsLoader} so constant lookups (and, in later v0.0.9 slices, other reflection
191
191
  # artefacts) consult the cache. Pass `nil` (the default) to skip caching for this environment.
192
192
  # @return [Rigor::Environment]
193
- # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
193
+ # rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists
194
194
  def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil,
195
195
  plugin_registry: nil, dependency_source_index: nil,
196
196
  rbs_extended_reporter: nil, boundary_cross_reporter: nil,
@@ -253,11 +253,13 @@ module Rigor
253
253
  # extensions resolve (e.g. `Integer#minutes` on a Rails project) — turning a systematic false
254
254
  # `call.undefined-method` into a no-op while a project WITHOUT the gem still sees the genuine
255
255
  # diagnostic. Appended last so any RBS the project already supplies wins, and skipped for a gem whose
256
- # opt-in plugin twin is loaded (no duplicate declaration). The paths ride in
257
- # `loader_signature_paths`, so the env cache descriptor digests them for free.
256
+ # opt-in plugin twin is REACHABLE — loaded as a plugin, or wired in by `signature_paths:` (#672) —
257
+ # so the two never both declare the methods. The paths ride in `loader_signature_paths`, so the env
258
+ # cache descriptor digests them for free.
258
259
  missing_gems, overlay_paths = missing_gems_and_overlay_paths(
259
260
  locked: locked, default_libraries: merged_libraries, bundle_sig_paths: gem_sig_paths,
260
- rbs_collection_paths: collection_paths, plugin_registry: plugin_registry
261
+ rbs_collection_paths: collection_paths, plugin_registry: plugin_registry,
262
+ signature_paths: resolved_paths
261
263
  )
262
264
  loader_signature_paths = resolved_paths + plugin_sig_paths + gem_sig_paths +
263
265
  collection_paths + overlay_paths
@@ -276,7 +278,11 @@ module Rigor
276
278
  libraries: merged_libraries,
277
279
  signature_paths: loader_signature_paths,
278
280
  cache_store: cache_store,
279
- virtual_rbs: virtual_rbs
281
+ virtual_rbs: virtual_rbs,
282
+ # Issue #610 — the plugin-contributed subset is loaded LAST and may stand down against a colliding
283
+ # generic arity. Only the bundled plugins' own `sig/` is deferred: the user's `signature_paths:`,
284
+ # the bundle walk and `rbs collection install` all stay authoritative.
285
+ deferred_signature_paths: plugin_sig_paths
280
286
  )
281
287
  # ADR-20 slice 2c + 2e — seed hkt_registry with the bundled builtins. The Environment's
282
288
  # `#hkt_registry` getter then LAZILY merges in the RBS env scan on first call so fast paths that
@@ -297,7 +303,6 @@ module Rigor
297
303
  missing_rbs_bundle_path: bundle_root
298
304
  )
299
305
  end
300
- # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
301
306
 
302
307
  private
303
308
 
@@ -330,27 +335,45 @@ module Rigor
330
335
  # and consumed twice: as `[gem_name, version]` pairs for the ADR-82 WD9 constant-ownership index, and
331
336
  # as the eligible-gem set for the ADR-72 overlay resolution. Returns `[pairs, overlay_paths]`.
332
337
  def missing_gems_and_overlay_paths(locked:, default_libraries:, bundle_sig_paths:,
333
- rbs_collection_paths:, plugin_registry:)
338
+ rbs_collection_paths:, plugin_registry:, signature_paths: [])
334
339
  return [[], []] if locked.empty?
335
340
 
336
341
  rows = RbsCoverageReport.classify(
337
342
  locked_gems: locked, default_libraries: default_libraries,
338
343
  bundle_sig_paths: bundle_sig_paths, rbs_collection_paths: rbs_collection_paths
339
344
  ).select { |row| row.source == :missing }
340
- overlays = gem_overlay_paths(missing_gem_names: rows.map(&:gem_name), plugin_registry: plugin_registry)
345
+ overlays = gem_overlay_paths(
346
+ missing_gem_names: rows.map(&:gem_name), plugin_registry: plugin_registry,
347
+ signature_paths: signature_paths
348
+ )
341
349
  [rows.map { |row| [row.gem_name, row.version] }, overlays]
342
350
  end
343
351
 
344
352
  # ADR-72 — resolve the bundled RBS overlay directories to load for this project. A gem is eligible when
345
- # it is classified `:missing` (see {missing_rbs_gem_rows}) and its conflicting opt-in plugin (if any)
346
- # is not loaded. Returns `[Pathname]`, deterministically ordered, or `[]` when no eligible gem.
347
- def gem_overlay_paths(missing_gem_names:, plugin_registry:)
353
+ # it is classified `:missing` (see {missing_rbs_gem_rows}) and its conflicting opt-in plugin twin (if
354
+ # any) is NOT already reachable. Returns `[Pathname]`, deterministically ordered, or `[]` when no
355
+ # eligible gem.
356
+ #
357
+ # "Reachable" is two questions, not one. The plugin being LOADED is the route ADR-72 anticipated. The
358
+ # second is the route issue #672 found: a project that wires the plugin's signatures through
359
+ # `signature_paths:` instead of `plugins:` reaches the same `.rbs` with no registry entry to key on, so
360
+ # both halves loaded, `RBS::DefinitionBuilder` raised `DuplicatedMethodDefinitionError` for every class
361
+ # they share, and each one silently degraded to `Dynamic[top]` — the run reported LESS while exiting 0.
362
+ #
363
+ # This method keeps the POLICY (which gem maps to which plugin id, and when an overlay is eligible at
364
+ # all); `RbsLoader.bundled_overlay_twin_signatures_loaded?` answers the filesystem half, beside the
365
+ # overlay-side answer to the same question, and carries the reasoning for why it asks what an entry
366
+ # LOADS rather than what it looks like.
367
+ def gem_overlay_paths(missing_gem_names:, plugin_registry:, signature_paths: [])
348
368
  return [] if missing_gem_names.empty?
349
369
 
350
370
  loaded_ids = plugin_registry ? plugin_registry.ids.to_set : Set.new
351
371
  eligible = missing_gem_names.reject do |gem_name|
352
372
  plugin_id = GEM_OVERLAY_PLUGIN_IDS[gem_name]
353
- plugin_id && loaded_ids.include?(plugin_id)
373
+ next false unless plugin_id
374
+
375
+ loaded_ids.include?(plugin_id) ||
376
+ RbsLoader.bundled_overlay_twin_signatures_loaded?(plugin_id, signature_paths)
354
377
  end.sort
355
378
  RbsLoader.gem_overlay_sig_paths(eligible)
356
379
  end
@@ -23,7 +23,7 @@ module Rigor
23
23
  # `Nominal[Hash, [K, V]]` the shape is projected to its underlying nominal so the existing
24
24
  # generic-acceptance pipeline continues to apply; the converse direction (a Tuple receiver accepting a
25
25
  # generic Array) stays conservative because the analyzer cannot verify arity from a raw nominal alone.
26
- # rubocop:disable Metrics/ModuleLength
26
+ # rubocop:disable-next Metrics/ModuleLength
27
27
  module Acceptance
28
28
  module_function
29
29
 
@@ -67,11 +67,15 @@ module Rigor
67
67
  Type::Refined => :accepts_refined,
68
68
  Type::Intersection => :accepts_intersection,
69
69
  Type::Tuple => :accepts_tuple,
70
- Type::HashShape => :accepts_hash_shape
70
+ Type::HashShape => :accepts_hash_shape,
71
+ Type::Result => :accepts_result,
72
+ Type::Maybe => :accepts_maybe,
73
+ Type::StructClass => :accepts_class_factory,
74
+ Type::DataClass => :accepts_class_factory
71
75
  }.freeze
72
76
  private_constant :TYPE_HANDLERS
73
77
 
74
- # rubocop:disable Metrics/ClassLength
78
+ # rubocop:disable-next Metrics/ClassLength
75
79
  class << self
76
80
  private
77
81
 
@@ -165,20 +169,28 @@ module Rigor
165
169
  end
166
170
  end
167
171
 
168
- # Singleton[C] only accepts another Singleton[D] where D is a subclass of (or equal to) C. Any other
169
- # carrier (instance, constant, ...) is no, because the singleton type's inhabitants are the class
170
- # objects themselves.
172
+ # Singleton[C] only accepts another Singleton[D] (or StructClass / DataClass) where D is a subclass
173
+ # of (or equal to) C. Any other carrier (instance, constant, ...) is no, because the singleton type's
174
+ # inhabitants are the class objects themselves.
171
175
  def accepts_singleton(self_type, other_type, mode)
172
- unless other_type.is_a?(Type::Singleton)
173
- return Type::AcceptsResult.no(
174
- mode: mode,
175
- reasons: "Singleton[#{self_type.class_name}] does not accept #{other_type.class}"
176
- )
177
- end
176
+ actual_name =
177
+ case other_type
178
+ when Type::Singleton then other_type.class_name
179
+ when Type::StructClass, Type::DataClass
180
+ return Type::AcceptsResult.yes(mode: mode, reasons: "exact name match") if
181
+ other_type.class_name == self_type.class_name
182
+
183
+ class_factory_base_name(other_type)
184
+ else
185
+ return Type::AcceptsResult.no(
186
+ mode: mode,
187
+ reasons: "Singleton[#{self_type.class_name}] does not accept #{other_type.class}"
188
+ )
189
+ end
178
190
 
179
191
  class_subtype_result(
180
192
  target_name: self_type.class_name,
181
- actual_name: other_type.class_name,
193
+ actual_name: actual_name,
182
194
  mode: mode,
183
195
  kind: :singleton
184
196
  )
@@ -195,11 +207,13 @@ module Rigor
195
207
  # - HashShape{*} when self is the Hash (or a supertype) family, projected to
196
208
  # `Nominal[Hash, [union(keys), union(values)]]`.
197
209
  # - Singleton: never (wrong value kind).
210
+ # - StructClass / DataClass: class-side carriers, accepted when C is a meta nominal (Class, Module, ...).
198
211
  def accepts_nominal(self_type, other_type, mode)
199
212
  case other_type
200
213
  when Type::Nominal then accepts_nominal_from_nominal(self_type, other_type, mode)
201
214
  when Type::Constant then accepts_nominal_from_constant(self_type, other_type, mode)
202
215
  when Type::Singleton then accepts_nominal_from_singleton(self_type, other_type, mode)
216
+ when Type::StructClass, Type::DataClass then accepts_nominal_from_class_factory(self_type, other_type, mode)
203
217
  when Type::IntegerRange then accepts_nominal_from_integer_range(self_type, other_type, mode)
204
218
  else accepts_nominal_from_shape(self_type, other_type, mode)
205
219
  end
@@ -234,6 +248,8 @@ module Rigor
234
248
  # is bounded because every refinement carrier's `base` is closer to the nominal layer.
235
249
  accepts(self_type, other_type.base, mode: mode)
236
250
  .with_reason("projected #{other_type.class.name.split('::').last} to its base")
251
+ when Type::Result, Type::Maybe
252
+ accepts_nominal_from_monad(self_type, other_type, mode)
237
253
  else
238
254
  Type::AcceptsResult.no(
239
255
  mode: mode,
@@ -242,6 +258,32 @@ module Rigor
242
258
  end
243
259
  end
244
260
 
261
+ def accepts_nominal_from_monad(self_type, other_type, mode)
262
+ cls = self_type.class_name.sub(/\A::/, "")
263
+ case other_type
264
+ when Type::Result
265
+ unless %w[Dry::Monads::Result Result].include?(cls)
266
+ return Type::AcceptsResult.no(mode: mode, reasons: "Nominal[#{self_type.class_name}] rejects Result")
267
+ end
268
+
269
+ target = Type::Combinator.nominal_of(
270
+ self_type.class_name,
271
+ type_args: [other_type.ok_type, other_type.err_type]
272
+ )
273
+ accepts(self_type, target, mode: mode).with_reason("projected Result to Nominal")
274
+ when Type::Maybe
275
+ unless %w[Dry::Monads::Maybe Maybe].include?(cls)
276
+ return Type::AcceptsResult.no(mode: mode, reasons: "Nominal[#{self_type.class_name}] rejects Maybe")
277
+ end
278
+
279
+ target = Type::Combinator.nominal_of(
280
+ self_type.class_name,
281
+ type_args: [other_type.value_type]
282
+ )
283
+ accepts(self_type, target, mode: mode).with_reason("projected Maybe to Nominal")
284
+ end
285
+ end
286
+
245
287
  # `Nominal[Integer]` (and anything Integer is-a, like Numeric) accepts any `IntegerRange`; nothing
246
288
  # else does. Argument-bearing `Nominal`s never accept `IntegerRange` because IntegerRange has no
247
289
  # type args.
@@ -275,11 +317,11 @@ module Rigor
275
317
  # introspection patterns. The rule conservatively answers `:yes` for `Module` (every singleton is at
276
318
  # least a Module) and for `Class` / `Object` / `BasicObject` (the class object inherits from those).
277
319
  # Other Nominals fall through to the default `:no`.
278
- META_NOMINALS_FROM_SINGLETON = %w[Module Class Object BasicObject].freeze
279
- private_constant :META_NOMINALS_FROM_SINGLETON
320
+ CLASS_OBJECT_NOMINALS = %w[Module Class Object Kernel BasicObject].freeze
321
+ private_constant :CLASS_OBJECT_NOMINALS
280
322
 
281
323
  def accepts_nominal_from_singleton(self_type, other_type, mode)
282
- if META_NOMINALS_FROM_SINGLETON.include?(self_type.class_name)
324
+ if CLASS_OBJECT_NOMINALS.include?(self_type.class_name)
283
325
  return Type::AcceptsResult.yes(
284
326
  mode: mode,
285
327
  reasons: "Singleton[#{other_type.class_name}] is-a #{self_type.class_name}"
@@ -292,6 +334,38 @@ module Rigor
292
334
  )
293
335
  end
294
336
 
337
+ def accepts_nominal_from_class_factory(self_type, other_type, mode)
338
+ if CLASS_OBJECT_NOMINALS.include?(self_type.class_name) && self_type.type_args.empty?
339
+ return Type::AcceptsResult.yes(
340
+ mode: mode,
341
+ reasons: "#{other_type.describe} is-a #{self_type.class_name}"
342
+ )
343
+ end
344
+
345
+ if self_type.class_name == "Class" && self_type.type_args.one?
346
+ return accepts_class_factory_instance_arg(self_type.type_args.first, other_type, mode)
347
+ end
348
+
349
+ Type::AcceptsResult.no(
350
+ mode: mode,
351
+ reasons: "Nominal[#{self_type.class_name}] rejects #{other_type.class}"
352
+ )
353
+ end
354
+
355
+ def accepts_class_factory_instance_arg(formal, factory, mode)
356
+ if factory.class_name
357
+ named = accepts(formal, Type::Combinator.nominal_of(factory.class_name), mode: mode)
358
+ return named.with_reason("matched the factory class name") if named.yes?
359
+ end
360
+
361
+ accepts(formal, Type::Combinator.nominal_of(class_factory_base_name(factory)), mode: mode)
362
+ .with_reason("projected the factory instance type")
363
+ end
364
+
365
+ def class_factory_base_name(factory)
366
+ factory.is_a?(Type::StructClass) ? "Struct" : "Data"
367
+ end
368
+
295
369
  def accepts_nominal_from_nominal(self_type, other_type, mode)
296
370
  class_result = class_subtype_result(
297
371
  target_name: self_type.class_name,
@@ -753,6 +827,63 @@ module Rigor
753
827
  Type::AcceptsResult.no(mode: mode, reasons: reason)
754
828
  end
755
829
 
830
+ def accepts_result(self_type, other_type, mode)
831
+ case other_type
832
+ when Type::Result
833
+ combine_arg_results(
834
+ [
835
+ accepts(self_type.ok_type, other_type.ok_type, mode: mode),
836
+ accepts(self_type.err_type, other_type.err_type, mode: mode)
837
+ ],
838
+ mode
839
+ )
840
+ when Type::Nominal
841
+ cls = other_type.class_name.sub(/\A::/, "")
842
+ if %w[Dry::Monads::Result Result].include?(cls) && other_type.type_args.size == 2
843
+ combine_arg_results(
844
+ [
845
+ accepts(self_type.ok_type, other_type.type_args[0], mode: mode),
846
+ accepts(self_type.err_type, other_type.type_args[1], mode: mode)
847
+ ],
848
+ mode
849
+ )
850
+ else
851
+ Type::AcceptsResult.no(mode: mode, reasons: "Result does not accept Nominal[#{other_type.class_name}]")
852
+ end
853
+ else
854
+ Type::AcceptsResult.no(
855
+ mode: mode,
856
+ reasons: "Result does not accept #{other_type.class}"
857
+ )
858
+ end
859
+ end
860
+
861
+ def accepts_maybe(self_type, other_type, mode)
862
+ case other_type
863
+ when Type::Maybe
864
+ accepts(self_type.value_type, other_type.value_type, mode: mode)
865
+ when Type::Nominal
866
+ cls = other_type.class_name.sub(/\A::/, "")
867
+ if %w[Dry::Monads::Maybe Maybe].include?(cls) && other_type.type_args.size == 1
868
+ accepts(self_type.value_type, other_type.type_args[0], mode: mode)
869
+ else
870
+ Type::AcceptsResult.no(mode: mode, reasons: "Maybe does not accept Nominal[#{other_type.class_name}]")
871
+ end
872
+ else
873
+ Type::AcceptsResult.no(
874
+ mode: mode,
875
+ reasons: "Maybe does not accept #{other_type.class}"
876
+ )
877
+ end
878
+ end
879
+
880
+ def accepts_class_factory(self_type, other_type, mode)
881
+ Type::AcceptsResult.no(
882
+ mode: mode,
883
+ reasons: "#{self_type.describe} rejects #{other_type.class}"
884
+ )
885
+ end
886
+
756
887
  # Uses Ruby's actual class hierarchy via Object.const_get to answer "is D a subclass of C?" for core,
757
888
  # stdlib, and application classes. When either name fails to resolve we surface "maybe": the caller
758
889
  # (overload selector) treats yes/maybe identically, so the conservative answer keeps overload
@@ -791,14 +922,35 @@ module Rigor
791
922
  end
792
923
  end
793
924
 
925
+ # Resolves a class name taken from the analysed program without triggering `const_missing`
926
+ # or executing a pending autoload.
927
+ #
928
+ # `const_defined?(part, false)` answers true for a REGISTERED-BUT-NOT-YET-TRIGGERED autoload,
929
+ # so on its own it does not mean "already in memory" and `const_get` would execute the target
930
+ # file inside the analyzer. `Module#autoload?` separates the two: it returns the registered
931
+ # path while the autoload is pending and nil once it has run, so declining on it declines
932
+ # exactly the dangerous case and still resolves a class genuinely loaded ([#680], [#689]).
933
+ #
934
+ # The rescue catches `StandardError`, `ScriptError` and `SystemExit`. `Interrupt`,
935
+ # `SignalException` and `NoMemoryError` stay uncaught on purpose.
794
936
  def resolve_class(name)
795
- Object.const_get(name)
796
- rescue NameError
937
+ return nil unless name.is_a?(::String)
938
+
939
+ parts = name.delete_prefix("::").split("::")
940
+ return nil if parts.empty?
941
+
942
+ mod = ::Object
943
+ parts.each do |part|
944
+ return nil unless mod.is_a?(::Module) && mod.const_defined?(part, false)
945
+ return nil if mod.autoload?(part)
946
+
947
+ mod = mod.const_get(part, false)
948
+ end
949
+ mod.is_a?(::Module) ? mod : nil
950
+ rescue ::StandardError, ::ScriptError, ::SystemExit
797
951
  nil
798
952
  end
799
953
  end
800
- # rubocop:enable Metrics/ClassLength
801
954
  end
802
- # rubocop:enable Metrics/ModuleLength
803
955
  end
804
956
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_walker"
6
+ require_relative "block_parameter_binder"
7
+
8
+ module Rigor
9
+ module Inference
10
+ # The outer locals a block body can REBIND — the one name set that ADR-56's captured-local write-back
11
+ # (`StatementEvaluator#write_back_block_captures`), the escaping-block narrowing drop
12
+ # (`StatementEvaluator#drop_captured_narrowing`) and issue #587's per-element fold
13
+ # (`ExpressionTyper#per_element_captured_bindings`) all key on. It lives in one place so the four
14
+ # cannot disagree about what "captured" means.
15
+ #
16
+ # A write counts across every local-write form — plain `=` (`LocalVariableWriteNode`), the operator /
17
+ # `||=` / `&&=` compound forms, and a multi-assign target (`x, y = …` → `LocalVariableTargetNode` under
18
+ # a `MultiWriteNode`) — at ANY depth: a block is a closure, so a write inside a nested block binds the
19
+ # same outer variable. Block-introduced names (parameters, numbered parameters, `;`-locals) and names
20
+ # not bound in the outer scope are excluded; a write to either is not a captured rebind of an outer
21
+ # variable.
22
+ module CapturedLocals
23
+ LOCAL_WRITE_NODES = [
24
+ Prism::LocalVariableWriteNode,
25
+ Prism::LocalVariableOperatorWriteNode,
26
+ Prism::LocalVariableOrWriteNode,
27
+ Prism::LocalVariableAndWriteNode,
28
+ Prism::LocalVariableTargetNode
29
+ ].freeze
30
+
31
+ module_function
32
+
33
+ # @param block_node [Prism::BlockNode]
34
+ # @param base_scope [Rigor::Scope] the call-site scope the block closes over.
35
+ # @return [Array<Symbol>] the captured names the body writes, each once, in first-write order.
36
+ def writes(block_node, base_scope)
37
+ body = block_node.body
38
+ return [] if body.nil?
39
+
40
+ introduced = introduced_locals(block_node)
41
+ outer_writes = []
42
+ Source::NodeWalker.each(body) do |descendant|
43
+ next unless LOCAL_WRITE_NODES.any? { |klass| descendant.is_a?(klass) }
44
+ next if introduced.include?(descendant.name)
45
+ next unless base_scope.locals.key?(descendant.name)
46
+
47
+ outer_writes << descendant.name
48
+ end
49
+ outer_writes.uniq
50
+ end
51
+
52
+ # Names the block itself introduces: parameters (numbered parameters included, via
53
+ # `BlockParameterBinder`) plus the explicit `;`-prefixed block-locals on `BlockParametersNode`.
54
+ def introduced_locals(block_node)
55
+ introduced = Set.new(BlockParameterBinder.new.bind(block_node).keys)
56
+ params_root = block_node.parameters
57
+ params_root.locals.each { |loc| introduced << loc.name } if params_root.is_a?(Prism::BlockParametersNode)
58
+ introduced
59
+ end
60
+ end
61
+ end
62
+ end