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
@@ -9,9 +9,10 @@ module Rigor
9
9
  #
10
10
  # The mapping from Rails column types to Ruby class names is deliberately conservative —
11
11
  # `:string`/`:text` → `String`, `:integer`/`:bigint` → `Integer`, `:boolean` → `bool`,
12
- # `:datetime`/`:timestamp` → `Time`, `:date` → `Date`, `:decimal`/`:float` → `Float`. Exotic types
13
- # (json, jsonb, ltree, hstore, custom) fall back to `Object` so the plugin stays silent rather than
14
- # guessing.
12
+ # `:datetime`/`:timestamp` → `Time`, `:date` → `Date`, `:float` → `Float`. A `:decimal` column's class
13
+ # follows its scale, so it is typed by {.decimal_ruby_type} from the parser that reads the scale, and
14
+ # reads `Object` here. Exotic types (json, jsonb, ltree, hstore, custom) fall back to `Object` so the
15
+ # plugin stays silent rather than guessing.
15
16
  class SchemaTable
16
17
  Column = Struct.new(:name, :type, :ruby_type, :array, keyword_init: true) do
17
18
  def to_h = { name: name, type: type, ruby_type: ruby_type, array: array }
@@ -21,14 +22,16 @@ module Rigor
21
22
  end
22
23
  end
23
24
 
24
- # Map ActiveRecord column types → Ruby class names.
25
25
  RUBY_TYPE_MAPPING = {
26
26
  string: "String",
27
27
  text: "String",
28
28
  integer: "Integer",
29
29
  bigint: "Integer",
30
+ # PostgreSQL's dumper writes `id: :serial` / `t.serial` for a sequence-backed integer column.
31
+ serial: "Integer",
32
+ bigserial: "Integer",
30
33
  float: "Float",
31
- decimal: "Float",
34
+ decimal: "Object",
32
35
  boolean: "bool",
33
36
  datetime: "Time",
34
37
  timestamp: "Time",
@@ -46,7 +49,7 @@ module Rigor
46
49
  }.freeze
47
50
 
48
51
  # Implicit columns that every Rails table has unless the schema explicitly opts out. The plugin
49
- # assumes these exist; users who run `create_table id: false` get no implicit `id` column from
52
+ # assumes these exist; users who run `create_table id: false` or `id: nil` get no implicit `id` column from
50
53
  # the parser, but most apps never disable it.
