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
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Rigor
6
+ module Plugin
7
+ # One **template unit** — a non-Ruby source file a plugin compiled into Ruby, handed to the engine to
8
+ # be parsed and typed like a file (ADR-16 Tier D, revived by #392 as this seam; design note
9
+ # `docs/design/20260816-effect-labels.md` § 11.3).
10
+ #
11
+ # ADR-16's Tier D declared `external_files:` — "files evaluated as if their body were pasted at a
12
+ # declared call site, with `self` typed as a declared class" — and ADR-60 WD1 removed it for want of a
13
+ # consumer. What Tier D lacked, and what this carries, is a **source transform with a line map** ahead
14
+ # of parsing: the file on disk is not Ruby, so the engine analyses `ruby_source` and reports at the
15
+ # template's own lines.
16
+ #
17
+ # A unit is pure data. The plugin's transform runs ONCE, on the parent, before any analysis; nothing
18
+ # here is a callable, so the unit survives `Marshal` into a fork-pool worker exactly as the macro
19
+ # substrate's value objects do.
20
+ #
21
+ # - `logical_name` — the name the unit is keyed by, independent of the handler: `users/show.html`, not
22
+ # `users/show.html.erb`. Becomes the `view:<logical_name>` effect-unit key, so an ERB → Haml rewrite
23
+ # is not a rename.
24
+ # - `path` — the template file as the user wrote it. Every diagnostic and every source trace names it.
25
+ # - `ruby_source` — the compiled Ruby the engine parses.
26
+ # - `line_map` — `{ ruby_source line => template line }`, 1-based. A line the map does not mention
27
+ # anchors at the nearest mapped line before it, and at line 1 when there is none. **An EMPTY map is
28
+ # the identity, not "no positions"**: every compiled line reports at its own number and the columns
29
+ # pass through untouched. That is correct only for a transform that emits nothing the template did
30
+ # not contain — a transform that prepends so much as one preamble line and omits the map reports
31
+ # every finding off by that many lines. Omit it only for a byte-preserving transform.
32
+ # - `self_type` — the fully-qualified class the body's `self` is typed as, or nil for a bare body.
33
+ # - `locals` — `{ name => type name }`, the parameters the render site passes.
34
+ # - `ivar_seeds` — `{ "@name" => type name }`, the assigns the rendering action set.
35
+ # - `transform_id` — the compiler's identity (`"erubi-1.13"`). Joins the unit digest, so re-compiling
36
+ # with a different compiler re-analyses. Defaults to the declaring plugin's `id@version`.
37
+ #
38
+ # - `suppressed_rules` — rule-id PREFIXES the engine drops for this unit's diagnostics (#393). The
39
+ # per-unit rule posture: a compiled template is not code the user wrote, and a plugin that knows its
40
+ # compiler's idioms is the only party that can say which families of finding are meaningful there.
41
+ # `["call."]` is what an ERB plugin declares while the synthesised receivers are still coarse — the
42
+ # unit still contributes its EFFECTS, which is what the view layer is analysed for first, without
43
+ # the typing half costing the project a diagnostic per line. Empty means "report everything", which
44
+ # is what a plugin whose synthesis it trusts should declare.
45
+ #
46
+ # A type name that resolves to nothing is not an error here: the engine falls back to `Dynamic`, which
47
+ # taints honestly rather than fabricating a type the plugin could not justify (ADR-5).
48
+ class TemplateUnit
49
+ # Bumped whenever the engine changes what it synthesises from a unit — the seeding rules, the
50
+ # scope binding, the effect-unit key spelling. It rides the unit digest, so a change here
51
+ # invalidates every cached run that analysed a unit, exactly as a changed transform does.
52
+ SYNTHESIS_VERSION = 2
53
+
54
+ # The prefix an effect-unit key carries for a template unit. Deliberately not a {Effects::MethodKey}
55
+ # shape: a view has no owner class and no selector, and spelling one would put a method in the
56
+ # snapshot that no call site can name.
57
+ KEY_PREFIX = "view:"
58
+
59
+ IVAR_NAME = /\A@[A-Za-z_][A-Za-z0-9_]*\z/
60
+ private_constant :IVAR_NAME
61
+
62
+ LOCAL_NAME = /\A[a-z_][A-Za-z0-9_]*\z/
63
+ private_constant :LOCAL_NAME
64
+
65
+ # A rule prefix is a dotted family (`call.`) or a whole rule id (`call.undefined-method`); the match
66
+ # is `String#start_with?`, so the former covers the latter. A bare `""` would suppress the unit
67
+ # entirely and is refused.
68
+ RULE_PREFIX = /\A[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*\.?\z/
69
+ private_constant :RULE_PREFIX
70
+
71
+ attr_reader :logical_name, :path, :ruby_source, :line_map, :self_type, :locals, :ivar_seeds,
72
+ :transform_id, :suppressed_rules
73
+
74
+ # Every field is one declared property of the unit; grouping them behind a context object would only
75
+ # move the same list into a second value class.
76
+ def initialize(logical_name:, path:, ruby_source:, line_map: {}, self_type: nil, locals: {}, # rubocop:disable Metrics/ParameterLists
77
+ ivar_seeds: {}, transform_id: nil, suppressed_rules: [])
78
+ @logical_name = validate_string!("logical_name", logical_name)
79
+ @path = validate_string!("path", path)
80
+ @ruby_source = validate_source!(ruby_source)
81
+ @line_map = validate_line_map!(line_map)
82
+ @self_type = self_type.nil? ? nil : validate_string!("self_type", self_type)
83
+ @locals = validate_bindings!("locals", locals, LOCAL_NAME)
84
+ @ivar_seeds = validate_bindings!("ivar_seeds", ivar_seeds, IVAR_NAME)
85
+ @transform_id = transform_id.nil? ? nil : validate_string!("transform_id", transform_id)
86
+ @suppressed_rules = validate_rule_prefixes!(suppressed_rules)
87
+ freeze
88
+ end
89
+
90
+ # The effect-unit key and the snapshot key: `view:users/show.html`.
91
+ def unit_key
92
+ "#{KEY_PREFIX}#{@logical_name}"
93
+ end
94
+
95
+ # The unit's cache identity — **source bytes + transform id + synthesis version**, the triple
96
+ # `docs/internal-spec/cache.md` § computed-value keys asks a synthesised input to carry. The
97
+ # template's own bytes are deliberately NOT in it: the engine analyses `ruby_source`, and two
98
+ # templates that compile to the same Ruby have the same answer.
99
+ #
100
+ # @param fallback_transform_id — the declaring plugin's `id@version`, used when the unit
101
+ # named no transform of its own.
102
+ def digest(fallback_transform_id = nil)
103
+ Digest::SHA256.hexdigest(
104
+ [
105
+ "source:#{@ruby_source}",
106
+ "transform:#{@transform_id || fallback_transform_id || 'unknown'}",
107
+ "suppressed:#{@suppressed_rules.join(',')}",
108
+ "synthesis:#{SYNTHESIS_VERSION}"
109
+ ].join("\x00")
110
+ )
111
+ end
112
+
113
+ # The template line a `ruby_source` line reports at. Falls back to the nearest mapped line before it
114
+ # — a synthesised prologue or a buffer-append the compiler emitted between two template lines still
115
+ # points into the template rather than past its end — and to line 1 when the map says nothing at all.
116
+ def template_line(ruby_line)
117
+ return ruby_line if @line_map.empty?
118
+ return @line_map[ruby_line] if @line_map.key?(ruby_line)
119
+
120
+ before = @line_map.keys.select { |line| line < ruby_line }
121
+ before.empty? ? 1 : @line_map[before.max]
122
+ end
123
+
124
+ def to_h
125
+ {
126
+ "logical_name" => @logical_name, "path" => @path, "ruby_source" => @ruby_source,
127
+ "line_map" => @line_map, "self_type" => @self_type, "locals" => @locals,
128
+ "ivar_seeds" => @ivar_seeds, "transform_id" => @transform_id,
129
+ "suppressed_rules" => @suppressed_rules
130
+ }
131
+ end
132
+
133
+ def ==(other)
134
+ other.is_a?(TemplateUnit) && other.to_h == to_h
135
+ end
136
+ alias eql? ==
137
+
138
+ def hash
139
+ [self.class, to_h].hash
140
+ end
141
+
142
+ private
143
+
144
+ def validate_string!(field, value)
145
+ text = value.to_s
146
+ raise ArgumentError, "TemplateUnit #{field} must be a non-empty String" if text.empty?
147
+
148
+ text.dup.freeze
149
+ end
150
+
151
+ def validate_source!(value)
152
+ raise ArgumentError, "TemplateUnit ruby_source must be a String" unless value.is_a?(String)
153
+
154
+ value.dup.freeze
155
+ end
156
+
157
+ # 1-based on both sides. A zero or negative line is a compiler bug the engine would turn into a
158
+ # diagnostic positioned off the top of the file, so it is refused at construction.
159
+ def validate_line_map!(value)
160
+ raise ArgumentError, "TemplateUnit line_map must be a Hash" unless value.is_a?(Hash)
161
+
162
+ value.to_h do |from, to|
163
+ unless from.is_a?(Integer) && to.is_a?(Integer) && from.positive? && to.positive?
164
+ raise ArgumentError, "TemplateUnit line_map must map positive Integer to positive Integer"
165
+ end
166
+
167
+ [from, to]
168
+ end.freeze
169
+ end
170
+
171
+ def validate_rule_prefixes!(value)
172
+ raise ArgumentError, "TemplateUnit suppressed_rules must be an Array" unless value.is_a?(Array)
173
+
174
+ value.map do |prefix|
175
+ text = prefix.to_s
176
+ unless text.match?(RULE_PREFIX)
177
+ raise ArgumentError, "TemplateUnit suppressed_rules entry #{text.inspect} is malformed"
178
+ end
179
+
180
+ text.dup.freeze
181
+ end.uniq.freeze
182
+ end
183
+
184
+ def validate_bindings!(field, value, pattern)
185
+ raise ArgumentError, "TemplateUnit #{field} must be a Hash" unless value.is_a?(Hash)
186
+
187
+ value.to_h do |name, type_name|
188
+ key = name.to_s
189
+ raise ArgumentError, "TemplateUnit #{field} key #{key.inspect} is malformed" unless key.match?(pattern)
190
+
191
+ [key.dup.freeze, validate_string!("#{field} value", type_name)]
192
+ end.freeze
193
+ end
194
+ end
195
+ end
196
+ end
@@ -36,7 +36,7 @@ module Rigor
36
36
 
37
37
  @trusted_gems = trusted_gems.map { |g| g.to_s.dup.freeze }.uniq.sort.freeze
38
38
  @allowed_read_roots = allowed_read_roots
39
- .map { |path| File.expand_path(path).freeze }
39
+ .map { |path| File.absolute_path(path).freeze }
40
40
  .uniq
41
41
  .sort
42
42
  .freeze
@@ -46,10 +46,10 @@ module Rigor
46
46
  end
47
47
 
48
48
  # @return true when the absolute path falls inside any allowed read root. Symlinks are
49
- # resolved through `File.expand_path` only (no `realpath`); plugins with adversarial intent are out of
49
+ # resolved through `File.absolute_path` only (no `realpath`); plugins with adversarial intent are out of
50
50
  # scope per ADR-2.
51
51
  def allow_read?(path)
52
- absolute = File.expand_path(path.to_s)
52
+ absolute = File.absolute_path(path.to_s)
53
53
  @allowed_read_roots.any? { |root| inside?(absolute, root) }
54
54
  end
55
55
 
data/lib/rigor/plugin.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "plugin/type_node_resolver"
4
4
  require_relative "plugin/macro"
5
+ require_relative "plugin/template_unit"
5
6
  require_relative "plugin/manifest"
6
7
  require_relative "plugin/access_denied_error"
7
8
  require_relative "plugin/trust_policy"
@@ -17,8 +17,8 @@ module Rigor
17
17
  #
18
18
  # The answer (issue #260's recorded decision) is one table set, built once, threaded to both sides: the
19
19
  # {Mutator} base scope and {DiagnosticOracle}'s runner seam. This module builds that table set — the FULL
20
- # discovery bundle (`discovered_def_nodes`, `discovered_methods`, superclasses, includes, …), not just the
21
- # class-identity slice site selection needed, because resolving `find` on `singleton(Account)` is what makes
20
+ # discovery bundle (`discovered_def_nodes`, `discovered_methods`, superclasses, includes, extends, …), not just
21
+ # the class-identity slice site selection needed, because resolving `find` on `singleton(Account)` is what makes
22
22
  # the site killable.
23
23
  #
24
24
  # The shape is a plain Hash of {Scope::DiscoveryIndex} slot names, which is exactly what both consumers take:
@@ -84,9 +84,10 @@ module Rigor
84
84
  # collectors driven over the same tree) that {Analysis::Runner::ProjectPrePasses#discover} uses. Empty
85
85
  # tables are dropped so an empty seed stays `{}` and every consumer's "seed nothing" branch keeps working.
86
86
  #
87
- # `discovered_class_sources` is deliberately NOT carried: the runner itself seeds it only under dependency
88
- # recording (it is read by the ancestry accessors solely to record cross-file edges), and Tier 2 never
89
- # records.
87
+ # `discovered_class_sources` is carried as the runner carries it on every run: Tier 2 never records edges,
88
+ # but an ancestor-order reader asks whether a class is declared in more than one file. The mixin tables
89
+ # (`discovered_includes`, `discovered_prepends`, `discovered_extends`) ride the same fold, so a singleton
90
+ # `extend` reads the same from a per-file scope under coverage and protection as it does under `check`.
90
91
  def discovery_tables(paths)
91
92
  seed_tables(Inference::ScopeIndexer.discovered_project_index_for_paths(paths))
92
93
  end
@@ -99,17 +100,27 @@ module Rigor
99
100
  tables = { discovered_classes: index.fetch(:classes) }
100
101
  %i[
101
102
  def_nodes def_nestings singleton_def_nodes def_sources singleton_def_sources superclasses includes
102
- method_visibilities methods data_member_layouts struct_member_layouts
103
+ extends prepends unpositioned_mixins class_sources
104
+ method_visibilities methods parameter_envelopes data_member_layouts struct_member_layouts
105
+ deferred_ranges refinements global_write_census
103
106
  ].each do |slot|
104
107
  tables[seed_key(slot)] = def_index.fetch(slot)
105
108
  end
106
- tables.reject { |_, table| table.nil? || table.empty? }
109
+ # ADR-119 WD1 — the pairs travel as one: an unpaired empty table is dropped here, a pair only when both
110
+ # halves are empty (`compact_pairs`), so a non-empty sibling is never stranded by an empty member.
111
+ tables.merge!(def_index.fetch(:siblings))
112
+ Scope::DiscoveryIndex.compact_pairs(
113
+ tables.reject { |name, table| !Scope::DiscoveryIndex.paired?(name) && (table.nil? || table.empty?) }
114
+ )
107
115
  end
108
116
 
117
+ UNPREFIXED_SLOTS = %i[data_member_layouts struct_member_layouts unpositioned_mixins].freeze
118
+ private_constant :UNPREFIXED_SLOTS
119
+
109
120
  # The def-index slot names match the {Scope::DiscoveryIndex} ones under a `discovered_` prefix, except the
110
- # two member-layout tables, which carry the same name on both sides.
121
+ # two member-layout tables and the unpositioned-mixin table, which carry the same name on both sides.
111
122
  def seed_key(slot)
112
- slot.to_s.end_with?("member_layouts") ? slot : :"discovered_#{slot}"
123
+ UNPREFIXED_SLOTS.include?(slot) ? slot : :"discovered_#{slot}"
113
124
  end
114
125
  end
115
126
  end
@@ -20,7 +20,6 @@ module Rigor
20
20
  [diagnostic.rule, diagnostic.path, diagnostic.line, diagnostic.column, diagnostic.message]
21
21
  end
22
22
 
23
- # @return the signature set of `diagnostics`.
24
23
  def signatures_of(diagnostics)
25
24
  diagnostics.to_set { |diagnostic| of(diagnostic) }
26
25
  end
@@ -140,7 +140,7 @@ module Rigor
140
140
  end
141
141
 
142
142
  def snapshot_root_candidates(roots, configured)
143
- [roots, Array(roots).map { |path| File.expand_path(path) },
143
+ [roots, Array(roots).map { |path| File.absolute_path(path) },
144
144
  configured, Array(configured).map { |path| relative_to_pwd(path) }]
145
145
  .compact.reject(&:empty?).uniq
146
146
  end
@@ -148,8 +148,8 @@ module Rigor
148
148
  # `path` rendered relative to the working directory when it lies underneath it; unchanged otherwise.
149
149
  # Shared by the root-candidate list and the per-file snapshot lookup, which face the same skew.
150
150
  def relative_to_pwd(path)
151
- absolute = File.expand_path(path)
152
- prefix = "#{File.expand_path(Dir.pwd)}#{File::SEPARATOR}"
151
+ absolute = File.absolute_path(path)
152
+ prefix = "#{File.absolute_path(Dir.pwd)}#{File::SEPARATOR}"
153
153
  absolute.start_with?(prefix) ? absolute.delete_prefix(prefix) : path.to_s
154
154
  end
155
155
 
@@ -182,8 +182,7 @@ module Rigor
182
182
  # `Unavailable` rescue above and disables the cache, which is the only sound reading of a key slot
183
183
  # that could not be computed.
184
184
  def engine_source_entries
185
- identity = Cache::EngineSource.process_identity
186
- identity.nil? ? [] : [config_entry("engine-source", identity)]
185
+ Cache::EngineSource.key_config_entries
187
186
  end
188
187
 
189
188
  # Probed BEFORE the snapshot load, for the reason string rather than for soundness. Since #289
@@ -106,6 +106,12 @@ module Rigor
106
106
  # `docs/type-specification/rbs-extended.md`.
107
107
  INFERRED_RETURN_DIRECTIVE = "rigor:v1:inferred-return"
108
108
 
109
+ # `%a{rigor:v1:inferred-signature}` — every type slot on the member, parameters and return alike,
110
+ # is a placeholder: the author's comment block asserted nothing about this member's own contract.
111
+ # Strictly narrower than {INFERRED_RETURN_DIRECTIVE}, which also covers a member with an authored
112
+ # parameter and no authored return. Normative in `docs/type-specification/rbs-extended.md`.
113
+ INFERRED_SIGNATURE_DIRECTIVE = "rigor:v1:inferred-signature"
114
+
109
115
  module_function
110
116
 
111
117
  # Whether `method_def` declares its return type inferred rather than contracted (issue #823).
@@ -131,6 +137,27 @@ module Rigor
131
137
  annotations.any? { |annotation| annotation.string.to_s.strip == INFERRED_RETURN_DIRECTIVE }
132
138
  end
133
139
 
140
+ # Whether `method_def` declares its ENTIRE signature inferred — every parameter and the return alike
141
+ # — rather than only its return (issue #991).
142
+ #
143
+ # `inferred_return?` alone cannot separate a fully bare `def` from one whose parameters ARE declared
144
+ # (`# @rbs num: Float`) and only its return defaulted: both trip the same return-only provenance,
145
+ # because no equivalent per-parameter survivor exists to tell them apart. This reads the plugin's
146
+ # narrower, member-level fact instead: true only when nothing at all was asserted about the member.
147
+ # `call.wrong-arity` declines on this rather than on {inferred_return?}, since a parameter the author
148
+ # DID annotate is an assertion about the parameter list, and the parameter list is exactly what arity
149
+ # is a question about (see the rule's own comment).
150
+ #
151
+ # @param method_def — an `RBS::Definition::Method`, or anything else answering `#annotations`.
152
+ def inferred_signature?(method_def)
153
+ return false if method_def.nil?
154
+
155
+ annotations = method_def.annotations
156
+ return false if annotations.nil? || annotations.empty?
157
+
158
+ annotations.any? { |annotation| annotation.string.to_s.strip == INFERRED_SIGNATURE_DIRECTIVE }
159
+ end
160
+
134
161
  # Reads RBS::Extended predicate effects off `RBS::Definition::Method#annotations`. Returns the effects in
135
162
  # source order; duplicates and unrecognised `rigor:v1:` directives are dropped. Returns an empty array (NEVER
136
163
  # `nil`) for a method with no recognised annotations so callers can iterate unconditionally.
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ # {Reflection}'s constant-ancestor walk ([#354](https://github.com/rigortype/rigor/issues/354)), split out for
5
+ # the reader rather than for the loader: the whole-spelling ladder's step 2 (`reflection.rb`) and the
6
+ # segment-wise path walk (`reflection/constant_path.rb`) both ask which project classes and modules a
7
+ # name's owner inherits constants from, and this file answers that once for both. Reopening the module
8
+ # keeps the walk behind the facade its callers already read.
9
+ module Reflection
10
+ # #354 — thread-local slot for the per-run ancestor-scope memo. See {.ancestor_constant_scopes}.
11
+ ANCESTOR_SCOPES_KEY = :__rigor_ancestor_constant_scopes__
12
+ private_constant :ANCESTOR_SCOPES_KEY
13
+
14
+ # The recording run's slot. See {.recorded_ancestor_constant_scopes}.
15
+ RECORDED_ANCESTOR_SCOPES_KEY = :__rigor_recorded_ancestor_constant_scopes__
16
+ private_constant :RECORDED_ANCESTOR_SCOPES_KEY
17
+
18
+ module_function
19
+
20
+ # #354 — the project classes and modules whose own constants `class_name` inherits, in Ruby's
21
+ # ancestor order: the entries of `class_name`'s `Scope::ResolutionChain` — prepended modules, then
22
+ # included ones, then the superclass's chain likewise — each once. `class_name` itself is excluded:
23
+ # step 1 already covered it.
24
+ #
25
+ # Issue #1571 — this was a breadth-first walk of its own, which is not Ruby's order: `class C < Base;
26
+ # include A` with `A` including `M` reached `Base::X` (depth 1) before `M::X` (depth 2), while Ruby's
27
+ # ancestors are `[C, A, M, Base]`. It reads the chain's `:constants` flavor, whose predicate for a
28
+ # project class also admits a module that holds only constants.
29
+ #
30
+ # Only PROJECT ancestors appear: an as-written name that resolves to no discovered class or module
31
+ # is an external entry on the chain and contributes nothing here, so a `class Foo <
32
+ # ActiveRecord::Base` adds no candidate and a constant owned by an RBS-known ancestor still resolves
33
+ # only if the bare name reaches it at step 3. That gap is deliberate for this slice: widening to the
34
+ # RBS ancestor graph is a separate question with its own FP surface.
35
+ #
36
+ # Memoised per run because step 2 runs on every constant reference whose lexical candidates all
37
+ # miss — which is the common case for a core-class reference (`String` inside `class Foo`). The
38
+ # bucket keys on the identity of the runner-seeded run-generation token (ADR-84 WD2), falling
39
+ # back to the per-file discovery table for runner-less scopes, so a re-run in one process (LSP,
40
+ # ADR-62 warm loop) cannot hit stale entries.
41
+ def ancestor_constant_scopes(class_name, scope)
42
+ ancestor_constant_worlds(class_name, scope).first
43
+ end
44
+ private_class_method :ancestor_constant_scopes
45
+
46
+ # ADR-24 / #1570 — the first ancestor that owns a constant, asked as {.ancestor_constant_scopes} orders
47
+ # them and settled by `Scope::ResolutionChain#settle`: the block's first truthy answer where the chain
48
+ # stands, and otherwise the answer of the breadth-first order this rung used before the chain
49
+ # (`Scope::ResolutionChain::MasterOrder`) — where a skipped `include` would put a different owner first,
50
+ # which constant Ruby reads depends on the order the bodies ran.
51
+ def agreed_ancestor_hit(class_name, scope, &)
52
+ scopes, chain = ancestor_constant_worlds(class_name, scope)
53
+ owner, hit = first_ancestor_hit(scopes, &)
54
+ verdict = chain.settle(scope, owner, owner: owner) do |retro|
55
+ first_ancestor_hit(constant_scopes_of(retro, class_name), &)&.first
56
+ end
57
+ return hit if verdict == :chain
58
+
59
+ first_ancestor_hit(master_constant_scopes(class_name, scope), &)&.last
60
+ end
61
+ private_class_method :agreed_ancestor_hit
62
+
63
+ def master_constant_scopes(class_name, scope)
64
+ Scope::ResolutionChain::MasterOrder.breadth_first(scope, class_name.to_s, :constants)
65
+ end
66
+ private_class_method :master_constant_scopes
67
+
68
+ def first_ancestor_hit(entries)
69
+ entries.each do |entry|
70
+ hit = yield entry
71
+ return [entry, hit] if hit
72
+ end
73
+ nil
74
+ end
75
+ private_class_method :first_ancestor_hit
76
+
77
+ # `[scopes, chain]` — {.ancestor_constant_scopes} and the chain they were read from, which settles the
78
+ # answer.
79
+ def ancestor_constant_worlds(class_name, scope)
80
+ # ADR-46: the answer depends on every class on the chain, and the memo is run-scoped rather than
81
+ # file-scoped — a hit would skip the recording and under-record the edge for every later file. A
82
+ # recording run reads the chain, which is memoised per discovery index, and files its edges on
83
+ # every call.
84
+ return recorded_ancestor_constant_scopes(class_name, scope) if Analysis::DependencyRecorder.active?
85
+
86
+ generation = scope.run_generation || scope.discovered_superclasses
87
+ slot = Thread.current[ANCESTOR_SCOPES_KEY]
88
+ unless slot && slot[0].equal?(generation)
89
+ slot = [generation, {}]
90
+ Thread.current[ANCESTOR_SCOPES_KEY] = slot
91
+ end
92
+ bucket = slot[1]
93
+ bucket.fetch(class_name) { bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) }
94
+ end
95
+ private_class_method :ancestor_constant_worlds
96
+
97
+ # The recording run's memo. The answer is every project entry of the chain, so its edges are the
98
+ # root's and every entry's — what `Scope::ResolutionChain#record` files, which a computation does and a
99
+ # hit replays, in the same order. One slot keyed on the discovery index: the answer and the edges are
100
+ # both read from it, so the key is exact, and the slot never pins more than the index it serves.
101
+ def recorded_ancestor_constant_scopes(class_name, scope)
102
+ discovery = scope.discovery
103
+ slot = Thread.current[RECORDED_ANCESTOR_SCOPES_KEY]
104
+ unless slot && slot[0].equal?(discovery)
105
+ slot = [discovery, {}]
106
+ Thread.current[RECORDED_ANCESTOR_SCOPES_KEY] = slot
107
+ end
108
+ bucket = slot[1]
109
+ worlds = bucket[class_name]
110
+ return bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) if worlds.nil?
111
+
112
+ Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants).record(scope)
113
+ worlds
114
+ end
115
+ private_class_method :recorded_ancestor_constant_scopes
116
+
117
+ # Reads the chain whole, so a recording run files every class on it (a no-op otherwise).
118
+ def compute_ancestor_constant_scopes(class_name, scope)
119
+ chain = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :constants)
120
+ chain.record(scope)
121
+ scopes = constant_scopes_of(chain, class_name)
122
+ [scopes, chain].freeze
123
+ end
124
+ private_class_method :compute_ancestor_constant_scopes
125
+
126
+ # The chain's project entries in order, each once, without `class_name` itself.
127
+ def constant_scopes_of(chain, class_name)
128
+ out = []
129
+ chain.entries.each do |entry|
130
+ name = entry.name
131
+ out << name unless name.nil? || name == class_name || out.include?(name)
132
+ end
133
+ out.freeze
134
+ end
135
+ private_class_method :constant_scopes_of
136
+
137
+ # Whether `name` is a namespace the project declares — the predicate the chain's `:constants` flavor
138
+ # resolves ancestor names with, and the path walk's (`reflection/constant_path.rb`) test for a head.
139
+ def known_project_namespace?(name, scope)
140
+ scope.discovered_superclasses.key?(name) ||
141
+ scope.discovered_includes.key?(name) ||
142
+ scope.discovered_classes.key?(name)
143
+ end
144
+ private_class_method :known_project_namespace?
145
+ end
146
+ end
@@ -35,10 +35,17 @@ module Rigor
35
35
  # silent wrong answer for a silent absent one at no gain (the same reasoning as
