rigortype 0.3.8 → 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 (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -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
 
@@ -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,15 +81,18 @@ 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
79
91
 
80
- # @param plugin_registry [Rigor::Plugin::Registry, nil]
81
- # @param superclasses [Hash{String=>String,Array<String>}] the project's as-written superclass table
92
+ # @param superclasses — the project's as-written superclass table
82
93
  # (`Scope::DiscoveryIndex#discovered_superclasses`). Empty is legal and simply means no row matches
83
94
  # through inheritance.
84
- # @param includes [Hash{String=>Array<String>}] the project's as-written include / prepend table
95
+ # @param includes — the project's as-written include / prepend table
85
96
  # (`Scope::DiscoveryIndex#discovered_includes`), for the frameworks whose contract is a module
86
97
  # rather than a base class (#456).
87
98
  def self.build(plugin_registry, superclasses: NO_ROWS, includes: NO_ROWS)
@@ -111,6 +122,7 @@ module Rigor
111
122
  @self_rows = {}
112
123
  @result_rows = {}
113
124
  @edges = []
125
+ @unit_callee_rows = []
114
126
  @labels_by_owner = {}
115
127
  @entry_points = []
116
128
  @declared_ancestry = {}
@@ -144,13 +156,10 @@ module Rigor
144
156
 
145
157
  # The row colouring `owner`'s `selector`, found on `owner` itself or on a project ancestor of it.
146
158
  #
147
- # @param owner [String, nil] the receiver's class name as the syntax or the typer named it
148
- # @param singleton [Boolean] whether the call is `Owner.selector`
149
- # @return [Row, nil]
159
+ # @param owner — the receiver's class name as the syntax or the typer named it
160
+ # @param singleton — whether the call is `Owner.selector`
150
161
  def class_row(owner, singleton, selector)
151
- return nil if owner.nil? || @class_rows.empty?
152
-
153
- bucket = @class_rows[singleton]
162
+ bucket = owner.nil? ? nil : @class_rows[singleton]
154
163
  return nil if bucket.nil?
155
164
 
156
165
  ancestry(owner).each do |candidate|
@@ -163,9 +172,7 @@ module Rigor
163
172
  # The row colouring `path`'s `selector`, where `path` is a receiver expression (`"Rails.cache"`).
164
173
  # Exact — a receiver path names one object and has no ancestry to walk.
165
174
  def path_row(path, selector)
166
- return nil if path.nil? || @path_rows.empty?
167
-
168
- @path_rows[path]&.[](selector)
175
+ path.nil? ? nil : @path_rows[path]&.[](selector)
169
176
  end
170
177
 
171
178
  # The row colouring `path`'s `selector` for a receiver rooted at implicit self (`"self.flash.now"`),
@@ -173,11 +180,8 @@ module Rigor
173
180
  # `owner_class`'s project ancestry — a receiver-less `session` outside a controller is a different
174
181
  # `session`.
175
182
  def self_path_row(path, selector, owner_class)
176
- return nil if path.nil? || @self_rows.empty?
177
-
178
- row = @self_rows[path]&.[](selector)
179
- return nil if row.nil?
180
- 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)
181
185
 
182
186
  row
183
187
  end
@@ -203,9 +207,7 @@ module Rigor
203
207
 
204
208
  # Whether `class_name` is `ancestor`, or reaches it through the project's own `class … <` lines.
205
209
  def descends_from?(class_name, ancestor)
206
- return false if class_name.nil?
207
-
208
- ancestry(class_name).include?(ancestor)
210
+ !class_name.nil? && ancestry(class_name).include?(ancestor)
209
211
  end
210
212
 
211
213
  # `registry` extended with every plugin's `effect_labels:`, each under its own owner so
@@ -227,10 +229,10 @@ module Rigor
227
229
  note_root_demotion(contribution)
228
230
  (@labels_by_owner[contribution.owner] ||= []).concat(contribution.labels)
229
231
  contribution.attributions.each { |entry| absorb_attribution(contribution, entry) }
