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
@@ -113,6 +113,13 @@ module Rigor
113
113
  )
114
114
  end
115
115
 
116
+ # Whether one entry selects a class — by its fully-qualified name for a `namespace:` entry, by any
117
+ # file that defines it for a `match:` one.
118
+ #
119
+ # Public since #391: `rigor sig-gen`'s emission asks the same question this module's own
120
+ # {.for_classes} asks, and asks it with a defining file it already has. It was private while
121
+ # {.for_classes} was the only caller; a second caller with the same inputs is not a reason to
122
+ # reimplement the predicate.
116
123
  def selects?(entry, class_name, paths)
117
124
  return namespace_match?(entry.namespace, class_name) if entry.namespace
118
125
 
@@ -164,7 +171,7 @@ module Rigor
164
171
  def files_by_class(sources, project_root)
165
172
  root = "#{File.absolute_path(project_root.to_s).chomp('/')}/"
166
173
  sources.each_with_object({}) do |(key, paths), out|
167
- owner = MethodKey.owner(key)
174
+ owner = MethodKey.envelope_owner(key)
168
175
  next if owner.nil?
169
176
 
170
177
  bucket = (out[owner] ||= [])
@@ -177,7 +184,7 @@ module Rigor
177
184
  absolute.start_with?(root) ? absolute[root.length..] : path.to_s
178
185
  end
179
186
 
180
- private_class_method :selects?, :match_segments?, :match_deep?, :files_by_class, :relativize
187
+ private_class_method :match_segments?, :match_deep?, :files_by_class, :relativize
181
188
  end
182
189
  end
183
190
  end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Effects
