rigortype 0.3.8 → 0.3.9

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 (447) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/gem_overlay/activesupport/core_ext.rbs +109 -13
  5. data/docs/handbook/02-everyday-types.md +17 -14
  6. data/docs/handbook/03-narrowing.md +37 -4
  7. data/docs/handbook/05-methods-and-blocks.md +1 -1
  8. data/docs/handbook/06-classes.md +2 -2
  9. data/docs/handbook/07-rbs-and-extended.md +14 -1
  10. data/docs/handbook/11-sig-gen.md +72 -1
  11. data/docs/handbook/README.md +1 -1
  12. data/docs/handbook/appendix-elixir.md +2 -2
  13. data/docs/handbook/appendix-go.md +2 -2
  14. data/docs/handbook/appendix-java-csharp.md +2 -2
  15. data/docs/handbook/appendix-mypy.md +2 -2
  16. data/docs/handbook/appendix-phpstan.md +1 -1
  17. data/docs/handbook/appendix-rust.md +1 -1
  18. data/docs/handbook/appendix-type-theory.md +4 -4
  19. data/docs/handbook/appendix-typescript.md +1 -1
  20. data/docs/llms.txt +2 -0
  21. data/docs/manual/02-cli-reference.md +32 -4
  22. data/docs/manual/03-configuration.md +11 -0
  23. data/docs/manual/04-diagnostics.md +11 -0
  24. data/docs/manual/07-plugins.md +15 -5
  25. data/docs/manual/08-skills.md +23 -2
  26. data/docs/manual/11-ci.md +9 -0
  27. data/docs/manual/15-type-protection-coverage.md +8 -0
  28. data/docs/manual/16-rbs-extended-annotations.md +6 -1
  29. data/docs/manual/18-removing-dead-code.md +10 -8
  30. data/docs/manual/plugins/rigor-actionpack.md +18 -0
  31. data/docs/manual/plugins/rigor-activerecord.md +28 -0
  32. data/docs/manual/plugins/rigor-activesupport-core-ext.md +32 -2
  33. data/docs/manual/plugins/rigor-rbs-inline.md +42 -1
  34. data/lib/rigor/analysis/baseline.rb +2 -2
  35. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  36. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  37. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  38. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +1 -1
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  40. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/rule_walk.rb +39 -4
  42. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  43. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  44. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  45. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  46. data/lib/rigor/analysis/check_rules.rb +200 -32
  47. data/lib/rigor/analysis/crash_signature.rb +8 -24
  48. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  52. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  53. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  54. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  55. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  56. data/lib/rigor/analysis/fact_store.rb +6 -1
  57. data/lib/rigor/analysis/incremental.rb +16 -0
  58. data/lib/rigor/analysis/incremental_session.rb +81 -22
  59. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  60. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  61. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -5
  62. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  63. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -2
  65. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  66. data/lib/rigor/analysis/result.rb +1 -3
  67. data/lib/rigor/analysis/rule_catalog.rb +4 -1
  68. data/lib/rigor/analysis/run_cache_key.rb +1 -1
  69. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  70. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  71. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  72. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +177 -26
  73. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  74. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -10
  75. data/lib/rigor/analysis/runner/pool_coordinator.rb +256 -88
  76. data/lib/rigor/analysis/runner/project_pre_passes.rb +12 -6
  77. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  78. data/lib/rigor/analysis/runner.rb +133 -35
  79. data/lib/rigor/analysis/worker_session.rb +9 -10
  80. data/lib/rigor/bleeding_edge.rb +12 -24
  81. data/lib/rigor/builtins/hkt_builtins.rb +2 -2
  82. data/lib/rigor/builtins/imported_refinements.rb +155 -21
  83. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  84. data/lib/rigor/builtins/regex_refinement.rb +4 -4
  85. data/lib/rigor/builtins/static_return_refinements.rb +5 -6
  86. data/lib/rigor/cache/annotation_location.rb +2 -4
  87. data/lib/rigor/cache/descriptor.rb +58 -20
  88. data/lib/rigor/cache/engine_source.rb +4 -4
  89. data/lib/rigor/cache/file_digest.rb +9 -1
  90. data/lib/rigor/cache/incremental_snapshot.rb +40 -6
  91. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  92. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  93. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  94. data/lib/rigor/cache/rbs_descriptor.rb +79 -11
  95. data/lib/rigor/cache/rbs_environment.rb +8 -4
  96. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  97. data/lib/rigor/cache/store.rb +77 -32
  98. data/lib/rigor/cli/annotate_command.rb +5 -6
  99. data/lib/rigor/cli/check_command.rb +57 -10
  100. data/lib/rigor/cli/check_invocation.rb +6 -11
  101. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  102. data/lib/rigor/cli/coverage_command.rb +1 -1
  103. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  104. data/lib/rigor/cli/coverage_scan.rb +1 -5
  105. data/lib/rigor/cli/diff_command.rb +1 -1
  106. data/lib/rigor/cli/doc_links.rb +3 -3
  107. data/lib/rigor/cli/docs_command.rb +2 -2
  108. data/lib/rigor/cli/doctor_command.rb +85 -38
  109. data/lib/rigor/cli/effects_command.rb +2 -2
  110. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  111. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  112. data/lib/rigor/cli/explain_command.rb +34 -1
  113. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  114. data/lib/rigor/cli/lsp_command.rb +1 -1
  115. data/lib/rigor/cli/mcp_command.rb +1 -1
  116. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  117. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  118. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  119. data/lib/rigor/cli/plugin_command.rb +1 -1
  120. data/lib/rigor/cli/plugins_command.rb +2 -1
  121. data/lib/rigor/cli/prism_colorizer.rb +2 -2
  122. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  123. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  124. data/lib/rigor/cli/sig_gen_command.rb +2 -2
  125. data/lib/rigor/cli/skill_command.rb +1 -1
  126. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  127. data/lib/rigor/cli/skill_describe.rb +36 -35
  128. data/lib/rigor/cli/trace_command.rb +4 -4
  129. data/lib/rigor/cli/trace_renderer.rb +5 -6
  130. data/lib/rigor/cli/triage_command.rb +1 -1
  131. data/lib/rigor/cli/type_of_command.rb +10 -6
  132. data/lib/rigor/cli/type_scan_command.rb +4 -4
  133. data/lib/rigor/cli/unused_command.rb +11 -3
  134. data/lib/rigor/cli/upgrade_command.rb +1 -1
  135. data/lib/rigor/cli.rb +68 -6
  136. data/lib/rigor/config_audit.rb +30 -5
  137. data/lib/rigor/configuration/severity_profile.rb +6 -6
  138. data/lib/rigor/configuration.rb +43 -4
  139. data/lib/rigor/effects/attribution.rb +1 -3
  140. data/lib/rigor/effects/config_envelopes.rb +6 -8
  141. data/lib/rigor/effects/definition_lines.rb +25 -6
  142. data/lib/rigor/effects/effect_table.rb +0 -1
  143. data/lib/rigor/effects/entry_points.rb +0 -2
  144. data/lib/rigor/effects/envelope_check.rb +8 -8
  145. data/lib/rigor/effects/envelope_index.rb +5 -8
  146. data/lib/rigor/effects/framework_units.rb +4 -6
  147. data/lib/rigor/effects/identity.rb +4 -6
  148. data/lib/rigor/effects/inline_anchor.rb +7 -7
  149. data/lib/rigor/effects/label_intent.rb +3 -4
  150. data/lib/rigor/effects/liskov_check.rb +8 -8
  151. data/lib/rigor/effects/method_key.rb +1 -1
  152. data/lib/rigor/effects/plugin_facts.rb +4 -6
  153. data/lib/rigor/effects/propagator.rb +2 -3
  154. data/lib/rigor/effects/scanner.rb +1 -1
  155. data/lib/rigor/effects/signature_sources.rb +3 -5
  156. data/lib/rigor/effects/snapshot.rb +10 -10
  157. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  158. data/lib/rigor/effects/unit_scan.rb +10 -10
  159. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  160. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  161. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  162. data/lib/rigor/environment/class_registry.rb +3 -1
  163. data/lib/rigor/environment/failure_slot.rb +2 -2
  164. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  165. data/lib/rigor/environment/lockfile_resolver.rb +51 -4
  166. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  167. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  168. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  169. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  170. data/lib/rigor/environment/rbs_loader.rb +388 -59
  171. data/lib/rigor/environment.rb +105 -20
  172. data/lib/rigor/flow_contribution/merger.rb +0 -2
  173. data/lib/rigor/flow_contribution.rb +11 -13
  174. data/lib/rigor/inference/acceptance.rb +153 -8
  175. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  176. data/lib/rigor/inference/body_fixpoint.rb +5 -5
  177. data/lib/rigor/inference/budget_trace.rb +1 -5
  178. data/lib/rigor/inference/captured_locals.rb +2 -3
  179. data/lib/rigor/inference/closure_escape_analyzer.rb +2 -4
  180. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  181. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  182. data/lib/rigor/inference/element_read_widening.rb +168 -0
  183. data/lib/rigor/inference/expression_typer.rb +763 -143
  184. data/lib/rigor/inference/fork_map.rb +5 -7
  185. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  186. data/lib/rigor/inference/hkt_registry.rb +4 -7
  187. data/lib/rigor/inference/index_write_widening.rb +1 -5
  188. data/lib/rigor/inference/macro_block_self_type.rb +1 -4
  189. data/lib/rigor/inference/method_dispatcher/block_folding.rb +2 -9
  190. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  191. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  192. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  193. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  194. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +2 -2
  195. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  197. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  198. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  199. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +61 -47
  200. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  201. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +165 -31
  202. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  203. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  204. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  206. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  208. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  209. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -5
  210. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  211. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  212. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  213. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  214. data/lib/rigor/inference/method_dispatcher.rb +12 -13
  215. data/lib/rigor/inference/method_parameter_binder.rb +4 -6
  216. data/lib/rigor/inference/multi_target_binder.rb +1 -3
  217. data/lib/rigor/inference/mutation_rejoin.rb +168 -0
  218. data/lib/rigor/inference/mutation_widening.rb +100 -51
  219. data/lib/rigor/inference/narrowing.rb +299 -46
  220. data/lib/rigor/inference/optimistic_origin.rb +1 -7
  221. data/lib/rigor/inference/origin_lookup.rb +2 -3
  222. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  223. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  224. data/lib/rigor/inference/precision_scanner.rb +3 -4
  225. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  226. data/lib/rigor/inference/project_patched_scanner.rb +3 -3
  227. data/lib/rigor/inference/protection_scanner.rb +1 -2
  228. data/lib/rigor/inference/range_constant.rb +57 -0
  229. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  230. data/lib/rigor/inference/receiver_alias.rb +3 -3
  231. data/lib/rigor/inference/refinement_mutation.rb +72 -0
  232. data/lib/rigor/inference/scope_indexer.rb +688 -151
  233. data/lib/rigor/inference/statement_evaluator.rb +221 -50
  234. data/lib/rigor/inference/string_mutation.rb +60 -0
  235. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  236. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  237. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  238. data/lib/rigor/inference/version_guard.rb +28 -21
  239. data/lib/rigor/inference/void_origin.rb +3 -3
  240. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  241. data/lib/rigor/language_server/buffer_table.rb +4 -4
  242. data/lib/rigor/language_server/completion_provider.rb +1 -1
  243. data/lib/rigor/language_server/debouncer.rb +1 -1
  244. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  245. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  246. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  247. data/lib/rigor/language_server/hover_provider.rb +1 -1
  248. data/lib/rigor/language_server/hover_renderer.rb +2 -2
  249. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  250. data/lib/rigor/language_server/project_context.rb +2 -5
  251. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  252. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  253. data/lib/rigor/language_server/server.rb +9 -9
  254. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  255. data/lib/rigor/language_server/uri.rb +1 -1
  256. data/lib/rigor/plugin/base.rb +90 -21
  257. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  258. data/lib/rigor/plugin/effect_attribution.rb +7 -7
  259. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  260. data/lib/rigor/plugin/fact_store.rb +6 -6
  261. data/lib/rigor/plugin/io_boundary.rb +93 -8
  262. data/lib/rigor/plugin/isolation.rb +29 -6
  263. data/lib/rigor/plugin/loader.rb +31 -12
  264. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  265. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  266. data/lib/rigor/plugin/manifest.rb +23 -1
  267. data/lib/rigor/plugin/registry.rb +42 -12
  268. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +12 -1
  269. data/lib/rigor/plugin/trust_policy.rb +2 -4
  270. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  271. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  272. data/lib/rigor/project_environment.rb +138 -0
  273. data/lib/rigor/protection/analysis_guard.rb +93 -14
  274. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  275. data/lib/rigor/protection/dependency_closure.rb +5 -8
  276. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  277. data/lib/rigor/protection/discovery_seed.rb +11 -11
  278. data/lib/rigor/protection/kill_signature.rb +2 -4
  279. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  280. data/lib/rigor/protection/mutation_cache.rb +6 -9
  281. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  282. data/lib/rigor/protection/mutator.rb +2 -1
  283. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  284. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  287. data/lib/rigor/rbs_extended.rb +61 -21
  288. data/lib/rigor/reflection/constant_path.rb +126 -0
  289. data/lib/rigor/reflection.rb +26 -10
  290. data/lib/rigor/runtime/jit.rb +6 -8
  291. data/lib/rigor/scope/discovery_index.rb +30 -0
  292. data/lib/rigor/scope.rb +203 -4
  293. data/lib/rigor/sig_gen/generator.rb +115 -22
  294. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  295. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  296. data/lib/rigor/sig_gen/observation_collector.rb +17 -15
  297. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  298. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  299. data/lib/rigor/sig_gen/renderer.rb +4 -5
  300. data/lib/rigor/sig_gen/skip_reason_catalog.rb +111 -0
  301. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  302. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  303. data/lib/rigor/sig_gen/writer.rb +23 -6
  304. data/lib/rigor/signature_path_audit.rb +153 -6
  305. data/lib/rigor/source/literals.rb +0 -23
  306. data/lib/rigor/source/node_children.rb +0 -2
  307. data/lib/rigor/source/node_locator.rb +5 -11
  308. data/lib/rigor/source/node_walker.rb +2 -5
  309. data/lib/rigor/triage/catalogue.rb +1 -3
  310. data/lib/rigor/triage.rb +3 -5
  311. data/lib/rigor/type/accepts_result.rb +24 -6
  312. data/lib/rigor/type/combinator.rb +68 -5
  313. data/lib/rigor/type/data_class.rb +2 -2
  314. data/lib/rigor/type/data_instance.rb +4 -4
  315. data/lib/rigor/type/float_range.rb +128 -0
  316. data/lib/rigor/type/hash_shape.rb +5 -5
  317. data/lib/rigor/type/integer_range.rb +13 -8
  318. data/lib/rigor/type/nominal.rb +8 -2
  319. data/lib/rigor/type/refined.rb +3 -3
  320. data/lib/rigor/type/struct_class.rb +3 -3
  321. data/lib/rigor/type/struct_instance.rb +4 -4
  322. data/lib/rigor/type.rb +1 -0
  323. data/lib/rigor/type_node/generic.rb +1 -1
  324. data/lib/rigor/type_node/range_literal.rb +25 -0
  325. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  326. data/lib/rigor/type_node.rb +1 -0
  327. data/lib/rigor/version.rb +1 -1
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  329. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  330. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  331. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -0
  332. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  333. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  334. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  335. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +1 -0
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +33 -30
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  338. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  339. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +28 -2
  340. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  341. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  342. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  343. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  344. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  345. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  346. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +1 -0
  347. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +3 -4
  348. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  349. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  350. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +42 -7
  351. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  352. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +119 -14
  355. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  356. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +1 -0
  357. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  358. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  359. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  360. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  361. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  362. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  363. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  364. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  365. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  366. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  367. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  368. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  369. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  370. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  371. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  372. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +6 -3
  373. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  374. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  375. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +0 -3
  376. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  377. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  378. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  379. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  380. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  381. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +1 -0
  382. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  383. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  384. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  385. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -0
  386. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  387. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -10
  388. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +3 -3
  389. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +2 -2
  390. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -0
  395. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  396. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  397. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +128 -3
  398. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  399. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  400. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  403. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  404. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  405. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  406. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  407. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  408. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  409. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  410. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -1
  411. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +1 -0
  414. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  415. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +4 -5
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +5 -5
  423. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  424. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  425. data/sig/rigor/cache.rbs +2 -3
  426. data/sig/rigor/environment.rbs +5 -0
  427. data/sig/rigor/inference.rbs +3 -10
  428. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  429. data/sig/rigor/plugin/manifest.rbs +2 -1
  430. data/sig/rigor/reflection.rbs +3 -1
  431. data/sig/rigor/scope.rbs +24 -3
  432. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -0
  433. data/sig/rigor/type.rbs +27 -1
  434. data/sig/rigor.rbs +1 -0
  435. data/skills/rigor-ask/SKILL.md +8 -6
  436. data/skills/rigor-plugin-author/references/02-walker-and-types.md +13 -2
  437. data/skills/rigor-project-init/SKILL.md +6 -3
  438. data/skills/rigor-project-init/references/02-configure.md +10 -6
  439. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  440. data/skills/rigor-type-oracle/SKILL.md +228 -0
  441. data/skills/rigor-type-oracle/references/01-oracle-commands.md +261 -0
  442. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  443. data/skills/rigor-type-oracle/references/03-gap-protocol.md +128 -0
  444. data/skills/rigor-unused-adjudicate/SKILL.md +8 -5
  445. metadata +26 -3
  446. data/lib/rigor/cli/probe_environment.rb +0 -85
  447. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -14,9 +14,11 @@ require_relative "body_fixpoint"
