rigortype 0.4.6 → 0.4.8

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 (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -235,7 +235,7 @@ module Rigor
235
235
  # A subset of a multi-collector run that its nodes keep asking for, built the first time and reused:
236
236
  # each collector alone (a pair in which one declines), the run without each collector (a larger run in
237
237
  # which one declines), and the run split by `factory_block` and `lexical_prefix` variant where it mixes
238
- # them. `key` is a Symbol for a split, or `[kind, collector]` for the other two.
238
+ # them. `kind` names the table; `key` is the collector, or the rule's Symbol for a split.
239
239
  def subset(kind, collector = nil)
240
240
  table = (@subsets[kind] ||= {}.compare_by_identity)
241
241
  key = collector || kind
@@ -5,3 +5,7 @@
5
5
  # the indexer, which loads the walk. `ScopeIndexer` itself requires the traversal file rather than this one, which
6
6
  # keeps the two free of a require cycle.
7
7
  require_relative "scope_indexer"
8
+
9
+ # {Rigor::Reflection.class_declarations_in}'s collectors: the plugin-facing walk lives with `Reflection` but needs the
10
+ # walk above, so it is loaded here rather than by `reflection/declarations`, which the engine-free paths load.
11
+ require_relative "../reflection/declaration_collectors"
@@ -5,6 +5,7 @@ require_relative "../type"
5
5
  require_relative "../source/node_walker"
6
6
  require_relative "mutation_rejoin"
7
7
  require_relative "receiver_alias"
8
+ require_relative "set_mutation"
8
9
 
9
10
  module Rigor
10
11
  module Inference
@@ -54,11 +55,31 @@ module Rigor
54
55
  # and so does `it` / `_1` wherever it is read, and an accumulator parameter (`inject([]) { |acc, e| acc << e }`):
55
56
  # the rule is wider than "an element", which only loses precision. It does not follow an alias (`y = x`) or
56
57
  # an element passed to a call (`add(x)`), which the straight-line widening does not either.
57
- def element_mutation?(body, names)
58
+ #
59
+ # A Set-only mutator (`add`, `merge`, `subtract`, … — {SetMutation::MUTATORS} less `SHAPE_MUTATORS`) counts
60
+ # only when `set_held` is given and answers true (issue #2363). The names are read without their receiver,
61
+ # and `merge` on a Hash element returns a new Hash, so the caller answers from the collection's type whether
62
+ # an element could be a Set at all ({holds_set?}). The block runs at the first such site, at most once.
63
+ def element_mutation?(body, names, &set_held)
58
64
  return false if body.nil?
59
65
 
60
66
  names |= nested_parameter_names(body)
61
- Source::NodeWalker.each(body).any? { |node| element_mutation_site?(node, names) }
67
+ Source::NodeWalker.each(body).any? { |node| element_mutation_site?(node, names, set_held) }
68
+ end
69
+
70
+ # True when `type` holds a `Set` at some depth: as itself, a union member, a `Tuple` element, a `HashShape`
71
+ # value or a type argument. A collection whose type holds none has no element a Set-only mutator can be
72
+ # mutating, so a `merge` on one of its elements is `Hash#merge`, which returns a new Hash.
73
+ def holds_set?(type)
74
+ case type
75
+ when Type::Constant then SetMutation.constant?(type)
76
+ when Type::Nominal then type.class_name == "Set" || type.type_args.any? { |arg| holds_set?(arg) }
77
+ when Type::Tuple then type.elements.any? { |element| holds_set?(element) }
78
+ when Type::HashShape then type.pairs.each_value.any? { |value| holds_set?(value) }
79
+ when Type::Union then type.members.any? { |member| holds_set?(member) }
80
+ when Type::Difference then holds_set?(type.base)
81
+ else false
82
+ end
62
83
  end
63
84
 
64
85
  # The names `parameters` (a block's parameter list, or a `for` index) binds.
@@ -80,9 +101,12 @@ module Rigor
80
101
  end
81
102
  end
82
103
 
83
- def element_mutation_site?(node, names)
104
+ def element_mutation_site?(node, names, set_held)
84
105
  if node.is_a?(Prism::CallNode)
85
- MutationWidening::SHAPE_MUTATORS.include?(node.name) && element_rooted?(node.receiver, names)
106
+ return false unless element_rooted?(node.receiver, names)
107
+
108
+ MutationWidening::SHAPE_MUTATORS.include?(node.name) ||
109
+ (!set_held.nil? && SetMutation::MUTATORS.include?(node.name) && set_held.call)
86
110
  else
87
111
  ELEMENT_WRITE_NODES.include?(node.class) && element_rooted?(node.receiver, names)
88
112
  end
@@ -101,15 +125,23 @@ module Rigor
101
125
  # A block call hands each of its receiver's elements to the block's parameters, and a body that mutates one in
102
126
  # place (`a.each { |x| x << 2 }`) changes the shape the receiver's binding still carries one level down
103
127
  # (`[[1]]`). The receiver's variables drop their shapes at every depth ({widen}), as a `for` loop's
104
- # value does ({StatementEvaluator#for_collection_after}).
128
+ # value does ({StatementEvaluator#for_collection_after}). A receiver that is a call exposing a variable's
129
+ # elements (`h.values`, `a.dup`, `a.each_with_index`) names that variable ({ReceiverAlias.element_sources}).
105
130
  def widen_element_aliases(receiver, body, names, scope)
106
- return scope unless element_mutation?(body, names)
131
+ return scope unless element_mutation?(body, names) { receiver_holds_set?(receiver, scope) }
107
132
 
108
133
  widen_receiver(receiver, scope)
109
134
  end
110
135
 
136
+ def receiver_holds_set?(receiver, scope)
137
+ ReceiverAlias.element_sources(receiver).any? do |read|
138
+ getter, = MutationWidening::ALIAS_ACCESSORS[read.class]
139
+ getter && holds_set?(scope.public_send(getter, ReceiverAlias.read_name(read)))
140
+ end
141
+ end
142
+
111
143
  def widen_receiver(receiver, scope)
112
- ReceiverAlias.candidates(receiver).reduce(scope) { |acc, read| widen_read(read, acc) }
144
+ ReceiverAlias.element_sources(receiver).reduce(scope) { |acc, read| widen_read(read, acc) }
113
145
  end
114
146
 
115
147
  # `a.each(&:strip!)`: a block argument naming a mutator mutates every element it is handed.
@@ -8,7 +8,7 @@ module Rigor
8
8
  # file's methods: the file was never parsed this run, so there is no live node to store. A handle records
9
9
  # the four things needed to resolve, fingerprint, or scope it without the node:
10
10
  #
11
- # - `path` + `node_id` — the resolution key. The per-run parse memo ({Scope#resolve_def_handle}) parses the
11
+ # - `path` + `node_id` — the resolution key. The per-run parse memo ({DefNodeResolver.resolve}) parses the
12
12
  # file once and returns the `Prism::DefNode` whose `node_id` matches (Prism `node_id` is stable across
13
13
  # repeated parses of identical bytes). `name` is the cross-check: a resolved node whose `name` differs
14
14
  # from the handle's forces a fresh walk (bundle/source skew that the digest gate should make impossible —
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "prism"
4
4
  require_relative "def_handle"
5
+ require_relative "../typing_state"
5
6
  require_relative "../source/node_children"
6
7
 
7
8
  module Rigor
@@ -20,23 +21,35 @@ module Rigor
20
21
  # file, and finally nil — conservative (the accessor then reports no user def, losing precision but never
21
22
  # firing a wrong diagnostic), never silent.
22
23
  #
23
- # The memo is a thread-local installed for the duration of a run (the {Cache::FileDigest} pattern:
24
- # process-local, Ractor-safe, never crosses a fork boundary, reset per run). Outside a run scope every
24
+ # The memo is a {TypingState} Frame installed for the duration of a run ({.with_run}) and restored on the way
25
+ # out: per thread, and a fork child inherits whatever is installed at the fork. Outside a run scope every
25
26
  # resolution parses directly — a correct if un-memoised fallback for runner-less probes.
26
27
  module DefNodeResolver
27
- MEMO_KEY = :rigor_def_node_resolver_memo
28
- private_constant :MEMO_KEY
28
+ # ADR-129 WD1 — the run's memo is a Frame: {.with_run} installs it for the run and restores the previous one, and
29
+ # whether one is installed changes answers (outside a run every resolution parses afresh and no `Module.nesting`
30
+ # is recorded). The installed hash is frozen; the four tables in it are extensional caches its owner fills in
31
+ # place without a touch (Memos by kind, whose entry is fixed once written): `nodes` is keyed on
32
+ # `(path, node_id)`, `indexes` and `refinement_queries` on the file's path, and `nestings` on the identity of
33
+ # the node this module minted, holding the nesting the handle's bundle row (or the file's `refine` bodies)
34
+ # recorded for it.
35
+ RUN_MEMO = TypingState.frame(
36
+ :def_node_resolver_run,
37
+ memos: %i[nodes indexes nestings refinement_queries],
38
+ reason: "installed per run by with_run; its tables are extensional caches, keyed on (path, node_id), on the " \
39
+ "path, and (nestings) on the minted node's identity, filled in place",
40
+ # G1′ witness: an incremental recheck types a file against handles of the unchanged files, and
41
+ # {.resolve} reads the run's tables from inside `type_of` (#2874).
42
+ witness: "spec/fixtures/typing_state_witnesses/def_node_resolver_run.rb"
43
+ )
44
+ private_constant :RUN_MEMO
29
45
 
30
46
  # Installs a fresh per-run resolution memo, restoring the previous one on exit (always, even on a raise).
31
47
  # `nodes` caches the resolved node per `(path, node_id)`; `indexes` caches each file's parse index;
32
48
  # `nestings` re-attaches each handle's recorded `Module.nesting` to the node this module minted for it
33
49
  # ({.rehydrated_nesting}).
34
- def self.with_run
35
- previous = Thread.current[MEMO_KEY]
36
- Thread.current[MEMO_KEY] = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity, refinement_queries: {} }
37
- yield
38
- ensure
39
- Thread.current[MEMO_KEY] = previous
50
+ def self.with_run(&)
51
+ memo = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity, refinement_queries: {} }.freeze
52
+ TypingState.current.with(RUN_MEMO, memo, &)
40
53
  end
