rigortype 0.3.9 → 0.4.0

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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  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/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -49,23 +49,32 @@ module Rigor
49
49
  attr_reader :target_gems
50
50
 
51
51
  attr_reader :id, :version, :description, :config_schema, :config_defaults, :produces, :consumes,
52
- :owns_receivers, :open_receivers, :type_node_resolvers, :block_as_methods,
52
+ :owns_receivers, :open_receivers, :rbs_complete_ancestors, :rbs_complete_extends,
53
+ :type_node_resolvers,
54
+ :block_as_methods,
53
55
  :heredoc_templates, :nested_class_templates, :trait_registries,
54
56
  :hkt_registrations, :hkt_definitions, :signature_paths, :protocol_contracts,
55
57
  :source_rbs_synthesizer, :additional_initializers,
56
58
  :effect_root, :effect_labels, :effect_attributions, :effect_edges, :effect_entry_points,
57
59
  :effect_ancestry
58
60
 
59
- def initialize( # rubocop:disable Metrics/ParameterLists
61
+ # #392 — the project-relative globs whose files this plugin compiles into {TemplateUnit}s. Purely a
62
+ # CLAIM: the engine globs these, reads each match's bytes and offers them to
63
+ # {Base#template_units_for_file}, which is where the transform itself lives. A plugin declaring none
64
+ # is never asked, and a run whose plugins declare none does no extra globbing at all.
65
+ attr_reader :template_globs
66
+
67
+ def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
60
68
  id:, version:,
61
69
  description: nil, config_schema: {}, target_gems: [],
62
- produces: [], consumes: [], owns_receivers: [], open_receivers: [], type_node_resolvers: [],
70
+ produces: [], consumes: [], owns_receivers: [], open_receivers: [], rbs_complete_ancestors: [],
71
+ rbs_complete_extends: [], type_node_resolvers: [],
63
72
  block_as_methods: [], heredoc_templates: [], nested_class_templates: [],
64
73
  trait_registries: [],
65
74
  hkt_registrations: [], hkt_definitions: [], signature_paths: [], protocol_contracts: [],
66
75
  source_rbs_synthesizer: nil, additional_initializers: [],
67
76
  effect_root: nil, effect_labels: [], effect_attributions: [], effect_edges: [],
68
- effect_entry_points: [], effect_ancestry: []
77
+ effect_entry_points: [], effect_ancestry: [], template_globs: []
69
78
  )
70
79
  validate_id!(id)
71
80
  validate_version!(version)
@@ -74,6 +83,8 @@ module Rigor
74
83
  validate_target_gems!(target_gems)
75
84
  validate_owns_receivers!(owns_receivers)
76
85
  validate_open_receivers!(open_receivers)
86
+ validate_rbs_complete_ancestors!(rbs_complete_ancestors)
87
+ validate_rbs_complete_extends!(rbs_complete_extends)
77
88
  validate_type_node_resolvers!(type_node_resolvers)
78
89
  validate_block_as_methods!(block_as_methods)
79
90
  validate_heredoc_templates!(heredoc_templates)
@@ -93,10 +104,11 @@ module Rigor
93
104
  hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
94
105
  source_rbs_synthesizer)
95
106
  assign_target_gems(target_gems)
107
+ assign_rbs_complete_ancestors(rbs_complete_ancestors, rbs_complete_extends)
96
108
  assign_nested_class_templates(nested_class_templates)
97
109
  assign_additional_initializers(additional_initializers)
98
110
  assign_effect_fields(effect_root, effect_labels, effect_attributions, effect_edges,
99
- effect_entry_points, effect_ancestry)
111
+ effect_entry_points, effect_ancestry, template_globs)
100
112
  freeze
101
113
  end
102
114
 
@@ -133,6 +145,13 @@ module Rigor
133
145
  end
134
146
  private :assign_target_gems
135
147
 
148
+ # ADR-43 WD4 — assigned outside assign_fields (which already carries the maximum positional arity).
149
+ def assign_rbs_complete_ancestors(rbs_complete_ancestors, rbs_complete_extends)
150
+ @rbs_complete_ancestors = rbs_complete_ancestors.map { |c| c.to_s.dup.freeze }.freeze
151
+ @rbs_complete_extends = rbs_complete_extends.map { |c| c.to_s.dup.freeze }.freeze
152
+ end
153
+ private :assign_rbs_complete_ancestors
154
+
136
155
  # Assigned outside assign_fields (which already carries the maximum positional arity) — set in
