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
@@ -245,8 +245,8 @@ module Rigor
245
245
  end
246
246
 
247
247
  # `literal-string` — a `String` that is statically known to come from a source-code literal (or a
248
- # composition of literals). v0.0.9 tracks this flow through interpolation `"#{...}"`, leaving
249
- # propagation through `+` / `<<` to a later slice. Every `Constant<String>` is implicitly
248
+ # composition of literals). Interpolation `"#{...}"` and `+` / `*` composition keep
249
+ # the flow (see {literal_string_compatible?}). Every `Constant<String>` is implicitly
250
250
  # literal-string-compatible; the carrier exists for cases where the concrete value is unknown but
251
251
  # literal-ness has been established (an RBS::Extended `return: literal-string` annotation, or
252
252
  # interpolation over literal-bearing parts).
@@ -433,8 +433,7 @@ module Rigor
433
433
  # - `Type::Tuple[A, B, C]` → `Constant<0> | Constant<1> | Constant<2>`.
434
434
  # - `Type::Nominal["Hash", [K, V]]` → `K` (untyped if absent).
435
435
  # - `Type::Nominal["Array", [E]]` → `non-negative-int`.
436
- # - `Type::Constant` whose value is a Hash / Array / Range — project through the literal's
437
- # per-element keys.
436
+ # - `Type::Constant` wrapping a Range with an Integer begin — `non-negative-int`.
438
437
  #
439
438
  # Other inputs (`Top`, `Dynamic`, untyped Nominals, `Union`, `Refined`, `Difference`,
440
439
  # `Intersection`) project to `top` so the type function always returns a value — callers may narrow
@@ -456,8 +455,7 @@ module Rigor
456
455
  # - `Type::Tuple[A, B, C]` → `A | B | C`.
457
456
  # - `Type::Nominal["Hash", [K, V]]` → `V` (untyped if absent).
458
457
  # - `Type::Nominal["Array", [E]]` → `E` (untyped if absent).
459
- # - `Type::Constant` whose value is a Hash / Array / Range — union of `Constant<…>` for each
460
- # element.
458
+ # - `Type::Constant` wrapping an integer Range — the `IntegerRange` it spans.
461
459
  def value_of(type)
462
460
  case type
463
461
  when HashShape then hash_shape_values(type)
@@ -528,8 +526,7 @@ module Rigor
528
526
  # Phase A handles `Type::HashShape` (literal-key K). Phase B (slice 5) extends to `Type::Tuple`
529
527
  # (integer-index K) — `pick_of[Tuple[A, B, C], 0 | 2]` evaluates to `Tuple[A, C]`. Non-shape inputs
530
528
  # (`Type::Nominal`, etc.) return `type` unchanged ("lossy degradation"; the
531
- # `dynamic.shape.lossy-projection` diagnostic that flags the boundary lands when caller-side
532
- # diagnostic threading arrives).
529
+ # `dynamic.shape.lossy-projection` diagnostic that flags the boundary).
533
530
  def pick_of(type, keys)
534
531
  case type
535
532
  when HashShape then hash_shape_pick(type, keys)
@@ -597,8 +594,7 @@ module Rigor
597
594
  # site where the projection was authored.
598
595
  #
599
596
  # `HashShape` and `Tuple` carry shape-level information the projections honour; every other carrier
600
- # is lossy. Slice 5b wires diagnostic emission through `RbsExtended` / parser callers; this
601
- # predicate stands alone in slice 5 for unit-test coverage and future composition.
597
+ # is lossy.
602
598
  def shape_projection_lossy?(type)
603
599
  !type.is_a?(HashShape) && !type.is_a?(Tuple)
604
600
  end
@@ -792,9 +788,9 @@ module Rigor
792
788
  top
793
789
  end
794
790
 
795
- # `Type::Constant` only carries scalar literals (Integer / Float / String / Symbol / Range /
796
- # Rational / Complex / true / false / nil); Array and Hash literals become Tuple / HashShape
797
- # carriers earlier in the typer. Range is the only scalar with meaningful key/value projections.
791
+ # `Type::Constant` only carries scalar literals (`Constant::SCALAR_CLASSES`); Array and Hash literals
792
+ # become Tuple / HashShape carriers earlier in the typer. Range is the only scalar with meaningful
793
+ # key/value projections.
798
794
  def constant_keys(value)
