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
@@ -12,12 +12,14 @@ module Rigor
12
12
  # Per-session cache of the project-wide analyzer state the LSP reads on every request — chiefly the
13
13
  # `Environment` (with its ~100-300ms RBS env build), a read-only `Cache::Store` that lets the runner hit
14
14
  # the on-disk RBS cache without writing back, and (since the pre-pass cache slice) a frozen
15
- # {Rigor::Analysis::ProjectScan} snapshot covering the plugin registry, dependency-source index, and
16
- # pre-pass scanner outputs.
15
+ # {Rigor::Analysis::ProjectScan} snapshot covering the plugin registry, dependency-source index,
16
+ # pre-pass scanner outputs and (since #1038) the compiled template-unit index.
17
17
  #
18
18
  # The pre-pass scan lets `DiagnosticPublisher#run_analysis` build a `Runner` with `prebuilt:` so per-buffer
19
19
  # publishes skip plugin `#prepare`, the synthetic-method scanner, the project-patched scanner, and the
20
- # dependency-source walker. For projects with substrate plugins / opt-in dependency source / sizeable
20
+ # dependency-source walker — and, since #1038, the template-unit transform: a publish recompiles only a
21
+ # template that moved on disk since the scan was built (plus the buffer's own, always), rather than every
22
+ # view in the project. For projects with substrate plugins / opt-in dependency source / sizeable
21
23
  # `pre_eval:` configuration this cuts publish wall time substantially — for the trivial case the savings
22
24
  # are small (the per-publish path is already ≈2ms once Environment is warm).
23
25
  #
@@ -94,7 +94,7 @@ module Rigor
94
94
  type: "string",
95
95
  enum: %w[untyped observed],
96
96
  description: "Parameter policy: untyped (default) or observed " \
97
- "(harvests call-site argument types from spec/)"
97
+ "(harvests call-site argument types from the configured test_paths)"
98
98
  },
99
99
  config: { type: "string", description: "Path to .rigor.yml (optional)" }
100
100
  }
@@ -134,6 +134,7 @@ module Rigor
134
134
  }
135
135
  }
136
136
  ].freeze
137
+ Ractor.make_shareable(TOOLS)
137
138
 
138
139
  def initialize(config_path: nil, err: $stderr)
139
140
  @config_path = config_path
@@ -8,6 +8,7 @@ require "prism"
8
8
  require_relative "manifest"
9
9
  require_relative "node_context"
10
10
  require_relative "../analysis/diagnostic"
11
+ require_relative "../cache/engine_source"
11
12
  # `producer generation_cap:` defaults to (and validates against) `Cache::Store::UNBOUNDED_GENERATIONS`, and a
12
13
  # plugin class body can be evaluated before anything else pulled the cache layer in.
13
14
  require_relative "../cache/store"
@@ -492,6 +493,10 @@ module Rigor
492
493
  @fact_cache = {}
493
494
  @producer_value_cache = {}
494
495
  @producer_errors = {}
496
+ # Issue #1051 — the run-scoped disclosure table ({#disclose_once}, and since #1060 {#emit_once}).
497
+ # Allocated here for the same reason as the three memos above: a registration is a Hash-content
498
+ # mutation, so it stays sound on a plugin that freezes itself in `initialize`.
499
+ @run_disclosures = {}
495
500
  end
496
501
 
497
502
  # Override in subclasses to wire any state the plugin needs from the injected service container.
@@ -545,6 +550,38 @@ module Rigor
545
550
  []
546
551
  end
547
552
 
