rigortype 0.3.8 → 0.4.0

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 (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../type"
4
+ require_relative "range_constant"
5
+ require_relative "content_join"
4
6
 
5
7
  module Rigor
6
8
  module Inference
@@ -27,10 +29,7 @@ module Rigor
27
29
  module Acceptance
28
30
  module_function
29
31
 
30
- # @param self_type [Rigor::Type]
31
- # @param other_type [Rigor::Type]
32
- # @param mode [Symbol] `:gradual` (default) or `:strict`.
33
- # @return [Rigor::Type::AcceptsResult]
32
+ # @param mode — `:gradual` (default) or `:strict`.
34
33
  def accepts(self_type, other_type, mode: :gradual)
35
34
  raise ArgumentError, "Acceptance mode #{mode.inspect} is not implemented yet" unless mode == :gradual
36
35
 
@@ -63,6 +62,7 @@ module Rigor
63
62
  Type::Nominal => :accepts_nominal,
64
63
  Type::Constant => :accepts_constant,
65
64
  Type::IntegerRange => :accepts_integer_range,
65
+ Type::FloatRange => :accepts_float_range,
66
66
  Type::Difference => :accepts_difference,
67
67
  Type::Refined => :accepts_refined,
68
68
  Type::Intersection => :accepts_intersection,
@@ -75,10 +75,64 @@ module Rigor
75
75
  }.freeze
76
76
  private_constant :TYPE_HANDLERS
77
77
 
78
+ UNREAD_HASH_REFUSED_KEY = :__rigor_acceptance_unread_hash_refused__
79
+ private_constant :UNREAD_HASH_REFUSED_KEY
80
+
81
+ # Whether a `maybe` for `param` against `arg` rests on a record that could not read a hash's entries: a
82
+ # `Hash` with a gradual arm ({#gradual_hash?}) or an open `HashShape`. The record cannot prove the key set
83
+ # of either, and its `maybe` climbs through whatever holds the record. `OverloadSelector`'s strict pass
84
+ # reads that `maybe` as no evidence for the overload, so this re-asks with each such record answering
85
+ # `no` and reports whether the pair then fails. A `maybe` from anywhere else (a subclass only the Ruby
86
+ # source declares, a splat under a parameter that is no record) survives the re-ask and still counts.
87
+ def maybe_rests_on_unread_hash?(param, arg)
88
+ return false unless contains_type?(param) { |type| type.is_a?(Type::HashShape) }
89
+ return false unless contains_type?(arg) { |type| unread_hash?(type) }
90
+
91
+ refusing_unread_hashes { accepts(param, arg, mode: :gradual).no? }
92
+ end
93
+
78
94
  # rubocop:disable-next Metrics/ClassLength
79
95
  class << self
80
96
  private
81
97
 
98
+ # The flag lives on the thread, not on a parameter, because the record may sit any number of `accepts`
99
+ # calls below the one asked (a tuple element, a `Hash` value, a union member), and those calls go
100
+ # through each carrier's `Type#accepts`. It is set only for the duration of the block.
101
+ def refusing_unread_hashes
102
+ previous = Thread.current[UNREAD_HASH_REFUSED_KEY]
103
+ Thread.current[UNREAD_HASH_REFUSED_KEY] = true
104
+ yield
105
+ ensure
106
+ Thread.current[UNREAD_HASH_REFUSED_KEY] = previous
107
+ end
108
+
109
+ # The `maybe` a record answers for a hash whose entries it cannot read, or `no` inside
110
+ # {#refusing_unread_hashes}.
111
+ def unread_hash_maybe(mode, reason)
112
+ return hash_shape_no(mode, "#{reason} (refused as evidence)") if Thread.current[UNREAD_HASH_REFUSED_KEY]
113
+
114
+ Type::AcceptsResult.maybe(mode: mode, reasons: reason)
115
+ end
116
+
117
+ def unread_hash?(type)
118
+ gradual_hash?(type) || (type.is_a?(Type::HashShape) && type.open?)
119
+ end
120
+
121
+ def contains_type?(type, &)
122
+ return true if yield(type)
123
+
124
+ components =
125
+ case type
126
+ when Type::Union then type.members
127
+ when Type::Nominal then type.type_args
128
+ when Type::Tuple then type.elements
129
+ when Type::HashShape then type.pairs.values
130
+ when Type::Difference then [type.base]
131
+ else return false
132
+ end
133
+ components.any? { |component| contains_type?(component, &) }
134
+ end
135
+
82
136
  def accepts_one(self_type, other_type, mode)
83
137
  handler = TYPE_HANDLERS[self_type.class]
84
138
  return send(handler, self_type, other_type, mode) if handler
@@ -201,7 +255,8 @@ module Rigor
201
255
  # {#accepts_nominal_args});
