rigortype 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
  4. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  5. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  6. data/docs/handbook/01-getting-started.md +5 -2
  7. data/docs/handbook/11-sig-gen.md +61 -5
  8. data/docs/handbook/appendix-phpstan.md +4 -4
  9. data/docs/manual/02-cli-reference.md +192 -27
  10. data/docs/manual/03-configuration.md +38 -4
  11. data/docs/manual/04-diagnostics.md +1 -1
  12. data/docs/manual/06-baseline.md +6 -4
  13. data/docs/manual/09-editor-integration.md +13 -2
  14. data/docs/manual/10-mcp-server.md +47 -3
  15. data/docs/manual/12-caching.md +9 -1
  16. data/docs/manual/plugins/rigor-actionmailer.md +30 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +49 -11
  18. data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
  19. data/docs/manual/plugins/rigor-devise.md +15 -0
  20. data/docs/manual/plugins/rigor-rspec.md +2 -1
  21. data/lib/rigor/analysis/baseline.rb +24 -27
  22. data/lib/rigor/analysis/buffer_binding.rb +14 -0
  23. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  24. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  25. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
  26. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
  28. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
  29. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
  30. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  31. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
  32. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
  33. data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
  34. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
  35. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  36. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
  37. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  38. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  39. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  40. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  41. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
  42. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  43. data/lib/rigor/analysis/check_rules.rb +217 -52
  44. data/lib/rigor/analysis/crash_signature.rb +2 -2
  45. data/lib/rigor/analysis/dependency_recorder.rb +15 -4
  46. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  47. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  48. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  49. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  50. data/lib/rigor/analysis/edit_replay.rb +154 -0
  51. data/lib/rigor/analysis/fact_store.rb +3 -3
  52. data/lib/rigor/analysis/incremental.rb +37 -0
  53. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
  54. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  55. data/lib/rigor/analysis/incremental_session.rb +202 -43
  56. data/lib/rigor/analysis/path_expansion.rb +41 -0
  57. data/lib/rigor/analysis/require_relative_closure.rb +100 -0
  58. data/lib/rigor/analysis/rule_catalog.rb +7 -3
  59. data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
  60. data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
  61. data/lib/rigor/analysis/run_stats.rb +2 -2
  62. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
  63. data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
  64. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
  65. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
  66. data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
  67. data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
  68. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  70. data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
  71. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  72. data/lib/rigor/analysis/runner.rb +233 -120
  73. data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
  74. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  75. data/lib/rigor/analysis/template_units.rb +9 -1
  76. data/lib/rigor/analysis/worker_session.rb +38 -33
  77. data/lib/rigor/bleeding_edge.rb +3 -6
  78. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  79. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  80. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  81. data/lib/rigor/cache/descriptor.rb +6 -5
  82. data/lib/rigor/cache/incremental_snapshot.rb +60 -18
  83. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  84. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  85. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  86. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  87. data/lib/rigor/cache/rbs_descriptor.rb +54 -3
  88. data/lib/rigor/cache/rbs_environment.rb +4 -1
  89. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  90. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  91. data/lib/rigor/cache/store.rb +11 -10
  92. data/lib/rigor/ci_detector.rb +1 -1
  93. data/lib/rigor/cli/annotate_command.rb +24 -19
  94. data/lib/rigor/cli/baseline_command.rb +145 -55
  95. data/lib/rigor/cli/catalogue.rb +256 -0
  96. data/lib/rigor/cli/check_command.rb +188 -93
  97. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  98. data/lib/rigor/cli/command.rb +15 -1
  99. data/lib/rigor/cli/coverage_command.rb +21 -8
  100. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  101. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  102. data/lib/rigor/cli/coverage_scan.rb +3 -5
  103. data/lib/rigor/cli/diff_command.rb +27 -21
  104. data/lib/rigor/cli/docs_command.rb +1 -2
  105. data/lib/rigor/cli/doctor_command.rb +12 -12
  106. data/lib/rigor/cli/effects_command.rb +50 -40
  107. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  108. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  109. data/lib/rigor/cli/effects_renderer.rb +2 -1
  110. data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
  111. data/lib/rigor/cli/explain_command.rb +18 -8
  112. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  113. data/lib/rigor/cli/json_document.rb +41 -0
  114. data/lib/rigor/cli/lens_command.rb +17 -13
  115. data/lib/rigor/cli/lsp_command.rb +250 -23
  116. data/lib/rigor/cli/mcp_command.rb +14 -8
  117. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  118. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  119. data/lib/rigor/cli/options.rb +60 -4
  120. data/lib/rigor/cli/plugin_command.rb +93 -46
  121. data/lib/rigor/cli/plugins_command.rb +22 -18
  122. data/lib/rigor/cli/plugins_renderer.rb +34 -8
  123. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  124. data/lib/rigor/cli/protection_renderer.rb +2 -1
  125. data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
  126. data/lib/rigor/cli/sig_gen_command.rb +130 -34
  127. data/lib/rigor/cli/skill_command.rb +32 -3
  128. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  129. data/lib/rigor/cli/skill_describe.rb +1 -1
  130. data/lib/rigor/cli/trace_command.rb +14 -9
  131. data/lib/rigor/cli/triage_command.rb +10 -11
  132. data/lib/rigor/cli/triage_renderer.rb +2 -1
  133. data/lib/rigor/cli/type_of_command.rb +18 -12
  134. data/lib/rigor/cli/type_of_project_scope.rb +24 -0
  135. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  136. data/lib/rigor/cli/type_scan_command.rb +14 -9
  137. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  138. data/lib/rigor/cli/unused_command.rb +27 -17
  139. data/lib/rigor/cli/upgrade_command.rb +5 -0
  140. data/lib/rigor/cli.rb +209 -54
  141. data/lib/rigor/config_audit.rb +17 -6
  142. data/lib/rigor/configuration/severity_profile.rb +5 -5
  143. data/lib/rigor/configuration.rb +86 -34
  144. data/lib/rigor/effects/definition_context.rb +0 -2
  145. data/lib/rigor/effects/envelope.rb +1 -1
  146. data/lib/rigor/effects/framework_units.rb +1 -1
  147. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  148. data/lib/rigor/effects/propagator.rb +1 -1
  149. data/lib/rigor/effects/registry.rb +2 -2
  150. data/lib/rigor/effects/snapshot.rb +1 -1
  151. data/lib/rigor/effects/taint_cause.rb +0 -2
  152. data/lib/rigor/effects/unit_scan.rb +9 -13
  153. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
  154. data/lib/rigor/environment/class_registry.rb +3 -3
  155. data/lib/rigor/environment/core_root.rb +77 -0
  156. data/lib/rigor/environment/default_libraries.rb +1 -1
  157. data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
  158. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  159. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  160. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  161. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  162. data/lib/rigor/environment/member_consistency.rb +56 -7
  163. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  164. data/lib/rigor/environment/project_path_gems.rb +124 -0
  165. data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
  166. data/lib/rigor/environment/rbs_loader.rb +592 -70
  167. data/lib/rigor/environment/reflection.rb +7 -7
  168. data/lib/rigor/environment/required_features.rb +136 -32
  169. data/lib/rigor/environment/ruby_library_roots.rb +122 -0
  170. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  171. data/lib/rigor/environment.rb +44 -11
  172. data/lib/rigor/flow_contribution/merger.rb +4 -4
  173. data/lib/rigor/inference/acceptance.rb +93 -32
  174. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  175. data/lib/rigor/inference/block_run_count.rb +178 -0
  176. data/lib/rigor/inference/budget_trace.rb +8 -2
  177. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  178. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  179. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  180. data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
  181. data/lib/rigor/inference/callee_write_census.rb +815 -0
  182. data/lib/rigor/inference/construction_escape.rb +217 -0
  183. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  184. data/lib/rigor/inference/deep_erasure.rb +170 -0
  185. data/lib/rigor/inference/def_handle.rb +1 -1
  186. data/lib/rigor/inference/def_node_resolver.rb +1 -1
  187. data/lib/rigor/inference/def_return_typer.rb +204 -12
  188. data/lib/rigor/inference/definer_resolution.rb +21 -5
  189. data/lib/rigor/inference/definers_index.rb +106 -0
  190. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  191. data/lib/rigor/inference/element_read_widening.rb +49 -3
  192. data/lib/rigor/inference/error_info.rb +17 -3
  193. data/lib/rigor/inference/expression_typer.rb +173 -57
  194. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  195. data/lib/rigor/inference/global_write_census.rb +110 -1
  196. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  197. data/lib/rigor/inference/hkt_body.rb +1 -2
  198. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  199. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  200. data/lib/rigor/inference/hkt_registry.rb +3 -5
  201. data/lib/rigor/inference/indexed_narrowing.rb +4 -7
  202. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  203. data/lib/rigor/inference/ivar_writer_census.rb +246 -0
  204. data/lib/rigor/inference/key_presence_guard.rb +2 -1
  205. data/lib/rigor/inference/loop_value_record.rb +57 -0
  206. data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
  207. data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
  208. data/lib/rigor/inference/match_rebinding.rb +11 -1
  209. data/lib/rigor/inference/memo_audit.rb +55 -0
  210. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  211. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  212. data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
  213. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
  214. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  215. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
  217. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  218. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
  219. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  220. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
  221. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  222. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  223. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
  224. data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
  225. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
  226. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  227. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  228. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  229. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  230. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  231. data/lib/rigor/inference/method_dispatcher.rb +109 -29
  232. data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
  233. data/lib/rigor/inference/module_function_state.rb +23 -1
  234. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  235. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  236. data/lib/rigor/inference/mutation_widening.rb +162 -29
  237. data/lib/rigor/inference/narrowing.rb +113 -38
  238. data/lib/rigor/inference/object_mixins.rb +13 -1
  239. data/lib/rigor/inference/optimistic_origin.rb +1 -1
  240. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  241. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  242. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  243. data/lib/rigor/inference/project_method_ownership.rb +6 -3
  244. data/lib/rigor/inference/project_seed_tables.rb +2 -1
  245. data/lib/rigor/inference/project_yielder.rb +122 -0
  246. data/lib/rigor/inference/receiver_alias.rb +20 -0
  247. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
  248. data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
  249. data/lib/rigor/inference/remover_holder_scan.rb +233 -0
  250. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  251. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  252. data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
  253. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  254. data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
  255. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
  256. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  257. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  258. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
  259. data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
  260. data/lib/rigor/inference/scope_indexer.rb +614 -146
  261. data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
  262. data/lib/rigor/inference/set_mutation.rb +189 -0
  263. data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
  264. data/lib/rigor/inference/statement_evaluator.rb +3036 -278
  265. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  266. data/lib/rigor/inference/synthetic_method_index.rb +23 -0
  267. data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
  268. data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
  269. data/lib/rigor/inference/toplevel_def_census.rb +59 -0
  270. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  271. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  272. data/lib/rigor/inference/void_tail_summary.rb +2 -1
  273. data/lib/rigor/language_server/buffer_table.rb +9 -3
  274. data/lib/rigor/language_server/completion_provider.rb +26 -20
  275. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  276. data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
  277. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  278. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  279. data/lib/rigor/language_server/hover_provider.rb +1 -7
  280. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  281. data/lib/rigor/language_server/project_context.rb +98 -4
  282. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  283. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  284. data/lib/rigor/language_server/server.rb +12 -6
  285. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  286. data/lib/rigor/lens/plugins.rb +1 -1
  287. data/lib/rigor/mcp/server.rb +82 -3
  288. data/lib/rigor/mcp.rb +1 -1
  289. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  290. data/lib/rigor/plugin/base.rb +26 -11
  291. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  292. data/lib/rigor/plugin/io_boundary.rb +1 -1
  293. data/lib/rigor/plugin/loader.rb +25 -8
  294. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  295. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  296. data/lib/rigor/plugin/manifest.rb +38 -16
  297. data/lib/rigor/plugin/registry.rb +41 -3
  298. data/lib/rigor/plugin/services.rb +2 -3
  299. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  300. data/lib/rigor/plugin/trust_policy.rb +2 -2
  301. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  302. data/lib/rigor/plugin.rb +6 -8
  303. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  304. data/lib/rigor/project_environment.rb +16 -2
  305. data/lib/rigor/protection/analysis_guard.rb +1 -1
  306. data/lib/rigor/protection/discovery_seed.rb +96 -2
  307. data/lib/rigor/protection/mutator.rb +1 -1
  308. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  309. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  310. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  311. data/lib/rigor/rbs_extended.rb +2 -3
  312. data/lib/rigor/reflection/constant_ancestors.rb +5 -3
  313. data/lib/rigor/rooted_glob.rb +25 -2
  314. data/lib/rigor/run_scoped_slots.rb +38 -0
  315. data/lib/rigor/runtime/jit.rb +1 -1
  316. data/lib/rigor/scope/discovery_index.rb +19 -6
  317. data/lib/rigor/scope/resolution_chain.rb +3 -1
  318. data/lib/rigor/scope.rb +379 -36
  319. data/lib/rigor/sig_gen/classification.rb +4 -0
  320. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  321. data/lib/rigor/sig_gen/generator.rb +668 -84
  322. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  323. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  324. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  325. data/lib/rigor/sig_gen/method_candidate.rb +23 -4
  326. data/lib/rigor/sig_gen/observation_collector.rb +7 -6
  327. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  328. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  329. data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
  330. data/lib/rigor/sig_gen/renderer.rb +21 -12
  331. data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
  332. data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
  333. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  334. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  335. data/lib/rigor/sig_gen/write_result.rb +23 -12
  336. data/lib/rigor/sig_gen/writer.rb +217 -37
  337. data/lib/rigor/type/acceptance_router.rb +2 -2
  338. data/lib/rigor/type/accepts_result.rb +1 -1
  339. data/lib/rigor/type/app.rb +4 -6
  340. data/lib/rigor/type/combinator.rb +13 -16
  341. data/lib/rigor/type/constant.rb +48 -7
  342. data/lib/rigor/type/hash_shape.rb +26 -40
  343. data/lib/rigor/type/struct_instance.rb +3 -2
  344. data/lib/rigor/type/union.rb +1 -1
  345. data/lib/rigor/type_node/identifier.rb +1 -2
  346. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  347. data/lib/rigor/type_node/string_literal.rb +1 -2
  348. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  349. data/lib/rigor/type_node/union.rb +3 -6
  350. data/lib/rigor/value_semantics.rb +1 -10
  351. data/lib/rigor/version.rb +1 -1
  352. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  353. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  354. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  355. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
  356. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  357. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
  358. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  359. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  360. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  361. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  362. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  363. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
  364. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  365. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
  366. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  367. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  368. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  369. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  370. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
  371. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
  372. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
  373. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  374. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  375. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
  376. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  377. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  378. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  379. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
  380. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
  381. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  382. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
  383. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  384. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  385. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  386. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
  387. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  388. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  389. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  390. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  391. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  392. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  393. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  394. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
  395. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  396. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  397. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  398. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  399. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
  400. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  401. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  402. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  403. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  404. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  405. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
  406. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  407. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  408. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  409. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  410. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  411. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
  412. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  413. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  414. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
  421. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  422. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
  423. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  424. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
  425. data/sig/rigor/analysis/baseline.rbs +0 -2
  426. data/sig/rigor/analysis/fact_store.rbs +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  428. data/sig/rigor/cli/diff_command.rbs +1 -1
  429. data/sig/rigor/cli/explain_command.rbs +1 -1
  430. data/sig/rigor/effects/file_collection.rbs +5 -6
  431. data/sig/rigor/effects/registry.rbs +8 -6
  432. data/sig/rigor/effects/summary.rbs +6 -5
  433. data/sig/rigor/environment.rbs +5 -3
  434. data/sig/rigor/inference/optimistic_origin.rbs +11 -0
  435. data/sig/rigor/inference.rbs +10 -1
  436. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  437. data/sig/rigor/plugin/base.rbs +1 -0
  438. data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
  439. data/sig/rigor/plugin/effect_attribution.rbs +25 -10
  440. data/sig/rigor/plugin/effect_edge.rbs +12 -5
  441. data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
  442. data/sig/rigor/plugin/io_boundary.rbs +6 -5
  443. data/sig/rigor/plugin/manifest.rbs +16 -18
  444. data/sig/rigor/plugin/protocol_contract.rbs +6 -9
  445. data/sig/rigor/plugin/registry.rbs +1 -1
  446. data/sig/rigor/scope.rbs +51 -50
  447. data/sig/rigor/type.rbs +0 -6
  448. data/skills/rigor-plugin-author/SKILL.md +4 -4
  449. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  450. data/skills/rigor-plugin-review/SKILL.md +3 -3
  451. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
  452. metadata +55 -3