137
156
  # `initialize` before the final freeze. ADR-36 nested-class emission tier.
138
157
  def assign_nested_class_templates(nested_class_templates)
@@ -162,15 +181,27 @@ module Rigor
162
181
  private :validate_effect_fields!
163
182
 
164
183
  def assign_effect_fields(effect_root, effect_labels, effect_attributions, effect_edges,
165
- effect_entry_points, effect_ancestry)
184
+ effect_entry_points, effect_ancestry, template_globs)
166
185
  @effect_root = effect_root.nil? ? nil : effect_root.to_s.dup.freeze
167
186
  @effect_labels = effect_labels.map { |label| label.to_s.dup.freeze }.uniq.sort.freeze
168
187
  @effect_attributions = effect_attributions.dup.freeze
169
188
  @effect_edges = effect_edges.dup.freeze
170
189
  @effect_entry_points = effect_entry_points.dup.freeze
171
190
  @effect_ancestry = effect_ancestry.dup.freeze
191
+ assign_template_globs(template_globs)
172
192
  end
173
193
  private :assign_effect_fields
194
+
195
+ # #392 — a glob is a String, and an absolute one is refused: a template unit is a project file, and
196
+ # a plugin reaching outside the project root is the trust boundary ADR-2 § "Plugin Trust and I/O
197
+ # Policy" draws. Validated and assigned in one call, because `#initialize` is at its line budget.
198
+ def assign_template_globs(globs)
199
+ validate_array_of!("template_globs", globs, "non-empty relative glob Strings") do |glob|
200
+ glob.is_a?(String) && !glob.empty? && !glob.start_with?("/") && !glob.include?("..")
201
+ end
202
+ @template_globs = globs.map { |glob| glob.to_s.dup.freeze }.uniq.freeze
203
+ end
204
+ private :assign_template_globs
174
205
  # rubocop:enable Metrics/ParameterLists, Metrics/AbcSize
175
206
 
176
207
  public
@@ -196,7 +227,7 @@ module Rigor
196
227
  errors
197
228
  end
198
229
 
199
- def to_h # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
230
+ def to_h # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
200
231
  {
201
232
  "id" => id,
202
233
  "version" => version,
@@ -208,6 +239,8 @@ module Rigor
208
239
  "consumes" => consumes.map { |c| consumption_hash(c) },
209
240
  "owns_receivers" => owns_receivers,
210
241
  "open_receivers" => open_receivers,
242
+ "rbs_complete_ancestors" => rbs_complete_ancestors,
243
+ "rbs_complete_extends" => rbs_complete_extends,
211
244
  "type_node_resolvers" => type_node_resolvers.map { |r| r.class.name },
212
245
  "block_as_methods" => block_as_methods.map(&:to_h),
213
246
  "heredoc_templates" => heredoc_templates.map(&:to_h),
@@ -224,7 +257,8 @@ module Rigor
224
257
  "effect_attributions" => effect_attributions.map(&:to_h),
225
258
  "effect_edges" => effect_edges.map(&:to_h),
226
259
  "effect_entry_points" => effect_entry_points.map(&:to_h),
227
- "effect_ancestry" => effect_ancestry.map(&:to_h)
260
+ "effect_ancestry" => effect_ancestry.map(&:to_h),
261
+ "template_globs" => template_globs
228
262
  }
229
263
  end
230
264
 
@@ -233,10 +267,11 @@ module Rigor
233
267
  # otherwise. A third-party plugin's `effect_root:` is ignored here (and warned about by the registry),
234
268
  # so it opens only the root named after itself.
235
269
  def effect_owner
236
- return id if effect_root.nil?
270
+ root = effect_root
271
+ return id if root.nil?
237
272
  return id unless FirstParty.bundled?(id)
238
273
 
239
- effect_root
274
+ root
240
275
  end
241
276
 
242
277
  # Whether this plugin's `effect_attributions:` may carry `discharge: true` (ADR-103 WD6).
@@ -388,6 +423,32 @@ module Rigor
388
423
  validate_array_of!("open_receivers", open_receivers, "non-empty String") { |c| c.is_a?(String) && !c.empty? }
389
424
  end
390
425
 
