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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "digest"
4
4
 
5
+ require_relative "callee_rule"
5
6
  require_relative "entry_points"
6
7
  require_relative "label_set"
7
8
  require_relative "registry"
@@ -65,7 +66,14 @@ module Rigor
65
66
  # `discharge` is the RESOLVED grant, not the manifest's request: {.build} has already demoted a
66
67
  # non-first-party plugin's `true` to `false` and recorded a warning, so nothing downstream has to
67
68
  # re-ask who may discharge.
68
- Row = Data.define(:key, :labels, :narrow, :discharge, :within, :taint, :plugin_id) do
69
+ # `callee` names a {CalleeRule} rule (#1048) and `responds` marks a row whose call supplies the
70
+ # unit's answer, so a unit rule on the same receiver stands down. `receiver` is carried because a
71
+ # UNIT rule has no call node to read one off. `callee_fallbacks` is the plugin's lookup-order table
72
+ # for the rule's answer (#1065), carried verbatim to the edge the rule records.
73
+ Row = Data.define(:key, :labels, :narrow, :discharge, :within, :taint, :plugin_id, :receiver,
74
+ :callee, :callee_fallbacks, :responds) do
75
+ def initialize(receiver: nil, callee: nil, callee_fallbacks: nil, responds: false, **) = super
76
+
69
77
  def discharge? = discharge
70
78
  end
71
79
 
@@ -73,6 +81,10 @@ module Rigor
73
81
  # plugin pointed.
74
82
  Edge = Data.define(:target, :receiver, :selector, :plugin_id)
75
83
 
84
+ # The rows whose `callee:` names a {CalleeRule::UNIT_RULES} rule — applied once per unit by
85
+ # {UnitScan}, from the unit's own owner and key rather than from a call node.
86
+ attr_reader :unit_callee_rows
87
+
76
88
  def self.empty
77
89
  @empty ||= new(contributions: [], superclasses: NO_ROWS, includes: NO_ROWS)
78
90
  end
@@ -110,6 +122,7 @@ module Rigor
110
122
  @self_rows = {}
111
123
  @result_rows = {}
112
124
  @edges = []
125
+ @unit_callee_rows = []
113
126
  @labels_by_owner = {}
114
127
  @entry_points = []
115
128
  @declared_ancestry = {}
@@ -146,9 +159,7 @@ module Rigor
146
159
  # @param owner — the receiver's class name as the syntax or the typer named it
147
160
  # @param singleton — whether the call is `Owner.selector`
148
161
  def class_row(owner, singleton, selector)
149
- return nil if owner.nil? || @class_rows.empty?
150
-
151
- bucket = @class_rows[singleton]
162
+ bucket = owner.nil? ? nil : @class_rows[singleton]
152
163
  return nil if bucket.nil?
153
164
 
154
165
  ancestry(owner).each do |candidate|
@@ -161,9 +172,7 @@ module Rigor
161
172
  # The row colouring `path`'s `selector`, where `path` is a receiver expression (`"Rails.cache"`).
162
173
  # Exact — a receiver path names one object and has no ancestry to walk.
163
174
  def path_row(path, selector)
164
- return nil if path.nil? || @path_rows.empty?
165
-
166
- @path_rows[path]&.[](selector)
175
+ path.nil? ? nil : @path_rows[path]&.[](selector)
167
176
  end
168
177
 
169
178
  # The row colouring `path`'s `selector` for a receiver rooted at implicit self (`"self.flash.now"`),
@@ -171,11 +180,8 @@ module Rigor
171
180
  # `owner_class`'s project ancestry — a receiver-less `session` outside a controller is a different
172
181
  # `session`.
173
182
  def self_path_row(path, selector, owner_class)
174
- return nil if path.nil? || @self_rows.empty?
175
-
176
- row = @self_rows[path]&.[](selector)
177
- return nil if row.nil?
178
- return nil unless descends_from?(owner_class, row.within)
183
+ row = path.nil? ? nil : @self_rows[path]&.[](selector)
184
+ return nil if row.nil? || !descends_from?(owner_class, row.within)
179
185
 
180
186
  row
181
187
  end
@@ -201,9 +207,7 @@ module Rigor
201
207
 
