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
@@ -56,8 +56,7 @@ module Rigor
56
56
  end
57
57
 
58
58
  # @param root — the local-variable scope to scan.
59
- # @param layout_lookup — a `String -> Array[Symbol] | nil` resolver mapping a constant receiver name to its
60
- # struct member list.
59
+ # @param layout_lookup — a resolver mapping a constant receiver name to its struct member list.
61
60
  # @return the fold-safe local names.
62
61
  def fold_safe_locals(root, layout_lookup)
63
62
  return EMPTY if root.nil?
@@ -283,8 +282,7 @@ module Rigor
283
282
  # `[1].each { s.x = v }; s.x` would read the construction value — verified unsound on #525.
284
283
  #
285
284
  # `for` shares the enclosing locals rather than opening a scope, so the block rationale does not apply
286
- # to it; it is a boundary for the same reason `while` is (below), and this comment previously gave the
287
- # wrong reason for it.
285
+ # to it; it is a boundary for the same reason `while` is (below).
288
286
  #
289
287
  # `while` / `until` are boundaries because a single static pass cannot model a member setter across
290
288
  # ITERATIONS. Issue #597 tried to lift this: `eval_loop` does join the body's exit scope with the
@@ -327,8 +325,8 @@ module Rigor
327
325
  return nil unless meta_constant?(call_node.receiver, :Struct)
328
326
 
329
327
  args = call_node.arguments&.arguments || []
330
- # `[0..-2]` cannot return nil for a start of 0, but `Array#[](Range) -> Array[T]?` says it can;
331
- # the `|| []` keeps the read well-typed on that worst case.
328
+ # `[0..-2]` cannot return nil for a start of 0, but the RBS signature of `Array#[]` with a Range says it
329
+ # can; the `|| []` keeps the read well-typed on that worst case.
332
330
  positional = args.last.is_a?(Prism::KeywordHashNode) ? (args[0..-2] || []) : args
333
331
  positional = positional[1..] if positional.first.is_a?(Prism::StringNode)
334
332
  return nil if positional.nil? || positional.empty?
@@ -64,11 +64,10 @@ module Rigor
64
64
  registries = collect_trait_registries(plugin_registry)
65
65
  nested_templates = collect_nested_class_templates(plugin_registry)
66
66
  return SyntheticMethodIndex::EMPTY if templates.empty? && registries.empty? && nested_templates.empty?
67
- # Tier B alone cannot emit without an environment: every trait entry — the direct route and the
68
- # concern-re-targeted one — funnels through `module_instance_method_names`, whose first line answers `[]`
69
- # for a nil environment. The production pre-pass hands Tier B a (lazy) environment since #476, so this
70
- # gate now guards the direct callers that pass none (a spec, a probe): a registry-only scan with
71
- # no environment would otherwise pay a whole-project parse to build a provably empty index. It mirrors
67
+ # Tier B alone cannot emit without an environment: every trait entry funnels through
68
+ # `module_instance_method_names`, which answers `[]` for a nil environment. The production pre-pass always
69
+ # passes one (#476), so this gate guards the direct callers that pass none (a spec, a probe): a
70
+ # registry-only scan would otherwise pay a whole-project parse to build a provably empty index. It mirrors
72
71
  # that nil guard and goes with it.
73
72
  return SyntheticMethodIndex::EMPTY if environment.nil? && templates.empty? && nested_templates.empty?
74
73
 
@@ -534,7 +533,7 @@ module Rigor
534
533
  # (resolved through `registry.modules_by_symbol`; unknown
535
534
  # symbols silently skipped per design decision (2)).
536
535
  #
537
- # Returns an Array<String> of module names in `always_included` order followed by argument order.
536
+ # Returns the module names: the `always_included` ones first, then those for the symbol arguments in order.
538
537
  def resolve_trait_modules(registry, call_node)
539
538
  modules = registry.always_included.dup
