rigortype 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +50 -3
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +152 -26
  10. data/docs/manual/03-configuration.md +36 -3
  11. data/docs/manual/06-baseline.md +6 -4
  12. data/docs/manual/09-editor-integration.md +13 -2
  13. data/docs/manual/10-mcp-server.md +38 -2
  14. data/docs/manual/12-caching.md +4 -3
  15. data/docs/manual/plugins/rigor-actionpack.md +19 -11
  16. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  17. data/lib/rigor/analysis/baseline.rb +24 -27
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  20. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  21. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  22. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  24. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +0 -1
  25. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  26. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  27. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  28. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  29. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  30. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  31. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  32. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  33. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  35. data/lib/rigor/analysis/check_rules.rb +124 -44
  36. data/lib/rigor/analysis/crash_signature.rb +2 -2
  37. data/lib/rigor/analysis/dependency_recorder.rb +4 -4
  38. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  39. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  40. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  41. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  42. data/lib/rigor/analysis/edit_replay.rb +93 -6
  43. data/lib/rigor/analysis/fact_store.rb +3 -3
  44. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  45. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  46. data/lib/rigor/analysis/incremental_session.rb +27 -16
  47. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  48. data/lib/rigor/analysis/run_stats.rb +2 -2
  49. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +2 -2
  50. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  51. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +193 -93
  52. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  53. data/lib/rigor/analysis/runner/pool_coordinator.rb +147 -323
  54. data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -3
  55. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  56. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  57. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  58. data/lib/rigor/analysis/runner.rb +77 -71
  59. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  60. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  61. data/lib/rigor/analysis/template_units.rb +9 -1
  62. data/lib/rigor/analysis/worker_session.rb +24 -31
  63. data/lib/rigor/bleeding_edge.rb +3 -6
  64. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  65. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  66. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  67. data/lib/rigor/cache/descriptor.rb +6 -5
  68. data/lib/rigor/cache/incremental_snapshot.rb +31 -12
  69. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  70. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  71. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  72. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  73. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  74. data/lib/rigor/cache/rbs_environment.rb +4 -1
  75. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  76. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  77. data/lib/rigor/cache/store.rb +11 -10
  78. data/lib/rigor/ci_detector.rb +1 -1
  79. data/lib/rigor/cli/annotate_command.rb +24 -19
  80. data/lib/rigor/cli/baseline_command.rb +145 -55
  81. data/lib/rigor/cli/catalogue.rb +256 -0
  82. data/lib/rigor/cli/check_command.rb +108 -102
  83. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  84. data/lib/rigor/cli/command.rb +15 -1
  85. data/lib/rigor/cli/coverage_command.rb +21 -8
  86. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  87. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  88. data/lib/rigor/cli/coverage_scan.rb +3 -5
  89. data/lib/rigor/cli/diff_command.rb +25 -20
  90. data/lib/rigor/cli/docs_command.rb +1 -2
  91. data/lib/rigor/cli/doctor_command.rb +12 -12
  92. data/lib/rigor/cli/effects_command.rb +50 -40
  93. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  94. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  95. data/lib/rigor/cli/effects_renderer.rb +2 -1
  96. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  97. data/lib/rigor/cli/explain_command.rb +18 -8
  98. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  99. data/lib/rigor/cli/json_document.rb +41 -0
  100. data/lib/rigor/cli/lens_command.rb +12 -8
  101. data/lib/rigor/cli/lsp_command.rb +250 -23
  102. data/lib/rigor/cli/mcp_command.rb +12 -7
  103. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  104. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  105. data/lib/rigor/cli/options.rb +40 -1
  106. data/lib/rigor/cli/plugin_command.rb +93 -46
  107. data/lib/rigor/cli/plugins_command.rb +17 -18
  108. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  109. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  110. data/lib/rigor/cli/protection_renderer.rb +2 -1
  111. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  112. data/lib/rigor/cli/sig_gen_command.rb +84 -27
  113. data/lib/rigor/cli/skill_command.rb +32 -3
  114. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  115. data/lib/rigor/cli/skill_describe.rb +1 -1
  116. data/lib/rigor/cli/trace_command.rb +14 -9
  117. data/lib/rigor/cli/triage_command.rb +10 -11
  118. data/lib/rigor/cli/triage_renderer.rb +2 -1
  119. data/lib/rigor/cli/type_of_command.rb +16 -11
  120. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  121. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  122. data/lib/rigor/cli/type_scan_command.rb +14 -9
  123. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  124. data/lib/rigor/cli/unused_command.rb +27 -17
  125. data/lib/rigor/cli/upgrade_command.rb +5 -0
  126. data/lib/rigor/cli.rb +209 -54
  127. data/lib/rigor/config_audit.rb +1 -1
  128. data/lib/rigor/configuration/severity_profile.rb +5 -5
  129. data/lib/rigor/configuration.rb +72 -30
  130. data/lib/rigor/effects/definition_context.rb +0 -2
  131. data/lib/rigor/effects/envelope.rb +1 -1
  132. data/lib/rigor/effects/framework_units.rb +1 -1
  133. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  134. data/lib/rigor/effects/propagator.rb +1 -1
  135. data/lib/rigor/effects/registry.rb +2 -2
  136. data/lib/rigor/effects/snapshot.rb +1 -1
  137. data/lib/rigor/effects/taint_cause.rb +0 -2
  138. data/lib/rigor/effects/unit_scan.rb +9 -13
  139. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  140. data/lib/rigor/environment/class_registry.rb +3 -3
  141. data/lib/rigor/environment/core_root.rb +77 -0
  142. data/lib/rigor/environment/default_libraries.rb +1 -1
  143. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  144. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  145. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  146. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  147. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  148. data/lib/rigor/environment/member_consistency.rb +56 -7
  149. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  150. data/lib/rigor/environment/project_path_gems.rb +124 -0
  151. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  152. data/lib/rigor/environment/rbs_loader.rb +480 -67
  153. data/lib/rigor/environment/reflection.rb +7 -7
  154. data/lib/rigor/environment/required_features.rb +0 -3
  155. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  156. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  157. data/lib/rigor/environment.rb +41 -10
  158. data/lib/rigor/flow_contribution/merger.rb +4 -4
  159. data/lib/rigor/inference/acceptance.rb +93 -32
  160. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  161. data/lib/rigor/inference/block_run_count.rb +178 -0
  162. data/lib/rigor/inference/budget_trace.rb +8 -2
  163. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  164. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  165. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  166. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  167. data/lib/rigor/inference/callee_write_census.rb +815 -0
  168. data/lib/rigor/inference/construction_escape.rb +217 -0
  169. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  170. data/lib/rigor/inference/deep_erasure.rb +39 -7
  171. data/lib/rigor/inference/def_handle.rb +1 -1
  172. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  173. data/lib/rigor/inference/def_return_typer.rb +46 -4
  174. data/lib/rigor/inference/definer_resolution.rb +4 -2
  175. data/lib/rigor/inference/definers_index.rb +106 -0
  176. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  177. data/lib/rigor/inference/element_read_widening.rb +49 -3
  178. data/lib/rigor/inference/expression_typer.rb +116 -49
  179. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  180. data/lib/rigor/inference/global_write_census.rb +76 -2
  181. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  182. data/lib/rigor/inference/hkt_body.rb +1 -2
  183. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  184. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  185. data/lib/rigor/inference/hkt_registry.rb +3 -5
  186. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  187. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  188. data/lib/rigor/inference/ivar_writer_census.rb +51 -0
  189. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  190. data/lib/rigor/inference/loop_value_record.rb +8 -4
  191. data/lib/rigor/inference/match_rebinding/frame.rb +46 -0
  192. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  193. data/lib/rigor/inference/match_rebinding.rb +11 -1
  194. data/lib/rigor/inference/memo_audit.rb +55 -0
  195. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  197. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  199. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  200. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  201. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  203. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  204. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  206. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  208. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  209. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  210. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  211. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  212. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  213. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  214. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +64 -14
  217. data/lib/rigor/inference/module_function_state.rb +11 -1
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  219. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  220. data/lib/rigor/inference/mutation_widening.rb +157 -27
  221. data/lib/rigor/inference/narrowing.rb +47 -33
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  223. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  226. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  227. data/lib/rigor/inference/project_yielder.rb +11 -17
  228. data/lib/rigor/inference/receiver_alias.rb +20 -0
  229. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  230. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  231. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  232. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  233. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  234. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  235. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  236. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +58 -15
  237. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  238. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  239. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  240. data/lib/rigor/inference/scope_indexer.rb +137 -46
  241. data/lib/rigor/inference/self_call_ivar_writes.rb +84 -12
  242. data/lib/rigor/inference/set_mutation.rb +189 -0
  243. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  244. data/lib/rigor/inference/statement_evaluator.rb +2328 -138
  245. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  246. data/lib/rigor/inference/synthetic_method_scanner.rb +5 -6
  247. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  248. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  249. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  250. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  251. data/lib/rigor/language_server/buffer_table.rb +3 -3
  252. data/lib/rigor/language_server/completion_provider.rb +26 -20
  253. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  254. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  255. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  256. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  257. data/lib/rigor/language_server/hover_provider.rb +1 -7
  258. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  259. data/lib/rigor/language_server/project_context.rb +39 -1
  260. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  261. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  262. data/lib/rigor/language_server/server.rb +7 -3
  263. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  264. data/lib/rigor/lens/plugins.rb +1 -1
  265. data/lib/rigor/mcp/server.rb +73 -2
  266. data/lib/rigor/mcp.rb +1 -1
  267. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  268. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  269. data/lib/rigor/plugin/io_boundary.rb +1 -1
  270. data/lib/rigor/plugin/loader.rb +25 -8
  271. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  272. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  273. data/lib/rigor/plugin/manifest.rb +9 -13
  274. data/lib/rigor/plugin/registry.rb +3 -3
  275. data/lib/rigor/plugin/services.rb +2 -3
  276. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  277. data/lib/rigor/plugin/trust_policy.rb +2 -2
  278. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  279. data/lib/rigor/plugin.rb +6 -8
  280. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  281. data/lib/rigor/project_environment.rb +16 -2
  282. data/lib/rigor/protection/analysis_guard.rb +1 -1
  283. data/lib/rigor/protection/discovery_seed.rb +44 -5
  284. data/lib/rigor/protection/mutator.rb +1 -1
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  287. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  288. data/lib/rigor/rbs_extended.rb +2 -3
  289. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  290. data/lib/rigor/rooted_glob.rb +15 -1
  291. data/lib/rigor/run_scoped_slots.rb +38 -0
  292. data/lib/rigor/runtime/jit.rb +1 -1
  293. data/lib/rigor/scope/discovery_index.rb +19 -6
  294. data/lib/rigor/scope/resolution_chain.rb +3 -1
  295. data/lib/rigor/scope.rb +365 -31
  296. data/lib/rigor/sig_gen/classification.rb +4 -0
  297. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  298. data/lib/rigor/sig_gen/generator.rb +371 -58
  299. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  300. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  301. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  302. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  303. data/lib/rigor/sig_gen/observation_collector.rb +3 -5
  304. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  305. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  306. data/lib/rigor/sig_gen/renderer.rb +15 -9
  307. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  308. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  309. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  310. data/lib/rigor/sig_gen/write_result.rb +23 -12
  311. data/lib/rigor/sig_gen/writer.rb +217 -37
  312. data/lib/rigor/type/acceptance_router.rb +2 -2
  313. data/lib/rigor/type/accepts_result.rb +1 -1
  314. data/lib/rigor/type/app.rb +4 -6
  315. data/lib/rigor/type/combinator.rb +9 -13
  316. data/lib/rigor/type/constant.rb +4 -5
  317. data/lib/rigor/type/hash_shape.rb +21 -40
  318. data/lib/rigor/type/union.rb +1 -1
  319. data/lib/rigor/type_node/identifier.rb +1 -2
  320. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  321. data/lib/rigor/type_node/string_literal.rb +1 -2
  322. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  323. data/lib/rigor/type_node/union.rb +3 -6
  324. data/lib/rigor/value_semantics.rb +1 -10
  325. data/lib/rigor/version.rb +1 -1
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  332. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +66 -19
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  340. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  347. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  348. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  350. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  351. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  352. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  355. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  356. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  357. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  358. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  359. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  360. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  361. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  362. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  363. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  364. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  365. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  366. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  367. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  368. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  369. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  370. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  371. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  372. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  373. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  374. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  375. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  376. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  377. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  381. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  382. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  383. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  386. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  387. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  388. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  389. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  390. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  391. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  392. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  393. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  394. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  395. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  396. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  397. data/sig/rigor/analysis/baseline.rbs +0 -3
  398. data/sig/rigor/analysis/fact_store.rbs +1 -0
  399. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  400. data/sig/rigor/cli/diff_command.rbs +1 -1
  401. data/sig/rigor/cli/explain_command.rbs +1 -1
  402. data/sig/rigor/effects/registry.rbs +2 -2
  403. data/sig/rigor/inference.rbs +4 -1
  404. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  405. data/sig/rigor/plugin/base.rbs +1 -0
  406. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  407. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  408. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  409. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  410. data/sig/rigor/plugin/manifest.rbs +9 -9
  411. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  412. data/sig/rigor/scope.rbs +23 -3
  413. data/skills/rigor-plugin-author/SKILL.md +4 -4
  414. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  415. data/skills/rigor-plugin-review/SKILL.md +3 -3
  416. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  417. metadata +36 -3
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../source/node_walker"
6
+
7
+ module Rigor
8
+ module Inference
9
+ module ScopeIndexer
10
+ # Issue #2291 — the class-ivar census types an `@x = LIT` write from the type the constant's own write binds, the
11
+ # same way a method body reads `LIT`. The census runs before the file's in-source constant table is seeded, so
12
+ # without it the write typed under no constant table and a reader over the ivar (`def q = @q`) read `Dynamic[top]`
13
+ # where `def g = LIT` read the constant's type.
14
+ #
15
+ # One instance serves one census walk. The file's constant table is built on the first constant-valued write, so
16
+ # a file with none pays nothing; it is built as {ScopeIndexer#index} builds it later (typed under the census's
17
+ # scope, merged over the scope's project seed, a mutated literal widened), and resolved through the ordinary
18
+ # constant lookup. {#type_of} answers nil, leaving the write's ordinary typing, when:
19
+ #
20
+ # - the value is not a constant read (`LIT`, `A::LIT`, `::LIT`); a path over anything else (`self.class::LIT`)
21
+ # resolves to nothing, as it does in a method body;
22
+ # - the file may rebind a constant of that last segment: a second write in any form, an operator, `||=` or `&&=`
23
+ # write, a multiple-assignment target, or a `const_set` / `remove_const` naming it — or naming anything but a
24
+ # literal, which may be it.
25
+ #
26
+ # A constant another file assigns resolves only through the scope's own project seed, as it did before.
27
+ class ConstantIvarValue
28
+ WRITE_NODES = [Prism::ConstantWriteNode, Prism::ConstantPathWriteNode].freeze
29
+ REBIND_NODES = [Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
30
+ Prism::ConstantPathOperatorWriteNode, Prism::ConstantPathOrWriteNode,
31
+ Prism::ConstantPathAndWriteNode, Prism::ConstantTargetNode,
32
+ Prism::ConstantPathTargetNode].freeze
33
+ REBIND_CALLS = %i[const_set remove_const].freeze
34
+ ANY_NAME = :any
35
+ private_constant :WRITE_NODES, :REBIND_NODES, :REBIND_CALLS, :ANY_NAME
36
+
37
+ def initialize(root, scope)
38
+ @root = root
39
+ @scope = scope
40
+ end
41
+
42
+ # The type `value` reads under the file's constant table, or nil when the write keeps its ordinary typing.
43
+ def type_of(value, scope)
44
+ return nil unless value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
45
+ return nil if rebound?(value.name)
46
+
47
+ scope.with_discovery(scope.discovery.with(in_source_constants: constants)).type_of(value)
48
+ end
49
+
50
+ private
51
+
52
+ def constants
53
+ @constants ||= begin
54
+ literal_mutations = ScopeIndexer.collect_literal_receiver_mutations(@root)
55
+ per_file = ScopeIndexer.widen_mutated_constants(
56
+ ScopeIndexer.build_in_source_constants(@root, @scope), literal_mutations[:constants]
57
+ )
58
+ ScopeIndexer.merge_seeded_constants(@scope.in_source_constants, per_file)
59
+ end
60
+ end
61
+
62
+ def rebound?(name)
63
+ @rebound ||= rebound_names
64
+ @rebound.include?(ANY_NAME) || @rebound.include?(name)
65
+ end
66
+
67
+ def rebound_names
68
+ written = Set.new
69
+ rebound = Set.new
70
+ Source::NodeWalker.each(@root) do |node|
71
+ case node
72
+ when *WRITE_NODES
73
+ name = write_name(node)
74
+ rebound << name unless written.add?(name)
75
+ when *REBIND_NODES then rebound << write_name(node)
76
+ when Prism::CallNode then rebound << call_name(node) if REBIND_CALLS.include?(node.name)
77
+ end
78
+ end
79
+ rebound
80
+ end
81
+
82
+ def write_name(node) = node.respond_to?(:target) ? node.target.name : node.name
83
+
84
+ def call_name(node)
85
+ argument = node.arguments&.arguments&.first
86
+ return argument.unescaped.to_sym if argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
87
+
88
+ ANY_NAME
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -6,6 +6,7 @@ require_relative "../../reflection"
6
6
  require_relative "../method_parameter_binder"