202
208
  # Whether `class_name` is `ancestor`, or reaches it through the project's own `class … <` lines.
203
209
  def descends_from?(class_name, ancestor)
204
- return false if class_name.nil?
205
-
206
- ancestry(class_name).include?(ancestor)
210
+ !class_name.nil? && ancestry(class_name).include?(ancestor)
207
211
  end
208
212
 
209
213
  # `registry` extended with every plugin's `effect_labels:`, each under its own owner so
@@ -225,10 +229,10 @@ module Rigor
225
229
  note_root_demotion(contribution)
226
230
  (@labels_by_owner[contribution.owner] ||= []).concat(contribution.labels)
227
231
  contribution.attributions.each { |entry| absorb_attribution(contribution, entry) }
228
- contribution.edges.each do |entry|
229
- @edges << Edge.new(target: entry.target, receiver: entry.receiver, selector: entry.method,
230
- plugin_id: contribution.id)
231
- end
232
+ @edges.concat(contribution.edges.map do |entry|
233
+ Edge.new(target: entry.target, receiver: entry.receiver, selector: entry.method,
234
+ plugin_id: contribution.id)
235
+ end)
232
236
  @entry_points.concat(contribution.entry_points)
233
237
  contribution.ancestry.each { |entry| absorb_ancestry(contribution, entry) }
234
238
  end
@@ -267,8 +271,22 @@ module Rigor
267
271
  def absorb_attribution(contribution, entry)
268
272
  row = Row.new(key: entry.key, labels: LabelSet.new(entry.labels), narrow: entry.narrow,
269
273
  discharge: discharge_granted?(contribution, entry), within: entry.within,
270
- taint: entry.taint, plugin_id: contribution.id)
274
+ taint: entry.taint, plugin_id: contribution.id, receiver: entry.receiver,
275
+ callee: callee_rule_for(contribution, entry), responds: entry.responds,
276
+ callee_fallbacks: entry.callee_fallbacks)
271
277
  (bucket_for(entry)[entry.receiver] ||= {})[entry.method.to_s] = row
278
+ @unit_callee_rows << row if row.callee && CalleeRule.unit_rule?(row.callee)
279
+ end
280
+
281
+ # #1048 — a `callee:` naming a rule the engine does not implement is dropped with a warning rather
282
+ # than silently doing nothing, for the reason {#note_root_demotion} gives: a contribution that
283
+ # quietly evaporates reads to its author as the feature being broken.
284
+ def callee_rule_for(contribution, entry)
285
+ return entry.callee if entry.callee.nil? || CalleeRule.known?(entry.callee)
286
+
287
+ @warnings << "plugin #{contribution.id.inspect} names an unknown callee rule " \
288
+ "#{entry.callee.inspect} on #{entry.key}; the row contributes no edge"
289
+ nil
272
290
  end
273
291
 
274
292
  # Which index a row lands in, from its receiver spelling.
@@ -351,22 +369,16 @@ module Rigor
351
369
  bucket.sort.map do |receiver, rows|
352
370
  [receiver,
353
371
  rows.sort.map do |selector, row|
354
- [selector, row.labels.to_a, row.narrow, row.discharge, row.within, row.taint]
372
+ [selector, row.labels.to_a, row.narrow, row.discharge, row.within, row.taint,
373
+ row.callee, row.callee_fallbacks, row.responds]
355
374
  end]
356
375
  end
357
376
  end
358
377
 
359
378
  def finalize
360
- @class_rows.each_value { |bucket| bucket.each_value(&:freeze) }
361
- @class_rows.each_value(&:freeze)
362
- @class_rows.freeze
363
- @path_rows.each_value(&:freeze)
364
- @path_rows.freeze
365
- @self_rows.each_value(&:freeze)
366
- @self_rows.freeze
367
- @result_rows.each_value(&:freeze)
368
- @result_rows.freeze
369
- @edges.freeze
379
+ @class_rows.each_value { |bucket| bucket.each_value(&:freeze).freeze }
380
+ [@path_rows, @self_rows, @result_rows].each { |bucket| bucket.each_value(&:freeze).freeze }
381
+ [@class_rows, @edges, @unit_callee_rows].each(&:freeze)
370
382
  @labels_by_owner.each_value(&:uniq!)
