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
@@ -9,6 +9,12 @@ require_relative "../environment"
9
9
  require_relative "../rbs_extended"
10
10
  require_relative "../analysis/fact_store"
11
11
  require_relative "../builtins/regex_refinement"
12
+ require_relative "guard_rebinding"
13
+ require_relative "last_line"
14
+ require_relative "operand_effects"
15
+ require_relative "optimistic_origin"
16
+ require_relative "project_method_ownership"
17
+ require_relative "receiver_alias"
12
18
 
13
19
  module Rigor
14
20
  module Inference
@@ -41,14 +47,36 @@ module Rigor
41
47
  module Narrowing
42
48
  TRUSTED_EQUALITY_LITERAL_CLASSES = [String, Symbol, Integer, TrueClass, FalseClass, NilClass].freeze
43
49
  SINGLETON_LITERAL_CLASSES = [TrueClass, FalseClass, NilClass].freeze
44
- ClassNarrowingContext = Data.define(:exact, :polarity, :environment)
50
+ # `scope` is the analysed file's seeded scope where the caller has one, and nil otherwise (the
51
+ # Fact-shaped and RSpec-matcher entries hold an environment but no scope). Only the #898 singleton
52
+ # path reads it, and only to withhold a `Bot`, so a nil scope is the pre-#898 answer exactly.
53
+ ClassNarrowingContext = Data.define(:exact, :polarity, :environment, :scope)
45
54
  # A recognised `=~` regex pattern operand: the pattern `source` string and whether it was
46
55
  # compiled in extended (`//x`) mode. Extraction from a literal `RegularExpressionNode` and from
47
56
  # a value-pinned `Constant[Regexp]` share this carrier so the participation walk and the
48
57
  # extended-mode bail read one shape.
49
58
  RegexMatchPattern = Data.define(:source, :extended)
59
+ # Issue #1429 — a predicate receiver whose binding an edge can narrow ({.receiver_slot}): a local, an instance
60
+ # variable or a global (`name` its Symbol), or a constant reference (`name` its {.constant_key}). `current` is
61
+ # the type the receiver reads before the predicate, a global's read type when the scope does not bind it.
62
+ ReceiverSlot = Data.define(:kind, :name, :current)
63
+ # The globals a guard narrows without a record ({Scope#with_guarded_global}): `$_` and `$~` live in the frame's
64
+ # special-variable slot, which a called method does not reach and their own machinery forgets
65
+ # (`Scope#forget_last_line`, `Scope#forget_match_globals`), and `$!` / `$@` read the rescue clause that is
66
+ # running, which a call that returns leaves as it was.
67
+ UNRECORDED_GUARD_GLOBALS = %i[$_ $~ $! $@].freeze
68
+ # Issue #1429 — the value-pinned classes: a `Nominal` of one of them is the single value itself, which no
69
+ # program replaces with another class's instance, so it counts with the literal carriers
70
+ # ({.disjoint_nominal_guard?}, `respond_to?`'s edge).
71
+ VALUE_PINNED_CLASSES = %w[NilClass TrueClass FalseClass].freeze
50
72
  private_constant :TRUSTED_EQUALITY_LITERAL_CLASSES, :SINGLETON_LITERAL_CLASSES, :ClassNarrowingContext,
51
- :RegexMatchPattern
73
+ :RegexMatchPattern, :ReceiverSlot, :UNRECORDED_GUARD_GLOBALS, :VALUE_PINNED_CLASSES
74
+
75
+ # Issue #1017 — the tallest stack of conditionals a condition may carry and still contribute facts. A
76
+ # conditional guard analyses its predicate and both arms, so each level can triple the analysis; past this
77
+ # height the guard declines to "no narrowing". Height counts conditionals reached through the shapes the
78
+ # analyser walks (parentheses, a body's last statement, `&&` / `||`, `!`, a predicate, an arm).
79
+ CONDITIONAL_GUARD_DEPTH = 2
52
80
 
53
81
  module_function
54
82
 
@@ -117,10 +145,9 @@ module Rigor
117
145
  # falsey fragments above: `:truthy` when no inhabitant is falsey (the falsey fragment is
118
146
  # `Bot`), `:falsey` when no inhabitant is truthy, nil when both fragments are inhabited —
119
147
  # or when the type itself is nil / `Bot` (dead code is not a certainty claim). This is the
120
- # single owner of the judgment both branch-elision consumers read
121
- # (`ExpressionTyper#elide_or_union` on the value side, `StatementEvaluator#live_branch_for_if`
122
- # on the scope side), so the type a dead branch is elided from and the scope that stops
123
- # flowing through it can never disagree.
148
+ # single owner of the judgment branch elision reads (`StatementEvaluator#live_branch_for_if`,
149
+ # which a value-position conditional reaches too since issue #1003), so the type a dead
150
+ # branch is elided from and the scope that stops flowing through it can never disagree.
124
151
  def predicate_certainty(type)
125
152
  return nil if type.nil? || type.is_a?(Type::Bot)
126
153
 
@@ -139,9 +166,11 @@ module Rigor
139
166
  # counterpart of the scope narrowing {.case_when_scopes} performs for the same condition
140
167
  # shape, kept here so the branch a `case` expression's type drops and the clause whose
141
168
  # body scope goes dead derive from one judgment.
142
- def class_pattern_certainty(subject_type, class_name, environment:)
143
- truthy_bot = narrow_class(subject_type, class_name, environment: environment).is_a?(Type::Bot)
144
- falsey_bot = narrow_not_class(subject_type, class_name, environment: environment).is_a?(Type::Bot)
169
+ def class_pattern_certainty(subject_type, class_name, environment:, scope: nil)
170
+ truthy_bot = narrow_class(subject_type, class_name, environment: environment,
171
+ scope: scope).is_a?(Type::Bot)
172
+ falsey_bot = narrow_not_class(subject_type, class_name, environment: environment,
173
+ scope: scope).is_a?(Type::Bot)
145
174
 
146
175
  return :no if truthy_bot && !falsey_bot
147
176
  return :yes if !truthy_bot && falsey_bot
@@ -219,6 +248,54 @@ module Rigor
219
248
  narrow_integer_comparison_dispatch(type, comparator, bound)
220
249
  end
221
250
 
251
+ # Float-comparison fragment of `type` against a numeric literal `bound` (ADR-109 WD5). This
252
+ # is a TRUTHY-EDGE rule only: `x > c` is false for NaN, so the set that satisfies it is the
253
+ # range `Float[c..]`, but the set that fails it is that range's complement PLUS NaN, which no
254
+ # range spells. Callers apply it to the truthy edge and keep the entry type on the falsy edge.
255
+ # `x > c` narrows to the closed envelope `Float[c..]` rather than the exact `(c, ∞]` (one
256
+ # double wider; the exact form is deferred until a rule demands the removed point), `x < c`
257
+ # to the exact `Float[...c]`. Hooks in:
258
+ # - `Constant<Float>` is preserved when it satisfies the comparison, otherwise collapsed to
259
+ # `Bot` (a NaN constant satisfies nothing);
260
+ # - `FloatRange` becomes its intersection with the half-line, `Bot` when empty;
261
+ # - `Nominal[Float]` becomes the half-line itself;
262
+ # - `Union` narrows each member independently; Integer-rooted and other carriers flow
263
+ # through unchanged (the Integer rule above owns them).
264
+ def narrow_float_comparison(type, comparator, bound)
265
+ return type unless (bound.is_a?(Integer) || bound.is_a?(Float)) && %i[< <= > >=].include?(comparator)
266
+
267
+ bound = bound.to_f
268
+ return type if bound.nan? || bound.infinite?
269
+
270
+ narrow_float_comparison_dispatch(type, comparator, bound)
271
+ end
272
+
273
+ # The truthy edge of a numeric comparison: the Integer rule for Integer-rooted members, then
274
+ # the Float rule for Float-rooted ones. Never apply this to a falsy edge (see
275
+ # {.narrow_float_comparison}).
276
+ def narrow_numeric_comparison_truthy(type, comparator, bound)
277
+ narrow_float_comparison(narrow_integer_comparison(type, comparator, bound), comparator, bound)
278
+ end
279
+
280
+ # Intersects the Float-rooted members of `type` with `range` (a `FloatRange`), leaving every
281
+ # other member unchanged. `x.nan?` narrows its FALSY edge with `non_nan_float`; `x.finite?`
282
+ # narrows its TRUTHY edge with `finite_float`; the other edge of each keeps the entry type
283
+ # (NaN is truthy for `nan?` and nothing else is known there; `!finite?` admits NaN and ±∞).
284
+ def narrow_float_to_range(type, range)
285
+ case type
286
+ when Type::Constant
287
+ type.value.is_a?(Float) && !range.covers?(type.value) ? Type::Combinator.bot : type
288
+ when Type::FloatRange
289
+ intersect_float_bounds(type, range.min, range.max, range.exclude_end?)
290
+ when Type::Nominal
291
+ float_nominal?(type) ? range : type
292
+ when Type::Union
293
+ Type::Combinator.union(*type.members.map { |m| narrow_float_to_range(m, range) })
294
+ else
295
+ type
296
+ end
297
+ end
298
+
222
299
  # Equality fragment of `type` against an Integer `value`. `Constant<Integer>` is preserved
223
300
  # when it equals `value`, otherwise collapses to `Bot`. `IntegerRange` covers? `value`
224
301
  # narrows to `Constant[value]`; an out-of-range comparison collapses to `Bot`.
@@ -261,17 +338,35 @@ module Rigor
261
338
  # preserved; disjoint hierarchies collapse to `Bot`. Classes the environment cannot
262
339
  # resolve fall back to the conservative answer (the type unchanged) so the analyzer never
263
340
  # asserts narrowing it cannot prove.
264
- def narrow_class(type, class_name, exact: false, environment: Environment.default)
265
- context = ClassNarrowingContext.new(exact: exact, polarity: :positive, environment: environment)
341
+ def narrow_class(type, class_name, exact: false, environment: Environment.default, scope: nil)
342
+ context = ClassNarrowingContext.new(exact: exact, polarity: :positive, environment: environment,
343
+ scope: scope)
266
344
  narrow_class_dispatch(type, class_name, context)
267
345
  end
268
346
 
347
+ # Issue #1429 — true when `type` holds a member whose `Bot` under `is_a?(class_name)` rests only on Rigor's
348
+ # reading of its type: a `Nominal`, other than the value-pinned `nil` / `true` / `false` classes, whose class
349
+ # the environment orders `:disjoint` from `class_name`. A `Nominal` is a type Rigor inferred or read from a
350
+ # signature, and a program can hold another object there: `$stdout` is typed `IO`, and a test runs it with a
351
+ # `StringIO`, which is not an `IO` subclass. The narrowing still reads such a guard's edge as `Bot`, so the
352
+ # arm is not checked; what this answers is whether the `Bot` may stand as a verdict that the code is dead
353
+ # (`flow.unreachable-clause`). The carriers that record what the file literally shows (`Constant`, `Tuple`,
354
+ # `HashShape`, `Singleton`) and the value-pinned classes do not count, so their verdicts stand.
355
+ def disjoint_nominal_guard?(type, class_name, environment: Environment.default)
356
+ members = type.is_a?(Type::Union) ? type.members : [type]
357
+ members.any? do |member|
358
+ member.is_a?(Type::Nominal) && !VALUE_PINNED_CLASSES.include?(member.class_name) &&
359
+ member.class_name != class_name && environment.class_ordering(member.class_name, class_name) == :disjoint
360
+ end
361
+ end
362
+
269
363
  # Mirror of {.narrow_class} for the falsey edge of `is_a?`/`kind_of?`/`instance_of?`.