540
539
  positional_symbols(call_node, registry).each do |symbol|
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../run_scoped_slots"
4
+ require_relative "memo_audit"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # Issue #2515 — whether the `<toplevel>` def table's answer for a name IS the method a receiverless call of that
9
+ # name runs, so the callee mutation floor may read the def as written. {.reliable?} is false (the floor reads every
10
+ # argument as written, `StatementEvaluator::FLOOR_EVERY`) when any of these holds for `name`:
11
+ #
12
+ # - {Scope#toplevel_def_ambiguity} names a reason (the slot is contested, several ownerless definition rows of the
13
+ # name exist, a `def self.name` defines it on `main`, an `alias` introduces it, an anonymous class body defines
14
+ # it, `Object` defines it or has a module prepended);
15
+ # - a `pre_eval:` patch defines `name` on `Object`, `Kernel` or `BasicObject`.
16
+ #
17
+ # A visibility toggle (`private :name`, `public :name`) does not change which def answers a receiverless call, and
18
+ # `method_missing` on `Object` only answers a name nothing defines, so neither is a hazard.
19
+ #
20
+ # The answer is a function of the discovery index and the name (the environment is the run's), so it is memoised
21
+ # per index. A recorder is told the `defines:<name>` name edge on every call, hit or not
22
+ # ({Scope#toplevel_def_ambiguity}), so a file that starts or stops defining the name re-checks the caller; the memo
23
+ # is bypassed while one is active, as the callee floor's other memos are.
24
+ module ToplevelDefCensus
25
+ OBJECT_OWNERS = %w[Object Kernel BasicObject].freeze
26
+ RELIABLE_KEY = RunScopedSlots.register(:rigor_toplevel_def_reliable)
27
+ private_constant :OBJECT_OWNERS, :RELIABLE_KEY
28
+
29
+ module_function
30
+
31
+ # True when the `<toplevel>` table's def for `name` is the method a receiverless call of `name` runs.
32
+ def reliable?(scope, name)
33
+ return compute?(scope, name) if Analysis::DependencyRecorder.active? || MemoAudit.suspended?
34
+
35
+ table = (Thread.current[RELIABLE_KEY] ||= ObjectSpace::WeakKeyMap.new)
36
+ by_name = (table[scope.discovery] ||= {})
37
+ return by_name[name] = compute?(scope, name) unless by_name.key?(name)
38
+
39
+ MemoAudit.check("top-level def census", by_name[name], name) { compute?(scope, name) }
40
+ end
41
+
42
+ def compute?(scope, name)
43
+ return false unless scope.toplevel_def_ambiguity(name).nil?
44
+
45
+ !patched?(scope, name)
46
+ end
47
+
48
+ # True when a `pre_eval:` patch defines `name` on an `Object`-level class.
49
+ def patched?(scope, name)
50
+ patched = scope.environment&.project_patched_methods
51
+ return false if patched.nil?
52
+
53
+ patched.by_key.each_key.any? do |(owner, method_name, _kind)|
54
+ method_name == name && OBJECT_OWNERS.include?(owner)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -29,9 +29,10 @@ module Rigor
29
29
  # written in; a refinement seed (`non-empty-array[0]`) still can, since its remover does not close a literal.
30
30
  #
31
31
  # A site whose widening declines leaves the binding as it is, exactly as the straight-line seam does: a
32
- # precise nominal (a declared or inferred `Array[String]` is a claim this seam may not grow), a receiver
33
- # that is no carrier, or a name the mutator tables do not list for that carrier (`store` on a `Tuple`, a method
34
- # its class does not define). The caller MUST therefore not read an unchanged binding as describing later
32
+ # precise nominal under a site that stores nothing it can see (a declared or inferred `Array[String]` is a claim
33
+ # this seam may not grow; an element rewriter such as `map!` replaces it with the gradual arm, issue #2427), a
34
+ # receiver that is no carrier, or a name the mutator tables do not list for that carrier (`store` on a `Tuple`, a
35
+ # method its class does not define). The caller MUST therefore not read an unchanged binding as describing later
35
36
  # iterations. An empty-witness refinement under a storing mutator that keeps the witness is not such a decline:
36
37
  # the straight-line widening grows it (issue #936), and gives a site whose arguments describe nothing it stores
37
38
  # (`non-empty-array[String]` under `map!`) the gradual arm itself ({RewriteMutation}),
@@ -64,10 +65,11 @@ module Rigor
64
65
  CalleeStore = Data.define(:call, :arguments)
65
66
 