7
+ # Which side of its class a definition written at some point lands on, and what `self` its body runs
8
+ # on, or that the syntax does not say. The scanner keys a unit `Class.m` or `Class#m` from the first, and
9
+ # scans the body with the second (the singleton bit {MutationClassifier} and
10
+ # {LocalOwnership.constructor?} read).
11
+ #
12
+ # Ruby answers the two from different places, which is why one bit carried down the walk got both
13
+ # wrong in turn:
14
+ #
15
+ # - a receiver-less `def` lands on the **default definee**: the class the lexically enclosing body
16
+ # opened, which is the singleton class inside `class << self`. A method body does not move it, so
17
+ # `def inner` inside `def self.outer` defines `Foo#inner`, while inside a `def` in `class << self` it
18
+ # defines `Foo.inner`.
19
+ # - `define_method` and the `attr_*` macros are calls on **`self`**, so they define instance methods of
20
+ # the module `self` is: `Foo#x` inside `def self.setup`, where `self` is `Foo`, and `Foo.x` inside
21
+ # `class << self`, where it is `Foo`'s singleton class.
22
+ #
23
+ # Only the spellings that name the new `self` move either answer: `class << self`, and a block given to
24
+ # `class_eval` / `instance_eval` (or an `_exec` / `module_` alias) on `self` or on `singleton_class`.
25
+ # Where the syntax cannot say, the answer is **unknown** and a definition there is no unit. That covers
26
+ # the block of `Class.new` and its kin, an eval on any other receiver, `class << obj`, and a method
27
+ # defined on one object or on a singleton class's own singleton class. Filed under the enclosing class,
28
+ # such a definition would join that class's real method of the same name, and a `Class.new(self)`
29
+ # override shares its name by design.
30
+ #
31
+ # `self_kind` is `:instance`, `:class` (the class the enclosing namespace opened), `:singleton_class`
32
+ # (that class's singleton class) or `:unknown`, and `definee_singleton` is true, false, or nil when
33
+ # unknown. Every context is allocated once.
34
+ class DefinitionContext < Data.define(:self_kind, :definee_singleton)
35
+ CLASS_EVAL_CALLS = %i[class_eval class_exec module_eval module_exec].to_set.freeze
36
+ INSTANCE_EVAL_CALLS = %i[instance_eval instance_exec].to_set.freeze
37
+ # The builders whose block defines into a class or module the call itself creates.
38
+ ANONYMOUS_BUILDERS = { Class: :new, Module: :new, Struct: :new, Data: :define }.freeze
39
+ # The `self_kind`s that are a class object.
40
+ CLASS_SELVES = %i[class singleton_class].to_set.freeze
41
+ private_constant :CLASS_EVAL_CALLS, :INSTANCE_EVAL_CALLS, :ANONYMOUS_BUILDERS, :CLASS_SELVES
42
+
43
+ CONTEXTS = %i[instance class singleton_class unknown].to_h do |self_kind|
44
+ [self_kind, [false, true, nil].to_h do |definee|
45
+ [definee, new(self_kind: self_kind, definee_singleton: definee)]
46
+ end.freeze]
47
+ end.freeze
48
+ private_constant :CONTEXTS
49
+
50
+ def self.of(self_kind, definee_singleton)
51
+ CONTEXTS.fetch(self_kind).fetch(definee_singleton)
52
+ end
53
+
54
+ # Whether a child of `node` may run under another context than `node` does: a call's block, or a
55
+ # `class <<` body. Only these need {#for_child}, and asking once per node rather than once per child
56
+ # keeps the walks from paying a call for every element of a large literal.
57
+ def self.rebinds?(node)
58
+ node.is_a?(Prism::SingletonClassNode) || (node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode))
59
+ end
60
+
61
+ # A class body.
62
+ CLASS_BODY = of(:class, false)
63
+ # A `class << self` body: `self` and the definee are both the singleton class.
64
+ SINGLETON_CLASS_BODY = of(:singleton_class, true)
65
+ # The body of an instance method of the class.
66
+ INSTANCE_METHOD_BODY = of(:instance, false)
67
+ # The top level, where `self` is `main`, an instance of `Object`, and a `def` defines `Object#m`.
68
+ TOP_LEVEL = INSTANCE_METHOD_BODY
69
+ # Where the syntax says neither what `self` is nor where a `def` lands. Every block inside stays here.
70
+ UNKNOWN = of(:unknown, nil)
71
+
72
+ # Whether `self` is a class object here — the singleton bit a unit's body is scanned with.
73
+ def singleton?
74
+ CLASS_SELVES.include?(self_kind)
75
+ end
76
+
77
+ # Whether `self` is the singleton class here, so a call on it (`define_method`, an `attr_*` macro,
78
+ # `include`, `prepend`, `alias_method`) works on the singleton class rather than the class.
79
+ def self_singleton_class?
80
+ self_kind == :singleton_class
81
+ end
82
+
83
+ # The context the body of `def` node `node` runs under, or nil where no key names the method. The
84
+ # unit is keyed singleton exactly when that context is {#singleton?}. The body keeps this default
85
+ # definee: a `def` nested in it lands where one written beside it would.
86
+ #
87
+ # `def self.x` defines a singleton method only where `self` is the class. In an instance method it
88
+ # defines a method on one object, and in `class << self` one on the singleton class's own singleton
89
+ # class. `def Const.x` keeps the reading it has always had, a method of the class the unit is keyed
90
+ # under; any other receiver is an object no key names.
91
+ def def_body(node)
92
+ case node.receiver
93
+ when nil
94
+ DefinitionContext.of(definee_singleton ? :class : :instance, definee_singleton) unless definee_singleton.nil?
95
+ when Prism::SelfNode
96
+ self if self_kind == :class
97
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
98
+ DefinitionContext.of(:class, definee_singleton) unless self_kind == :unknown
99
+ end
100
+ end
101
+
102
+ # The context the block of a literal-name `define_method` written here runs under, or nil where no key
103
+ # names the method; an `attr_*` macro here defines on the same side. Both are calls on `self`, which
104
+ # only a class and its singleton class answer, and the block keeps the definee it closed over.
105
+ def module_call_body
106
+ case self_kind
107
+ when :class then DefinitionContext.of(:instance, definee_singleton)
108
+ when :singleton_class then DefinitionContext.of(:class, definee_singleton)
109
+ end
110
+ end
111
+
112
+ # The context `child` runs under as a child of `node`, a node {.rebinds?} accepts. A call's block and a
113
+ # `class <<` body may rebind `self`; every other child keeps this context. (A block is a call's only
114
+ # `BlockNode` child.)
115
+ def for_child(node, child)
116
+ case node
117
+ when Prism::CallNode then child.is_a?(Prism::BlockNode) ? block(node) : self
118
+ when Prism::SingletonClassNode then child.equal?(node.body) ? singleton_class_body(node.expression) : self
119
+ else self
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ # `class << self` on the class opens its singleton class; any other `class << …` opens one no key names.
126
+ def singleton_class_body(expression)
127
+ self_kind == :class && expression.is_a?(Prism::SelfNode) ? SINGLETON_CLASS_BODY : UNKNOWN
128
+ end
129
+
130
+ def block(call)
131
+ name = call.name
132
+ if CLASS_EVAL_CALLS.include?(name) then class_eval_block(call.receiver)
133
+ elsif INSTANCE_EVAL_CALLS.include?(name) then instance_eval_block(call.receiver)
134
+ elsif anonymous_builder?(call) then UNKNOWN
135
+ else self
136
+ end
137
+ end
138
+
139
+ # `class_eval` makes its receiver both `self` and the definee.
140
+ def class_eval_block(receiver)
141
+ if self_receiver?(receiver) && singleton? then DefinitionContext.of(self_kind, self_kind == :singleton_class)
142
+ elsif singleton_class_receiver?(receiver) then SINGLETON_CLASS_BODY
143
+ else UNKNOWN
144
+ end
145
+ end
146
+
147
+ # `instance_eval` makes its receiver `self` and the receiver's singleton class the definee. On the
148
+ # class that is the singleton class. On the singleton class it is that class's own singleton class,
149
+ # which no key names, so a `define_method` there is keyed and a `def` is not.
150
+ def instance_eval_block(receiver)
151
+ if self_receiver?(receiver) && self_kind == :class then DefinitionContext.of(:class, true)
152
+ elsif (self_receiver?(receiver) && self_kind == :singleton_class) || singleton_class_receiver?(receiver)
153
+ DefinitionContext.of(:singleton_class, nil)
154
+ else UNKNOWN
155
+ end
156
+ end
157
+
158
+ def self_receiver?(receiver)
159
+ receiver.nil? || receiver.is_a?(Prism::SelfNode)
160
+ end
161
+
162
+ # `singleton_class` called on the class.
163
+ def singleton_class_receiver?(receiver)
164
+ self_kind == :class && receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class &&
165
+ self_receiver?(receiver.receiver) && receiver.arguments.nil? && receiver.block.nil?
166
+ end
167
+
168
+ # `Class.new { … }` and its kin, spelled on the core constant.
169
+ def anonymous_builder?(call)
170
+ receiver = call.receiver
171
+ name = case receiver
172
+ when Prism::ConstantReadNode then receiver.name
173
+ when Prism::ConstantPathNode then receiver.name if receiver.parent.nil?
174
+ end
175
+ !name.nil? && ANONYMOUS_BUILDERS[name] == call.name
176
+ end
177
+ end
178
+ end
179
+ end
@@ -29,11 +29,19 @@ module Rigor
29
29
  # The lanes are kept **raw** here — a declared label a proven one already subsumes is dropped where