202
256
  # - Constant[v] when v.is_a?(klass(C)). The type_args of self are ignored here because a Constant
203
257
  # carries a concrete value, not a generic instantiation, and the analyzer has no way to refute the
204
- # args from a literal alone.
258
+ # args from a literal alone. A Range value is the one exception — see
259
+ # {#accepts_range_constant_type_arg}.
205
260
  # - Tuple[*] when self is the Array (or a supertype) family. The Tuple is projected to
206
261
  # `Nominal[Array, [union(elements)]]` so the existing generic-arg machinery handles it.
207
262
  # - HashShape{*} when self is the Hash (or a supertype) family, projected to
@@ -215,6 +270,7 @@ module Rigor
215
270
  when Type::Singleton then accepts_nominal_from_singleton(self_type, other_type, mode)
216
271
  when Type::StructClass, Type::DataClass then accepts_nominal_from_class_factory(self_type, other_type, mode)
217
272
  when Type::IntegerRange then accepts_nominal_from_integer_range(self_type, other_type, mode)
273
+ when Type::FloatRange then accepts_nominal_from_float_range(self_type, other_type, mode)
218
274
  else accepts_nominal_from_shape(self_type, other_type, mode)
219
275
  end
220
276
  end
@@ -311,6 +367,32 @@ module Rigor
311
367
  end
312
368
  end
313
369
 
370
+ # ADR-109 WD4 — the Float twin of the IntegerRange rule: `Nominal[Float]` and everything Float
371
+ # is-a accept any `FloatRange`; nothing else does.
372
+ FLOAT_NOMINAL_ANCESTORS = %w[Float Numeric Comparable Object BasicObject].freeze
373
+ private_constant :FLOAT_NOMINAL_ANCESTORS
374
+
375
+ def accepts_nominal_from_float_range(self_type, _other_type, mode)
376
+ unless self_type.type_args.empty?
377
+ return Type::AcceptsResult.no(
378
+ mode: mode,
379
+ reasons: "Nominal[#{self_type.class_name}] with type args rejects FloatRange"
380
+ )
381
+ end
382
+
383
+ if FLOAT_NOMINAL_ANCESTORS.include?(self_type.class_name)
384
+ Type::AcceptsResult.yes(
385
+ mode: mode,
386
+ reasons: "FloatRange is-a #{self_type.class_name}"
387
+ )
388
+ else
389
+ Type::AcceptsResult.no(
390
+ mode: mode,
391
+ reasons: "Nominal[#{self_type.class_name}] rejects FloatRange"
392
+ )
393
+ end
394
+ end
395
+
314
396
  # v0.0.2 — meta-type rule. A `Singleton[T]` is the class object for `T`, so it is an instance of
315
397
  # `Class` (when `T` is a class) and always an instance of `Module`. Without this rule a method whose
316
398
  # parameter is typed `Class | Module` would reject every `is_a?(SomeClass)` call and similar
@@ -492,7 +574,7 @@ module Rigor
492
574
 
493
575
  def accepts_nominal_from_constant(self_type, constant, mode)
494
576
  ruby_class = resolve_class(self_type.class_name)
495
- return constant_is_a_result(ruby_class, constant, self_type, mode) if ruby_class
577
+ return accepts_loaded_class_from_constant(ruby_class, constant, self_type, mode) if ruby_class
496
578
 
497
579
  # The host process may not have required the constant's declared self_type (e.g. `BigDecimal`
498
580
  # since Ruby 3.4 is no longer a default gem). Fall back to inspecting the value's own class
@@ -527,6 +609,13 @@ module Rigor
527
609
  end
528
610
  end
529
611
 
612
+ def accepts_loaded_class_from_constant(ruby_class, constant, self_type, mode)
613
+ result = constant_is_a_result(ruby_class, constant, self_type, mode)
614
+ return result unless result.yes?
615
+
616
+ accepts_range_constant_type_arg(self_type, constant, mode) || result
617
+ end
618
+
530
619
  def constant_is_a_result(ruby_class, constant, self_type, mode)
531
620
  if constant.value.is_a?(ruby_class)
532
621
  Type::AcceptsResult.yes(mode: mode, reasons: "Constant value is_a?(#{self_type.class_name})")
@@ -538,10 +627,53 @@ module Rigor
538
627
  end
539
628
  end
540
629
 