@@ -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
@@ -100,7 +102,8 @@ module Rigor
100
102
  publish_batch(eligible)
101
103
  end
102
104
 
103
- # Runs one whole-project save round and publishes to the publish set (#246). Called from `didSave`.
105
+ # Runs one whole-project save round and publishes to the publish set (#246), then re-publishes each dirty buffer
106
+ # from its own bytes (ADR-124). Called from `didSave`.
104
107
  #
105
108
  # Analysis scope is the whole project; the PUBLISH SET is the open buffers that are not dirty, plus the
106
109
  # buffer that was just saved. A dirty buffer is excluded because only its own `didChange` analysis has
@@ -117,12 +120,21 @@ module Rigor
117
120
  end
118
121
  end
119
122
 
123
+ # Re-publishes every open buffer from its own bytes, each through {#publish_for}. Called after the project
124
+ # context is invalidated (a watched file or the configuration changed): every marker on screen was computed
125
+ # against the world that is gone, and since ADR-124 a buffer's markers read other files' declarations, so
126
+ # they can move without the buffer itself changing.
127
+ def republish_open = @buffer_table.uris.each { |uri| publish_for(uri) }
128
+
120
129
  # Publishes an EMPTY diagnostic array for `uri`. The LSP-spec idiom for "clear inline markers" — called