553
+ # #392 — the **template-unit** transform: compile one non-Ruby project file into the Ruby the engine
554
+ # analyses (ADR-16 Tier D, revived; design note § 11.3). The engine globs the manifest's
555
+ # `template_globs:`, reads each match's bytes, and calls this once per match on the PARENT, before any
556
+ # analysis — so the transform runs exactly once per file per run and its result marshals into every
557
+ # fork-pool worker.
558
+ #
559
+ # Returns an `Array<Rigor::Plugin::TemplateUnit>`; an empty array declines the file, which is the
560
+ # right answer for a match the plugin's glob caught but its compiler cannot read. The default returns
561
+ # `[]`, so a plugin that declares no `template_globs:` is never asked and one that does may still
562
+ # decline per file.
563
+ #
564
+ # A raise isolates exactly like `#diagnostics_for_file`: the file contributes no unit and the run
565
+ # continues. The hook is the one place a template-unit plugin runs code — everything downstream is
566
+ # the frozen value object — which is what keeps the engine's analysis of a unit identical to its
567
+ # analysis of a `.rb` file.
568
+ def template_units_for_file(path:, source:) # rubocop:disable Lint/UnusedMethodArgument
569
+ []
570
+ end
571
+
572
+ # #1047 — called once at the start of every template-unit COLLECTION PASS over this plugin's claim,
573
+ # before any `#template_units_for_file` of that pass and whether or not the pass ends up calling it
574
+ # (a warm pass may carry every unit and compile none). A plugin whose transform reads state gathered
575
+ # ACROSS its claimed files — rigor-actionpack seeds a partial's locals from the render sites in every
576
+ # view — memoises that state on its instance, and a long-lived owner keeps the instance across
577
+ # passes; this is where such a plugin learns that the memo may now be stale. The collector's
578
+ # per-call order is not a substitute: a warm pass offers only the editor's buffer, so no sequence of
579
+ # paths can tell one pass from the next.
580
+ #
581
+ # The default does nothing. A raise is swallowed: the pass continues, and the plugin's own
582
+ # revalidation simply does not run for it.
583
+ def template_units_pass_started; end
584
+
548
585
  # ADR-37 slice 1 — runs the plugin's declared {.node_rule}s over one file and returns their
549
586
  # diagnostics. The engine owns the single AST walk here so plugin authors never hand-roll a traversal:
550
587
  # every node reachable from `root` is offered to each rule whose `node_type` it satisfies
@@ -619,6 +656,27 @@ module Rigor
619
656
  []
620
657
  end
621
658
 
659
+ # ADR-113 WD4 (#1082) — enumerates the members this plugin synthesizes on `class_name`, for the
660
+ # `rigor lens` declaration map. A `dynamic_return` rule answers a TYPE for one call site at a time;
661
+ # nothing on that path can say "`User` has `email`, `name`, …", which is the fact grep can never
662
+ # find on a DSL-generated member. This hook is that per-class enumeration.
663
+ #
664
+ # Returns an `Array` of `{name:, kind:, type:}` Hashes. `name` is the member name as a caller spells
665
+ # it (String); `kind` is a Symbol grouping same-shaped members (`:column_reader`) — the lens
666
+ # collapses a group into one line; `type` is the `Rigor::Type` the plugin commits to for the member
667
+ # itself, or nil where it does not commit to one. It is the member-level answer, not the best a
668
+ # typed call site can do: a plugin that narrows `user.name` to `String` on a written receiver but
669
+ # declines to type the bare `name` read reports `Dynamic[top]` here. A member the plugin claims
670
+ # but deliberately leaves dynamic carries `Rigor::Type::Combinator.untyped` — the explicit
671
+ # `Dynamic[top]` answer, distinct from nil's "no answer".
672
+ #
673
+ # The hook is off the `check` hot path (ADR-52): `rigor lens` invokes it, `check` never does. It may
674
+ # read state `#prepare` built. The default returns `[]` — a plugin that synthesizes no enumerable
675
+ # members has nothing to declare.
676
+ def declared_members(class_name) # rubocop:disable Lint/UnusedMethodArgument
677
+ []
678
+ end
679
+
622
680
  # Builds a `Rigor::Analysis::Diagnostic` positioned at a Prism `node` for return from
623
681
  # `#diagnostics_for_file`. Internalises the 1-based `line` / `start_column + 1` convention every plugin
624
682
  # otherwise re-derives by hand, so authors pass the node and the message/severity/rule rather than
@@ -686,12 +744,108 @@ module Rigor
686
744
  end
687
745
 
688
746
  # ADR-60 WD4 — the `StandardError` a prior {#producer_value} call rescued for `id`, or nil when it
689
- # succeeded or was never called. Plugins surface it as a load-error diagnostic from
690
- # `#diagnostics_for_file`.
747
+ # succeeded or was never called. Plugins surface it as a run-scoped `load-error` disclosure through
748
+ # {#disclose_once} (#1056) — a failed index is a fact about the run's inputs, not about a file.
691
749
  def producer_error(id)
692
750
  @producer_errors[id.to_sym]
693
751
  end
694
752
 