30
30
  # output is rendered ({LabelSet#excluding_subsumed_by}), never in the table, because a further join
31
31
  # has to see what was actually declared.
32
+ # `unclaimed` is the transitive reading of {Summary#unclaimed?}: true when this method, or anything
33
+ # it reaches, called something nothing described (#391). It is not a label, not a taint, and is read
34
+ # by `rigor sig-gen`'s annotation emission alone — not by the report, the snapshot, or any
35
+ # diagnostic.
32
36
  class Entry < Data.define(:key, :direct, :proven, :undischarged, :declared, :exhaustive, :causes,
33
- :edges)
34
- def initialize(undischarged: nil, declared: nil, **rest)
37
+ :edges, :unclaimed)
38
+ def initialize(undischarged: nil, declared: nil, unclaimed: false, **rest)
35
39
  super(undischarged: undischarged || rest.fetch(:proven), declared: declared || LabelSet::EMPTY,
36
- **rest)
40
+ unclaimed: unclaimed ? true : false, **rest)
41
+ end
42
+
43
+ def unclaimed?
44
+ unclaimed
37
45
  end
38
46
 
39
47
  # The declared labels worth printing beside `proven` — the rendering rule, in one place so the
@@ -157,7 +157,7 @@ module Rigor
157
157
  # synthesized `attr_*` / `define_method` members the effects scanner adds.
158
158
  def keys_by_class(table)
159
159
  table.keys.each_with_object({}) do |key, out|
160
- owner = MethodKey.owner(key)
160
+ owner = MethodKey.envelope_owner(key)
161
161
  next if owner.nil?
