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
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../reflection"
4
+ require_relative "../../source/parameter_envelope"
5
+
6
+ module Rigor
7
+ module Analysis
8
+ module CheckRules
9
+ # Issue #992 — the positional envelope `call.wrong-arity` may check a call against when no signature
10
+ # declares the method: the {Source::ParameterEnvelope} the project's own `def` records in
11
+ # `Scope::DiscoveryIndex#discovered_parameter_envelopes`, and only when nothing the project can see
12
+ # could make a different definition of that name the one that runs.
13
+ #
14
+ # Asked in two stages, because every condition past the first can only WITHHOLD a firing:
15
+ #
16
+ # 1. {#owner_envelope} walks the receiver's superclass chain to the first class whose own body, or one of
17
+ # whose project mixins, records the name. Every record at that level must agree — `include` and
18
+ # `prepend` share one table, so a mixin's definition may shadow the class's own — and none may be
19
+ # opaque. A call that fits this envelope is silent, and nothing below is consulted.
20
+ # 2. {#authoritative?} runs only for a call that would fire. It declines when a `method_missing` /
21
+ # `respond_to_missing?` hook or a dynamic-surface mark sits anywhere in the chain; when a mixin the
22
+ # project does not declare is not a known RBS module that lacks the name; when a `pre_eval:` patch or
23
+ # a plugin's synthetic method names it; when the owner does not make the method public; and when any
24
+ # project subclass of the receiver — whose instances the receiver's type admits — records a different
25
+ # or opaque envelope for it.
26
+ #
27
+ # Deliberately NOT in scope: a constructor reached through `Class#new` (nothing records `initialize` as
28
+ # `[:singleton, :new]`, so `.new` finds no owner unless the class writes `def self.new` itself); keyword
29
+ # arity (the envelope's required-keyword flag declines, as `compute_arity_envelope` declines an RBS
30
+ # function with required keywords); and a `class_eval` whose receiver is not a constant, which the
31
+ # discovery walk cannot name.
32
+ class SourceArity
33
+ MISSING_HOOKS = %i[method_missing respond_to_missing?].freeze
34
+ private_constant :MISSING_HOOKS
35
+
36
+ # One class on the walk: its own name, the project modules its mixins resolve to (transitively through
37
+ # their own `include`s), and the candidate-name lists of the mixins that resolve to no project module.
38
+ Level = Data.define(:class_name, :modules, :externals)
39
+ private_constant :Level
40
+
41
+ def initialize(scope, method_name, kind)
42
+ @scope = scope
43
+ @method_name = method_name.to_sym
44
+ @kind = kind
45
+ @name_memo = {}
46
+ end
47
+
48
+ # The nearest definition's envelope, or nil when there is none or it is not one shape. Remembers the
49
+ # levels it walked for {#authoritative?}.
50
+ #
51
+ # Its ADR-46 reads are withheld ({Analysis::DependencyRecorder.withhold}) until the caller settles
52
+ # the verdict with {#settle_by_definitions} or {#settle_by_walk}.
53
+ def owner_envelope(class_name)
54
+ @owner_entries = []
55
+ @ambiguous = false
56
+ envelope, @withheld = Analysis::DependencyRecorder.withhold { walk_to_owner(class_name) }
57
+ envelope
58
+ end
59
+
60
+ # True when the nearest level recorded the name but not as one envelope.
61
+ def ambiguous? = @ambiguous
62
+
63
+ # For a silent verdict that only a definition can overturn — the call fits, the owner takes a required
64
+ # keyword, or nothing on the chain records the name: the owner `def`s themselves (their symbol edges)
65
+ # and a definition appearing at a level the walk passed over (a name-keyed negative edge). A mixin or
66
+ # reopening added at the owner's level makes it opaque, and everything {#authoritative?} reads can only
67
+ # withhold, so none of these needs the file-level class edge the bucket reads would otherwise record.
68
+ def settle_by_definitions
69
+ return if @withheld.nil?
70
+
71
+ @owner_entries.each do |name, kind|
72
+ if kind == :singleton
73
+ @scope.user_singleton_def_site_for(name, @method_name)
74
+ else
75
+ @scope.user_def_site_for(name, @method_name)
76
+ end
77
+ end
78
+ separator = @kind == :singleton ? "." : "#"
79
+ @passed.each { |name| Analysis::DependencyRecorder.read_missing(:method, "#{name}#{separator}#{@method_name}") }
80
+ end
81
+
82
+ # For every other verdict — a firing, or an opaque owner level whose opacity another file's edit can
83
+ # lift — the walk's reads are the dependency, so they are recorded as read.
84
+ def settle_by_walk
85
+ Analysis::DependencyRecorder.replay(@withheld) unless @withheld.nil?
86
+ end
87
+
88
+ # Stage 2, for the class {#owner_envelope} last answered.
89
+ def authoritative?(class_name)
90
+ !load_order_dependent?(class_name) && !object_extension_may_shadow? &&
91
+ @levels.all? { |level| clean_level?(level) && public_at?(level.class_name) } &&
92
+ chain_free_of_hooks?(class_name) &&
93
+ subclasses_agree?(class_name, @envelope)
94
+ end
95
+
96
+ private
97
+
98
+ def walk_to_owner(class_name)
99
+ @levels = []
100
+ @passed = []
101
+ walked_whole_chain?(class_name) do |current|
102
+ level = level_for(current)
103
+ @levels << level
104
+ found = level_envelopes(level)
105
+ if found.empty?
106
+ @passed.concat([current] + level.modules)
107
+ next
108
+ end
109
+
110
+ envelope = found.first
111
+ @owner_entries = owner_entries(level)
112
+ unless found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope)
113
+ @ambiguous = true
114
+ return nil
115
+ end
116
+
117
+ return @envelope = envelope
118
+ end
119
+ nil
120
+ end
121
+
122
+ def owner_entries(level)
123
+ own = @scope.parameter_envelopes_of(level.class_name).key?([@kind, @method_name])
124
+ entries = own ? [[level.class_name, @kind]] : []
125
+ level.modules.each do |mod|
126
+ entries << [mod, :instance] if @scope.parameter_envelopes_of(mod).key?([:instance, @method_name])
127
+ end
128
+ entries
129
+ end
130
+
131
+ # The survey corpus's own lesson. `PStore.new(path).transaction { … }` inside `module TDiary::IO`
132
+ # resolves `PStore` to the project's `TDiary::IO::PStore` when that class's file is loaded, and to the
133
+ # stdlib `::PStore` when it is not — and tdiary loads it only under one `io_class` setting, so under
134
+ # the default one the call is correct code. The analysis universe holds every file at once, so it
135
+ # always picks the inner class. Whenever the receiver's class is one of SEVERAL known classes the
136
+ # call site's `Module.nesting` spells with the same last segment, which one the receiver is depends
137
+ # on load order, and a verdict against the project class's `def` is not one the program backs.
138
+ def load_order_dependent?(class_name)
139
+ segment = class_name.to_s.split("::").last
140
+ candidates = Rigor::Reflection.lexical_nesting_chain(@scope).map { |entry| "#{entry}::#{segment}" }
141
+ candidates << segment
142
+ known = candidates.uniq.select { |name| known_class?(name) }
143
+ known.include?(class_name.to_s) && known.size > 1
144
+ end
145
+
146
+ # A module some method body hands to `obj.extend` sits ahead of the object's class, so if it defines
147
+ # the name at all, that object answers with the module's definition.
148
+ def object_extension_may_shadow?
149
+ @scope.discovered_parameter_envelopes.any? do |_name, bucket|
150
+ bucket.key?(Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK) && bucket.key?([:instance, @method_name])
151
+ end
152
+ end
153
+
154
+ def known_class?(name)
155
+ @scope.discovered_classes.key?(name) || Rigor::Reflection.rbs_class_known?(name, scope: @scope)
156
+ end
157
+
158
+ # False when the walk stopped at the ADR-41 budget rather than at the top of the chain: budget
159
+ # exhaustion is uncertainty, and every caller reads it as a reason to decline.
160
+ def walked_whole_chain?(class_name)
161
+ current = class_name.to_s
162
+ seen = {}
163
+ while current && !seen[current]
164
+ return false if seen.size >= Scope::ANCESTOR_WALK_LIMIT
165
+
166
+ seen[current] = true
167
+ yield current
168
+ current = resolve(current, @scope.superclass_of(current))
169
+ end
170
+ true
171
+ end
172
+
173
+ def level_for(class_name)
174
+ modules = []
175
+ externals = []
176
+ collect_mixins(class_name, own_mixins(class_name), modules, externals, {})
177
+ Level.new(class_name: class_name, modules: modules, externals: externals)
178
+ end
179
+
180
+ # Instance methods reach a receiver through `include` / `prepend`; class methods through `extend`, and
181
+ # an extended module's own `include`s reach the same singleton.
182
+ def own_mixins(class_name)
183
+ @kind == :singleton ? (@scope.discovered_extends[class_name] || []) : @scope.includes_of(class_name)
184
+ end
185
+
186
+ # Issue #986 — a name the compact-header rename collision left ambiguous resolves to no ONE class,
187
+ # but BOTH classes it names are ancestors at runtime; only their MRO order is unknowable. Taking
188
+ # both as levels is what keeps this rule alive for the rest of the receiver: a method only one of
189
+ # them declares still has one envelope and still fires, a method they disagree about lands two on
190
+ # `#envelope_for`'s join and declines there, and the class's own `def`s, its superclass's and its
191
+ # unambiguous mixins are untouched. Letting the decline arrive as an unknown EXTERNAL mixin instead
192
+ # suppressed the rule for every level of the receiver.
193
+ def collect_mixins(owner, raw_names, modules, externals, seen)
194
+ raw_names.each do |raw|
195
+ resolved = resolve(owner, raw)
196
+ names = resolved ? [resolved] : @scope.ambiguous_ancestor_resolutions(owner, raw)
197
+ next externals << @scope.ancestor_name_candidates(owner, raw) if names.empty?
198
+
199
+ names.each do |name|
200
+ next if seen[name]
201
+
202
+ seen[name] = true
203
+ modules << name
204
+ collect_mixins(name, @scope.includes_of(name), modules, externals, seen)
205
+ end
206
+ end
207
+ end
208
+
209
+ def resolve(owner, raw)
210
+ return nil if raw.nil?
211
+
212
+ bucket = (@name_memo[owner] ||= {})
213
+ return bucket[raw] if bucket.key?(raw)
214
+
215
+ bucket[raw] = @scope.ancestor_name_candidates(owner, raw).find { |name| project_class?(name) }
216
+ end
217
+
218
+ def project_class?(name)
219
+ @scope.known_user_class?(name) || @scope.discovered_parameter_envelopes.key?(name)
220
+ end
221
+
222
+ # A module's methods reach the receiver as instance methods whichever side the receiver is on.
223
+ def level_envelopes(level)
224
+ own = @scope.parameter_envelopes_of(level.class_name)[[@kind, @method_name]]
225
+ mixed = level.modules.map { |mod| @scope.parameter_envelopes_of(mod)[[:instance, @method_name]] }
226
+ ([own] + mixed).compact
227
+ end
228
+
229
+ def clean_level?(level)
230
+ ([level.class_name] + level.modules).none? { |name| dynamic_surface?(name) || project_patched?(name) } &&
231
+ level.externals.all? { |candidates| external_mixin_lacks_method?(candidates) }
232
+ end
233
+
234
+ def dynamic_surface?(class_name)
235
+ Scope::DiscoveryIndex.rewritten_surface?(@scope.parameter_envelopes_of(class_name))
236
+ end
237
+
238
+ def project_patched?(class_name)
239
+ environment = @scope.environment
240
+ patched = environment&.project_patched_methods
241
+ return true if patched && !patched.empty? && patch_names_method?(patched, class_name)
242
+
243
+ synthetic = environment&.synthetic_method_index
244
+ return false if synthetic.nil? || synthetic.empty?
245
+
246
+ synthetic.knows_class?(class_name) || !synthetic.lookup_instance(class_name, @method_name).empty? ||
247
+ !synthetic.lookup_singleton(class_name, @method_name).empty?
248
+ end
249
+
250
+ def patch_names_method?(patched, class_name)
251
+ %i[instance singleton].any? do |kind|
252
+ !patched.lookup(class_name: class_name, method_name: @method_name, kind: kind).nil?
253
+ end
254
+ end
255
+
256
+ # A mixin the project does not declare may define the name with any arity, and `prepend` lets it win
257
+ # over the class's own `def`. Only a module RBS knows, and whose declaration lacks the name, is
258
+ # evidence that it does not.
259
+ def external_mixin_lacks_method?(candidates)
260
+ name = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: @scope) }
261
+ return false if name.nil?
262
+
263
+ Rigor::Reflection.instance_method_definition(name, @method_name, scope: @scope).nil?
264
+ rescue StandardError
265
+ false
266
+ end
267
+
268
+ def public_at?(class_name)
269
+ visibility = @scope.discovered_method_visibility(class_name, @method_name)
270
+ visibility.nil? || visibility == :public
271
+ end
272
+
273
+ # A hook anywhere in the chain, including above the owner, and on either side: a class that answers
274
+ # names it does not define is a class whose `def`s are not the whole story of what a call reaches.
275
+ def chain_free_of_hooks?(class_name)
276
+ walked_whole_chain?(class_name) do |current|
277
+ ([current] + level_for(current).modules).each do |name|
278
+ envelopes = @scope.parameter_envelopes_of(name)
279
+ return false if dynamic_surface?(name)
280
+ return false if MISSING_HOOKS.any? { |hook| hook_recorded?(envelopes, hook) }
281
+ end
282
+ end
283
+ end
284
+
285
+ def hook_recorded?(envelopes, hook)
286
+ envelopes.key?([:instance, hook]) || envelopes.key?([:singleton, hook])
287
+ end
288
+
289
+ # Every project class whose superclass chain reaches the receiver's class: a value typed as the
290
+ # receiver may be any of them, and each one's own definition of the name is what runs for it.
291
+ def subclasses_agree?(class_name, envelope)
292
+ each_subclass(class_name) do |subclass|
293
+ level = level_for(subclass)
294
+ return false unless clean_level?(level)
295
+ return false unless level_envelopes(level).all?(envelope)
296
+ end
297
+ true
298
+ end
299
+
300
+ def each_subclass(class_name)
301
+ children = children_by_parent
302
+ queue = children.fetch(class_name.to_s, []).dup
303
+ seen = {}
304
+ until queue.empty?
305
+ subclass = queue.shift
306
+ next if seen[subclass]
307
+
308
+ seen[subclass] = true
309
+ yield subclass
310
+ queue.concat(children.fetch(subclass, []))
311
+ end
312
+ end
313
+
314
+ def children_by_parent
315
+ @scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
316
+ parent = resolve(child, raw_parent)
317
+ (children[parent] ||= []) << child if parent
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Analysis
7
+ module CheckRules
8
+ # Issue #1367 (ADR-117 Decision point 1, WD2) — what the interpreter's setter for each special global accepts:
9
+ # the envelope `global.write-type-mismatch` and `global.readonly-write` judge a write against.
10
+ #
11
+ # The setter is the authority, not the RBS declaration of the global. Where the two differ, the setter wins:
12
+ # `$;` is declared `Regexp | String | nil`, but its setter also takes any object with `to_str`, and `$stdout` is
13
+ # declared `IO`, but its setter takes anything that responds to `write`. Each entry names the CRuby function
14
+ # that enforces it and was confirmed on Ruby 4.0.5, whose error the comment quotes.
15
+ #
16
+ # A special whose setter accepts every value is absent, so a write to it is never checked: `$stdin`
17
+ # (ADR-117 WD2), `$_`, `$VERBOSE` / `$-v` / `$-w`, `$DEBUG` / `$-d` and `$=`. So is `$@`, whose setter raises
18
+ # `ArgumentError` or `TypeError` depending on whether `$!` is set when it runs. `$&`, `` $` ``, `$'`, `$+`
19
+ # and `$1`..`$9` cannot be assigned at all: that is a syntax error, not a write.
20
+ module SpecialGlobalSetters
21
+ # A setter that raises `TypeError` unless the value is an instance of one of `classes` (a subclass counts,
22
+ # since the setter tests the object's built-in type) or, when `conversion` names a method, responds to it:
23
+ # the implicit conversion the setter calls (`to_str`, `to_int`) or the method it asks `respond_to?` about
24
+ # (`write`). `accepts` is the phrase the diagnostic quotes.
25
+ Contract = Data.define(:classes, :conversion, :accepts) do
26
+ # Whether an instance of the core class `class_name` is one the setter takes by type. Both sides are core
27
+ # classes, whose hierarchy is Ruby's own, so the running interpreter answers it.
28
+ def accepts_class?(class_name)
29
+ klass = ::Object.const_get(class_name)
30
+ classes.any? { |accepted| klass <= ::Object.const_get(accepted) }
31
+ end
32
+ end
33
+
34
+ # `rb_str_setter` (string.c), reached through `rb_deprecated_str_setter` and io.c's `deprecated_rs_setter`,
35
+ # tests `T_STRING` and calls no conversion: `$/ = 1` raises "value of $/ must be String", and so do a
36
+ # `Regexp` and an object with `to_str`.
37
+ STRING_OR_NIL = Contract.new(classes: %w[String NilClass].freeze, conversion: nil, accepts: "a String or nil")
38
+ # `rb_fs_setter` (string.c): nil, a String, a Regexp, or an object with `to_str` (`rb_fs_check`).
39
+ # `$; = 1` raises "value of $; must be String or Regexp".
40
+ FIELD_SEPARATOR = Contract.new(
41
+ classes: %w[String Regexp NilClass].freeze, conversion: :to_str,
42
+ accepts: "a String, a Regexp, nil, or an object with `to_str'"
43
+ )
44
+ # `match_setter` (re.c): nil or a MatchData (`Check_Type(val, T_MATCH)`). `$~ = 1` raises
45
+ # "wrong argument type Integer (expected MatchData)".
46
+ MATCH_DATA = Contract.new(classes: %w[MatchData NilClass].freeze, conversion: nil,
47
+ accepts: "a MatchData or nil")
48
+ # `set_arg0` (ruby.c) calls `StringValueCStr`: a String or an object with `to_str`, and not nil. `$0 = 1`
49
+ # raises "no implicit conversion of Integer into String".
50
+ PROGRAM_NAME = Contract.new(
51
+ classes: %w[String].freeze, conversion: :to_str, accepts: "a String or an object with `to_str'"
52
+ )
53
+ # `argf_lineno_setter` (io.c) calls `NUM2INT`: an Integer, a Float, or an object with `to_int`, and not nil.
54
+ # `$. = "1"` raises "no implicit conversion of String into Integer".
55
+ LINE_NUMBER = Contract.new(
56
+ classes: %w[Integer
57
+ Float].freeze, conversion: :to_int, accepts: "an Integer, a Float, or an object with `to_int'"
58
+ )
59
+ # `opt_i_set` (io.c) through `argf_inplace_mode_set`: nil or false turns in-place mode off; anything else
60
+ # goes through `StringValueCStr`. `$-i = 1` raises "no implicit conversion of Integer into String".
61
+ INPLACE_MODE = Contract.new(
62
+ classes: %w[String NilClass FalseClass].freeze, conversion: :to_str,
63
+ accepts: "a String, nil, false, or an object with `to_str'"
64
+ )
65
+ # `stdout_setter` / `stderr_setter` (io.c) call `must_respond_to(id_write, …)`, which asks `respond_to?` and so
66
+ # honours `respond_to_missing?`. `$stdout = 1` raises "$stdout must have write method, Integer given".
67
+ # ADR-117 WD2: this `_Writer` requirement is the only contract a stream write is checked against.
68
+ WRITER = Contract.new(classes: [].freeze, conversion: :write, accepts: "an object that responds to `write'")
69
+
70
+ CONTRACTS = {
71
+ "$/": STRING_OR_NIL,
72
+ "$-0": STRING_OR_NIL,
73
+ "$,": STRING_OR_NIL,
74
+ "$\\": STRING_OR_NIL,
75
+ "$;": FIELD_SEPARATOR,
76
+ "$-F": FIELD_SEPARATOR,
77
+ "$~": MATCH_DATA,
78
+ "$0": PROGRAM_NAME,
79
+ "$PROGRAM_NAME": PROGRAM_NAME,
80
+ "$.": LINE_NUMBER,
81
+ "$-i": INPLACE_MODE,
82
+ "$stdout": WRITER,
83
+ "$>": WRITER,
84
+ "$stderr": WRITER
85
+ }.freeze
86
+
87
+ # The specials whose setter is `rb_gvar_readonly_setter` — declared with `rb_define_readonly_variable`, as a
88
+ # virtual variable without a setter, or with that setter by name. Any write raises `NameError`
89
+ # ("$! is a read-only variable"), whatever the value.
90
+ #
91
+ # - `$!` (eval.c); `$$` and `$?` (process.c).
92
+ # - `$<`, `$FILENAME` and `$*` (io.c, ARGF).
93
+ # - `$:` / `$LOAD_PATH` / `$-I`, `$"` / `$LOADED_FEATURES` (load.c).
94
+ # - `$-W`, and the `-p` / `-l` / `-a` switches `$-p`, `$-l` and `$-a` (ruby.c).
95
+ READ_ONLY = Set[
96
+ :$!, :$$, :$?, :$<, :$FILENAME, :$*, :$:, :$LOAD_PATH, :$-I, :$", :$LOADED_FEATURES,
97
+ :$-W, :$-p, :$-l, :$-a
98
+ ].freeze
99
+
100
+ # The literal nodes `global.write-type-mismatch` judges, by the core class of the object each evaluates to.
101
+ # An interpolated String, Symbol or Regexp is still a new instance of that class.
102
+ LITERAL_CLASSES = {
103
+ Prism::IntegerNode => "Integer", Prism::FloatNode => "Float", Prism::RationalNode => "Rational",
104
+ Prism::ImaginaryNode => "Complex", Prism::StringNode => "String", Prism::InterpolatedStringNode => "String",
105
+ Prism::SymbolNode => "Symbol", Prism::InterpolatedSymbolNode => "Symbol", Prism::ArrayNode => "Array",
106
+ Prism::HashNode => "Hash", Prism::RegularExpressionNode => "Regexp",
107
+ Prism::InterpolatedRegularExpressionNode => "Regexp", Prism::NilNode => "NilClass",
108
+ Prism::TrueNode => "TrueClass", Prism::FalseNode => "FalseClass"
109
+ }.freeze
110
+
111
+ # How the diagnostic names a literal of each class.
112
+ LITERAL_DESCRIPTIONS = { "NilClass" => "nil", "TrueClass" => "true", "FalseClass" => "false" }.freeze
113
+
114
+ module_function
115
+
116
+ # The setter contract a write of `name` is checked against, or nil when its setter accepts every value or the
117
+ # global is no special.
118
+ def contract_for(name)
119
+ CONTRACTS[name]
120
+ end
121
+
122
+ # The core class of the object `node` evaluates to when it is a literal ({LITERAL_CLASSES}), looking
123
+ # through parentheses around a single expression, or nil for any other expression.
124
+ def literal_class(node)
125
+ while node.is_a?(Prism::ParenthesesNode)
126
+ body = node.body
127
+ return nil unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
128
+
129
+ node = body.body.first
130
+ end
131
+ LITERAL_CLASSES[node.class]
132
+ end
133
+
134
+ def literal_description(class_name)
135
+ LITERAL_DESCRIPTIONS.fetch(class_name) do
136
+ "#{class_name.start_with?('A', 'E', 'I', 'O', 'U') ? 'an' : 'a'} #{class_name} literal"
137
+ end
138
+ end
139
+
140
+ def read_only?(name)
141
+ READ_ONLY.include?(name)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../../source/node_children"
6
+ require_relative "../../inference/narrowing"
6
7
  require_relative "inferred_param_guard"