51
54
  IMPLICIT_COLUMNS = [
52
55
  Column.new(name: "id", type: :integer, ruby_type: "Integer").freeze
@@ -84,6 +87,17 @@ module Rigor
84
87
  def self.ruby_type_for(column_type)
85
88
  RUBY_TYPE_MAPPING.fetch(column_type.to_sym, "Object")
86
89
  end
90
+
91
+ # Maps a `decimal` column's scale to its Ruby class. Rails registers `Type::DecimalWithoutScale`
92
+ # (an Integer) at scale 0 and `Type::Decimal` (a BigDecimal) at any other scale. An absent or
93
+ # non-literal scale (`nil`) is left to the adapter, which decides it, so it reads as `Object`.
94
+ def self.decimal_ruby_type(scale)
95
+ case scale
96
+ when 0 then "Integer"
97
+ when Integer then "BigDecimal"
98
+ else "Object"
99
+ end
100
+ end
87
101
  end
88
102
  end
89
103
  end
@@ -138,21 +138,23 @@ module Rigor
138
138
  end
139
139
 
140
140
  def build_column(name, rest)
141
- type_str, array = extract_type(rest)
141
+ type_str, array, scale = extract_type(rest)
142
142
  return nil if type_str.empty?
143
143
 
144
144
  rails_type = PG_TYPE_TO_RAILS.fetch(type_str, type_str.to_sym)
145
+ ruby_type = rails_type == :decimal ? SchemaTable.decimal_ruby_type(scale) : SchemaTable.ruby_type_for(rails_type)
145
146
  SchemaTable::Column.new(
146
147
  name: name,
147
148
  type: rails_type,
148
- ruby_type: SchemaTable.ruby_type_for(rails_type),
149
+ ruby_type: ruby_type,
149
150
  array: array
150
151
  )
151
152
  end
152
153
 
153
154
  # The type is every token before the first uppercase-initial token (a column modifier such as
154
155
  # `NOT`, `NULL`, `DEFAULT`, `GENERATED`, `COLLATE`). A trailing `[]` marks a Postgres array column;
155
- # a `(length[,scale])` specifier is dropped. Returns `[normalized_type, array?]`.
156
+ # a `(length[,scale])` specifier is dropped. Returns `[normalized_type, array?, scale]`, where `scale` is
157
+ # the literal second number of a `(precision, scale)` specifier, or nil when there is none.
156
158
  def extract_type(rest)
157
159
  type_tokens = []
158
160
  rest.split(/\s+/).each do |token|
@@ -162,8 +164,9 @@ module Rigor
162
164
  end
163
165
  type_str = type_tokens.join(" ")
164
166
  array = type_str.include?("[]")
167
+ scale = type_str[/\(\s*\d+\s*,\s*(\d+)\s*\)/, 1]&.to_i
165
168
  type_str = type_str.gsub("[]", "").gsub(/\([0-9,\s]*\)/, "").strip.downcase
166
- [type_str, array]
169
+ [type_str, array, scale]
167
170
  end
168
171
  end
169
172
  end
@@ -59,12 +59,20 @@ module Rigor
59
59
  # capital R) is intentional — keeps the constant lookup distinct from `::ActiveRecord` even though the
60
60
  # gem name is hyphenated.
61
61
  #
62
- # Note: this plugin is the seventh worked example. It does NOT require `active_record` at runtime — it
63
- # only reads project source, the same way the other examples do. Rigor stays decoupled from Rails.
62
+ # Note: this plugin does NOT require `active_record` at runtime — it only reads project source, the
63
+ # same way the other examples do. Rigor stays decoupled from Rails.
64
64
  class Activerecord < Rigor::Plugin::Base
65
65
  manifest(
66
66
  id: "activerecord",
67
67
  target_gems: ["activerecord"],
68
+ # 0.16.0, 2026-11-13 (#2956) — the `id` column takes the primary key's type (`id: :uuid` reads `String`)
69
+ # instead of always `Integer`, and `serial` / `bigserial` columns read `Integer`. The schema table
70
+ # payload changes, so the bump follows the 0.15.0 entry.
71
+ #
72
+ # 0.15.0, 2026-11-07 (#2945) — `create_table`'s `primary_key: "name"` registers that column, so a query
73
+ # on it, or on an `alias_attribute` targeting it, no longer reports `unknown-column`. The schema table
74
+ # payload changes, and the bump (part of the producer cache KEY) stops a warm run serving the old one.
75
+ #
68
76
  # 0.14.0, 2026-10-10 (#1721) — the plugin publishes `:reachability_roots`: the models ActiveRecord loads
69
77
  # by name from an association. The association rows gain `macro`, `class_name_option`, `through` and
70
78
  # `source_type_option`, so a cached 0.13.0 payload lacks the fields the roots are derived from and
@@ -124,7 +132,7 @@ module Rigor
124
132
  # a scope lambda body / class-method body now contributes `Relation[Model]` via `scope.self_type`
125
133
  # instead of falling through to `Kernel#select` (the IO multiplexer, `Array[String]` return). Plus
126
134
  # `:select` added to the relation-entry-point list.
127
- version: "0.14.0",
135
+ version: "0.16.0",
128
136
  description: "Types ActiveRecord finders against the project's db/schema.rb and AR models.",
129
137
  config_schema: {
130
138
  "schema_file" => { kind: :string, default: "db/schema.rb" },
@@ -491,8 +499,8 @@ module Rigor
491
499
  # The project's OWN definition wins, and is not merely a precision preference: `def name` on the
492
500
  # model — or on an ancestor it declares — is an override Ruby dispatches to, and the plugin tier sits
493
501
  # ABOVE the engine's body-inference tiers, so answering here would displace the override's real return
494
- # type with `Dynamic`. `discovered_method_through_ancestors?` is the same table the veto's own
495
- # discovery arm reads, and it fails toward declining (a budget-exhausted walk answers true).
502
+ # type with `Dynamic`. `Reflection.project_method_defined(source: :discovered)` is the same table the veto's own
503
+ # discovery arm reads, and it fails toward declining (a budget-exhausted walk answers `maybe`).
496
504
  def implicit_self_instance_member_type(call_node, scope, index)
497
505
  return nil if scope.nil?
498
506
  return nil unless call_node.arguments.nil?
@@ -504,7 +512,9 @@ module Rigor
504
512
  entry = index.find(self_type.class_name)
505
513
  return nil if entry.nil?
506
514
  return nil unless instance_member_name?(entry, call_node.name)
507
- return nil if scope.discovered_method_through_ancestors?(self_type.class_name, call_node.name, :instance)
515
+ return nil unless Rigor::Reflection.project_method_defined(
516
+ self_type.class_name, call_node.name, kind: :instance, source: :discovered, scope: scope
517
+ ).no?
508
518
  return nil if rbs_answers_before_object?(self_type.class_name, call_node.name, scope)
509
519
 
510
520
  Rigor::Type::Combinator.untyped
@@ -552,9 +562,22 @@ module Rigor
552
562
  !entry.column(column_name).nil?
553
563
  end
554
564
 
565
+ # A project `self.find` on the model or an ancestor, or a walk that ran out of budget before ruling one out
566
+ # (`maybe`): the veto fails toward declining, as the discovery walk always has.
567
+ def project_find_defined?(entry, scope)
568
+ project_singleton_defined?(entry, :find, scope)
569
+ end
570
+
571
+ # The same veto for any class-side selector the plugin answers in Rails' name (`find`, `select`).
572
+ def project_singleton_defined?(entry, name, scope)
573
+ !Rigor::Reflection.project_method_defined(entry.class_name, name, kind: :singleton, source: :discovered,
574
+ scope: scope).no?
575
+ end
576
+
555
577
  # Class-side finders + the class-side relation entry points. `find` returns the model, or an Array of
556
- # them for two or more ids, or with a block the model or `nil`; `find_by!` returns the model; `find_by` adds the `nil` arm; `where` /
557
- # `all` / `order` / `limit` / `none` open a relation. The relation then carries its element type
578
+ # them for two or more ids, or with a block the model or `nil`; `find_by!` returns the model;
579
+ # `find_by` adds the `nil` arm; `where` / `all` / `order` / `limit` / `none` open a relation. The
580
+ # relation then carries its element type
558
581
  # through any further chained query method via the bundled `ActiveRecord::Relation` RBS.
559
582
  def finder_return_type(call_node, entry, scope)
560
583
  case call_node.name
@@ -571,7 +594,7 @@ module Rigor
571
594
 
572
595
  # A model that defines its own `self.find` (`find(owner, id)`) takes two arguments for its own
573
596
  # reasons, so the Array reading of Rails' `find` is withheld and the project's method answers.
574
- return nil if scope.discovered_method_through_ancestors?(entry.class_name, :find, :singleton)
597
+ return nil if project_find_defined?(entry, scope)
575
598
 
576
599
  Rigor::Type::Combinator.nominal_of("Array", type_args: [model])
577
600
  when :find_by!
@@ -583,13 +606,10 @@ module Rigor
583
606
  Rigor::Type::Combinator.nominal_of(entry.class_name),
584
607
  Rigor::Type::Combinator.constant_of(nil)
585
608
  )
586
- when :where, :all, :order, :limit, :none, :select
587
- # `:select` was added to close Mastodon's
588
- # `scope :duplicate_uris, -> { select(:uri).group(:uri).having(...) }` shape: the implicit-self
589
- # `select(:uri)` inside the scope lambda body had been resolving to `Kernel#select` (IO
590
- # multiplexer, return `Array[String]`), masking the AR class-side relation entry point. The rest
591
- # of the query DSL chains through the bundled `ActiveRecord::Relation` RBS once a relation is
592
- # open.
609
+ when :select then class_select_return_type(call_node, entry, scope)
610
+ when :where, :all, :order, :limit, :none
611
+ # The rest of the query DSL chains through the bundled `ActiveRecord::Relation` RBS once a
612
+ # relation is open.
593
613
  relation_of(entry.class_name)
594
614
  when :table_name
595
615
  table_name_return_type(entry)
@@ -600,6 +620,23 @@ module Rigor
600
620
  end
601
621
  end
602
622
 
623
+ # `:select` was added to close Mastodon's `scope :duplicate_uris, -> { select(:uri).group(:uri).having(...) }`
624
+ # shape: the implicit-self `select(:uri)` inside the scope lambda body had been resolving to `Kernel#select`
625
+ # (IO multiplexer, return `Array[String]`), masking the AR class-side relation entry point.
626
+ #
627
+ # `Model.select` is `all.select`, so a block makes it `Enumerable#select` over the loaded records, as the
628
+ # bundled `Relation#select` overload says (#1408), and the answer is an Array of the model. Fields next to a
629
+ # block answer the Array too, for the reason given on that overload. A model that defines its own
630
+ # `self.select` keeps its own answer for the block forms, as it does for `find`'s block and multi-id
631
+ # forms (`project_find_defined?`); the builder form stays the relation, as `find`'s one-id form stays the
632
+ # model.
633
+ def class_select_return_type(call_node, entry, scope)
634
+ return relation_of(entry.class_name) if call_node.block.nil?
635
+ return nil if project_singleton_defined?(entry, :select, scope)
636
+
637
+ Rigor::Type::Combinator.nominal_of("Array", type_args: [Rigor::Type::Combinator.nominal_of(entry.class_name)])
638
+ end
639
+
603
640
  # `Model.find { |m| … }` / `Model.find(&:pred?)` never reaches the id lookup: `Core.find` passes a
604
641
  # block to `super` (`all.find`), whose `Relation#find` does `return super if block_given?`, landing on
605
642
  # `Enumerable#find` over the loaded records. So the answer is the element or `nil`, exactly what the
@@ -614,7 +651,7 @@ module Rigor
614
651
  # `Relation#find` overload does. A project `self.find` keeps its own answer, as it does for the
615
652
  # multi-id form.
616
653
  def block_find_return_type(call_node, entry, scope)
617
- return nil if scope.discovered_method_through_ancestors?(entry.class_name, :find, :singleton)
654
+ return nil if project_find_defined?(entry, scope)
618
655
  return Rigor::Type::Combinator.untyped unless call_argument_count(call_node).zero?
619
656
 
620
657
  Rigor::Type::Combinator.union(
@@ -231,12 +231,26 @@ module ActiveRecord
231
231
  def left_outer_joins: (*untyped) -> self
232
232
  %a{pure}
233
233
  def references: (*untyped) -> self
234
- # With a block, `select` is Enumerable's and returns an Array. It
235
- # stays `self`: on this open receiver a method the file does not
236
- # declare is not reported, while on `Array[Elem]` any ActiveSupport
237
- # method the bundled overlay leaves out would be.
234
+ # With a block, `select` is `Enumerable#select` over the loaded
235
+ # records and returns an Array (`return super()` when a block is
236
+ # given, `relation/query_methods.rb:413` in activerecord 8.1.3.1).
237
+ # Fields and a block together raise `ArgumentError` there; Rails 4
238
+ # ignored the fields and returned the Array, so that form answers
239
+ # the Array too, which holds on either version. Without a block it
240
+ # is the query builder.
241
+ #
242
+ # `Array[Elem]` is a closed receiver, unlike this one: an
243
+ # ActiveSupport Array method on the result (`pluck`, `second`,
244
+ # `to_sentence`, `blank?`) resolves only with the
245
+ # `rigor-activesupport-core-ext` plugin enabled, and reports
246
+ # `call.undefined-method` without it, as it does after `to_a`.
247
+ #
248
+ # The `%a{pure}` is the builder form's. The block form's
249
+ # `io.db.read` comes from the plugin's `RELATION_MATERIALIZERS` row,
250
+ # which labels both forms today (#1318).
238
251
  %a{pure}
239
- def select: (*untyped) -> self
252
+ def select: (*untyped) { (Elem) -> boolish } -> Array[Elem]
253
+ | (*untyped) -> self
240
254
  %a{pure}
241
255
  def reselect: (*untyped) -> self
242
256
  %a{pure}
@@ -105,10 +105,9 @@ module Rigor
105
105
  walk(node.body, inner_path, &) if node.body
106
106
  end
107
107
 
108
- # The full constant name a `class` / `module` declaration defines, given the rendered local name
109
- # and the enclosing lexical path. A ROOTED local name (`class ::User`) names the top-level constant
110
- # whatever the nesting, so the lexical path is dropped and the `::` with it; otherwise the name is
111
- # appended to the path (`class User` inside `module Admin` → `Admin::User`).
108
+ # The full constant name a `class` / `module` declaration defines. A ROOTED local name (`class ::User`)
109
+ # names the top-level constant whatever the nesting, so the lexical path is dropped; otherwise the
110
+ # name is appended to it (`class User` inside `module Admin` → `Admin::User`).
112
111
  def declared_constant_name(local_name, lexical_path)
113
112
  return local_name.delete_prefix("::") if local_name.start_with?("::")
114
113
 
@@ -8,7 +8,7 @@ module Rigor
8
8
  #
9
9
  # Each row is `{ name:, kind: }`:
10
10
  #
11
- # - `name` — String, the attachment method name as the user invokes it (`"avatar"`).
11
+ # - `name` — the attachment method name as the user invokes it (`"avatar"`).
12
12
  # - `kind` — `:singular` (`has_one_attached`) or `:collection` (`has_many_attached`).
13
13
  class AttachmentIndex
14
14
  attr_reader :entries
@@ -89,13 +89,15 @@ module Rigor
89
89
  # decline — they take side-effecting argument types that the RBS surface already covers.
90
90
  dynamic_return receivers: -> { attachment_index&.class_names || [] } do |call_node, scope|
91
91
  next nil unless call_node.is_a?(Prism::CallNode)
92
- next nil if call_node.receiver.nil?
92
+
93
+ receiver = call_node.receiver
94
+ next nil if receiver.nil?
93
95
  next nil unless call_node.arguments.nil?
94
96
 
95
97
  index = attachment_index
96
98
  next nil if index.nil? || index.empty?
97
99
 
98
- receiver_type = scope.type_of(call_node.receiver)
100
+ receiver_type = scope.type_of(receiver)
99
101
  next nil unless receiver_type.is_a?(Rigor::Type::Nominal)
100
102
 
101
103
  # `AttachmentIndex#attachments_for` de-roots the query itself (#621) — no retry arm here.
@@ -137,19 +139,14 @@ module Rigor
137
139
  nil
138
140
  end
139
141
 
140
- # Issue #1056 — "the attachment index did not load" is a fact about the run's INPUTS, not about the
141
- # file being analysed, so both outcomes are registered rather than returned: see
142
- # {Plugin::Base#disclose_once} for the channel and the position. Returned from the per-file hook they
143
- # carried no once-guard at all — the `.uniq` collapsed repeats within one instance's list, but the
144
- # whole list was re-emitted on every analysed file.
142
+ # Issue #1056 — "the attachment index did not load" is a fact about the run's INPUTS, not the file
143
+ # being analysed, so it is registered via {Plugin::Base#disclose_once} rather than returned from the
144
+ # per-file hook (which had no once-guard and re-emitted the list on every file).
145
145
  #
146
- # The `key`s carry an ordinal prefix because the engine emits a plugin's disclosures in KEY order
147
- # (#1051), and a refused read must still precede a discovery failure the way `@load_errors` records
148
- # them — `@load_errors` is now keyed by that same string, which also caps it: `#attachment_index`
149
- # re-attempts the load per call site, and the old Array appended a fresh interpolated string every
150
- # time (the unbounded-growth shape #569 measured at 4.2 M retained strings on Redmine). The key, not
151
- # the message, is the identity: a second refusal naming a different path collapses into the first row
152
- # rather than adding one — the notice is "the index did not load", said once.
146
+ # The `key`s carry an ordinal prefix because the engine emits disclosures in KEY order (#1051), and
147
+ # a refused read must precede a discovery failure. `@load_errors` is keyed by the same string, which
148
+ # caps it: `#attachment_index` re-attempts the load per call site (#569). The key, not the message,
149
+ # is the identity, so a second refusal collapses into the first row.
153
150
  def disclose_load_errors
154
151
  @load_errors.each do |key, message|
155
152
  disclose_once(key, message: message, severity: :warning, rule: "load-error")
@@ -3,4 +3,4 @@
3
3
  # Entry point matching the `rigor-<id>` gem-name convention. The Plugin::Loader's `require_gem!` step
4
4
  # calls `require "rigor-activestorage"`, and this file loads the plugin's actual implementation under
5
5
  # the `rigor/plugin/` namespace so the require / register pair fires.
6
- require "rigor/plugin/activestorage"
6
+ require_relative "rigor/plugin/activestorage"
@@ -12,7 +12,7 @@ module Rigor
12
12
  # the core_ext predicate surface — the single cheapest purity win in a Rails app, per WD10 — is issue
13
13
  # #388 and lands in this plugin's shipped RBS, not here (`try` was audited and skipped: it dispatches
14
14
  # to whatever method the caller names, so its purity is a fact about the call site, not about `try`).
15
- # One row below (`in_time_zone_row`) is #388's own addition: `DateTime#in_time_zone` reads `Time.zone`
15
+ # The `in_time_zone_rows` group is #388's own addition: `DateTime#in_time_zone` reads `Time.zone`
16
16
  # through a default argument, a gap the RBS sweep found but can only fix here.
17
17
  #
18
18
  # ## The clock
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rigor/type"
4
+
5
+ module Rigor
6
+ module Plugin
7
+ class ActivesupportCoreExt < Rigor::Plugin::Base
8
+ # #2433 — the result type of `Hash#symbolize_keys` / `#to_options`.
9
+ #
10
+ # ActiveSupport implements them as `transform_keys { |key| key.to_sym rescue key }`, so a String or
11
+ # Symbol key becomes a Symbol and a key without `to_sym` (an Integer, a Float) survives unchanged. One
12
+ # RBS return type cannot say that: `Hash[Symbol, V]` reads an Integer key as a Symbol, and
13
+ # `Hash[Symbol | K, V]` makes a String-keyed hash fail a declared `Hash[Symbol, untyped]` parameter.
14
+ # So the RBS rows answer `Hash[untyped, V]` and this module maps each key member where it can.
15
+ #
16
+ # A key that is not a String or Symbol (an Integer, a project class) reads `Dynamic[top]`: no Rigor fact proves
17
+ # a class has no `to_sym` (a mixin, a `pre_eval` file or a reopening can add one), so a kept key could claim
18
+ # a type the program does not have. It declines (`nil`, the RBS row answers) when a receiver member is
19
+ # neither a `Hash[K, V]` nor `nil`. A `nil` member is dropped: `nil.symbolize_keys` raises, and the engine
20
+ # adds `nil` back for `&.`.
21
+ module SymbolizeKeys
22
+ HASH = "Hash"
23
+ SYMBOLIZED_CLASSES = %w[String Symbol].freeze
24
+
25
+ module_function
26
+
27
+ # Answers `Hash[K', V]` (the union of such over the receiver's Hash members), or nil to decline.
28
+ def result(receiver_type, scope)
29
+ members = receiver_type.is_a?(Rigor::Type::Union) ? receiver_type.members : [receiver_type]
30
+ answers = members.reject { |member| nil_member?(member) }.map { |member| member_answer(member, scope) }
31
+ return nil if answers.empty? || answers.any?(:decline)
32
+
33
+ hashes = answers.reject { |answer| answer == :empty }
34
+ hashes.empty? ? nil : Rigor::Type::Combinator.union(*hashes)
35
+ end
36
+
37
+ # An empty closed shape is `{}`: it adds no key, so it drops out of the union rather than declining it.
38
+ def member_answer(member, scope)
39
+ return :empty if member.is_a?(Rigor::Type::HashShape) && member.closed? && member.pairs.empty?
40
+
41
+ key_type, value_type = hash_arguments(member)
42
+ return :decline if key_type.nil?
43
+
44
+ Rigor::Type::Combinator.nominal_of(HASH, type_args: [map_key(key_type, scope), value_type])
45
+ end
46
+
47
+ def nil_member?(type)
48
+ (type.is_a?(Rigor::Type::Constant) && type.value.nil?) ||
49
+ (type.is_a?(Rigor::Type::Nominal) && type.class_name == "NilClass")
50
+ end
51
+
52
+ def hash_arguments(type)
53
+ case type
54
+ when Rigor::Type::Nominal
55
+ type.class_name == HASH && type.type_args.size == 2 ? type.type_args : nil
56
+ when Rigor::Type::Difference
57
+ hash_arguments(type.base)
58
+ when Rigor::Type::HashShape
59
+ shape_arguments(type)
60
+ end
61
+ end
62
+
63
+ # A closed, non-empty shape reads as `Hash[<its keys>, <union of its values>]`.
64
+ def shape_arguments(shape)
65
+ return nil if shape.open? || shape.pairs.empty?
66
+
67
+ keys = shape.pairs.keys.map { |key| Rigor::Type::Combinator.constant_of(key) }
68
+ [Rigor::Type::Combinator.union(*keys), Rigor::Type::Combinator.union(*shape.pairs.values)]
69
+ end
70
+
71
+ def map_key(type, scope)
72
+ members = type.is_a?(Rigor::Type::Union) ? type.members : [type]
73
+ Rigor::Type::Combinator.union(*members.map { |member| map_member(member, scope) })
74
+ end
75
+
76
+ def map_member(member, scope)
77
+ case member
78
+ when Rigor::Type::Constant
79
+ member.value.is_a?(String) || member.value.is_a?(Symbol) ? symbol : Rigor::Type::Combinator.untyped
80
+ when Rigor::Type::Nominal
81
+ symbolized_class?(member.class_name, scope) ? symbol : Rigor::Type::Combinator.untyped
82
+ else Rigor::Type::Combinator.untyped
83
+ end
84
+ end
85
+
86
+ def symbol
87
+ Rigor::Type::Combinator.nominal_of("Symbol")
88
+ end
89
+
90
+ # String, Symbol and their subclasses go through `to_sym`.
91
+ def symbolized_class?(name, scope)
92
+ SYMBOLIZED_CLASSES.include?(name) ||
93
+ SYMBOLIZED_CLASSES.any? { |base| %i[equal subclass].include?(ordering(name, base, scope)) }
94
+ end
95
+
96
+ def ordering(name, base, scope)
97
+ Rigor::Reflection.class_ordering(name, base, scope: scope)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -3,6 +3,7 @@
3
3
  require "rigor/plugin"
4
4
 
5
5
  require_relative "activesupport_core_ext/effects"
6
+ require_relative "activesupport_core_ext/symbolize_keys"
6
7
 
7
8
  module Rigor
8
9
  module Plugin
@@ -43,12 +44,15 @@ module Rigor
43
44
  manifest(
44
45
  id: "activesupport-core-ext",
45
46
  target_gems: ["activesupport"],
47
+ # Bumped 2026-10-27 (#2433) — `Hash#symbolize_keys` / `#to_options` map each key member through a
48
+ # `dynamic_return` hook instead of claiming `Hash[Symbol, V]`.
49
+ #
46
50
  # Bumped 2026-09-10 (#534 item 7) — `ActiveSupport::Concern` is declared, so `extend
47
51
  # ActiveSupport::Concern` resolves instead of reading as an unresolved constant.
48
52
  #
49
53
  # Bumped 2026-09-02 (#632) — `ActiveSupport::Duration`'s reader surface (`ago`/`to_i`/`iso8601`/…)
50
54
  # is now declared, open_receivers-protected; see the class comment above.
51
- version: "0.5.0",
55
+ version: "0.6.0",
52
56
  description: "RBS bundle for the most-frequently-flagged ActiveSupport core_ext extensions, " \
53
57
  "plus the `ActiveSupport::Duration` type of the numeric time multipliers and its " \
54
58
  "reader surface.",
@@ -128,10 +132,12 @@ module Rigor
128
132
 
129
133
  dynamic_return methods: DURATION_MULTIPLIERS do |call_node, scope|
130
134
  next nil unless call_node.is_a?(Prism::CallNode)
131
- next nil unless call_node.receiver # `day` with no receiver is somebody's own method
135
+
136
+ receiver = call_node.receiver
137
+ next nil unless receiver # `day` with no receiver is somebody's own method
132
138
  next nil unless call_node.arguments.nil?
133
139
  next nil unless call_node.block.nil?
134
- next nil unless numeric_receiver?(scope&.type_of(call_node.receiver))
140
+ next nil unless numeric_receiver?(scope&.type_of(receiver))
135
141
 
136
142
  Rigor::Type::Combinator.nominal_of(DURATION_NOMINAL)
137
143
  end
@@ -190,20 +196,35 @@ module Rigor
190
196
  argument = duration_arithmetic_argument(call_node)
191
197
  next nil if argument.nil?
192
198
 
193
- # The ARGUMENT is checked before the receiver: it is the cheaper of the two on the overwhelmingly
194
- # common shapes (`i + 1`, `n * 2`), and a call with no Duration operand must cost as little as
195
- # possible — this rule is consulted on every `+` / `-` / `*` in the project. The early exit is
196
- # what makes the ordering worth anything: an argument that is no Duration operand at all
197
- # (`"a" + b`, `list + other`) declines here and never types its receiver on this rule's behalf.
199
+ # The ARGUMENT is checked before the receiver: it is the cheaper test on the common shapes (`i + 1`,
200
+ # `n * 2`), and this rule is consulted on every `+` / `-` / `*`. An argument that is no Duration
201
+ # operand (`"a" + b`, `list + other`) declines here and never types its receiver on this rule's behalf.
198
202
  argument_kind = duration_operand_kind(scope&.type_of(argument))
199
203
  next nil if argument_kind.nil?
200
204
 
201
- receiver_kind = duration_operand_kind(scope&.type_of(call_node.receiver))
205
+ receiver = call_node.receiver
206
+ # Prism gives every parsed `+` / `-` / `*` call a receiver; the guard is for the RBS `Node?` type.
207
+ next nil if receiver.nil?
208
+
209
+ receiver_kind = duration_operand_kind(scope&.type_of(receiver))
202
210
  next nil if receiver_kind.nil?
203
211
 
204
212
  duration_arithmetic_result(call_node.name, receiver_kind, argument_kind)
205
213
  end
206
214
 
215
+ # #2433 — `symbolize_keys` / `to_options` keep a key without `to_sym`, so the key type is mapped per
216
+ # member; see {SymbolizeKeys}. The RBS rows answer `Hash[untyped, V]` wherever this declines.
217
+ dynamic_return methods: %i[symbolize_keys to_options] do |call_node, scope|
218
+ next nil unless call_node.is_a?(Prism::CallNode)
219
+
220
+ receiver = call_node.receiver
221
+ next nil unless receiver
222
+ next nil unless call_node.arguments.nil?
223
+
224
+ # A block is ignored by `symbolize_keys`, so it does not decline.
225
+ SymbolizeKeys.result(scope&.type_of(receiver), scope)
226
+ end
227
+
207
228
  # ADR-88 WD1 — both tables are static and the RBS bundle ships with the gem; nothing here reads a
208
229
  # project file, so a project enabling this plugin stays incremental-capable.
209
230
  def incremental_state_fingerprint