270
364
  # Inhabitants that DO satisfy the predicate are removed; inhabitants that do not are
271
365
  # preserved. Conservative on Top/Dynamic/Bot (preserved unchanged) because the analyzer
272
366
  # cannot prove the negative without a richer carrier.
273
- def narrow_not_class(type, class_name, exact: false, environment: Environment.default)
274
- context = ClassNarrowingContext.new(exact: exact, polarity: :negative, environment: environment)
367
+ def narrow_not_class(type, class_name, exact: false, environment: Environment.default, scope: nil)
368
+ context = ClassNarrowingContext.new(exact: exact, polarity: :negative, environment: environment,
369
+ scope: scope)
275
370
  narrow_class_dispatch(type, class_name, context)
276
371
  end
277
372
 
@@ -285,11 +380,11 @@ module Rigor
285
380
  # Walk the current type's union members, keep each part disjoint from `base`, and add
286
381
  # the removed-value Constant once when any current member covers it. `assert s is
287
382
  # ~non-empty-string` over `s: String | nil` narrows to `Constant[""] | NilClass`.
288
- # - `IntegerRange[a, b]` (v0.0.5+ slice). Complement is the two open halves `int<min,
289
- # a-1>` and `int<b+1, max>`, each intersected with the integer-domain parts of
383
+ # - `IntegerRange[a, b]` (v0.0.5+ slice). Complement is the two open halves
384
+ # `Integer[..a-1]` and `Integer[b+1..]`, each intersected with the integer-domain parts of
290
385
  # `current_type`. Non-integer parts (nil, String, …) of a Union receiver survive
291
- # unchanged. `assert n is ~int<5, 10>` over `n: Integer | nil` narrows to `int<min, 4> |
292
- # int<11, max> | NilClass`.
386
+ # unchanged. `assert n is ~Integer[5..10]` over `n: Integer | nil` narrows to `Integer[..4] |
387
+ # Integer[11..] | NilClass`.
293
388
  # - `Type::Intersection[M1, M2, …]` (v0.0.5+ slice). De Morgan: `D \ (M1 ∩ M2) = (D \ M1)
294
389
  # ∪ (D \ M2)`. Each member's complement is computed independently within
295
390
  # `current_type` and the results are unioned. Members the algebra cannot complement
@@ -335,12 +430,18 @@ module Rigor
335
430
  fact.type
336
431
  end
337
432
 
433
+ # Issue #1429 — the key a constant reference's guard narrowing is recorded under ({Scope#constant_narrowing}):
434
+ # its spelling, with a leading `::` kept, since `::Foo` and `Foo` may name two constants. nil for a path on a
435
+ # dynamic base (`expr::Foo`) and for any other node.
436
+ def constant_key(node)
437
+ name = Source::ConstantPath.qualified_name_or_nil(node)
438
+ return nil if name.nil?
439
+
440
+ Source::ConstantPath.rooted?(node) ? "::#{name}" : name
441
+ end
442
+
338
443
  # Public predicate analyser. Returns `[truthy_scope, falsey_scope]`, always; when no
339
444
  # narrowing rule matches the predicate node both entries are the receiver scope unchanged.
340
- #
341
- # @param node [Prism::Node, nil]
342
- # @param scope [Rigor::Scope]
343
- # @return [Array(Rigor::Scope, Rigor::Scope)]
344
445
  def predicate_scopes(node, scope)
345
446
  return [scope, scope] if node.nil?
346
447
 
@@ -348,6 +449,37 @@ module Rigor
348
449
  result || [scope, scope]
349
450
  end
350
451
 
452
+ # Issue #1468 — the scope the arguments and block of a safe-navigation call `recv&.m(…)` run under. Ruby
453
+ # evaluates them only once `recv` is known non-nil, so `klass&.new(klass.flag?)` reads `klass` without its
454
+ # `nil`, as `if klass then klass.new(klass.flag?) end` does. Only `nil` is removed: `false&.m(x)` still calls
455
+ # `m`. The call's own value keeps its skipped-call `nil` (`ExpressionTyper#safe_navigation_call_type`).
456
+ #
457
+ # The receiver is one a guard narrows ({.receiver_slot}: a local, instance variable, global or constant), or a
458
+ # safe-navigation call on such a receiver whose own arguments and block hold no write or jump
459
+ # ({OperandEffects}): `a&.b(a.c)&.d(a.e)` runs `d` only when `a&.b(…)` answered non-nil, which it cannot on a
460
+ # nil `a`, and nothing between the two reads rebinds `a`. `scope` itself when nothing narrows, and for a call
461
+ # that is not `&.`.
462
+ def safe_navigation_scope(call_node, scope)
463
+ return scope unless call_node.safe_navigation?
464
+
465
+ non_nil_receiver_scope(call_node.receiver, scope) || scope
466
+ end
467
+
468
+ # The scope the block of a safe-navigation call is entered from: {.safe_navigation_scope}, unless the narrowing
469
+ # may not hold for the whole body. An argument that writes may rebind the receiver before the block runs
470
+ # (`f&.two(f = nil) { f.run }`), and a block that writes the receiver hands the next iteration what it wrote
471
+ # (`a&.each { a.size; a = nil }`); either keeps `scope`.
472
+ def safe_navigation_block_scope(call_node, scope)
473
+ return scope unless call_node.safe_navigation?
474
+ return scope if OperandEffects.any?(call_node.arguments)
475
+
476
+ root = call_node.receiver
477
+ root = root.receiver while root.is_a?(Prism::CallNode) && root.safe_navigation?
478
+ return scope if root.respond_to?(:name) && OperandEffects.written_variables(call_node.block).include?(root.name)
479
+
480
+ safe_navigation_scope(call_node, scope)
481
+ end
482
+
351
483
  # Slice 7 phase 5 — `case`/`when` narrowing.
352
484
  #
353
485
  # Given the subject of a `case` (the expression after the `case` keyword) and an array of
@@ -365,12 +497,6 @@ module Rigor
365
497
  # scope. The catalogue mirrors {.case_equality_target_class}: static class/module
366
498
  # constants narrow as `is_a?`; integer/float-endpoint ranges narrow to `Numeric`;
367
499
  # string-endpoint ranges and regexp literals narrow to `String`.
368
- #
369
- # @param subject [Prism::Node, nil] the `case` subject.
370
- # @param conditions [Array<Prism::Node>] the `when`
371
- # clause's `conditions` array.
372
- # @param scope [Rigor::Scope]
373
- # @return [Array(Rigor::Scope, Rigor::Scope)]
374
500
  def case_when_scopes(subject, conditions, scope)
375
501
  # C1 — `case x when /re/` runs `/re/ === x`, which sets the regex match-data globals
376
502
  # exactly as a successful `=~` does. Narrow `$~`/`$&`/`$1..$N` on the clause body (the
@@ -378,17 +504,17 @@ module Rigor
378
504
  # a different regex. Applied even when the subject is not a narrowable local read.
379
505
  body_scope = apply_when_regex_globals(conditions, scope)
380
506
 
381
- return [body_scope, scope] unless subject.is_a?(Prism::LocalVariableReadNode)
382
-
383
- local_name = subject.name
384
- current = scope.local(local_name)
385
- return [body_scope, scope] if current.nil?
507
+ # Issue #1429 — a global or constant subject (`case $stdout when StringIO`) narrows as a local does.
508
+ slot = receiver_slot(subject, scope, kinds: CASE_SUBJECT_KINDS)
509
+ return [body_scope, scope] if slot.nil?
386
510
 
387
- truthy, = accumulate_case_when_scopes(body_scope, local_name, current, conditions)
388
- _, falsey = accumulate_case_when_scopes(scope, local_name, current, conditions)
389
- [truthy, falsey]
511
+ truthy_type, falsey_type = case_when_types(scope, slot.current, conditions)
512
+ [narrow_slot(body_scope, slot, truthy_type), narrow_slot(scope, slot, falsey_type)]
390
513
  end
391
514
 
515
+ CASE_SUBJECT_KINDS = %i[local global constant].freeze
516
+ private_constant :CASE_SUBJECT_KINDS
517
+
392
518
  # When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
393
519
  # match-data globals on the body edge (same rule as `analyse_regex_match_predicate`'s
394
520
  # truthy edge). With multiple regex conditions (`when /a/, /b/`) the body is reachable
@@ -411,8 +537,29 @@ module Rigor
411
537
 
412
538
  # Internal analyser. Returns `[truthy_scope, falsey_scope]` when the predicate shape is
413
539
  # recognised, or `nil` to signal "no narrowing" so the public surface can fall back to the
414
- # entry scope.
540
+ # entry scope. Issue #1359 — a condition whose value is a `gets`-family reader's, the bare call
541
+ # or a variable written with it (`while gets`, `if (line = gets)`), also binds `$_` on each edge
542
+ # to the line the reader returned ({LastLine.predicate_scopes}).
415
543
  def analyse(node, scope)
544
+ edges = analyse_shape(node, scope)
545
+ reader = last_line_condition(node)
546
+ reader ? LastLine.predicate_scopes(reader, scope, edges) : edges
547
+ end
548
+
549
+ # The reader call a condition's value is, or nil.
550
+ def last_line_condition(node)
551
+ call =
552
+ case node
553
+ when Prism::CallNode then node
554
+ when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
555
+ Prism::GlobalVariableWriteNode
556
+ node.value
557
+ end
558
+ call if call.is_a?(Prism::CallNode) && LastLine::READERS.include?(call.name)
559
+ end
560
+ private_class_method :last_line_condition
561
+
562
+ def analyse_shape(node, scope)
416
563
  case node
417
564
  when Prism::ParenthesesNode
418
565
  analyse_parentheses(node, scope)
@@ -424,20 +571,19 @@ module Rigor
424
571
  analyse_local_write(node, scope)
425
572
  when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode
426
573
  analyse_ivar(node, scope)
427
- when Prism::ClassVariableWriteNode
428
- analyse_cvar_write(node, scope)
429
- when Prism::GlobalVariableWriteNode
430
- analyse_global_write(node, scope)
574
+ when Prism::ClassVariableWriteNode, Prism::GlobalVariableWriteNode
575
+ analyse_cvar_or_global_write(node, scope)
576
+ when Prism::GlobalVariableReadNode, Prism::ConstantReadNode, Prism::ConstantPathNode
577
+ analyse_slot_read(node, scope)
431
578
  when Prism::CallNode
