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
@@ -0,0 +1,368 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Effects
7
+ # Callee resolution for plugin attribution rows (#1048; ADR-103 WD10 / WD13; design note § 11.2).
8
+ #
9
+ # A framework method can be an **edge** as well as a label. `render :show` inside `UsersController`
10
+ # runs `app/views/users/show.html.erb`, synchronously and in-process, and since #393 that template is
11
+ # an effect unit keyed `view:users/show.html` sitting in the very same summaries table. Nothing
12
+ # produced the edge, because the only plugin-facing edge surface ({Plugin::EffectEdge}) names a
13
+ # receiver *class* and mints units on a class body, and {Plugin::EffectAttribution} carried labels but
14
+ # no callee.
15
+ #
16
+ # This module is the missing half, and it is deliberately shaped exactly like {Narrowing}: the plugin
17
+ # writes a **rule name** on its row (`callee: "rails_render"`) and the engine owns the strategy. A
18
+ # block would have to run inside the per-file effect scan — the one place ADR-103 WD13 forbids
19
+ # anything that resolves, walks or types — and would not survive the fork-pool / Ractor boundary. A
20
+ # name is a String: declarative, Marshal-clean, and reviewable in the plugin's manifest.
21
+ #
22
+ # ## What a rule may read
23
+ #
24
+ # **The call's own argument literals, the unit's owner class, and the unit's own key.** No dataflow,
25
+ # no typer question, no filesystem. A rule that needed to know whether `app/views/users/show.html.erb`
26
+ # exists would make a plugin row's meaning a function of the view tree, which the scan does not have
27
+ # and must not read; instead an unresolvable answer is `nil` and a resolvable-looking one that no unit
28
+ # answers is **dropped by the propagator**, which restores the row's taint from
29
+ # {FileCollection::Edge#taint_if_unresolved}. So "the template is not in the table" and "the argument
30
+ # was computed" both keep the `template-not-analysed` taint, and only a render that reached a real
31
+ # unit clears it.
32
+ #
33
+ # ## Two kinds of rule
34
+ #
35
+ # - a **site rule** ({SITE_RULES}) answers for one call node — `render :show`, `render partial: "card"`;
36
+ # - a **unit rule** ({UNIT_RULES}) answers for a whole unit and reads no node at all. Rails' *implicit*
37
+ # render is the case that needs one: an action that falls off its end without rendering still renders
38
+ # `<controller>/<action>`, and the fact that produces the edge is the **absence** of a call. Only the
39
+ # unit scan can observe that, which is why the rule is applied there rather than in
40
+ # {FrameworkUnits} — a class body cannot see which of its methods responded.
41
+ #
42
+ # A unit rule contributes an **edge and nothing else**: no labels, no taint. That is what keeps it
43
+ # FP-safe on the private helper a controller also defines — `def load_user` gets an edge to
44
+ # `view:users/load_user.html`, no unit answers it, and the unit is exactly as it was.
45
+ module CalleeRule
46
+ # The callee an applied rule names, as the two halves {FileCollection::Edge} carries. The key the
47
+ # propagator reconstructs is `"#{receiver}.#{selector}"` — `view:users/show` + `html`.
48
+ #
49
+ # `fallbacks` is the ordered list of selectors the propagator retries when `selector` resolves to
50
+ # nothing (#1065), or nil for none. The rule only COPIES it off the plugin's table: whether
51
+ # `view:users/_row.js` exists is a question about the merged table, which the scan cannot ask.
52
+ Callee = Data.define(:receiver, :selector, :fallbacks) do
53
+ def initialize(fallbacks: nil, **) = super
54
+ end
55
+
56
+ # Must agree with {MethodKey::TEMPLATE_UNIT_PREFIX} and `Plugin::TemplateUnit::KEY_PREFIX`; pinned
57
+ # equal by spec.
58
+ TEMPLATE_PREFIX = "view:"
59
+
60
+ # The format a controller action renders absent a `formats:` override — Rails' own default, and the
61
+ # only one a `render :show` with no other evidence may be read as.
62
+ DEFAULT_FORMAT = "html"
63
+
64
+ # Rules that answer for one call node.
65
+ SITE_RULES = %w[rails_render rails_render_partial].freeze
66
+
67
+ # Rules that answer for a whole unit, from its owner and its own key. A UNIT rule may additionally
68
+ # be applied **per format arm** of a `respond_to` block (#1071): the arm is the unit's own body, and
69
+ # the arm's format is a literal the unit scan read, so it travels as data the same way the unit's key
70
+ # does.
71
+ UNIT_RULES = %w[rails_implicit_render].freeze
72
+
73
+ RULES = (SITE_RULES + UNIT_RULES).freeze
74
+
75
+ # `render json:` / `plain:` / `inline:` and friends render no template. They are listed so the rule
76
+ # can decline rather than read a `partial:` that is not there — declining keeps the row's taint,
77
+ # which is the conservative answer for a shape this rule does not model.
78
+ NON_TEMPLATE_OPTIONS = %w[json xml plain text html body js inline file nothing].freeze
79
+
80
+ # Template handlers, which a logical name never carries: `Plugin::TemplateUnit#logical_name` is
81
+ # `users/show.html`, not `users/show.html.erb`. An author may still write the handler out —
82
+ # `render template: "users/show.html.erb"` is legal Rails — so it is stripped rather than left to
83
+ # build a key no unit could ever answer.
84
+ HANDLERS = %w[erb haml slim jbuilder builder rabl ruby].freeze
85
+
86
+ module_function
87
+
88
+ def known?(name)
89
+ RULES.include?(name.to_s)
90
+ end
91
+
92
+ def site_rule?(name)
93
+ SITE_RULES.include?(name.to_s)
94
+ end
95
+
96
+ def unit_rule?(name)
97
+ UNIT_RULES.include?(name.to_s)
98
+ end
99
+
100
+ # Applies a site rule to one call node.
101
+ #
102
+ # @param name — the row's `callee:`
103
+ # @param node — the `Prism::CallNode` the row matched
104
+ # @param owner_class — the unit's owner (`"UsersController"`, `"ActionView::Base"`)
105
+ # @param unit_key — the unit's own key: a selector for a method, `view:users/show.html` for a
106
+ # template unit
107
+ # @param fallbacks — the row's `callee_fallbacks:` table (#1065). Only a rule whose selector the
108
+ # CONTEXT supplied consults it; see {rails_render_partial}.
109
+ # @return the {Callee} the rule named, or nil whenever it cannot settle the target from
110
+ # literals alone
111
+ def site(name, node, owner_class:, unit_key: nil, fallbacks: nil)
112
+ case name.to_s
113
+ when "rails_render" then rails_render(node, owner_class)
114
+ when "rails_render_partial" then rails_render_partial(node, unit_key, fallbacks)
115
+ end
116
+ end
117
+
118
+ # Applies a unit rule. Reads no node; a `format:` from a `respond_to` arm (#1071) narrows the
119
+ # template the rule names from the default to that arm's own. Nil, or absent (`DEFAULT_FORMAT`).
120
+ #
121
+ # @return the {Callee} the rule named, or nil.
122
+ def unit(name, owner_class:, unit_key: nil, format: nil)
123
+ case name.to_s
124
+ when "rails_implicit_render" then rails_implicit_render(owner_class, unit_key, format: format)
125
+ end
126
+ end
127
+
128
+ # `render` inside a controller. The positional form names an **action template**
129
+ # (`render :show` → `users/show`), which is the one place a controller and a view disagree about
130
+ # what a bare string means.
131
+ #
132
+ # Never consults a format fallback (#1065). The format here is either one the author wrote, or
133
+ # Rails' `html` default standing in for a REQUEST format the rule cannot see — and the lookup order
134
+ # a controller-side render follows is derived from that request (`request.formats`, an `Accept`
135
+ # header), not from anything in the source.
136
+ def rails_render(node, owner_class)
137
+ directory = controller_directory(owner_class)
138
+ return nil if directory.nil?
139
+
140
+ format = format_for(node, DEFAULT_FORMAT)
141
+ return nil if format.nil?
142
+
143
+ name = template_name(node, directory) || partial_name(node, directory, layout: false)
144
+ name.nil? ? nil : template_callee(name, format)
145
+ end
146
+
147
+ # `render` inside a template. A bare positional argument is a **partial** here, and so is `layout:`
148
+ # — in a view `render layout: "shared/wrapper"` names `shared/_wrapper`, not an `app/views/layouts`
149
+ # file, because `RenderingHelper#render` rewrites `layout:` to `partial:` when a block is given.
150
+ # Since #1047 a layout compiles to a unit like any other template, so such an edge resolves where
151
+ # the named partial exists and keeps its taint where it does not.
152
+ #
153
+ # **The format fallback (#1065).** The format travels from the enclosing unit, and while a `.js.erb`
154
+ # template is rendering Rails' lookup context holds `[:js, :html]` — `LookupContext#formats=` appends
155
+ # `:html` to a lone `:js`, and `AbstractRenderer#prepend_formats` puts the template's own format in
156
+ # front of the request's. So `render partial: "watchers"` from `_set_watcher.js.erb` runs
157
+ # `_watchers.js.erb` where one exists and `_watchers.html.erb` otherwise. The rule copies the row's
158
+ # table for the inherited format onto the callee, and the propagator takes the first key that
159
+ # resolves.
160
+ #
161
+ # The table is consulted only for an INHERITED format. A `formats:` / `format:` keyword or a format
162
+ # spelled into the name is the author's word, and the fallback stands down: that is the direction
163
+ # that keeps a taint rather than guessing at a lookup the call overrode.
164
+ def rails_render_partial(node, unit_key, fallbacks)
165
+ directory, inherited = template_context(unit_key)
166
+ return nil if directory.nil?
167
+
168
+ format = format_for(node, inherited)
169
+ return nil if format.nil?
170
+
171
+ retry_formats = format_keyword?(node) ? nil : fallbacks&.fetch(format, nil)
172
+ explicit = keyword_name(node, "template")
173
+ return template_callee(qualify(explicit, directory), format, retry_formats) if explicit
174
+
175
+ name = partial_name(node, directory) || positional_partial(node, directory)
176
+ name.nil? ? nil : template_callee(name, format, retry_formats)
177
+ end
178
+
179
+ # Rails' implicit render: an action that never rendered still renders `<controller>/<action>`. A
180
+ # `respond_to` arm (#1071) is the same convention per format: `format.js` with no responding block
181
+ # renders `<controller>/<action>.js`, so the rule narrows the format it answers for, and an arm that
182
+ # IS answered outright is not applied at all (the unit scan decides that half and calls with the
183
+ # arm's format only for arms that still fall through).
184
+ def rails_implicit_render(owner_class, unit_key, format: nil)
185
+ directory = controller_directory(owner_class)
186
+ return nil if directory.nil? || unit_key.nil?
187
+
188
+ action = unit_key.to_s
189
+ return nil unless /\A[a-z_][A-Za-z0-9_]*[?!=]?\z/.match?(action)
190
+ return nil if action.end_with?("?", "!", "=")
191
+
192
+ template_callee("#{directory}/#{action}", format || DEFAULT_FORMAT)
193
+ end
194
+
195
+ # `UsersController` → `users`; `Admin::UsersController` → `admin/users`. A class whose name does not
196
+ # end in `Controller` is not one this rule can read a view directory off, and answers nil.
197
+ def controller_directory(owner_class)
198
+ return nil if owner_class.nil?
199
+
200
+ segments = owner_class.to_s.split("::")
201
+ last = segments.pop
202
+ return nil unless last&.end_with?("Controller") && last != "Controller"
203
+
204
+ segments.push(last.delete_suffix("Controller"))
205
+ segments.map { |segment| underscore(segment) }.join("/")
206
+ end
207
+
208
+ # `[directory, format]` for a template unit's own key — `view:users/show.html` → `["users", "html"]`.
209
+ # A partial rendered from a `.json` template is a `.json` partial, which is why the format travels.
210
+ def template_context(unit_key)
211
+ key = unit_key.to_s
212
+ return [nil, nil] unless key.start_with?(TEMPLATE_PREFIX)
213
+
214
+ logical = key.delete_prefix(TEMPLATE_PREFIX)
215
+ name, _, format = logical.rpartition(".")
216
+ return [nil, nil] if name.empty?
217
+
218
+ directory = name.include?("/") ? name[0...name.rindex("/")] : ""
219
+ [directory, format]
220
+ end
221
+
222
+ # The explicit template spellings: `template: "users/show"` and `action: :edit`.
223
+ def template_name(node, directory)
224
+ explicit = keyword_name(node, "template")
225
+ return qualify(explicit, directory) if explicit
226
+
227
+ action = keyword_name(node, "action")
228
+ return "#{directory}/#{action}" if action && !action.include?("/")
229
+ return action if action
230
+
231
+ positional_template(node, directory)
232
+ end
233
+
234
+ # `render :show` / `render "show"` / `render "admin/form"`, but only when no option keyword the rule
235
+ # does not model is present.
236
+ def positional_template(node, directory)
237
+ name = literal_name(positional(node).first)
238
+ return nil if name.nil?
239
+
240
+ qualify(name, directory)
241
+ end
242
+
243
+ # `render "card"` inside a template — the same literal, read as a partial.
244
+ def positional_partial(node, directory)
245
+ name = literal_name(positional(node).first)
246
+ name.nil? ? nil : partialize(qualify(name, directory))
247
+ end
248
+
249
+ # `partial: "card"` / `partial: "users/card"`, and the view-side `layout:`. `collection:` changes
250
+ # how many times the partial runs and not which one, so it is read and ignored on purpose: an
251
+ # effect summary is an upper bound over the body, not a count.
252
+ def partial_name(node, directory, layout: true)
253
+ name = keyword_name(node, "partial") || (layout ? keyword_name(node, "layout") : nil)
254
+ name.nil? ? nil : partialize(qualify(name, directory))
255
+ end
256
+
257
+ # The requested format, or nil to decline. Absent is `fallback`; a literal narrows; anything
258
+ # computed is genuinely unknown and must not be guessed at `html`.
259
+ def format_for(node, fallback)
260
+ return nil if non_template?(node)
261
+
262
+ value = keyword_argument(node, "formats") || keyword_argument(node, "format")
263
+ return fallback if value.nil?
264
+
265
+ literal = literal_name(value) || literal_name(array_head(value))
266
+ literal&.split(".")&.last
267
+ end
268
+
269
+ def format_keyword?(node)
270
+ !(keyword_argument(node, "formats") || keyword_argument(node, "format")).nil?
271
+ end
272
+
273
+ def non_template?(node)
274
+ NON_TEMPLATE_OPTIONS.any? { |option| keyword_argument(node, option) }
275
+ end
276
+
277
+ def array_head(node)
278
+ node.elements.first if node.is_a?(Prism::ArrayNode)
279
+ end
280
+
281
+ # `fallbacks` survives only when the name spelled no format of its own: `render "row.json"` from a
282
+ # `.js` template is the author naming `json`, and a `js` table has nothing to say about it.
283
+ def template_callee(name, format, fallbacks = nil)
284
+ spelled = split_suffixes(name, nil).last
285
+ name, format = split_suffixes(name, format)
286
+ return nil if name.nil? || name.empty? || format.nil? || format.empty?
287
+ return nil if name.include?(" ") || format.include?(" ") || format.include?(".")
288
+
289
+ fallbacks = nil unless spelled.nil? && fallbacks && !fallbacks.empty?
290
+ Callee.new(receiver: "#{TEMPLATE_PREFIX}#{name}", selector: format, fallbacks: fallbacks)
291
+ end
292
+
293
+ # Splits a written handler and format off the name's last segment, so `render "show.json"` names
294
+ # `view:users/show` + `json` and `render template: "users/show.html.erb"` names
295
+ # `view:users/show` + `html` rather than the impossible `view:users/show.html.erb.html`. A name
296
+ # that spells a format wins over the rule's default, because the author said so; a name that
297
+ # spells only a handler keeps the default.
298
+ def split_suffixes(name, format)
299
+ return [name, format] if name.nil?
300
+
301
+ directory, separator, base = name.rpartition("/")
302
+ segments = base.split(".")
303
+ return [name, format] if segments.length <= 1
304
+
305
+ stem = segments.shift
306
+ segments.reject! { |segment| HANDLERS.include?(segment) }
307
+ ["#{directory}#{separator}#{stem}", segments.first || format]
308
+ end
309
+
310
+ # `"card"` in `users` → `"users/_card"`; `"admin/card"` → `"admin/_card"`. A name already spelled
311
+ # with the underscore keeps it, because that is what Rails accepts too.
312
+ def partialize(name)
313
+ return nil if name.nil?
314
+
315
+ directory, separator, base = name.rpartition("/")
316
+ return base.start_with?("_") ? name : "_#{name}" if separator.empty?
317
+
318
+ base.start_with?("_") ? name : "#{directory}/_#{base}"
319
+ end
320
+
321
+ # A name with a `/` is rooted at the views directory; a bare one is relative to the rendering
322
+ # unit's own directory.
323
+ def qualify(name, directory)
324
+ return nil if name.nil? || name.empty?
325
+ return name if name.include?("/") || directory.nil? || directory.empty?
326
+
327
+ "#{directory}/#{name}"
328
+ end
329
+
330
+ def keyword_name(node, key)
331
+ literal_name(keyword_argument(node, key))
332
+ end
333
+
334
+ def literal_name(value)
335
+ case value
336
+ when Prism::SymbolNode, Prism::StringNode then value.unescaped
337
+ end
338
+ end
339
+
340
+ def positional(node)
341
+ node.arguments&.arguments&.grep_v(Prism::KeywordHashNode) || []
342
+ end
343
+
344
+ def keyword_argument(node, name)
345
+ hash = node.arguments&.arguments&.find { |argument| argument.is_a?(Prism::KeywordHashNode) }
346
+ pair = hash&.elements&.find do |element|
347
+ element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode) &&
348
+ element.key.unescaped == name
349
+ end
350
+ pair&.value
351
+ end
352
+
353
+ # `ActiveStorage` → `active_storage`. The engine's own reading of a constant segment; deliberately
354
+ # not an inflector call, because a rule may read nothing outside the strings it was given.
355
+ def underscore(segment)
356
+ segment.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
357
+ end
358
+
359
+ private_class_method :rails_render, :rails_render_partial, :rails_implicit_render,
360
+ :controller_directory, :template_context, :template_name,
361
+ :positional_template, :positional_partial, :partial_name,
362
+ :format_for, :format_keyword?, :non_template?, :array_head, :template_callee,
363
+ :split_suffixes, :partialize,
364
+ :qualify, :keyword_name, :literal_name, :positional, :keyword_argument,
365
+ :underscore
366
+ end
367
+ end
368
+ end
@@ -36,14 +36,17 @@ module Rigor
36
36
  class Error < StandardError