41
54
 
42
55
  # Whether a per-run memo scope is in force. The rehydrated `Module.nesting` ({.rehydrated_nesting}) is
@@ -44,7 +57,12 @@ module Rigor
44
57
  # per-run memo scope" clause states — and lets a spec assert that no handle-consuming entry point
45
58
  # resolves outside it, which is the difference between the peel fallback being a documented edge and
46
59
  # being silently reachable from production.
47
- def self.run_scope? = !Thread.current[MEMO_KEY].nil?
60
+ def self.run_scope? = !TypingState.current[RUN_MEMO].nil?
61
+
62
+ # Issue #3160 — the nodes this run's memo has minted so far, keyed `[path, node_id]` (nil outside a run scope).
63
+ # The return memo keys on them, so the runner keeps the table to find an unchanged file's def in the memo once
64
+ # the run scope has closed.
65
+ def self.resolved_nodes = TypingState.current[RUN_MEMO]&.fetch(:nodes)
48
66
 
49
67
  # Resolves `handle` to a `Prism::DefNode` (the same object across the run for a given (path, node_id)), or
50
68
  # nil. A non-handle argument is returned unchanged, so callers can pass a table value that is either a live
@@ -52,7 +70,7 @@ module Rigor
52
70
  def self.resolve(handle)