66
67
  COLLECTION_CLASSES = %w[Array Hash].freeze
68
+ CONTAINER_CLASSES = %w[Array Hash Set].freeze
67
69
  FLOORED_CLASSES = %w[Array Hash String].freeze
68
70
 
69
71
  NO_ARG_NODES = [].freeze
70
- private_constant :COLLECTION_CLASSES, :FLOORED_CLASSES, :NO_ARG_NODES
72
+ private_constant :COLLECTION_CLASSES, :CONTAINER_CLASSES, :FLOORED_CLASSES, :NO_ARG_NODES
71
73
 
72
74
  module_function
73
75
 
@@ -99,6 +101,103 @@ module Rigor
99
101
  VALUE_PRESERVING.include?(method_name) && !literal_carrier?(type) ? widened : gradual_content(widened)
100
102
  end
101
103
 
104
+ # Issue #2296 — what the caller's argument `type` can hold after a callee that mutates the parameter inside a
105
+ # block (`def poke(acc) = 2.times { |i| acc[i] = "x" }`). Nothing at the call says what the block stores, so
106
+ # each carrier takes one unknown store and keeps its element type beside the gradual arm, as a block-captured
107
+ # binding does ({.widen}): `[7, 9]` reads `Array[Dynamic[top] | Integer]`, `{ a: 1 }`
108
+ # `Hash[Dynamic[top] | Symbol, Dynamic[top] | Integer]`, `Set[1]` `Set[Dynamic[top] | Integer]`, a literal
109
+ # String `String`. Not {.content_floor}'s bare carrier: dropping the element type there widened
110
+ # `ScopeIndexer.finalize_constant_writes` to `Hash[untyped, untyped]` under sig-gen (#2295). A class-level
111
+ # nominal (`Hash[String, Set[String]]`) is a claim this seam may not grow, and is left alone; a value-pinned one
112
+ # (`Hash["a" | "b", 1]`) holds a first-store answer the unknown store falsifies, and takes the gradual arm, as a
113
+ # block's unknown-store binding does.
114
+ def callee_block_floor(type)
115
+ widened = widen_store(type, :[]=, unknown_arguments(2)) || type
116
+ widened = callee_set_floor(widened) || widened
117
+ dynamic_elements(value_pinned_collection?(widened) ? gradual_content(widened) : widened)
118
+ end
119
+
120
+ # Issue #2296 — `type` after a callee that grows the parameter through a Set-only adder (`s.add(x)`,
121
+ # `s.merge(xs)`): a Set carrier takes one unknown store, and anything else, a Hash under `merge` included, is
122
+ # returned as nil (unchanged).
123
+ def callee_set_floor(type)
124
+ widened = widen_store(type, :add, unknown_arguments(1))
125
+ widened && dynamic_elements(widened)
126
+ end
127
+
128
+ # Issue #2647 — `type` with each container element the unknown store widened (`Dynamic[top] | String`, which an
129
+ # element read back out fails a literal-typed parameter with, although the callee stored a value the call never
130
+ # shows) made a gradual `Dynamic[String]`: the floor may forget a fact, never claim the nominal class. An element
131
+ # that carries no `Dynamic[top]` arm (a class-level `Array[Integer]`, which the floor leaves alone) is returned as
132
+ # it is. Scoped to the callee floor's two entry points, so a local mutation's unknown-store widening keeps its
133
+ # answer.
134
+ def dynamic_elements(type)
135
+ return type unless needs_dynamic?(type)
136
+
137
+ case type
138
+ when Type::Union then Type::Combinator.union(*type.members.map { |member| dynamic_elements(member) })
139
+ when Type::Difference then Type::Combinator.difference(dynamic_elements(type.base), type.removed)
140
+ when Type::Nominal
141
+ return type unless CONTAINER_CLASSES.include?(type.class_name) && !type.type_args.empty?
142
+
143
+ Type::Combinator.nominal_of(type.class_name, type_args: type.type_args.map { |arg| dynamic_element(arg) })
144
+ else type
145
+ end
146
+ end
147
+
148
+ # True when {.dynamic_elements} would change `type`: a container element with a `Dynamic[top]` member and a class
149
+ # member (neither a literal nor `Dynamic[top]`). Reads with `any?` only, so the common unchanged type is not
150
+ # rebuilt.
151
+ def needs_dynamic?(type)
152
+ case type
153
+ when Type::Union then type.members.any? { |member| needs_dynamic?(member) }
154
+ when Type::Difference then needs_dynamic?(type.base)
155
+ when Type::Nominal
156
+ CONTAINER_CLASSES.include?(type.class_name) && type.type_args.any? { |arg| needs_dynamic_element?(arg) }
157
+ else false
158
+ end
159
+ end
160
+
161
+ def needs_dynamic_element?(arg)
162
+ return false unless arg.is_a?(Type::Union)
163
+
164
+ arg.members.any? { |member| gradual_top?(member) } &&
165
+ arg.members.any? { |member| !member.is_a?(Type::Constant) && !gradual_top?(member) }
166
+ end
167
+
168
+ def gradual_top?(member) = member.is_a?(Type::Dynamic) && member.static_facet.is_a?(Type::Top)
169
+
170
+ # `Dynamic[top] | A | B` as `Dynamic[A | B]` for the class members; a literal member (a value-pinned key) and any
171
+ # other element stay as they are.
172
+ def dynamic_element(arg)
173
+ members = arg.is_a?(Type::Union) ? arg.members : [arg]
174
+ gradual, known = members.partition { |member| gradual_top?(member) }
175
+ return arg if gradual.empty?
176
+
177
+ literals, classes = known.partition { |member| member.is_a?(Type::Constant) }
178
+ return arg if classes.empty?
179
+
180
+ Type::Combinator.union(*literals, Type::Combinator.dynamic(Type::Combinator.union(*classes)))
181
+ end
182
+
183
+ def unknown_arguments(count) = Array.new(count) { Type::Combinator.untyped }
184
+
185
+ # False only when {.callee_block_floor} and {.callee_set_floor} are sure to leave `type` as it is: a type that is
186
+ # no String, Array, Hash or Set carrier in any form. A cheap gate, so a call passing only such locals skips the
187
+ # census closures those floors read.
188
+ def callee_floorable?(type)
189
+ case type
190
+ when Type::Union then type.members.any? { |member| callee_floorable?(member) }
191
+ when Type::Tuple, Type::HashShape, Type::Difference, Type::Refined, Type::Intersection then true
192
+ when Type::Constant then type.value.is_a?(String) || type.value.is_a?(::Set)
193
+ when Type::Nominal then FLOORABLE_NOMINALS.include?(type.class_name)
194
+ else false
195
+ end
196
+ end
197
+
198
+ FLOORABLE_NOMINALS = %w[Array Hash Set String].freeze
199
+ private_constant :FLOORABLE_NOMINALS
200
+
102
201
  # Every `Array` / `Hash` / `String` carrier `type` can be as its bare carrier: `Array[Dynamic[top]]`,