14
14
  require_relative "captured_locals"
15
15
  require_relative "dynamic_origin"
16
16
  require_relative "../analysis/check_rules/inferred_param_guard"
17
+ require_relative "../analysis/check_rules/published_constant_guard"
17
18
  require_relative "struct_fold_safety"
18
19
  require_relative "closure_escape_analyzer"
19
20
  require_relative "content_join"
21
+ require_relative "element_read_widening"
20
22
  require_relative "indexed_narrowing"
21
23
  require_relative "index_write_widening"
22
24
  require_relative "method_dispatcher"
@@ -107,7 +109,9 @@ module Rigor
107
109
  Prism::SingletonClassNode => :eval_singleton_class,
108
110
  Prism::CallNode => :eval_call,
109
111
  Prism::BlockNode => :eval_block,
112
+ Prism::LambdaNode => :eval_lambda,
110
113
  Prism::ReturnNode => :eval_return,
114
+ Prism::NextNode => :eval_next,
111
115
  Prism::BreakNode => :eval_break,
112
116
  Prism::MatchWriteNode => :eval_match_write
113
117
  }.freeze
@@ -121,6 +125,18 @@ module Rigor
121
125
  RETURN_SINK_KEY = :rigor_return_sink
122
126
  private_constant :RETURN_SINK_KEY