37
37
  end
38
38
 
39
- DATA_PATH = File.expand_path("../../../data/effects/core.yml", __dir__)
39
+ DATA_PATH = File.expand_path("../../../data/effects/core.yml", __dir__).freeze
40
40
 
41
41
  # The mutator sets a value class may name, by reference. Adding a name here is the only way a
42
- # class gets one — the data file may not spell a selector list of its own.
42
+ # class gets one — the data file may not spell a selector list of its own. Each is the set
43
+ # {MutationClassifier#mutating?} reads for that class, so a posture's answer and the classifier's
44
+ # cannot disagree: `hash` is the classifier's union of the widening's two Hash tables, because
45
+ # `compare_by_identity` mutates the receiver although the widening keeps it off `HASH_MUTATORS`.
43
46
  MUTATOR_SETS = {
44
47
  "array" => Inference::MutationWidening::ARRAY_MUTATORS,
45
- "hash" => Inference::MutationWidening::HASH_MUTATORS,
46
- "string" => MutationClassifier::STRING_MUTATORS
48
+ "hash" => MutationClassifier::HASH_MUTATORS,
49
+ "string" => Inference::StringMutation::MUTATORS
47
50
  }.freeze
48
51
 
49
52
  NO_MUTATORS = Set[].freeze
@@ -39,12 +39,16 @@ module Rigor
39
39
 