371
383
  @labels_by_owner.reject! { |_, labels| labels.empty? }
372
384
  @labels_by_owner.freeze
@@ -48,12 +48,15 @@ module Rigor
48
48
  # @param collection — the run's merged per-file collections
49
49
  # @param discharge — the `effects.tolerated:` policy the undischarged lane is computed
50
50
  # under; {Discharge.none} makes the two lanes equal.
51
- def propagate(collection, discharge: Discharge.none)
51
+ # @param declined_unit_keys — unit keys the run KNOWS a source file for and has no summary for
52
+ # (#1065): a template a plugin claimed and declined. An edge's fallback list is not tried past one
53
+ # of them, because the file the framework runs is the one that produced nothing.
54
+ def propagate(collection, discharge: Discharge.none, declined_unit_keys: nil)
52
55
  return EffectTable.empty if collection.summaries.empty?
53
56
 
54
57
  summaries = collection.summaries
55
58
  state = seed(summaries, discharge)
56
- edges = resolve_edges(collection, state)
59
+ edges = resolve_edges(collection, state, declined_unit_keys)
57
60
  iterate(state, edges)
58
61
  EffectTable.new(build_entries(summaries, edges, state))
59
62
  rescue StandardError
@@ -62,12 +65,14 @@ module Rigor
62
65
 
63
66
  # `{caller_key => [callee_key]}`, sorted and de-duplicated. Seeds the `unresolved-super` taint in
64
67
  # the same pass, because whether a `super` resolved is exactly what this resolution answers.
65
- def resolve_edges(collection, state)
66
- index = Index.new(collection)
68
+ def resolve_edges(collection, state, declined_unit_keys = nil)
69
+ index = Index.new(collection, declined_unit_keys: declined_unit_keys)
67
70
  collection.edges.each_with_object({}) do |(caller_key, list), out|
68
71
  targets = list.flat_map do |edge|
69
- resolved = index.targets_for(edge)
72
+ resolved = index.resolve(edge)
70
73
  taint_unresolved_super(state, caller_key, edge) if edge.super_call && resolved.empty?
74
+ taint_unresolved_callee(state, caller_key, edge) if edge.taint_if_unresolved && resolved.empty?
75
+ mark_unclaimed(state, caller_key) if edge.unclaimed && !edge.super_call && !index.owner_resolved?(edge)
71
76
  resolved
72
77
  end.uniq.sort
73
78
  out[caller_key] = targets.freeze unless targets.empty?
@@ -95,6 +100,47 @@ module Rigor
95
100
  entry[:causes] << ["unresolved-super", edge.selector].freeze
96
101
  end
97
102
 
103
+ # #1048 — a `callee:` edge a plugin's row produced, whose named unit is not in the table: the
104
+ # template the render site pointed at was never analysed, so the row's `template-not-analysed` cause
105
+ # is seeded here after all.
106
+ #
107
+ # The taint is ADDED on failure rather than subtracted on success, which is what keeps the fixpoint
108
+ # monotone and the answer independent of visit order. It is also why a row whose rule declined
109
+ # outright — a computed `render foo`, a `render json:` — never reaches this: {UnitScan} tainted that
110
+ # site directly and recorded no edge, so nothing about an unresolvable render changed.
111
+ def taint_unresolved_callee(state, caller_key, edge)
112
+ entry = state[caller_key]
113
+ return if entry.nil?
114
+
115
+ entry[:exhaustive] = false
116
+ # Frozen here rather than trusted from the edge: `Marshal.load` of a `Data` bypasses
117
+ # `initialize`, so a pooled worker's collection restores the pair unfrozen while a sequential
118
+ # one has the scan's frozen original. A cause travels into a `Set` shared by the whole fixpoint,
119
+ # and the two paths must hand it the same value. `#freeze` on an already-frozen array is free.
120
+ entry[:causes] << edge.taint_if_unresolved.freeze
121
+ end
122
+
123
+ # #391 — an edge nothing bounded whose receiver's OWN ancestry holds no project definition: the
124
+ # callee's footprint was described by nobody, so the closure is "what the analyzer read", not
125
+ # "what the method does".
126
+ #
127
+ # The question is deliberately {Index#owner_resolved?} and NOT "did this edge resolve to nothing".
128
+ # An ordinary edge also joins every project subclass override of the selector (the closed-world
129
+ # join of ADR-103 WD4), so a receiver typed as a gem class a project subclass happens to override
130
+ # resolves non-empty while the receiver's own dispatch target is still undescribed — `B.run` on a
131
+ # `Base2 < Vendor::Client` reaches `Vendor::Client#run` at run time whatever `Sub2#run` does.
132
+ #
133
+ # Decided HERE for the same reason the `super` taint is: only the merged ancestry can say whether
134
+ # the edge resolves. Unlike that taint it is not a cause and does not touch exhaustiveness — every
135
+ # existing consumer reads exactly what it read before. Its one reader is sig-gen's emission, which
136
+ # must not write `%a{pure}` about a callee nobody described.
137
+ def mark_unclaimed(state, caller_key)
138
+ entry = state[caller_key]
139
+ return if entry.nil?
140
+
141
+ entry[:unclaimed] = true
142
+ end
143
+
98
144
  # Causes are carried as a Set through the fixpoint and flattened back to a sorted Array in