7
7
  require_relative "../global_write_census"
8
8
  require_relative "unseen_ivar_writers"
9
+ require_relative "derived_param_value"
9
10
 
10
11
  module Rigor
11
12
  module Inference
@@ -21,7 +22,13 @@ module Rigor
21
22
  # every gate, and {EMPTY} otherwise; an absent write keeps the census's ordinary rvalue typing, so a declined
22
23
  # case reads exactly what it read before:
23
24
  #
24
- # - the write's value is a bare read of the parameter (`@x = x`), not an expression over it;
25
+ # - the write's value reads at least one parameter, and every local it reads is a parameter with a declared type.
26
+ # A bare read (`@x = x`) records that type; an expression over parameters (`@x = x.dup.freeze`,
27
+ # `@x = Array(x)`, `@x = f ? x : y`, issue #2321) records the value typed with each parameter it reads bound to
28
+ # its declared type, unless {DerivedParamValue} declines it (a parameter that holds `untyped` at any depth or
29
+ # has a default outside its declared type, or a call argument whose type may match a narrower overload). A
30
+ # value that holds a block, a lambda or a nested `def` is not credited: their own parameters are locals the
31
+ # scan does not bind;
25
32
  # - the write is not inside a block or a lambda, so a block parameter of the same name and a block that runs