53
71
  return handle unless handle.is_a?(DefHandle)
54
72
 
55
- memo = Thread.current[MEMO_KEY]
73
+ memo = TypingState.current[RUN_MEMO]
56
74
  return locate(handle, {}) if memo.nil?
57
75
 
58
76
  key = [handle.path, handle.node_id].freeze
@@ -96,7 +114,7 @@ module Rigor
96
114
  # def_node_resolver_spec.rb` pins that, so a new one added outside a run scope fails there rather than
97
115
  # silently reverting to the peel.
98
116
  def self.rehydrated_nesting(node)
99
- memo = Thread.current[MEMO_KEY]
117
+ memo = TypingState.current[RUN_MEMO]
100
118
  memo && memo[:nestings][node]
101
119
  end
102
120
 
@@ -104,7 +122,7 @@ module Rigor
104
122
  # is recorded like any other: it is the bundle saying the def is written at the top level, and dropping
105
123
  # it would leave the warm path peeling the caller's namespace where a cold run resolves at the top
106
124
  # level. Only `nil` — a bundle that recorded nothing for the def — is skipped, and
107
- # `Cache::IncrementalSnapshot::SCHEMA` 17 is what keeps a pre-#716 blob (where `nil` MEANT top level)
125
+ # the `SCHEMA` bump to 17 (`Cache::IncrementalSnapshot`) keeps a pre-#716 blob, where `nil` MEANT top level,
108
126
  # from being read as one.