7
8
 
8
9
  module Rigor
@@ -29,6 +30,12 @@ module Rigor
29
30
  # The false-positive envelope mirrors `flow.always-truthy-condition`: clauses inside loops / blocks are
30
31
  # skipped (mutation tracking through those is incomplete), and the rule reads the engine's own
31
32
  # narrowing rather than recomputing it, so the diagnostic and the body typing can never diverge.
33
+ #
34
+ # Issue #1429 — a `:disjoint` clause is not reported when its `Bot` rests on a `Nominal` subject member whose
35
+ # class is disjoint from a condition's ({Inference::Narrowing.disjoint_nominal_guard?}): the `when C` or `in C`
36
+ # is code evidence that the subject can hold such an object (`io = STDOUT; case io when StringIO`), and Rigor's
37
+ # reading of the subject's type is what the evidence contradicts. The body scope stays `Bot`, so no call on the
38
+ # subject in it is checked. A subject that is a literal carrier (`x = 1; case x when String`) is still reported.
32
39
  class UnreachableClauseCollector
33
40
  LOOP_OR_BLOCK_NODE_CLASSES = [
34
41
  Prism::WhileNode, Prism::UntilNode, Prism::ForNode, Prism::BlockNode
@@ -42,7 +49,7 @@ module Rigor
42
49
  DEFENSIVE_EXIT_CALLS = %i[raise fail throw abort exit].freeze
43
50
 
44
51
  # @!attribute kind
45
- # @return [Symbol] `:disjoint` (this clause's type is disjoint
52
+ # @return `:disjoint` (this clause's type is disjoint
46
53
  # from the still-possible subject), `:prior_exhaustion` (an
47
54
  # earlier clause already covered the subject), or
48
55
  # `:exhausted_else` (every subject value is covered, so the
@@ -63,7 +70,7 @@ module Rigor
63
70
 
64
71
  # Legacy single-collector walk — kept as the oracle the ADR-53 Track B equivalence harness compares
65
72
  # {RuleWalk} against; deleted when Track B completes.
66
- # @return [Array<Result>] one entry per provably-dead `when` clause.
73
+ # @return one entry per provably-dead `when` clause.
67
74
  def collect(root)
68
75
  walk(root, in_loop_or_block: false)
69
76
  @results.freeze
@@ -139,13 +146,30 @@ module Rigor
139
146
  return if scope.nil?
140
147
  return unless scope.local(subject_name).is_a?(Type::Bot)
141
148
 
149
+ kind = when_clause_kind(clause, subject_name)
150
+ return if kind == :disjoint &&
151
+ disjoint_nominal_subject?(@scope_index[clause.conditions.first], subject_name, clause.conditions)
152
+
142
153
  @results << Result.new(
143
154
  clause: clause, body: clause.statements, subject_name: subject_name,
144
155
  condition_source: clause.conditions.map(&:slice).join(", "),
145
- kind: when_clause_kind(clause, subject_name), keyword: "when"
156
+ kind: kind, keyword: "when"
146
157
  )
147
158
  end
148
159
 
160
+ # True when the subject, as `entry` binds it, holds a non-literal `Nominal` whose class the class one of
161
+ # `patterns` names is disjoint from (issue #1429, see the class comment).
162
+ def disjoint_nominal_subject?(entry, subject_name, patterns)
163
+ subject_type = entry&.local(subject_name)
164
+ return false if subject_type.nil?
165
+
166
+ patterns.any? do |condition|
167
+ class_name = Inference::Narrowing.lexical_class_name(condition, entry)
168
+ class_name &&
169
+ Inference::Narrowing.disjoint_nominal_guard?(subject_type, class_name, environment: entry.environment)
170
+ end
171
+ end
172
+
149
173
  # A dead `when` is `:prior_exhaustion` when the subject was already narrowed to `bot` BEFORE this
150
174
  # clause (an earlier clause covered it) — read from the entry scope the engine recorded on the
151
175
  # clause's first condition node (ADR-47 WD2). Otherwise the subject was still concrete entering the
@@ -170,13 +194,23 @@ module Rigor
170
194
  return if scope.nil?
171
195
  return unless scope.local(subject_name).is_a?(Type::Bot)
172
196
 
197
+ kind = in_clause_kind(clause, subject_name)
198
+ return if kind == :disjoint && disjoint_nominal_pattern?(clause, subject_name)
199
+
173
200
  @results << Result.new(
174
201
  clause: clause, body: clause.statements, subject_name: subject_name,
175
202
  condition_source: clause.pattern.slice,
176
- kind: in_clause_kind(clause, subject_name), keyword: "in"
203
+ kind: kind, keyword: "in"
177
204
  )
178
205
  end
179
206
 
207
+ # {#disjoint_nominal_subject?} for a bare class pattern, `in C` or `in C => x`.
208
+ def disjoint_nominal_pattern?(clause, subject_name)
209
+ pattern = clause.pattern
210
+ pattern = pattern.value if pattern.is_a?(Prism::CapturePatternNode)
211
+ disjoint_nominal_subject?(@scope_index[clause.pattern], subject_name, [pattern])
212
+ end
213
+
180
214
  def in_clause_kind(clause, subject_name)
181
215
  entry = @scope_index[clause.pattern]
182
216
  return :prior_exhaustion if entry && entry.local(subject_name).is_a?(Type::Bot)
@@ -52,7 +52,6 @@ module Rigor
52
52
  # Legacy single-collector walk — kept as the oracle the ADR-53 Track B equivalence harness compares
53
53
  # {RuleWalk} against. Walks the whole subtree once, checking each consumer node's value slots.
54
54
  # Returns one {Result} per value-context use of a recovered-`void` call.
55
- # @return [Array<Result>]
56
55
  def collect(root)
57
56
  walk(root)
58
57
  @results.freeze