162
162
 
163
163
  (out[owner] ||= []) << key
@@ -135,6 +135,21 @@ module Rigor
135
135
  envelope
136
136
  end
137
137
 
138
+ # Whether ANY stratum carries an annotation for this key — ⊤ envelopes included.
139
+ #
140
+ # {#[]} is the import question and rightly refuses a ⊤ envelope: a bound that bounds nothing is not
141
+ # a bound. `rigor sig-gen`'s emission asks the other one (#391): an author who wrote
142
+ # `%a{rigor:v1:effect bogus.nonsense}` has still written about this method, and replacing their
143
+ # typo with an inferred `%a{pure}` would delete the only evidence that they meant to bound it — the
144
+ # `effect.unknown-label` report points at the annotation, and there would no longer be one.
145
+ def annotated?(owner, singleton, selector)
146
+ return false if owner.nil? || empty?
147
+
148
+ key = "#{owner}#{singleton ? '.' : '#'}#{selector}"
149
+ !(@method_envelopes[key] || @class_envelopes[owner] || config_envelope(owner) ||
150
+ @accepted[key]).nil?
151
+ end
152
+
138
153
  private
139
154
 
140
155
  # The first `namespace:` entry selecting `owner`, memoised per class name — one call site's owner
@@ -35,12 +35,59 @@ module Rigor
35
35
  # selector rather than a receiver's, and the propagator resolves it against the ancestry *above* that
36
36
  # class with no closed-world override join — a different question from every other edge, which is why
37
37
  # it is a field rather than a convention over the other three.
38
- Edge = Data.define(:receiver_class, :kind, :selector, :self_call, :super_call) do
38
+ #
39
+ # `constant_receiver` marks a call whose receiver the author wrote as a **constant path** (#1039).
40
+ # The collector keys an edge on the receiver's TYPE, so `self.class.new` inside `Base`, `klass.new`
41
+ # on a `Singleton[Base]` local and a receiver-less `new` in a singleton body all produce the very
42
+ # same tuple as a literal `Base.new` — and the first three really do construct a subclass. Only the
43
+ # constructor rule reads it, and only to DROP the closed-world subclass join a written constant
44
+ # cannot reach.
45
+ #
46
+ # `unclaimed` marks a site NOTHING bounded: no catalogue row, no plugin row, no imported envelope
47
+ # (#391). It is not a taint and never becomes one — an unresolved edge here is overwhelmingly an
48
+ # inherited or gem call the catalogue simply has no row for, which is why the model drops it — but
49
+ # it IS the difference between "the analyzer read every callee" and "the analyzer read the callees
50
+ # it had something to read". Only sig-gen's annotation emission consults it, through
51
+ # {EffectTable::Entry#unclaimed?}: writing `%a{pure}` is a claim about a callee nobody described,
52
+ # and the emitter must decline rather than invent one.
53
+ # `taint_if_unresolved` is #1048's inversion of a taint. A `render :show` whose {CalleeRule} named
54
+ # `view:users/show.html` may or may not reach a unit — only the merged table knows, exactly as only
55
+ # the merged ancestry knows whether a `super` resolves — so the row's `template-not-analysed` cause
56
+ # travels ON the edge as a frozen `[cause, detail]` pair and {Propagator} seeds it only where the
57
+ # edge landed on nothing. Adding a cause where the walk failed rather than subtracting one where it
58
+ # succeeded keeps every step of the fixpoint monotone, and it is the same shape
59
+ # {Propagator.taint_unresolved_super} already has.
60
+ #
61
+ # `fallback_selectors` is #1065's lookup order: the selectors the propagator retries, in order, when
62
+ # `selector` resolves to nothing — `["html"]` on a `view:watchers/_watchers` + `js` edge, because a
63
+ # `.js.erb` template's lookup context is `[:js, :html]`. The FIRST that resolves is the edge's
64
+ # target and the taint stays unseeded; only when every one fails is `taint_if_unresolved` seeded.
65
+ # One ordered list rather than one edge per candidate, because two edges would join BOTH units
66
+ # where both exist, and only one of them runs.
67
+ Edge = Data.define(:receiver_class, :kind, :selector, :self_call, :super_call, :unclaimed,
68
+ :constant_receiver, :taint_if_unresolved, :fallback_selectors) do
39
69
  # Defaulted because every producer but the `super` one records an ordinary call, and an ordinary