630
+ # Issue #833 — the single exception to "the type_args of self are ignored for a Constant".
631
+ #
632
+ # A `Constant<Range>` is the one literal that does pin a generic instantiation: the engine
633
+ # builds it only when both endpoints are static, so `Range[T]` CAN be refuted from the literal.
634
+ # Without this, `Nominal[Range, [Integer]]` accepted `0.0...1.0` and `OverloadSelector` pass 1
635
+ # pinned whichever `Range[…]` arm came first in declaration order — for `Kernel#rand` that is
636
+ # `(::Range[Integer]) -> Integer?`, ahead of every Float arm.
637
+ #
638
+ # Returns nil — "no opinion, keep the class-level answer" — outside the envelope: a raw `Range`
639
+ # or any other arity of type_args, and a range the shared reader cannot read endpoints from.
640
+ # A missing endpoint contributes nothing, because a beginless or endless literal inhabits
641
+ # `Range[T]` for every T (`Range[Integer?]` is the common core spelling of the slicing param).
642
+ #
643
+ # Known coarse spot, deliberately not special-cased: a mixed-endpoint literal (`1.0..2`) is
644
+ # rejected by `Range[Integer]` and by `Range[Float]` alike, so overload selection falls through
645
+ # to its gradual pass and then to the first-overload fallback exactly as it did before.
646
+ def accepts_range_constant_type_arg(self_type, constant, mode)
647
+ return nil unless self_type.type_args.size == 1
648
+
649
+ endpoints = RangeConstant.literal_endpoints(constant)
650
+ return nil if endpoints.nil?
651
+
652
+ element = self_type.type_args.first
653
+ combine_range_endpoint_results(
654
+ endpoints.map { |endpoint| accepts(element, Type::Combinator.constant_of(endpoint), mode: mode) },
655
+ element,
656
+ mode
657
+ )
658
+ end
659
+
660
+ def combine_range_endpoint_results(per_endpoint, element, mode)
661
+ if per_endpoint.any?(&:no?)
662
+ Type::AcceptsResult.no(mode: mode, reasons: "a Range endpoint is rejected by #{element.describe}")
663
+ elsif per_endpoint.any?(&:maybe?)
664
+ Type::AcceptsResult.maybe(
665
+ mode: mode,
666
+ reasons: "a Range endpoint could not be proven accepted by #{element.describe}"
667
+ )
668
+ else
669
+ Type::AcceptsResult.yes(mode: mode, reasons: "every Range endpoint accepted by #{element.describe}")
670
+ end
671
+ end
672
+
541
673
  # IntegerRange[a..b] accepts:
542
674
  # - Constant[n] where n is an Integer covered by [a..b];
543
675
  # - IntegerRange[c..d] where [c..d] ⊆ [a..b];
544
- # - Nominal[Integer] only when self is the universal range (`int<min, max>`), since otherwise an
676
+ # - Nominal[Integer] only when self is the universal range (`Integer[min..max]`), since otherwise an
545
677
  # arbitrary Integer could fall outside the bound.
546
678
  # Anything else is rejected.
547
679
  def accepts_integer_range(self_type, other_type, mode)
@@ -616,13 +748,77 @@ module Rigor
616
748
  end
617
749
  end
618
750
 
751
+ # ADR-109 WD4 — `FloatRange` accepts:
752
+ # - Constant[f] where f is a Float the range covers (never NaN: `cover?` compares);
753
+ # - FloatRange[c..d] whose canonical closed bounds sit inside self's.
754
+ # `Nominal[Float]` is never accepted, not even by the universal range: an arbitrary Float may be
755
+ # NaN, and the universal range is exactly "every Float except NaN".
756
+ def accepts_float_range(self_type, other_type, mode)
757
+ case other_type
758
+ when Type::Constant
759
+ accepts_float_range_from_constant(self_type, other_type, mode)
760
+ when Type::FloatRange
761
+ accepts_float_range_from_float_range(self_type, other_type, mode)
762
+ when Type::Nominal
763
+ Type::AcceptsResult.no(
764
+ mode: mode,
765
+ reasons: "#{self_type.describe} rejects Nominal[#{other_type.class_name}] (could be NaN or fall outside)"
766
+ )
767
+ else
768
+ Type::AcceptsResult.no(
769
+ mode: mode,
770
+ reasons: "FloatRange rejects #{other_type.class}"
771
+ )
772
+ end
773
+ end
774
+
775
+ def accepts_float_range_from_constant(self_type, constant, mode)
776
+ unless constant.value.is_a?(Float)
777
+ return Type::AcceptsResult.no(
778
+ mode: mode,
779
+ reasons: "FloatRange rejects non-Float Constant"
780
+ )
781
+ end
782
+
783
+ if self_type.covers?(constant.value)
784
+ Type::AcceptsResult.yes(
785
+ mode: mode,
786
+ reasons: "Constant[#{constant.value}] is in #{self_type.describe}"
787
+ )
788
+ else
789
+ Type::AcceptsResult.no(
790
+ mode: mode,
791
+ reasons: "Constant[#{constant.value}] outside #{self_type.describe}"
792
+ )
793
+ end
794
+ end
795
+
796
+ def accepts_float_range_from_float_range(self_type, other_range, mode)
797
+ if self_type.min <= other_range.min && other_range.canonical_max <= self_type.canonical_max
798
+ Type::AcceptsResult.yes(
799
+ mode: mode,
800
+ reasons: "#{other_range.describe} ⊆ #{self_type.describe}"
801
+ )
802
+ else
803
+ Type::AcceptsResult.no(
804
+ mode: mode,
805
+ reasons: "#{other_range.describe} not contained in #{self_type.describe}"
806
+ )
807
+ end
808
+ end
809
+
619
810
  # `Difference[base, removed]` accepts another type X when the base accepts X *and* X's value set is