103
202
  # `Hash[Dynamic[top], Dynamic[top]]`, `String`. A literal, a nominal (a precise one included) and a refinement
104
203
  # over one ({.carrier_class}) all floor; so `non-empty-string`, which a callee can empty, reads `String`.
@@ -124,9 +124,8 @@ module Rigor
124
124
 
125
125
  # How a walk position reaches the exit scope: whether a local rebind there is threaded, whether an
126
126
  # instance variable's is, whether a class variable's or global's is, the offset below which a rebind
127
- # precedes a jump,
128
- # whether the position binds targets only (`:multi_assign` under a multi-assign, `:capture` under any
129
- # other binding construct, false otherwise), and the locals a nested block shadows.
127
+ # precedes a jump, whether the position binds targets only (`:multi_assign` under a multi-assign,
128
+ # `:capture` under any other binding construct, false otherwise), and the locals a nested block shadows.
130
129
  Position = Data.define(:local, :ivar, :other, :horizon, :binding, :shadowed)
131
130
  private_constant :Position
132
131
 
@@ -5,6 +5,7 @@ require "prism"
5
5
  require_relative "void_origin"
6
6
  require_relative "../reflection"
7
7
  require_relative "../source/node_children"
8
+ require_relative "../run_scoped_slots"
8
9
 
9
10
  module Rigor