426
+ # ADR-43 WD4 — `rbs_complete_ancestors:` declares the class names this plugin's bundled RBS covers
427
+ # COMPLETELY: a method call the signature does not declare is genuinely absent at runtime. A
428
+ # Ruby-source subclass of a listed class may then bridge inherited calls to the ancestor's RBS
429
+ # (the ADR-43 allow-list's manifest-declared half). The dual of `open_receivers:` — that field
430
+ # marks a class open to suppress diagnostics on an unbounded surface; this one marks a class
431
+ # closed to ENABLE them on subclasses. Listing a class whose signature is partial would turn
432
+ # every omitted inherited method into a `call.undefined-method` false positive on subclass
433
+ # receivers, so only classes the plugin's `signature_paths:` declares exhaustively belong here.
434
+ def validate_rbs_complete_ancestors!(rbs_complete_ancestors)
435
+ validate_array_of!("rbs_complete_ancestors", rbs_complete_ancestors, "non-empty String") do |c|
436
+ c.is_a?(String) && !c.empty?
437
+ end
438
+ end
439
+
440
+ # `rbs_complete_extends:` is the extend-edge twin of `rbs_complete_ancestors:` (ADR-43 WD4): it names
441
+ # the MODULES whose bundled RBS instance surface is complete, so a project `extend M` in a class or
442
+ # module body lets singleton-side calls on that class resolve M's declared instance methods
443
+ # (`class F; extend T::Sig; sig { ... }; end` reaching `T::Sig#sig`). Same completeness rule: a
444
+ # module whose declared surface is partial does not belong here — the extend edge would misreport
445
+ # real calls as `call.undefined-method`.
446
+ def validate_rbs_complete_extends!(rbs_complete_extends)
447
+ validate_array_of!("rbs_complete_extends", rbs_complete_extends, "non-empty String") do |c|
448
+ c.is_a?(String) && !c.empty?
449
+ end
450
+ end
451
+
391
452
  # ADR-13 slice 2 — `type_node_resolvers:` declares the plugin-supplied `TypeNodeResolver` instances the
392
453
  # parser consults (in slice 3) when an RBS::Extended payload's named- or generic-type head misses the
393
454
  # built-in registry. Slice 2 carries the declarations on the manifest and the registry exposes them in
@@ -373,6 +373,30 @@ module Rigor
373
373
  @open_receivers_set.include?(class_name.to_s)
374
374
  end
375
375
 
376
+ # ADR-43 WD4 — the aggregate set of "RBS-complete ancestor" class names declared across loaded
377
+ # plugins (manifest `rbs_complete_ancestors:`). `RbsDispatch`'s inherited-call bridge consults it
378
+ # alongside the engine's own allow-list: a Ruby-source subclass of a listed class resolves
379
+ # inherited calls against the ancestor's RBS, and the signature-reading rules apply to them.
380
+ attr_reader :rbs_complete_ancestors
381
+
382
+ def rbs_complete_ancestor?(class_name)
383
+ return false if class_name.nil?
384
+
385
+ @rbs_complete_ancestors_set.include?(class_name.to_s)
386
+ end
387
+
388
+ # The extend-edge twin of `rbs_complete_ancestors` — the aggregate set of module names declared via
389
+ # manifest `rbs_complete_extends:`. `RbsDispatch`'s singleton bridge consults it for `extend M`
390
+ # edges in project source: `class F; extend T::Sig; sig { ... }; end` resolves `sig` through
391
+ # `T::Sig`'s bundled RBS instance surface.
392
+ attr_reader :rbs_complete_extends
393
+
394
+ def rbs_complete_extends?(class_name)
395
+ return false if class_name.nil?
396
+
397
+ @rbs_complete_extends_set.include?(class_name.to_s)
398
+ end
399
+
376
400
  # ADR-28 — flat, ordered list of every loaded plugin's path-scoped method-protocol contracts, in
377
401
  # plugin registration order. Read from each plugin's `#protocol_contracts` (which the manifest backs
378
402
  # by default but a plugin MAY override to fold in per-project config). Consumed by
@@ -435,14 +459,24 @@ module Rigor
435
459
  # nil where {#guarded_manifest} caught a raising manifest read.
436
460
  def compile_aggregates(manifests)
437
461
  @additional_initializers = manifests.flat_map { |m| m&.additional_initializers || [] }.freeze