109
127
  def self.record_nesting(memo, node, nesting)
110
128
  return if node.nil? || nesting.nil?
@@ -117,7 +135,7 @@ module Rigor
117
135
  # read: the typed refined arm reads another file's refine body through it. The bodies' `Module.nesting`
118
136
  # is recorded as a handle's is, so the re-typed body reads its constants where it is written.
119
137
  def self.refinement_query(path)
120
- memo = Thread.current[MEMO_KEY]
138
+ memo = TypingState.current[RUN_MEMO]
121
139
  return build_refinement_query(path, nil) if memo.nil?
122
140
 
123
141
  queries = memo[:refinement_queries]
@@ -35,7 +35,7 @@ module Rigor
35
35
  body = def_node.body
36
36
  return nil if body.nil?
37
37
 
38
- last = body_last_expression(body)
38
+ last = primary_value_expression(body)
39
39
  return nil if last.nil?
40
40
 
41
41
  inner_scope = scope_index[last] || scope_index[body] || scope_index[def_node]
@@ -46,11 +46,54 @@ module Rigor
46
46
 
47
47
  lookup_context = keep_lookup_nil ? LookupContext.new(scope_index, truthy_guards(body)) : nil
48
48
  last_type = with_lookup_nil(last_type, last, inner_scope, lookup_context) if lookup_context
49
- union_with_explicit_returns(body, last_type, scope_index, lookup_context: lookup_context)
49
+ arm_types = rescue_arm_types(body, inner_scope, scope_index, lookup_context)
50
+ return nil if arm_types.nil?
51
+
52
+ union_with_explicit_returns(body, Type::Combinator.union(last_type, *arm_types), scope_index,
53
+ lookup_context: lookup_context)
50
54
  rescue StandardError
51
55
  nil
52
56
  end
53
57
 
58
+ # Issue #2410 — the value a def body ends on when no exception escapes its primary path. A def-level
59
+ # `rescue … else … end` body is a `BeginNode` whose else-clause, when present, replaces the body's value (the
60
+ # ensure-clause's value is discarded, so it never counts).
61
+ def primary_value_expression(body)
62
+ return body_last_expression(body) unless body.is_a?(Prism::BeginNode) && body.else_clause
63
+
64
+ body_last_expression(body.else_clause.statements)
65
+ end
66
+
67
+ # Issue #2410 — the value of each arm of a def-level rescue chain, which the method returns when that arm
68
+ # completes. An empty arm returns `nil`; an arm ending in `raise`, `retry` or `return` types as bottom and so
69
+ # adds nothing here (a `return`'s value is collected with the other explicit returns). Nil when an arm cannot
70
+ # be typed: the body's value alone would be a narrower, wrong contract.
71
+ def rescue_arm_types(body, inner_scope, scope_index, lookup_context)
72
+ return [] unless body.is_a?(Prism::BeginNode)
73
+
74
+ types = []
75
+ arm = body.rescue_clause
76
+ while arm
77
+ type = rescue_arm_type(arm, inner_scope, scope_index, lookup_context)
78
+ return nil if type.nil?
79
+
80
+ types << type
81
+ arm = arm.subsequent
82
+ end
83
+ types
84
+ end
85
+
86
+ def rescue_arm_type(arm, inner_scope, scope_index, lookup_context)
87
+ value = body_last_expression(arm.statements)
88
+ return Type::Combinator.constant_of(nil) if value.nil?
89
+
90
+ scope = scope_index[value] || scope_index[arm] || inner_scope
91
+ type = safe_type_of(scope, value)
92
+ return nil if type.nil?
93
+
94
+ lookup_context ? with_lookup_nil(type, value, scope, lookup_context) : type
95
+ end
96
+
54
97
  # What {#optimistic_value?} reads beside a node's scope: the engine's per-node scopes, and for each lookup