121
130
  # from `didClose` so clients drop stale highlights when the user closes a buffer.
122
131
  def publish_empty(uri)
123
132
  notify(uri, [])
124
133
  end
125
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
+
126
138
  # Cancels every in-flight debounced task. Called from `Server#handle_shutdown` so pending publishes
127
139
  # don't fire against a closed STDOUT.
128
140
  def cancel_pending
@@ -160,8 +172,7 @@ module Rigor
160
172
 
161
173
  # True when a save arrived mid-round and the loop should run once more. Releases ownership otherwise.
162
174
  # `next`, not `break`: inside `Mutex#synchronize`'s block, `break` returns from the SYNCHRONIZE call, so
163
- # a `break` written here would leave the caller's `loop` spinning forever — which is exactly what it did
164
- # before this was split out.
175
+ # a `break` here would leave the caller's `loop` spinning forever.
165
176
  def consume_pending
166
177
  @round_lock.synchronize do
167
178
  next(@round_running = false) unless @round_pending
@@ -178,6 +189,7 @@ module Rigor
178
189
  def execute_project_round(saved_uri)
179
190
  generation = @project_context.generation
180
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
181
193
  return if generation != @project_context.generation
182
194
 
183
195
  by_path = diagnostics.group_by(&:path)
@@ -187,16 +199,20 @@ module Rigor
187
199
 