799
795
  return non_negative_int if value.is_a?(Range) && value.begin.is_a?(Integer)
800
796
 
@@ -1040,9 +1036,10 @@ module Rigor
1040
1036
  # No type carrier has a method that changes it once built (a built type is frozen; one `Marshal.load`
1041
1037
  # restores is not, but nothing mutates it either), so a memoised key cannot go stale. The memo is weak
1042
1038
  # in both key and value: it keeps neither the type nor the string alive, and an entry whose string
1043
- # nothing else holds is dropped at the next GC. It is keyed by identity on purpose: `Constant[0.0]` and
1044
- # `Constant[-0.0]` are equal and hash alike, but render differently. A non-main Ractor may not read the
1045
- # module's ivar, so a pool worker renders the key every time, as every run did before.
1039
+ # nothing else holds is dropped at the next GC. It is keyed by identity on purpose: two equal members
1040
+ # need not render alike (`Constant[1..2]` and `Constant[1..2.0]`), so equality is not a safe key. A
1041
+ # non-main Ractor may not read the module's ivar, so a pool worker renders the key every time, as every
1042
+ # run did before.
1046
1043
  def sort_key(member)
1047
1044
  return member.describe(:short) unless Ractor.main?
1048
1045
 
@@ -8,9 +8,8 @@ require_relative "plain_lattice"
8
8
  module Rigor
9
9
  module Type
10
10
  # A literal carrier under ADR-3 OQ1 Option C (Hybrid). Wraps a Ruby literal value of one of the
11
- # supported immutable-ish classes. Compound literal shapes (Tuple, HashShape, Record) get dedicated
12
- # classes in later slices; Range is carried only when both static endpoints are known enough for
13
- # tuple slicing.
11
+ # supported immutable-ish classes. Compound literal shapes (Tuple, HashShape) have their own classes;
12
+ # Range is carried only when both static endpoints are known enough for tuple slicing.
14
13
  #
15
14
  # See docs/adr/4-type-inference-engine.md for the tentative answer to the open question and
16
15
  # docs/type-specification/rigor-extensions.md for the refinement neighbourhood this carrier lives in.
@@ -74,12 +73,15 @@ module Rigor
74
73
 
75
74
  # RBS supports `Literal` types for booleans, nil, integer literals (positive and negative), symbol
76
75
  # literals, and string literals. Erasing to these preserves the carrier's precision at the RBS
77
- # boundary — `Constant<64>` round-trips as `64`, not as `Integer` — and
76
+ # boundary — the constant 64 round-trips as `64`, not as `Integer` — and
78
77
  # `RbsTypeTranslator#translate_literal` already maps the parsed RBS Literal back to `Constant`.
79
78
  # Scalar carriers without RBS Literal support (Float, Range, Rational, Complex, Regexp, Pathname)
80
79
  # keep their pre-existing widen-to-class-name behaviour because RBS rejects their literal spellings
81
80
  # as syntax errors. `Range` is generic (`Range[out E]`), and a bare `Range` fails `rbs validate`, so
82
- # it carries the class of its endpoints, or `untyped` when both are nil (#1891).
81
+ # it carries the class of its endpoints, or `untyped` when both are nil (#1891). `Set` is
82
+ # generic too, and a set is mutable: the constant's elements are only what it held when it was built, and
83
+ # `x << "a"` adds a member they do not name. It erases to `Set[untyped]`, as an Array or Hash literal's
84
+ # element type is widened rather than pinned (#2177).
83
85
  def erase_to_rbs
84
86
  case value
85
87
  when true then "true"
@@ -88,6 +90,7 @@ module Rigor
88
90
  when Integer then value.to_s
89
91
  when Symbol, String then value.inspect
90
92
  when Range then "Range[#{range_element_rbs}]"
93
+ when ::Set then "Set[untyped]"
91
94
  else value.class.name
92
95
  end
93
96
  end
@@ -96,13 +99,51 @@ module Rigor
96
99
 
97
100
  include Rigor::Type::AcceptanceRouter
98
101
 
102
+ # Two carriers are equal when they pin the same value, which for a Float is stricter than Ruby's `==`
103
+ # (#1513): `0.0` and `-0.0` are distinct values (`1.0 / x` tells them apart), so `0.0 | -0.0` keeps both
104
+ # members, and a NaN equals another NaN, so `==` stays reflexive and a union never collects one NaN member
105
+ # per occurrence. A Complex compares its parts the same way.
99
106
  def ==(other)