10
11
  module Inference
@@ -62,7 +63,7 @@ module Rigor
62
63
  # graceful-degradation branch is needed. `def_node` identity is stable per run (the ADR-85
63
64
  # {DefNodeResolver} yields one node object per `(path, node_id)`), the same identity the ADR-84
64
65
  # return memo relies on.
65
- MEMO_KEY = :__rigor_void_tail_summary_memo__
66
+ MEMO_KEY = RunScopedSlots.register(:__rigor_void_tail_summary_memo__)
66
67
  private_constant :MEMO_KEY
67
68
 
68
69
  def initialize(scope)
@@ -13,9 +13,9 @@ module Rigor
13
13
  # with no `range`) stays supported — it is still legal under incremental sync and is what `didOpen` and a
14
14
  # client-side resync send.
15
15
  class BufferTable
16
- # @!attribute uri [String] the LSP DocumentUri (e.g. `file:///abs/path/lib/foo.rb`).
17
- # @!attribute bytes [String] the current full text of the buffer.
18
- # @!attribute version [Integer] the monotonically increasing LSP version number.
16
+ # @!attribute uri — the LSP DocumentUri (e.g. `file:///abs/path/lib/foo.rb`).
17
+ # @!attribute bytes — the current full text of the buffer.
18
+ # @!attribute version — the monotonically increasing LSP version number.
19
19
  Entry = Data.define(:uri, :bytes, :version)
20
20
 
21
21
  def initialize
@@ -42,7 +42,7 @@ module Rigor
42
42
  @project_context = project_context
43
43
  end
44
44
 
45
- # @return LSP `CompletionItem[]` or nil
45
+ # @return LSP completion items, or nil
46
46
  # when the cursor isn't at a position the provider can
47
47
  # enumerate completions for. Returning nil maps to
48
48
  # `result: null` per the LSP spec — clients treat it as
@@ -65,19 +65,20 @@ module Rigor
65
65
  line: locate_at[0] + 1, character: locate_at[1] + 1)
66
66
  return nil if node.nil?
67
67
 
68
+ query = ScopeQuery.new(path: path, bytes: bytes_to_parse, index: nil)
68
69
  case node
69
70
  when Prism::CallNode
70
71
  # `hash[:|` patches to `hash[:KEY_SENTINEL]`, an index access call whose argument is the sentinel
71
72
  # symbol; NodeLocator at the sentinel returns the inner CallNode for `[]`. Hash-key completion wins
72
73
  # when the call is an index access on a HashShape carrier; otherwise fall through to method
73
74
  # completion.
74
- hash_key_completion_for(node, parse_result.value, path) ||
75
- method_completion_for(node, parse_result.value, path)
75
+ hash_key_completion_for(node, parse_result.value, query) ||
76
+ method_completion_for(node, parse_result.value, query)
76
77
  when Prism::SymbolNode
77
78
  # The sentinel symbol's NodeLocator hit; walk up via the AST to find the enclosing `[]` call.
78
- hash_key_completion_for_symbol(node, parse_result.value, path)
79
+ hash_key_completion_for_symbol(node, parse_result.value, query)
79
80
  when Prism::ConstantPathNode
80
- constant_path_completion_for(node, path)
81
+ constant_path_completion_for(node)
81
82
  end
82
83
  end
83
84
 
@@ -141,11 +142,11 @@ module Rigor
141
142
  nil
142
143
  end
143
144
 
144
- def method_completion_for(call_node, root, path)
145
+ def method_completion_for(call_node, root, query)
145
146
  receiver_node = call_node.receiver
146
147
  return nil if receiver_node.nil? # implicit-self — slice-3 territory of completion
147
148
 
148
- index = build_scope_index(root, path)
149
+ index = build_scope_index(root, query)
149
150
  receiver_scope = index[receiver_node]
150
151
  receiver_type = receiver_scope.type_of(receiver_node)
151
152
  method_completions(receiver_type, receiver_scope)
@@ -155,13 +156,13 @@ module Rigor
155
156
  # `[]` call AND the receiver's inferred type is a `Type::HashShape`. Returns nil for any other receiver
156
157
  # carrier so the dispatcher falls through to method completion (which still surfaces Hash's methods for