99
145
  # {build_entries}. A Set is what {absorb} needs: unioning one along an edge must cost the source's
100
146
  # size and allocate NOTHING when it adds nothing, and the array-concat-and-uniq it replaces
@@ -105,7 +151,8 @@ module Rigor
105
151
  proven: summary.proven,
106
152
  undischarged: discharge.inert? ? summary.proven : discharge.undischarged(summary.bundles),
107
153
  declared: summary.declared,
108
- exhaustive: summary.exhaustive?, causes: Set.new(summary.causes)
154
+ exhaustive: summary.exhaustive?, causes: Set.new(summary.causes),
155
+ unclaimed: summary.unclaimed?
109
156
  }
110
157
  end
111
158
  end
@@ -164,6 +211,10 @@ module Rigor
164
211
  target[:exhaustive] = false
165
212
  changed = true
166
213
  end
214
+ if !target[:unclaimed] && source[:unclaimed]
215
+ target[:unclaimed] = true
216
+ changed = true
217
+ end
167
218
  causes = target[:causes]
168
219
  source[:causes].each { |cause| changed = true if causes.add?(cause) }
169
220
  changed
@@ -190,25 +241,31 @@ module Rigor
190
241
  declared: closed[:declared],
191
242
  exhaustive: closed[:exhaustive],
192
243
  causes: closed[:causes].sort_by { |cause, detail| [cause, detail.to_s] }.freeze,
193
- edges: edges.fetch(key, NO_EDGES)
244
+ edges: edges.fetch(key, NO_EDGES),
245
+ unclaimed: closed[:unclaimed]
194
246
  )
195
247
  end
196
248
  end
197
249
 
198
- private_class_method :resolve_edges, :taint_unresolved_super, :seed, :iterate, :reverse_edges,
199
- :absorb, :join_lane, :build_entries
250
+ private_class_method :resolve_edges, :taint_unresolved_super, :taint_unresolved_callee,
251
+ :mark_unclaimed, :seed, :iterate,
252
+ :reverse_edges, :absorb, :join_lane, :build_entries
200
253
 
201
254
  # The class graph a run's collections describe, and the edge resolution over it. Built once per
202
255
  # propagation; every lookup is a Hash read.
203
256
  class Index
204
- def initialize(collection)
257
+ def initialize(collection, declined_unit_keys: nil)
205
258
  @summaries = collection.summaries
259
+ @declined = declined_unit_keys.nil? || declined_unit_keys.empty? ? nil : declined_unit_keys.to_a.to_set
206
260
  @superclasses = collection.superclasses
207
261
  @includes = collection.includes
208
262
  @classes = build_classes(collection)
209
263
  @descendants = build_descendants(collection.superclasses)
210
264
  @descendant_closures = {}
211
265
  @targets = {}
266
+ # #391 — `{memo key => whether the receiver's own ancestry answered}`, filled by
267
+ # {#call_targets} so {#owner_resolved?} costs a Hash read rather than a second ancestor walk.
268
+ @owner_resolved = {}
212
269
  end
213
270
 