438
- @open_receivers = manifests.flat_map { |m| (m&.open_receivers || []).map(&:to_s) }.uniq.freeze
462
+ @open_receivers = aggregate_class_names(manifests, :open_receivers)
439
463
  @open_receivers_set = @open_receivers.to_set.freeze
464
+ @rbs_complete_ancestors = aggregate_class_names(manifests, :rbs_complete_ancestors)
465
+ @rbs_complete_ancestors_set = @rbs_complete_ancestors.to_set.freeze
466
+ @rbs_complete_extends = aggregate_class_names(manifests, :rbs_complete_extends)
467
+ @rbs_complete_extends_set = @rbs_complete_extends.to_set.freeze
440
468
  @type_node_resolvers = manifests.flat_map { |m| m&.type_node_resolvers || [] }.freeze
441
469
  @protocol_contracts = @plugins.flat_map { |p| safe_protocol_contracts(p) }.freeze
442
470
  @contracts_by_path = {}
443
471
  @effect_memo = {}
444
472
  end
445
473
 
474
+ # The shared shape of `open_receivers:` / `rbs_complete_ancestors:` — a flat uniq'd class-name list
475
+ # aggregated across manifests, each entry normalised to String.
476
+ def aggregate_class_names(manifests, field)
477
+ manifests.flat_map { |m| (m&.public_send(field) || []).map(&:to_s) }.uniq.freeze
478
+ end
479
+
446
480
  # Fail-soft per plugin: a plugin whose `effect_attributions:` override raises (a missing
447
481
  # `config/application.rb`, an unreadable path) contributes nothing rather than taking the run down.
448
482
  # An effect contribution is an enrichment; ADR-2 isolates plugin failures at the analyzer boundary
@@ -21,7 +21,12 @@ module Rigor
21
21
  # dropping an annotation it parsed. They are not the same news — the first says the file contributed
22
22
  # nothing, the second that it contributed all but one thing — so they carry distinct diagnostic ids.
23
23
  # Defaults to `:failed`, the pre-WD12 meaning, so an older caller records what it always did.
24
- Entry = Data.define(:plugin_id, :path, :message, :kind)
24
+ #
25
+ # `:contradiction` (ADR-112 WD5, issue #1075) is the consistency rule's error: a member two sources
26
+ # declare in ways that contradict, or a refinement outside its own declared type. It is the one kind
27
+ # positioned at a real line (`line`, nil for every other kind), because it usually sits in a `.rbs`
28
+ # file whose positions are exact; the others are file-level rows at line 1.
29
+ Entry = Data.define(:plugin_id, :path, :message, :kind, :line)
25
30
 
26
31
  def initialize
27
32
  @entries = []
@@ -34,12 +39,13 @@ module Rigor
34
39
  # file list and then drains each worker's stream into this one reporter, so without the dedup a
35
40
  # `--workers N` run printed N copies of a row a `--workers=0` run printed once. The same collapse the
36
41
  # {RbsExtended::Reporter} drain documents, for the same reason.
37
- def record(plugin_id:, path:, message:, kind: :failed)
42
+ def record(plugin_id:, path:, message:, kind: :failed, line: nil)
38
43
  entry = Entry.new(
39
44
  plugin_id: plugin_id.to_s.dup.freeze,
40
45
  path: path.to_s.dup.freeze,
41
46
  message: message.to_s.dup.freeze,
42
- kind: kind
47
+ kind: kind,
48
+ line: line
43
49
  )
44
50
  return nil if @seen.key?(entry)
45
51
 
@@ -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
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"
@@ -99,7 +99,9 @@ module Rigor
99
99
  tables = { discovered_classes: index.fetch(:classes) }
100
100
  %i[
101
101
  def_nodes def_nestings singleton_def_nodes def_sources singleton_def_sources superclasses includes
102
- method_visibilities methods data_member_layouts struct_member_layouts
102
+ prepends
103
+ method_visibilities methods parameter_envelopes data_member_layouts struct_member_layouts
104
+ deferred_ranges refinements global_write_census
103
105
  ].each do |slot|
104
106
  tables[seed_key(slot)] = def_index.fetch(slot)
105
107
  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