188
200
  notify(uri, (by_path[path] || []).filter_map { |diagnostic| to_lsp_diagnostic(diagnostic, path) })
189
201
  end
202
+ republish_dirty
190
203
  end
191
204
 
205
+ # The dirty buffers the round may not speak for, re-published from their own bytes (ADR-124). The save that
206
+ # started the round may have moved a declaration a dirty buffer's markers read across files, and nothing
207
+ # else re-analyses that buffer until its next change or save. Each goes through {#publish_for}, so the
208
+ # debouncer and `PublishBatcher` order it against the buffer's own `didChange` publishes: a re-publish run
209
+ # here, beside them, could land its older bytes' markers after a newer edit's.
210
+ def republish_dirty = @buffer_table.uris.each { |uri| publish_for(uri) if @buffer_table.dirty?(uri) }
211
+
192
212
  # The URIs this round may speak for: every open buffer whose bytes are the ones on disk. The just-saved
193
213
  # URI qualifies by definition (the client wrote it), and is named explicitly so a client that sends
194
214
  # `didSave` without a preceding applied `didChange` still refreshes it.
195
- def publish_set(saved_uri)
196
- ([saved_uri] + @buffer_table.uris).uniq.select do |uri|
197
- @buffer_table.open?(uri) && !@buffer_table.dirty?(uri) && !@buffer_table.desynchronized?(uri)
198
- end
199
- end
215
+ def publish_set(saved_uri) = ([saved_uri] + @buffer_table.uris).uniq.select { |uri| @buffer_table.on_disk?(uri) }
200
216
 