620
811
  # provably disjoint from `removed`. The disjointness test is the subtle part — it is NOT the same as
621
812
  # `removed.accepts(X)`, because `Nominal[String]` includes `""` even though `Constant[""]` does not
622
- # "accept" `Nominal[String]`. The conservative rule here: we can prove disjointness only when X is
623
- # itself a `Constant` carrier (compare values directly) or another `Difference` with the same
624
- # removed value (already exhibits the disjointness). Any other shape — Nominal, Union, IntegerRange
625
- # — could overlap the removed value, so the difference rejects it under gradual mode.
813
+ # "accept" `Nominal[String]`. The conservative rule here: we can prove disjointness only when X is a
814
+ # carrier whose value set is visibly apart from `removed` — a `Constant` (compare values directly),
815
+ # another `Difference` with the same removed value (already exhibits the disjointness), an
816
+ # `IntegerRange` that skips an Integer removed value, a `Tuple` whose fixed arity differs from a
817
+ # removed `Tuple`, or a `HashShape` with a required key a closed removed `HashShape` cannot hold. The
818
+ # last three cover the empty witnesses of `non-zero-int`, `non-empty-array[T]` and
819
+ # `non-empty-hash[K, V]`. Any other carrier — a Nominal above all — is no proof and the difference
820
+ # rejects it under gradual mode, even where its values happen to exclude `removed`. A Union or
821
+ # Intersection argument never reaches this rule whole: `accepts` splits it into members first.
626
822
  def accepts_difference(self_type, other_type, mode)
627
823
  base_result = accepts(self_type.base, other_type, mode: mode)
628
824
  return base_result if base_result.no?
@@ -651,9 +847,27 @@ module Rigor
651
847
  # Disjointness is monotonic over Intersection: if any member is provably disjoint from
652
848
  # `removed`, the meet is too.
653
849
  other_type.members.any? { |m| provably_disjoint_from_removed?(m, removed) }
850
+ when Type::IntegerRange
851
+ removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && !other_type.covers?(removed.value)
852
+ when Type::Tuple
853
+ # A Tuple's arity is fixed, so it shares no inhabitant with a Tuple of another arity — the
854
+ # zero-arity witness `non-empty-array[T]` removes included.
855
+ removed.is_a?(Type::Tuple) && removed.elements.size != other_type.elements.size
856
+ when Type::HashShape
857
+ hash_shape_excludes_removed?(other_type, removed)
654
858
  end
655
859
  end
656
860
 
861
+ # Every inhabitant of `shape` holds each of its required keys, and a closed `removed` shape holds no
862
+ # key outside its pairs, so one required key `removed` does not list makes the two disjoint. Against
863
+ # the closed `{}` witness `non-empty-hash[K, V]` removes, that is any required key; an optional key
864
+ # proves nothing, since the shape is inhabited without it.
865
+ def hash_shape_excludes_removed?(shape, removed)
866
+ return false unless removed.is_a?(Type::HashShape) && removed.closed?
867
+
868
+ shape.required_keys.any? { |key| !removed.pairs.key?(key) }
869
+ end
870
+
657
871
  # `Refined[base, predicate]` accepts another type X when the base accepts the *base* of X *and* X is
658
872
  # provably contained in the predicate's value set. The base check is delegated to
659
873
  # `accepts(self.base, X.base)` so handlers like `accepts_nominal` see Nominal-vs-Nominal and return
@@ -788,11 +1002,15 @@ module Rigor
788
1002
 
789
1003
  # HashShape{k1: T1, ...} accepts another HashShape when every required key of self is required on
790
1004
  # the other side and Ti accepts Ui (depth covariant). Optional keys may be absent on the other side;
791
- # when present, their values are checked. A closed self rejects known or possible extra keys. Other
792
- # types are rejected; the converse direction (a Nominal accepting a HashShape) is handled by
1005
+ # when present, their values are checked. A closed self rejects known extra keys, and answers at most
1006
+ # `maybe` for an open source: `h = { a: 1 }; h.default = 0` reopens a shape whose key set is still
1007
+ # exactly the literal's. Other types are rejected, except a `Hash` with a gradual arm (see
1008
+ # {#gradual_hash?}); the converse direction (a Nominal accepting a HashShape) is handled by
793
1009
  # `accepts_nominal` via projection.