26
33
  # under another `self` (`instance_eval`, `Class.new`, `define_method`) never borrow the method's parameter;
27
34
  # - the method body never writes the parameter, in any form and at any depth (`x = Integer(x)`, `x ||= …`,
@@ -33,8 +40,8 @@ module Rigor
33
40
  # - the class's primary RBS declaration is the project's (`sig/` or rbs-inline), not core, stdlib or a gem's:
34
41
  # a project reopening of a library class (`Thread::SizedQueue#initialize`) would otherwise borrow the
35
42
  # library's parameter type for its own `def`;
36
- # - the body neither calls `binding` nor passes code to `eval` / `instance_eval` / `class_eval` /
37
- # `module_eval` / `instance_exec` as an argument, any of which can rewrite the parameter unseen;
43
+ # - the body neither calls `binding` nor passes code as an argument to an `eval`-family call
44
+ # (`OPAQUE_EVAL_CALLS`), any of which can rewrite the parameter unseen;
38
45
  # - nothing outside the class's own census may write the ivar ({UnseenIvarWriters}): no project ancestor, and
39
46
  # no other object's code (`o.instance_variable_set(:@x, v)`);
40
47
  # - the declared type is not `untyped`, which is what the write already records.
@@ -57,8 +64,8 @@ module Rigor
57
64
  module_function
