rigortype 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # Issue #2039 — the instance variables an implicit-self or `self.` call's callee visibly assigns a value that may
10
+ # be `nil`, read without a proof. {SelfCallIvarWrites.of} declines for any reason it cannot rule out another
11
+ # write (a `yield` or a call on a parameter in the callee, `include Comparable`, `method_missing`, a reopening);
12
+ # the statement reset (`InterveningIvarReset.widen` under `mark: :unproven`) still leaves a name this answers
13
+ # unmarked, since a callee that writes `@tag = nil` where the program shows it makes the read nil-reportable
14
+ # whatever else it runs.
15
+ #
16
+ # A write counts only when its value can be `nil` by its syntax alone ({.nil_value?}): a `nil` literal, a
17
+ # safe-navigation call, a conditional with a missing arm or an arm that is one of these, an `a || b` whose `b` is
18
+ # one and whose `a` is no truthy literal, or an `a && b` whose `a` is one, or whose `b` is one and whose `a` is
19
+ # not `false`. A method call's result, a parameter or any other computed value does not (`@tag = @tag.strip`
20
+ # rewrites a value the guard proved present), nor does a call past a safe-navigation call (`@peer&.tag.upcase`
21
+ # raises on a `nil` receiver rather than answering `nil`). A multiple assignment counts a target whose own value
22
+ # in a literal list is one of these, or is missing; every target when the value is one of these; every target
23
+ # after the first when the value is a literal Ruby does not splat (`@a, @b = 1`); and a nested target
24
+ # (`(@a, @b), @c = ...`) on the same rule against its own value. A splat, a rest target, or any other value
25
+ # (`@a, @b = pair`, which Ruby splats when it is an Array) counts nothing.
26
+ #
27
+ # The callee is the first `def` of the call's name found on `self`'s class or, failing that, a project entry of
28
+ # its instance chain (`Scope#user_def_for`). An implicit-self or `self.` call in a read body is read the same
29
+ # way (`def reset = clear_tag`), up to {DEF_BUDGET} bodies. A `define_method`, `attr_writer`,
30
+ # `instance_variable_set`, `send` or `method_missing` writer shows no write node and answers nothing, and so
31
+ # does a nil-writer deeper than the budget.
32
+ module DirectIvarWrites
33
+ # Bodies that do not run where they are written, or run on another `self`; a lambda literal's body runs only
34
+ # when it is called.
35
+ NON_RUNNING_NODES = [
36
+ Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::LambdaNode
37
+ ].freeze
38
+ # Literals that are never `nil` or `false` and that a multiple assignment does not splat (no Array literal).
39
+ TRUTHY_LITERALS = [
40
+ Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode, Prism::StringNode,
41
+ Prism::InterpolatedStringNode, Prism::XStringNode, Prism::InterpolatedXStringNode, Prism::SymbolNode,
42
+ Prism::InterpolatedSymbolNode, Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode,
43
+ Prism::TrueNode, Prism::HashNode, Prism::RangeNode, Prism::SourceFileNode, Prism::SourceLineNode,
44
+ Prism::SourceEncodingNode
45
+ ].freeze
46
+ # Calls by these names leave the method (or may, through a `catch` or a `rescue` above it).
47
+ LEAVING_CALLS = %i[raise fail throw exit exit! abort].freeze
48
+ # The most `def` bodies one read follows.
49
+ DEF_BUDGET = 8
50
+ private_constant :NON_RUNNING_NODES, :TRUTHY_LITERALS, :LEAVING_CALLS, :DEF_BUDGET
51
+
52
+ module_function
53
+
54
+ # The names, as a `Set`, of the instance variables the callee of `call_node` assigns a possibly-nil value in
55
+ # its own body; empty when the callee resolves to no `def` the tables hold.
56
+ def of(call_node, scope)
57
+ names = Set.new
58
+ self_type = scope.self_type
59
+ return names unless self_type.is_a?(Type::Nominal)
60
+
61
+ read_callee(call_node, self_type.class_name.to_s, scope, names, {}.compare_by_identity)
62
+ names
63
+ rescue StandardError
64
+ Set.new
65
+ end
66
+
67
+ # Whether `node`, a value, can be `nil` by its syntax alone: a `nil` literal, a safe-navigation call, an
68
+ # `if` / `unless` / ternary / `case` with a missing arm or an arm whose value is one of these (a `case ... in`
69
+ # with no `else` raises instead), or an `||` / `&&` that can answer one of these.
70
+ def nil_value?(node)
71
+ case node
72
+ when nil, Prism::NilNode then true
73
+ when Prism::CallNode then node.safe_navigation?
74
+ when Prism::ParenthesesNode then nil_value?(last_value(node.body))
75
+ when Prism::IfNode then nil_value?(last_value(node.statements)) || else_nil?(node.subsequent)
76
+ when Prism::UnlessNode then nil_value?(last_value(node.statements)) || else_nil?(node.else_clause)
77
+ when Prism::CaseNode, Prism::CaseMatchNode then case_nil?(node)
78
+ when Prism::OrNode, Prism::AndNode then logical_nil?(node)
79
+ else false
80
+ end
81
+ end
82
+
83
+ # A `case ... in` with no `else` raises NoMatchingPatternError rather than answering `nil`.
84
+ def case_nil?(node)
85
+ return true if node.conditions.any? { |arm| nil_value?(last_value(arm.statements)) }
86
+ return false if node.is_a?(Prism::CaseMatchNode) && node.else_clause.nil?
87
+
88
+ else_nil?(node.else_clause)
89
+ end
90
+
91
+ # `a || b` answers `b` unless `a` is truthy; `a && b` answers `a` unless `a` is truthy, and `b` otherwise.
92
+ def logical_nil?(node)
93
+ if node.is_a?(Prism::OrNode)
94
+ nil_value?(node.right) && !truthy_literal?(node.left)
95
+ else
96
+ nil_value?(node.left) || (nil_value?(node.right) && !node.left.is_a?(Prism::FalseNode))
97
+ end
98
+ end
99
+
100
+ def read_callee(call_node, class_name, scope, names, visited)
101
+ def_node = callee_def(call_node, class_name, scope)
102
+ return if def_node.nil? || visited.key?(def_node) || visited.size >= DEF_BUDGET
103
+
104
+ visited[def_node] = true
105
+ read_body(def_node.body, names) { |call, into| read_callee(call, class_name, scope, into, visited) }
106
+ end
107
+
108
+ # Issue #2482 — the straight-line last-write rule. Over the top-level statements of a `def` body, in source
109
+ # order, a `nil` write to `@x` is dropped when a later top-level statement writes `@x` unconditionally (a plain
110
+ # `@x = v`, `@x ||= v` or a multiple-assignment target) with a value that is not `nil` by its syntax alone
111
+ # ({.non_nil_value?}). A value this module merely does not count as `nil` (`@user = User.find_by(id)`) drops
112
+ # nothing: a call's result may be `nil` at run time.
113
+ # Nothing is dropped across a statement that may leave the method first ({.may_leave?}), and a statement that
114
+ # may leave keeps its own `nil` writes. A write nested in a conditional, a loop, a `rescue`, a block or an
115
+ # `&&` / `||` is not unconditional and drops nothing. A body with a `rescue` or `ensure` is read whole.
116
+ def read_body(body, names, &on_self_call)
117
+ statements = straight_line(body)
118
+ return collect(body, names) { |call| on_self_call.call(call, names) } unless statements
119
+
120
+ live = Set.new
121
+ statements.each do |statement|
122
+ own = Set.new
123
+ collect(statement, own) { |call| on_self_call.call(call, own) }
124
+ if may_leave?(statement)
125
+ names.merge(live).merge(own)
126
+ live.clear
127
+ else
128
+ live.subtract(overwritten(statement)).merge(own)
129
+ end
130
+ end
131
+ names.merge(live)
132
+ end
133
+
134
+ # The top-level statements of a `def` body, seen through a lone parenthesised group (`def r = (a; b)`); nil
135
+ # for a body with a `rescue` / `ensure` clause, which is no straight line.
136
+ def straight_line(body)
137
+ return nil unless body.is_a?(Prism::StatementsNode)
138
+
139
+ only = body.body.first if body.body.size == 1
140
+ return straight_line(only.body) if only.is_a?(Prism::ParenthesesNode) && only.body.is_a?(Prism::StatementsNode)
141
+
142
+ body.body
143
+ end
144
+
145
+ # The instance variables the top-level `statement` writes unconditionally with a value that is not `nil` by its
146
+ # syntax alone.
147
+ def overwritten(statement)
148
+ case statement
149
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode
150
+ non_nil_value?(statement.value) ? [statement.name] : []
151
+ when Prism::MultiWriteNode then non_nil_targets(statement, statement.value)
152
+ else []
153
+ end
154
+ end
155
+
156
+ # The ivar targets of `targets` that `value` binds to a value not `nil` by its syntax alone: each against its own
157
+ # element of an Array literal, or the first against a literal Ruby does not splat. A rest target answers none,
158
+ # since the census declines it ({.destructure_nil_writes}); so does any other value (`@a, @tag = pair`, where
159
+ # `pair` may answer `[1]`).
160
+ def non_nil_targets(targets, value)
161
+ return [] unless targets.rest.nil?
162
+
163
+ elements =
164
+ if value.is_a?(Prism::ArrayNode)
165
+ return [] if value.elements.any?(Prism::SplatNode)
166
+
167
+ value.elements
168
+ elsif truthy_literal?(value) then [value]
169
+ else
170
+ []
171
+ end
172
+ targets.lefts.zip(elements).flat_map { |target, element| non_nil_target(target, element) }
173
+ end
174
+
175
+ def non_nil_target(target, value)
176
+ case target
177
+ when Prism::InstanceVariableTargetNode then non_nil_value?(value) ? [target.name] : []
178
+ when Prism::MultiTargetNode then non_nil_targets(target, value)
179
+ else []
180
+ end
181
+ end
182
+
183
+ # Whether `node` is never `nil` by its syntax alone: a truthy literal, an Array literal or a lambda literal.
184
+ def non_nil_value?(node) = truthy_literal?(node) || node.is_a?(Prism::ArrayNode) || node.is_a?(Prism::LambdaNode)
185
+
186
+ # Whether control may leave the method from inside `node` before its next statement runs: a `return`,
187
+ # `break`, `next`, `redo`, `retry`, `yield` or `super` (which may pass the block on), a call with a block or a
188
+ # `&` argument (whose block may `break` or `throw`), or a `raise` / `throw` / `exit` call. Read through every
189
+ # child, a lambda's body included, so it answers yes rather than miss one.
190
+ def may_leave?(node)
191
+ return false unless node.is_a?(Prism::Node)
192
+ return true if leaving_node?(node)
193
+
194
+ found = false
195
+ node.rigor_each_child { |child| found ||= may_leave?(child) }
196
+ found
197
+ end
198
+
199
+ def leaving_node?(node)
200
+ case node
201
+ when Prism::ReturnNode, Prism::BreakNode, Prism::NextNode, Prism::RedoNode, Prism::RetryNode, Prism::YieldNode,
202
+ Prism::SuperNode, Prism::ForwardingSuperNode
203
+ true
204
+ when Prism::CallNode then !node.block.nil? || LEAVING_CALLS.include?(node.name)
205
+ else false
206
+ end
207
+ end
208
+
209
+ # The chain fallback passes over an external entry rather than stopping at it: a `def` found past one only
210
+ # keeps a reset unmarked, which reports no more than the reset did before #2039.
211
+ def callee_def(call_node, class_name, scope)
212
+ return nil unless call_node.is_a?(Prism::CallNode)
213
+
214
+ name = call_node.name
215
+ own = def_node(scope.user_def_for(class_name, name))
216
+ return own if own
217
+
218
+ Scope::ResolutionChain.for(scope, class_name, :instance, :methods).search(scope) do |entry|
219
+ entry.external? ? nil : def_node(scope.user_def_for(entry.name, name))
220
+ end
221
+ end
222
+
223
+ def def_node(node) = node.is_a?(Prism::DefNode) && node.receiver.nil? ? node : nil
224
+
225
+ def collect(node, names, &on_self_call)
226
+ return unless node.is_a?(Prism::Node)
227
+ return if NON_RUNNING_NODES.any? { |klass| node.is_a?(klass) }
228
+
229
+ nil_writes(node, names)
230
+ on_self_call.call(node) if self_call?(node)
231
+ node.rigor_each_child { |child| collect(child, names, &on_self_call) }
232
+ end
233
+
234
+ def self_call?(node)
235
+ node.is_a?(Prism::CallNode) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
236
+ end
237
+
238
+ def nil_writes(node, names)
239
+ case node
240
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode
241
+ names << node.name if nil_value?(node.value)
242
+ when Prism::MultiWriteNode then multi_nil_writes(node, names)
243
+ end
244
+ end
245
+
246
+ def multi_nil_writes(node, names) = destructure_nil_writes(node, node.value, names)
247
+
248
+ # The targets of `targets` (a `MultiWriteNode`, or a nested `MultiTargetNode`) that `value` (nil for a missing
249
+ # element) may bind to `nil`: each against its own element of a literal list (a missing element is `nil`),
250
+ # every one when `value` itself may be `nil`, and every one after the first when `value` is a literal Ruby does
251
+ # not splat. A splat, a rest target or a computed value answers nothing, since it may be an Array.
252
+ def destructure_nil_writes(targets, value, names)
253
+ return unless targets.rest.nil?
254
+
255
+ if value.is_a?(Prism::ArrayNode)
256
+ return if value.elements.any?(Prism::SplatNode)
257
+
258
+ targets.lefts.each_with_index { |target, index| target_nil_write(target, value.elements[index], names) }
259
+ elsif nil_value?(value)
260
+ targets.lefts.each { |target| target_nil_write(target, nil, names) }
261
+ elsif truthy_literal?(value)
262
+ first, *others = targets.lefts
263
+ target_nil_write(first, value, names)
264
+ others.each { |target| target_nil_write(target, nil, names) }
265
+ end
266
+ end
267
+
268
+ def target_nil_write(target, value, names)
269
+ case target
270
+ when Prism::InstanceVariableTargetNode then names << target.name if nil_value?(value)
271
+ when Prism::MultiTargetNode then destructure_nil_writes(target, value, names)
272
+ end
273
+ end
274
+
275
+ # Whether `node` is a literal that is never `nil` or `false` and is no Array literal: `1 || nil` is never `nil`,
276
+ # and `@a, @b = 1` does not splat its value.
277
+ def truthy_literal?(node) = TRUTHY_LITERALS.any? { |klass| node.is_a?(klass) }
278
+
279
+ def last_value(statements)
280
+ statements.is_a?(Prism::StatementsNode) ? statements.body.last : statements
281
+ end
282
+
283
+ # Whether an `else` clause (or `elsif`) leaves the conditional's value possibly `nil`: missing, or its value
284
+ # is.
285
+ def else_nil?(clause)
286
+ case clause
287
+ when nil then true
288
+ when Prism::ElseNode then nil_value?(last_value(clause.statements))
289
+ else nil_value?(clause)
290
+ end
291
+ end
292
+ private_class_method :read_callee, :read_body, :straight_line, :overwritten, :non_nil_targets,
293
+ :non_nil_target, :non_nil_value?,
294
+ :may_leave?, :leaving_node?, :callee_def, :def_node, :collect, :self_call?, :nil_writes,
295
+ :case_nil?, :logical_nil?, :multi_nil_writes,
296
+ :destructure_nil_writes,
297
+ :target_nil_write, :truthy_literal?, :last_value, :else_nil?
298
+ end
299
+ end
300
+ end
@@ -16,6 +16,11 @@ module Rigor
16
16
  # with an argument return a sub-ARRAY, not an element, so they are excluded by arity below.