432
- analyse_call(node, scope)
433
- when Prism::AndNode
434
- analyse_and(node, scope)
435
- when Prism::OrNode
436
- analyse_or(node, scope)
579
+ analyse_call(write_receiver_as_read(node) || node, scope)
580
+ when Prism::AndNode, Prism::OrNode, Prism::IfNode, Prism::UnlessNode
581
+ analyse_composition(node, scope)
437
582
  when Prism::MatchWriteNode
438
583
  analyse_match_write(node, scope)
439
584
  end
440
585
  end
586
+ private_class_method :analyse_shape
441
587
 
442
588
  # rubocop:disable-next Metrics/ClassLength
443
589
  class << self
@@ -480,7 +626,7 @@ module Rigor
480
626
  end
481
627
 
482
628
  # Complement of an `IntegerRange[a, b]` within `current_type`. Splits the range
483
- # complement into the two open halves `int<min, a-1>` and `int<b+1, max>` (skipping a
629
+ # complement into the two open halves `Integer[..a-1]` and `Integer[b+1..]` (skipping a
484
630
  # half when its bound is infinity), then intersects each half with the integer-domain
485
631
  # parts of `current_type`. Non-integer parts of a Union receiver (nil, String, …)
486
632
  # survive unchanged.
@@ -505,8 +651,8 @@ module Rigor
505
651
  Type::Combinator.union(*survivors)
506
652
  end
507
653
 
508
- # Returns the two open halves of an IntegerRange's complement: the left half `int<-∞,
509
- # a-1>` (when `a` is finite) and the right half `int<b+1, ∞>` (when `b` is finite).
654
+ # Returns the two open halves of an IntegerRange's complement: the left half
655
+ # `Integer[..a-1]` (when `a` is finite) and the right half `Integer[b+1..]` (when `b` is finite).
510
656
  # Universal ranges (both bounds infinite) yield an empty array — the complement is
511
657
  # empty.
512
658
  def integer_range_complement_halves(range)
@@ -794,6 +940,12 @@ module Rigor
794
940
  ]
795
941
  end
796
942
 
943
+ def analyse_cvar_or_global_write(node, scope)
944
+ return analyse_cvar_write(node, scope) if node.is_a?(Prism::ClassVariableWriteNode)
945
+
946
+ analyse_global_write(node, scope)
947
+ end
948
+
797
949
  def analyse_cvar_write(node, scope)
798
950
  current = scope.cvar(node.name)
799
951
  return nil if current.nil?
@@ -814,6 +966,76 @@ module Rigor
814
966
  ]
815
967
  end
816
968
 
969
+ # Issue #1429 — `if $g`, `return unless $g`, `if CONFIG`: the truthiness guard on a global or constant
970
+ # read, the counterpart of {#analyse_local_read} ({#narrow_slot}).
971
+ def analyse_slot_read(node, scope)
972
+ slot = receiver_slot(node, scope, kinds: GLOBAL_AND_CONSTANT)
973
+ return nil if slot.nil?
974
+
975
+ [
976
+ narrow_slot(scope, slot, narrow_truthy(slot.current)),
977
+ narrow_slot(scope, slot, narrow_falsey(slot.current))
978
+ ]
979
+ end
980
+
981
+ GLOBAL_AND_CONSTANT = %i[global constant].freeze
982
+ ALL_SLOT_KINDS = %i[local ivar global constant].freeze
983
+ private_constant :GLOBAL_AND_CONSTANT, :ALL_SLOT_KINDS
984
+
985
+ # The {ReceiverSlot} `node` reads, among `kinds`, or nil. A local or instance variable needs a binding. A
986
+ # global the scope does not bind is read as it reads unbound, and a constant reference as it resolves
987
+ # (through any narrowing already on it), so a guard narrows it from that type.
988
+ def receiver_slot(node, scope, kinds: ALL_SLOT_KINDS)
989
+ case node
990
+ when Prism::LocalVariableReadNode then variable_slot(:local, node.name, scope.local(node.name), kinds)
991
+ when Prism::InstanceVariableReadNode then variable_slot(:ivar, node.name, scope.ivar(node.name), kinds)
992
+ when Prism::GlobalVariableReadNode then global_slot(node, scope, kinds)
993
+ when Prism::ConstantReadNode, Prism::ConstantPathNode then constant_slot(node, scope, kinds)
994
+ end
995
+ end
996
+
997
+ def variable_slot(kind, name, current, kinds)
998
+ return nil if current.nil? || !kinds.include?(kind)
999
+
1000
+ ReceiverSlot.new(kind: kind, name: name, current: current)
1001
+ end
1002
+
1003
+ def global_slot(node, scope, kinds)
1004
+ return nil unless kinds.include?(:global)
1005
+
1006
+ ReceiverSlot.new(kind: :global, name: node.name, current: scope.global(node.name) || scope.type_of(node))
1007
+ end
1008
+
1009
+ def constant_slot(node, scope, kinds)
1010
+ return nil unless kinds.include?(:constant)
1011
+
1012
+ key = constant_key(node)
1013
+ return nil if key.nil?
1014
+
1015
+ ReceiverSlot.new(kind: :constant, name: key, current: scope.type_of(node))
1016
+ end
1017
+
1018
+ # `scope` with `slot`'s receiver read as `type` on one edge. A local or instance variable rebinds as its
1019
+ # analysers always did. A global or constant is left alone when the edge learns nothing (`type` is what
1020
+ # it read), and otherwise narrows with its pre-guard type recorded, so code that may rebind it restores
1021
+ # the binding ({Scope#forget_guard_narrowings}); the frame-local specials skip the record.
1022
+ def narrow_slot(scope, slot, type)
1023
+ case slot.kind
1024
+ when :local then scope.with_local(slot.name, type)
1025
+ when :ivar then scope.with_ivar(slot.name, type)
1026
+ when :global then narrow_global_slot(scope, slot, type)
1027
+ else
1028
+ type == slot.current ? scope : scope.with_constant_narrowing(slot.name, type, slot.current)
1029
+ end
1030
+ end
1031
+
1032
+ def narrow_global_slot(scope, slot, type)
1033
+ return scope if type == slot.current
1034
+
1035
+ record = !UNRECORDED_GUARD_GLOBALS.include?(slot.name)
1036
+ scope.with_guarded_global(slot.name, type, slot.current, record: record)
1037
+ end
1038
+
817
1039
  # `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a `MatchWriteNode` listing the
818
1040
  # named-capture targets. The parent `eval_match_write` has already bound each target to
819
1041
  # `String | nil`; in the truthy branch (the regex matched) every named capture is
@@ -869,6 +1091,25 @@ module Rigor
869
1091
  # literals (Slice 6 phase 2 sub-phase 2).
870
1092
  # Anything else returns nil so the surrounding analyser falls through to the
871
1093
  # no-narrowing fallback.
1094
+ # Issue #1223 — `(x = e).nil?`, `(x ||= e).empty?`, `(@x = e).is_a?(C)`. A write's value is the binding
1095
+ # it leaves, and a predicate is narrowed in the scope after its receiver ran, so the call narrows the
1096
+ # variable exactly as the same call on a read of it would. Every analyser keys its receiver on a read
1097
+ # node, so the call is analysed with its receiver replaced by one ({ReceiverAlias.read_of}). Before #1223
1098
+ # the statement evaluator did not thread a receiver's write at all, so the local was unbound and there
1099
+ # was nothing to narrow; nil when the receiver is not such a write, bare or parenthesised on its own.
1100
+ def write_receiver_as_read(node)
1101
+ write = written_receiver(node.receiver)
1102
+ write && node.copy(receiver: ReceiverAlias.read_of(write))
1103
+ end
1104
+
1105
+ def written_receiver(receiver)
1106
+ while receiver.is_a?(Prism::ParenthesesNode) && receiver.body.is_a?(Prism::StatementsNode) &&
1107
+ receiver.body.body.size == 1
1108
+ receiver = receiver.body.body.first
1109
+ end
1110
+ receiver if ReceiverAlias.variable_write?(receiver)
1111
+ end
1112
+
872
1113
  def analyse_call(node, scope)
873
1114
  return nil if node.block
874
1115
 
@@ -994,7 +1235,7 @@ module Rigor
994
1235
 
995
1236
  def simple_dispatch_name?(name)
996
1237
  %i[nil? ! is_a? kind_of? instance_of? == != === =~ match? key? has_key? empty? any?
997
- none? respond_to?].include?(name)
1238
+ none? respond_to? nan? finite?].include?(name)
998
1239
  end
999
1240
 
1000
1241
  def dispatch_call_simple(node, scope, name)
@@ -1009,6 +1250,7 @@ module Rigor
1009
1250
  when :key?, :has_key? then analyse_key_presence_predicate(node, scope)
1010
1251
  when :empty?, :any?, :none? then analyse_array_emptiness_predicate(node, scope, name)
1011
1252
  when :respond_to? then analyse_respond_to_predicate(node, scope)
1253
+ when :nan?, :finite? then analyse_float_class_predicate(node, scope, name)
1012
1254
  end
1013
1255
  end
1014
1256
 
@@ -1018,11 +1260,19 @@ module Rigor
1018
1260
  # symbol is one of `NilClass`'s own methods (`:to_s`, `:inspect`, `:nil?`, …) — `nil`
1019
1261
  # DOES respond to those, so the truthy edge admits a nil receiver and we narrow nothing.
1020
1262
  #
1263
+ # Issue #1429 — the truthy edge also admits `m` itself. `respond_to?` is a guard written in the code
1264
+ # (ADR-117 Decision point 3), so a member of the receiver's type whose class Rigor knows to lack `m`
1265
+ # is not what the edge holds: members that may respond keep their type, and when no member may, the
1266
+ # receiver reads `Dynamic[top]` ({#respond_to_admitted}). `$stdout.respond_to?(:string) ?
1267
+ # $stdout.string : ""` on an `IO`-typed `$stdout` therefore does not report `undefined method`.
1268
+ # `Dynamic[top]` rather than `Dynamic[IO]`: method availability on a `Dynamic[T]` is checked against
1269
+ # `T` (special-types.md), so only the untyped carrier is sure to keep the guarded call quiet.
1270
+ #
1021
1271
  # Conservative floor: narrow only on a literal `Symbol`/`String` argument resolved
1022
1272
  # against the RBS environment; a non-literal symbol, a missing argument, or a symbol
1023
1273
  # that IS in `NilClass`'s method set declines. The falsey edge is always the no-op
1024
- # ("does not respond" proves little about the receiver's type). Narrowing-only: it
1025
- # removes the `nil` constituent and never promotes a non-nil type.
1274
+ # ("does not respond" proves little about the receiver's type). A local, instance variable,
1275
+ # global or constant receiver narrows ({#receiver_slot}).
1026
1276
  def analyse_respond_to_predicate(node, scope)
1027
1277
  return nil if node.block
1028
1278
  return nil if node.arguments.nil? || node.arguments.arguments.size != 1
@@ -1031,16 +1281,63 @@ module Rigor
1031
1281
  return nil if sym.nil?