58
65
 
59
66
  # Runs the block with `scope` as the one whose project tables the unseen-writer and single-file gates read, in
60
- # place of the census's own scope. `rigor sig-gen` indexes each file under a scope that carries no project
61
- # tables, so without it another file's writer, ancestor or reopening would go unseen.
67
+ # place of the census's own scope. `rigor sig-gen` indexes each file over the project tables less the member
68
+ # layouts; this scope carries all of them, so another file's writer, ancestor or reopening is seen.
62
69
  def with_writer_scope(scope)
63
70
  return yield if scope.nil?
64
71
 
@@ -82,13 +89,15 @@ module Rigor
82
89
  writes = []
83
90
  return EMPTY if scan(def_node.body, written, writes, in_block: false) == :opaque
84
91
 
85
- writes.reject! { |write| written.include?(write.value.name) }
86
- return EMPTY if writes.empty?
92
+ reads = unwritten_reads(writes, written)
93
+ return EMPTY if reads.empty?
87
94
 
88
95
  types = declared_types(def_node, class_name, scope)
89
96
  writers = writer_scope(scope)
90
- credited = writes.each_with_object({}.compare_by_identity) do |write, acc|
91
- type = types[write.value.name]
97
+ credited = reads.each_with_object({}.compare_by_identity) do |(write, names), acc|
98
+ next unless names.all? { |name| types.key?(name) }
99
+
100
+ type = value_type(write.value, names, types, def_node, scope)
92
101
  acc[write] = type if type && !UnseenIvarWriters.possible?(writers, class_name, write.name)