40
- # call is not a `super`.
41
- def initialize(super_call: false, **) = super
70
+ # call is not a `super`, and because a producer that says nothing about `unclaimed` /
71
+ # `constant_receiver` means the safe value of each.
72
+ #
73
+ # The defaults do NOT rescue a cache written before a member existed: `Marshal.load` of a `Data`
74
+ # whose member list has grown raises `TypeError: struct size differs`, which the store reads as a
75
+ # miss. That is the right outcome and not the one relied on — the cache identity carries a schema
76
+ # component ({Identity}, `schema:5`), so such an entry is never offered in the first place.
77
+ def initialize(super_call: false, unclaimed: false, constant_receiver: false,
78
+ taint_if_unresolved: nil, fallback_selectors: nil, **)
79
+ super
80
+ end
42
81
  end
43
82
 
83
+ # An ancestry entry that names nothing and can never resolve. The scanner records it where a class
84
+ # body says its ancestry or its constructor is not readable from the source — a superclass
85
+ # expression that is not a constant path (`class K < Struct.new(:a)`), or an `alias` / `alias_method`
86
+ # that makes `initialize` some other method (#1039). It matches no summary key, so every existing
87
+ # walk steps over it; the constructor rule asks for it by name, because "unreadable" is exactly what
88
+ # that rule must not read as "empty".
89
+ OPAQUE_ANCESTOR = "<opaque>"
90
+
44
91
  NO_TABLE = {}.freeze
45
92
  private_constant :NO_TABLE
46
93
 
@@ -107,7 +154,12 @@ module Rigor
107
154
 
108
155
  fold_summaries(summaries, collection.summaries)
109
156
  fold_lists(edges, collection.edges)
110
- superclasses.update(collection.superclasses)
157
+ # The opaque sentinel is sticky across files as well as within one: a class whose constructor one
158
+ # file says is unreadable does not become readable because another file spells its `<`, and the
159
+ # fold must not depend on which file the run reads first.
160
+ superclasses.update(collection.superclasses) do |_key, mine, theirs|
161
+ mine.include?(OPAQUE_ANCESTOR) ? mine : theirs
162
+ end
111
163
  fold_lists(includes, collection.includes)
112
164
  end
113
165
 
@@ -167,7 +219,9 @@ module Rigor
167
219
  end
168
220
 
169
221
  def edge_order(edge)
170
- [edge.receiver_class.to_s, edge.kind.to_s, edge.selector, edge.self_call ? 1 : 0, edge.super_call ? 1 : 0]
222
+ [edge.receiver_class.to_s, edge.kind.to_s, edge.selector, edge.self_call ? 1 : 0,
223
+ edge.super_call ? 1 : 0, edge.constant_receiver ? 1 : 0,
224
+ Array(edge.taint_if_unresolved).join("\x00"), Array(edge.fallback_selectors).join("\x00")]
171
225
  end
172
226
  end
173
227
  end
@@ -196,7 +196,7 @@ module Rigor
196
196
  end
197
197
 
198
198
  # The uniqueness validator's own query. It rides the DECLARED lane with no taint, exactly as every
199
- # other first-party plugin contribution does (ADR-103 WD6): the plugin read the app's own
199
+ # other first-party plugin contribution does (ADR-103 WD6 / WD17): the plugin read the app's own
200
200
  # `validates … uniqueness: true` and knows what Rails does with it, but the analyzer did not read a
201
201
  # body, so this is a trusted claim rather than a proof.
202
202
  def uniqueness_summary(class_name)
@@ -51,6 +51,22 @@ module Rigor
51
51
  def digest(configuration:, registry: Registry.default, catalog: Catalog.default, plugin_facts: nil)