157
158
  # genuine method calls on a hash receiver).
158
- def hash_key_completion_for(call_node, root, path)
159
+ def hash_key_completion_for(call_node, root, query)
159
160
  return nil unless call_node.name == :[]
160
161
 
161
162
  receiver_node = call_node.receiver
162
163
  return nil if receiver_node.nil?
163
164
 
164
- index = build_scope_index(root, path)
165
+ index = build_scope_index(root, query)
165
166
  receiver_type = index[receiver_node].type_of(receiver_node)
166
167
  return nil unless receiver_type.is_a?(Type::HashShape)
167
168
 
@@ -171,11 +172,11 @@ module Rigor
171
172
  # When NodeLocator returns the sentinel SymbolNode directly (rather than the enclosing CallNode), walk
172
173
  # up the AST for the smallest `[]` call containing the symbol's location. Re-walks the root once; cheap
173
174
  # for LSP-sized buffers.
174
- def hash_key_completion_for_symbol(symbol_node, root, path)
175
+ def hash_key_completion_for_symbol(symbol_node, root, query)
175
176
  call_node = enclosing_index_call(root, symbol_node)
176
177
  return nil if call_node.nil?
177
178
 
178
- hash_key_completion_for(call_node, root, path)
179
+ hash_key_completion_for(call_node, root, query)
179
180
  end
180
181
 
181
182
  def enclosing_index_call(root, symbol_node)
@@ -212,11 +213,11 @@ module Rigor
212
213
  # resolves to a class / module FQN; we enumerate every known class whose name is an immediate child of
213
214
  # that parent. Top-level constants (`::Foo`) and parent-less paths are not yet supported (queued for
214
215
  # slice 6 follow-up).
215
- def constant_path_completion_for(const_path_node, path)
216
+ def constant_path_completion_for(const_path_node)
216
217
  parent_fqn = parent_fqn_of(const_path_node)
217
218
  return nil if parent_fqn.nil?
218
219
 
219
- scope = base_scope(path)
220
+ scope = Scope.empty(environment: @project_context.environment) # reads only the environment
220
221
  children = enumerate_constant_children(parent_fqn, scope)
221
222
  return nil if children.empty?
222
223
 
@@ -277,8 +278,14 @@ module Rigor
277
278
  }
278
279
  end
279
280
 
280
- def base_scope(_path)
281
- Scope.empty(environment: @project_context.environment)
281
+ # The (path, bytes) a scope query is seeded from: the bytes the parse saw, sentinel splice included. `index`
282
+ # memoises the request's scope index, so a `hash[...]` request that falls through from hash-key to method
283
+ # completion builds it once.
284
+ ScopeQuery = Struct.new(:path, :bytes, :index)
285
+ private_constant :ScopeQuery
286
+
287
+ def seeded_scope(query)
288
+ @project_context.seeded_scope_for(path: query.path, bytes: query.bytes)
282
289
  end
283
290
 
284
291
  def locate_node(source:, root:, line:, character:)
@@ -287,12 +294,11 @@ module Rigor
287
294
  nil
288
295
  end
289
296
 
290
- def build_scope_index(root, _path)
291
- scope = Scope.empty(environment: @project_context.environment)
292
- Inference::ScopeIndexer.index(root, default_scope: scope)
297
+ def build_scope_index(root, query)
298
+ query.index ||= Inference::ScopeIndexer.index(root, default_scope: seeded_scope(query))
293
299
  end
294
300
 
295
- # Returns an Array<Hash> of LSP `CompletionItem`s for every public method callable on the receiver.
301
+ # Returns the LSP completion items for every public method callable on the receiver.
296
302
  # Slice B3 extends the slice-B1 floor with:
297
303
  # - `Type::Refined` / `Type::Difference` — enumerate the underlying nominal (refinement narrows the
298
304
  # value set, not the method set).
@@ -312,7 +318,7 @@ module Rigor
312
318
  end
313
319
  end
314
320
 
315
- # Returns `[{Symbol => RBS::Definition::Method}, :instance | :singleton]` for the receiver. Composite
321
+ # Returns the receiver's method table with its dispatch kind (`:instance` or `:singleton`). Composite
316
322
  # carriers (Union / Intersection / Refined / shape carriers) reduce to instance-method enumeration; the