230
- contribution.edges.each do |entry|
231
- @edges << Edge.new(target: entry.target, receiver: entry.receiver, selector: entry.method,
232
- plugin_id: contribution.id)
233
- 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)
234
236
  @entry_points.concat(contribution.entry_points)
235
237
  contribution.ancestry.each { |entry| absorb_ancestry(contribution, entry) }
236
238
  end
@@ -269,8 +271,22 @@ module Rigor
269
271
  def absorb_attribution(contribution, entry)
270
272
  row = Row.new(key: entry.key, labels: LabelSet.new(entry.labels), narrow: entry.narrow,
271
273
  discharge: discharge_granted?(contribution, entry), within: entry.within,
272
- 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)
273
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
274
290
  end
275
291
 
276
292
  # Which index a row lands in, from its receiver spelling.
@@ -353,22 +369,16 @@ module Rigor
353
369
  bucket.sort.map do |receiver, rows|
354
370
  [receiver,
355
371
  rows.sort.map do |selector, row|
356
- [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]
357
374
  end]
358
375
  end
359
376
  end
360
377
 
361
378
  def finalize
362
- @class_rows.each_value { |bucket| bucket.each_value(&:freeze) }
363
- @class_rows.each_value(&:freeze)
364
- @class_rows.freeze
365
- @path_rows.each_value(&:freeze)
366
- @path_rows.freeze
367
- @self_rows.each_value(&:freeze)
368
- @self_rows.freeze
369
- @result_rows.each_value(&:freeze)
370
- @result_rows.freeze
371
- @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)
372
382
  @labels_by_owner.each_value(&:uniq!)
373
383
  @labels_by_owner.reject! { |_, labels| labels.empty? }
374
384
  @labels_by_owner.freeze
@@ -45,16 +45,18 @@ module Rigor
45
45
 
46
46
  module_function
47
47
 
48
- # @param collection [FileCollection] the run's merged per-file collections
49
- # @param discharge [Discharge] the `effects.tolerated:` policy the undischarged lane is computed
48
+ # @param collection — the run's merged per-file collections
49
+ # @param discharge — the `effects.tolerated:` policy the undischarged lane is computed
50
50
  # under; {Discharge.none} makes the two lanes equal.
51
- # @return [EffectTable]
52
- 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)
53
55
  return EffectTable.empty if collection.summaries.empty?
54
56
 
55
57
  summaries = collection.summaries
56
58
  state = seed(summaries, discharge)
57
- edges = resolve_edges(collection, state)
59
+ edges = resolve_edges(collection, state, declined_unit_keys)
58
60
  iterate(state, edges)
59
61
  EffectTable.new(build_entries(summaries, edges, state))
60
62
  rescue StandardError
@@ -63,12 +65,14 @@ module Rigor
63
65
 
64
66
  # `{caller_key => [callee_key]}`, sorted and de-duplicated. Seeds the `unresolved-super` taint in
65
67
  # the same pass, because whether a `super` resolved is exactly what this resolution answers.
66
- def resolve_edges(collection, state)
67
- 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)
68
70
  collection.edges.each_with_object({}) do |(caller_key, list), out|
69
71
  targets = list.flat_map do |edge|
70
- resolved = index.targets_for(edge)
72
+ resolved = index.resolve(edge)
71
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)
72
76
  resolved
73
77
  end.uniq.sort
74
78
  out[caller_key] = targets.freeze unless targets.empty?
@@ -96,6 +100,47 @@ module Rigor
96
100
  entry[:causes] << ["unresolved-super", edge.selector].freeze
97
101
  end
98
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
+
99
144
  # Causes are carried as a Set through the fixpoint and flattened back to a sorted Array in
100
145
  # {build_entries}. A Set is what {absorb} needs: unioning one along an edge must cost the source's
101
146
  # size and allocate NOTHING when it adds nothing, and the array-concat-and-uniq it replaces
@@ -106,7 +151,8 @@ module Rigor
106
151
  proven: summary.proven,
107
152
  undischarged: discharge.inert? ? summary.proven : discharge.undischarged(summary.bundles),
108
153
  declared: summary.declared,