214
271
  # Every project method key `edge` may reach: the definition its ancestry resolves to, plus every
@@ -218,17 +275,237 @@ module Rigor
218
275
  # one such tuple is asked for once per call site in the project. `ApplicationRecord#save` alone is
219
276
  # thousands of sites on a Rails app, each of which used to re-walk the whole subclass forest.
220
277
  def targets_for(edge)
221
- @targets[[edge.receiver_class, edge.kind, edge.selector, edge.super_call]] ||= begin
278
+ @targets[memo_key(edge)] ||= begin
222
279
  separator = edge.kind == :singleton ? "." : "#"
223
- edge.super_call ? super_targets(edge, separator) : call_targets(edge, separator)
280
+ key = memo_key(edge)
281
+ if edge.super_call
282
+ super_targets(edge, separator)
283
+ else
284
+ constructor_targets(edge, key) || call_targets(edge, key, separator)
285
+ end
224
286
  end
225
287
  end
226
288
 
289
+ # {#targets_for}, then — only where that answered nothing — the first of the edge's
290
+ # `fallback_selectors` that resolves (#1065), spelled as the same edge with that selector. A unit
291
+ # under the requested key therefore always wins and a fallback never joins a second target beside
292
+ # it: the lookup runs ONE template, and an edge to both would put a label on the caller that no
293
+ # execution of the render produces. Empty when every candidate fails, which is what lets the
294
+ # caller seed `taint_if_unresolved` exactly as before.
295
+ # A requested key the run **declined** — a template whose file exists and whose plugin produced no
296
+ # unit — stops there. "No unit answers" and "no such template" are different facts, and only the
297
+ # second licenses the framework's next candidate: an `_x.js.haml` beside an `_x.html.erb` is run by
298
+ # Action View as Haml, so joining the ERB unit would be a label no execution produces.
299
+ def resolve(edge)
300
+ resolved = targets_for(edge)
301
+ return resolved unless resolved.empty? && edge.fallback_selectors
302
+ return NO_TARGETS if declined?(edge)
303
+
304
+ fallback_targets(edge)
305
+ end
306
+
307
+ # Whether the receiver's OWN ancestry holds a project definition of the selector — the half of
308
+ # {#targets_for} the closed-world subclass join hides (#391).
309
+ #
310
+ # `targets_for` answers "which project bodies may this edge reach", and for an effect closure the
311
+ # join is right: Ruby has no `final`, so a subclass override really can run. It is the wrong
312
+ # question for "is this callee described at all". A receiver typed as a class the project only
313
+ # subclasses dispatches into that class's own implementation, which lives in a gem or in core;
314
+ # that a project subclass overrides the same selector says nothing about it. Only emission asks
315
+ # this, and only to decline.
316
+ def owner_resolved?(edge)
317
+ targets_for(edge)
318
+ @owner_resolved.fetch(memo_key(edge), false)
319
+ end
320
+
227
321
  private
228
322
 