317
323
  # receiver-class label that lands in each CompletionItem's `detail` still comes from each method's own
318
324
  # `defs.first.implemented_in`, so the rendered prefix stays accurate per-method.
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module LanguageServer
5
+ # Issue #2231 — shows each {ConfigAudit} warning the editor has not seen yet as a `window/showMessage`
6
+ # (`MessageType.Warning`), worded as `rigor check` prints it on STDERR. {ProjectContext#unannounced_config_warnings}
7
+ # decides what is new, so a call after a reload that changed nothing writes nothing. Called on `initialized` and
8
+ # after each save round's configuration reload (`DiagnosticPublisher`).
9
+ class ConfigWarningAnnouncer
10
+ # LSP `MessageType.Warning`, the `type` of a `window/showMessage` notification.
11
+ MESSAGE_TYPE_WARNING = 2
12
+
13
+ def initialize(writer:, project_context:)
14
+ @writer = writer
15
+ @project_context = project_context
16
+ end
17
+
18
+ def announce
19
+ return unless @project_context.respond_to?(:unannounced_config_warnings)
20
+
21
+ @project_context.unannounced_config_warnings.each do |warning|
22
+ @writer.write(method: "window/showMessage",
23
+ params: { type: MESSAGE_TYPE_WARNING, message: "rigor: #{warning.message}" })
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -4,6 +4,7 @@ require "tempfile"
4
4
 
5
5
  require_relative "uri"
6
6
  require_relative "publish_batcher"
7
+ require_relative "config_warning_announcer"
7
8
  require_relative "../analysis/runner"
8
9
  require_relative "../analysis/buffer_binding"
9
10
 
@@ -19,7 +20,7 @@ module Rigor
19
20
  # buffers' debounce timers elapse around the same moment, the `PublishBatcher` coalesces them into one
20
21
  # `#publish_many` round dispatched across the fork-based worker pool (issue #142) instead of N
21
22
  # independent, GVL-serialized `Runner` calls.
22
- class DiagnosticPublisher
23
+ class DiagnosticPublisher # rubocop:disable Metrics/ClassLength
23
24
  # Maps Rigor severity symbols to LSP DiagnosticSeverity
24
25
  # integers per spec § "Diagnostic":
25
26
  # 1 = Error, 2 = Warning, 3 = Information, 4 = Hint.
@@ -51,6 +52,7 @@ module Rigor
51
52
  @project_context = project_context
52
53
  @debouncer = debouncer
53
54
  @debounce_seconds = debounce_seconds
55
+ @config_warning_announcer = ConfigWarningAnnouncer.new(writer: writer, project_context: project_context)
54
56
  @round_lock = Mutex.new
55
57
  @round_running = false
56
58
  @round_pending = false
@@ -130,6 +132,9 @@ module Rigor
130
132
  notify(uri, [])
131
133
  end
132
134
 
135
+ # Issue #2231 — shows the configuration warnings the editor has not seen yet ({ConfigWarningAnnouncer}).
136
+ def announce_config_warnings = @config_warning_announcer.announce
137
+
133
138
  # Cancels every in-flight debounced task. Called from `Server#handle_shutdown` so pending publishes
134
139
  # don't fire against a closed STDOUT.
135
140
  def cancel_pending
@@ -167,8 +172,7 @@ module Rigor
167
172
 
168
173
  # True when a save arrived mid-round and the loop should run once more. Releases ownership otherwise.
169
174
  # `next`, not `break`: inside `Mutex#synchronize`'s block, `break` returns from the SYNCHRONIZE call, so
170
- # a `break` written here would leave the caller's `loop` spinning forever — which is exactly what it did
171
- # before this was split out.
175
+ # a `break` here would leave the caller's `loop` spinning forever.
172
176
  def consume_pending
173
177
  @round_lock.synchronize do
174
178
  next(@round_running = false) unless @round_pending
@@ -185,6 +189,7 @@ module Rigor
185
189
  def execute_project_round(saved_uri)
186
190
  generation = @project_context.generation
187
191
  diagnostics = @project_context.project_diagnostics
