rigortype 0.4.5 → 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 (452) 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 +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../reflection"
4
+ require_relative "../rbs_type_translator"
5
+
6
+ module Rigor
7
+ module Inference
8
+ module ScopeIndexer
9
+ # Issue #1406 — an ivar the class's RBS declares, as `@x: T` or through an `attr_reader x: T` /
10
+ # `attr_writer` / `attr_accessor` member (in `sig/`, or inline `attr_reader :x #: T`), reads the declared type
11
+ # in place of the census's join of the writes. The declaration is the author's contract for the field, so it
12
+ # wins over every write the census saw, #541's untyped `attr_writer` write included; whether the writes
13
+ # honour it is ADR-112 WD5's question (#1075), not this one's. It serves
14
+ # checking only: `rigor sig-gen` indexes with `declared_ivar_types: false` and keeps the census's answer.
15
+ #
16
+ # Only an ivar the census already holds is replaced, so a declaration adds no entry the file's writes did not.
17
+ # A declaration is not taken when it is `untyped` (what rbs-inline supplies for a bare `attr_reader :x`, which
18
+ # the type specification reads as a placeholder) or mentions a type variable (`@x: T` in `class Box[T]` is
19
+ # unsolved at a read), and the census entry stands.
20
+ module DeclaredIvarTypes
21
+ module_function
22
+
23
+ def apply!(accumulator, scope)
24
+ environment = scope&.environment
25
+ return if environment.nil?
26
+
27
+ accumulator.each do |class_name, entries|
28
+ next unless class_name.is_a?(String) # a guard: the side-table keys are deleted before this runs
29
+
30
+ declared = declared_ivars(class_name, environment)
31
+ next if declared.empty?
32
+
33
+ accumulator[class_name] = entries.to_h do |ivar, type|
34
+ [ivar, declared.fetch(ivar, type)]
35
+ end
36
+ end
37
+ end
38
+
39
+ # `{ivar name => Rigor::Type}` for the ivars `class_name`'s RBS instance definition declares with a usable
40
+ # type, or `{}`.
41
+ def declared_ivars(class_name, environment)
42
+ definition = Reflection.instance_definition(class_name, environment: environment)
43
+ return {} if definition.nil?
44
+
45
+ instance = Type::Combinator.nominal_of(class_name)
46
+ definition.instance_variables.each_with_object({}) do |(name, variable), acc|
47
+ type = translate(variable.type, instance, environment)
48
+ acc[name] = type if type
49
+ end
50
+ rescue StandardError # a guard: a definition that fails to build declares nothing
51
+ {}
52
+ end
53
+
54
+ def translate(rbs_type, instance, environment)
55
+ return nil unless rbs_type.free_variables.empty?
56
+
57
+ type = RbsTypeTranslator.translate(rbs_type, self_type: instance, instance_type: instance,
58
+ alias_expander: environment.rbs_loader)
59
+ type.is_a?(Type::Dynamic) ? nil : type
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../reflection"
6
+ require_relative "../method_parameter_binder"
7
+ require_relative "../global_write_census"
8
+ require_relative "unseen_ivar_writers"
9
+ require_relative "derived_param_value"
10
+
11
+ module Rigor
12
+ module Inference
13
+ module ScopeIndexer
14
+ # Issue #1154 — the class-ivar census types an `@x = x` write whose right-hand side reads a parameter of the
15
+ # enclosing instance `def` as that parameter's DECLARED type: the method's RBS signature, from `sig/` or from
16
+ # an rbs-inline `# @rbs` / `#:` annotation, read through {MethodParameterBinder}, the binder the method body's
17
+ # own entry scope uses. Without it the census typed the rvalue with no local bindings, so a constructor field
18
+ # read `Dynamic[top]` in every sibling method even where `sig/` declared `initialize: (String x) -> void`.
19
+ #
20
+ # The census is flow-free, so a write is credited only where the parameter can hold nothing but its
21
+ # argument. {.collect} answers `{write node => declared type}` (an identity Hash) for the writes that pass
22
+ # every gate, and {EMPTY} otherwise; an absent write keeps the census's ordinary rvalue typing, so a declined
23
+ # case reads exactly what it read before:
24
+ #
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;
32
+ # - the write is not inside a block or a lambda, so a block parameter of the same name and a block that runs
33
+ # under another `self` (`instance_eval`, `Class.new`, `define_method`) never borrow the method's parameter;
34
+ # - the method body never writes the parameter, in any form and at any depth (`x = Integer(x)`, `x ||= …`,
35
+ # a `rescue => x` or pattern target);
36
+ # - the owner is a class, not a module (a module's ivar belongs to whichever object includes it), and its
37
+ # RBS declares the method itself (`defined_in`), so an inherited signature never types a redefinition;
38
+ # - the signature has exactly one overload, whose parameter lists are typed (`(?)` declines) and mention no
39
+ # type variable (a class `Box[T]` or a method `[T]` variable is unsolved at the write);
40
+ # - the class's primary RBS declaration is the project's (`sig/` or rbs-inline), not core, stdlib or a gem's:
41
+ # a project reopening of a library class (`Thread::SizedQueue#initialize`) would otherwise borrow the
42
+ # library's parameter type for its own `def`;
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;
45
+ # - nothing outside the class's own census may write the ivar ({UnseenIvarWriters}): no project ancestor, and
46
+ # no other object's code (`o.instance_variable_set(:@x, v)`);
47
+ # - the declared type is not `untyped`, which is what the write already records.
48
+ module DeclaredParamWrites
49
+ EMPTY = {}.compare_by_identity.freeze
50
+
51
+ SCOPE_BARRIERS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
52
+ private_constant :SCOPE_BARRIERS
53
+
54
+ BLOCK_NODES = [Prism::BlockNode, Prism::LambdaNode].freeze
55
+ private_constant :BLOCK_NODES
56
+
57
+ OPAQUE_EVAL_CALLS = %i[eval instance_eval class_eval module_eval instance_exec class_exec
58
+ module_exec].to_set.freeze
59
+ private_constant :OPAQUE_EVAL_CALLS
60
+
61
+ WRITER_SCOPE_KEY = :__rigor_declared_param_writer_scope__
62
+ private_constant :WRITER_SCOPE_KEY
63
+
64
+ module_function
65
+
66
+ # Runs the block with `scope` as the one whose project tables the unseen-writer and single-file gates read, in
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.
69
+ def with_writer_scope(scope)
70
+ return yield if scope.nil?
71
+
72
+ previous = Thread.current[WRITER_SCOPE_KEY]
73
+ Thread.current[WRITER_SCOPE_KEY] = scope
74
+ begin
75
+ yield
76
+ ensure
77
+ Thread.current[WRITER_SCOPE_KEY] = previous
78
+ end
79
+ end
80
+
81
+ def writer_scope(scope) = Thread.current[WRITER_SCOPE_KEY] || scope
82
+
83
+ # `{InstanceVariableWriteNode => Rigor::Type}` for `def_node`'s creditable writes, or {EMPTY}. The caller
84
+ # passes only an instance `def`: a singleton body's `@x` is the class object's.
85
+ def collect(def_node, class_name, scope)
86
+ return EMPTY if def_node.body.nil? || def_node.parameters.nil?
87
+
88
+ written = Set.new
89
+ writes = []
90
+ return EMPTY if scan(def_node.body, written, writes, in_block: false) == :opaque
91
+
92
+ reads = unwritten_reads(writes, written)
93
+ return EMPTY if reads.empty?
94
+
95
+ types = declared_types(def_node, class_name, scope)
96
+ writers = writer_scope(scope)
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)
101
+ acc[write] = type if type && !UnseenIvarWriters.possible?(writers, class_name, write.name)
102
+ end
103
+ credited.empty? ? EMPTY : credited
104
+ rescue StandardError
105
+ EMPTY
106
+ end
107
+
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
147
+ # any form at any depth. A read at depth zero outside a block names the method's own local, which is the
148
+ # parameter when the body never writes the name.
149
+ #
150
+ # Answers `:opaque` when the body can rewrite a local through code the scan cannot read: a `binding` call, or
151
+ # an `eval`-family call given an argument ({OPAQUE_EVAL_CALLS}).
152
+ def scan(node, written, writes, in_block:)
153
+ return unless node.is_a?(Prism::Node)
154
+ return if SCOPE_BARRIERS.any? { |klass| node.is_a?(klass) }
155
+
156
+ case node
157
+ when Prism::LocalVariableWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode,
158
+ Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableTargetNode
159
+ written << node.name
160
+ when Prism::CallNode
161
+ return :opaque if opaque_call?(node)
162
+ end
163
+ writes << node if !in_block && node.is_a?(Prism::InstanceVariableWriteNode)
164
+ inner = in_block || BLOCK_NODES.any? { |klass| node.is_a?(klass) }
165
+ node.rigor_each_child do |child|
166
+ return :opaque if scan(child, written, writes, in_block: inner) == :opaque
167
+ end
168
+ nil
169
+ end
170
+
171
+ # `binding` hands the frame's locals to code that may write them (`binding.eval("x = 's'")`,
172
+ # `Helper.rw(binding)`); a string passed to an `eval`-family call is code the scan cannot read.
173
+ def opaque_call?(node)
174
+ return true if node.name == :binding
175
+
176
+ OPAQUE_EVAL_CALLS.include?(node.name) && !(node.arguments&.arguments || []).empty?
177
+ end
178
+
179
+ # The declared, non-`untyped` type of each parameter, or `{}` when a gate declines the method.
180
+ def declared_types(def_node, class_name, scope)
181
+ environment = scope&.environment
182
+ return {} if environment.nil?
183
+ return {} if environment.rbs_module?(class_name)
184
+ return {} unless Reflection.project_declared_class?(class_name, environment: environment)
185
+
186
+ rbs_method = Reflection.instance_method_definition(class_name, def_node.name, environment: environment)
187
+ return {} unless sole_typed_overload?(rbs_method, class_name)
188
+
189
+ binder = MethodParameterBinder.new(environment: environment, class_path: class_name, singleton: false)
190
+ binder.bind(def_node).reject { |_name, type| type.is_a?(Type::Dynamic) }
191
+ end
192
+
193
+ def sole_typed_overload?(rbs_method, class_name)
194
+ return false if rbs_method.nil? || rbs_method.defined_in.to_s.delete_prefix("::") != class_name
195
+
196
+ method_types = rbs_method.method_types
197
+ return false unless method_types.size == 1
198
+
199
+ function = method_types.first.type
200
+ function.respond_to?(:required_positionals) && function.free_variables.empty?
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -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