123
127
 
128
+ # Thread-local sink (an Array of `[NextNode, Type]`) collecting the value each `next` carries out of the block it
129
+ # leaves, so `ExpressionTyper#type_block_body` can join those arms into the block's value type. Issue #841: the
130
+ # block-return pass modelled only the fall-through tail, so `ops.all? { |o| next false unless o; true }` read as
131
+ # `Constant[true]` and folded the call to always-truthy — the same defect `RETURN_SINK_KEY` fixes one level up for
132
+ # a method's early `return`. The flow value of a `next` stays `Bot`.
133
+ #
134
+ # The sink also collects `next`s from nested blocks / loops / defs evaluated under the same installation (they do
135
+ # not install their own), so the consumer filters by node identity against a statically computed
136
+ # directly-targeting set, exactly as the break sink does. nil means "not collecting".
137
+ NEXT_SINK_KEY = :rigor_next_sink
138
+ private_constant :NEXT_SINK_KEY
139
+
124
140
  # Thread-local sink (an Array of `[BreakNode, Scope]`) collecting the scope at each `break` reached while
125
141
  # evaluating a loop body, so `eval_loop` / `eval_for` can join a `break`-path binding (`flag = true; break`) into
126
142
  # the loop continuation that the fall-through would otherwise drop. Stacks like the return sink: a nested loop
@@ -131,6 +147,19 @@ module Rigor
131
147
  BREAK_SINK_KEY = :rigor_break_sink
132
148
  private_constant :BREAK_SINK_KEY
133
149
 