109
- exhaustive: summary.exhaustive?, causes: Set.new(summary.causes)
154
+ exhaustive: summary.exhaustive?, causes: Set.new(summary.causes),
155
+ unclaimed: summary.unclaimed?
110
156
  }
111
157
  end
112
158
  end
@@ -165,6 +211,10 @@ module Rigor
165
211
  target[:exhaustive] = false
166
212
  changed = true
167
213
  end
214
+ if !target[:unclaimed] && source[:unclaimed]
215
+ target[:unclaimed] = true
216
+ changed = true
217
+ end
168
218
  causes = target[:causes]
169
219
  source[:causes].each { |cause| changed = true if causes.add?(cause) }
170
220
  changed
@@ -191,25 +241,31 @@ module Rigor
191
241
  declared: closed[:declared],
192
242
  exhaustive: closed[:exhaustive],
193
243
  causes: closed[:causes].sort_by { |cause, detail| [cause, detail.to_s] }.freeze,
194
- edges: edges.fetch(key, NO_EDGES)
244
+ edges: edges.fetch(key, NO_EDGES),
245
+ unclaimed: closed[:unclaimed]
195
246
  )
196
247
  end
197
248
  end
198
249
 
199
- private_class_method :resolve_edges, :taint_unresolved_super, :seed, :iterate, :reverse_edges,
200
- :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
201
253
 
202
254
  # The class graph a run's collections describe, and the edge resolution over it. Built once per
203
255
  # propagation; every lookup is a Hash read.
204
256
  class Index
205
- def initialize(collection)
257
+ def initialize(collection, declined_unit_keys: nil)
206
258
  @summaries = collection.summaries
259
+ @declined = declined_unit_keys.nil? || declined_unit_keys.empty? ? nil : declined_unit_keys.to_a.to_set
207
260
  @superclasses = collection.superclasses
208
261
  @includes = collection.includes
209
262
  @classes = build_classes(collection)
210
263
  @descendants = build_descendants(collection.superclasses)
211
264
  @descendant_closures = {}
212
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 = {}
213
269
  end
214
270
 
215
271
  # Every project method key `edge` may reach: the definition its ancestry resolves to, plus every
@@ -219,17 +275,237 @@ module Rigor
219
275
  # one such tuple is asked for once per call site in the project. `ApplicationRecord#save` alone is
220
276
  # thousands of sites on a Rails app, each of which used to re-walk the whole subclass forest.
221
277
  def targets_for(edge)
222
- @targets[[edge.receiver_class, edge.kind, edge.selector, edge.super_call]] ||= begin
278
+ @targets[memo_key(edge)] ||= begin
223
279
  separator = edge.kind == :singleton ? "." : "#"
224
- 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
225
286
  end
226
287
  end
227
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
+
228
321
  private
229
322
 
230
- 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)
231
506
  targets = []
232
507
  owner = resolve_owner(edge.receiver_class, separator, edge.selector)
508
+ @owner_resolved[memo_key] = !owner.nil?
233
509
  targets << owner if owner
234
510
  descendant_closure(edge.receiver_class).each do |subclass|
235
511
  key = "#{subclass}#{separator}#{edge.selector}"
@@ -260,7 +536,7 @@ module Rigor
260
536
 
261
537
  # Ancestry order mirrors the engine's: the class itself, the modules it includes, then its
262
538
  # superclass, recursively. Cycle-guarded, because a project may declare one. Ancestry names
263
- # 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
264
540
  # enqueued and the most-qualified one comes first, so the right constant wins the race and a
265
541
  # spelling that names nothing simply matches no key.
266
542
  def resolve_owner(class_name, separator, selector)
@@ -273,8 +549,10 @@ module Rigor
273
549
  #
274
550
  # The includes are instance-side only. `include M` puts `M#m` between the class and its
275
551
  # superclass, which is exactly where `super` looks, while `M.m` is a singleton method `include`
276
- # never contributes; a singleton `super` that really does reach a module went through `extend` or
277
- # 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.
278
556
  def resolve_super(class_name, separator, selector)
279
557
  queue = separator == "#" ? @includes.fetch(class_name, []).dup : []
280
558
  queue.concat(@superclasses.fetch(class_name, []))