192
+ announce_config_warnings # the round re-read the configuration; the reload stands even if the result does not
188
193
  return if generation != @project_context.generation
189
194
 
190
195
  by_path = diagnostics.group_by(&:path)
@@ -30,7 +30,7 @@ module Rigor
30
30
  @project_context = project_context
31
31
  end
32
32
 
33
- # @return LSP `DocumentSymbol[]` for the
33
+ # @return LSP document symbols for the
34
34
  # buffer at `uri`. Returns nil when the URI isn't open or
35
35
  # doesn't parse cleanly enough to surface symbols — LSP
36
36
  # clients fall back to no-outline in that case.
@@ -23,8 +23,7 @@ module Rigor
23
23
  @project_context = project_context
24
24
  end
25
25
 
26
- # @return LSP `FoldingRange[]` for the
27
- # buffer, or nil when the URI isn't open / parseable.
26
+ # @return LSP folding ranges for the buffer, or nil when the URI isn't open / parseable.
28
27
  def provide(uri)
29
28
  path, entry = buffer_for(uri)
30
29
  return nil if entry.nil?
@@ -44,7 +44,7 @@ module Rigor
44
44
  node = locate_node(source: entry.bytes, root: parse_result.value, line: line + 1, character: character + 1)
45
45
  return nil if node.nil?
46
46
 
47
- scope = base_scope(path)
47
+ scope = @project_context.seeded_scope_for(path: path, bytes: entry.bytes)
48
48
  index = Inference::ScopeIndexer.index(parse_result.value, default_scope: scope)
49
49
  node_scope = index[node]
50
50
  type = node_scope.type_of(node)
@@ -59,12 +59,6 @@ module Rigor
59
59
  rescue Source::NodeLocator::OutOfRangeError
60
60
  nil
61
61
  end
62
-
63
- def base_scope(_path)
64
- # Pulls the Environment from the cached ProjectContext so each hover avoids repeating the RBS-load
65
- # build.
66
- Scope.empty(environment: @project_context.environment)
67
- end
68
62
  end
69
63
  end
70
64
  end
@@ -60,9 +60,8 @@ module Rigor
60
60
  end
61
61
 
62
62
  # Renders a `CallNode` hover when the receiver's type can be mapped to a known RBS class and the method
63
- # is declared there. Returns nil for shapes the slice-A1 floor doesn't handle (implicit `self`, Union /
64
- # Refined / Shape receivers — those land in slices A2-A4 and slice 7); the caller falls back to the
65
- # default body.
63
+ # is declared there. Returns nil for shapes this renderer does not handle (implicit `self`, Union /
64
+ # Refined / Shape receivers); the caller falls back to the default body.
66
65
  def render_call(call_node, return_type, node_scope_lookup)
67
66
  receiver_node = call_node.receiver
68
67
  return nil if receiver_node.nil?
@@ -133,17 +132,16 @@ module Rigor
133
132
  body << "#{return_type.describe}\n"
134
133
  body << "```"
135
134
  if (doc = rbs_documentation(definition))
136
- # Close the code fence first so the comment text renders as prose, then a fresh fenced block isn't
137
- # necessary — plain markdown below the code reads better in clients.
135
+ # The fence is already closed, so the RBS comment renders as plain markdown below the code.
138
136
  body << "\n\n---\n\n#{doc}"
139
137
  end
140
138
  body
141
139
  end
142
140
 
143
141
  # Returns the concatenated text of every RBS comment attached to the method definition, or nil when no
144
- # comments exist. `RBS::Definition::Method#comments` is an Array<AST::Comment> with each entry's
145
- # `.string` carrying the raw text (newline-terminated `# foo` lines). Multiple comments are joined with a
146
- # blank line so each upstream `# Foo bar` paragraph is preserved.
142
+ # comments exist. `RBS::Definition::Method#comments` is a list of `AST::Comment`s, each with its raw text
143
+ # in `.string` (newline-terminated `# foo` lines). Multiple comments are joined with a blank line so each
144
+ # upstream `# Foo bar` paragraph is preserved.
147
145
  def rbs_documentation(definition)
148
146
  comments = definition.respond_to?(:comments) ? definition.comments : nil
149
147
  return nil if comments.nil? || comments.empty?