150
+ # Thread-local sink (an Array of `[BreakNode, Type]`) collecting the value each `break` carries out of the
151
+ # construct it leaves. Issue #853: `break value` terminates the yielding CALL and is that call's value, so —
152
+ # unlike `next`, which {NEXT_SINK_KEY} joins into the block — these arms are consumed one level up, by
153
+ # `ExpressionTyper#call_dispatch_type_for`. Without them `ops.all? { |o| break false unless o; true }` folded to
154
+ # `Constant[true]` and warned about a program that really can answer false.
155
+ #
156
+ # Separate from {BREAK_SINK_KEY}, which records SCOPES for the loop-continuation join: the two consumers want
157
+ # different things from the same node, and a loop body evaluated under a call's collection must keep feeding its
158
+ # own sink. Both are filtered by node identity against a statically computed directly-targeting set, so a `break`
159
+ # that belongs to an inner loop or block never reaches the wrong consumer. nil means "not collecting".
160
+ BREAK_VALUE_SINK_KEY = :rigor_break_value_sink
161
+ private_constant :BREAK_VALUE_SINK_KEY
162
+
134
163
  # Lexical class frame: the `name:` field is the qualified class name as it would render in Ruby (e.g.,
135
164
  # `"Foo::Bar"`); the `singleton:` field is `true` for `class << self` frames so nested defs resolve to
136
165
  # singleton-method RBS lookups.
@@ -145,20 +174,18 @@ module Rigor
145
174
  EMPTY_NESTING = [].freeze
146
175
  private_constant :EMPTY_NESTING
147
176
 
148
- # @param scope [Rigor::Scope]
149
- # @param tracer [Rigor::Inference::FallbackTracer, nil]
150
- # @param on_enter [#call, nil] optional `(node, scope) ->` callable
177
+ # @param on_enter — optional `(node, scope) ->` callable
151
178
  # invoked once at the start of every {#evaluate} call (the node
152
179
  # itself, *before* its handler runs). Threaded through every
153
180
  # recursive `sub_eval` so the tooling that builds a per-node
154
181
  # scope index (`Rigor::Inference::ScopeIndexer`) can record the
155
182
  # entry scope for every Prism node the evaluator visits without
156
183
  # the StatementEvaluator carrying any additional state itself.
157
- # @param class_context [Array<ClassFrame>] lexical class scope used
184
+ # @param class_context lexical class scope used
158
185
  # by {#eval_def} to look up the method's RBS signature. Each
159
186
  # `ClassNode`/`ModuleNode` entry pushes a frame; `SingletonClassNode`
160
187
  # over `self` flips the innermost frame to singleton mode.
161
- # @param converged_loop_recording [Boolean] when true (and an
188
+ # @param converged_loop_recording when true (and an
162
189
  # `on_enter` recorder is installed), {#eval_loop} re-evaluates a
163
190
  # fixpoint-tracked loop body ONE extra time from the CONVERGED
164
191
  # bindings so the last-visit-wins per-node scope index reflects
@@ -193,11 +220,40 @@ module Rigor
193
220
  [result, sink]
194
221
  end
195
222
 
223
+ # Runs `block` with a fresh `next` sink installed, then yields the collected `[NextNode, Type]` pairs to the
224
+ # caller. Stacks like the return sink, and for the same reason: a block body evaluated inside another block body's
225
+ # evaluation must not spill its arms into the outer collection. Used by `ExpressionTyper#type_block_body` to join
226
+ # the `next` arms into the block's value type.
227
+ def self.with_next_sink
228
+ previous = Thread.current[NEXT_SINK_KEY]
229
+ sink = []
230
+ Thread.current[NEXT_SINK_KEY] = sink
231
+ begin
232
+ result = yield
233
+ ensure
234
+ Thread.current[NEXT_SINK_KEY] = previous
235
+ end
236
+ [result, sink]
237
+ end
238
+
239
+ # Runs `block` with a fresh `break`-value sink installed, then yields the collected `[BreakNode, Type]` pairs to
240
+ # the caller. Stacks like the other sinks: a block body evaluated inside another block body's evaluation must not
241
+ # spill its arms into the outer collection. Used by `ExpressionTyper#call_dispatch_type_for` to union the `break`
242
+ # arms into the yielding call's type (issue #853).
243
+ def self.with_break_value_sink
244
+ previous = Thread.current[BREAK_VALUE_SINK_KEY]
245
+ sink = []
246
+ Thread.current[BREAK_VALUE_SINK_KEY] = sink
247
+ begin
248
+ result = yield
249
+ ensure
250
+ Thread.current[BREAK_VALUE_SINK_KEY] = previous
251
+ end
252
+ [result, sink]
253
+ end
254
+
196
255
  # Evaluate `node` under the receiver scope. Returns `[type, scope']` where `type` is the value the node produces
197
256
  # and `scope'` is the scope observable after the node has run. The receiver scope is never mutated.
198
- #
199
- # @param node [Prism::Node]
200
- # @return [Array(Rigor::Type, Rigor::Scope)]
201
257
  def evaluate(node)
202
258
  @on_enter&.call(node, @scope)
203
259
 
@@ -246,14 +302,27 @@ module Rigor
246
302
  # to a follow-up; for now they degrade to "type the rhs, do not rebind" via the default branch in {#evaluate}.
247
303
  def eval_local_write(node)
248
304
  rhs_type, post_rhs = sub_eval(node.value, scope)
305
+ bound = bind_local_write(node, rhs_type, post_rhs)
306
+ # Issue #667 — `m = AppConfig::MODE` makes `m` a copy of a constant the reader's file does not
307
+ # declare, so `m == :production` folding to `true` is the project's configuration and not a logic
308
+ # error the author can see. Stamped LAST, across every binding path above: an RHS can be both an
309
+ # ADR-58 ivar copy and a published-constant copy (`@mode = AppConfig::MODE` in the ctor, `m = @mode`
310
+ # here), and the two marks answer unrelated questions.
311
+ return [rhs_type, bound] unless published_constant_copy?(node.value, rhs_type, post_rhs)
312
+
313
+ [rhs_type, bound.with_published_constant_mark(:local, node.name)]
314
+ end
315
+
316
+ # The binding half of {#eval_local_write}: the three mutually exclusive provenance paths a local write
317
+ # can take, unchanged from when they were inline.
318
+ def bind_local_write(node, rhs_type, post_rhs)
249
319
  # ADR-58 WD1 — `r = @right` where `@right`'s optionality is purely declaration-sourced makes `r`
250
320
  # declaration-sourced too (the survey's exact rotation/traversal shape `r = @right; r.key`). The mark is
251
321
  # computed on the RHS *value*'s provenance — a pure ivar read of a currently declaration-sourced ivar — so it
252
322
  # survives the local copy. Any other RHS (a call result, a method-local-nil-bearing value) leaves the local
253
323
  # flow-live and the diagnostic fires as before.