40
40
  # Per-file, per-thread accumulator. Not frozen and never shared: it lives for one `analyze_file`.
41
41
  class Accumulator
42
- attr_reader :path, :calls, :attribution, :envelopes, :plugin_facts
42
+ attr_reader :path, :calls, :attribution, :envelopes, :plugin_facts, :unit_key, :unit_owner
43
43
  attr_accessor :root
44
44
 
45
45
  def initialize(path, attribution: Attribution.empty, envelopes: EnvelopeIndex.empty,
46
- plugin_facts: PluginFacts.empty)
46
+ plugin_facts: PluginFacts.empty, unit_key: nil, unit_owner: nil)
47
47
  @path = path
48
+ # #392 — set for a template unit, where the whole file is ONE effect unit keyed
49
+ # `view:<logical_name>` under the plugin's declared `self`.
50
+ @unit_key = unit_key
51
+ @unit_owner = unit_owner
48
52
  @attribution = attribution
49
53
  @envelopes = envelopes
50
54
  @plugin_facts = plugin_facts
@@ -92,10 +96,11 @@ module Rigor
92
96
  # {EnvelopeIndex} of #386, carried the same way and for the same reason — the declared lane a call
93
97
  # site imports must not depend on which process typed the file.
94
98
  def collect_for(path, attribution: Attribution.empty, envelopes: EnvelopeIndex.empty,
95
- plugin_facts: PluginFacts.empty)
99
+ plugin_facts: PluginFacts.empty, unit_key: nil, unit_owner: nil)
96
100
  previous = Thread.current[KEY]