52
52
  Digest::SHA256.hexdigest(
53
53
  [
54
+ # #391 — the summary schema itself. A cached collection written before {Summary#unclaimed?}
55
+ # existed restores with the bit false, which is the direction sig-gen's emission must never
56
+ # guess, so such an entry must not be served rather than merely read carefully.
57
+ #
58
+ # schema:3 — #1039 moved edge RESOLUTION: a singleton `new` edge on a project class now
59
+ # reaches that class's `#initialize`. The warm lanes serve a propagated `EffectTable` (and its
60
+ # `unclaimed` bits) rather than re-resolving, so an entry written under the old rule would
61
+ # answer the old closure for source nothing changed.
62
+ #
63
+ # schema:4 — #1048 added {FileCollection::Edge#taint_if_unresolved}, so a marshalled edge
64
+ # written under schema:3 no longer restores at all (`Data` with a grown member list raises
65
+ # `TypeError`), and a warm entry would otherwise be offered and then read as a miss.
66
+ #
67
+ # schema:5 — #1065 added {FileCollection::Edge#fallback_selectors}, which both grows the
68
+ # marshalled member list again and changes which unit a cached `view:` edge resolves to.
69
+ "schema:5",
54
70
  "vocabulary:#{registry.vocabulary_version}",
55
71
  "catalog:#{catalog.identity}",
56
72
  "effects:#{config_digest(configuration)}",
@@ -11,8 +11,8 @@ module Rigor
11
11
  #
12
12
  # Ruby has no by-ref parameters, so `mutate.local` cannot mean "a write into an out-parameter" as it
13
13
  # does in PHP. It means the mutated receiver is invisible to the caller, and that is an ownership
14
- # question: a local whose every assignment allocates (`[]`, `{}`, `""`, `.new`, `.dup`) and which never
15
- # escapes the body is one no caller can observe being mutated.
14
+ # question: a local whose every assignment allocates (`[]`, `{}`, `""`, `Foo.new`, `.dup`) and which
15
+ # never escapes the body is one no caller can observe being mutated.
16
16
  #
17
17
  # The analysis is deliberately **flow-insensitive and whole-body**: a local that escapes anywhere
18
18
  # disqualifies, even if the escape happens after the mutation. That is strictly more conservative than
@@ -23,16 +23,17 @@ module Rigor
23
23
  # This is the tracer slice's approximation, not the eventual answer. `ClosureEscapeAnalyzer` answers a
24
24
  # different question (fact retention, not "does the code contain") and is deliberately left alone.
25
25
  module LocalOwnership
26
- # Assignment right-hand sides that witness a fresh allocation. `.new` and `.dup` / `.clone` follow
26
+ # Assignment right-hand sides that witness a fresh allocation. `.dup` / `.clone` follow
27
27
  # [ADR-76](../adr/76-effect-modeling-freeze-dup-shape-preservation.md)'s reading of `dup` as the
28
- # allocation witness.
29
- ALLOCATING_SELECTORS = %i[new dup clone].to_set.freeze
28
+ # allocation witness; `.new` is {constructor?}'s, because only a class object's `new` is `Class#new`.
29
+ COPYING_SELECTORS = %i[dup clone].to_set.freeze
30
30
 
31
31
  module_function
32
32
 
33
33
  # The set of frame-owned local names in `body`, given the method's parameter names (a parameter is
34
- # never frame-owned — the caller holds the same object, so mutating it is `mutate.instance`).
35
- def owned(body, parameter_names)
34
+ # never frame-owned — the caller holds the same object, so mutating it is `mutate.instance`) and
35
+ # whether `self` is a class there ({constructor?}).
36
+ def owned(body, parameter_names, singleton:)
36
37
  return Set.new if body.nil?
37
38
 
38
39
  assignments = {}
@@ -42,23 +43,48 @@ module Rigor
42
43
  assignments.filter_map do |name, values|
43
44
  next if escaped.include?(name) || parameter_names.include?(name)
44
45
 
45
- name if values.all? { |value| allocation?(value) }
46
+ name if values.all? { |value| allocation?(value, singleton: singleton) }
46
47
  end.to_set
47
48
  end
48
49
 
49
50
  # Whether `node` is an expression that allocates a fresh object this frame is the sole holder of.
50
- def allocation?(node)
51
+ # `singleton` is whether the enclosing unit's `self` is a class, as {constructor?} reads it.
52
+ def allocation?(node, singleton:)
51
53
  case node
52
54
  when Prism::ArrayNode, Prism::HashNode, Prism::StringNode, Prism::InterpolatedStringNode,
53
55
  Prism::LambdaNode
54
56
  true
55
57
  when Prism::CallNode
56
- ALLOCATING_SELECTORS.include?(node.name) || unary_plus_string?(node)
58
+ COPYING_SELECTORS.include?(node.name) || constructor?(node, singleton: singleton) ||
59
+ unary_plus_string?(node)
57
60
  else
58
61
  false
59
62
  end
60
63
  end
61
64
 
65
+ # A `new` whose receiver the author wrote as a class object: a constant path, a `class` call
66
+ # (`self.class`, `other.class`), or `self` — explicit or implicit — in a singleton-method body. Any
67
+ # other receiver may be an object whose `new` only shares the name: an ActiveRecord association's
68
+ # `new` builds a record into the association's own target, where the caller can reach it, and a
69
+ # gem's method gives no edge that would carry that to the caller. `self` in an instance method is
70
+ # such a receiver too — an association extension's `new` is the association's.
71
+ def constructor?(node, singleton:)
72
+ return false unless node.name == :new
73
+
74
+ receiver = node.receiver
75
+ case receiver
76
+ when nil, Prism::SelfNode then singleton
77
+ when Prism::ConstantReadNode, Prism::ConstantPathNode then true
78
+ when Prism::CallNode then class_call?(receiver)
79
+ else false
80
+ end
81
+ end
82
+
83
+ # `x.class` — `Kernel#class` answers the receiver's class, unless the receiver overrides it.
84
+ def class_call?(node)
85
+ node.name == :class && node.arguments.nil? && node.block.nil?
86
+ end
87
+
62
88
  # `+""` — the frozen-string-literal era's spelling of "a fresh mutable String".
63
89
  def unary_plus_string?(node)
64
90
  node.name == :+@ && node.receiver.is_a?(Prism::StringNode)
@@ -125,8 +151,8 @@ module Rigor
125
151
  last.is_a?(Prism::LocalVariableReadNode) ? [last.name.to_s] : []
126
152
  end
127
153
 
128
- private_class_method :collect, :record_assignment, :record_escapes, :stored_value, :note_read,
129
- :trailing_reads
154
+ private_class_method :class_call?, :collect, :record_assignment, :record_escapes, :stored_value,
155
+ :note_read, :trailing_reads
130
156
  end
131
157
  end
132
158
  end
@@ -12,6 +12,11 @@ module Rigor
12
12
  # key, and `effects.attribution:` in `.rigor.yml` names one. A key the loader accepts and the scanner
13
13
  # would never produce is a table that silently matches nothing.
14
14
  module MethodKey
15
+ # #393 — the prefix a template unit's key carries (`Plugin::TemplateUnit::KEY_PREFIX`, repeated here
16
+ # rather than required so the effects layer keeps no dependency on the plugin layer; the two are
17
+ # pinned equal by a spec).
18
+ TEMPLATE_UNIT_PREFIX = "view:"
19
+
15
20
  module_function
16
21
 
17
22
  # @return `[owner, separator, selector]`, or nil when `key` is
@@ -35,6 +40,22 @@ module Rigor
35
40
  def owner(key)
36
41
  split(key)&.first
37
42
  end
43
+
44
+ # #393 — a template unit's key: `view:users/show.html`. Deliberately NOT a method key — a view has
45
+ # no owner class and no selector — but {.split} cannot tell, because the format segment gives the
46
+ # string a dot: `owner("view:users/show.html")` answers `"view:users/show"`, a class name no run
47
+ # ever produced. Anything grouping keys by their envelope-bearing owner has to ask this first.
48
+ def template_unit?(key)
49
+ key.to_s.start_with?(TEMPLATE_UNIT_PREFIX)
50
+ end
51
+
52
+ # The name an `effects.envelopes:` entry selects a key by: the owner class for a method key, and the
53
+ # unit key ITSELF for a template unit. A view is its own envelope subject — there is no class to
54
+ # hang the bound on, and `Runner#effect_sources` already knows which file the key came from, which
55
+ # is what a `match:` entry needs.
56
+ def envelope_owner(key)
57
+ template_unit?(key) ? key.to_s : owner(key)
58
+ end
38
59
  end
39
60
  end
40
61
  end
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../inference/mutation_widening"
6
6
  require_relative "label_set"
7
+ require_relative "local_ownership"
7
8
 
8
9
  module Rigor
9
10
  module Effects
@@ -18,7 +19,9 @@ module Rigor
18
19
  # proven lane is worse than a missing one — the proven lane is the one a verdict may read (ADR-5).
19
20
  # 2. **Who owns the receiver?** `self` and its ivars are `mutate.self` (`mutate.static` in singleton
20
21
  # context), a class variable is `mutate.static`, a parameter is `mutate.instance`, a frame-owned
21
- # local is `mutate.local`. **Anything else answers nil**, and the caller records an
22
+ # local is `mutate.local`, and so is a receiver that is itself an allocation (`{}.compare_by_identity`,
23
+ # `raw.dup.force_encoding(e)`) by the local rule's own witness ({LocalOwnership.allocation?}): each
24
+ # evaluation is a new object no caller holds yet. **Anything else answers nil**, and the caller records an
22
25
  # `unknown-ownership` taint rather than a proven bare `mutate`: Ruby's ownership is a dataflow
23
26
  # question, and a proven parent label on a fresh-but-unproven receiver would put findings on correct
24
27
  # code (WD14).
@@ -26,18 +29,23 @@ module Rigor
26
29
  # The only selectors a mutation may be claimed from without knowing the receiver's class.
27
30
  UNIVERSAL_MUTATORS = %i[[]=].to_set.freeze
28
31
 
29
- # `String`'s receiver-mutating surface. `Array` / `Hash` reuse the hand-audited sets the widening
30
- # rules already maintain, cited rather than re-derived (ADR-103 WD3).
31
- STRING_MUTATORS = %i[
32
- << concat replace insert prepend clear
33
- upcase! downcase! capitalize! swapcase! reverse!
34
- strip! lstrip! rstrip! chomp! chop! squeeze! succ! next!
35
- sub! gsub! tr! tr_s! delete! slice! []=
36
- ].to_set.freeze
37
-
38
32
  # `foo=`, and deliberately not `==` / `<=` / `!=` / `===`.
39
33
  ATTRIBUTE_WRITER = /\A[a-z_][A-Za-z0-9_]*=\z/
40
34
 
35
+ # Every Hash method that mutates its receiver. The widening keeps two tables because they answer two different
36
+ # questions of a `HashShape` — `MutationWidening::HASH_MUTATORS` changes the pair set,
37
+ # `HashLookupMutation::MUTATORS` changes what a read of the pairs answers — but either is a write to the
38
+ # receiver, and that is all this asks: `h.compare_by_identity` raises `FrozenError` on a frozen hash as
39
+ # `h.clear` does (unless the hash already compares by identity, when it returns before the check), and a
40
+ # method whose one effect was `@h.compare_by_identity` did not read as mutating while this cited the first
41
+ # table alone.
42
+ #
43
+ # `rehash` is in neither table because it changes no read a shape can state: a shape's keys are literals, which
44
+ # a rebuild never merges. It still rebuilds the receiver's table in place, and it is the one name spelt here.
45
+ HASH_MUTATORS = (
46
+ Inference::MutationWidening::HASH_MUTATORS | Inference::HashLookupMutation::MUTATORS | Set[:rehash]
47
+ ).freeze
48
+
41
49
  LABELS = {
42
50
  self_state: LabelSet.new(["mutate.self"]),
43
51
  static: LabelSet.new(["mutate.static"]),
@@ -57,10 +65,12 @@ module Rigor
57
65
  name = node.name
58
66
  return true if UNIVERSAL_MUTATORS.include?(name) || ATTRIBUTE_WRITER.match?(name.to_s)
59
67
 
68
+ # The per-class sets are the hand-audited ones the widening rules maintain, cited rather than re-derived
69
+ # (ADR-103 WD3): a list kept here drifted from the widening's, and missed `force_encoding`.
60
70
  case receiver_class
61
71
  when "Array" then Inference::MutationWidening::ARRAY_MUTATORS.include?(name)
62
- when "Hash" then Inference::MutationWidening::HASH_MUTATORS.include?(name)
63
- when "String" then STRING_MUTATORS.include?(name)
72
+ when "Hash" then HASH_MUTATORS.include?(name)
73
+ when "String" then Inference::StringMutation::MUTATORS.include?(name)
64
74
  else false
65
75
  end
66
76
  end
@@ -78,6 +88,7 @@ module Rigor
78
88
  @singleton ? :static : :self_state
79
89
  when Prism::ClassVariableReadNode then :static
80
90
  when Prism::LocalVariableReadNode then local_ownership(receiver.name.to_s)
91
+ else :local if LocalOwnership.allocation?(receiver, singleton: @singleton)
81
92
  end
82
93
  end
83
94