229
- def call_targets(edge, separator)
323
+ def declined?(edge)
324
+ !@declined.nil? && @declined.include?("#{edge.receiver_class}.#{edge.selector}")
325
+ end
326
+
327
+ def fallback_targets(edge)
328
+ edge.fallback_selectors.each do |selector|
329
+ resolved = targets_for(edge.with(selector: selector, fallback_selectors: nil))
330
+ return resolved unless resolved.empty?
331
+ end
332
+ NO_TARGETS
333
+ end
334
+
335
+ def memo_key(edge)
336
+ [edge.receiver_class, edge.kind, edge.selector, edge.super_call, edge.constant_receiver]
337
+ end
338
+
339
+ NEW_SELECTOR = "new"
340
+ INITIALIZE_SELECTOR = "initialize"
341
+ private_constant :NEW_SELECTOR, :INITIALIZE_SELECTOR
342
+
343
+ # Class objects whose `new` is Ruby's own reflective constructor rather than a project class's
344
+ # (#1039). `Class.new` allocates an anonymous class and runs `Class#initialize`, NOT the
345
+ # `#initialize` of whatever the project happens to have named `Class`; the same holds for
346
+ # `Module.new`, and for `Struct.new` / `Data.define`, which build a class rather than an instance.
347
+ # Listed by name rather than left to the project-known guard below, because a project that reopens
348
+ # `Class` at all would otherwise turn every `Class.new` in it into a call on that reopening.
349
+ RESERVED_CONSTRUCTOR_OWNERS = %w[Class Module Struct Data].freeze
350
+ private_constant :RESERVED_CONSTRUCTOR_OWNERS
351
+
352
+ # Superclass spellings whose `#initialize` is Ruby's own and empty. A project chain that ends here
353
+ # — or ends implicitly, with no `<` at all — has no constructor body anywhere, which is what
354
+ # {#empty_constructor?} has to establish before it may say ∅ rather than "undescribed".
355
+ EMPTY_CONSTRUCTOR_ROOTS = %w[Object BasicObject].freeze
356
+ private_constant :EMPTY_CONSTRUCTOR_ROOTS
357
+
358
+ # #1039 — `Const.new` on a class the project defines, resolved to that class's `#initialize`.
359
+ #
360
+ # The collector records the call as `(Const, :singleton, "new")`, because that is what the typer
361
+ # saw; nothing in the project defines `Const.new`, so the edge resolved to nothing and every
362
+ # constructor body stayed out of its caller's closure. `new` is the one selector in Ruby whose
363
+ # dispatch target is spelled under a different key, and this is the rewrite: the same ancestor walk
364
+ # {#resolve_owner} performs, on the instance side, for `initialize`.
365
+ #
366
+ # Three boundaries, and each of them can only narrow:
367
+ #
368
+ # - a project `def self.new` **wins**. It is the definition `Const.new` actually reaches, and a
369
+ # constructor that overrides `new` is the case where `#initialize` is not the answer. Resolved
370
+ # through the singleton ancestry, so an inherited `self.new` wins too.
371
+ # - the receiver must be a class the project defines, and must not be one of
372
+ # {RESERVED_CONSTRUCTOR_OWNERS}. `Class.new { … }` is not a constructor call on a project class
373
+ # and must never reach a project `#initialize`; a gem class the project only calls `new` on is
374
+ # undescribed exactly as it was, and stays unclaimed.
375
+ # - the closed-world subclass join is **dropped for a written constant receiver, and only for one**.
376
+ # `Base.new` names the class object it constructs, so `Sub#initialize` cannot run and joining it
377
+ # would put a proven label on the caller that no execution of that site can produce — the `super`
378
+ # argument (see {#super_targets}) rather than the ordinary-call one. But the edge is keyed on the
379
+ # receiver's TYPE, and `self.class.new`, a `Singleton[Base]` local's `.new` and a receiver-less
380
+ # `new` in a singleton body produce the identical tuple while genuinely constructing a subclass.
381
+ # {FileCollection::Edge#constant_receiver} is what tells them apart, and every shape that is not
382
+ # a written constant keeps the join — over `Sub#initialize` and over a subclass `Sub.new` alike.
383
+ # - an ancestry the scanner could not read ({FileCollection::OPAQUE_ANCESTOR} — a non-constant
384
+ # superclass expression, an aliased `initialize`) declines, leaving the caller as unclaimed as it
385
+ # was before this rule existed.
386
+ #
387
+ # @return the targets, or `nil` when the rewrite does not apply and ordinary resolution should run
388
+ def constructor_targets(edge, memo_key)
389
+ return nil unless constructor_edge?(edge)
390
+
391
+ owner = resolve_owner(edge.receiver_class, "#", INITIALIZE_SELECTOR)
392
+ return nil unless owner || empty_constructor?(edge.receiver_class)
393
+
394
+ @owner_resolved[memo_key] = true
395
+ targets = owner ? [owner] : []
396
+ targets.concat(subclass_constructors(edge.receiver_class)) unless edge.constant_receiver
397
+ targets.uniq.freeze
398
+ end
399
+
400
+ # Whether the `new` rewrite applies to this edge at all — the guards of {#constructor_targets},
401
+ # each of which can only decline: a singleton `new` on a project class that is not one of Ruby's
402
+ # own class builders, whose singleton ancestry defines no `new` of its own, and whose constructor
403
+ # the scan could read both above it and (where the join applies) below it.
404
+ def constructor_edge?(edge)
405
+ edge.kind == :singleton && edge.selector == NEW_SELECTOR &&
406
+ !RESERVED_CONSTRUCTOR_OWNERS.include?(edge.receiver_class) &&
407
+ project_class?(edge.receiver_class) &&
408
+ resolve_owner(edge.receiver_class, ".", NEW_SELECTOR).nil? &&
409
+ !opaque_ancestry?(edge.receiver_class) &&
410
+ (edge.constant_receiver || !opaque_descendant?(edge.receiver_class))
411
+ end
412
+
413
+ # Every constructor a subclass of `class_name` supplies — its own `#initialize`, and its own
414
+ # `.new` where it overrides one. The closed-world join of step 2, spelled for the two keys a
415
+ # constructor can live under.
416
+ def subclass_constructors(class_name)
417
+ descendant_closure(class_name).each_with_object([]) do |subclass, keys|
418
+ keys << "#{subclass}##{INITIALIZE_SELECTOR}" if @summaries.key?("#{subclass}##{INITIALIZE_SELECTOR}")
419
+ keys << "#{subclass}.#{NEW_SELECTOR}" if @summaries.key?("#{subclass}.#{NEW_SELECTOR}")
420
+ end
421
+ end
422
+
423
+ # The same question DOWNWARD, and only where the closed-world join applies. A subclass whose own
424
+ # constructor is unreadable — `class AliasedChild < Parent; alias initialize setup` — is reached by
425
+ # `self.class.new` in `Parent`, and {#subclass_constructors} would find no `AliasedChild#initialize`
426
+ # key and say nothing. The join is the whole reason this edge may construct a subclass at all, so an
427
+ # unreadable one in the closure declines the edge instead.
428
+ def opaque_descendant?(class_name)
429
+ descendant_closure(class_name).any? do |subclass|
430
+ @includes.fetch(subclass, []).include?(FileCollection::OPAQUE_ANCESTOR) ||
431
+ @superclasses.fetch(subclass, []).include?(FileCollection::OPAQUE_ANCESTOR)
432
+ end
433
+ end
434
+
435
+ # Whether anything in `class_name`'s ancestry told the scanner its constructor is not readable from
436
+ # the source (#1039): a superclass expression that is not a constant path, or an `alias` that makes
437
+ # `initialize` another method. The sentinel rides the ancestry tables, so one walk finds it wherever
438
+ # in the chain it was recorded.
439
+ def opaque_ancestry?(class_name)
440
+ queue = [class_name]
441
+ seen = Set.new
442
+ until queue.empty?
443
+ current = queue.shift
444
+ next unless seen.add?(current)
445
+ return true if current == FileCollection::OPAQUE_ANCESTOR
446
+
447
+ queue.concat(@includes.fetch(current, []))
448
+ queue.concat(@superclasses.fetch(current, []))
449
+ end
450
+ false
451
+ end
452
+
453
+ # Whether `class_name`'s constructor is *known* to be `BasicObject#initialize`, whose footprint is
454
+ # ∅ (#1039).
455
+ #
456
+ # This is the design choice in the rewrite. "No `#initialize` in the ancestry" has two readings —
457
+ # the project defines none and none exists (a plain `class Bare; end`, constructed by Ruby's own
458
+ # empty constructor), or the project defines none and a gem's base class does. The first resolves
459
+ # to a known-empty definition: the edge contributes nothing AND leaves its caller claimed, because
460
+ # the callee was read — there is simply nothing in it. The second is undescribed and must keep
461
+ # marking its caller unclaimed. Only an ancestry that closes inside the project can tell them
462
+ # apart, so that is what this walks, and it answers `false` the moment the walk leaves.
463
+ #
464
+ # No summary row is invented for `BasicObject#initialize`. A row would be a *method of the
465
+ # project* in every table that lists them — the snapshot, `rigor effects`, the pure report — for a
466
+ # definition the project does not contain; the answer belongs to the edge, and the edge is already
467
+ # the thing {#owner_resolved?} is asked about.
468
+ #
469
+ # An `include` anywhere in the chain ends the walk conservatively. The collection's include table
470
+ # is a flat list of as-written *candidate spellings* (`include Foo` inside `module A` records both
471
+ # `A::Foo` and `Foo`), so it cannot say whether a class includes one project module or one gem
472
+ # module the project cannot see — and a module is free to define `initialize`. Such a class keeps
473
+ # exactly today's behaviour: its `new` edge resolves to nothing and its callers stay unclaimed.
474
+ # The table holds instance-side includes only. A module mixed into the singleton class, by
475
+ # `extend` or by an `include` inside `class << self`, is not in it, so a `new` such a module
476
+ # supplies is not seen, and the walk can answer `true` for a class whose `new` does work.
477
+ def empty_constructor?(class_name)
478
+ queue = [class_name]
479
+ seen = Set.new
480
+ until queue.empty?
481
+ current = queue.shift
482
+ next unless seen.add?(current)
483
+ return false unless project_class?(current)
484
+ return false unless @includes.fetch(current, []).empty?
485
+
486
+ parents = @superclasses.fetch(current, [])
487
+ next if parents.empty?
488
+
489
+ known = parents.find { |candidate| project_class?(candidate) }
490
+ return false if known.nil? && parents.none? { |candidate| EMPTY_CONSTRUCTOR_ROOTS.include?(candidate) }
491
+
492
+ queue << known if known
493
+ end
494
+ true
495
+ end
496
+
497
+ # Whether the project defines this class at all: it defines a method on it, declares its
498
+ # superclass, or declares what it includes. Any one of the three is a `class` body the scanner
499
+ # read, which is what licenses reading its silence about `initialize` as an answer.
500
+ def project_class?(class_name)
501
+ !class_name.nil? &&
502
+ (@classes.include?(class_name) || @superclasses.key?(class_name) || @includes.key?(class_name))
503
+ end
504
+
505
+ def call_targets(edge, memo_key, separator)
230
506
  targets = []
