rigortype 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -80,6 +80,20 @@ module Rigor
80
80
  type.is_a?(Type::Constant) && type.value.is_a?(String)
81
81
  end
82
82
 
83
+ # A predicate refinement over String (`decimal-int-string`, `lowercase-string`) under a String mutator → its
84
+ # `String` base. Any in-place mutation can falsify the predicate (`s.clear` leaves a `decimal-int-string`
85
+ # empty, `s << "A"` a `lowercase-string` mixed-case), so none keeps it. `nil` for every other input.
86
+ def widen_refined(refined, method_name)
87
+ return nil unless refined?(refined)
88
+ return nil unless MUTATORS.include?(method_name)
89
+
90
+ refined.base
91
+ end
92
+
93
+ def refined?(type)
94
+ type.is_a?(Type::Refined) && type.base.is_a?(Type::Nominal) && type.base.class_name == "String"
95
+ end
96
+
83
97
  # Whether `method_name`, called with `arg_types` (the call's positional argument types, possibly none), can leave
84
98
  # a non-empty receiver empty, and so retract a `non-empty-string` refinement. Every mutator outside
85
99
  # {EMPTY_PRESERVING} can, except a {TRANSLATORS} call whose replacement is provably non-empty.
@@ -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
 
@@ -324,21 +323,29 @@ module Rigor
324
323
  # - `class_methods do ... end` blocks are NOT yet handled —
325
324
  # singleton-level emission is out of scope per the slice-3
326
325
  # floor framing.
326
+ #
327
+ # Issue #3005 — the `included do` blocks of every body of a module some body extends with Concern are read: a
328
+ # reopening that does not repeat the `extend` runs its block on a module that is already a concern (loaded
329
+ # first, its `included do` raises, so no running program has the includer without the calls). A module no
330
+ # body extends with Concern contributes nothing.
327
331
  CONCERN_NAME = "ActiveSupport::Concern"
328
332
 
329
333
  def build_concern_index(asts)
330
- index = {}
334
+ bodies = Hash.new { |hash, name| hash[name] = [] }
331
335
  asts.each_value do |ast|
332
336
  next if ast.nil?
333
337
 
334
338
  walk_module_decls(ast, []) do |module_name, body|
335
- next if module_name.nil? || body.nil?
336
- next unless concern_module_body?(body)
337
-
338
- deferred_calls = collect_included_do_calls(body)
339
- index[module_name] = deferred_calls.freeze if deferred_calls.any?
339
+ bodies[module_name] << body unless module_name.nil? || body.nil?
340
340
  end
341
341
  end
342
+ index = {}
343
+ bodies.each do |module_name, list|
344
+ next unless list.any? { |body| concern_module_body?(body) }
345
+
346
+ deferred_calls = list.flat_map { |body| collect_included_do_calls(body) }
347
+ index[module_name] = deferred_calls.freeze if deferred_calls.any?
348
+ end
342
349
  index.freeze
343
350
  end
344
351
 
@@ -360,18 +367,34 @@ module Rigor
360
367
  end
361
368
 
362
369
  # Recognises a module body that begins with (or contains at top level) an `extend ActiveSupport::Concern`
363
- # statement.
370
+ # statement. Issue #3005 — `self.extend` and a receiverless or `self.` `send` / `public_send` /
371
+ # `__send__(:extend, …)` are the same call.
364
372
  def concern_module_body?(body)
365
373
  return false unless body.respond_to?(:body)
366
374
 
367
375
  body.body.any? do |stmt|
368
- next false unless stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? && stmt.name == :extend
369
-
370
- args = stmt.arguments&.arguments || []
371
- args.any? { |arg| const_name_string(arg) == CONCERN_NAME }
376
+ args = concern_extend_arguments(stmt)
377
+ args&.any? { |arg| const_name_string(arg) == CONCERN_NAME }
372
378
  end
373
379
  end
374
380
 