93
102
  end
94
103
  credited.empty? ? EMPTY : credited
@@ -96,7 +105,45 @@ module Rigor
96
105
  EMPTY
97
106
  end
98
107
 
99
- # Collects the `@x = local` writes outside every block, and the name of every local the body writes in
108
+ # `{write => names of the locals its value reads}` for each write whose value reads at least one local and no
109
+ # local the body writes.
110
+ def unwritten_reads(writes, written)
111
+ writes.each_with_object({}.compare_by_identity) do |write, acc|
112
+ names = parameter_reads(write.value)
113
+ next if names.nil? || names.empty? || names.any? { |name| written.include?(name) }
114
+
115
+ acc[write] = names
116
+ end
117
+ end
118
+
119
+ # The names of the locals `value` reads, or nil when it holds a block, a lambda or a scope barrier.
120
+ def parameter_reads(value, names = Set.new)
121
+ return names unless value.is_a?(Prism::Node)
122
+ return nil if BLOCK_NODES.any? { |klass| value.is_a?(klass) }
123
+ return nil if SCOPE_BARRIERS.any? { |klass| value.is_a?(klass) }
124
+
125
+ names << value.name if value.is_a?(Prism::LocalVariableReadNode)
126
+ value.rigor_each_child do |child|
127
+ return nil if parameter_reads(child, names).nil?
128
+ end
129
+ names
130
+ end
131
+
132
+ # A bare read takes the declared type itself; an expression over the parameters is typed with each bound to
133
+ # its declared type, unless {DerivedParamValue} declines it.
134
+ def value_type(value, names, types, def_node, scope)
135
+ return types[value.name] if value.is_a?(Prism::LocalVariableReadNode)
136
+ return nil if DerivedParamValue.declined?(names, types, def_node, scope)
137
+
138
+ bound = names.reduce(scope) { |acc, name| acc.with_local(name, types[name]) }
139
+ return nil if DerivedParamValue.overload_sensitive_argument?(value, bound)
140
+
141
+ bound.type_of(value)
142
+ rescue StandardError
143
+ nil
144
+ end
145
+
146
+ # Collects the `@x = …` writes outside every block, and the name of every local the body writes in
100
147
  # any form at any depth. A read at depth zero outside a block names the method's own local, which is the
101
148
  # parameter when the body never writes the name.
102
149
  #
@@ -113,7 +160,7 @@ module Rigor
113
160
  when Prism::CallNode
114
161
  return :opaque if opaque_call?(node)
115
162
  end
116
- writes << node if !in_block && local_read_write?(node)
163
+ writes << node if !in_block && node.is_a?(Prism::InstanceVariableWriteNode)
117
164
  inner = in_block || BLOCK_NODES.any? { |klass| node.is_a?(klass) }
118
165
  node.rigor_each_child do |child|
119
166
  return :opaque if scan(child, written, writes, in_block: inner) == :opaque
@@ -129,10 +176,6 @@ module Rigor
129
176
  OPAQUE_EVAL_CALLS.include?(node.name) && !(node.arguments&.arguments || []).empty?
130
177
  end
131
178
 
132
- def local_read_write?(node)
133
- node.is_a?(Prism::InstanceVariableWriteNode) && node.value.is_a?(Prism::LocalVariableReadNode)
134
- end
135
-
136
179
  # The declared, non-`untyped` type of each parameter, or `{}` when a gate declines the method.
137
180
  def declared_types(def_node, class_name, scope)