753
+ # Issue #1051 — records a PROJECT-GLOBAL disclosure: a notice about the run's inputs ("the schema file
754
+ # is not there, so column checks are off") rather than about a line of a file. Registering the same
755
+ # `key` again — on this instance or on any other instance of this plugin in the same run — is a no-op,
756
+ # so the run emits the row exactly once however many workers, files or call sites reached it.
757
+ #
758
+ # disclose_once(:missing_schema,
759
+ # message: "rigor-activerecord: schema file `db/schema.rb` not found; …",
760
+ # severity: :info, rule: "load-error")
761
+ #
762
+ # Callable from any hook — `#prepare` (the usual place: the disclosure is a fact about the project,
763
+ # known before a single file is read), `#diagnostics_for_file`, or a `node_rule` block. It returns
764
+ # `nil` and emits NOTHING itself: the engine harvests the table after analysis, de-duplicates it by
765
+ # `(plugin id, key)` across the coordinator and every pool worker, and emits one row per surviving
766
+ # pair at `.rigor.yml:1:1` — the position every other run-level plugin row already uses (plugin load
767
+ # errors, `#prepare` raises, `plugin_trust.read-refused`).
768
+ #
769
+ # That position is the point of the facility. The pre-#1051 idiom was an `@emitted` flag consulted from
770
+ # `#diagnostics_for_file`, which positions the row at whichever file the plugin instance happened to
771
+ # see first — per WORKER under `--workers N`, so the row was duplicated, and after #393 the file it
772
+ # landed on could be a template unit's `.erb` path, reading as a claim about a view. A disclosure has
773
+ # no source position to be right about; `.rigor.yml` is where the user configured the input it is
774
+ # about.
775
+ #
776
+ # `severity:` defaults to `:info`, the grade plugins use for "here is what I recognised"; pass
777
+ # `:warning` for "I could not run". `key` is any object with a stable `#to_s` (a Symbol reads best) and
778
+ # is never shown to the user — it is the de-duplication identity, so it must not interpolate anything
779
+ # that differs between workers.
780
+ def disclose_once(key, message:, severity: :info, rule: "load-error")
781
+ id = key.to_s
782
+ return nil if run_registration_taken?(id, batch: false)
783
+
784
+ @run_disclosures[id] = {
785
+ key: id, message: message.to_s, severity: severity.to_sym, rule: rule.to_s
786
+ }.freeze
787
+ nil
788
+ end
789
+
790
+ # Issue #1060 — the POSITIONED sibling of {#disclose_once}: registers a whole batch of fully built
791
+ # diagnostics under `key`, emitted once per run however many plugin instances (fork-pool workers)
792
+ # register it. Each row keeps its own `path` / `line` / `column` — use this when a project-wide scan
793
+ # produces rows that are right about a position (a view template's line) but no single analysed file
794
+ # owns them, so returning them from `#diagnostics_for_file` would repeat them once per worker.
795
+ #
796
+ # emit_once(:view_diagnostics, scan_view_files(index))
797
+ #
798
+ # The question that chooses between the two channels is whether the row has a position it could be
799
+ # right about: a notice about the run's inputs has none and goes through {#disclose_once}
800
+ # (`.rigor.yml:1:1`); a row naming a real file and line comes here and keeps it.
801
+ #
802
+ # De-duplication is by `key` alone and the first registration wins: a later batch under the same key
803
+ # is dropped whole, on this instance or on any other instance in the run, never merged row by row.
804
+ # Every worker scans the same project, so a row-wise union could only add a worker's partial or
805
+ # divergent view. `key` shares one namespace per plugin with {#disclose_once} — the two channels are
806
+ # one registration table — but a key keeps the kind it was first registered as: repeating a key on the
807
+ # same channel is a no-op, and reusing it on the OTHER channel (either order) raises `ArgumentError`
808
+ # rather than silently dropping a genuine batch or disclosure.
809
+ #
810
+ # Callable from `#prepare`, `#diagnostics_for_file`, or a `node_rule` block; returns `nil` and emits
811
+ # nothing itself. The engine stamps `source_family: "plugin.<id>"` exactly as it does for a per-file
812
+ # row, so the qualified rule and the position a baseline keys on are the ones `#diagnostics_for_file`
813
+ # would have produced. Each row is copied and frozen on registration so the batch rides the fork
814
+ # payload and the Ractor `:done` message unchanged; a non-{Rigor::Analysis::Diagnostic} element
815
+ # raises `ArgumentError`, which the engine reports as this plugin's `runtime-error`.
816
+ def emit_once(key, diagnostics)
817
+ id = key.to_s
818
+ return nil if run_registration_taken?(id, batch: true)
819
+
820
+ batch = Array(diagnostics).map do |row|
821
+ next row.dup.freeze if row.is_a?(Rigor::Analysis::Diagnostic)
822
+
823
+ raise ArgumentError, "emit_once expects Rigor::Analysis::Diagnostic rows, got #{row.class}"
824
+ end
825
+ @run_disclosures[id] = { key: id, diagnostics: batch.freeze }.freeze
826
+ nil
827
+ end
828
+
829
+ # Engine-facing reader for {#disclose_once} and {#emit_once}: this instance's registrations in
830
+ # registration order; a positioned batch is the record carrying `:diagnostics`. Plugin authors never
831
+ # call it — {Rigor::Analysis::Runner} and {Rigor::Analysis::WorkerSession} drain it, and the
832
+ # de-duplication across instances happens there.
833
+ def run_disclosure_records
834
+ @run_disclosures.values
835
+ end
836
+
837
+ # #1060 — true when `id` is already registered on the same channel (the repeat is a no-op); raises when
838
+ # it is registered on the other one, because either answer there would drop a genuine registration.
839
+ def run_registration_taken?(id, batch:)
840
+ existing = @run_disclosures[id]
841
+ return false if existing.nil?
842
+ return true if existing.key?(:diagnostics) == batch
843
+
844
+ taken = batch ? "a #disclose_once disclosure" : "an #emit_once batch"
845
+ raise ArgumentError, "run-scoped key #{id.inspect} is already registered as #{taken}; use a distinct key"
846
+ end
847
+ private :run_registration_taken?
848
+
695
849
  # Boilerplate-reduction helper (review §1.3): the "did you mean …?" suggestion every