36
36
  # {.toplevel_first_constant_type}'s caller-derived rungs).
37
37
  def resolve_constant_path_name(name, scope, &known)
38
+ walk_constant_path(name, scope, known, caller_derived: true)
39
+ end
40
+
41
+ # {.resolve_constant_path_name}'s walk. `caller_derived: false` is {.resolve_constant_type}'s flag: the
42
+ # head skips the enclosing class's ancestors — the one step a recorded top-level body takes from its
43
+ # caller — while every later segment still walks its owner's ancestors, as Ruby does.
44
+ def walk_constant_path(name, scope, known, caller_derived:)
38
45
  segments = name.to_s.split("::")
39
46
  return nil unless segments.size > 1
40
47
 
41
- owner = path_head_owner(segments.first, scope)
48
+ owner = path_head_owner(segments.first, scope, caller_derived)
42
49
  last = segments.size - 1
43
50
  segments.each_with_index do |segment, index|
44
51
  next if index.zero?
@@ -50,25 +57,34 @@ module Rigor
50
57
  end
51
58
  owner
52
59
  end
60
+ private_class_method :walk_constant_path
53
61
 
54
62
  # Step 2.5's rung as a type: the segment-wise name, then that name looked up like any other
55
63
  # candidate so the answer comes from the same source-precedence order as every other rung.