55
98
  # under a guard the source of the guarding predicates ({#truthy_guards}).
56
99
  LookupContext = Data.define(:scope_index, :guards)
@@ -83,7 +126,7 @@ module Rigor
83
126
  end
84
127
 
85
128
  def optimistic_call?(node, scope, context)
86
- if scope.optimistic_origins[node]
129
+ if scope.optimistic_origin(node)
87
130
  return false if guarded_lookup?(node, context) || non_empty_end?(node, scope)
88
131
 
89
132
  return true
@@ -227,8 +270,7 @@ module Rigor
227
270
 
228
271
  scope = scope_index[return_node] || scope_index[args.first]
229
272
  return if scope.nil?
230
- # `return a, b` packs into a Tuple at runtime; the MVP only handles the single-value form. Multi-arg
231
- # returns contribute no type to keep the implementation focused.
273
+ # `return a, b` packs into a Tuple at runtime; only the single-value form is typed.
232
274
  return unless args.size == 1
233
275
 
234
276
  type = safe_type_of(scope, args.first)
@@ -25,8 +25,10 @@ module Rigor
25
25
  # truth-tested: {UNKNOWN} and {ABSENT} are both truthy, so any other use folds a decline into a firing arm.
26
26
  # `spec/rigor/inference/definer_resolution_case_in_spec.rb` fails on a call site of any other shape.
27
27
  #
28
- # It migrates one question at a time: `SourceArity` (C1b) and the relationship lints (C1c) read the instance
29
- # side; no production reader reads the singleton side yet (C2-a landed it for C2's typing sites).
28
+ # It migrates one question at a time: `SourceArity` (C1b, C2-c) asks for the receiver's own side, instance or
29
+ # singleton; the override lints (C1c) and the C2 typing sites (`ExpressionTyper#resolve_typing_def_with_owner`,
30
+ # `StatementEvaluator#own_definer_node`) read the instance side; `RefinedDispatch::OwnMethod.certain_owner?` and
31
+ # `CheckRules#reached_definer_visibility` pass the side they hold.
30
32
  module DefinerResolution
31
33
  Known = Data.define(:answer, :owner)
32
34
  UnknownResult = Data.define
@@ -80,11 +82,16 @@ module Rigor
80
82
  # the entry's answer, as `[owner_name, value]` (the owner is part of the answer so that two worlds naming
81
83
  # different parents disagree). It is the override lints' parent read: the nearest project ancestor after the
82
84
  # class whose block answers.
85
+ #
86
+ # Issue #3156 — a class or module the project declares but whose Ruby body records no method, superclass or
87
+ # include (`module PMod; end`, `class AMid` / `end`) is an external entry on the chain, which only its RBS
88
+ # declaration speaks for. The override block answers from RBS, so such an entry is asked too, under the
89
+ # project name it resolves to (`ResolutionChain::Entry#declared_name`); an undeclared external is not.
83
90
  def override_answer(scope)
84
91
  lambda do |chain, from|
85
- first_hit(chain, scope, from) do |entry|
86
- value = yield(entry.name)
87
- [entry.name, value] unless value.nil?
92
+ first_hit(chain, scope, from, askable_external: true) do |name|
93
+ value = yield(name)
94
+ [name, value] unless value.nil?
88
95
  end
89
96
  end
90
97
  end
@@ -329,17 +336,20 @@ module Rigor
329
336
  kind == :singleton ? [:singleton, method_name.to_sym] : method_name.to_sym
330
337
  end
331
338
 
332
- def first_hit(chain, scope, from)
339
+ # The first entry at or after `from` whose block answers. The default answer functions are handed the project
340
+ # entry; the override one (`askable_external:`) is handed a name, and also asks an external entry the project
341
+ # declares.
342
+ def first_hit(chain, scope, from, askable_external: false)
333
343
  index = from - 1