696
850
  # diagnostic-emitting plugin otherwise hand-rolls. Returns the closest of `candidates` to `name` via
697
851
  # `DidYouMean::SpellChecker` (the same engine Ruby's own `NoMethodError` hints use), or `nil` when
@@ -705,7 +859,6 @@ module Rigor
705
859
  DidYouMean::SpellChecker.new(dictionary: dictionary).correct(name.to_s).first
706
860
  end
707
861
 
708
- # Convenience accessor — `manifest` on the instance returns the class-level manifest declaration.
709
862
  def manifest
710
863
  self.class.manifest
711
864
  end
@@ -808,7 +961,12 @@ module Rigor
808
961
  return compute unless store
809
962
 
810
963
  prefixed_id = "plugin.#{manifest.id}.#{producer_id}"
811
- key_descriptor = compose_key_descriptor(descriptor)
964
+ begin
965
+ key_descriptor = compose_key_descriptor(descriptor)
966
+ rescue Cache::EngineSource::Unavailable
967
+ # An unidentifiable engine runs the producer uncached rather than keyed without it (issue #1009).
968
+ return compute
969
+ end
812
970
  lambda do
813
971
  store.fetch_or_validate(
814
972
  producer_id: prefixed_id,
@@ -1026,8 +1184,13 @@ module Rigor
1026
1184
  # plugin-author-supplied extension carrying IDENTITY inputs (gem-version pins, `ConfigEntry` rows,
1027
1185
  # configuration-file digests). The IoBoundary read history deliberately does NOT enter the key — it is
1028
1186
  # recorded post-compute into the dependency descriptor instead (see {#producer_dependency_descriptor}).
1187
+ #
1188
+ # Issue #1009 — the engine's source identity rides in the auto-built half. The manifest version is the
1189
+ # plugin author's promise, and a checkout editing a bundled plugin (or the engine API a producer computes
1190
+ # through) keeps it, so without the identity the next session served the previous build's value into a
1191
+ # run the run-result key had correctly re-analysed.
1029
1192
  def compose_key_descriptor(extra)
1030
- auto_built = Cache::Descriptor.new(plugins: [plugin_entry])
1193
+ auto_built = Cache::Descriptor.new(plugins: [plugin_entry], configs: Cache::EngineSource.key_config_entries)
1031
1194
  return auto_built if extra.nil?
1032
1195
 
1033
1196
  Cache::Descriptor.compose(auto_built, extra)
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module Rigor
6
+ module Plugin
7
+ # Decides, before `exe/rigor` re-execs itself under `RUBY_BOX=1`, whether the running Ruby can host
8
+ # Rigor inside `Ruby::Box` at all. A released Ruby up to and including 4.0.7 cannot: `env_copy()` drops
9
+ # the box a class/module-body proc carries when `Ractor.make_shareable` isolates it, so the first
10
+ # method call inside such a proc dereferences a NULL box and the VM segfaults (Ruby Bug #22260, fixed on
11
+ # CRuby master by `a4ad8e461a`). Rigor defines Ractor-shareable lambdas at module scope as a standing
12
+ # pattern, so on an affected Ruby the `ruby_box` strategy crashes the whole run rather than declining.
13
+ #
14
+ # The check is behavioural, not a version comparison: a child Ruby runs the bug's minimal reproducer
15
+ # under `RUBY_BOX=1` and must print the right answer. That admits a 4.1.0dev build with the fix and a
16
+ # future 4.0.x that backports it, and refuses a development build cut before the fix — none of which a
17
+ # `RUBY_VERSION` bound gets right. The probe costs one short-lived `ruby --disable-gems` process, paid
18
+ # only when a run opts into `ruby_box`.
19
+ #
20
+ # Loaded by `exe/rigor` before anything else, so this file requires nothing from Rigor.
21
+ module BoxProbe
22
+ # A class-body lambda isolated by `Ractor.make_shareable`, then a method dispatch inside it — the
23
+ # four ingredients the crash needs. Exits 2 when `Ruby::Box` is not active (a Ruby without the
24
+ # feature ignores `RUBY_BOX=1`), so a missing feature is told apart from a crash.
25
+ SCRIPT = <<~PROBE
26
+ exit 2 unless defined?(Ruby::Box) && Ruby::Box.respond_to?(:enabled?) && Ruby::Box.enabled?
27
+ module RigorBoxProbe
28
+ RATIONAL = Ractor.make_shareable(lambda { |*args| Rational(*args) })
29
+ end
30
+ print RigorBoxProbe::RATIONAL.call(3, 4).inspect
31
+ PROBE
32
+
33
+ EXPECTED_OUTPUT = "(3/4)"
34
+
35
+ # The reproducer answers in well under a second; a child still running after this is killed and
36
+ # counted as a failure, so a wedged probe cannot hang the launcher (or `rigor lsp` behind it).
37
+ DEADLINE_SECONDS = 10
38
+
39
+ module_function
40
+
41
+ # nil when `ruby` can run Rigor under `RUBY_BOX=1`; otherwise a short reason for the launcher's
42
+ # warning. Only a child killed by a signal is blamed on the bug; any other failure to answer is
43
+ # reported as the probe failing, never as a pass.
44
+ def unsupported_reason(ruby = RbConfig.ruby)
45
+ output, status = run(ruby)
46
+ return "the Ruby::Box probe could not run #{ruby}" if status.nil?
47
+ return nil if status.success? && output == EXPECTED_OUTPUT
48
+ return "Ruby::Box is not available in Ruby #{RUBY_VERSION}" if status.exitstatus == 2
49
+ return "the Ruby::Box probe failed (#{status})" unless status.signaled?
50
+
51
+ "Ruby #{RUBY_VERSION} crashes running Ractor-shareable procs inside Ruby::Box " \
52
+ "(Ruby Bug #22260; a Ruby carrying its fix is needed)"
53
+ end
54
+
55
+ # `RUBYOPT` is cleared so a `-r` of the caller's (bootsnap, a debugger, a coverage hook) cannot fail
56
+ # to load under `--disable-gems` and be mistaken for the crash; the probe needs nothing but core.
57
+ def run(ruby)
58
+ reader, writer = IO.pipe
59
+ pid = ::Process.spawn({ "RUBY_BOX" => "1", "RUBYOPT" => nil }, ruby, "--disable-gems", "-e", SCRIPT,
60
+ in: File::NULL, out: writer, err: File::NULL)
61
+ writer.close
62
+ output = read_until_eof(reader)
63
+ return [output, ::Process.wait2(pid).last] if output
64
+
65
+ ::Process.kill(:KILL, pid)
66
+ ::Process.wait(pid)
67
+ [nil, nil]
68
+ rescue ::SystemCallError
69
+ [nil, nil]
70
+ ensure
71
+ writer&.close unless writer&.closed?
72
+ reader&.close
73
+ end
74
+
75
+ # The child's whole stdout, or nil when it has not closed it by the deadline.
76
+ def read_until_eof(reader)
77
+ deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + DEADLINE_SECONDS
78
+ output = +""
79
+ loop do
80
+ remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
81
+ return nil unless remaining.positive? && reader.wait_readable(remaining)
82
+
83
+ chunk = reader.read_nonblock(4096, exception: false)
84
+ return output if chunk.nil?
85
+
86
+ output << chunk if chunk.is_a?(String)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -24,7 +24,7 @@ module Rigor
24
24
  # One bundled plugin: the gem name a `plugins:` entry spells, and the gems it models.
25
25
  Entry = Data.define(:gem_name, :plugin_id, :target_gems)
26
26
 
27
- BUNDLED_PLUGINS_ROOT = File.join(Loader::ENGINE_ROOT, "plugins")
27
+ BUNDLED_PLUGINS_ROOT = File.join(Loader::ENGINE_ROOT, "plugins").freeze
28
28
 
29
29
  @entries = nil
30
30
  @load_failures = {}
@@ -69,7 +69,7 @@ module Rigor
69
69
  SELF_HEAD = "self"
70
70
 
71
71
  attr_reader :receiver, :method, :singleton, :labels, :narrow, :discharge, :within, :on_result,
72
- :taint, :why
72
+ :taint, :callee, :callee_fallbacks, :responds, :why
73
73
 
74
74
  # The taint causes a plugin row may name. A closed subset of
75
75
  # {Rigor::Effects::TaintCause::ALL}: a plugin may say "and there is more here I cannot see", but
@@ -85,13 +85,31 @@ module Rigor
85
85
  # @param narrow — a {Rigor::Effects::Narrowing} handler name, when the call's own
86
86
  # argument literals settle a question the row cannot (`connection.execute("SELECT …")`)
87
87
  # @param discharge — see above; honoured only for a first-party bundled plugin
88
+ # @param callee — the name of a {Rigor::Effects::CalleeRule} rule, when a call to this
89
+ # method is also a call **graph edge** the syntax does not contain: `render :show` runs
90
+ # `app/views/users/show.html.erb`, which is an effect unit of its own (#393 / #1048). The rule
91
+ # reads the call's own argument literals and names the callee key; the engine owns the strategy,
92
+ # exactly as it does for `narrow:`.
93
+ # @param callee_fallbacks — the framework's lookup ORDER for a callee the rule named, as data (#1065):
94
+ # `{ "js" => ["html"] }` says a callee selector of `js` that no unit answers is retried as `html`
95
+ # before the row's `taint:` is seeded. Keys and values are selectors; each list is tried in order
96
+ # and the first that resolves wins. Meaningless — and refused — without `callee:`. Which rule
97
+ # consults it, and for which targets, is the rule's call rather than the row's: a table a plugin
98
+ # writes cannot know whether a format was inherited or written by the author.
99
+ # @param responds — whether a call to this method supplies the unit's answer, so a
100
+ # {Rigor::Effects::CalleeRule::UNIT_RULES} rule on the same receiver must not also apply. `render`,
101
+ # `redirect_to` and `head` each set it: an action that called one of them did not take Rails'
102
+ # implicit render, and edging it to the conventional template would attribute a view the action
103
+ # never rendered.
88
104
  # @param why — the audit justification, required exactly as `data/effects/core.yml` requires
89
105
  # one of every row: a label with no stated reason is a claim nobody can review.
90
106
  def initialize(receiver:, method:, labels:, why:, singleton: false, narrow: nil, discharge: false, # rubocop:disable Metrics/ParameterLists
91
- within: nil, on_result: false, taint: nil)
107
+ within: nil, on_result: false, taint: nil, callee: nil, callee_fallbacks: nil, responds: false)
92
108
  @receiver = validate_receiver!(receiver)