1032
1282
  return nil if nilclass_method?(sym, scope)
1033
1283
 
1034
- reader, writer = emptiness_receiver_accessors(node.receiver)
1035
- return nil if reader.nil?
1284
+ slot = receiver_slot(node.receiver, scope)
1285
+ return nil if slot.nil?
1036
1286
 
1037
- current = scope.public_send(reader, node.receiver.name)
1038
- return nil if current.nil?
1287
+ admitted = respond_to_admitted(narrow_non_nil(slot.current), sym.to_sym, scope)
1288
+ return nil if admitted.equal?(slot.current) || admitted == slot.current
1039
1289
 
1040
- non_nil = narrow_non_nil(current)
1041
- return nil if non_nil.equal?(current)
1290
+ [narrow_slot(scope, slot, admitted), scope]
1291
+ end
1292
+
1293
+ # The members of `type` that may respond to `method_name`, or, when Rigor knows every member lacks it
1294
+ # ({#lacks_method?}), `Dynamic[top]`: the members Rigor read the edge from are wrong about the object there,
1295
+ # and no class is named to read it as instead. A member Rigor cannot judge (`Dynamic`, `Bot`, a class no
1296
+ # signature declares) may respond.
1297
+ #
1298
+ # When every member lacking `m` records what the file literally shows (a literal, a `Tuple`, a `HashShape`, a
1299
+ # class object, `nil` / `true` / `false`), the edge is `Bot`. For a literal value that is exact
1300
+ # (`1.respond_to?(:string)` is false in every run); a class object's arm goes unchecked when a gem without a
1301
+ # signature adds the method (`Time.respond_to?(:zone)` under ActiveSupport).
1302
+ def respond_to_admitted(type, method_name, scope)
1303
+ members = type.is_a?(Type::Union) ? type.members : [type]
1304
+ kept = members.reject { |member| lacks_method?(member, method_name, scope) }
1305
+ return type if kept.size == members.size
1306
+ return Type::Combinator.union(*kept) unless kept.empty?
1307
+ return Type::Combinator.bot if members.all? { |member| literal_carrier?(member) }
1042
1308
 
1043
- [scope.public_send(writer, node.receiver.name, non_nil), scope]
1309
+ Type::Combinator.untyped
1310
+ end
1311
+
1312
+ LITERAL_CARRIERS = [Type::Constant, Type::Tuple, Type::HashShape, Type::Singleton].freeze
1313
+ private_constant :LITERAL_CARRIERS
1314
+
1315
+ def literal_carrier?(member)
1316
+ LITERAL_CARRIERS.any? { |carrier| member.is_a?(carrier) } ||
1317
+ (member.is_a?(Type::Nominal) && VALUE_PINNED_CLASSES.include?(member.class_name))
1318
+ end
1319
+
1320
+ # True only when every class `member` dispatches through is declared in RBS, is not a mixin module or
1321
+ # `Object` / `BasicObject` (whose instances may be of any class), is not a plugin's open receiver, and
1322
+ # neither its signature nor the project defines `method_name` on it.
1323
+ def lacks_method?(member, method_name, scope)
1324
+ targets = ProjectMethodOwnership.targets(member)
1325
+ return false if targets.nil? || targets.empty?
1326
+
1327
+ targets.all? { |class_name, kind| class_lacks_method?(class_name, method_name, kind, scope) }
1328
+ end
1329
+
1330
+ OPEN_SURFACE_CLASSES = %w[Object BasicObject].freeze
1331
+ private_constant :OPEN_SURFACE_CLASSES
1332
+
1333
+ def class_lacks_method?(class_name, method_name, kind, scope)
1334
+ return false if kind == :instance && OPEN_SURFACE_CLASSES.include?(class_name)
1335
+ return false unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
1336
+ return false if kind == :instance && scope.environment.rbs_module?(class_name)
1337
+ return false if scope.environment.plugin_registry&.open_receiver?(class_name)
1338
+ return false if ProjectMethodOwnership.defines?(class_name, method_name, kind, scope)
1339
+
1340
+ ExternalAncestorResolution.method_definition(class_name, method_name, kind, scope: scope).nil?
1044
1341
  end
1045
1342
 
1046
1343
  # True when `nil` responds to `sym` — i.e. `NilClass` (own, inherited
@@ -1359,13 +1656,17 @@ module Rigor
1359
1656
  # `String`; every other numbered group present in the pattern stays `String | nil` on
1360
1657
  # both edges (a truthy match leaves an optional group nil at runtime), so we do not
1361
1658
  # narrow it on the truthy edge.
1659
+ #
1660
+ # Both edges start from `scope` with the match globals forgotten (#1385): the match rebinds every one of them,
1661
+ # so a `$N` an earlier match narrowed says nothing here. Without that, a `when /(x)/` arm, or the right operand
1662
+ # of `s =~ /(a)(b)(c)/ && t =~ /(x)/`, kept `$2` and `$3` narrowed to `String` where Ruby reads nil.
1362
1663
  def regex_match_predicate_scopes(scope, unconditional)
1363
1664
  string_t = Type::Combinator.nominal_of("String")
1364
1665
  match_data_t = Type::Combinator.nominal_of("MatchData")
1365
1666
  nil_t = Type::Combinator.constant_of(nil)
1366
1667
 
1367
- truthy = scope
1368
- falsey = scope
1668
+ truthy = scope.forget_match_globals
1669
+ falsey = truthy
1369
1670
  truthy = truthy.with_global(:$~, match_data_t)
1370
1671
  falsey = falsey.with_global(:$~, nil_t)
1371
1672
  REGEX_MATCH_GLOBALS.each do |name|
@@ -1399,8 +1700,10 @@ module Rigor
1399
1700
  # them to non-nil `String` would be unsound. The walker is intentionally light (char
1400
1701
  # scan, not a regex-AST parse): backslash escapes are skipped; `(?:…)`, lookahead
1401
1702
  # `(?=…)`/`(?!…)`, and lookbehind `(?<=…)`/`(?<!…)` do not capture; named groups
1402
- # `(?<name>…)` do. Conservatism is one-directional — when in doubt a group is treated
1403
- # as conditional (dropped from the Set), never the reverse.
1703
+ # `(?<name>…)` / `(?'name'…)` do. Once the pattern has a named group, Ruby numbers only
1704
+ # the named groups and a plain `(…)` does not capture (#1471): `/(?<x>a)(b)/` has one
1705
+ # group, so `$2` is nil. Conservatism is one-directional — when in doubt a group is
1706
+ # treated as conditional (dropped from the Set), never the reverse.
1404
1707
  def unconditional_capture_groups(source)
1405
1708
  # `unconditional` collects every capturing index; a group is later removed (with its
1406
1709
  # whole subtree) when it is optionally quantified, nested under an optional ancestor,
@@ -1410,7 +1713,15 @@ module Rigor
1410
1713
  # mutually exclusive, so its descendant captures may be absent on a successful match;
1411
1714
  # the group itself still participates. Closing a frame rolls its subtree up to the
1412
1715
  # parent so an optional / alternated ancestor disqualifies it.
1413
- state = { unconditional: Set.new, stack: [[nil, [], false]], group_index: 0 }
1716
+ state = { unconditional: Set.new, stack: [[nil, [], false]], group_index: 0, named: named_groups?(source) }
1717
+ each_structural_char(source) { |pos, chr| scan_group_char(source, pos, chr, state) }
1718
+ # Drain the virtual root: a top-level `|` disqualifies all.
1719
+ finalize_frame(state, state[:stack].pop, optional: false)
1720
+ state[:unconditional]
1721
+ end
1722
+
1723
+ # Yields each position of `source` outside a backslash escape and a character class, with its char.
1724
+ def each_structural_char(source)
1414
1725
  pos = 0
1415
1726
  length = source.length
1416
1727
  while pos < length
@@ -1423,12 +1734,13 @@ module Rigor
1423
1734
  pos = skip_char_class(source, pos) + 1
1424
1735
  next
1425
1736
  end
1426
- scan_group_char(source, pos, chr, state)
1737
+ yield pos, chr
1427
1738
  pos += 1
1428
1739
  end
1429
- # Drain the virtual root: a top-level `|` disqualifies all.
1430
- finalize_frame(state, state[:stack].pop, optional: false)
1431
- state[:unconditional]
1740
+ end
1741
+
1742
+ def named_groups?(source)
1743
+ each_structural_char(source) { |pos, chr| break true if chr == "(" && named_group?(source, pos) } == true
1432
1744
  end
1433
1745
 
1434
1746
  # Updates the walk `state` at a group-relevant char during
@@ -1438,7 +1750,7 @@ module Rigor
1438
1750
  case chr
1439
1751
  when "("
1440
1752
  idx = nil
1441
- if capturing_group?(source, pos)
1753
+ if capturing_group?(source, pos, named: state[:named])
1442
1754
  idx = (state[:group_index] += 1)
1443
1755
  state[:unconditional] << idx
1444
1756
  end
@@ -1477,11 +1789,20 @@ module Rigor
1477
1789
  end
1478
1790
  end
1479
1791
 
1480
- def capturing_group?(source, pos)
1481
- return true unless source[pos + 1] == "?"
1792
+ # A plain `(…)` captures unless the pattern has a named group (`named:`); of the `(?…)` forms only a named
1793
+ # group does.
1794
+ def capturing_group?(source, pos, named:)
1795
+ return !named unless source[pos + 1] == "?"
1796
+
1797
+ named_group?(source, pos)
1798
+ end
1799
+
1800
+ # `(?<name>…)` and `(?'name'…)` are named groups; `(?<=…)`/`(?<!…)` (lookbehind) and
1801
+ # `(?:…)`/`(?=…)`/`(?!…)` are not.
1802
+ def named_group?(source, pos)
1803
+ return false unless source[pos + 1] == "?"
1804
+ return true if source[pos + 2] == "'"
1482
1805
 
1483
- # `(?<name>…)` captures; `(?<=…)`/`(?<!…)` (lookbehind) and
1484
- # `(?:…)`/`(?=…)`/`(?!…)` do not.
1485
1806
  source[pos + 2] == "<" && source[pos + 3] != "=" && source[pos + 3] != "!"
1486
1807
  end
1487
1808
 
@@ -1551,19 +1872,41 @@ module Rigor
1551
1872
  return nil unless node.arguments.arguments.size == 2
1552
1873
 
1553
1874
  low, high = node.arguments.arguments
1554
- return nil unless low.is_a?(Prism::IntegerNode) && high.is_a?(Prism::IntegerNode)
1875
+ return nil unless numeric_literal_node?(low) && numeric_literal_node?(high)
1555
1876
 
1556
1877
  local_name = node.receiver.name
1557
1878
  current = scope.local(local_name)
1558
1879
  return nil if current.nil?
1559
1880
 