254
- if declaration_sourced_ivar_read?(node.value, post_rhs)
255
- return [rhs_type, post_rhs.with_declaration_sourced_local(node.name, rhs_type)]
256
- end
324
+ return post_rhs.with_declaration_sourced_local(node.name, rhs_type) if
325
+ declaration_sourced_ivar_read?(node.value, post_rhs)
257
326
 
258
327
  bound = post_rhs.with_local(node.name, rhs_type)
259
328
  # ADR-67 WD6b — a local whose RHS is (transitively) rooted at an inferred parameter inherits the
@@ -261,14 +330,24 @@ module Rigor
261
330
  # same lower-bound reason (`vindex = codepoints[i] - x; vindex < y`). The mark is STICKY across
262
331
  # narrowing/joins (see `Scope#without_inferred_param_mark`), so a genuine rewrite from a non-param RHS
263
332
  # must clear it here. No-op unless the `parameter_inference:` gate seeded a parameter this RHS reaches.
264
- if Analysis::CheckRules::InferredParamGuard.rooted?(node.value, scope)
265
- return [rhs_type, bound.with_inferred_param_mark(node.name)]
266
- end
333
+ return bound.with_inferred_param_mark(node.name) if
334
+ Analysis::CheckRules::InferredParamGuard.rooted?(node.value, scope)
267
335
 
268
336
  bound = bound.without_inferred_param_mark(node.name)
269
337
  bound = bound.with_local_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
270
- bound = bound.with_optimistic_local(node.name, optimistic_rhs_origin(node.value, post_rhs))
271
- [rhs_type, bound]
338
+ bound.with_optimistic_local(node.name, optimistic_rhs_origin(node.value, post_rhs))
339
+ end
340
+
341
+ # Issue #667 — true when this write copies a value whose constancy rests on a foreign published
342
+ # constant. The `Type::Constant` pre-gate is what keeps the question cheap and is not merely an
343
+ # optimisation: the mark exists to withhold `flow.always-truthy-condition`, which only ever fires on a
344
+ # predicate that folded, so a non-constant RHS has nothing to withhold. It also keeps the guard's one
345
+ # interprocedural hop off every `x = foo(y)` in a project that publishes anything at all — the same
346
+ # gate {Analysis::CheckRules::AlwaysTruthyConditionCollector} puts in front of it.
347
+ def published_constant_copy?(value_node, rhs_type, scope_after_rhs)
348
+ return false unless rhs_type.is_a?(Type::Constant)
349
+
350
+ Analysis::CheckRules::PublishedConstantGuard.rooted?(value_node, scope_after_rhs)
272
351
  end
273
352
 
274
353
  # Issue #286 — the optimistic-nil-free counterpart of {#rhs_origin}, differing in two ways. It does not
@@ -314,6 +393,11 @@ module Rigor
314
393
  bound = post_rhs.with_ivar(node.name, rhs_type)
315
394
  bound = bound.with_ivar_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
316
395
  bound = bound.with_optimistic_ivar(node.name, optimistic_rhs_origin(node.value, post_rhs))
396
+ # Issue #667 — the ivar twin of the local stamp. This is the SAME-method half; the cross-method one
397
+ # (`@mode = AppConfig::MODE` in `initialize`, read in a sibling) rides the class-ivar census and is
398
+ # stamped by {#seed_instance_ivars}.
399
+ bound = bound.with_published_constant_mark(:ivar, node.name) if
400
+ published_constant_copy?(node.value, rhs_type, post_rhs)
317
401
  [rhs_type, bound]
318
402
  end
319
403
 
@@ -351,24 +435,38 @@ module Rigor
351
435
  # The class context a meta-new rvalue block is entered under, or nil when the rvalue is not that shape. The
352
436
  # KEY must be the one `ScopeIndexer` filed the body's defs and member layout under, so the two passes agree —
353
437
  # which is why the decision is delegated to the ScopeIndexer's own recognition rather than re-spelled here. A
354
- # `Prism::ConstantWriteNode` whose rvalue `ScopeIndexer.meta_new_block_body` recognises is keyed by the
355
- # constant's qualified name, one frame appended to the lexical context exactly as a `class Const` keyword body
356
- # would be. Every other block form — a constant-PATH write (`A::B = Struct.new do end`, which has no
357
- # constant-keyed registration) or a shape the ScopeIndexer's stricter argument check rejects
358
- # (`Const = Struct.new(*names) do … end`) — was registered under the call site's anonymous name and is
359
- # entered under that.
438
+ # constant write whose rvalue `ScopeIndexer.meta_new_block_body` recognises is keyed by the constant's
439
+ # qualified name, one frame appended to the lexical context exactly as a `class Const` keyword body would be
440
+ # the path spelling included since [#703](https://github.com/rigortype/rigor/issues/703). A shape the
441
+ # ScopeIndexer's stricter argument check rejects (`Const = Struct.new(*names) do … end`) is registered under
442
+ # the call site's anonymous name and is entered under that.
360
443
  def meta_new_constant_body_context(node)
361
444
  call_node = node.value
362
445
  return nil unless call_node.is_a?(Prism::CallNode) && call_node.block.is_a?(Prism::BlockNode)
363
446
 
364
- if node.is_a?(Prism::ConstantWriteNode) && ScopeIndexer.meta_new_block_body(node)
365
- return @class_context + [ClassFrame.new(name: node.name.to_s, singleton: false)]
366
- end
447
+ constant = meta_new_constant_context(node)
448
+ return constant if constant
367
449
 
368
450
  anonymous = AnonymousMetaClass.name_for(call_node, scope.source_path)
369
451
  anonymous && [ClassFrame.new(name: anonymous, singleton: false)]
370
452
  end
371
453
 
454
+ # The frame stack for a constant-keyed meta-new body, or nil when nothing keys it by a constant. A ROOTED
455
+ # path write re-anchors at the top level the way a rooted `class ::Rooted::Bar` header does
456
+ # ({#eval_class_or_module}), so the stack resets to that frame alone rather than gaining one under the
457
+ # enclosure — `current_class_path` joins the stack, and `ScopeIndexer`'s own prefix resets there too.
458
+ def meta_new_constant_context(node)
459
+ return nil unless ScopeIndexer.meta_new_block_body(node)
460
+
461
+ case node
462
+ when Prism::ConstantWriteNode
463
+ @class_context + [ClassFrame.new(name: node.name.to_s, singleton: false)]
464
+ when Prism::ConstantPathWriteNode
465
+ frame = ClassFrame.new(name: Source::ConstantPath.qualified_name(node.target), singleton: false)
466
+ Source::ConstantPath.rooted?(node.target) ? [frame] : @class_context + [frame]
467
+ end
468
+ end
469
+
372
470
  # Slice 7 phase 3 — compound writes (||=, &&=, +=/-=/...) for every variable kind. Each handler:
373
471
  # 1. Reads the current type from the appropriate scope
374
472
  # binding map (or `Dynamic[Top]` when unbound).
@@ -550,10 +648,23 @@ module Rigor
550
648
  # join will not consume is pure overhead, and `<<` on a String buffer is one of the commonest calls