@@ -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,97 @@
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
+ module_function
15
+
16
+ # #354 — the project classes and modules whose own constants `class_name` inherits, in Ruby's
17
+ # ancestor order: included / prepended modules before the superclass (Ruby places mixins nearer),
18
+ # transitively, breadth-first. `class_name` itself is excluded — step 1 already covered it.
19
+ #
20
+ # Only PROJECT ancestors appear. `Scope#superclass_of` / `#includes_of` carry as-written names
21
+ # from the discovery pre-pass, and an as-written name that resolves to no discovered class or
22
+ # module is dropped — so a `class Foo < ActiveRecord::Base` contributes nothing and a constant
23
+ # owned by an RBS-known ancestor still resolves only if the bare name reaches it at step 3. That
24
+ # gap is deliberate for this slice: widening to the RBS ancestor graph is a separate question
25
+ # with its own FP surface.
26
+ #
27
+ # Memoised per run because step 2 runs on every constant reference whose lexical candidates all
28
+ # miss — which is the common case for a core-class reference (`String` inside `class Foo`). The
29
+ # bucket keys on the identity of the runner-seeded run-generation token (ADR-84 WD2), falling
30
+ # back to the per-file discovery table for runner-less scopes, so a re-run in one process (LSP,
31
+ # ADR-62 warm loop) cannot hit stale entries.
32
+ def ancestor_constant_scopes(class_name, scope)
33
+ # ADR-46: `superclass_of` / `includes_of` record a cross-file class dependency per consumer
34
+ # file, and the memo is run-scoped rather than file-scoped — a hit would skip the recording and
35
+ # under-record the edge for every later file. Recording runs are rare (incremental only), so
36
+ # they simply bypass the memo rather than complicate its key.
37
+ return compute_ancestor_constant_scopes(class_name, scope) if Analysis::DependencyRecorder.active?
38
+
39
+ generation = scope.run_generation || scope.discovered_superclasses
40
+ slot = Thread.current[ANCESTOR_SCOPES_KEY]
41
+ unless slot && slot[0].equal?(generation)
42
+ slot = [generation, {}]
43
+ Thread.current[ANCESTOR_SCOPES_KEY] = slot
44
+ end
45
+ bucket = slot[1]
46
+ bucket.fetch(class_name) { bucket[class_name] = compute_ancestor_constant_scopes(class_name, scope) }
47
+ end
48
+ private_class_method :ancestor_constant_scopes
49
+
50
+ def compute_ancestor_constant_scopes(class_name, scope)
51
+ queue = [class_name]
52
+ seen = { class_name => true }
53
+ out = []
54
+ until queue.empty?
55
+ current = queue.shift
56
+ # Mixins first, then the superclass — Ruby's ancestor order.
57
+ scope.includes_of(current).each do |raw|
58
+ resolved = resolve_ancestor_name(current, raw, scope)
59
+ next if resolved.nil? || seen[resolved]
60
+
61
+ seen[resolved] = true
62
+ out << resolved
63
+ queue << resolved
64
+ end
65
+ raw_super = scope.superclass_of(current)
66
+ next if raw_super.nil?
67
+
68
+ resolved_super = resolve_ancestor_name(current, raw_super, scope)
69
+ next if resolved_super.nil? || seen[resolved_super]
70
+
71
+ seen[resolved_super] = true
72
+ out << resolved_super
73
+ queue << resolved_super
74
+ end
75
+ out.freeze
76
+ end
77
+ private_class_method :compute_ancestor_constant_scopes
78
+
79
+ # Resolves an ancestor name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) against the nesting in
80
+ # force where the subclass's header is written — `Scope#ancestor_name_candidates`, the single
81
+ # owner of that order, which `Scope#enqueue_ancestors` reads for method lookup and the
82
+ # override-visibility rule reads for its own walk. Returns nil when no candidate names a
83
+ # discovered project class or module.
84
+ def resolve_ancestor_name(subclass_qualified, raw, scope)
85
+ scope.ancestor_name_candidates(subclass_qualified, raw)
86
+ .find { |candidate| known_project_namespace?(candidate, scope) }
87
+ end
88
+ private_class_method :resolve_ancestor_name
89
+
90
+ def known_project_namespace?(name, scope)
91
+ scope.discovered_superclasses.key?(name) ||
92
+ scope.discovered_includes.key?(name) ||
93
+ scope.discovered_classes.key?(name)
94
+ end
95
+ private_class_method :known_project_namespace?
96
+ end
97
+ end