56
- def constant_path_type(name, scope)
57
- qualified = resolve_constant_path_name(name, scope) { |candidate| constant_type_at(candidate, scope) }
58
- qualified && constant_type_at(qualified, scope)
64
+ #
65
+ # Issue #1290 — the final segment's candidates are looked up with the reference's `shadows` names
66
+ # ({.constant_type_at}), so an owner's own `B` another file writes stops the walk before the owner's
67
+ # superclass answers with its published `B`.
68
+ def constant_path_type(name, scope, shadows:, caller_derived: true)
69
+ known = ->(candidate) { constant_type_at(candidate, scope, shadows) }
70
+ qualified = walk_constant_path(name, scope, known, caller_derived: caller_derived)
71
+ qualified && constant_type_at(qualified, scope, shadows)
59
72
  end
60
73
  private_class_method :constant_path_type
61
74
 
62
75
  # The namespace a path's FIRST segment names — the bare-name ladder, restricted to answers that can
63
76
  # OWN the segments that follow. A value constant is not one of them, so it declines here rather
64
77
  # than producing a namespace prefix no source knows.
65
- def path_head_owner(head, scope)
78
+ def path_head_owner(head, scope, caller_derived)
66
79
  hit = first_namespace_hit(lexical_nesting_chain(scope), head, scope)