93
109
  @method = method.to_sym
94
110
  @singleton = singleton ? true : false
111
+ @callee = callee.nil? ? nil : callee.to_s.dup.freeze
112
+ @responds = responds ? true : false
95
113
  @labels = normalize_labels(labels)
96
114
  @narrow = narrow.nil? ? nil : narrow.to_s.dup.freeze
97
115
  @discharge = discharge ? true : false
@@ -99,6 +117,7 @@ module Rigor
99
117
  @on_result = on_result ? true : false
100
118
  validate_on_result!
101
119
  @taint = validate_taint!(taint)
120
+ @callee_fallbacks = validate_callee_fallbacks!(callee_fallbacks)
102
121
  @why = validate_why!(why)
103
122
  freeze
104
123
  end
@@ -125,7 +144,8 @@ module Rigor
125
144
  {
126
145
  "receiver" => @receiver, "method" => @method.to_s, "singleton" => @singleton,
127
146
  "labels" => @labels, "narrow" => @narrow, "discharge" => @discharge, "within" => @within,
128
- "on_result" => @on_result, "taint" => @taint
147
+ "on_result" => @on_result, "taint" => @taint, "callee" => @callee,
148
+ "callee_fallbacks" => @callee_fallbacks, "responds" => @responds
129
149
  }