794
1010
  def accepts_hash_shape(self_type, other_type, mode)
795
1011
  unless other_type.is_a?(Type::HashShape)
1012
+ return gradual_hash_verdict(self_type, other_type, mode) if gradual_hash?(other_type)
1013
+
796
1014
  return Type::AcceptsResult.no(
797
1015
  mode: mode,
798
1016
  reasons: "HashShape does not accept #{other_type.class}"
@@ -803,8 +1021,6 @@ module Rigor
803
1021
  return hash_shape_no(mode, "HashShape missing required keys: #{missing.inspect}") unless missing.empty?
804
1022
 
805
1023
  if self_type.closed?
806
- return hash_shape_no(mode, "HashShape closed target rejects open source") if other_type.open?
807
-
808
1024
  extra = other_type.pairs.keys - self_type.pairs.keys
809
1025
  unless extra.empty?
810
1026
  return hash_shape_no(mode, "HashShape closed target rejects extra keys: #{extra.inspect}")
@@ -812,9 +1028,59 @@ module Rigor
812
1028
  end
813
1029
 
814
1030
  per_entry = hash_shape_entry_results(self_type, other_type, mode)
815
- combine_arg_results(per_entry, mode)
1031
+ result = combine_arg_results(per_entry, mode)
1032
+ return result unless result.yes? && self_type.closed? && other_type.open?
1033
+
1034
+ unread_hash_maybe(mode, "HashShape closed target cannot check open source")
1035
+ end
1036
+
1037
+ # A `Hash` nominal — directly or as the base of a `Difference` (`non-empty-hash[K, V]`) — that is raw or
1038
+ # carries a `Dynamic` arm in a type argument. A hash literal with a `**splat` entry carries the arm by
1039
+ # design, `Hash.new` filled key by key reads as the raw `Hash`, and a declared `Hash[Symbol, untyped]`
1040
+ # is the author saying the hash may hold anything (the reading `MutationRejoin#regrowable_carrier?`
1041
+ # gives a declared gradual arm). The runtime value may then hold exactly the record's keys, so a shape
1042
+ # cannot reject it on the arm's account, the way `Hash[K, V]` accepts a raw `Hash` as maybe. A `Hash`
1043
+ # subclass is not read: its entries need not be the record's (`HashWithIndifferentAccess` stores String
1044
+ # keys).
1045
+ def gradual_hash?(type)
1046
+ nominal = type.is_a?(Type::Difference) ? type.base : type
1047
+ nominal.is_a?(Type::Nominal) && nominal.class_name == "Hash" &&
1048
+ (nominal.type_args.empty? || nominal.type_args.any? { |arg| gradual_arm?(arg) })
816
1049
  end
817
1050
 
1051
+ # `maybe`, unless an argument without a gradual arm already rules the record out: a precise `K` that
1052
+ # rejects a required key (`Hash[String, untyped]` never holds `:a`), or a precise `V` none of whose
1053
+ # members a required key's type accepts.
1054
+ def gradual_hash_verdict(self_type, other_type, mode)
1055
+ nominal = other_type.is_a?(Type::Difference) ? other_type.base : other_type
1056
+ key_type, value_type = nominal.type_args
1057
+ missing = unholdable_keys(self_type, key_type, mode)
1058
+ return hash_shape_no(mode, "Hash key type cannot hold required keys: #{missing.inspect}") if missing.any?
1059
+
1060
+ refused = refused_value_keys(self_type, value_type, mode)
1061
+ return hash_shape_no(mode, "Hash value type refused for required keys: #{refused.inspect}") if refused.any?
1062
+
1063
+ unread_hash_maybe(mode, "HashShape cannot check #{other_type.describe(:short)}")
1064
+ end
1065
+
1066
+ def unholdable_keys(self_type, key_type, mode)
1067
+ return [] if key_type.nil? || gradual_arm?(key_type)
1068
+
1069
+ self_type.required_keys.select { |key| key_type.accepts(Type::Combinator.constant_of(key), mode: mode).no? }
1070
+ end
1071
+
1072
+ def refused_value_keys(self_type, value_type, mode)
1073
+ return [] if value_type.nil? || gradual_arm?(value_type)
1074
+
1075
+ members = ContentJoin.union_members(value_type)
1076
+ self_type.required_keys.select do |key|
1077
+ formal = self_type.pairs.fetch(key)
1078
+ members.all? { |member| accepts(formal, member, mode: mode).no? }
1079
+ end
1080
+ end
1081
+
1082
+ def gradual_arm?(type) = ContentJoin.union_members(type).any?(Type::Dynamic)
1083
+
818
1084
  def hash_shape_entry_results(self_type, other_type, mode)
819
1085
  self_type.pairs.filter_map do |key, formal|
820
1086
  next unless other_type.pairs.key?(key)
@@ -936,7 +1202,7 @@ module Rigor
936
1202
  def resolve_class(name)
937
1203
  return nil unless name.is_a?(::String)
938
1204
 
939
- parts = name.delete_prefix("::").split("::")
1205
+ parts = class_name_parts(name)
940
1206
  return nil if parts.empty?
941
1207
 
942
1208
  mod = ::Object
@@ -950,7 +1216,16 @@ module Rigor
950
1216
  rescue ::StandardError, ::ScriptError, ::SystemExit
951
1217
  nil
952
1218
  end
1219
+
1220
+ # #775 — the split of a class name is a pure function of the string, and the subtype check asks it
1221
+ # twice per Nominal-vs-Nominal comparison (~370k times on the lib self-check) for the same few
1222
+ # hundred names. Only the split is memoised; the constant walk above still runs every time, so a
1223
+ # class loaded (or a spec constant removed) after the first ask is answered by the live tree.
1224
+ def class_name_parts(name)
1225
+ @class_name_parts[name] ||= name.delete_prefix("::").split("::").freeze
1226
+ end
953
1227
  end
1228
+ @class_name_parts = {}
954
1229
  end
955
1230
  end
956
1231
  end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "multi_target_binder"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # The auto-splat half of {BlockParameterBinder}: given a block's positional parameter shape and the one
9
+ # value the receiver yields, it answers the positional table the block's names are bound from.
10
+ #
11
+ # Ruby blocks (NOT lambdas) spread a single yielded array-ish value across their positionals, and CRuby
12
+ # decides that on the PARAMETER LIST alone ({ParameterShape#splats?}) — never on the value. So the
13
+ # carrier only decides how precisely each position can be filled, and a carrier this module cannot
14
+ # decompose lowers the positions to `Dynamic[Top]` rather than cancelling the spread (issue #1116).
15
+ #
16
+ # Each member of the carrier (a union distributes; anything else is its own single member) contributes
17
+ # one {Table}, and {join} folds them into the answer:
18
+ #
19
+ # - a `Type::Tuple` spreads element-wise, leading positionals (required, then optional) from the head
20
+ # and, with a rest present, trailing positionals from the tail with the rest absorbing the middle —
21
+ # the split `MultiTargetBinder` applies to `a, *r, b = tuple`, so `|*r, v|` over `[K, V]` binds `v` to
22
+ # `V`. A slot past the tuple is left unfilled and lands on `Dynamic[Top]`;
23
+ # - an `Array[T]` ({MultiTargetBinder.array_element_type}) binds `T` to every required and trailing
24
+ # positional, `Dynamic[Top]` to every optional one (a short array hands it its default, not `nil`),
25
+ # and `Array[T]` to a named rest. A short array pads the fixed positions with `nil` at runtime, so
26
+ # those positions are reported optimistic (issue #1093);
27
+ # - an opaque array carrier — a raw `Array`, an `Array[untyped]` / `Array[top]`, a `Refined` /
28
+ # `Difference` over either, or a `Dynamic` over any `Array` however precise — binds `Dynamic[Top]` to
29
+ # every position: the floor `MultiTargetBinder` binds for the same undecomposable right-hand side, and
30
+ # the reason `[1, 2].tap { |a, b| a.succ }` no longer reads `a` as the whole `Array` (issue #1116);
31
+ # - a bare `nil` binds `nil` to every position. `nil` has no `to_ary`, so CRuby passes it as the one
32
+ # argument and `|a, b|` sees `nil, nil` — which is also what the wrap `[nil]` pads to, so the two
33
+ # readings agree here, and {join} takes that `nil` back out of any position another member fills;
34
+ # - every other member takes the same `Dynamic[Top]` floor as the opaque one, and does NOT license the
35
+ # spread on its own. It is the member the union knows least about, and joining it as a value would put
36
+ # a nil-bearing element beside a nil-free one: `String#scan`'s `String | Array[String?]` yield joins a
37
+ # capture's `String?` against the whole match's `String` and fires `call.possible-nil-receiver` on the
38
+ # correlated-invariant `refinements[name.to_sym]`, which ADR-5 ranks above the precision.
39
+ #
40
+ # The spread needs at least one Tuple, `Array[T]` or opaque array member; a carrier with none of them
41
+ # ({#for} answers nil) leaves the binder's declared types alone, which binds the value to the first
42
+ # parameter and `Dynamic[Top]` after. So a lone value with no `to_ary` is NOT wrapped as `[value]` the
43
+ # way `a, b = value` wraps it: the one-value premise of auto-splat is the RBS block signature's arity,
44
+ # which nothing here can check against the actual yield, and a wrong wrap binds `nil` to a parameter the
45
+ # block receives a value in.
46
+ #
47
+ # See docs/internal-spec/inference-engine.md for the binding contract.
48
+ module BlockAutoSplat
49
+ # The positional part of a parameter list, as counts: all the auto-splat rule reads. An explicit
50
+ # `BlockParametersNode` list and a numbered-parameter block (`maximum` required positionals) both
51
+ # reduce to it, so the two spellings cannot drift.
52
+ ParameterShape = Data.define(:required, :optional, :post, :rest) do
53
+ def self.of(params_node)
54
+ new(required: params_node.requireds.size, optional: params_node.optionals.size,
55
+ post: params_node.posts.size, rest: !params_node.rest.nil?)
56
+ end
57
+
58
+ # The `_1` / `_2` form, read as the explicit list of `arity` required positionals.
59
+ def self.of_arity(arity)
60
+ new(required: arity, optional: 0, post: 0, rest: false)
61
+ end
62
+
63
+ def positional_count = required + optional + post
64
+
65
+ # CRuby's own condition (`vm_callee_setup_block_arg` / `setup_parameters_complex`): a block splats a
66
+ # lone array argument when it has a mandatory positional (`lead + post > 0`) or more than one
67
+ # optional, except a bare `|a|` (the iseq's `ambiguous_param0`). So `|k, *r|`, `|*r, v|`,
68
+ # `|a = 1, b = 2|` and the trailing-comma `|k,|` (Prism's `ImplicitRestNode`) splat, while `|*r|`,
69
+ # `|a = 1, *r|` and `|a, &b|` / `|a, k: 1|` do not.
70
+ def splats?
71
+ mandatory = required + post
72
+ return false unless mandatory.positive? || optional > 1
73
+
74
+ !(mandatory == 1 && optional.zero? && !rest)
75
+ end
76
+ end
77
+
78
+ # A positional table: the type per position, the positions bound optimistically, and the named rest's
79
+ # type (nil for the `Array[Dynamic[Top]]` default). One arm produces one; {join} folds them into one.
80
+ # Private as a name — {#for} hands the folded value out, and nothing outside constructs one.
81
+ Table = Data.define(:types, :optimistic_positions, :rest_type)
82
+ private_constant :Table
83
+
84
+ # The arm kinds that make a member an array as far as the spread is concerned, so that a union
85
+ # carrying one of them spreads even where its other members do not decompose.
86
+ ARRAY_CARRIER_ARMS = %i[tuple array opaque].freeze
87
+ private_constant :ARRAY_CARRIER_ARMS
88
+
89
+ module_function
90
+
91
+ # @param shape — the block's {ParameterShape}.
92
+ # @param carrier — the one value the receiver yields.
93
+ # @return the folded {Table}, or nil when no member is an array carrier and the
94
+ # spread must leave the declared types alone.
95
+ def for(shape, carrier)
96
+ arms = arms_of(carrier)
97
+ return nil if arms.nil?
98
+
99
+ join(arms.map { |member, kind| table_for(shape, member, kind) })
100
+ end
101
+
102
+ def arms_of(carrier)
103
+ members = carrier.is_a?(Type::Union) ? carrier.members : [carrier]
104
+ arms = members.map { |member| [member, arm_of(member)] }
105
+ arms.any? { |_, kind| ARRAY_CARRIER_ARMS.include?(kind) } ? arms : nil
106
+ end
107
+
108
+ def arm_of(member)
109
+ return :tuple if member.is_a?(Type::Tuple)
110
+ return :array if MultiTargetBinder.array_element_type(member)
111
+ return :opaque if opaque_array_carrier?(member)
112
+ return :nil_value if nil_literal?(member)
113
+
114
+ :unknown
115
+ end
116
+
117
+ # A carrier that is an array the module may not decompose into per-position types: the raw `Array`, an
118
+ # `Array[untyped]` / `Array[top]`, a `Refined` / `Difference` over either, and a `Dynamic` over ANY
119
+ # carrier that would license the spread — `Dynamic[Array[Integer]]`, `Dynamic[[Integer, String]]`,
120
+ # `Dynamic[Array[Integer] | nil]` alike. {MultiTargetBinder.array_element_type} declines every
121
+ # `Dynamic` wrapper on purpose: the value is gradual, and projecting its static facet would hand each
122
+ # position a bare `T` that licenses the negative rules (issue #1093, ADR-5). That is a reason to floor
123
+ # the positions, not a reason to leave the whole value on the first parameter, so the facet decides
124
+ # only whether CRuby would splat.
125
+ def opaque_array_carrier?(type)
126
+ case type
127
+ when Type::Nominal then type.class_name == "Array"
128
+ when Type::Refined, Type::Difference then opaque_array_carrier?(type.base)
129
+ when Type::Dynamic then !arms_of(type.static_facet).nil?
130
+ else false
131
+ end
132
+ end
133
+
134
+ def table_for(shape, member, kind)
135
+ case kind
136
+ when :tuple then tuple_table(shape, member.elements)
137
+ when :array then array_table(shape, MultiTargetBinder.array_element_type(member))
138
+ when :nil_value then uniform_table(shape, member)
139
+ else uniform_table(shape, Type::Combinator.untyped)
140
+ end
141
+ end
142
+
143
+ # The split is mirrored rather than delegated to `MultiTargetBinder.decompose_tuple` because that one
144
+ # pads a missing slot with `Constant[nil]` and softens `X | nil`; a block slot past the tuple has
145
+ # always bound `Dynamic[Top]`. Without a rest the trailing positionals continue from the head (Ruby
146
+ # fills `|a, b = 1, c|` from the head when the tuple is long enough).
147
+ def tuple_table(shape, elements)
148
+ head = shape.required + shape.optional
149
+ posts = shape.post
150
+ tail_start = shape.rest ? [elements.size - posts, head].max : head
151
+ types = Array.new(head) { |i| elements[i] } + Array.new(posts) { |j| elements[tail_start + j] }
152
+ Table.new(types: types, optimistic_positions: [], rest_type: nil)
153
+ end
154
+
155
+ def array_table(shape, element)
156
+ required = shape.required
157
+ optional = shape.optional
158
+ posts = shape.post
159
+ Table.new(
160
+ types: Array.new(required) { element } + Array.new(optional) { Type::Combinator.untyped } +
161
+ Array.new(posts) { element },
162
+ optimistic_positions: (0...required).to_a + ((required + optional)...(required + optional + posts)).to_a,
163
+ rest_type: Type::Combinator.nominal_of("Array", type_args: [element])
164
+ )
165
+ end
166
+
167
+ # One type for every position: the `Dynamic[Top]` floor of a member the module cannot decompose, or
168
+ # the `nil` a `nil` member hands each of them. The `nil` rest_type leaves a named rest on the binder's
169
+ # `Array[Dynamic[Top]]` default, which is right for both — CRuby gives `nil.tap { |a, *r| }` an empty
170
+ # `r`, and an opaque carrier says nothing about the leftovers.
171
+ def uniform_table(shape, type)
172
+ Table.new(types: Array.new(shape.positional_count) { type }, optimistic_positions: [], rest_type: nil)
173
+ end
174
+
175
+ # A single member's table is used as is. Across members, a position any member leaves unfilled (a slot
176
+ # past a short Tuple) or fills with `Dynamic[Top]` stays `Dynamic[Top]`, and a member's bare `nil`
177
+ # drops out of a position another member fills with a value, that position then being optimistic —
178
+ # `MultiTargetBinder`'s cross-member softening. A named rest joins only when every member supplies
179
+ # one; otherwise the binder's `Array[Dynamic[Top]]` default stands.
180
+ def join(tables)
181
+ types, softened = join_types(tables.map(&:types))
182
+ rests = tables.map(&:rest_type)
183
+ Table.new(
184
+ types: types,
185
+ optimistic_positions: (tables.flat_map(&:optimistic_positions) + softened).uniq,
186
+ rest_type: rests.include?(nil) ? nil : Type::Combinator.union(*rests)
187
+ )
188
+ end
189
+
190
+ # @return the joined table and the positions it softened.
191
+ def join_types(tables)
192
+ return [tables.first, []] if tables.size == 1
193
+
194
+ softened = []
195
+ joined = Array.new(tables.map(&:size).max) do |i|
196
+ column = tables.map { |table| table[i] }
197
+ next Type::Combinator.untyped if column.any? { |t| t.nil? || t == Type::Combinator.untyped }
198
+
199
+ firm = column.reject { |t| nil_literal?(t) }
200
+ next Type::Combinator.union(*column) if firm.empty? || firm.size == column.size
201
+
202
+ softened << i
203
+ Type::Combinator.union(*firm)
204
+ end
205
+ [joined, softened]
206
+ end
207
+
208
+ def nil_literal?(type) = type.is_a?(Type::Constant) && type.value.nil?
209
+
210
+ # {#for} is the whole surface; the arms and the fold are this module's own business, and keeping them
211
+ # off the singleton stops a caller reaching past the fold for one member's table.
212
+ private_class_method :arms_of, :arm_of, :opaque_array_carrier?, :table_for, :tuple_table, :array_table,
213
+ :uniform_table, :join, :join_types, :nil_literal?
214
+ end
215
+ end
216
+ end