551
649
  # there is. The content-adder table skips a non-adding mutator (`pop`, `sort!`); `joinable_receiver?`
552
650
  # skips a receiver whose current binding is not a literal-shape carrier.
651
+ # Both mutator-receiver widenings, against one typing of the call's arguments: the bindings the
652
+ # receiver NAMES ({MutationWidening.widen_after_call}) and, when it names none because the
653
+ # receiver is an element read into a local, that element's pin inside its container
654
+ # ({ElementReadWidening.widen_element_read}, issue #643).
655
+ def widen_mutated_receivers(call_node, current_scope)
656
+ arg_types = mutator_arg_types(call_node, current_scope)
657
+ widened = MutationWidening.widen_after_call(call_node: call_node, current_scope: current_scope,
658
+ arg_types: arg_types)
659
+ ElementReadWidening.widen_element_read(call_node: call_node, current_scope: widened, arg_types: arg_types)
660
+ end
661
+
553
662
  def mutator_arg_types(call_node, current_scope)
554
663
  return MutationWidening::NO_ARG_TYPES unless ContentJoin::CONTENT_ADDERS.include?(call_node.name)
555
- return MutationWidening::NO_ARG_TYPES unless MutationWidening.joinable_receiver?(call_node.receiver,
556
- current_scope)
664
+ unless MutationWidening.joinable_receiver?(call_node.receiver, current_scope) ||
665
+ ElementReadWidening.joinable_element_read?(call_node.receiver, current_scope)
666
+ return MutationWidening::NO_ARG_TYPES
667
+ end
557
668
 
558
669
  content_arg_types(call_node, scope)
559
670
  end
@@ -1557,8 +1668,7 @@ module Rigor
1557
1668
  # Flow-folding G1 / G2 — widen a local- or instance-variable binding when the call is an in-place mutator on it
1558
1669
  # (e.g. `arms << x`, `@tags << hashtag`). Stops a literal-shape carrier (`Tuple` / `HashShape`) from outliving
1559
1670
  # its justification when the value is mutated. Always-safe (loses precision, never invents facts).
1560
- post_scope = MutationWidening.widen_after_call(call_node: node, current_scope: post_scope,
1561
- arg_types: mutator_arg_types(node, post_scope))
1671
+ post_scope = widen_mutated_receivers(node, post_scope)
1562
1672
  # ADR-48 slice 4 — Struct member-setter re-typing. After `s.x = v` on a fold-safe StructInstance local, rebind
1563
1673
  # `s` to a StructInstance with member `:x` replaced by the assigned type, so a later `s.x` folds to `v` and a
1564
1674
  # sibling `s.y` stays precise. `call_type` is the setter's own result (the assigned value type). Sound only for
@@ -2071,8 +2181,7 @@ module Rigor
2071
2181
  classification = classify_closure_escape(node)
2072
2182
  return post_scope if classification == :non_escaping
2073
2183
 