130
150
  end
131
151
 
@@ -166,9 +186,16 @@ module Rigor
166
186
  value.dup.freeze
167
187
  end
168
188
 
189
+ # A row normally has to say something. The one exception is a row whose whole contribution is a
190
+ # `callee:` EDGE — Rails' implicit render is a fact about an action that made no call at all, so
191
+ # there is no site to colour and no label to state; what the row buys is the edge to the template.
192
+ # Reading the edge and inventing a label beside it would put `rails.response.write` on every private
193
+ # helper a controller happens to define.
169
194
  def normalize_labels(labels)
170
195
  list = Array(labels).map { |label| label.to_s.dup.freeze }
171
- raise ArgumentError, "effect attribution for #{@receiver} must declare at least one label" if list.empty?
196
+ if list.empty? && @callee.nil?
197
+ raise ArgumentError, "effect attribution for #{@receiver} must declare at least one label"
198
+ end
172
199
 
173
200
  list.uniq.sort.freeze
174
201
  end
@@ -197,6 +224,33 @@ module Rigor
197
224
  "got #{taint.inspect}"
198
225
  end
199
226
 
227
+ # A selector is one dot-free, space-free segment — the half of a callee key after the receiver
228
+ # (`html` in `view:users/show.html`). Anything else could never be spliced into a key a unit answers,
229
+ # and a fallback that silently resolved nothing would read to its author as the feature being broken.
230
+ FALLBACK_SELECTOR = /\A[a-z0-9_]+\z/
231
+ private_constant :FALLBACK_SELECTOR
232
+
233
+ def validate_callee_fallbacks!(fallbacks)
234
+ return nil if fallbacks.nil?
235
+ raise ArgumentError, "effect attribution #{key} names `callee_fallbacks:` without a `callee:`" if @callee.nil?
236
+ raise ArgumentError, "effect attribution #{key} `callee_fallbacks:` must be a Hash" unless fallbacks.is_a?(Hash)
237
+
238
+ fallbacks.to_h do |from, to|
239
+ list = Array(to).map { |selector| fallback_selector!(selector) }.uniq
240
+ selector = fallback_selector!(from)
241
+ list.delete(selector)
242
+ [selector, list.freeze]
243
+ end.sort.to_h.freeze
244
+ end
245
+
246
+ def fallback_selector!(selector)
247
+ value = selector.to_s
248
+ return value.dup.freeze if FALLBACK_SELECTOR.match?(value)
249
+
250
+ raise ArgumentError, "effect attribution #{key} `callee_fallbacks:` selector must be one lowercase " \
251
+ "segment, got #{selector.inspect}"
252
+ end
253
+
200
254
  def validate_why!(why)