67
80
  return hit if hit
68
81
 
69
- prefix = enclosing_class_path(scope)
82
+ prefix = caller_derived ? enclosing_class_path(scope) : nil
70
83
  unless prefix.nil? || prefix.empty?
71
- hit = first_namespace_hit(bounded_ancestor_scopes(prefix, scope), head, scope)
84
+ hit = bounded_agreed_hit(prefix, scope) do |entry|
85
+ candidate = "#{entry}::#{head}"
86
+ candidate if namespace_known?(candidate, scope)
87
+ end
72
88
  return hit if hit
73
89
  end
74
90
 
@@ -93,28 +109,40 @@ module Rigor
93
109
  candidate = "#{owner}::#{segment}"
94
110
  return candidate if yield(candidate)
95
111
 
96
- bounded_ancestor_scopes(owner, scope).each do |ancestor|
112
+ bounded_agreed_hit(owner, scope) do |ancestor|
97
113
  inherited = "#{ancestor}::#{segment}"
98
- return inherited if yield(inherited)
114
+ inherited if yield(inherited)
99
115
  end
100
- nil
101
116
  end
102
117
  private_class_method :constant_in_namespace
103
118
 
119
+ # {.agreed_ancestor_hit} over the budget-bounded scope lists: the first answer where
120
+ # `Scope::ResolutionChain#settle` lets the chain stand, and otherwise the breadth-first order's.
121
+ def bounded_agreed_hit(class_name, scope, &)
122
+ scopes, chain = ancestor_constant_worlds(class_name, scope)
123
+ owner, hit = first_ancestor_hit(bounded(scopes), &)
124
+ verdict = chain.settle(scope, owner, owner: owner) do |retro|
125
+ first_ancestor_hit(bounded(constant_scopes_of(retro, class_name)), &)&.first
126
+ end
127
+ return hit if verdict == :chain
128
+
129
+ first_ancestor_hit(bounded(master_constant_scopes(class_name, scope)), &)&.last
130
+ end
131
+ private_class_method :bounded_agreed_hit
132
+
104
133
  # How many of {.ancestor_constant_scopes}' entries one segment may consult, under
105
134
  # `Scope::ANCESTOR_WALK_LIMIT` — the budget `Scope`'s method lookup already spends on this same
106
135
  # graph, reused rather than re-chosen so one pathological ancestry costs one bound. The list
107
136
  # itself is the memoised one step 2 builds, so this bounds the per-segment consultation the walk
108
137
  # adds and not the graph. Reported through the shared counter, so a truncated walk is visible in a
109
138
  # budget trace instead of looking like a plain miss.
110
- def bounded_ancestor_scopes(class_name, scope)
111
- scopes = ancestor_constant_scopes(class_name, scope)
139
+ def bounded(scopes)
112
140
  return scopes if scopes.size <= Scope::ANCESTOR_WALK_LIMIT
113
141
 
114
142
  Inference::BudgetTrace.hit(Inference::BudgetTrace::ANCESTOR_WALK_LIMIT)
115
143
  scopes.first(Scope::ANCESTOR_WALK_LIMIT)
116
144
  end
117
- private_class_method :bounded_ancestor_scopes
145
+ private_class_method :bounded
118
146
 
119
147
  # Whether `name` can OWN a constant. `class_known?` covers the RBS and registry classes and modules;
120
148
  # the project tables cover a namespace the project declares with no RBS for it.