100
- other.is_a?(Constant) && value.class == other.value.class && value == other.value
107
+ other.is_a?(Constant) && value.class == other.value.class && Constant.same_value?(value, other.value)
101
108
  end
102
109
  alias eql? ==
103
110
 
104
111
  def hash
105
- [Constant, value.class, value].hash
112
+ [Constant, value.class, Constant.value_key(value)].hash
113
+ end
114
+
115
+ # True when `left` and `right` (of one class) are the same value in the sense of {#==}.
116
+ def self.same_value?(left, right)
117
+ case left
118
+ when Float then same_float?(left, right)
119
+ when Complex then same_float?(left.real, right.real) && same_float?(left.imaginary, right.imaginary)
120
+ else left == right
121
+ end
122
+ end
123
+
124
+ def self.same_float?(left, right)
125
+ return left == right unless left.is_a?(Float) && right.is_a?(Float)
126
+ return right.nan? if left.nan?
127
+
128
+ left.eql?(right) && (!left.zero? || (1.0 / left).positive? == (1.0 / right).positive?)
129
+ end
130
+
131
+ # A key whose `eql?` / `hash` agree with {.same_value?}: `uniq { |v| Constant.value_key(v) }` keeps
132
+ # `0.0` and `-0.0` apart and folds every NaN into one. Any other value is its own key, so plain `uniq`
133
+ # semantics are unchanged for it.
134
+ def self.value_key(value)
135
+ case value
136
+ when Float then float_key(value)
137
+ when Complex then [Complex, float_key(value.real), float_key(value.imaginary)]
138
+ else value
139
+ end
140
+ end
141
+
142
+ def self.float_key(value)
143
+ return value unless value.is_a?(Float)
144
+ return :nan if value.nan?
145
+
146
+ value.zero? ? [Float, (1.0 / value)] : value
106
147
  end
107
148
 
108
149
  def inspect