231
507
  owner = resolve_owner(edge.receiver_class, separator, edge.selector)
508
+ @owner_resolved[memo_key] = !owner.nil?
232
509
  targets << owner if owner
233
510
  descendant_closure(edge.receiver_class).each do |subclass|
234
511
  key = "#{subclass}#{separator}#{edge.selector}"
@@ -259,7 +536,7 @@ module Rigor
259
536
 
260
537
  # Ancestry order mirrors the engine's: the class itself, the modules it includes, then its
261
538
  # superclass, recursively. Cycle-guarded, because a project may declare one. Ancestry names
262
- # arrive as as-written candidate lists (see `Scanner#lexical_candidates`); every candidate is
539
+ # arrive as as-written candidate lists (see `AncestryRecorder#lexical_candidates`); every candidate is
263
540
  # enqueued and the most-qualified one comes first, so the right constant wins the race and a
264
541
  # spelling that names nothing simply matches no key.
265
542
  def resolve_owner(class_name, separator, selector)
@@ -272,8 +549,10 @@ module Rigor
272
549
  #
273
550
  # The includes are instance-side only. `include M` puts `M#m` between the class and its
274
551
  # superclass, which is exactly where `super` looks, while `M.m` is a singleton method `include`
275
- # never contributes; a singleton `super` that really does reach a module went through `extend` or
276
- # a `class << self` include, neither of which is collected, so it resolves to nothing and taints.
552
+ # never contributes. A singleton `super` that really does reach a module went through `extend` or
553
+ # a `class << self` include, neither of which is collected, so the walk steps over the module: it
554
+ # reaches the superclass chain's definition when there is one, without the module's labels, and
555
+ # resolves to nothing and taints when there is not.
277
556
  def resolve_super(class_name, separator, selector)
278
557
  queue = separator == "#" ? @includes.fetch(class_name, []).dup : []
279
558
  queue.concat(@superclasses.fetch(class_name, []))
@@ -28,7 +28,7 @@ module Rigor
28
28
  class InvalidLabelError < Error
29
29
  end
30
30
 
31
- DATA_PATH = File.expand_path("../../../data/effects/registry.yml", __dir__)
31
+ DATA_PATH = File.expand_path("../../../data/effects/registry.yml", __dir__).freeze
32
32
 
33
33
  # How far a misspelling may be from a known label before {#suggest} declines to guess. Two
34
34
  # edits catches a transposition or a dropped segment character ("io.nte", "nondet.tim")