17
17
  ELEMENT_READERS = %i[[] first last dig].to_set.freeze
18
18
 
19
+ # A literal Symbol or String key an `[]` / `dig` step selects, which names one value of a `HashShape`
20
+ # (issue #2363). Against a `Tuple` it selects no statically known position, so it widens every one, as a
21
+ # non-literal index does.
22
+ KeyStep = Data.define(:key)
23
+
19
24
  module_function
20
25
 
21
26
  # Widens an element read through an index / `first` / `last` / `dig` chain rooted at a LOCAL,
@@ -61,7 +66,8 @@ module Rigor
61
66
 
62
67
  # `[<local read>, [step, …]]` for a chain of element reads rooted at a local variable, or
63
68
  # `nil` when the expression is anything else. A step is an Integer position (negative counts
64
- # from the end, as Ruby's own indexing does) or `:all` when the index is not a literal.
69
+ # from the end, as Ruby's own indexing does), a {KeyStep} for a literal Symbol or String key, or `:all`
70
+ # when the index is anything else.
65
71
  #
66
72
  # Every mutation site's receiver is asked, and nearly every one is a plain variable read, so a
67
73
  # receiver that is no call answers before the step list is allocated.
@@ -104,7 +110,12 @@ module Rigor
104
110
  # `:all` stands for "some position, not statically known" — every position is widened, which