334
344
  found = nil
335
345
  chain.search(scope, from) do |entry|
336
346
  index += 1
337
- next if entry.external?
347
+ next unless (name = entry.external? ? askable_external && entry.declared_name(scope) : entry.name)
338
348
 
339
- answer = yield(entry)
349
+ answer = yield(askable_external ? name : entry)
340
350
  next if answer.nil?
341
351
 
342
- found = Hit.new(answer, entry.name, index, entry.side)
352
+ found = Hit.new(answer, name, index, entry.side)
343
353
  end
344
354
  found
345
355
  end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../typing_state"
4
+ require_relative "../memo_audit"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # The `method_name (Symbol) → [owner names]` inverted index the override gate reads
9
+ # (`ExpressionTyper#method_definers_index`), over one instance or singleton def table.
10
+ #
11
+ # Issue #2300 — a file's def table is its own `def`s merged over the cross-file seed
12
+ # (`ScopeIndexer#merge_def_node_tables`, `#merge_singleton_def_nodes`, then the #526 extend fold), a new Hash per
13
+ # file. Inverting it whole scanned every project `def` once per analysed file. {.record_layers} remembers the
14
+ # seed a final merged table was built over, and {.for} then inverts the seed ONCE per seed table and, per file,
15
+ # only the owners whose inner table is not the seed's own object ({Layered}).
16
+ #
17
+ # The two answers hold the same owners: an owner the merge left alone shares the seed's inner table, so its
18
+ # names are the seed's, and every other owner is read from the merged table itself. That holds only while the
19
+ # seed cannot change under the cached index, so a seed whose tables are not all frozen, or a merged table
20
+ # missing one of the seed's owners, is inverted whole, as is a table {.record_layers} was not told about. Only
21
+ # the order of a name's owners can differ, and the gate asks `any?` of the list.
22
+ module DefinersIndex
23
+ # These thread slots are bounded, like `ExpressionTyper#method_definers_index`'s two ways. Each is a fixed
24
+ # number of entries, replaced rather than accumulated: two `[merged, seed]` pairs (the last file's two def
25
+ # tables) and two seed indexes. The run that filled them releases them when it ends (run-scoped, #2399),
26
+ # so a finished run keeps none of its seed tables or their indexes alive.
27
+ #
28
+ # ADR-129 WD1 — all three are run-scoped Memos keyed on table identity. The layers say only which seed a merged
29
+ # table was built over, a shortcut: {.for} answers the same index owners, `Layered` or whole, so the answer is
30
+ # a function of the table alone. Each seed index is a pure function of its seed table.
31
+ #
32
+ # Each hit goes through `MemoAudit.check`: a `Layered` answer is compared, in {.canonical} form, with the table
33
+ # inverted whole, and a seed index with the seed inverted again. While an audit recomputation runs
34
+ # ({MemoAudit.suspended?}) the three are neither read nor written, so it inverts every table whole.
35
+ LAYERS = TypingState.memo(
36
+ :definers_index_layers,
37
+ run_scoped: true, audit: "definers index layers",
38
+ reason: "the seed each of the last two merged tables was built over; keyed on table identity"
39
+ )
40
+ SEED_INDEX = TypingState.memo(
41
+ :definers_index_seed,
42
+ run_scoped: true, audit: "definers index seed",
43
+ reason: "pure function of the seed table it keys on"
44
+ )
45
+ SEED_INDEX_ALT = TypingState.memo(
46
+ :definers_index_seed_alt,
47
+ run_scoped: true, audit: "definers index seed alt",
48
+ reason: "pure function of the seed table it keys on"
49
+ )
50
+ private_constant :LAYERS, :SEED_INDEX, :SEED_INDEX_ALT
51
+
52
+ # A name's owners: the seed's, less the owners the file replaced, plus the replaced owners' own.
53
+ class Layered
54
+ def initialize(seed_index, replaced, replaced_index)
55
+ @seed_index = seed_index
56
+ @replaced = replaced
57
+ @replaced_index = replaced_index
58
+ @answers = {}
59
+ end
60
+
61
+ def [](method_name)
62
+ return @answers[method_name] if @answers.key?(method_name)
63
+
64
+ from_seed = @seed_index[method_name]
65
+ from_seed = from_seed.reject { |owner| @replaced.key?(owner) } if from_seed && !@replaced.empty?
66
+ from_file = @replaced_index[method_name]
67
+ owners = from_file.nil? ? from_seed : (from_seed || []) + from_file
68
+ @answers[method_name] = owners.nil? || owners.empty? ? nil : owners
69
+ end
70
+
71
+ # Every name some owner may define, for {DefinersIndex.canonical}.
72
+ def names = @seed_index.keys | @replaced_index.keys
73
+ end
74
+
75
+ module_function
76
+
77
+ # Records that the final `merged` table was built over `seed`. The last two are kept (a file merges an
78
+ # instance and a singleton table), replaced rather than accumulated.
79
+ def record_layers(merged, seed)
80
+ return if MemoAudit.enabled? && MemoAudit.suspended?
81
+
82
+ state = TypingState.current
83
+ previous = state[LAYERS]
84
+ state[LAYERS] = [[merged, seed], previous&.first].compact
85
+ end
86
+
87
+ # The definers index over `table`.
88
+ def for(table)
89
+ return build(table) if MemoAudit.suspended?
90
+
91
+ seed = TypingState.current[LAYERS]&.find { |entry| entry[0].equal?(table) }&.last
92
+ return build(table) if seed.nil? || !deeply_frozen?(seed)
93
+
94
+ replaced = {}
95
+ kept = 0
96
+ table.each do |owner, methods|
97
+ if methods.equal?(seed[owner])
98
+ kept += 1
99
+ else
100
+ replaced[owner] = methods
101
+ end
102
+ end
103
+ return build(table) unless kept + replaced.count { |owner, _| seed.key?(owner) } == seed.size
104
+
105
+ audit_layered(Layered.new(seed_index(seed), replaced, build(replaced)), table)
106
+ end
107
+
108
+ # `index`, after checking it against `table` inverted whole when the audit is on.
109
+ def audit_layered(index, table)
110
+ return index unless MemoAudit.enabled?
111
+
112
+ MemoAudit.check("definers index layers", canonical(index), table.size) { canonical(build(table)) }
113
+ index
114
+ end
115
+
116
+ # `index` as a plain `name → sorted owners` Hash, so a `Layered` index and a whole one compare equal when they
117
+ # hold the same owners. Audit-only: it reads every name.
118
+ def canonical(index)
119
+ names = index.is_a?(Layered) ? index.names : index.keys
120
+ names.each_with_object({}) do |name, out|
121
+ owners = index[name]
122
+ out[name] = owners.sort if owners
123
+ end
124
+ end
125
+
126
+ def deeply_frozen?(seed)
127
+ seed.frozen? && seed.each_value.all?(&:frozen?)
128
+ end
129
+
130
+ # The seed's index, built once per seed table. Two ways, an instance and a singleton seed table.
131
+ def seed_index(seed)
132
+ state = TypingState.current
133
+ slot = state[SEED_INDEX]
134
+ if slot && slot[0].equal?(seed)
135
+ return MemoAudit.check("definers index seed", slot[1], seed.size) do
136
+ build(seed)
137
+ end
138
+ end
139
+
140
+ slot = state[SEED_INDEX_ALT]
141
+ if slot && slot[0].equal?(seed)
142
+ return MemoAudit.check("definers index seed alt", slot[1], seed.size) { build(seed) }
143
+ end
144
+
145
+ index = MemoAudit.key_audit("definers index seed") { build(seed) }
146
+ state[SEED_INDEX_ALT] = state[SEED_INDEX]
147
+ state[SEED_INDEX] = [seed, index]
148
+ index
149
+ end
150
+
151
+ def build(table)
152
+ index = {}
153
+ table.each do |class_name, methods|
154
+ next if class_name == ScopeIndexer::TOP_LEVEL_DEF_KEY
155
+
156
+ methods.each_key { |method_name| (index[method_name] ||= []) << class_name }
157
+ end
158
+ index
159
+ end
160
+ end
161
+ end
162
+ end