97
101
  accumulator = Accumulator.new(path.to_s, attribution: attribution, envelopes: envelopes,
98
- plugin_facts: plugin_facts)
102
+ plugin_facts: plugin_facts, unit_key: unit_key,
103
+ unit_owner: unit_owner)
99
104
  Thread.current[KEY] = accumulator
100
105
  @mutex.synchronize { @active_count += 1 }
101
106
  yield
@@ -207,7 +212,8 @@ module Rigor
207
212
  Scanner.scan(
208
213
  root: accumulator.root, path: accumulator.path, calls: accumulator.calls,
209
214
  attribution: accumulator.attribution, envelopes: accumulator.envelopes,
210
- plugin_facts: accumulator.plugin_facts
215
+ plugin_facts: accumulator.plugin_facts, unit_key: accumulator.unit_key,
216
+ unit_owner: accumulator.unit_owner
211
217
  )
212
218
  rescue StandardError
213
219
  FileCollection.new(path: accumulator.path, failed: true)
@@ -69,9 +69,8 @@ module Rigor
69
69
 
70
70
  # Resolves `Configuration#effects_envelopes` against a registry.
71
71
  #
72
- # @param entries [Array<Hash>] the loaded, shape-validated entries
73
- # @param registry [Registry] the vocabulary, project extensions included
74
- # @return [Array<Entry>]
72
+ # @param entries — the loaded, shape-validated entries
73
+ # @param registry — the vocabulary, project extensions included
75
74
  def build(entries:, registry:)