105
111
  # is the only answer that cannot under-approximate.
106
112
  def index_step(node)
107
- node.is_a?(Prism::IntegerNode) ? node.value : :all
113
+ case node
114
+ when Prism::IntegerNode then node.value
115
+ when Prism::SymbolNode then KeyStep.new(node.unescaped.to_sym)
116
+ when Prism::StringNode then KeyStep.new(node.unescaped)
117
+ else :all
118
+ end
108
119
  end
109
120
 
110
121
  # The type at `steps` inside `type`, or `nil` when the path does not resolve to a widenable
@@ -117,6 +128,10 @@ module Rigor
117
128
  tuple_positions(type, steps.first).lazy
118
129
  .filter_map { |i| element_at_path(type.elements[i], steps.drop(1)) }
119
130
  .first
131
+ when Type::HashShape
132
+ hash_shape_keys(type, steps.first).lazy
133
+ .filter_map { |key| element_at_path(type.pairs[key], steps.drop(1)) }
134
+ .first
120
135
  when Type::Union
121
136
  type.members.lazy.filter_map { |member| element_at_path(member, steps) }.first
122
137
  end
@@ -136,6 +151,7 @@ module Rigor
136
151
 
137
152
  case type
138
153
  when Type::Tuple then widen_tuple_path(type, steps, method_name, arg_types, &element_widening)