201
217
  def run_and_notify(uri, path)
202
218
  entry = @buffer_table[uri]
@@ -239,11 +255,13 @@ module Rigor
239
255
  def publish_batch(jobs)
240
256
  with_tempfiles(jobs) do |bound_jobs|
241
257
  bindings = bound_jobs.map { |job| job.fetch(:binding) }
258
+ @project_context.prepare_discovery_seed
242
259
  dispatcher = Analysis::Runner::BufferPoolDispatcher.new(
243
260
  configuration: @project_context.configuration,
244
261
  cache_store: @project_context.cache_store,
245
262
  environment: @project_context.environment,
246
263
  prebuilt: @project_context.project_scan,
264
+ discovery_seed_for: @project_context.method(:discovery_seed_for),
247
265
  workers: worker_count
248
266
  )
249
267
  diagnostics_per_binding = dispatcher.analyze(bindings)
@@ -282,6 +300,7 @@ module Rigor
282
300
  collect_stats: false,
283
301
  buffer: binding,
284
302
  prebuilt: @project_context.project_scan,
303
+ discovery_seed: @project_context.discovery_seed_for(binding),
285
304
  environment: @project_context.environment
286
305
  )
287
306
  result = runner.run([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?
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "tempfile"
4
+
3
5
  require_relative "../environment"
4
6
  require_relative "../project_environment"
5
7
  require_relative "../cache/store"
@@ -7,6 +9,9 @@ require_relative "../analysis/runner"
7
9
  require_relative "../analysis/incremental_session"
8
10
  require_relative "../cache/incremental_snapshot"
9
11
  require_relative "../effects/identity"
12
+ require_relative "../analysis/path_expansion"
13
+ require_relative "../protection/discovery_seed"
14
+ require_relative "../config_audit"
10
15
 
11
16
  module Rigor
12
17
  module LanguageServer
@@ -58,6 +63,7 @@ module Rigor
58
63
  @primed_key = nil
59
64
  @buffer_sources = {}
60
65
  @disk_feature_files = nil
66
+ @disk_sig_names = nil
61
67
  @environment_features = nil
62
68
  @generation = 0
63
69
  @environment = nil
@@ -65,6 +71,24 @@ module Rigor
65
71
  @project_scan = nil
66
72
  @incremental_session = nil
67
73
  @session_primed = false
74
+ @announced_config_warnings = Set.new
75
+ @announce_lock = Mutex.new
76
+ end
77
+
78
+ # Issue #2231 — the {ConfigAudit} warnings for the configuration as last loaded that the editor has not been
79
+ # shown yet, or an empty array. `rigor check` prints these on every run; the server is one long run, so it
80
+ # announces each warning once: at `initialized` for the configuration it started with, and after a later reload
81
+ # (every save round re-reads `.rigor.yml`) only the warnings that were not in the previous reload's set. The
82
+ # comparison is by message, not by order, so reordering entries announces nothing, and fixing one of two
83
+ # warnings does not show the other again. A warning that was fixed and then comes back is new and is shown.
84
+ # Called from the dispatch thread and the save round's thread alike, hence the lock.
85
+ def unannounced_config_warnings
86
+ @announce_lock.synchronize do
87
+ warnings = ConfigAudit.warnings(@configuration)
88
+ fresh = warnings.reject { |warning| @announced_config_warnings.include?(warning.message) }
89
+ @announced_config_warnings = warnings.to_set(&:message)
90
+ fresh
91
+ end
68
92
  end
69
93
 
70
94
  # Returns the cached `Rigor::Environment` for this session, building it on first access. The build
@@ -112,6 +136,47 @@ module Rigor
112
136
  @project_scan ||= build_project_scan
113
137
  end
114
138
 
139
+ # Issue #2147 — the cross-file discovery tables a per-buffer publish seeds its `Runner` with
140
+ # (`discovery_seed:`, #260). A `prebuilt:` runner runs no discovery pass of its own, so without them every
141
+ # class another file declares reads as an external ancestor and a publish answers differently from
142
+ # `rigor check` over the same tree. The per-file bundles are walked once per generation; each publish
143
+ # re-folds them with `buffer`'s file re-walked from the buffer's bytes
144
+ # ({Protection::DiscoverySeed.tables_for_buffer}), so the buffer's own declarations win over its on-disk
145
+ # ones, and a file whose bytes on disk moved since the bundles were built is re-walked too (its digest no
146
+ # longer matches).
147
+ #
148
+ # The buffer's file is matched by its real path and handed to the fold under the buffer's own spelling: the
149
+ # binding substitutes bytes only for a path spelled exactly as its logical path (the URI's absolute path),
150
+ # while the configured paths may be relative (`lib`, the default) or reach the file through a symlink.
151
+ def discovery_seed_for(buffer)
152
+ Protection::DiscoverySeed.tables_for_buffer(paths: discovery_paths_for(buffer), bundles: discovery_bundles,
153
+ buffer: buffer, constant_sources: true)
154
+ end
155
+
156
+ # Issue #2282 — the base scope an editor query (hover, completion, signature help) probes with: the same
157
+ # cross-file discovery seed the diagnostic publisher hands its `Runner` ({#discovery_seed_for}), so a constant
158
+ # or method another file defines answers what `rigor check` would. The seed fold reads the buffer through a
159
+ # {Analysis::BufferBinding}, so `bytes` is staged in a tempfile for the call. An empty seed (no configured
160
+ # files, or nothing discovered) leaves the bare single-file scope every query used before.
161
+ def seeded_scope_for(path:, bytes:)
162
+ seed = Tempfile.create(["rigor-lsp-query-", ".rb"]) do |tmp|
163
+ # Staged as the publisher stages them; the seed's job is to withhold the stale on-disk declarations of
164
+ # this file, so an empty-bytes stage is not observable by design.
165
+ tmp.write(bytes)
166
+ tmp.flush
167
+ discovery_seed_for(Analysis::BufferBinding.new(logical_path: path, physical_path: tmp.path))
168
+ end
169
+ Protection::DiscoverySeed.seeded_scope(environment: environment, seed: seed)
170
+ end
171
+
172
+ # Builds what {#discovery_seed_for} reads once per generation, so a fork batch's children inherit it instead
173
+ # of each walking the project.
174
+ def prepare_discovery_seed
175
+ discovery_bundles
176
+ discovery_real_paths
177
+ nil
178
+ end
179
+
115
180
  # Whole-project diagnostics for one save round (#246). The first call primes an in-process
116
181
  # {Analysis::IncrementalSession} — seeded from the on-disk snapshot a terminal
117
182
  # `rigor check --incremental` may have left, and otherwise from a full baseline; every later call is a
@@ -168,9 +233,14 @@ module Rigor
168
233
  # file.
169
234
  def required_features
170
235
  disk = (@disk_feature_files ||= Environment::RequiredFeatures.feature_files(@configuration))
171
- found = disk.reject { |path, _| @buffer_sources.key?(path) }.values.flatten
172
- found += @buffer_sources.values.flat_map { |source| Environment::RequiredFeatures.features_of(source) }
173
- found.uniq.sort.freeze
236
+ scanned = disk.reject { |path, _| @buffer_sources.key?(path) }.values
237
+ scanned += @buffer_sources.values.map { |source| Environment::RequiredFeatures.scan_source(source) }
238
+ features = scanned.flat_map(&:first).uniq
239
+ names = scanned.flat_map(&:last).to_set
240
+ names.merge(@disk_sig_names ||= Environment::RequiredFeatures.sig_names(
241
+ Environment::RequiredFeatures.configured_sig_paths(@configuration)
242
+ ))
243
+ Environment::RequiredFeatures.relevant(features, names).sort.freeze
174
244
  end
175
245
 
176
246
  # Drops every cached collaborator and bumps the generation. The next reader rebuilds from scratch.
@@ -188,7 +258,11 @@ module Rigor
188
258
  @environment = nil
189
259
  @environment_features = nil
190
260
  @disk_feature_files = nil
261
+ @disk_sig_names = nil
191
262
  @project_scan = nil
263
+ @discovery_paths = nil
264
+ @discovery_real_paths = nil
265
+ @discovery_bundles = nil
192
266
  # The session's per-file cache was computed against the old environment / project scan, so it cannot
193
267
  # outlive them. The next save round primes a fresh one.
194
268
  @incremental_session = nil
@@ -214,7 +288,8 @@ module Rigor
214
288
  def reload_configuration
215
289
  return unless @configuration_loader
216
290
 
217
- @configuration = @configuration_loader.call
291
+ loaded = @configuration_loader.call
292
+ @announce_lock.synchronize { @configuration = loaded }
218
293
  rescue StandardError
219
294
  nil
220
295
  end
@@ -230,6 +305,25 @@ module Rigor
230
305
  )
231
306
  end
232
307
 
308
+ # The configured project files, expanded as `rigor check` expands its configured paths.
309
+ def discovery_paths
310
+ @discovery_paths ||= Analysis::PathExpansion.configured_files(@configuration).freeze
311
+ end
312
+
313
+ # Every entry that reaches the buffer's file is respelled, so a file configured under two roots (`lib` and a
314
+ # symlink to it) is not folded from disk through the second.
315
+ def discovery_paths_for(buffer)
316
+ Protection::DiscoverySeed.respell_for_buffer(discovery_paths, buffer, real_paths: discovery_real_paths)
317
+ end
318
+
319
+ def discovery_real_paths
320
+ @discovery_real_paths ||= discovery_paths.map { |path| Rigor::Analysis::BufferBinding.real_path(path) }.freeze
321
+ end
322
+
323
+ def discovery_bundles
324
+ @discovery_bundles ||= Protection::DiscoverySeed.bundles(paths: discovery_paths)
325
+ end
326
+
233
327
  def build_project_scan
234
328
  runner = Analysis::Runner.new(
235
329
  configuration: @configuration,
@@ -15,10 +15,9 @@ module Rigor
15
15
  # dropped. The same claim/consume shape `DiagnosticPublisher#run_project_round` already uses for the
16
16
  # whole-project save round (#246), generalised to an arbitrary key type and an arbitrary batch action.
17
17
  class PublishBatcher
18
- # @param on_batch — `(keys) -> void`, called with the deduplicated Array of keys pending at the
19
- # start of one round. May be called more than once in a row when keys keep arriving while a round
20
- # runs.
21
- # @param on_error — `(exception) -> void`, called when `on_batch` raises. A round must
18
+ # @param on_batch — called with the deduplicated keys pending at the start of one round. May be called
19
+ # more than once in a row when keys keep arriving while a round runs.
20
+ # @param on_error — called with the exception when `on_batch` raises. A round must
22
21
  # never wedge the coalescing lock for every future `#enqueue` call — ownership is always released
23
22
  # before this fires. Whatever was mid-flight when the round raised is lost; anything enqueued by a
24
23
  # concurrent `#enqueue` call after this round's drain but before the rescue stays pending and rides
@@ -20,10 +20,8 @@ module Rigor
20
20
  @project_context = project_context
21
21
  end
22
22
 
23
- # @param positions — LSP `Position[]` — each
24
- # `{ line:, character: }` 0-based.
25
- # @return one `SelectionRange` per
26
- # position, or nil when the URI / buffer isn't resolvable.
23
+ # @param positions — LSP positions, each `{ line:, character: }` 0-based.
24
+ # @return one `SelectionRange` per position, or nil when the URI / buffer isn't resolvable.
27
25
  def provide(uri, positions)
28
26
  path, entry = buffer_for(uri)
29
27
  return nil if entry.nil?
@@ -194,7 +194,12 @@ module Rigor
194
194
 
195
195
  # `initialized` is a notification — no response body. Nothing is registered here yet: the server handles
196
196
  # `workspace/didChangeWatchedFiles` when a client sends it, but does not register for it dynamically.
197
+ #
198
+ # Issue #2231 — the configuration the server started with is audited here, the first point at which the
199
+ # client is listening, so a `.rigor.yml` key that does nothing is named in the editor as `rigor check`
200
+ # names it on STDERR.
197
201
  def handle_initialized
202
+ @publisher&.announce_config_warnings
198
203
  nil
199
204
  end
200
205
 
@@ -267,8 +272,7 @@ module Rigor
267
272
 
268
273
  # textDocument/hover REQUEST. Slice 5 returns either a `Hover` payload (markdown contents wrapping type
269
274
  # + erased-RBS info) or nil when no expression is at the queried position. Nil maps to `result: null`
270
- # per LSP spec; clients suppress the popup. Returns `MethodNotFound` when no hover_provider is wired
271
- # (slice 1-4 behaviour).
275
+ # per LSP spec; clients suppress the popup. Returns `MethodNotFound` when no hover_provider is wired.
272
276
  def handle_hover(params)
273
277
  return method_not_found("textDocument/hover") unless @hover_provider
274
278
 
@@ -282,11 +286,12 @@ module Rigor
282
286
  end
283
287
 
284
288
  # workspace/didChangeWatchedFiles NOTIFICATION. Invalidates the ProjectContext so cached pre-pass /
285
- # Environment is rebuilt on the next request. Slice 7's floor: any watched file change triggers a full
286
- # context rebuild. Per-file surgical invalidation (per design doc § "Project context refresh") is a
287
- # follow-up; this is the LSP-correct floor.
289
+ # Environment is rebuilt on the next request, and re-publishes the open buffers against the new world
290
+ # (ADR-124). Slice 7's floor: any watched file change triggers a full context rebuild. Per-file surgical
291
+ # invalidation (per design doc § "Project context refresh") is a follow-up; this is the LSP-correct floor.
288
292
  def handle_did_change_watched_files(_params)
289
293
  @project_context&.invalidate!
294
+ @publisher&.republish_open
290
295
  nil
291
296
  end
292
297
 
@@ -295,6 +300,7 @@ module Rigor
295
300
  # (.rigor.yml / Gemfile.lock / etc).
296
301
  def handle_did_change_configuration(_params)
297
302
  @project_context&.invalidate!
303
+ @publisher&.republish_open
298
304
  nil
299
305
  end
300
306
 
@@ -349,7 +355,7 @@ module Rigor
349
355
  )
350
356
  end
351
357
 
352
- # textDocument/documentSymbol REQUEST. Returns the `DocumentSymbol[]` outline for the buffer at the
358
+ # textDocument/documentSymbol REQUEST. Returns the document-symbol outline for the buffer at the
353
359
  # requested URI. Returns `MethodNotFound` when no provider is wired.
354
360
  def handle_document_symbol(params)
355
361
  return method_not_found("textDocument/documentSymbol") unless @document_symbol_provider
@@ -25,7 +25,7 @@ module Rigor
25
25
  # parses the buffer, locates the enclosing `CallNode`, infers the receiver's type, and returns the
26
26
  # method's first overload as a `SignatureInformation`.
27
27
  #
28
- # Slice C1 (this commit) ships:
28
+ # Slice C1 ships:
29
29
  # - Sentinel patching for `obj.foo(|` so Prism's parse succeeds (mirrors `CompletionProvider`'s slice B4
30
30
  # pattern).
31
31
  # - First-overload signature only.
@@ -111,7 +111,7 @@ module Rigor
111
111
  end
112
112
 
113
113
  def build_signature(call_node, root, path, bytes, line, character)
114
- scope_index = build_scope_index(root, path)
114
+ scope_index = build_scope_index(root, path, bytes)
115
115
  receiver_node = call_node.receiver
116
116
  return nil if receiver_node.nil?
117
117
 
@@ -197,8 +197,8 @@ module Rigor
197
197
  end
198
198
  end
199
199
 
200
- def build_scope_index(root, _path)
201
- scope = Scope.empty(environment: @project_context.environment)
200
+ def build_scope_index(root, path, bytes)
201
+ scope = @project_context.seeded_scope_for(path: path, bytes: bytes)
202
202
  Inference::ScopeIndexer.index(root, default_scope: scope)
203
203
  end
204
204
 
@@ -29,7 +29,7 @@ module Rigor
29
29
  return Rigor::VERSION if absolute.start_with?("#{BUNDLED_PLUGINS}/")
30
30
 
31
31
  spec = Gem.loaded_specs.each_value.find do |gem|
32
- absolute.start_with?("#{gem.full_gem_path}/") # rigor:disable undefined-method
32
+ absolute.start_with?("#{gem.full_gem_path}/")
33
33
  end
34
34
  spec&.version&.to_s
35
35
  rescue StandardError