76
75
  entries.each_with_index.map do |entry, index|
77
76
  labels = Array(entry["effect"]).map(&:to_s)
@@ -86,11 +85,10 @@ module Rigor
86
85
 
87
86
  # The class-level envelopes the entries put on a project.
88
87
  #
89
- # @param entries [Array<Entry>]
90
- # @param class_names [Enumerable<String>] every class the run collected units for
91
- # @param sources [Hash{String => Array<String>}] `Runner#effect_sources` — `{method key => [path]}`
92
- # @param project_root [String] what `sources` paths are relativised against
93
- # @return [Hash{String => Envelope}] one envelope per selected class, keyed by class name
88
+ # @param class_names — every class the run collected units for
89
+ # @param sources — `Runner#effect_sources` — `{method key => [path]}`
90
+ # @param project_root — what `sources` paths are relativised against
91
+ # @return one envelope per selected class, keyed by class name
94
92
  def for_classes(entries:, class_names:, sources: {}, project_root: Dir.pwd)
95
93
  return NO_ENVELOPES if entries.empty?
96
94
 
@@ -115,6 +113,13 @@ module Rigor
115
113
  )
116
114
  end
117
115
 
116
+ # Whether one entry selects a class — by its fully-qualified name for a `namespace:` entry, by any
117
+ # file that defines it for a `match:` one.
118
+ #
119
+ # Public since #391: `rigor sig-gen`'s emission asks the same question this module's own
120
+ # {.for_classes} asks, and asks it with a defining file it already has. It was private while
121
+ # {.for_classes} was the only caller; a second caller with the same inputs is not a reason to
122
+ # reimplement the predicate.
118
123
  def selects?(entry, class_name, paths)