381
+ CONCERN_SEND_CALLS = %i[send public_send __send__].freeze
382
+ private_constant :CONCERN_SEND_CALLS
383
+
384
+ # The modules an `extend` statement on the module's own `self` names, or nil for any other statement.
385
+ def concern_extend_arguments(stmt)
386
+ return nil unless stmt.is_a?(Prism::CallNode)
387
+ return nil unless stmt.receiver.nil? || stmt.receiver.is_a?(Prism::SelfNode)
388
+
389
+ args = stmt.arguments&.arguments || []
390
+ return args if stmt.name == :extend
391
+ return nil unless CONCERN_SEND_CALLS.include?(stmt.name)
392
+
393
+ name = args.first
394
+ sent = name.unescaped if name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
395
+ sent == "extend" ? args.drop(1) : nil
396
+ end
397
+
375
398
  def collect_included_do_calls(body)
376
399
  body.body.flat_map do |stmt|
377
400
  included_do = stmt.is_a?(Prism::CallNode) && stmt.receiver.nil? && stmt.name == :included
@@ -534,7 +557,7 @@ module Rigor
534
557
  # (resolved through `registry.modules_by_symbol`; unknown
535
558
  # symbols silently skipped per design decision (2)).
536
559
  #
537
- # Returns an Array<String> of module names in `always_included` order followed by argument order.
560
+ # Returns the module names: the `always_included` ones first, then those for the symbol arguments in order.
538
561
  def resolve_trait_modules(registry, call_node)
539
562
  modules = registry.always_included.dup
540
563
  positional_symbols(call_node, registry).each do |symbol|
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../typing_state"
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
+ # ADR-129 WD1 — a run-scoped Memo, per discovery index and name: the answer is a function of both, and
27
+ # `MemoAudit` checks it on a hit.
28
+ RELIABLE = TypingState.memo(
29
+ :toplevel_def_reliable,
30
+ run_scoped: true, audit: "top-level def census",
31
+ reason: "keyed by discovery index, then name; the environment is the run's; audited by MemoAudit"
32
+ )
33
+ private_constant :OBJECT_OWNERS, :RELIABLE
34
+
35
+ module_function
36
+
37
+ # True when the `<toplevel>` table's def for `name` is the method a receiverless call of `name` runs.
38
+ def reliable?(scope, name)
39
+ return compute?(scope, name) if Analysis::DependencyRecorder.active? || MemoAudit.suspended?
40
+
41
+ table = TypingState.current.weak_key_map(RELIABLE)
42
+ by_name = (table[scope.discovery] ||= {})
43
+ unless by_name.key?(name)
44
+ return by_name[name] = MemoAudit.key_audit("top-level def census") { compute?(scope, name) }
45
+ end
46
+
47
+ MemoAudit.check("top-level def census", by_name[name], name) { compute?(scope, name) }
48
+ end
49
+
50
+ def compute?(scope, name)
51
+ return false unless scope.toplevel_def_ambiguity(name).nil?
52
+
53
+ !patched?(scope, name)
54
+ end
55
+
56
+ # True when a `pre_eval:` patch defines `name` on an `Object`-level class.
57
+ def patched?(scope, name)
58
+ patched = scope.environment&.project_patched_methods
59
+ return false if patched.nil?
60
+
61
+ patched.by_key.each_key.any? do |(owner, method_name, _kind)|
62
+ method_name == name && OBJECT_OWNERS.include?(owner)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ 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) = Type::Combinator.untyped?(member)
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
 
@@ -57,7 +57,7 @@ module Rigor
57
57
  end
58
58
 
59
59
  def candidate?(entry_type, joined_type)
60
- entry_type.is_a?(Type::Dynamic) && entry_type.static_facet.is_a?(Type::Top) &&
60
+ Type::Combinator.untyped?(entry_type) &&
61
61
  joined_type.is_a?(Type::Union) && joined_type.members.any?(Type::Dynamic)
62
62
  end
63
63
 
@@ -5,6 +5,8 @@ require "prism"
5
5
  require_relative "void_origin"
6
6
  require_relative "../reflection"
7
7
  require_relative "../source/node_children"
8
+ require_relative "../typing_state"
9
+ require_relative "../memo_audit"
8
10
 
9
11
  module Rigor