1560
- truthy = narrow_integer_comparison(
1561
- narrow_integer_comparison(current, :>=, low.value),
1881
+ # Truthy edge only, for Integer and Float members alike (ADR-109 WD5): a Float literal
1882
+ # bound leaves the Integer members untouched, and vice versa.
1883
+ truthy = narrow_numeric_comparison_truthy(
1884
+ narrow_numeric_comparison_truthy(current, :>=, low.value),
1562
1885
  :<=, high.value
1563
1886
  )
1564
1887
  [scope.with_local(local_name, truthy), scope]
1565
1888
  end
1566
1889
 
1890
+ # `x.nan?` / `x.finite?` (ADR-109 WD5). `nan?` narrows the FALSY edge to the non-NaN Floats
1891
+ # and leaves the truthy edge alone (NaN is not a carrier); `finite?` narrows the TRUTHY
1892
+ # edge to the finite Floats and leaves the falsy edge alone (it admits NaN and ±∞). A
1893
+ # non-Float member is untouched on both edges.
1894
+ def analyse_float_class_predicate(node, scope, predicate)
1895
+ return nil unless argument_free?(node)
1896
+ return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
1897
+
1898
+ local_name = node.receiver.name
1899
+ current = scope.local(local_name)
1900
+ return nil if current.nil?
1901
+
1902
+ case predicate
1903
+ when :nan?
1904
+ [scope, scope.with_local(local_name, narrow_float_to_range(current, Type::Combinator.non_nan_float))]
1905
+ when :finite?
1906
+ [scope.with_local(local_name, narrow_float_to_range(current, Type::Combinator.finite_float)), scope]
1907
+ end
1908
+ end
1909
+
1567
1910
  # Helper for {.narrow_integer_not_equal}. Only adjusts when the value sits exactly on
1568
1911
  # one endpoint, so the result stays contiguous; otherwise the input range is preserved.
1569
1912
  def narrow_integer_range_not_equal(range, value)
@@ -1628,20 +1971,96 @@ module Rigor
1628
1971
  current = scope.local(local_name)
1629
1972
  return nil if current.nil?
1630
1973
 
1631
- truthy = narrow_integer_comparison(current, normalised_op, bound)
1974
+ # ADR-109 WD5 — the Float rule runs on the truthy edge only: the falsy edge of `x > c`
1975
+ # admits NaN, so its Float-rooted members keep the entry type (the Integer rule below
1976
+ # leaves them untouched by construction).
1977
+ truthy = narrow_numeric_comparison_truthy(current, normalised_op, bound)
1632
1978
  falsey = narrow_integer_comparison(current, INVERT_COMPARISON_OP[normalised_op], bound)
1633
1979
  [scope.with_local(local_name, truthy), scope.with_local(local_name, falsey)]
1634
1980
  end
1635
1981
 
1982
+ # An Integer or Float literal. Prism folds a leading minus into the literal, so `-1.5` is one
1983
+ # `FloatNode`.
1984
+ def numeric_literal_node?(node)
1985
+ node.is_a?(Prism::IntegerNode) || node.is_a?(Prism::FloatNode)
1986
+ end
1987
+
1636
1988
  def comparison_local_literal(left, right, comparator)
1637
- if left.is_a?(Prism::LocalVariableReadNode) && right.is_a?(Prism::IntegerNode)
1989
+ if left.is_a?(Prism::LocalVariableReadNode) && numeric_literal_node?(right)
1638
1990
  return [left.name, comparator, right.value]
1639
1991
  end
1640
- return nil unless right.is_a?(Prism::LocalVariableReadNode) && left.is_a?(Prism::IntegerNode)
1992
+ return nil unless right.is_a?(Prism::LocalVariableReadNode) && numeric_literal_node?(left)
1641
1993
 
1642
1994
  [right.name, REVERSE_COMPARISON_OP[comparator], left.value]
1643
1995
  end
1644
1996
 
1997
+ def narrow_float_comparison_dispatch(type, comparator, bound)
1998
+ case type
1999
+ when Type::Constant
2000
+ # A non-Float constant belongs to another rule and is never dropped here. The test is
2001
+ # the positive comparison, negated once: NaN fails every comparison, which is Ruby's
2002
+ # answer too, and rewriting `!(v < c)` as `v >= c` would silently let NaN through.
2003
+ return type unless type.value.is_a?(Float)
2004
+
2005
+ float_constant_satisfies?(type.value, comparator, bound) ? type : Type::Combinator.bot
2006
+ when Type::FloatRange
2007
+ lo, hi, hi_exclusive = float_comparison_half_line(comparator, bound)
2008
+ intersect_float_bounds(type, lo, hi, hi_exclusive)
2009
+ when Type::Nominal
2010
+ return type unless float_nominal?(type)
2011
+
2012
+ lo, hi, hi_exclusive = float_comparison_half_line(comparator, bound)
2013
+ intersect_float_bounds(Type::Combinator.non_nan_float, lo, hi, hi_exclusive)
2014
+ when Type::Union
2015
+ Type::Combinator.union(
2016
+ *type.members.map { |m| narrow_float_comparison_dispatch(m, comparator, bound) }
2017
+ )
2018
+ else
2019
+ type
2020
+ end
2021
+ end
2022
+
2023
+ def float_nominal?(nominal)
2024
+ nominal.class_name == "Float" && nominal.type_args.empty?
2025
+ end
2026
+
2027
+ def float_constant_satisfies?(value, comparator, bound)
2028
+ case comparator
2029
+ when :< then value < bound
2030
+ when :<= then value <= bound
2031
+ when :> then value > bound
2032
+ when :>= then value >= bound
2033
+ end
2034
+ end
2035
+
2036
+ # `[lo, hi]` with `hi` exclusive or not: `x < c` is `[-∞, c)`, `x <= c` is `[-∞, c]`, and both
2037
+ # `x > c` and `x >= c` are the closed envelope `[c, ∞]` (WD5).
2038
+ def float_comparison_half_line(comparator, bound)
2039
+ case comparator
2040
+ when :< then [-Float::INFINITY, bound, true]
2041
+ when :<= then [-Float::INFINITY, bound, false]
2042
+ else [bound, Float::INFINITY, false]
2043
+ end
2044
+ end
2045
+
2046
+ # Intersects `range` with `[lo, hi]` (`hi` exclusive when `hi_exclusive`), keeping whichever
2047
+ # upper bound is tighter on the canonical closed form together with its own exclusivity, so
2048
+ # `Float[0.0...1.0] ∩ [-∞, 2.0]` still displays `Float[0.0...1.0]`. Empty is `Bot`.
2049
+ def intersect_float_bounds(range, lower, upper, upper_exclusive)
2050
+ new_min = [range.min, lower].max
2051
+ half_canonical_max = upper_exclusive ? upper.prev_float : upper
2052
+ if half_canonical_max < range.canonical_max
2053
+ new_max = upper
2054
+ exclusive = upper_exclusive
2055
+ else
2056
+ new_max = range.max
2057
+ exclusive = range.exclude_end?
2058
+ end
2059
+ return Type::Combinator.bot if new_min > (exclusive ? new_max.prev_float : new_max)
2060
+
2061
+ Type::Combinator.float_range(new_min, new_max, exclude_end: exclusive)
2062
+ end
2063
+
1645
2064
  def narrow_integer_equal_dispatch(type, value)
1646
2065
  case type
1647
2066
  when Type::Constant then narrow_integer_equal_constant(type, value)
@@ -1814,9 +2233,30 @@ module Rigor
1814
2233
  analyse_class_predicate_on_local(node, scope, class_name, exact)
1815
2234
  when Prism::CallNode
1816
2235
  analyse_class_predicate_on_chain(node, scope, class_name, exact)
2236
+ else
2237
+ analyse_class_predicate_on_slot(node.receiver, scope, class_name, exact)
1817
2238
  end
1818
2239
  end
1819
2240
 
2241
+ # Issue #1429 — `$stdout.is_a?(StringIO)`, `STDOUT.kind_of?(StringIO)`: a global or constant receiver
2242
+ # narrows as a local does ({#class_predicate_scopes}), with the pre-guard type recorded ({#narrow_slot}).
2243
+ def analyse_class_predicate_on_slot(receiver, scope, class_name, exact)
2244
+ slot = receiver_slot(receiver, scope, kinds: GLOBAL_AND_CONSTANT)
2245
+ return nil if slot.nil?
2246
+
2247
+ class_slot_scopes(scope, slot, class_name, exact: exact)
2248
+ end
2249
+
2250
+ def class_slot_scopes(scope, slot, class_name, exact:)
2251
+ environment = scope.environment
2252
+ truthy = narrow_class(slot.current, class_name, exact: exact, environment: environment, scope: scope)
2253
+ [
2254
+ narrow_slot(scope, slot, truthy),
2255
+ narrow_slot(scope, slot, narrow_not_class(slot.current, class_name, exact: exact,
2256
+ environment: environment, scope: scope))
2257
+ ]
2258
+ end
2259
+
1820
2260
  # The class name a `is_a?` / `kind_of?` / `instance_of?` argument denotes: the top-level
1821
2261
  # `bare_name` when the reference is written `::Foo` (nil when nothing declares it there),
1822
2262
  # the lexical resolution otherwise.
@@ -1854,8 +2294,10 @@ module Rigor
1854
2294
  current = scope.type_of(node.receiver)
1855
2295
  return nil if current.nil?
1856
2296
 
1857
- truthy_type = narrow_class(current, class_name, exact: exact, environment: scope.environment)
1858
- falsey_type = narrow_not_class(current, class_name, exact: exact, environment: scope.environment)
2297
+ truthy_type = narrow_class(current, class_name, exact: exact, environment: scope.environment,
2298
+ scope: scope)
2299
+ falsey_type = narrow_not_class(current, class_name, exact: exact, environment: scope.environment,
2300
+ scope: scope)
1859
2301
 
1860
2302
  [
1861
2303
  scope.with_method_chain_narrowing(*address, truthy_type),
@@ -1907,13 +2349,25 @@ module Rigor
1907
2349
  # shapes at once. Because the repair lives in the shared derivation, `is_a?`, `case`/`when`, `===`
1908
2350
  # and the constant typer were fixed together rather than one guard shape at a time. A scope no
1909
2351
  # declaration walk built still falls back to the peel; see {Reflection.lexical_nesting_chain}.
2352
+ #
2353
+ # A path whose head and tail have DIFFERENT owners is the one shape no `<prefix>::<full path>`
2354
+ # candidate can name (#656), so the whole-path loop falls through to the segment-wise walk
2355
+ # before it gives up: `A::B` guarded inside `P::Guard` with `P::A < P::Base` is `P::Base::B`,
2356
+ # which the loop above only ever reached as the top-level `A::B` — a real but different class,
2357
+ # and every reader call on the narrowed receiver was then checked against it. The walk is
2358
+ # {Reflection.resolve_constant_path_name}, shared with the constant typer so the guard and the
2359
+ # value it guards cannot name two classes, and it takes this scope's acceptance test so a
2360
+ # candidate is still adopted only when the environment or discovery knows it as a class.
1910
2361
  def resolve_class_name_lexically(bare_name, scope)
1911
2362
  chain = lexical_nesting_for(scope)
1912
2363
  chain.each do |prefix|
1913
2364
  candidate = "#{prefix}::#{bare_name}"
1914
2365
  return candidate if class_known_to_scope?(scope, candidate)
1915
2366
  end
1916
- bare_name
2367
+ walked = Reflection.resolve_constant_path_name(bare_name, scope) do |candidate|
2368
+ class_known_to_scope?(scope, candidate)
2369
+ end
2370
+ walked || bare_name
1917
2371
  end
1918
2372
 
1919
2373
  # Combines the environment's RBS-known set with the scope's in-source
@@ -1934,16 +2388,8 @@ module Rigor
1934
2388
  end
1935
2389
 
1936
2390
  def class_predicate_scopes(scope, name, current, class_name, exact:)
1937
- [
1938
- scope.with_local(
1939
- name,
1940
- narrow_class(current, class_name, exact: exact, environment: scope.environment)
1941
- ),
1942
- scope.with_local(
1943
- name,
1944
- narrow_not_class(current, class_name, exact: exact, environment: scope.environment)
1945
- )
1946
- ]
2391
+ slot = ReceiverSlot.new(kind: :local, name: name, current: current)
2392
+ class_slot_scopes(scope, slot, class_name, exact: exact)
1947
2393
  end
1948
2394
 
1949
2395
  # Slice 7 phase 4 — `===`-narrowing. The case-equality predicate `<receiver> === local`
@@ -1977,9 +2423,23 @@ module Rigor
1977
2423
  analyse_case_equality_receiver(node.receiver, scope, arg.name, current)
1978
2424
  when Prism::CallNode
1979
2425
  analyse_case_equality_on_chain(node.receiver, arg, scope)
2426
+ else
2427
+ analyse_case_equality_on_slot(node.receiver, arg, scope)
1980
2428
  end
1981
2429
  end
1982
2430
 
2431
+ # Issue #1429 — `StringIO === $stdout`, `StringIO === STDOUT`: the class-constant receiver form on a global or
2432
+ # constant argument. The Range / Regexp receivers stay local-only.
2433
+ def analyse_case_equality_on_slot(receiver, arg, scope)
2434
+ class_name = lexical_class_name(receiver, scope)
2435
+ return nil if class_name.nil?
2436
+
2437
+ slot = receiver_slot(arg, scope, kinds: GLOBAL_AND_CONSTANT)
2438
+ return nil if slot.nil?
2439
+
2440
+ class_slot_scopes(scope, slot, class_name, exact: false)
2441
+ end
2442
+
1983
2443
  # `Class === <local/ivar>.<method>` — the case-equality counterpart of
1984
2444
  # {.analyse_class_predicate_on_chain}. The `open3` idiom `if Hash === cmd.last` narrows
1985
2445
  # `cmd.last` to the class inside the branch, recorded as a single-hop method-chain
@@ -1996,8 +2456,10 @@ module Rigor
1996
2456
  current = scope.type_of(chain_arg)
1997
2457
  return nil if current.nil?
1998
2458
 
1999
- truthy_type = narrow_class(current, class_name, exact: false, environment: scope.environment)
2000
- falsey_type = narrow_not_class(current, class_name, exact: false, environment: scope.environment)
2459
+ truthy_type = narrow_class(current, class_name, exact: false, environment: scope.environment,
2460
+ scope: scope)
2461
+ falsey_type = narrow_not_class(current, class_name, exact: false, environment: scope.environment,
2462
+ scope: scope)
2001
2463
  [
2002
2464
  scope.with_method_chain_narrowing(*address, truthy_type),
2003
2465
  scope.with_method_chain_narrowing(*address, falsey_type)
@@ -2012,7 +2474,8 @@ module Rigor
2012
2474
  target_class = case_equality_target_class(receiver)
2013
2475
  return nil if target_class.nil?
2014
2476
 
2015
- narrowed = narrow_class(current, target_class, exact: false, environment: scope.environment)
2477
+ narrowed = narrow_class(current, target_class, exact: false, environment: scope.environment,
2478
+ scope: scope)
2016
2479
  [
2017
2480
  scope.with_local(local_name, narrowed),
2018
2481
  scope.with_local(local_name, current)
@@ -2195,6 +2658,10 @@ module Rigor
2195
2658
  # that position, or nil when the overload shape does not allow a precise match.
2196
2659
  def lookup_positional_arg(call_node, method_def, target_name)
2197
2660
  arguments = call_node.arguments&.arguments || []
2661
+ # A `(?)` overload (`RBS::Types::UntypedFunction`, #1430) names no parameter and accepts any argument list,
2662
+ # so a call may reach it with the target's position holding anything: no overload proves the target.
2663
+ return nil unless method_def.method_types.all? { |mt| mt.type.respond_to?(:required_positionals) }
2664
+
2198
2665
  method_def.method_types.each do |mt|
2199
2666
  params = mt.type.required_positionals + mt.type.optional_positionals
2200
2667
  index = params.find_index { |param| param.name == target_name }
@@ -2210,7 +2677,9 @@ module Rigor
2210
2677
  # cannot statically classify are treated as "no narrowing": the body falls back to the
2211
2678
  # union of what we did learn (or the entry type when nothing learned), and the falsey
2212
2679
  # edge is the entry type (because we cannot prove the unknown condition didn't match).
2213
- def accumulate_case_when_scopes(scope, local_name, current, conditions)
2680
+ #
2681
+ # `[body type, next-clause type]` of a `when` clause's conditions over the subject type `current`.
2682
+ def case_when_types(scope, current, conditions)
2214
2683
  truthy_members = []
2215
2684
  falsey_type = current
2216
2685
  fully_narrowable = true
@@ -2227,10 +2696,7 @@ module Rigor
2227
2696
  end
2228
2697
 
2229
2698
  truthy = truthy_members.empty? ? current : Type::Combinator.union(*truthy_members)
2230
- [
2231
- scope.with_local(local_name, truthy),
2232
- scope.with_local(local_name, fully_narrowable ? falsey_type : current)
2233
- ]
2699
+ [truthy, fully_narrowable ? falsey_type : current]
2234
2700
  end
2235
2701
 
2236
2702
  # Per-condition rule. Returns `nil` when the condition shape is not recognised (caller
@@ -2283,8 +2749,9 @@ module Rigor
2283
2749
 
2284
2750
  def class_when_result(scope, current, target, falsey_acc)
2285
2751
  {
2286
- truthy: narrow_class(current, target, exact: false, environment: scope.environment),
2287
- falsey: narrow_not_class(falsey_acc, target, exact: false, environment: scope.environment),
2752
+ truthy: narrow_class(current, target, exact: false, environment: scope.environment, scope: scope),
2753
+ falsey: narrow_not_class(falsey_acc, target, exact: false, environment: scope.environment,
2754
+ scope: scope),
2288
2755
  fully_narrowable: true
2289
2756
  }
2290
2757
  end
@@ -2368,6 +2835,11 @@ module Rigor
2368
2835
  # through {#rooted_class_predicate_name}, which layers #614's extra decline on top of the
2369
2836
  # same walk. A rooted `::Foo` names the top level and never a lexically nearer shadow, so
2370
2837
  # it keeps the un-walked spelling. nil for any non-constant shape, as before.
2838
+ #
2839
+ # PUBLIC, alone among the helpers in this block (#655): `case`/`when`'s VALUE side lives in
2840
+ # `ExpressionTyper` and matched on the as-written spelling, so the two halves of one `case`
2841
+ # disagreed — the flow side narrowed through this walk while the value side resolved the
2842
+ # pattern against whatever the environment knew under that literal name.
2371
2843
  def lexical_class_name(node, scope)
2372
2844
  bare_name = static_class_name(node)
2373
2845
  return nil if bare_name.nil?
@@ -2375,6 +2847,7 @@ module Rigor
2375
2847
 
2376
2848
  resolve_class_name_lexically(bare_name, scope)
2377
2849
  end
2850
+ public :lexical_class_name
2378
2851
 
2379
2852
  # ----- narrow_class / narrow_not_class helpers -----
2380
2853
 
@@ -2437,8 +2910,7 @@ module Rigor
2437
2910
  end
2438
2911
 
2439
2912
  # Issue #657 — `Bot` is the assertion "this can never match", and only a KNOWN-disjoint ordering
2440
- # supports it. `subclass_of?` folds `:disjoint` and `:unknown` into one `false`, which is safe on
2441
- # the negative edge (where the pre-state survives) and an over-claim on the positive one:
2913
+ # supports it (the reason {#declines_bot?} exists, and the repro it was written against):
2442
2914
  #
2443
2915
  # module Taggable; def tag = "t"; end
2444
2916
  # class Integer; include Taggable; end
@@ -2460,8 +2932,7 @@ module Rigor
2460
2932
  def narrow_constant_to_class(constant, class_name, context)
2461
2933
  rigor_class = constant.value.class.name
2462
2934
  return constant if subclass_of?(rigor_class, class_name, context)
2463
- return Type::Combinator.untyped if !context.exact &&
2464
- class_ordering(rigor_class, class_name, context) == :unknown
2935
+ return Type::Combinator.untyped if declines_bot?(rigor_class, class_name, context)
2465
2936
 
2466
2937
  Type::Combinator.bot
2467
2938
  end
@@ -2477,9 +2948,12 @@ module Rigor
2477
2948
  # `is_a?(Integer)`) narrow DOWN to `Nominal[class_name]`; otherwise (disjoint
2478
2949
  # hierarchies under `is_a?`, mismatch under `instance_of?`) collapse to `Bot`.
2479
2950
  # Conservative when the analyzer environment cannot resolve either class.
2951
+ #
2952
+ # A disjoint `Bot` narrows the edge but is not always a verdict that the code is dead: see
2953
+ # {.disjoint_nominal_guard?} (issue #1429).
2480
2954
  def narrow_nominal_to_class(nominal, class_name, context)
2481
2955
  return nominal if nominal.class_name == class_name
2482
- return Type::Combinator.bot if context.exact
2956
+ return exact_nominal_to_class(nominal, class_name, context) if context.exact
2483
2957
 
2484
2958
  case class_ordering(nominal.class_name, class_name, context)
2485
2959
  when :superclass then Type::Combinator.nominal_of(class_name)
@@ -2494,6 +2968,17 @@ module Rigor
2494
2968
  end
2495
2969
  end
2496
2970
 
2971
+ # `instance_of?(class_name)` compares the exact class, so its truthy edge holds `class_name` itself. Issue
2972
+ # #1429: when `class_name` is a class below the nominal's (`Numeric` under `instance_of?(Integer)`), that is
2973
+ # the edge's type; the edge was `Bot` before, for every class but the nominal's own. A module is never an
2974
+ # object's class, and any other ordering leaves no class the object can have, so those stay `Bot`.
2975
+ def exact_nominal_to_class(nominal, class_name, context)
2976
+ return Type::Combinator.bot if context.environment.rbs_module?(class_name)
2977
+ return Type::Combinator.bot unless class_ordering(nominal.class_name, class_name, context) == :superclass
2978
+
2979
+ Type::Combinator.nominal_of(class_name)
2980
+ end
2981
+
2497
2982
  def narrow_nominal_not_class(nominal, class_name, context)
2498
2983
  return Type::Combinator.bot if nominal.class_name == class_name
2499
2984
  return nominal if context.exact
@@ -2505,8 +2990,14 @@ module Rigor
2505
2990
  end
2506
2991
  end
2507
2992
 
2993
+ # A shape projects to `Array` / `Hash`, and reopening either of those to include a project module
2994
+ # leaves the ordering `:unknown` for the same reason #657's `Integer` does — so `[1, 2]` under `when
2995
+ # Taggable` reached the same unsupported `Bot`. The carrier differs; the missing evidence does not.
2508
2996
  def narrow_shape_to_class(shape, projected_class, class_name, context)
2509
- subclass_of?(projected_class, class_name, context) ? shape : Type::Combinator.bot
2997
+ return shape if subclass_of?(projected_class, class_name, context)
2998
+ return Type::Combinator.untyped if declines_bot?(projected_class, class_name, context)
2999
+
3000
+ Type::Combinator.bot
2510
3001
  end
2511
3002
 
2512
3003
  def narrow_shape_not_class(shape, projected_class, class_name, context)
@@ -2517,8 +3008,69 @@ module Rigor
2517
3008
  # subclass of `Module`). Asking `Foo.is_a?(Class)` returns true; `Foo.is_a?(Foo)`
2518
3009
  # returns false unless `Foo` is `Class` itself. We approximate this by treating
2519
3010
  # singletons uniformly as `Class` instances.
3011
+ #
3012
+ # The approximation answers through `Class`, so a target `Class` cannot be ordered against — a
3013
+ # project module, typically — lands on the same `:unknown` #657 collapsed to `Bot`, and `case Widget
3014
+ # when Meta` was reported unreachable where `Widget` had `extend Meta`.
3015
+ #
3016
+ # Issue #898 — that decline is silent about a target the environment DOES order against `Class`, and
3017
+ # `extend Comparable` is one: both names are in RBS, neither includes the other, so the ordering says
3018
+ # `:disjoint` and the collapse fired on an arm MRI takes (`Widget.is_a?(Comparable)` is true — the
3019
+ # `extend` put `Comparable` in `Widget`'s SINGLETON ancestry, which asking about `Class` cannot see).
3020
+ # {#singleton_extend_declines_bot?} supplies the missing half from the project's own `extend` record.
2520
3021
  def narrow_singleton_to_class(singleton, class_name, context)
2521
- subclass_of?("Class", class_name, context) ? singleton : Type::Combinator.bot
3022
+ return singleton if subclass_of?("Class", class_name, context)
3023
+ return Type::Combinator.untyped if declines_bot?("Class", class_name, context)
3024
+ return Type::Combinator.untyped if singleton_extend_declines_bot?(singleton, class_name, context)
3025
+
3026
+ Type::Combinator.bot
3027
+ end
3028
+
3029
+ # Whether the class object's own `extend` record forbids the `Bot` the `Class` approximation would
3030
+ # otherwise authorise (#898). An extended `M` puts every ancestor of `M` in the singleton's
3031
+ # ancestry, so the question per extended module is `M <= target`: `:equal` (`extend Comparable`
3032
+ # under `when Comparable`) and `:subclass` support the arm, and `:unknown` (`extend Meta` under
3033
+ # `when Comparable`, `Meta` being project source the environment never joined to RBS) cannot rule
3034
+ # it out. `:superclass` and `:disjoint` support NOTHING and must keep the collapse — `Integer`
3035
+ # under `when Integer` orders `:superclass` against an extended `Comparable`, and reading that as
3036
+ # support would retract `case Widget when Integer` from every class that extends a core module.
3037
+ #
3038
+ # The answer is `Dynamic[top]`, never the preserved `Singleton[C]`, and the negative edge
3039
+ # ({#narrow_singleton_not_class}) is untouched — so this record is read ONLY to withhold a claim,
3040
+ # never to make one. That asymmetry is deliberate, and it is what keeps the fix honest against an
3041
+ # extend record that is structurally incomplete: `Widget.extend(m)` at a call site and an `extend`
3042
+ # written in a file outside the analysed set are both invisible to the walk behind
3043
+ # {Scope#singleton_extends_of}, and the declaration-side record joined to it below is no more
3044
+ # complete — RBS says nothing about a class it does not declare. A shape that turned this table into a positive
3045
+ # `:subclass` verdict would have to be right about ABSENCE too, and it cannot be. Preserving
3046
+ # `Singleton[C]` would also hand the arm a receiver whose singleton method table has no `clamp` on
3047
+ # it, trading the unreachable-clause false positive for an undefined-method one — the same trap
3048
+ # #657 recorded on the `Constant` carrier.
3049
+ #
3050
+ # Issue #915 — two sources, read as one. The source record above sees only what Ruby source spells;
3051
+ # a project that declares `extend M` in its own `sig/` puts the same module in the same singleton
3052
+ # ancestry, and the environment's declaration-side answer
3053
+ # ({Environment#singleton_extended_modules}) is consulted alongside it. Neither is asked to be
3054
+ # complete; each can only add a module that IS there, which is all the one-directional read needs.
3055
+ def singleton_extend_declines_bot?(singleton, class_name, context)
3056
+ return false if context.exact
3057
+
3058
+ extended = singleton_ancestor_modules(singleton, context)
3059
+ return false if extended.empty?
3060
+
3061
+ extended.any? do |module_name|
3062
+ %i[equal subclass unknown].include?(class_ordering(module_name, class_name, context))
3063
+ end
3064
+ end
3065
+
3066
+ def singleton_ancestor_modules(singleton, context)
3067
+ scope = context.scope
3068
+ from_source = scope.nil? ? [] : scope.singleton_extends_of(singleton.class_name)
3069
+ from_rbs = context.environment&.singleton_extended_modules(singleton.class_name) || []
3070
+ return from_source if from_rbs.empty?
3071
+ return from_rbs if from_source.empty?
3072
+
3073
+ from_source | from_rbs
2522
3074
  end
2523
3075
 
2524
3076
  def narrow_singleton_not_class(singleton, class_name, context)
@@ -2534,6 +3086,22 @@ module Rigor
2534
3086
  end
2535
3087
  end
2536
3088
 
3089
+ # Whether the positive edge must decline the `Bot` that `subclass_of?` returning false would
3090
+ # otherwise authorise (#657). `subclass_of?` folds `:disjoint` and `:unknown` into one `false`, and
3091
+ # only the first is evidence that the guard can never match; the second is the environment saying it
3092
+ # cannot tell, which any file reopening either side can make true.
3093
+ #
3094
+ # This only ever ADDS a decline: every caller keeps its own `subclass_of?` test first, so the
3095
+ # preserving edge is untouched, and a `:disjoint`, `:equal`, `:subclass` or `:superclass` verdict
3096
+ # never reaches here. There is no ordering it turns INTO a match — the honest answer for
3097
+ # "cannot tell" is Dynamic, never the asked class.
3098
+ #
3099
+ # `instance_of?` is exempt because it compares the exact class: a module is never an object's class,
3100
+ # so that edge's `Bot` rests on the guard's own semantics rather than on the ordering.
3101
+ def declines_bot?(rigor_class_name, target_class_name, context)
3102
+ !context.exact && class_ordering(rigor_class_name, target_class_name, context) == :unknown
3103
+ end
3104
+
2537
3105
  # Returns `true` when an instance of `rigor_class_name` satisfies
2538
3106
  # `is_a?(target_class_name)` (or `instance_of?(target_class_name)` when `exact: true`).
2539
3107
  # Falls back to the safe `false` when either name does not resolve through the analyzer
@@ -2555,25 +3123,15 @@ module Rigor
2555
3123
  context.environment.class_ordering(lhs, rhs)
2556
3124
  end
2557
3125
 
3126
+ # `x.nil?` on a local, instance variable, global or constant (#1429) read.
2558
3127
  def analyse_nil_predicate(receiver, scope)
2559
- case receiver
2560
- when Prism::LocalVariableReadNode
2561
- current = scope.local(receiver.name)
2562
- return nil if current.nil?
2563
-
2564
- [
2565
- scope.with_local(receiver.name, narrow_nil(current)),
2566
- scope.with_local(receiver.name, narrow_non_nil(current))
2567
- ]
2568
- when Prism::InstanceVariableReadNode
2569
- current = scope.ivar(receiver.name)
2570
- return nil if current.nil?
3128
+ slot = receiver_slot(receiver, scope)
3129
+ return nil if slot.nil?
2571
3130
 
2572
- [
2573
- scope.with_ivar(receiver.name, narrow_nil(current)),
2574
- scope.with_ivar(receiver.name, narrow_non_nil(current))
2575
- ]
2576
- end
3131
+ [
3132
+ narrow_slot(scope, slot, narrow_nil(slot.current)),
3133
+ narrow_slot(scope, slot, narrow_non_nil(slot.current))
3134
+ ]
2577
3135
  end
2578
3136
 
2579
3137
  # The three value-pinned classes a union arm can name whose instances are the single
@@ -2692,21 +3250,39 @@ module Rigor
2692
3250
  def analyse_safe_nav_receiver(node, scope)
2693
3251
  return nil unless node.safe_navigation?
2694
3252
 
2695
- receiver = node.receiver
2696
- reader, writer =
2697
- case receiver
2698
- when Prism::LocalVariableReadNode then %i[local with_local]
2699
- when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
2700
- else return nil
2701
- end
3253
+ narrowed = non_nil_slot_scope(node.receiver, scope)
3254
+ narrowed && [narrowed, scope]
3255
+ end
2702
3256
 
2703
- current = scope.public_send(reader, receiver.name)
2704
- return nil if current.nil?
3257
+ # `scope` with the receiver `node` reads (a local, instance variable, global or constant, #1429) narrowed to
3258
+ # its non-nil fragment, or nil when there is no such receiver or it holds no `nil` to remove.
3259
+ def non_nil_slot_scope(node, scope)
3260
+ slot = receiver_slot(node, scope)
3261
+ return nil if slot.nil?
2705
3262
 
2706
- non_nil = narrow_non_nil(current)
2707
- return nil if non_nil.equal?(current)
3263
+ non_nil = narrow_non_nil(slot.current)
3264
+ return nil if non_nil.equal?(slot.current)
2708
3265
 
2709
- [scope.public_send(writer, receiver.name, non_nil), scope]
3266
+ narrow_slot(scope, slot, non_nil)
3267
+ end
3268
+
3269
+ # The scope {.safe_navigation_scope} answers for a receiver, seen through the `&.` links of a chain whose
3270
+ # operands leave the root's binding alone, or nil. Unlike {.non_nil_slot_scope} it compares the fragment
3271
+ # structurally: `narrow_non_nil` rebuilds a union it removed nothing from, and a receiver that cannot be nil
3272
+ # must leave the call's operands on the scope they are typed from.
3273
+ def non_nil_receiver_scope(node, scope)
3274
+ if node.is_a?(Prism::CallNode)
3275
+ return nil unless node.safe_navigation?
3276
+ return nil if OperandEffects.any?(node.arguments) || OperandEffects.any?(node.block)
3277
+
3278
+ return non_nil_receiver_scope(node.receiver, scope)
3279
+ end
3280
+
3281
+ slot = receiver_slot(node, scope)
3282
+ return nil if slot.nil?
3283
+
3284
+ non_nil = narrow_non_nil(slot.current)
3285
+ non_nil == slot.current ? nil : narrow_slot(scope, slot, non_nil)
2710
3286
  end
2711
3287
 
2712
3288
  # Issue #606 slice 1 — the SAFE-NAV CHAIN forms. {.analyse_safe_nav_receiver} above proves
@@ -2778,12 +3354,17 @@ module Rigor
2778
3354
  return nil if suffixes.empty? && !allow_bare
2779
3355
 
2780
3356
  receiver = current.receiver
2781
- return nil unless receiver.is_a?(Prism::LocalVariableReadNode) ||
2782
- receiver.is_a?(Prism::InstanceVariableReadNode)
3357
+ return nil unless SLOT_READ_NODES.any? { |klass| receiver.is_a?(klass) }
2783
3358
 
2784
3359
  [receiver, suffixes]
2785
3360
  end
2786
3361
 
3362
+ SLOT_READ_NODES = [
3363
+ Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode,
3364
+ Prism::ConstantReadNode, Prism::ConstantPathNode
3365
+ ].freeze
3366
+ private_constant :SLOT_READ_NODES
3367
+
2787
3368
  # Which edge — if either — a chain's outcome narrows on. The inner suffixes (everything
2788
3369
  # below the outermost) only ever have to NOT return, so they are held to the raise test
2789
3370
  # alone; the outermost decides the edge per the table in {NIL_FALSEY_SUFFIXES}'s comment.
@@ -2798,20 +3379,7 @@ module Rigor
2798
3379
  end
2799
3380
 
2800
3381
  def safe_nav_chain_narrowed_scope(receiver, scope)
2801
- reader, writer =
2802
- case receiver
2803
- when Prism::LocalVariableReadNode then %i[local with_local]
2804
- when Prism::InstanceVariableReadNode then %i[ivar with_ivar]
2805
- else return nil
2806
- end
2807
-
2808
- current = scope.public_send(reader, receiver.name)
2809
- return nil if current.nil?
2810
-
2811
- non_nil = narrow_non_nil(current)
2812
- return nil if non_nil.equal?(current)
2813
-
2814
- scope.public_send(writer, receiver.name, non_nil)
3382
+ non_nil_slot_scope(receiver, scope)
2815
3383
  end
2816
3384
 
2817
3385
  # Issue #606 slice 1, second form — a safe-nav chain COMPARED to a value that cannot be
@@ -2921,14 +3489,23 @@ module Rigor
2921
3489
  return edges unless node.safe_navigation? && edges
2922
3490
 
2923
3491
  truthy, falsey = edges
2924
- safe = analyse_safe_nav_receiver(node, scope)
2925
- return edges unless safe
3492
+ slot = receiver_slot(node.receiver, scope)
3493
+ return edges if slot.nil?
2926
3494
 
2927
- receiver = node.receiver
2928
- reader = receiver.is_a?(Prism::InstanceVariableReadNode) ? :ivar : :local
2929
- non_nil = safe.first.public_send(reader, receiver.name)
2930
- writer = receiver.is_a?(Prism::InstanceVariableReadNode) ? :with_ivar : :with_local
2931
- [truthy.public_send(writer, receiver.name, non_nil), falsey]
3495
+ non_nil = narrow_non_nil(slot.current)
3496
+ return edges if non_nil.equal?(slot.current)
3497
+
3498
+ [narrow_slot(truthy, slot, non_nil), falsey]
3499
+ end
3500
+
3501
+ # The predicates composed from other predicates, whose edges are built from their
3502
+ # operands' edges.
3503
+ def analyse_composition(node, scope)
3504
+ case node
3505
+ when Prism::AndNode then analyse_and(node, scope)
3506
+ when Prism::OrNode then analyse_or(node, scope)
3507
+ else analyse_conditional(node, scope)
3508
+ end
2932
3509
  end
2933
3510
 
2934
3511
  # `a && b` short-circuits: the truthy edge is the truthy edge of `b` evaluated under
@@ -2938,7 +3515,7 @@ module Rigor
2938
3515
  # output scopes.
2939
3516
  def analyse_and(node, scope)
2940
3517
  truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
2941
- truthy_b, falsey_b = analyse(node.right, truthy_a) || [truthy_a, truthy_a]
3518
+ truthy_b, falsey_b = operand_edges(node.right, truthy_a)
2942
3519
  [truthy_b, falsey_a.join(falsey_b)]
2943
3520
  end
2944
3521
 
@@ -2947,9 +3524,105 @@ module Rigor
2947
3524
  # falsey scope evaluated under `a`'s falsey scope.
2948
3525
  def analyse_or(node, scope)
2949
3526
  truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
2950
- truthy_b, falsey_b = analyse(node.right, falsey_a) || [falsey_a, falsey_a]
3527
+ truthy_b, falsey_b = operand_edges(node.right, falsey_a)
2951
3528
  [truthy_a.join(truthy_b), falsey_b]
2952
3529
  end
3530
+
3531
+ # The edges of an operand that runs only on one edge of an earlier operand, which is the
3532
+ # step `&&` and `||` both take for their right operand. Issue #1359 — the operand runs after
3533
+ # that edge, so a `$_` the earlier operand narrowed is forgotten when the operand may set it
3534
+ # (`gets && log(items.each { gets })`), before the operand's own edges narrow it again.
3535
+ # Issue #1429 — likewise a guard's narrowing of a global or constant the operand may rebind
3536
+ # (`$sep && reset_sep && $sep.length`).
3537
+ def operand_edges(node, edge_scope)
3538
+ edge_scope = LastLine.forget_if_set(edge_scope, node)
3539
+ if edge_scope.guard_narrowed? && GuardRebinding.may_rebind?(node, edge_scope)
3540
+ edge_scope = edge_scope.forget_guard_narrowings
3541
+ end
3542
+ analyse(node, edge_scope) || [edge_scope, edge_scope]
3543
+ end
3544
+
3545
+ # Issue #1017 — `p ? q : r` (and `if` / `unless` in condition position) is truthy through
3546
+ # `p ∧ q` or `¬p ∧ r` and falsey through `p ∧ ¬q` or `¬p ∧ ¬r`. Each conjunction is the
3547
+ # `&&` step — the arm's edges under the predicate's edge ({#operand_edges}) — and each
3548
+ # disjunction is the `||` merge (`Scope#join`), so `p ? false : q` narrows as `!p && q`
3549
+ # does without a literal-arm rule: an arm whose value is settled contributes only its
3550
+ # live edge ({#settled_edges}). Beyond {CONDITIONAL_GUARD_DEPTH} the guard declines.
3551
+ #
3552
+ # The predicate's own edges are never dropped as settled. `scope` is the scope after the
3553
+ # whole conditional ran, so it has already joined the arms, and a carrier every arm
3554
+ # narrowed has lost its optimistic mark there (`Scope#with_local` drops it): a
3555
+ # `missable.nil?` predicate reads as a proof-looking `false` it is not. An arm is judged
3556
+ # under the predicate's edge instead, where a settled value follows from that edge.
3557
+ def analyse_conditional(node, scope)
3558
+ return nil if conditional_guard_height(node) > CONDITIONAL_GUARD_DEPTH
3559
+
3560
+ predicate, then_arm, else_arm = conditional_parts(node)
3561
+ truthy_p, falsey_p = operand_edges(predicate, scope)
3562
+ truthy_q, falsey_q = settled_edges(then_arm, truthy_p)
3563
+ truthy_r, falsey_r = settled_edges(else_arm, falsey_p)
3564
+ truthy = join_live_edges(truthy_q, truthy_r)
3565
+ falsey = join_live_edges(falsey_q, falsey_r)
3566
+ return nil if truthy.nil? && falsey.nil?
3567
+
3568
+ [truthy || scope, falsey || scope]
3569
+ end
3570
+
3571
+ # `[predicate, arm run when it is truthy, arm run when it is falsey]`. An arm is a body or
3572
+ # `nil` for an absent / empty one; an `elsif` is the nested conditional itself.
3573
+ def conditional_parts(node)
3574
+ if node.is_a?(Prism::UnlessNode)
3575
+ [node.predicate, else_body(node.else_clause), node.statements]
3576
+ else
3577
+ subsequent = node.subsequent
3578
+ [node.predicate, node.statements, subsequent.is_a?(Prism::ElseNode) ? else_body(subsequent) : subsequent]
3579
+ end
3580
+ end
3581
+
3582
+ def else_body(else_node)
3583
+ else_node&.statements
3584
+ end
3585
+
3586
+ # An operand's `[truthy, falsey]` edges with a dead edge as `nil`. An absent arm evaluates
3587
+ # to `nil`, so only its falsey edge is live. Otherwise the edge the operand's type proves
3588
+ # unreachable is dropped — unless that proof rests on an optimistically nil-free lookup
3589
+ # (ADR-101, issue #313), which `branch_certainty` refuses to elide an arm on as well.
3590
+ def settled_edges(node, scope)
3591
+ return [nil, scope] if node.nil?
3592
+
3593
+ truthy, falsey = operand_edges(node, scope)
3594
+ case predicate_certainty(scope.type_of(node))
3595
+ when :truthy then optimistic_operand?(node, scope) ? [truthy, falsey] : [truthy, nil]
3596
+ when :falsey then optimistic_operand?(node, scope) ? [truthy, falsey] : [nil, falsey]
3597
+ else [truthy, falsey]
3598
+ end
3599
+ end
3600
+
3601
+ def optimistic_operand?(node, scope)
3602
+ node = node.body.last while node.is_a?(Prism::StatementsNode) && !node.body.empty?
3603
+ !OptimisticOrigin.resolve(node, scope).nil?
3604
+ end
3605
+
3606
+ def join_live_edges(left, right)
3607
+ return right if left.nil?
3608
+ return left if right.nil?
3609
+
3610
+ left.join(right)
3611
+ end
3612
+
3613
+ # The number of conditionals stacked in `node` along the shapes {.analyse} walks.
3614
+ def conditional_guard_height(node)
3615
+ case node
3616
+ when Prism::IfNode, Prism::UnlessNode
3617
+ 1 + conditional_parts(node).map { |part| conditional_guard_height(part) }.max
3618
+ when Prism::ParenthesesNode then conditional_guard_height(node.body)
3619
+ when Prism::StatementsNode then conditional_guard_height(node.body.last)
3620
+ when Prism::AndNode, Prism::OrNode
3621
+ [conditional_guard_height(node.left), conditional_guard_height(node.right)].max
3622
+ when Prism::CallNode then node.name == :! ? conditional_guard_height(node.receiver) : 0
3623
+ else 0
3624
+ end
3625
+ end
2953
3626
  end
2954
3627
  end
2955
3628
  end