154
+ when Type::HashShape then widen_hash_shape_path(type, steps, method_name, arg_types, &element_widening)
139
155
  when Type::Union then widen_union_path(type, steps, method_name, arg_types, &element_widening)
140
156
  end
141
157
  end
@@ -154,6 +170,25 @@ module Rigor
154
170
  widened_any ? Type::Tuple.new(elements) : nil
155
171
  end
156
172
 
173
+ # A `HashShape` value reached by key: `h = { k: Set["a"] }; h[:k].add(1)` mutates the Set the shape's `:k`
174
+ # value carries, which widens in place while the keys and their policy survive, as a `Tuple`'s arity does.
175
+ def widen_hash_shape_path(shape, steps, method_name, arg_types, &)
176
+ pairs = shape.pairs.dup
177
+ rest = steps.drop(1)
178
+ widened_any = false
179
+ hash_shape_keys(shape, steps.first).each do |key|
180
+ inner = widen_through_path(pairs[key], rest, method_name, arg_types, &)
181
+ next if inner.nil?
182
+
183
+ pairs[key] = inner
184
+ widened_any = true
185
+ end
186
+ return nil unless widened_any
187
+
188
+ Type::HashShape.new(pairs, required_keys: shape.required_keys, optional_keys: shape.optional_keys,
189
+ read_only_keys: shape.read_only_keys, extra_keys: shape.extra_keys)
190
+ end
191
+
157
192
  def widen_union_path(union, steps, method_name, arg_types, &)
158
193
  widened_any = false
159
194
  members = union.members.map do |member|
@@ -170,11 +205,22 @@ module Rigor
170
205
  # position of THIS tuple, so no widening is justified.
171
206
  def tuple_positions(tuple, step)
172
207
  size = tuple.elements.size
173
- return (0...size).to_a if step == :all
208
+ return (0...size).to_a if step == :all || step.is_a?(KeyStep)
174
209
 
175
210
  position = step.negative? ? step + size : step
176
211
  position.between?(0, size - 1) ? [position] : []
177
212
  end
213
+
214
+ # The keys of `shape` one step selects: its literal key when the shape has it, every key for a step that is
215
+ # not statically known. An Integer step is a position (`first` / `last`, or an `[]` that would read a
216
+ # `Tuple`), which names no key here, so it selects nothing and the shape keeps today's reading.
217
+ def hash_shape_keys(shape, step)
218
+ case step
219
+ when :all then shape.pairs.keys
220
+ when KeyStep then shape.pairs.key?(step.key) ? [step.key] : []
221
+ else []
222
+ end
223
+ end
178
224
  end
179
225
  end
180
226
  end