10
12
  module Inference
@@ -53,8 +55,8 @@ module Rigor
53
55
  # cycle (nil). Memoising is therefore always sound, and the cycle guard only ever rejects a node that
54
56
  # is still mid-computation on the current chain (never yet memoised).
55
57
  class VoidTailSummary
56
- # Run-scoped memo, the ADR-84 `return_memo_bucket` house pattern: a single retained slot on
57
- # `Thread.current` keyed by the run-generation token's identity, so a re-run in one process (LSP,
58
+ # Run-scoped memo, the ADR-84 `return_memo_bucket` house pattern: a single retained slot in
59
+ # the thread's {TypingState} keyed by the run-generation token's identity, so a re-run in one process (LSP,
58
60
  # ADR-62 warm loop) lands in a fresh bucket and can neither serve stale entries nor accumulate
59
61
  # them. Thread-local ⇒ each fork-pool worker (a separate process) and each Ractor-pool worker (its
60
62
  # own Ractor-local `Thread.current`) builds its own bucket; the bucket hash is created and mutated
@@ -62,8 +64,15 @@ module Rigor
62
64
  # graceful-degradation branch is needed. `def_node` identity is stable per run (the ADR-85
63
65
  # {DefNodeResolver} yields one node object per `(path, node_id)`), the same identity the ADR-84
64
66
  # return memo relies on.
65
- MEMO_KEY = :__rigor_void_tail_summary_memo__
66
- private_constant :MEMO_KEY
67
+ #
68
+ # ADR-129 WD1 — a run-scoped Memo keyed on the token's identity: the bucket holds summaries that are pure
69
+ # functions of the def node they key on within one run.
70
+ MEMO = TypingState.memo(
71
+ :void_tail_summary,
72
+ run_scoped: true, audit: ["void tail origin", "void tail walk"],
73
+ reason: "a bucket of per-def-node summaries, keyed on the run-generation token's identity"
74
+ )
75
+ private_constant :MEMO
67
76
 
68
77
  def initialize(scope)
69
78
  @scope = scope
@@ -81,7 +90,11 @@ module Rigor
81
90
  return nil if def_node.nil?
82
91
 
83
92
  memo = memo_bucket
84
- return memo[def_node] if memo.key?(def_node)
93
+ if memo.key?(def_node)
94
+ return MemoAudit.check("void tail origin", memo[def_node], def_node) do
95
+ walk(def_node, owner, kind, {}.compare_by_identity)
96
+ end
97
+ end
85
98
 
86
99
  walk(def_node, owner, kind, {}.compare_by_identity)
87
100
  end
@@ -97,10 +110,14 @@ module Rigor
97
110
  return nil if visited.key?(def_node)
98
111
 
99
112
  memo = memo_bucket
100
- return memo[def_node] if memo.key?(def_node)
113
+ if memo.key?(def_node)
114
+ return MemoAudit.check("void tail walk", memo[def_node], def_node) do
115
+ walk(def_node, owner, kind, {}.compare_by_identity)
116
+ end
117
+ end
101
118
 
102
119
  visited[def_node] = true
103
- memo[def_node] = compute(def_node, owner, kind, visited)
120
+ memo[def_node] = MemoAudit.key_audit("void tail walk") { compute(def_node, owner, kind, visited) }
104
121
  end
105
122
 
106
123
  def compute(def_node, owner, kind, visited)
@@ -205,11 +222,14 @@ module Rigor
205
222
  end
206
223
 
207
224
  def memo_bucket
225
+ return {}.compare_by_identity if MemoAudit.enabled? && MemoAudit.suspended?
226
+
208
227
  generation = scope.run_generation || scope.discovered_def_nodes
209
- slot = Thread.current[MEMO_KEY]
228
+ state = TypingState.current
229
+ slot = state[MEMO]
210
230
  unless slot && slot[0].equal?(generation)
211
231
  slot = [generation, {}.compare_by_identity]
212
- Thread.current[MEMO_KEY] = slot
232
+ state[MEMO] = slot
213
233
  end
214
234
  slot[1]
215
235
  end
@@ -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