138
181
  environment = scope&.environment
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../acceptance"
6
+
7
+ module Rigor
8
+ module Inference
9
+ module ScopeIndexer
10
+ # Issue #2321 — the declines {DeclaredParamWrites} applies to a write whose value is an expression over declared
11
+ # parameters, beyond the ones a bare read (`@x = x`) shares. Each says the expression's type, evaluated with the
12
+ # parameters bound to their declared types, may not be the type the ivar holds at runtime:
13
+ #
14
+ # - a parameter whose declared type holds `untyped` at any depth (`Array[untyped]`, #2325): the answer is built
15
+ # on the part the author left untyped;
16
+ # - an optional parameter whose default value's type is not within its declared type
17
+ # (`def initialize(x = nil)` under `(?Integer x)`): the value may run on the default, which the binding does not
18
+ # carry;
19
+ # - an argument of any call in the value (`a + b`, `a + (b > 0 ? b : 0.5)`, `a[i]`, `Array(x)`, `super(…)`,
20
+ # `yield(…)`), typed under the parameter bindings, whose type is anything but a literal or an instance of a
21
+ # class no RBS class names as its superclass: the engine picks the first overload such an argument matches
22
+ # (`Integer#+` given `Integer | Float` answers `Integer`), not the join of every overload it may reach. A
23
+ # receiver dispatches per member and is not declined. A splat or keyword argument types as neither, so it
24
+ # declines too.
25
+ module DerivedParamValue
26
+ module_function
27
+
28
+ # Whether a write over the parameters `names`, declared `types`, is not credited for a parameter it reads.
29
+ def declined?(names, types, def_node, scope)
30
+ names.any? do |name|
31
+ carries_dynamic?(types[name]) || default_outside?(def_node, name, types[name], scope)
32
+ end
33
+ end
34
+
35
+ # Whether a call in `value` takes an argument whose type, under the parameter bindings of `bound`, may match an
36
+ # overload narrower than itself.
37
+ def overload_sensitive_argument?(value, bound)
38
+ return false unless value.is_a?(Prism::Node)
39
+
40
+ if value.is_a?(Prism::CallNode) || value.is_a?(Prism::SuperNode) || value.is_a?(Prism::YieldNode)
41
+ arguments = value.arguments&.arguments || []
42
+ return true if arguments.any? { |argument| overload_sensitive?(bound.type_of(argument), bound) }
43
+ end
44
+ value.rigor_each_child { |child| return true if overload_sensitive_argument?(child, bound) }
45
+ false
46
+ end
47
+
48
+ def carries_dynamic?(type)
49
+ case type
50
+ when Type::Dynamic then true
51
+ when Type::Union, Type::Intersection then type.members.any? { |member| carries_dynamic?(member) }
52
+ when Type::Nominal then type.type_args.any? { |arg| carries_dynamic?(arg) }
53
+ when Type::Tuple then type.elements.any? { |element| carries_dynamic?(element) }
54
+ when Type::HashShape then type.pairs.each_value.any? { |pair_value| carries_dynamic?(pair_value) }
55
+ # No RBS parameter spelling yields a refinement over `untyped`; the arm keeps the walk total.
56
+ when Type::Difference, Type::Refined then carries_dynamic?(type.base)
57
+ else false
58
+ end
59
+ end
60
+
61
+ # Whether an argument of this declared type may match an overload narrower than itself.
62
+ def overload_sensitive?(type, scope)
63
+ case type
64
+ when Type::Constant then false
65
+ when Type::Nominal then !leaf_class?(type.class_name, scope&.environment)
66
+ else true
67
+ end
68
+ end
69
+
70
+ def leaf_class?(class_name, environment)
71
+ return false if environment.nil? || environment.rbs_module?(class_name)
72
+ # A guard: a validated RBS signature names no unknown class, so this answers only for a broken environment.
73
+ return false unless environment.class_known?(class_name)
74
+
75
+ superclasses = environment.rbs_loader&.superclass_names
76
+ return false if superclasses.nil? || superclasses.empty?
77
+
78
+ !superclasses.include?(class_name.delete_prefix("::"))
79
+ end
80
+
81
+ # Whether `name` is an optional parameter of `def_node` whose default value's type is not within `declared`.
82
+ def default_outside?(def_node, name, declared, scope)
83
+ default = default_value(def_node.parameters, name)
84
+ return false if default.nil?
85
+
86
+ default_type = scope.type_of(default)
87
+ return true if carries_dynamic?(default_type)
88
+
89
+ !Acceptance.accepts(declared, default_type).yes? && !empty_collection_within?(declared, default_type)
90
+ end
91
+
92
+ # An empty `[]` or `{}` default is within any `Array[T]` or `Hash[K, V]` the declaration names, which the
93
+ # acceptance check answers only `maybe` (the empty literal has no element type to compare).
94
+ def empty_collection_within?(declared, default_type)
95
+ collection =
96
+ if default_type.is_a?(Type::Tuple) && default_type.elements.empty? then "Array"
97
+ elsif default_type.is_a?(Type::HashShape) && default_type.pairs.empty? then "Hash"
98
+ end
99
+ return false if collection.nil?
100
+
101
+ members = declared.is_a?(Type::Union) ? declared.members : [declared]
102
+ members.any? { |member| member.is_a?(Type::Nominal) && member.class_name == collection }
103
+ end
104
+
105
+ def default_value(parameters, name)
106
+ candidates = parameters.optionals + parameters.keywords
107
+ candidates.find { |param| param.respond_to?(:value) && param.name == name }&.value
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../../type"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module ScopeIndexer
9
+ # Issue #2499 — the accumulator idiom: a heap slot (an instance, class or global variable, or a constant)
10
+ # seeded with an empty literal and filled later, often from a method, a subclass, a mixin or another file
11
+ # the census cannot see. The seed is never proof that the slot is still empty, so the census records it
12
+ # without its emptiness: `""` and `+""` as `String`, `[]` as `Array[untyped]`, `{}` as
13
+ # `Hash[untyped, untyped]`. `String.new`, `Array.new` and `Hash.new` already type as their nominal.
14
+ #
15
+ # Only the empty spellings count. A non-empty literal (`@order = "asc"`, `MODES = %w[a b]`,
16
+ # `@opts = { a: 1 }`) keeps its exact type: such slots are modes and config tables, and an RBS parameter
17
+ # typed `"asc" | "desc"` must still accept them. A local keeps its flow precision; this applies only where
18
+ # the census seeds a heap slot.
19
+ module EmptySeed
20
+ module_function
21
+
22
+ # `type` with its emptiness dropped when `node` is an empty seed literal, otherwise `type` itself.
23
+ def erase(node, type)
24
+ return type unless empty_seed?(node)
25
+
26
+ members = type.is_a?(Type::Union) ? type.members : [type]
27
+ Type::Combinator.union(*members.map { |member| unpinned(member) })
28
+ end
29
+
30
+ # `{ name => value node }` for each leading target of a multiple assignment whose right-hand side is a literal
31
+ # list (`@a, @b = {}, []`), so each slot is erased from its own element. Empty for any other right-hand side.
32
+ def multi_write_values(node)
33
+ value = node.value
34
+ return EMPTY_VALUES unless value.is_a?(Prism::ArrayNode)
35
+
36
+ elements = value.elements
37
+ node.lefts.each_with_index.with_object({}) do |(target, index), values|
38
+ break values if index >= elements.size || elements[index].is_a?(Prism::SplatNode)
39
+ next unless target.respond_to?(:name)
40
+
41
+ values[target.name] = elements[index]
42
+ end
43
+ end
44
+
45
+ EMPTY_VALUES = {}.freeze
46
+ private_constant :EMPTY_VALUES
47
+
48
+ # A chained write (`@a = @b = ""`) stores the inner write's value in the outer slot too, so it is followed.
49
+ def empty_seed?(node)
50
+ case node
51
+ when Prism::StringNode then node.unescaped.empty?
52
+ when Prism::ArrayNode, Prism::HashNode then node.elements.empty?
53
+ when Prism::CallNode then copied_empty_seed?(node)
54
+ when *CHAINED_WRITES then empty_seed?(node.value)
55
+ else false
56
+ end
57
+ end
58
+
59
+ CHAINED_WRITES = [
60
+ Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::ClassVariableWriteNode,
61
+ Prism::GlobalVariableWriteNode, Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
62
+ ].freeze
63
+ private_constant :CHAINED_WRITES
64
+
65
+ # `+""`, the unfrozen empty String the accumulator idiom spells under `# frozen_string_literal: true`, and a
66
+ # `.dup` of an empty seed (`"".dup`, `[].dup`, `{}.dup`).
67
+ def copied_empty_seed?(node)
68
+ return false unless node.arguments.nil? && node.block.nil?
69
+
70
+ case node.name
71
+ when :+@ then node.receiver.is_a?(Prism::StringNode) && node.receiver.unescaped.empty?
72
+ when :dup then empty_seed?(node.receiver)
73
+ else false
74
+ end
75
+ end
76
+
77
+ def unpinned(member)
78
+ case member
79
+ when Type::Constant
80
+ member.value.is_a?(String) && member.value.empty? ? Type::Combinator.nominal_of("String") : member
81
+ when Type::Tuple
82
+ if member.elements.empty?
83
+ Type::Combinator.nominal_of("Array",
84
+ type_args: [Type::Combinator.untyped])
85
+ else
86
+ member
87
+ end
88
+ when Type::HashShape
89
+ return member unless member.pairs.empty?
90
+
91
+ Type::Combinator.nominal_of("Hash", type_args: [Type::Combinator.untyped, Type::Combinator.untyped])
92
+ else member
93
+ end
94
+ end
95
+ private_class_method :copied_empty_seed?, :unpinned
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../analysis/dependency_recorder"
4
+ require_relative "../../run_scoped_slots"
5
+ require_relative "../../type"
6
+ require_relative "../refined_dispatch/own_method"
7
+
8
+ module Rigor
9
+ module Inference
10
+ module ScopeIndexer
11
+ # Issues #2079 and #2170 — the instance-side ivar writes the class-ivar census of one file does not walk, joined
12
+ # into the ivars that census already holds for a class `K`:
13
+ #
14
+ # - the writes another file's reopening of `K` makes;
15
+ # - the writes of every project module and superclass on `K`'s instance resolution chain (`include`,
16
+ # `prepend`, transitively, and `<`), from this file's own census of that ancestor where it declares one
17
+ # (`writes`, taken before the read-before-write `nil` is added, so an ancestor's `initialize` exempts nothing
18
+ # and its read-before-write adds nothing, and only for an ivar an instance body of the ancestor writes in this
19
+ # file) and from every other file's entries for it.
20
+ #
21
+ # Another file's write comes from its `[:own_write_type, owner, ivar, value, path]` census entry
22
+ # ({IvarWriterCensus}): a literal joins as its `Constant`, anything else as `Dynamic[top]`. An ivar the census
23
+ # has no entry for gains none, so a read of an ivar only another file writes still reads `Dynamic[top]`. The
24
+ # join only widens, and every joined type is in the seed, so a `nil` it brings is declaration-sourced (ADR-58
25
+ # WD1). A file the scope names no path for (a census built outside the project pre-pass, `rigor sig-gen`'s
26
+ # unseeded scopes) joins nothing.
27
+ #
28
+ # ADR-46: each ivar read files `own_write_type:<owner>#<ivar>` for `K` and each chain owner, which the
29
+ # incremental session fires when a file's entries for the pair appear, vanish or change value; and the chain
30
+ # files its class edges ({Scope::ResolutionChain#record}), so an `include` added or removed re-checks the reader.
31
+ module IvarContributions
32
+ TABLE_KEY = RunScopedSlots.register(:__rigor_ivar_contributions_table__)
33
+ EMPTY = {}.freeze
34
+ private_constant :TABLE_KEY, :EMPTY
35
+
36
+ module_function
37
+
38
+ def join!(accumulator, writes, scope)
39
+ path = scope.source_path
40
+ return if path.nil?
41
+
42
+ path = canonical(path)
43
+
44
+ table = table(scope.discovered_global_write_census)
45
+ accumulator.each do |class_name, entries|
46
+ next unless class_name.is_a?(String) && !entries.empty?
47
+
48
+ owners = chain_owners(scope, class_name)
49
+ entries.each_key do |ivar|
50
+ joined = contributed(owners, class_name, ivar, table, writes, path)
51
+ entries[ivar] = Type::Combinator.union(entries[ivar], joined) if joined
52
+ end
53
+ end
54
+ end
55
+
56
+ # `K` and the project classes and modules on its instance chain, in chain order, with their class edges filed.
57
+ # A module's own census takes no chain: its ancestors' writes land on whatever includes it.
58
+ def chain_owners(scope, class_name)
59
+ return [class_name] if RefinedDispatch::OwnMethod.module_name?(scope, class_name)
60
+
61
+ chain = Scope::ResolutionChain.for(scope, class_name, :instance, :methods)
62
+ # A guard: the class body's own `include` / `<` resolution already files these edges; kept so the read
63
+ # states its own dependency.
64
+ chain.record(scope) if Analysis::DependencyRecorder.active?
65
+ ancestors = chain.entries.filter_map do |entry|
66
+ # An external entry has no name, so `filter_map` drops it. The side check is a guard: an instance chain
67
+ # holds only instance-side entries.
68
+ entry.name if entry.side == :instance && entry.name != class_name
69
+ end
70
+ [class_name, *ancestors.uniq]
71
+ rescue StandardError
72
+ [class_name]
73
+ end
74
+
75
+ # The union of what the owners contribute to `ivar`, or nil for nothing.
76
+ def contributed(owners, class_name, ivar, table, writes, path)
77
+ types = []
78
+ owners.each do |owner|
79
+ if Analysis::DependencyRecorder.active?
80
+ Analysis::DependencyRecorder.read_name(:own_write_type, "#{owner}##{ivar}")
81
+ end
82
+ by_path = table.dig(owner, ivar) || EMPTY
83
+ # This file's census of an ancestor counts only an ivar an instance body of it writes here: the census
84
+ # also holds a `def self.` write of a module (#1958), which runs on the module object.
85
+ own = writes.dig(owner, ivar) if owner != class_name && by_path.key?(path)
86
+ types << own if own
87
+ by_path.each do |value_path, values|
88
+ next if value_path == path
89
+
90
+ values.each { |value| types << value_type(value) }
91
+ end
92
+ end
93
+ types.empty? ? nil : Type::Combinator.union(*types)
94
+ end
95
+
96
+ # A String literal joins as `String`, not its `Constant`: a write elsewhere cannot see an in-place mutation of
97
+ # the String (`@s << "abc"`) in this file, in another or in an ancestor, so the value would be pinned too
98
+ # narrowly. Every other literal kind is immutable and joins as its `Constant`.
99
+ def value_type(value)
100
+ return Type::Combinator.untyped unless value.is_a?(Array)
101
+
102
+ literal = value[1]
103
+ literal.is_a?(String) ? Type::Combinator.nominal_of("String") : Type::Combinator.constant_of(literal)
104
+ end
105
+
106
+ # `path` resolved through symlinks, so a file the pre-pass tagged under one spelling (`/tmp/…`) is the one
107
+ # analysed under another (`/private/tmp/…`); the expanded spelling when it cannot be resolved.
108
+ def canonical(path)
109
+ File.realpath(path)
110
+ rescue SystemCallError
111
+ File.expand_path(path)
112
+ end
113
+
114
+ # `{ owner => { ivar => { path => [value, ...] } } }` over `census`'s path-tagged `:own_write_type` entries,
115
+ # built once per census and kept in one run-scoped slot keyed by its identity.
116
+ def table(census)
117
+ slot = Thread.current[TABLE_KEY]
118
+ return slot[1] if slot && slot[0].equal?(census)
119
+
120
+ table = {}
121
+ canonical_paths = {}
122
+ census.each do |entry|
123
+ next unless entry.size == 5 && entry[0] == :own_write_type
124
+
125
+ by_path = ((table[entry[1]] ||= {})[entry[2]] ||= {})
126
+ (by_path[canonical_paths[entry[4]] ||= canonical(entry[4])] ||= []) << entry[3]
127
+ end
128
+ Thread.current[TABLE_KEY] = [census, table]
129
+ table
130
+ end
131
+ private_class_method :chain_owners, :contributed, :value_type, :canonical, :table
132
+ end
133
+ end
134
+ end
135
+ end