201
255
  value = why.to_s
202
256
  raise ArgumentError, "effect attribution #{key} needs a `why:` justification" if value.empty?
@@ -35,13 +35,14 @@ module Rigor
35
35
  suffix = "/#{gem_name}.rb"
36
36
  $LOADED_FEATURES.rfind { |feature| feature.end_with?(suffix) }
37
37
  end
38
+ Ractor.make_shareable(FEATURE_RESOLVER)
38
39
 
39
40
  # #194 slice 2 (ADR-93 WD5) — the engine's own root, anchored from THIS file's location: the loader
40
41
  # lives at `<root>/lib/rigor/plugin/loader.rb`, so three levels up is the engine root. It resolves
41
42
  # identically in a git checkout and inside an installed `rigortype` gem, because the gem packages the
42
43
  # `plugins/` tree at the same relative path — which is exactly what makes it a trustworthy anchor for
43
44
  # the engine's own bundled plugin copies.
44
- ENGINE_ROOT = File.expand_path("../../..", __dir__)
45
+ ENGINE_ROOT = File.expand_path("../../..", __dir__).freeze
45
46
 
46
47
  # @param requirer — takes a gem name OR an absolute file path (#194 slice 2 — a bundled plugin is
47
48
  # required by its {.bundled_plugin_path}) and returns truthy on successful require. Defaulted to
@@ -36,9 +36,14 @@ module Rigor
36
36
  # - `method_names` — Array of Symbol method names. A call shape `<receiver_subclass>.get('/path') { ... }`
37
37
  # matches when `:get` is in this list. (Named `verbs:` before ADR-60 WD2 normalised the macro
38
38
  # value-object vocabulary.)
39
- # - `self_type` — Symbol selecting the kind of `self`-binding the substrate applies inside the block.
40
- # Slice 1a accepts only `:receiver_instance` (the block runs as an instance method of the receiver
41
- # class). Other kinds (`:receiver_singleton`, `:dsl_recorder`) are reserved for later slices.
39
+ # - `self_type` — the `self`-binding the substrate applies inside the block. `:receiver_instance`
40
+ # (the default) types `self` as an instance of the receiver class — the Sinatra contract. A String
41
+ # binds `self` to a *named* class instead of the receiver: `"Grape::Endpoint"` binds
42
+ # `Nominal[Grape::Endpoint]` (the block is `instance_eval`'d on an instance of that class, e.g. a
43
+ # route body on `Grape::Endpoint`), and `"singleton(Grape::API::Instance)"` binds
44
+ # `Singleton[Grape::API::Instance]` (the block is `instance_eval`'d on that *class object*, e.g.
45
+ # a `namespace` body on `Grape::API::Instance`). Reserved Symbol names (`:receiver_singleton`,
46
+ # `:dsl_recorder`) remain unaccepted.
42
47
  #
