rigortype 0.3.9 → 0.4.1

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 (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -13,9 +13,18 @@ require_relative "../source/literals"
13
13
  require_relative "../source/node_children"
14
14
  require_relative "../inference/def_return_typer"
15
15
  require_relative "../inference/scope_indexer"
16
+ require_relative "../inference/module_function_state"
16
17
  require_relative "../inference/rbs_type_translator"
18
+ require_relative "alias_index"
17
19
  require_relative "meta_class_shape"
18
20
  require_relative "rbs_validity"
21
+ require_relative "classification"
22
+ require_relative "effect_annotation"
23
+ require_relative "declaration_equivalence"
24
+ require_relative "inline_declarations"
25
+ require_relative "layout_index"
26
+ require_relative "method_candidate"
27
+ require_relative "writer"
19
28
 
20
29
  module Rigor
21
30
  module SigGen
@@ -44,6 +53,15 @@ module Rigor
44
53
  # - A declared `void` return is never compared as a value type. It is the author's statement that the
45
54
  # return is not part of the contract, which no inference can synthesize, so the method classifies
46
55
  # `equivalent` carrying `void` itself as the declared spelling (#836; see {#declares_void?}).
56
+ # - A declared `untyped` return (the RBS `Bases::Any` spelling, including the one ADR-93 synthesizes for
57
+ # a parameter-only inline `# @rbs` annotation) is the ABSENCE of a return statement, not the author's
58
+ # word about it — the opposite of `void`. It is treated like no declaration at all: the inferred return
59
+ # is proposed as `tighter-return`, carrying `untyped` as the declared spelling so `--diff` still shows a
60
+ # declaration existed (#995; see {#declared_untyped?}).
61
+ # - A `tighter-return` over the class's own declaration replaces the return and nothing else: the rest of
62
+ # the line is the declaration's, spelled as written, so `--overwrite` never changes which calls the
63
+ # signature accepts. A declaration with more than one overload, or one spelled as an `alias`, is left
64
+ # alone (#1436; see {#tighter_return_line}).
47
65
  # - A proposal that erases to an RBS literal never tightens an existing declaration: the declared type is
48
66
  # the author's abstraction over the body and the literal is what it hides (#837; see
49
67
  # {#pins_literal_over_declaration?}). A method with no declaration is unaffected — clause 1 still emits
@@ -62,11 +80,18 @@ module Rigor
62
80
  # produced by {ObservationCollector}. An empty Hash (the default)
63
81
  # means "no observations available; emit `untyped` for every
64
82
  # parameter position" per ADR-5 clause 2.
65
- def initialize(configuration:, paths:, observations: {}, include_private: false)
83
+ # @param overwrite — `--overwrite`: a member declared inline whose `sig/` copy disagrees is proposed as an
84
+ # `inline-overwrite` of the whole member instead of refused (ADR-112 WD4).
85
+ def initialize(configuration:, paths:, observations: {}, include_private: false, effect_annotator: nil,
86
+ overwrite: false)
66
87
  @configuration = configuration
88
+ @overwrite = overwrite
67
89
  @paths = paths
68
90
  @observations = normalize_observations(observations)
69
91
  @include_private = include_private
92
+ # ADR-103 WD9 — `nil` when the project's `effects:` opt-in is off, which is what keeps an
93
+ # effects-off run byte-identical to one from before the annotation slot was filled.
94
+ @effect_annotator = effect_annotator
70
95
  # Per-file scratch state. `analyse_file` resets each one to a fresh container for every file walked so
71
96
  # candidates from one file don't leak into another; initialising empty here gives downstream consumers
72
97
  # (`build_candidate`, `method_def_prefix`) a never-nil invariant without per-call-site defensive guards.
@@ -96,13 +121,99 @@ module Rigor
96
121
  resolved = resolve_paths(@paths)
97
122
  @environment = build_environment(resolved)
98
123
  candidates = resolved.flat_map { |path| analyse_file(path, @environment) }
99
- demote_overridden_base_methods(
100
- demote_unresolvable_superclasses(resolve_superclass_spellings(candidates))
124
+ annotate_effects(
125
+ demote_overridden_base_methods(
126
+ demote_unresolvable_superclasses(demote_inline_generic_classes(resolve_superclass_spellings(candidates)))
127
+ )
101
128
  )
102
129
  end
103
130
 
104
131
  private
105
132
 
133
+ # ADR-103 WD9 — the annotation-emission slot ADR-14 reserved, run LAST so a candidate a later
134
+ # demotion turns into a skip never carries an annotation it will not print. Only emittable rows are
135
+ # asked: an annotation is a claim attached to a line, and a row that produces no line makes none.
136
+ def annotate_effects(candidates)
137
+ return candidates if @effect_annotator.nil?
138
+
139
+ candidates.map do |candidate|
140
+ next candidate unless Classification::EMITTABLE.include?(candidate.classification)
141
+
142
+ annotations, reason = @effect_annotator.annotate(
143
+ class_name: candidate.class_name, method_name: candidate.method_name, kind: candidate.kind,
144
+ path: candidate.path
145
+ )
146
+ reason.nil? && annotations.empty? ? candidate : candidate.with_effect_annotation(annotations, reason)
147
+ end
148
+ end
149
+
150
+ # ADR-112 WD4 — nothing is written that would open a class an inline declaration makes generic (`# @rbs
151
+ # generic T`), or a namespace nested in one, in a file the project's `sig/` does not already declare it in;
152
+ # nor into a `sig/` declaration of the class whose type parameters are named otherwise (`class Box[U]`
153
+ # beside an inline `Box[T]`, which rbs accepts), where a copied `-> T` would name a parameter nothing binds.
154
+ #
155
+ # The writer spells a class header without type parameters, and rbs rejects a class whose declarations
156
+ # disagree on them (`GenericParameterMismatchError`): the class, and every class whose signature mentions
157
+ # it, fails its definition build and reads `Dynamic[top]` — while `rigor check` still exits 0. Skipping
158
+ # costs the generic class its `sig/` members; the inline declaration still binds when the source is
159
+ # analysed. A class `sig/` already declares is written into that declaration, whose parameters are the
160
+ # project's own.
161
+ def demote_inline_generic_classes(candidates)
162
+ generic = inline_declarations.generic_classes
163
+ return candidates if generic.empty?
164
+
165
+ layout = LayoutIndex.new(signature_paths: @configuration.signature_paths)
166
+ candidates.map do |candidate|
167
+ next candidate unless Classification::EMITTABLE.include?(candidate.classification)
168
+ next candidate unless unsafe_for_generic?(candidate.class_name, generic, layout)
169
+
170
+ demoted_candidate(candidate, :inline_generic_class)
171
+ end
172
+ end
173
+
174
+ def unsafe_for_generic?(class_name, generic, layout)
175
+ return false if class_name.nil?
176
+
177
+ file = layout.file_for(class_name)
178
+ return opens_generic_class?(class_name, generic) if file.nil?
179
+
180
+ generic.key?(class_name) && signature_type_params(file, class_name) != generic.fetch(class_name)
181
+ end
182
+
183
+ # The type parameter names `file` declares `class_name` with; nil when it cannot be read, which counts as
184
+ # a mismatch — writing into a declaration whose parameters are unknown is the unsafe direction.
185
+ def signature_type_params(file, class_name)
186
+ @signature_type_params ||= {}
187
+ @signature_type_params.fetch([file, class_name]) do
188
+ @signature_type_params[[file, class_name]] = read_signature_type_params(file, class_name)
189
+ end
190
+ end
191
+
192
+ def read_signature_type_params(file, class_name)
193
+ _buffer, _directives, decls = ::RBS::Parser.parse_signature(File.read(file))
194
+ find_declaration(decls, [], class_name)&.type_params&.map(&:name)
195
+ rescue StandardError
196
+ nil
197
+ end
198
+
199
+ def find_declaration(decls, prefix, class_name)
200
+ decls.each do |decl|
201
+ next unless decl.is_a?(::RBS::AST::Declarations::Class) || decl.is_a?(::RBS::AST::Declarations::Module)
202
+
203
+ name = (prefix + [decl.name.to_s.delete_prefix("::")]).join("::")
204
+ return decl if name == class_name
205
+
206
+ found = find_declaration(decl.members, name.split("::"), class_name)
207
+ return found if found
208
+ end
209
+ nil
210
+ end
211
+
212
+ def opens_generic_class?(class_name, generic)
213
+ segments = class_name.split("::")
214
+ (1..segments.size).any? { |n| generic.key?(segments.first(n).join("::")) }
215
+ end
216
+
106
217
  # Issue #744 — a base class's method is NOT emitted when a project subclass overrides it and the
107
218
  # override is not emitted itself.
108
219
  #
@@ -229,7 +340,8 @@ module Rigor
229
340
  inferred_return: candidate.inferred_return, declared_return_rbs: candidate.declared_return_rbs,
230
341
  rbs: candidate.rbs, skip_reason: candidate.skip_reason,
231
342
  namespace_kinds: candidate.namespace_kinds, class_shells: candidate.class_shells,
232
- class_superclasses: superclasses
343
+ class_superclasses: superclasses, declared_rbs: candidate.declared_rbs,
344
+ declared_annotations: candidate.declared_annotations
233
345
  )
234
346
  end
235
347
 
@@ -353,7 +465,7 @@ module Rigor
353
465
  @class_superclasses = {}
354
466
  @meta_layouts = collect_meta_layouts(scope_index)
355
467
  register_meta_classes
356
- defs = collect_method_definitions(parse_result.value)
468
+ defs = collect_method_definitions(parse_result.value, scope_index)
357
469
  # Candidate construction freezes the per-file maps above (see {#build_candidate}), so every registration
358
470
  # pass has to be finished before the first `build_candidate` call. Meta members lead the RETURNED order —
359
471
  # a value class's members and constructors read first, ahead of the methods its block body defines.
@@ -386,7 +498,8 @@ module Rigor
386
498
  # - `@module_function_methods` records `(class_name, method_name)` pairs where a `module_function` (no
387
499
  # args) call preceded the `def` inside a module body. The renderer emits `def self?.name` for these, the
388
500
  # RBS spelling that matches the dual instance + singleton dispatch the runtime produces.
389
- def collect_method_definitions(root)
501
+ def collect_method_definitions(root, scope_index)
502
+ @declared_types = scope_index[root].declared_types
390
503
  out = []
391
504
  walk_defs(root, [], false, false, out)
392
505
  out
@@ -431,6 +544,11 @@ module Rigor
431
544
  # `prefix` stays the nesting the header is WRITTEN in for {#record_superclass}: `::` re-anchors the
432
545
  # declaration, not the superclass expression beside it, which Ruby still evaluates in the enclosing cref.
433
546
  def descend_into_namespace?(node, prefix, out)
547
+ # Issue #1518 — a `class self::X` / `module self::X` the indexer declined (`self` is opaque, as under
548
+ # `REGISTRY.first.class_eval`) names no class, so its body must not be written out under the lexical
549
+ # `X`. The indexer's identity table is the single source of that decision.
550
+ return true if declined_self_header?(node)
551
+
434
552
  child_prefix = Source::ConstantPath.declaration_prefix(prefix, node.constant_path)
435
553
  return false unless child_prefix
436
554
 
@@ -441,6 +559,12 @@ module Rigor
441
559
  true
442
560
  end
443
561
 
562
+ def declined_self_header?(node)
563
+ path = node.constant_path
564
+ path.is_a?(Prism::ConstantPathNode) && path.parent.is_a?(Prism::SelfNode) &&
565
+ !@declared_types.key?(path)
566
+ end
567
+
444
568
  # ADR-14: a generated subclass declaration MUST carry its superclass, or the sidecar `sig/` misrepresents
445
569
  # the class (inherited members vanish → receiver dispatch degrades to `Dynamic`) and, worse, a nested
446
570
  # reference to an inherited type re-declares the class as a bare namespace on the RBS side and can
@@ -551,29 +675,22 @@ module Rigor
551
675
  walk_defs(namespace_node.body, prefix, false, false, out)
552
676
  end
553
677
 
678
+ # The directive's reach is {Inference::ModuleFunctionState.each_sig_gen_statement}.
554
679
  def walk_statements(stmts_node, prefix, in_singleton_class, module_function_active, out)
555
- stmts_node.body.each do |stmt|
556
- if module_function_directive?(stmt)
557
- module_function_active = true
558
- next
559
- end
560
- walk_defs(stmt, prefix, in_singleton_class, module_function_active, out)
680
+ Inference::ModuleFunctionState.each_sig_gen_statement(stmts_node.body,
681
+ module_function_active) do |stmt, active|
682
+ walk_defs(stmt, prefix, in_singleton_class, active, out)
561
683
  end
562
684
  end
563
685
 
564
- def module_function_directive?(node)
565
- return false unless node.is_a?(Prism::CallNode)
566
- return false unless node.name == :module_function && node.receiver.nil?
567
-
568
- (node.arguments&.arguments || []).empty?
569
- end
570
-
571
686
  def collect_def_node(node, prefix, in_singleton_class, module_function_active, out)
572
687
  return if prefix.empty?
573
688
 
574
689
  kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
575
690
  class_name = prefix.join("::")
576
- @module_function_methods << [class_name, node.name] if module_function_active && kind == :instance
691
+ if Inference::ModuleFunctionState.sig_gen_module_function?(module_function_active, kind)
692
+ @module_function_methods << [class_name, node.name]
693
+ end
577
694
  out << [node, class_name, kind]
578
695
  end
579
696
 
@@ -678,10 +795,22 @@ module Rigor
678
795
  # `ObservationCollector`'s `.new` → `:initialize` routing), positional and keyword arg types come from the
679
796
  # per-position / per-keyword union of observed types; otherwise every position keeps `untyped` per ADR-5
680
797
  # clause 2.
681
- def initialize_stub_candidate(path, def_node, class_name)
798
+ #
799
+ # Issue #1436 — a constructor the class's own `sig/` already declares is `equivalent`, like any other
800
+ # declared member, unless the stub clears an `untyped` the declaration still carries: that is the
801
+ # `--params=observed` upgrade `--overwrite` applies ({Writer.fewer_untyped?}, the writer's own test).
802
+ # Without observations the stub is all `untyped` and never qualifies, so it is never listed as new.
803
+ def initialize_stub_candidate(path, def_node, class_name, scope)
682
804
  params = def_node.parameters
683
805
  rbs = "def initialize: (#{render_param_list(params, class_name, :initialize)})" \
684
806
  "#{block_signature_suffix(params)} -> void"
807
+ method_def = lookup_existing_method(class_name, :initialize, :instance, scope&.environment, scope)
808
+ member = signature_member(method_def, class_name)
809
+ if member && !Writer.fewer_untyped?(rbs, member.location&.source.to_s)
810
+ declared = declares_void?(method_def) ? VOID_RETURN_RBS : build_declared_return(method_def)&.erase_to_rbs
811
+ return equivalent(path, def_node, class_name, :instance, Type::Combinator.untyped, declared)
812
+ end
813
+
685
814
  build_candidate(
686
815
  path: path, class_name: class_name, method_name: :initialize,
687
816
  kind: :instance, classification: Classification::NEW_METHOD,
@@ -706,12 +835,12 @@ module Rigor
706
835
 
707
836
  observations = arity_matched_observations(class_name, method_name, params)
708
837
  parts = []
709
- params.requireds.each_index { |i| parts << positional_type(observations, i, "") }
838
+ params.requireds.each_index { |i| parts << positional_type(observations, i, "", class_name) }
710
839
  offset = params.requireds.size
711
- params.optionals.each_index { |i| parts << positional_type(observations, offset + i, "?") }
840
+ params.optionals.each_index { |i| parts << positional_type(observations, offset + i, "?", class_name) }
712
841
  parts << "*untyped" if params.rest
713
842
  parts.concat(Array.new(params.posts.size, "untyped"))
714
- params.keywords.each { |kw| parts << render_keyword_param(kw, observations) }
843
+ params.keywords.each { |kw| parts << render_keyword_param(kw, observations, class_name) }
715
844
  parts.concat(keyword_rest_parts(params))
716
845
  parts.join(", ")
717
846
  end
@@ -761,15 +890,15 @@ module Rigor
761
890
 
762
891
  # A bare union is a valid positional type in RBS (`(String | Integer, ?String | Integer)`), so unlike the
763
892
  # return position it is not parenthesised.
764
- def positional_type(observations, index, prefix)
893
+ def positional_type(observations, index, prefix, owner = nil)
765
894
  types = observations.filter_map { |obs| obs.positional[index] }
766
- "#{prefix}#{union_erase(types)}"
895
+ "#{prefix}#{union_erase(types, owner: owner)}"
767
896
  end
768
897
 
769
- def render_keyword_param(keyword, observations)
898
+ def render_keyword_param(keyword, observations, owner = nil)
770
899
  optional_marker = keyword.is_a?(Prism::OptionalKeywordParameterNode) ? "?" : ""
771
900
  types = observations.filter_map { |obs| obs.keyword[keyword.name] }
772
- rendered = types.empty? ? "untyped" : paren_wrap_union(union_erase(types))
901
+ rendered = types.empty? ? "untyped" : paren_wrap_union(union_erase(types, owner: owner))
773
902
  "#{optional_marker}#{keyword.name}: #{rendered}"
774
903
  end
775
904
 
@@ -789,7 +918,15 @@ module Rigor
789
918
  def classify_def(path, def_node, class_name, kind, scope_index)
790
919
  return nil if visibility_excludes?(def_node, class_name, kind, scope_index)
791
920
  return nil if initialize_excludes?(def_node, kind)
792
- return initialize_stub_candidate(path, def_node, class_name) if non_trivial_initialize?(def_node, kind)
921
+
922
+ inline = inline_declarations.lookup(path, class_name, def_node.name, kind)
923
+ if inline
924
+ return skipped(path, def_node, class_name, kind, :inline_declared) if skip_inline_declared?
925
+ return inline_def_candidate(path, def_node, class_name, kind, scope_index, inline) if inline.declared?
926
+ end
927
+ if non_trivial_initialize?(def_node, kind)
928
+ return initialize_stub_candidate(path, def_node, class_name, scope_index[def_node])
929
+ end
793
930
 
794
931
  inferred = infer_return_type(def_node, scope_index)
795
932
  return skipped(path, def_node, class_name, kind, :untyped_return) if inferred.nil? || dynamic_top?(inferred)
@@ -804,6 +941,242 @@ module Rigor
804
941
  end
805
942
  end
806
943
 
944
+ # ADR-112 WD4 — the inline declarations of every analysed file, read once per run. See
945
+ # {InlineDeclarations} for why the synthesized text rather than the environment.
946
+ def inline_declarations
947
+ @inline_declarations ||= InlineDeclarations.build(@environment)
948
+ end
949
+
950
+ def skip_inline_declared?
951
+ @configuration.sig_gen_inline_declared == :skip
952
+ end
953
+
954
+ # ADR-112 WD4 — a `def` the author declared inline is written from that declaration, as rbs-inline reads it,
955
+ # not from what the body infers: `rigor check` reads the `sig/` member beside the inline one (ADR-112 WD5),
956
+ # so a line built from inference (`untyped` parameters, a return the body happens to prove) would override
957
+ # or contradict the contract the author wrote beside the code.
958
+ #
959
+ # The one inferred part is a return the author left unwritten (`# @rbs name: String`, no `return:`) —
960
+ # ADR-107's mixed provenance, parameters authored and return generated — filled from the body; a body that
961
+ # types as `untyped` there is skipped exactly as an undeclared one is. `initialize` returns `void` by
962
+ # definition, whatever its last expression is.
963
+ def inline_def_candidate(path, def_node, class_name, kind, scope_index, inline)
964
+ types = inline.method_types
965
+ return_defaulted = inline.return_inferred
966
+ if return_defaulted && initialize_def?(def_node, kind)
967
+ void = ::RBS::Types::Bases::Void.new(location: nil)
968
+ types = types.map { |mt| defaulted_return?(mt) ? with_return_type(mt, void) : mt }
969
+ return_defaulted = false
970
+ end
971
+
972
+ scope = scope_index[def_node]
973
+ method_def = lookup_existing_method(class_name, def_node.name, kind, scope&.environment, scope)
974
+ head = "#{method_def_prefix(class_name, def_node.name, kind)}#{def_node.name}: "
975
+ if return_defaulted
976
+ return mixed_provenance_candidate(path, def_node, class_name, kind, scope_index, inline, method_def, head)
977
+ end
978
+
979
+ inline_candidate(path, class_name, def_node.name, kind, head + types.join(" | "), inline, method_def)
980
+ end
981
+
982
+ # A parameter-only annotation (`# @rbs name: String`, no `return:`): parameters authored, return inferred.
983
+ # Only the authored half is weighed against `sig/`. When the parameters differ the method is refused, even
984
+ # under `--overwrite` — the body is typed under the parameters the environment binds, which are `sig/`'s
985
+ # once it declares the member, so a return inferred for the new line would describe parameters about to
986
+ # change. When they match, the return is an ordinary inferred proposal: {#compare_against_declared}
987
+ # leaves a declared `void`, a deliberately wider type and a declined literal alone, and proposes a
988
+ # strictly narrower return as `tighter-return`, spelled with the authored parameters.
989
+ def mixed_provenance_candidate(path, def_node, class_name, kind, scope_index, inline, method_def, head) # rubocop:disable Metrics/ParameterLists
990
+ types = inline.method_types
991
+ existing = signature_member(method_def, class_name)
992
+ if existing && parameters_differ?(existing, types, def_node.name, class_name)
993
+ return skipped(path, def_node, class_name, kind, :inline_differs)
994
+ end
995
+
996
+ inferred, overloads = inferred_overloads(def_node, scope_index, class_name, types)
997
+ return skipped(path, def_node, class_name, kind, :untyped_return) if inferred.nil?
998
+
999
+ rbs = head + overloads.join(" | ")
1000
+ if existing && annotations_present?(existing, inline) && existing_types(existing, def_node.name)
1001
+ return compare_against_declared(path, def_node, class_name, kind, inferred, method_def, rendered: rbs)
1002
+ end
1003
+
1004
+ inline_candidate(path, class_name, def_node.name, kind, rbs, inline, method_def, inferred)
1005
+ end
1006
+
1007
+ # `[inferred return, overload strings]`, or `[nil, nil]` when the body types as `untyped`.
1008
+ def inferred_overloads(def_node, scope_index, class_name, types)
1009
+ inferred = infer_return_type(def_node, scope_index)
1010
+ return [nil, nil] if inferred.nil? || dynamic_top?(inferred)
1011
+
1012
+ [inferred, with_defaulted_return(types, paren_wrap_union(elaborated_rbs(inferred, owner: class_name)))]
1013
+ end
1014
+
1015
+ # Whether a `sig/` member's overloads, returns aside, differ from the inline declaration's. Such a member is
1016
+ # refused even under `--overwrite` when the inline return is to be inferred (see {#inline_def_candidate});
1017
+ # deleting the `sig/` member lets the next run infer it under the inline parameters.
1018
+ def parameters_differ?(existing, types, method_name, class_name)
1019
+ written = existing_types(existing, method_name)
1020
+ return false if written.nil?
1021
+
1022
+ !equivalence(class_name).same_parameters?(written, types)
1023
+ end
1024
+
1025
+ def initialize_def?(def_node, kind)
1026
+ kind == :instance && def_node.name == :initialize
1027
+ end
1028
+
1029
+ def defaulted_return?(method_type)
1030
+ method_type.type.return_type.is_a?(::RBS::Types::Bases::Any)
1031
+ end
1032
+
1033
+ def with_return_type(method_type, return_type)
1034
+ method_type.update(type: method_type.type.with_return_type(return_type))
1035
+ end
1036
+
1037
+ # A class name no project declares, standing in for the inferred return while `RBS::MethodType#to_s` spells
1038
+ # the rest of an overload, so the return itself keeps sig-gen's own spelling (`[Float, String]`, a folded
1039
+ # alias) rather than being re-spelled through a parse.
1040
+ INFERRED_RETURN_PLACEHOLDER = "Rigor__SigGenInferredReturn"
1041
+ private_constant :INFERRED_RETURN_PLACEHOLDER
1042
+
1043
+ # Each overload whose return rbs-inline defaulted to `untyped` takes `returned` (already spelled for a
1044
+ # return position); the rest stay as written.
1045
+ def with_defaulted_return(method_types, returned)
1046
+ method_types.map do |method_type|
1047
+ defaulted_return?(method_type) ? spell_with_return(method_type, returned) : method_type.to_s
1048
+ end
1049
+ end
1050
+
1051
+ # One overload spelled with `returned` (already spelled for a return position) as its return.
1052
+ def spell_with_return(method_type, returned)
1053
+ placeholder = ::RBS::Parser.parse_type(INFERRED_RETURN_PLACEHOLDER)
1054
+ with_return_type(method_type, placeholder).to_s.sub(/-> #{INFERRED_RETURN_PLACEHOLDER}\z/o, "-> #{returned}")
1055
+ end
1056
+
1057
+ # Classifies an inline-declared member's `rbs` line against what `sig/` already says for it (ADR-112 WD4, as
1058
+ # decided on #1422):
1059
+ #
1060
+ # - nothing on this class itself (or only the inline declaration) — `new-method`;
1061
+ # - the same types, and every annotation the author wrote inline — `equivalent`;
1062
+ # - anything else — the two declarations disagree, and neither is presumed right. Without `--overwrite`
1063
+ # the member is REFUSED (`sig.skipped.inline-differs`): nothing is written, and `--write` / `--check`
1064
+ # exit 1 until a person reconciles them. With `--overwrite` it is `inline-overwrite`, which replaces the
1065
+ # whole `sig/` member with the inline line — never a mix of the two, whose slots (and type variables)
1066
+ # need not correspond.
1067
+ def inline_candidate(path, class_name, method_name, kind, rbs, inline, method_def, inferred = nil) # rubocop:disable Metrics/ParameterLists
1068
+ fields = { path: path, class_name: class_name, method_name: method_name, kind: kind,
1069
+ inferred_return: inferred }
1070
+ existing = signature_member(method_def, class_name)
1071
+ if existing.nil?
1072
+ build_candidate(**fields, classification: Classification::NEW_METHOD, rbs: rbs,
1073
+ declared_annotations: inline.annotation_lines)
1074
+ elsif current_copy?(existing, rbs, inline, method_name, class_name)
1075
+ build_candidate(**fields, classification: Classification::EQUIVALENT)
1076
+ elsif @overwrite
1077
+ # An `attr_*` member is replaced in its own spelling, by the inline attribute's.
1078
+ line = attr_member?(existing) && inline.attr_line ? inline.attr_line : rbs
1079
+ build_candidate(**fields, classification: Classification::INLINE_OVERWRITE, rbs: line,
1080
+ declared_annotations: inline.annotation_lines,
1081
+ declared_rbs: squish(existing.location&.source))
1082
+ else
1083
+ build_candidate(**fields, classification: Classification::SKIPPED, skip_reason: :inline_differs)
1084
+ end
1085
+ end
1086
+
1087
+ # The member a project `.rbs` declares for `class_name` itself — not an ancestor's, and not the inline
1088
+ # declaration, whose buffer is the synthesizer's `virtual:` one. An `overloading?` member (`def x: … |
1089
+ # ...`) is not a counterpart either: it adds overloads to the inline declaration rather than restating
1090
+ # it, and sig-gen leaves it alone.
1091
+ def signature_member(method_def, class_name)
1092
+ return nil if method_def.nil?
1093
+
1094
+ method_def.defs.filter_map do |type_def|
1095
+ next unless type_def.defined_in.to_s.delete_prefix("::") == class_name
1096
+
1097
+ member = type_def.member
1098
+ next if member.respond_to?(:overloading?) && member.overloading?
1099
+
1100
+ member unless member.location&.buffer&.name.to_s.start_with?("virtual:")
1101
+ end.first
1102
+ end
1103
+
1104
+ # The two declarations are compared as types ({DeclarationEquivalence}): parameter names, union spelling
1105
+ # and a leading `::` that resolves to the same constant do not count; overload order does. The copy must
1106
+ # also carry every annotation the author wrote inline. A `sig/` member whose text is unavailable is left
1107
+ # as it is rather than refused for good.
1108
+ def current_copy?(member, rbs, inline, method_name, class_name)
1109
+ written = existing_types(member, method_name)
1110
+ mine = member_types(rbs)
1111
+ return true if written.nil? || mine.nil?
1112
+
1113
+ equivalence(class_name).same?(written, mine) && annotations_present?(member, inline)
1114
+ end
1115
+
1116
+ def annotations_present?(member, inline)
1117
+ (inline.annotations - member.annotations.map { |annotation| annotation.string.to_s.strip }).empty?
1118
+ end
1119
+
1120
+ def attr_member?(member)
1121
+ member.is_a?(::RBS::AST::Members::AttrReader) || member.is_a?(::RBS::AST::Members::AttrWriter) ||
1122
+ member.is_a?(::RBS::AST::Members::AttrAccessor)
1123
+ end
1124
+
1125
+ # The overloads a `sig/` member declares for `method_name`: a `def`'s, parsed from its own text; an
1126
+ # attribute's reader (`() -> T`) or writer (`(T) -> T`), from its type.
1127
+ def existing_types(member, method_name)
1128
+ return member_types(member.location&.source) if member.is_a?(::RBS::AST::Members::MethodDefinition)
1129
+ return nil unless attr_member?(member)
1130
+
1131
+ shape = method_name.to_s.end_with?("=") ? "(#{member.type}) -> #{member.type}" : "() -> #{member.type}"
1132
+ [::RBS::Parser.parse_method_type(shape)]
1133
+ rescue ::RBS::BaseError
1134
+ nil
1135
+ end
1136
+
1137
+ def equivalence(class_name)
1138
+ @equivalence ||= {}
1139
+ @equivalence[class_name] ||= DeclarationEquivalence.new(resolve: type_name_resolver(class_name))
1140
+ end
1141
+
1142
+ # A type name as written, resolved the way RBS resolves it inside `class_name`'s declaration: an absolute
1143
+ # `::Foo` is `Foo`; a relative `Foo` is the innermost `…::Foo` the environment knows, so inside `module NS`
1144
+ # it is `NS::Foo` when `NS` declares one, and `::Foo` only when it does not.
1145
+ def type_name_resolver(class_name)
1146
+ nesting = class_name.split("::")
1147
+ lambda do |name|
1148
+ next name.delete_prefix("::") if name.start_with?("::")
1149
+
1150
+ nesting.size.downto(1).map { |n| (nesting.first(n) + [name]).join("::") }
1151
+ .find { |candidate| class_known?(candidate) } || name
1152
+ end
1153
+ end
1154
+
1155
+ def class_known?(name)
1156
+ @class_known ||= {}
1157
+ @class_known.fetch(name) { @class_known[name] = Reflection.rbs_class_known?(name, environment: @environment) }
1158
+ end
1159
+
1160
+ # A member line's overloads, parsed with type names unresolved so the two sides compare as written.
1161
+ def member_types(source)
1162
+ member_overloads(source)&.map(&:method_type)
1163
+ end
1164
+
1165
+ # The same, with each overload's own annotations (`%a{pure} (Integer) -> String`).
1166
+ def member_overloads(source)
1167
+ return nil if source.nil?
1168
+
1169
+ _buffer, _directives, decls = ::RBS::Parser.parse_signature("class Rigor__SigGenProbe\n#{source}\nend\n")
1170
+ parsed = decls.first&.members&.first
1171
+ parsed.is_a?(::RBS::AST::Members::MethodDefinition) ? parsed.overloads : nil
1172
+ rescue ::RBS::BaseError
1173
+ nil
1174
+ end
1175
+
1176
+ def squish(text)
1177
+ text.to_s.gsub(/\s+/, " ").strip
1178
+ end
1179
+
807
1180
  # Mirrors the `def.return-type-mismatch` rule's body-type extraction: type the implicit-return expression
808
1181
  # under the scope the indexer associated with the body. The parameter bindings (typed `untyped` per the
809
1182
  # indexer's default) come from `with_local` inside `StatementEvaluator`; the result is the carrier the
@@ -813,9 +1186,10 @@ module Rigor
813
1186
  # types with the implicit-return expression's type. The earlier MVP only typed the implicit-return path,
814
1187
  # which routinely produced single-branch artefacts like `parse_options: () -> nil` (the actual runtime
815
1188
  # return is `options | nil`) or `find: () -> V` (actually `V | nil` via `return nil unless ...`). The walk
816
- # excludes nested `DefNode` / lambda / block scopes whose returns belong to different methods. Delegates
817
- # to {Rigor::Inference::DefReturnTyper} — the same body-typing + explicit-return-union the `rigor
818
- # annotate` def-line annotator uses.
1189
+ # includes a `return` inside an ordinary block, which exits the method, and excludes the
1190
+ # {Rigor::Inference::ReturnBarrier} bodies — nested `def`, lambda, `define_method` — whose returns belong to a
1191
+ # different method (issue #1382). Delegates to {Rigor::Inference::DefReturnTyper} — the same body-typing +
1192
+ # explicit-return-union the `rigor annotate` def-line annotator uses.
819
1193
  def infer_return_type(def_node, scope_index)
820
1194
  Inference::DefReturnTyper.call(def_node, scope_index)
821
1195
  end
@@ -858,10 +1232,16 @@ module Rigor
858
1232
  VOID_RETURN_RBS = "void"
859
1233
  private_constant :VOID_RETURN_RBS
860
1234
 
861
- def compare_against_declared(path, def_node, class_name, kind, inferred, method_def)
1235
+ # @param rendered — the line a proposal is spelled as when it is not the ordinary inferred one: a
1236
+ # parameter-only inline annotation keeps its authored parameters ({#mixed_provenance_candidate}).
1237
+ def compare_against_declared(path, def_node, class_name, kind, inferred, method_def, rendered: nil)
862
1238
  return equivalent(path, def_node, class_name, kind, inferred, VOID_RETURN_RBS) if declares_void?(method_def)
863
1239
 
864
1240
  declared = build_declared_return(method_def)
1241
+ if declared_untyped?(declared)
1242
+ return declared_untyped_candidate(path, def_node, class_name, kind, inferred, method_def, rendered: rendered)
1243
+ end
1244
+
865
1245
  declared_rbs = declared&.erase_to_rbs
866
1246
  inferred_rbs = inferred.erase_to_rbs
867
1247
 
@@ -873,6 +1253,17 @@ module Rigor
873
1253
  return equivalent(path, def_node, class_name, kind, inferred, declared_rbs)
874
1254
  end
875
1255
 
1256
+ tighter_return_candidate(path, def_node, class_name, kind, inferred, method_def, declared_rbs, rendered)
1257
+ end
1258
+
1259
+ # Both tightening branches end here. The proposal is the declaration with its return replaced
1260
+ # ({#tighter_return_line}); when there is no single declared overload to replace it in, the method is
1261
+ # `equivalent` — the declaration stands, as it does for any narrowing the generator declines. `declared_rbs`
1262
+ # carries the class's own declaration so `--diff` and `--check` show the line the proposal replaces.
1263
+ def tighter_return_candidate(path, def_node, class_name, kind, inferred, method_def, declared_return, rendered) # rubocop:disable Metrics/ParameterLists
1264
+ line = rendered || tighter_return_line(def_node, method_def, class_name, kind, inferred)
1265
+ return equivalent(path, def_node, class_name, kind, inferred, declared_return) if line.nil?
1266
+
876
1267
  build_candidate(
877
1268
  path: path,
878
1269
  class_name: class_name,
@@ -880,11 +1271,68 @@ module Rigor
880
1271
  kind: kind,
881
1272
  classification: Classification::TIGHTER_RETURN,
882
1273
  inferred_return: inferred,
883
- declared_return_rbs: declared_rbs,
884
- rbs: render_rbs_line(def_node, inferred, class_name, kind)
1274
+ declared_return_rbs: declared_return,
1275
+ declared_rbs: declared_line(method_def, class_name),
1276
+ rbs: line
885
1277
  )
886
1278
  end
887
1279
 
1280
+ # Issue #1436 — a `tighter-return` changes the return and only the return. It used to be rendered like a
1281
+ # new method, from the `def`'s runtime shape: every parameter `untyped`, `(?)` spelled out as
1282
+ # `(*untyped)`, names and block types dropped. Under `--overwrite` that replaced a declared `(Integer x)`
1283
+ # with `(untyped)` — a change to which calls the signature accepts, made by a proposal that claims to be
1284
+ # about the return. The class's own declaration is kept whole instead: its visibility, the overload's
1285
+ # annotations, type parameters, parameters and block, with only the return replaced.
1286
+ #
1287
+ # Three shapes are declined (`nil`):
1288
+ # - several overloads. The body is typed once, so the inferred return covers every overload together and
1289
+ # says nothing about which overload returns what: collapsing them into one line would drop the
1290
+ # overloads, and giving each the joint return would widen the overloads the author wrote narrower.
1291
+ # - a name the class's own `sig/` spells as an `alias`. Its parameters are the aliased method's, and a
1292
+ # proposal would read as a rewrite of that method's line rather than of the alias. An alias only an
1293
+ # ancestor declares takes the override path below, like any other ancestor declaration.
1294
+ # - nothing else: when only an ancestor declares the method, the proposal is a new override on this
1295
+ # class, and is rendered from the `def`'s own shape like any new method. The ancestor's parameters
1296
+ # describe the ancestor's `def`, not this one, which may take other arguments (ADR-14 clause 2).
1297
+ def tighter_return_line(def_node, method_def, class_name, kind, inferred)
1298
+ member = signature_member(method_def, class_name)
1299
+ return render_rbs_line(def_node, inferred, class_name, kind) if member.nil?
1300
+ return nil if method_def.alias_of
1301
+
1302
+ overload = sole_declared_overload(member, method_def, def_node.name)
1303
+ return nil if overload.nil?
1304
+
1305
+ returned = paren_wrap_union(elaborated_rbs(inferred, owner: class_name))
1306
+ annotations = overload.annotations.map { |a| "#{a.location&.source || "%a{#{a.string}}"} " }.join
1307
+ visibility = member.respond_to?(:visibility) && member.visibility ? "#{member.visibility} " : ""
1308
+ "#{visibility}#{method_def_prefix(class_name, def_node.name, kind)}#{def_node.name}: " \
1309
+ "#{annotations}#{spell_with_return(overload.method_type, returned)}"
1310
+ end
1311
+
1312
+ # The one overload the class's own `sig/` member declares, parsed from its text with its annotations so it
1313
+ # keeps the spelling the author wrote (`Integer`, not the resolved `::Integer`). An attribute declares one
1314
+ # implicitly and carries no overload annotations.
1315
+ def sole_declared_overload(member, method_def, method_name)
1316
+ return nil unless method_def.method_types.size == 1
1317
+
1318
+ overloads =
1319
+ if member.is_a?(::RBS::AST::Members::MethodDefinition)
1320
+ member_overloads(member.location&.source)
1321
+ else
1322
+ existing_types(member, method_name)&.map do |method_type|
1323
+ ::RBS::AST::Members::MethodDefinition::Overload.new(method_type: method_type, annotations: [])
1324
+ end
1325
+ end
1326
+ overloads&.size == 1 ? overloads.first : nil
1327
+ end
1328
+
1329
+ # The class's own declaration, as the `-` line of `--diff` and `--check` shows it. A method only an
1330
+ # ancestor declares has none: the proposal adds an override rather than replacing a line.
1331
+ def declared_line(method_def, class_name)
1332
+ source = signature_member(method_def, class_name)&.location&.source
1333
+ source && squish(source)
1334
+ end
1335
+
888
1336
  # Issue #836 — a declared `void` is return INTENT, never a wide value type waiting to be narrowed, so it
889
1337
  # is not compared against the body at all.
890
1338
  #
@@ -907,6 +1355,27 @@ module Rigor
907
1355
  method_def.method_types.any? { |mt| mt.type.return_type.is_a?(RBS::Types::Bases::Void) }
908
1356
  end
909
1357
 
1358
+ # Issue #995 — a declared `untyped` (RBS `Bases::Any`, translated by
1359
+ # {Inference::RbsTypeTranslator} to `Type::Dynamic`) says nothing at all, unlike `void` (#836), which is
1360
+ # the author's word that the value is not part of the contract. `tighter?`'s acceptance check cannot
1361
+ # tell the two apart — `untyped` accepts and is accepted by everything, so its backward check, which
1362
+ # exists to refuse a declaration that carries real information the inferred form would narrow, also
1363
+ # refused this one — and the candidate fell into `equivalent`: neither emitted nor counted among the
1364
+ # skips. ADR-93's inline `# @rbs param: Type` annotation (no return clause) synthesizes exactly this
1365
+ # spelling for every method it documents, so the silent drop hit the parameter-annotated, return-bare
1366
+ # shape ADR-14 exists to help.
1367
+ def declared_untyped?(declared)
1368
+ declared.is_a?(Type::Dynamic)
1369
+ end
1370
+
1371
+ # Proposed without {#compare_against_declared}'s tightening guards: a declared `untyped` return carries no
1372
+ # information to weigh {#tighter?} or {#literal_decline?} against, the same position a method with no
1373
+ # declaration at all is in. The line is built as for any tightening ({#tighter_return_line}), and
1374
+ # `declared_return_rbs` carries `"untyped"` so the `[tighter, was: untyped]` print tag says one existed.
1375
+ def declared_untyped_candidate(path, def_node, class_name, kind, inferred, method_def, rendered: nil)
1376
+ tighter_return_candidate(path, def_node, class_name, kind, inferred, method_def, "untyped", rendered)
1377
+ end
1378
+
910
1379
  def build_declared_return(method_def)
911
1380
  translated = method_def.method_types.filter_map { |mt| translate_method_type_return(mt) }
912
1381
  return nil if translated.empty?
@@ -914,10 +1383,19 @@ module Rigor
914
1383
  translated.size == 1 ? translated.first : Type::Combinator.union(*translated)
915
1384
  end
916
1385
 
1386
+ # `alias_expander:` matters here as much as it does at any dispatch site (see
1387
+ # {Inference::RbsTypeTranslator}'s own doc comment): without it, a declared return spelled as a
1388
+ # project alias (`Type::t`, `Environment::ordering`, `Inference::closure_classification`) degrades
1389
+ # to `Dynamic[Top]` exactly like a bare `untyped` would, and {#declared_untyped?} cannot tell an
1390
+ # author's meaningful alias apart from an absent declaration. That collapsed a well-typed, already
1391
+ # exact declaration into a spurious {#declared_untyped_candidate} the moment #995 made "declared
1392
+ # untyped" a proposable position — the ADR-107 G3 gate caught it on this repo's own `sig/` tree
1393
+ # (`Environment#class_ordering`'s `-> ordering` and every `-> Type::t` return in `type.rbs` /
1394
+ # `inference.rbs` / `scope.rbs`).
917
1395
  def translate_method_type_return(method_type)
918
1396
  Inference::RbsTypeTranslator.translate(
919
1397
  method_type.type.return_type,
920
- self_type: nil, instance_type: nil, type_vars: {}
1398
+ self_type: nil, instance_type: nil, type_vars: {}, alias_expander: @environment&.rbs_loader
921
1399
  )
922
1400
  rescue StandardError
923
1401
  nil
@@ -1097,15 +1575,54 @@ module Rigor
1097
1575
  params = def_node.parameters
1098
1576
  head = "(#{render_param_list(params, class_name, def_node.name)})#{block_signature_suffix(params)}"
1099
1577
  prefix = method_def_prefix(class_name, def_node.name, kind)
1100
- "#{prefix}#{def_node.name}: #{head} -> #{paren_wrap_union(elaborated_rbs(inferred))}"
1578
+ "#{prefix}#{def_node.name}: #{head} -> #{paren_wrap_union(elaborated_rbs(inferred, owner: class_name))}"
1101
1579
  end
1102
1580
 
1103
1581
  # Routes the inferred carrier through {TypeElaborator} so bare generic nominals (`Array` / `Hash` / `Set`
1104
1582
  # / `Range` / `Enumerable`) get their `untyped` type parameters filled in before erasing to RBS. The
1105
1583
  # elaborator consults the class's RBS-declared type-parameter list via
1106
1584
  # `Reflection.class_type_param_names`.
1107
- def elaborated_rbs(type)
1108
- TypeElaborator.elaborate(type, environment: @environment).erase_to_rbs
1585
+ # `owner:` is the class or module the rendered member belongs to. {AliasIndex} needs it to judge whether
1586
+ # a matching alias is one this reader would expect to see; a call site that cannot supply it renders the
1587
+ # type unfolded, which is the same output as before #1002.
1588
+ def elaborated_rbs(type, owner: nil)
1589
+ alias_index.fold(TypeElaborator.elaborate(type, environment: @environment).erase_to_rbs, owner)
1590
+ end
1591
+
1592
+ # Issue #1002 — the reverse of {#translate_method_type_return}'s `alias_expander:`. Folding happens here,
1593
+ # at the single seam every rendered type passes through (`render_rbs_line`, `union_erase` for parameter
1594
+ # and ivar positions, the attr path), so `--print`, `--diff`, `--write` and `--format=json` cannot drift
1595
+ # from one another: they all read the `rbs` string a candidate was built with. Deliberately NOT in
1596
+ # {Type::Union#erase_to_rbs} — that carrier method is the type model's RBS surface, shared with
1597
+ # diagnostics, `rigor type-of` and the engine's own comparisons, and none of those should start naming a
1598
+ # project alias. Sig-gen's rendering preference is a CLI concern and stays on the CLI side.
1599
+ def alias_index
1600
+ @alias_index ||= AliasIndex.build(
1601
+ environment: @environment,
1602
+ signature_paths: project_signature_paths,
1603
+ excluded_paths: plugin_signature_paths
1604
+ )
1605
+ end
1606
+
1607
+ # The project's OWN signature paths, resolved exactly as `Environment.for_project` resolves its
1608
+ # `resolved_paths` — the configured list, or `<root>/sig` when the project configured none. The
1609
+ # loader's `signature_paths` is NOT the same list: it also carries plugin signature paths, gem `sig/`
1610
+ # directories discovered under the project's bundle, the `rbs collection` tree and Rigor's own gem
1611
+ # overlays, none of which declare vocabulary this project chose.
1612
+ def project_signature_paths
1613
+ configured = @configuration.signature_paths
1614
+ return configured unless configured.nil?
1615
+
1616
+ default = Pathname(Dir.pwd) / "sig"
1617
+ default.directory? ? [default] : []
1618
+ end
1619
+
1620
+ # A loaded plugin's own signature directory is the plugin's vocabulary, not the project's, whether the
1621
+ # plugin tier contributed it or the project wired it into `signature_paths:` itself (#697).
1622
+ def plugin_signature_paths
1623
+ @environment&.plugin_registry&.signature_paths || []
1624
+ rescue StandardError
1625
+ []
1109
1626
  end
1110
1627
 
1111
1628
  # RBS / Steep require return-position unions to be parenthesised when they appear bare at the top level of
@@ -1131,15 +1648,15 @@ module Rigor
1131
1648
  false
1132
1649
  end
1133
1650
 
1134
- def union_erase(types)
1651
+ def union_erase(types, owner: nil)
1135
1652
  return "untyped" if types.empty?
1136
- return elaborated_rbs(types.first) if types.size == 1
1653
+ return elaborated_rbs(types.first, owner: owner) if types.size == 1
1137
1654
 
1138
1655
  # `Type::Combinator.union` dedupes by structural type equality. The carrier-level `erase_to_rbs` now
1139
1656
  # absorbs `untyped` members and dedupes the post-erase strings (`String | String` → `String` for
1140
1657
  # distinct `Constant<"Alice">` / `Constant<"Bob">` envelopes), so the sig-gen layer only needs to
1141
1658
  # elaborate bare generics before erasing.
1142
- elaborated_rbs(Type::Combinator.union(*types))
1659
+ elaborated_rbs(Type::Combinator.union(*types), owner: owner)
1143
1660
  end
1144
1661
 
1145
1662
  # ADR-14 slice 4 — `attr_reader` / `attr_writer` / `attr_accessor` recognition. Each Symbol-named entry in
@@ -1155,6 +1672,7 @@ module Rigor
1155
1672
  attr_writer: [:writer],
1156
1673
  attr_accessor: %i[reader writer]
1157
1674
  }.freeze
1675
+ Ractor.make_shareable(ATTR_KINDS)
1158
1676
  private_constant :ATTR_KINDS
1159
1677
 
1160
1678
  # Per-file context the attr_* walker threads through its recursive descent. Keeps parameter lists in
@@ -1363,6 +1881,12 @@ module Rigor
1363
1881
  end
1364
1882
 
1365
1883
  def build_attr_candidate(class_name, method_name, variant, ivar_type, ctx)
1884
+ inline = inline_declarations.lookup(ctx.path, class_name, method_name, :instance)
1885
+ if inline
1886
+ return attr_skipped(ctx.path, class_name, method_name, :inline_declared) if skip_inline_declared?
1887
+ return inline_attr_candidate(class_name, method_name, inline, ctx) if inline.declared?
1888
+ end
1889
+
1366
1890
  if ivar_type.nil? || dynamic_top?(ivar_type)
1367
1891
  return attr_skipped(ctx.path, class_name, method_name, :untyped_return)
1368
1892
  end
@@ -1377,6 +1901,15 @@ module Rigor
1377
1901
  end
1378
1902
  end
1379
1903
 
1904
+ # ADR-112 WD4 — an attribute typed inline (`attr_reader :name #: String`) is written from that type, in
1905
+ # the long `def` form the rest of the attr path uses.
1906
+ def inline_attr_candidate(class_name, method_name, inline, ctx)
1907
+ scope = ctx.scope_index.each_value.first
1908
+ method_def = lookup_existing_method(class_name, method_name, :instance, scope&.environment, scope)
1909
+ rbs = "def #{method_name}: #{inline.method_types.join(' | ')}"
1910
+ inline_candidate(ctx.path, class_name, method_name, :instance, rbs, inline, method_def)
1911
+ end
1912
+
1380
1913
  def attr_new_candidate(path, class_name, method_name, variant, ivar_type)
1381
1914
  build_candidate(
1382
1915
  path: path,
@@ -1385,7 +1918,7 @@ module Rigor
1385
1918
  kind: :instance,
1386
1919
  classification: Classification::NEW_METHOD,
1387
1920
  inferred_return: ivar_type,
1388
- rbs: render_attr_rbs_line(method_name, variant, ivar_type)
1921
+ rbs: render_attr_rbs_line(method_name, variant, ivar_type, class_name)
1389
1922
  )
1390
1923
  end
1391
1924
 
@@ -1402,7 +1935,7 @@ module Rigor
1402
1935
  path: path, class_name: class_name, method_name: method_name,
1403
1936
  kind: :instance, classification: Classification::TIGHTER_RETURN,
1404
1937
  inferred_return: ivar_type, declared_return_rbs: declared_rbs,
1405
- rbs: render_attr_rbs_line(method_name, variant, ivar_type)
1938
+ rbs: render_attr_rbs_line(method_name, variant, ivar_type, class_name)
1406
1939
  )
1407
1940
  end
1408
1941
 
@@ -1426,8 +1959,8 @@ module Rigor
1426
1959
  # form can normalise post-emit; the writer-side member detection (slice 2) treats existing `attr_*`
1427
1960
  # declarations as user-authored so a paired source-side `attr_reader` never produces a duplicate `def`
1428
1961
  # insertion.
1429
- def render_attr_rbs_line(method_name, variant, ivar_type)
1430
- erased = elaborated_rbs(ivar_type)
1962
+ def render_attr_rbs_line(method_name, variant, ivar_type, owner = nil)
1963
+ erased = elaborated_rbs(ivar_type, owner: owner)
1431
1964
  wrapped = paren_wrap_union(erased)
1432
1965
  case variant
1433
1966
  when :reader then "def #{method_name}: () -> #{wrapped}"
@@ -1447,7 +1980,7 @@ module Rigor
1447
1980
  types = meta_member_types(class_name, layout.member_names)
1448
1981
  shape = MetaClassShape.of(
1449
1982
  kind: layout.kind, members: layout.member_names, keyword_init: layout.keyword_init,
1450
- member_types: types.transform_values { |type| paren_wrap_union(union_erase([type])) }
1983
+ member_types: types.transform_values { |type| paren_wrap_union(union_erase([type], owner: class_name)) }
1451
1984
  )
1452
1985
  shape.member_decls.filter_map do |member|
1453
1986
  meta_member_candidate(path, class_name, member, types, scope, environment)