@@ -28,14 +28,14 @@ module Rigor
28
28
  ALLOWED_KEY_CLASSES = [Symbol, String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
29
29
  EXTRA_KEY_POLICIES = %i[open closed].freeze
30
30
  POLICY_KEYWORDS = %i[required_keys optional_keys read_only_keys extra_keys].freeze
31
- # A Symbol whose text matches renders as a bare RBS record key (`lang:`) in {#erase_to_rbs};
32
- # anything else must be quoted with a fat arrow (`"data-contrast" =>`). See {#erase_key_prefix}.
31
+ # A Symbol whose text matches renders as a bare RBS record key (`lang:`) in {#describe} and {#erase_to_rbs};
32
+ # anything else is a quoted Symbol with a fat arrow (`:"data-contrast" =>`). See {#key_prefix}.
33
33
  BARE_RECORD_KEY = /\A[A-Za-z_][A-Za-z0-9_]*[?!]?\z/
34
34
 
35
35
  attr_reader :pairs, :required_keys, :optional_keys, :read_only_keys, :extra_keys
36
36
 
37
37
  # @param pairs — ordered map of
38
- # keys to declared types. Keys MUST be Symbol or String;
38
+ # keys to declared types. Keys MUST be one of ALLOWED_KEY_CLASSES;
39
39
  # values MUST be Rigor::Type instances. The hash is duped and
40
40
  # frozen at construction; callers MUST NOT mutate the input
41
41
  # afterwards (mutation does not affect the carrier, but the
@@ -72,9 +72,9 @@ module Rigor
72
72
  def erase_to_rbs
73
73
  return "{}" if pairs.empty? && closed?
74
74
  return hash_erasure unless closed?
75
- return hash_erasure if pairs.each_key.any? { |k| !k.is_a?(Symbol) }
75
+ return hash_erasure if pairs.each_key.any? { |k| !k.is_a?(Symbol) || !record_key_spellable?(k) }
76
76
 
77
- rendered = pairs.map { |k, v| "#{erase_key_prefix(k)} #{v.erase_to_rbs}" }
77
+ rendered = pairs.map { |k, v| "#{key_prefix(k)} #{v.erase_to_rbs}" }
78
78
  "{ #{rendered.join(', ')} }"
79
79
  end
80
80
 
@@ -193,47 +193,33 @@ module Rigor
193
193
  def render_entry(key, value, verbosity)
194
194
  prefix = []
195
195
  prefix << "readonly" if read_only_key?(key)
196
- rendered_key = optional_key?(key) ? "?#{render_key(key)}" : render_key(key)
197
- prefix << "#{rendered_key}#{key_separator(key)}"
196
+ prefix << key_prefix(key)
198
197
  "#{prefix.join(' ')} #{value.describe(verbosity)}"
199
198
  end
200
199
 
201
- # `describe` is a human-facing display contract (it feeds diagnostic messages, never a parser),
202
- # so it keeps the compact `"a":` form for a quoted key. Non-(Symbol|String) scalar keys render in
203
- # the natural hashrocket spelling (`1 => 2`, `1.0 => 4`, `nil => 0`) via {#key_separator}; their
204
- # `inspect` is the canonical Ruby literal, so describe stays deterministic. The RBS-erasure path
205
- # uses {#erase_key_prefix} instead, which must emit a parseable key.
206
- def render_key(key)
207
- case key
208
- when Symbol then key.to_s
209
- else key.inspect
210
- end
211
- end
200
+ # A key and its separator as RBS spells them, with a leading `?` for an optional key. `describe`
201
+ # (the diagnostic display) and {#erase_to_rbs} (which only ever sees Symbol keys — string-keyed
202
+ # shapes degrade to `Hash[...]` before reaching here) share it, so a key reads the same in a
203
+ # message as in the signature it erases to. A Symbol whose text is a plain identifier is a bare
204
+ # key with a colon (`lang:`, Ruby keywords like `class` included). Any other Symbol is a quoted
205
+ # Symbol literal with a fat arrow (`:"data-contrast" =>`): RBS rejects a bare non-identifier
206
+ # (`data-contrast:`) and a quoted key with a colon (`"data-contrast":`), and reads
207
+ # `"data-contrast" =>` as the String key, so the written record declared a key the body never has
208
+ # (#2176). Every other key, a String included, is its `inspect` literal with a fat arrow
209
+ # (`"a-b" =>`, `1 =>`, `nil =>`): the compact `"a-b":` would read as a Symbol key in Ruby, so a
210
+ # String-vs-Symbol key mismatch in a diagnostic was unreadable (#2235).
211
+ def key_prefix(key)
212
+ optional = optional_key?(key) ? "?" : ""
213
+ return "#{optional}#{key.inspect} =>" unless key.is_a?(Symbol)
214
+ return "#{optional}#{key}:" if BARE_RECORD_KEY.match?(key.to_s)
212
215
 
213
- # Symbol / String keys keep the compact colon form (`a: 1`, `"k": 2`); every other scalar key uses
214
- # the hashrocket (`1 => 2`) — the form the user would have to write in source.
215
- def key_separator(key)
216
- case key
217
- when Symbol, String then ":"
218
- else " =>"
219
- end
216
+ "#{optional}:#{key.to_s.inspect} =>"
220
217
  end
221
218
 
222
- # An RBS record entry's key + separator, for the {#erase_to_rbs} path (which only ever sees
223
- # Symbol keys — string-keyed shapes degrade to `Hash[...]` before reaching here). A Symbol whose
224
- # text is a plain identifier is a bare key with a colon (`lang:`); a hyphenated / punctuated
225
- # Symbol MUST use the quoted `"data-contrast" =>` form, because RBS rejects both a bare
226
- # non-identifier (`data-contrast:`) and a quoted key with a colon (`"data-contrast":`). Getting
227
- # this wrong made sig-gen emit an unparseable Mastodon `html_attributes` shape that crashed the
228
- # whole RBS env build. Valid identifier keys (incl. Ruby keywords like `class`) keep the colon
229
- # form, so only genuinely non-identifier keys change. Optional keys carry a leading `?`.
230
- def erase_key_prefix(key)
231
- optional = optional_key?(key) ? "?" : ""
232
- if BARE_RECORD_KEY.match?(key.to_s)
233
- "#{optional}#{key}:"
234
- else
235
- "#{optional}#{key.to_s.inspect} =>"
236
- end
219
+ # `String#inspect` escapes `#{`, `#$` and `#@` as `\#`, which RBS's parser does not read back as
220
+ # `#`. A key spelled that way would name a different Symbol, so the shape erases to `Hash[...]`.
221
+ def record_key_spellable?(key)
222
+ !key.to_s.inspect.include?("\\#")
237
223
  end
238
224
 
239
225
  def hash_erasure
@@ -63,10 +63,11 @@ module Rigor
63
63
  end
64
64
 
65
65
  # Erases to the tagging class nominal (conservative: the structural members are not RBS-expressible
66
- # as a class instance). The anonymous case erases to the `Struct` supertype.
66
+ # as a class instance). The anonymous case erases to the `Struct` supertype with an `untyped`
67
+ # element: `Struct[Elem]` is generic, and a bare `Struct` fails `rbs validate` (#2177).
67
68
  def erase_to_rbs
68
69
  name = class_name
69
- return "Struct" if name.nil?
70
+ return "Struct[untyped]" if name.nil?
70
71
 
71
72
  name
72
73
  end
@@ -49,7 +49,7 @@ module Rigor
49
49
  end
50
50
  end
51
51
 
52
- # ADR-1 § "RBS round-trip is lossless" + the value-lattice rule `untyped | T = untyped` (every `T`
52
+ # ADR-1 § "Erasure Must Be Conservative" + the value-lattice rule `untyped | T = untyped` (every `T`
53
53
  # is gradually consistent with `untyped`). When any union member erases to `"untyped"`, the whole
54
54
  # union erases to `"untyped"` — the RBS surface has no carrier for "Dynamic-origin alongside a
55
55
  # static facet", and the gradual-consistency contract guarantees the substitution is sound at every
@@ -22,8 +22,7 @@ module Rigor
22
22
  # Freeze the String field so the resulting Data object is `Ractor.shareable?` regardless of
23
23
  # whether the caller passed a `# frozen_string_literal: true` constant or a dynamically built
24
24
  # String. The same discipline applies to every other TypeNode value object — they live in the
25
- # parser's hot path and are the natural carriers to flow through future Ractor boundaries
26
- # (see CURRENT_WORK Open Items #8).
25
+ # parser's hot path and are the natural carriers to flow through future Ractor boundaries.
27
26
  super(name: name.frozen? ? name : name.dup.freeze)
28
27
  end
29
28
  end
@@ -16,7 +16,7 @@ module Rigor
16
16
  # resolver in the chain plus the built-in registry through the
17
17
  # same entry point.
18
18
  #
19
- # Constructed once per `Analysis::Runner.run` from
19
+ # Built by `Environment#build_name_scope` from
20
20
  # `Plugin::Registry#type_node_resolvers`. The chain is
21
21
  # immutable and re-entrant; the parser may consult it many
22
22
  # times for the same node.
@@ -7,8 +7,7 @@ module Rigor
7
7
  # `Pick[T, "name"]`, `pick_of[Shape, "a" | "b"]`, and downstream
8
8
  # plugin resolvers that accept literal key selectors.
9
9
  #
10
- # ADR-13 follow-up (`docs/CURRENT_WORK.md` engineering item
11
- # #2): the RBS::Extended grammar previously could not tokenise
10
+ # ADR-13 follow-up: the RBS::Extended grammar previously could not tokenise
12
11
  # `"name"` inside a type-arg position. The resolver translates
13
12
  # this node to a `Type::Constant` carrying the string value.
14
13
  # Slice 1 supports double-quoted strings without escape
@@ -7,8 +7,7 @@ module Rigor
7
7
  # `Pick[T, :name]`, `pick_of[Shape, :a | :b]`, and downstream
8
8
  # plugin resolvers that accept literal key selectors.
9
9
  #
10
- # ADR-13 follow-up (`docs/CURRENT_WORK.md` engineering item
11
- # #2): the RBS::Extended grammar previously could not tokenise
10
+ # ADR-13 follow-up: the RBS::Extended grammar previously could not tokenise
12
11
  # `:name` inside a type-arg position; this addition closes the
13
12
  # gap so `ImportedRefinements.parse` produces a uniform AST.
14
13
  # The resolver translates this node to a `Type::Constant`
@@ -5,8 +5,7 @@ module Rigor
5
5
  # Union AST node. Carries the nodes of a `T1 | T2 | …` union
6
6
  # expression authored at type-arg position.
7
7
  #
8
- # ADR-13 follow-up (`docs/CURRENT_WORK.md` engineering item
9
- # #2): together with {SymbolLiteral} / {StringLiteral} this
8
+ # ADR-13 follow-up: together with {SymbolLiteral} / {StringLiteral} this
10
9
  # closes the parser-side gap that prevented `Pick[T, :a | :b]`
11
10
  # / `Pick[T, "a" | "b"]` from tokenising. The resolver pass
12
11
  # recurses through each node and folds them via
@@ -16,10 +15,8 @@ module Rigor
16
15
  # shadowing `Data#members`, which is the introspection method
17
16
  # every `Data.define` mix-in inherits.
18
17
  #
19
- # Nodes are themselves any of the {TypeNode} carriers — the
20
- # parser builds a flat list (left-associative); nested
21
- # `Union` nodes are permitted but the parser does not
22
- # synthesise them by itself.
18
+ # The parser builds a flat list (left-associative); the constructor
19
+ # rejects a nested `Union` node.
23
20
  class Union < Data.define(:nodes)
24
21
  def initialize(nodes:)
25
22
  unless nodes.is_a?(Array) && nodes.size >= 2 && nodes.all? { |m| valid_member?(m) }
@@ -46,16 +46,7 @@ module Rigor
46
46
  key = (["self.class"] + fields.map(&:to_s)).join(", ")
47
47
 
48
48
  # Codegen (not per-call reflection) so the generated `==` / `hash` read fields
49
- # directly and run at hand-written speed on the inference hot path. For
50
- # `value_fields :class_name, :type_args` the emitted source is:
51
- #
52
- # def ==(other)
53
- # other.is_a?(self.class) && class_name == other.class_name && type_args == other.type_args
54
- # end
55
- # alias_method :eql?, :==
56
- # def hash
57
- # [self.class, class_name, type_args].hash
58
- # end
49
+ # directly and run at hand-written speed on the inference hot path.
59
50
  class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
60
51
  def ==(other)
61
52
  other.is_a?(self.class)#{guard}
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.4.5"
4
+ VERSION = "0.4.7"
5
5
  end
@@ -167,8 +167,8 @@ module Rigor
167
167
  end
168
168
 
169
169
  # Walks the channel body recursively (so `stream_from` / `stream_for` calls inside `subscribed`
170
- # / helper methods are picked up). Returns `[Array<String>, bool]` — the literal stream names +
171
- # whether any dynamic registration was seen.
170
+ # / helper methods are picked up). Returns the literal stream names and whether any dynamic
171
+ # registration was seen, as a pair.
172
172
  def collect_stream_registrations(node, names: [], dynamic: false)
173
173
  return [names, dynamic] if node.nil?
174
174
 
@@ -11,8 +11,8 @@ module Rigor
11
11
  # `subscription.perform("action_name", data)`.
12
12
  # - `stream_names` — the set of literal-string stream names registered via `stream_from "name"`
13
13
  # calls inside the channel body. Dynamic registrations (`stream_from interpolated_string`) are
14
- # recorded separately as `dynamic_streams: true` so the analyzer can suppress unknown-stream
15
- # warnings on any channel that has at least one dynamic registration.
14
+ # recorded separately as `dynamic_streams: true`; when any channel has one, the analyzer suppresses
15
+ # unknown-stream warnings for the whole run.
16
16
  class ChannelIndex
17
17
  Entry = Data.define(:class_name, :file_path, :action_methods, :stream_names, :dynamic_streams) do
18
18
  def includes_action?(name)
@@ -59,7 +59,6 @@ module Rigor
59
59
  @by_name.keys
60
60
  end
61
61
 
62
- # All literal stream names registered across every discovered channel.
63
62
  def all_stream_names
64
63
  @entries.flat_map { |e| e.stream_names.to_a }.to_set
65
64
  end
@@ -12,7 +12,7 @@ module Rigor
12
12
  # rigor-actioncable — validates ActionCable `<Channel>.broadcast_to(...)` and
13
13
  # `ActionCable.server.broadcast(stream_name, ...)` call sites against the discovered channel index.
14
14
  #
15
- # Tier 3F of the [Rails plugins roadmap](../../../../docs/design/20260508-rails-plugins-roadmap.md).
15
+ # Tier 3F of the [Rails plugins roadmap](../../../../../docs/design/20260508-rails-plugins-roadmap.md).
16
16
  # Statically discovers channel classes by walking `channel_search_paths` and parsing each file with
17
17
  # Prism — no `actioncable` runtime dependency.
18
18
  #