43
48
  # ## Ractor-shareability
44
49
  #
@@ -48,6 +53,13 @@ module Rigor
48
53
  SELF_TYPE_RECEIVER_INSTANCE = :receiver_instance
49
54
  VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE].freeze
50
55
 
56
+ CLASS_NAME_PATTERN = /[A-Z]\w*(?:::[A-Z]\w*)*/
57
+ # `self_type: "Foo::Bar"` — the block is `instance_eval`'d on an instance of `Foo::Bar`.
58
+ NAMED_SELF_TYPE_PATTERN = /\A#{CLASS_NAME_PATTERN}\z/
59
+ # `self_type: "singleton(Foo::Bar)"` — the block is `instance_eval`'d on the `Foo::Bar` class
60
+ # object itself (Grape's `namespace`/`route_param` bodies run on `Grape::API::Instance`).
61
+ SINGLETON_SELF_TYPE_PATTERN = /\Asingleton\((#{CLASS_NAME_PATTERN})\)\z/
62
+
51
63
  attr_reader :receiver_constraint, :method_names, :self_type
52
64
 
53
65
  def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
@@ -57,7 +69,7 @@ module Rigor
57
69
 
58
70
  @receiver_constraint = receiver_constraint.dup.freeze
59
71
  @method_names = method_names.map(&:to_sym).freeze
60
- @self_type = self_type
72
+ @self_type = self_type.is_a?(String) ? self_type.dup.freeze : self_type
61
73
  freeze
62
74
  end
63
75
 
@@ -81,6 +93,31 @@ module Rigor
81
93
  [receiver_constraint, method_names, self_type].hash
82
94
  end
83
95
 
96
+ # The class name a String `self_type` binds `self` to — `"Foo::Bar"` for both the plain and the
97
+ # `singleton(Foo::Bar)` form — or `nil` for `:receiver_instance`.
98
+ def self_type_name
99
+ return nil unless self_type.is_a?(String)
100
+
101
+ self_type[SINGLETON_SELF_TYPE_PATTERN, 1] || self_type
102
+ end
103
+
104
+ # Whether a String `self_type` binds the *class object* (`"singleton(Foo::Bar)"`) rather than an
105
+ # instance (`"Foo::Bar"`). Nominal-receiver call sites only match instance-binding entries —
106
+ # a `singleton(...)` binding exists solely for `instance_eval`-on-class contracts, which always
107
+ # surface as `Singleton[X]` receivers.
108
+ def singleton_binding?
109
+ self_type.is_a?(String) && self_type.match?(SINGLETON_SELF_TYPE_PATTERN)
110
+ end
111
+
112
+ # Whether `self_type` names a class whose *instance* the block is `instance_eval`'d on
113
+ # (`"Foo::Bar"`). These entries are the only ones that also match `Nominal[Y]` receivers: inside
114
+ # an already-narrowed `instance_eval` body (`self : Nominal[ParamsScope]`), a nested call on
115
+ # `self` re-enters the same context. `:receiver_instance` keeps its original Singleton-only
116
+ # contract — a nominal receiver calling a class-level verb is a different (unmodelled) shape.
117
+ def named_instance_binding?
118
+ self_type.is_a?(String) && !singleton_binding?
119
+ end
120
+
84
121
  private
85
122
 
86
123
  def validate_receiver_constraint!(value)
@@ -108,10 +145,12 @@ module Rigor
108
145
 
109
146
  def validate_self_type!(self_type)
110
147
  return if VALID_SELF_TYPES.include?(self_type)
148
+ return if self_type.is_a?(String) &&
149
+ (self_type.match?(NAMED_SELF_TYPE_PATTERN) || self_type.match?(SINGLETON_SELF_TYPE_PATTERN))
111
150
 
112
151
  raise ArgumentError,
113
- "Plugin::Macro::BlockAsMethod#self_type must be one of #{VALID_SELF_TYPES.inspect}, " \
114
- "got #{self_type.inspect}"
152
+ "Plugin::Macro::BlockAsMethod#self_type must be one of #{VALID_SELF_TYPES.inspect} " \
153
+ "or a class-name String ('Foo::Bar' / 'singleton(Foo::Bar)'), got #{self_type.inspect}"
115
154
  end
116
155
  end
117
156
  end