2074
- post_scope = drop_captured_narrowing(node.block, post_scope)
2075
- post_scope = widen_escaping_content_captures(node.block, post_scope)
2184
+ post_scope = escaping_closure_captures(node.block, post_scope)
2076
2185
  post_scope.with_fact(
2077
2186
  Analysis::FactStore::Fact.new(
2078
2187
  bucket: :dynamic_origin,
@@ -2562,15 +2671,22 @@ module Rigor
2562
2671
  ].freeze
2563
2672
  private_constant :INDEX_WRITE_NODES
2564
2673
 
2674
+ # The shared "not a content mutation" answer. This predicate runs on every node of every block, loop and
2675
+ # method body it censuses (~950k calls on the lib self-check) and almost always declines, so a fresh
2676
+ # `[nil, nil]` per decline was one of the largest allocation sites in the evaluator.
2677
+ NO_CONTENT_MUTATION = [nil, nil].freeze
2678
+ private_constant :NO_CONTENT_MUTATION
2679
+
2565
2680
  # `[receiver_name, node]` when `node` is a content mutation whose receiver is a local variable satisfying `accept`
2566
- # (depth predicate), else `[nil, nil]`. Covers `[]=`-style CallNode mutators and the index-write node forms.
2681
+ # (depth predicate), else the frozen `[nil, nil]`. Covers `[]=`-style CallNode mutators and the index-write node
2682
+ # forms.
2567
2683
  def content_mutation_target(node)
2568
2684
  is_call_mutator = node.is_a?(Prism::CallNode) && ContentJoin::CONTENT_ADDERS.include?(node.name)
2569
- return [nil, nil] unless is_call_mutator || index_write?(node)
2685
+ return NO_CONTENT_MUTATION unless is_call_mutator || index_write?(node)
2570
2686
 
2571
2687
  receiver = node.receiver
2572
- return [nil, nil] unless receiver.is_a?(Prism::LocalVariableReadNode)
2573
- return [nil, nil] unless yield(receiver)
2688
+ return NO_CONTENT_MUTATION unless receiver.is_a?(Prism::LocalVariableReadNode)
2689
+ return NO_CONTENT_MUTATION unless yield(receiver)
2574
2690
 
2575
2691
  [receiver.name, node]
2576
2692
  end
@@ -2675,6 +2791,32 @@ module Rigor
2675
2791
  sub_eval(node.body, scope)
2676
2792
  end
2677
2793
 
2794
+ # Issue #878 — `->() { }` and `lambda { }` build the same object, so they MUST type the same. The `lambda`
2795
+ # spelling is an ordinary call carrying a `Prism::BlockNode` and reaches {#eval_call}; the `->` spelling is a
2796
+ # node of its own, and without an entry here it was only ever typed as an expression (`Proc`) with its body left
2797
+ # un-evaluated. The body's reads and literals still reported — the rule walker falls back to the enclosing
2798
+ # method's scope — but a write made INSIDE the body joined no scope, so every later read in the body kept the
2799
+ # pre-write type.
2800
+ #
2801
+ # The body is run under the same entry scope `lambda { }` gets ({#build_block_entry_scope}, which binds the
2802
+ # parameters and enters `self` opaque), and the continuation gets the same escaping-closure treatment: a lambda
2803
+ # literal is a value that outlives the expression, exactly like the Proc `lambda` returns, so the outer locals it
2804
+ # can rebind lose their narrowing rather than being written back through ADR-56's non-escaping fixpoint. Both
2805
+ # halves are widenings — the two spellings now agree in both directions instead of `->` being the precise one.
2806
+ def eval_lambda(node)
2807
+ lambda_type = scope.type_of(node, tracer: tracer)
2808
+ sub_eval(node.body, build_block_entry_scope(nil, node)) unless node.body.nil?
2809
+
2810
+ [lambda_type, escaping_closure_captures(node, scope)]
2811
+ end
2812
+
2813
+ # The continuation effects of an escaping closure body: the outer locals it can rebind drop their narrowing, and
2814
+ # the ones it content-mutates are floored. Shared by the two spellings — a block on an escaping call
2815
+ # ({#record_closure_escape_if_any}) and a lambda literal ({#eval_lambda}) — so they cannot drift.
2816
+ def escaping_closure_captures(closure_node, post_scope)
2817
+ widen_escaping_content_captures(closure_node, drop_captured_narrowing(closure_node, post_scope))
2818
+ end
2819
+
2678
2820
  # Builds the entry scope for a block body. The block sees the outer scope's locals (Ruby's lexical scoping rule)
2679
2821
  # and adds bindings for every named block parameter on top. Parameter types come from the receiving method's RBS
2680
2822
  # signature when one is available; the rest default to `Dynamic[Top]`.
@@ -2703,7 +2845,11 @@ module Rigor
2703
2845
  params_root.locals.map(&:name)
2704
2846
  end
2705
2847
 
2848
+ # A lambda literal has no receiving method, so it passes no call node and every parameter defaults to
2849
+ # `Dynamic[Top]` — the same answer `lambda { |y| }` gets, where the implicit-self receiver yields no signature.
2706
2850
  def expected_block_param_types_for(call_node)
2851
+ return [] if call_node.nil?
2852
+
2707
2853
  receiver_type = call_node.receiver ? scope.type_of(call_node.receiver, tracer: tracer) : nil
2708
2854
  return [] if receiver_type.nil?
2709
2855
 
@@ -2846,7 +2992,15 @@ module Rigor
2846
2992
  # flow-live, so `seed_declaration_sourced_ivar` marks each seeded ivar: `possible-nil-receiver` then declines to
2847
2993
  # fire on the cross-method invariant unless a method-local write or narrowing makes the nil flow-live (which
2848
2994
  # drops the mark).
2849
- seeded.reduce(body_scope) { |acc, (name, type)| acc.seed_declaration_sourced_ivar(name, type) }
2995
+ #
2996
+ # Issue #667 — the same seed carries the published-constant mark for the ivars the class-ivar census
2997
+ # found assigned straight from a foreign published constant, so `@mode == :production` in a sibling
2998
+ # method withholds `flow.always-truthy-condition` the way the direct read of the constant does.
2999
+ marked = scope.published_constant_ivars_for(path)
3000
+ seeded.reduce(body_scope) do |acc, (name, type)|
3001
+ stamped = acc.seed_declaration_sourced_ivar(name, type)
3002
+ marked.include?(name) ? stamped.with_published_constant_mark(:ivar, name) : stamped
3003
+ end
2850
3004
  end
2851
3005
 
2852
3006
  # Cvars are visible from BOTH instance and singleton method bodies of the enclosing class, so this seed is
@@ -3013,7 +3167,22 @@ module Rigor
3013
3167
  # inferred.
3014
3168
  def eval_return(node)
3015
3169
  sink = Thread.current[RETURN_SINK_KEY]
3016
- record_return_value(node, sink) if sink
3170
+ sink << jump_value_type(node) if sink
3171
+ [Type::Combinator.bot, scope]
3172
+ end
3173
+
3174
+ # `next value` ends the current block invocation and makes `value` that invocation's result. Structurally the same
3175
+ # story as {#eval_return} one level down: the control-transfer value is `Bot`, and the escaping value is recorded
3176
+ # into the active `next` sink so `ExpressionTyper#type_block_body` joins it with the body's fall-through tail
3177
+ # (issue #841). The node itself is recorded alongside the type because the sink also collects `next`s belonging to
3178
+ # a nested block / loop / def; the consumer filters by identity.
3179
+ #
3180
+ # A `next` on a provably dead branch is never reached by the evaluator at all (`eval_if` / `eval_unless` skip the
3181
+ # dead arm), so the join is flow-sensitive for free: `[1, 2].map { |x| next nil if x.nil?; x.to_s }` keeps its
3182
+ # exact per-element answer.
3183
+ def eval_next(node)
3184
+ sink = Thread.current[NEXT_SINK_KEY]
3185
+ sink << [node, jump_value_type(node)] if sink
3017
3186
  [Type::Combinator.bot, scope]
3018
3187
  end
3019
3188
 
@@ -3022,26 +3191,28 @@ module Rigor
3022
3191
  # would drop (`flag = true; break` -> `flag` is `false | true` after the loop). nil sink = a `break` not inside an
3023
3192
  # inferred loop body (a block targeting a method, or top-level) — left to the existing escaping-block / no-op
3024
3193
  # handling.
3194
+ #
3195
+ # Issue #853: the value it carries out belongs to the yielding CALL, so it is recorded into the separate
3196
+ # break-value sink for `ExpressionTyper#call_dispatch_type_for` to union in. Both sinks are optional and
3197
+ # independent — a loop body collects scopes while an enclosing call collects values from the same walk.
3025
3198
  def eval_break(node)
3026
3199
  sink = Thread.current[BREAK_SINK_KEY]
3027
3200
  sink << [node, scope] if sink
3201
+ value_sink = Thread.current[BREAK_VALUE_SINK_KEY]
3202
+ value_sink << [node, jump_value_type(node)] if value_sink
3028
3203
  [Type::Combinator.bot, scope]
3029
3204
  end
3030
3205
 
3031
- def record_return_value(node, sink)
3206
+ # The value a `return` / `next` / `break` carries out of the construct it leaves. A bare jump carries nil; a
3207
+ # single argument carries its own type; `return a, b, c` (and `next` / `break` alike) packs `[a, b, c]`, so the
3208
+ # corresponding Tuple is contributed element-by-element. The argument is evaluated under the entry scope and the
3209
+ # resulting scope discarded — control leaves here, so nothing the argument binds is observable downstream.
3210
+ def jump_value_type(node)
3032
3211
  args = node.arguments&.arguments || []
3033
- # `return` with no argument returns nil; `return a` records the argument's type; `return a, b, c` packs a Tuple
3034
- # — in Ruby a multi-value return yields the array `[a, b, c]`, so the inferred return contributes the
3035
- # corresponding Tuple element-by-element.
3036
- if args.empty?
3037
- sink << Type::Combinator.constant_of(nil)
3038
- elsif args.size == 1
3039
- type, = sub_eval(args.first, scope)
3040
- sink << type
3041
- else
3042
- element_types = args.map { |arg| sub_eval(arg, scope).first }
3043
- sink << Type::Combinator.tuple_of(*element_types)
3044
- end
3212
+ return Type::Combinator.constant_of(nil) if args.empty?
3213
+ return sub_eval(args.first, scope).first if args.size == 1
3214
+
3215
+ Type::Combinator.tuple_of(*args.map { |arg| sub_eval(arg, scope).first })
3045
3216
  end
3046
3217
 
3047
3218
  def sub_eval(node, with_scope, class_context: @class_context, lexical_nesting: @lexical_nesting)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # The String half of {MutationWidening}'s straight-line seam (issue #617 residue (4)).
8
+ #
9
+ # `s = +"ab"; s << "c"; s == "ab"` folded `flow.always-truthy-condition` on correct code: the seam
10
+ # responded to `Tuple` / `HashShape` / empty-witness carriers only, while a `Constant[String]` is just as
11
+ # falsified by an in-place mutator as a `Tuple` is by `<<`. The ADR-56 block-capture path already widened
12
+ # the same binding, so the two seams disagreed about one carrier.
13
+ #
14
+ # It lives beside `MutationWidening` rather than inside it because the tables here answer a different
15
+ # question from the Array / Hash ones: those partition a carrier's SHAPE evidence, this one partitions a
16
+ # VALUE pin and an emptiness proof.
17
+ module StringMutation
18
+ # Receiver-mutating String methods only. The non-bang siblings (`upcase` vs `upcase!`, `sub` vs `sub!`)
19
+ # return a new String and leave the receiver's value intact, so they stay precise.
20
+ MUTATORS = %i[
21
+ << concat insert prepend replace clear []= slice!
22
+ sub! gsub! tr! tr_s! delete! squeeze! succ! next!
23
+ upcase! downcase! capitalize! swapcase! reverse!
24
+ strip! lstrip! rstrip! chomp! chop! force_encoding
25
+ ].to_set.freeze
26
+
27
+ # The {MUTATORS} that can leave the receiver EMPTY, and so retract a `non-empty-string` refinement. The
28
+ # rest are appenders or same-length rewrites: `s << x` / `s.prepend(x)` / `s.upcase!` cannot empty a
29
+ # string that already holds a character, so the refinement is still provable afterwards.
30
+ EMPTYING_MUTATORS = %i[
31
+ replace clear slice! []=
32
+ sub! gsub! delete! squeeze! strip! lstrip! rstrip! chomp! chop!
33
+ ].to_set.freeze
34
+
35
+ module_function
36
+
37
+ # `Constant["ab"]` under a String mutator → the bare `String` nominal. The pinned value is what the
38
+ # mutation falsifies, and nothing else about the carrier survives it, so there is no `values: :keep`
39
+ # half to preserve the way a `Tuple`'s surviving slots have one: an appended, substituted or
40
+ # case-folded string is a different string.
41
+ #
42
+ # Only a String-valued `Constant` widens. Every other literal (`Integer`, `Symbol`, `Range`) can only
43
+ # reach this arm through a name in the Array or Hash table and not in {MUTATORS}, and answering for it
44
+ # would erase a value no in-place mutator on that class can change.
45
+ #
46
+ # `nil` — no widening — for every other input, which is the contract `MutationWidening#widen_for_mutator`
47
+ # signals a decline with.
48
+ def widen_constant(constant, method_name)
49
+ return nil unless constant?(constant)
50
+ return nil unless MUTATORS.include?(method_name)
51
+
52
+ Type::Combinator.nominal_of("String")
53
+ end
54
+
55
+ def constant?(type)
56
+ type.is_a?(Type::Constant) && type.value.is_a?(String)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -55,10 +55,10 @@ module Rigor
55
55
  node.is_a?(Prism::ModuleNode) || node.is_a?(Prism::SingletonClassNode)
56
56
  end
57
57
 
58
- # @param root [Prism::Node, nil] the local-variable scope to scan.
59
- # @param layout_lookup [#call] a `String -> Array[Symbol] | nil` resolver mapping a constant receiver name to its
58
+ # @param root the local-variable scope to scan.
59
+ # @param layout_lookup a `String -> Array[Symbol] | nil` resolver mapping a constant receiver name to its
60
60
  # struct member list.
61
- # @return [Set<Symbol>] the fold-safe local names.
61
+ # @return the fold-safe local names.
62
62
  def fold_safe_locals(root, layout_lookup)
63
63
  return EMPTY if root.nil?
64
64
 
@@ -208,10 +208,9 @@ module Rigor
208
208
  # Nested `def` / `class` / `module` bodies are skipped: their statements do not run during THIS body's
209
209
  # evaluation. Blocks are descended into — they share `self`.
210
210
  #
211
- # @param body [Prism::Node, nil]
212
- # @param member_names [Enumerable<Symbol>] the receiver carrier's member names.
213
- # @yieldparam name [Symbol] an unrecognised self-call selector.
214
- # @yieldreturn [Boolean] whether that sibling method is itself self-fold-safe.
211
+ # @param member_names the receiver carrier's member names.
212
+ # @yieldparam name an unrecognised self-call selector.
213
+ # @yieldreturn whether that sibling method is itself self-fold-safe.
215
214
  def self_fold_safe_body?(body, member_names, &sibling_pure)
216
215
  return false if body.nil?
217
216
 
@@ -22,8 +22,7 @@ module Rigor
22
22
  class SyntheticMethodIndex
23
23
  attr_reader :entries, :class_names
24
24
 
25
- # @param entries [Array<SyntheticMethod>]
26
- # @param class_names [Array<String>, Set<String>] names of classes the substrate synthesises wholesale (ADR-36
25
+ # @param class_names — names of classes the substrate synthesises wholesale (ADR-36
27
26
  # nested-class emission — the variant subclasses that have no RBS/source declaration of their own). Recorded so
28
27
  # `Environment#class_known?` can resolve them as classes (their constant reference + `.new` dispatch) even
29
28
  # though nothing else in the type universe declares them. Tier B/C method emissions leave this empty (their
@@ -40,26 +40,24 @@ module Rigor
40
40
  module SyntheticMethodScanner # rubocop:disable Metrics/ModuleLength
41
41
  module_function
42
42
 
43
- # @param plugin_registry [Rigor::Plugin::Registry]
44
- # @param paths [Array<String>] absolute paths to the project
43
+ # @param paths — absolute paths to the project
45
44
  # source files to scan.
46
- # @param environment [Rigor::Environment, nil] used for
45
+ # @param environment used for
47
46
  # inheritance resolution against RBS-known classes
48
47
  # (ActiveRecord::Base, Dry::Struct, etc.) that aren't
49
48
  # declared in project source.
50
- # @param fact_store [Rigor::Plugin::FactStore, nil]
49
+ # @param fact_store
51
50
  # the per-run cross-plugin fact store. ADR-18 lookups
52
51
  # (`Plugin::Macro::HeredocTemplate::Emit#returns_from_arg`)
53
52
  # consult this at scan time to resolve per-call-site
54
53
  # return types from published facts; without it, those
55
54
  # emit rows fall back to their static `returns:` (or
56
55
  # `"untyped"` → `Dynamic[Top]`).
57
- # @param buffer [Rigor::Analysis::BufferBinding, nil]
56
+ # @param buffer
58
57
  # editor-mode buffer binding. When set, reads for the
59
58
  # logical path resolve to the buffer's physical path so
60
59
  # the pre-pass sees the in-flight bytes instead of the
61
60
  # on-disk copy.
62
- # @return [Rigor::Inference::SyntheticMethodIndex]
63
61
  def scan(plugin_registry:, paths:, environment: nil, fact_store: nil, buffer: nil)
64
62
  templates = collect_templates(plugin_registry)
65
63
  registries = collect_trait_registries(plugin_registry)