119
124
  return namespace_match?(entry.namespace, class_name) if entry.namespace
120
125
 
@@ -166,7 +171,7 @@ module Rigor
166
171
  def files_by_class(sources, project_root)
167
172
  root = "#{File.absolute_path(project_root.to_s).chomp('/')}/"
168
173
  sources.each_with_object({}) do |(key, paths), out|
169
- owner = MethodKey.owner(key)
174
+ owner = MethodKey.envelope_owner(key)
170
175
  next if owner.nil?
171
176
 
172
177
  bucket = (out[owner] ||= [])
@@ -179,7 +184,7 @@ module Rigor
179
184
  absolute.start_with?(root) ? absolute[root.length..] : path.to_s
180
185
  end
181
186
 
182
- private_class_method :selects?, :match_segments?, :match_deep?, :files_by_class, :relativize
187
+ private_class_method :match_segments?, :match_deep?, :files_by_class, :relativize
183
188
  end
184
189
  end
185
190
  end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Effects
7
+ # Which side of its class a definition written at some point lands on, and what `self` its body runs
8
+ # on, or that the syntax does not say. The scanner keys a unit `Class.m` or `Class#m` from the first, and
9
+ # scans the body with the second (the singleton bit {MutationClassifier} and
10
+ # {LocalOwnership.constructor?} read).
11
+ #
12
+ # Ruby answers the two from different places, which is why one bit carried down the walk got both
13
+ # wrong in turn:
14
+ #
15
+ # - a receiver-less `def` lands on the **default definee**: the class the lexically enclosing body
16
+ # opened, which is the singleton class inside `class << self`. A method body does not move it, so
17
+ # `def inner` inside `def self.outer` defines `Foo#inner`, while inside a `def` in `class << self` it
18
+ # defines `Foo.inner`.
19
+ # - `define_method` and the `attr_*` macros are calls on **`self`**, so they define instance methods of
20
+ # the module `self` is: `Foo#x` inside `def self.setup`, where `self` is `Foo`, and `Foo.x` inside
21
+ # `class << self`, where it is `Foo`'s singleton class.
22
+ #
23
+ # Only the spellings that name the new `self` move either answer: `class << self`, and a block given to
24
+ # `class_eval` / `instance_eval` (or an `_exec` / `module_` alias) on `self` or on `singleton_class`.
25
+ # Where the syntax cannot say, the answer is **unknown** and a definition there is no unit. That covers
26
+ # the block of `Class.new` and its kin, an eval on any other receiver, `class << obj`, and a method
27
+ # defined on one object or on a singleton class's own singleton class. Filed under the enclosing class,
28
+ # such a definition would join that class's real method of the same name, and a `Class.new(self)`
29
+ # override shares its name by design.
30
+ #
31
+ # `self_kind` is `:instance`, `:class` (the class the enclosing namespace opened), `:singleton_class`
32
+ # (that class's singleton class) or `:unknown`, and `definee_singleton` is true, false, or nil when
33
+ # unknown. Every context is allocated once.
34
+ class DefinitionContext < Data.define(:self_kind, :definee_singleton)
35
+ CLASS_EVAL_CALLS = %i[class_eval class_exec module_eval module_exec].to_set.freeze
36
+ INSTANCE_EVAL_CALLS = %i[instance_eval instance_exec].to_set.freeze
37
+ # The builders whose block defines into a class or module the call itself creates.
38
+ ANONYMOUS_BUILDERS = { Class: :new, Module: :new, Struct: :new, Data: :define }.freeze
39
+ # The `self_kind`s that are a class object.
40
+ CLASS_SELVES = %i[class singleton_class].to_set.freeze
41
+ private_constant :CLASS_EVAL_CALLS, :INSTANCE_EVAL_CALLS, :ANONYMOUS_BUILDERS, :CLASS_SELVES
42
+
43
+ CONTEXTS = %i[instance class singleton_class unknown].to_h do |self_kind|
44
+ [self_kind, [false, true, nil].to_h do |definee|
45
+ [definee, new(self_kind: self_kind, definee_singleton: definee)]
46
+ end.freeze]
47
+ end.freeze
48
+ private_constant :CONTEXTS
49
+
50
+ def self.of(self_kind, definee_singleton)
51
+ CONTEXTS.fetch(self_kind).fetch(definee_singleton)
52
+ end
53
+
54
+ # Whether a child of `node` may run under another context than `node` does: a call's block, or a
55
+ # `class <<` body. Only these need {#for_child}, and asking once per node rather than once per child
56
+ # keeps the walks from paying a call for every element of a large literal.
57
+ def self.rebinds?(node)
58
+ node.is_a?(Prism::SingletonClassNode) || (node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode))
59
+ end
60
+
61
+ # A class body.
62
+ CLASS_BODY = of(:class, false)
63
+ # A `class << self` body: `self` and the definee are both the singleton class.
64
+ SINGLETON_CLASS_BODY = of(:singleton_class, true)
65
+ # The body of an instance method of the class.
66
+ INSTANCE_METHOD_BODY = of(:instance, false)
67
+ # The top level, where `self` is `main`, an instance of `Object`, and a `def` defines `Object#m`.
68
+ TOP_LEVEL = INSTANCE_METHOD_BODY
69
+ # Where the syntax says neither what `self` is nor where a `def` lands. Every block inside stays here.
70
+ UNKNOWN = of(:unknown, nil)
71
+
72
+ # Whether `self` is a class object here — the singleton bit a unit's body is scanned with.
73
+ def singleton?
74
+ CLASS_SELVES.include?(self_kind)
75
+ end
76
+
77
+ # Whether `self` is the singleton class here, so a call on it (`define_method`, an `attr_*` macro,
78
+ # `include`, `prepend`, `alias_method`) works on the singleton class rather than the class.
79
+ def self_singleton_class?
80
+ self_kind == :singleton_class
81
+ end
82
+
83
+ # The context the body of `def` node `node` runs under, or nil where no key names the method. The
84
+ # unit is keyed singleton exactly when that context is {#singleton?}. The body keeps this default
85
+ # definee: a `def` nested in it lands where one written beside it would.
86
+ #
87
+ # `def self.x` defines a singleton method only where `self` is the class. In an instance method it
88
+ # defines a method on one object, and in `class << self` one on the singleton class's own singleton
89
+ # class. `def Const.x` keeps the reading it has always had, a method of the class the unit is keyed
90
+ # under; any other receiver is an object no key names.
91
+ def def_body(node)
92
+ case node.receiver
93
+ when nil
94
+ DefinitionContext.of(definee_singleton ? :class : :instance, definee_singleton) unless definee_singleton.nil?
95
+ when Prism::SelfNode
96
+ self if self_kind == :class
97
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
98
+ DefinitionContext.of(:class, definee_singleton) unless self_kind == :unknown
99
+ end
100
+ end
101
+
102
+ # The context the block of a literal-name `define_method` written here runs under, or nil where no key
103
+ # names the method; an `attr_*` macro here defines on the same side. Both are calls on `self`, which
104
+ # only a class and its singleton class answer, and the block keeps the definee it closed over.
105
+ def module_call_body
106
+ case self_kind
107
+ when :class then DefinitionContext.of(:instance, definee_singleton)
108
+ when :singleton_class then DefinitionContext.of(:class, definee_singleton)
109
+ end
110
+ end
111
+
112
+ # The context `child` runs under as a child of `node`, a node {.rebinds?} accepts. A call's block and a
113
+ # `class <<` body may rebind `self`; every other child keeps this context. (A block is a call's only
114
+ # `BlockNode` child.)
115
+ def for_child(node, child)
116
+ case node
117
+ when Prism::CallNode then child.is_a?(Prism::BlockNode) ? block(node) : self
118
+ when Prism::SingletonClassNode then child.equal?(node.body) ? singleton_class_body(node.expression) : self
119
+ else self
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ # `class << self` on the class opens its singleton class; any other `class << …` opens one no key names.
126
+ def singleton_class_body(expression)
127
+ self_kind == :class && expression.is_a?(Prism::SelfNode) ? SINGLETON_CLASS_BODY : UNKNOWN
128
+ end
129
+
130
+ def block(call)
131
+ name = call.name
132
+ if CLASS_EVAL_CALLS.include?(name) then class_eval_block(call.receiver)
133
+ elsif INSTANCE_EVAL_CALLS.include?(name) then instance_eval_block(call.receiver)
134
+ elsif anonymous_builder?(call) then UNKNOWN
135
+ else self
136
+ end
137
+ end
138
+
139
+ # `class_eval` makes its receiver both `self` and the definee.
140
+ def class_eval_block(receiver)
141
+ if self_receiver?(receiver) && singleton? then DefinitionContext.of(self_kind, self_kind == :singleton_class)
142
+ elsif singleton_class_receiver?(receiver) then SINGLETON_CLASS_BODY
143
+ else UNKNOWN
144
+ end
145
+ end
146
+
147
+ # `instance_eval` makes its receiver `self` and the receiver's singleton class the definee. On the
148
+ # class that is the singleton class. On the singleton class it is that class's own singleton class,
149
+ # which no key names, so a `define_method` there is keyed and a `def` is not.
150
+ def instance_eval_block(receiver)
151
+ if self_receiver?(receiver) && self_kind == :class then DefinitionContext.of(:class, true)
152
+ elsif (self_receiver?(receiver) && self_kind == :singleton_class) || singleton_class_receiver?(receiver)
153
+ DefinitionContext.of(:singleton_class, nil)
154
+ else UNKNOWN
155
+ end
156
+ end
157
+
158
+ def self_receiver?(receiver)
159
+ receiver.nil? || receiver.is_a?(Prism::SelfNode)
160
+ end
161
+
162
+ # `singleton_class` called on the class.
163
+ def singleton_class_receiver?(receiver)
164
+ self_kind == :class && receiver.is_a?(Prism::CallNode) && receiver.name == :singleton_class &&
165
+ self_receiver?(receiver.receiver) && receiver.arguments.nil? && receiver.block.nil?
166
+ end
167
+
168
+ # `Class.new { … }` and its kin, spelled on the core constant.
169
+ def anonymous_builder?(call)
170
+ receiver = call.receiver
171
+ name = case receiver
172
+ when Prism::ConstantReadNode then receiver.name
173
+ when Prism::ConstantPathNode then receiver.name if receiver.parent.nil?
174
+ end
175
+ !name.nil? && ANONYMOUS_BUILDERS[name] == call.name
176
+ end
177
+ end
178
+ end
179
+ end