rigortype 0.3.9 → 0.4.1

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 (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module ScopeIndexer
8
+ # ADR-119 WD3 — which of a file's method contributions are `possible`: the classifier the def-contribution
9
+ # producers ({ScopeIndexer#build_methods_and_def_nodes}, {ScopeIndexer#build_discovered_singleton_def_nodes})
10
+ # consult to fill `possible_discovered_methods` and the `contested_*` siblings.
11
+ #
12
+ # A contribution is `certain` when its statement executes whenever the file's top level executes. The certain
13
+ # regions are:
14
+ #
15
+ # - the program's statements, and the bodies of `class`, `module` and `class << self` declarations that are
16
+ # themselves certain;
17
+ # - the block of a meta-new constant write (`K = Class.new do … end`, `Module.new`, `Struct.new`,
18
+ # `Data.define`, {ScopeIndexer#meta_new_block_call}) and of a bare factory call
19
+ # ({AnonymousMetaClass.block_form_receiver}), when the write or the call is certain: Ruby runs the block once,
20
+ # as the new class's body (ADR-119 WD2, "Conditional definers");
21
+ # - a `begin` WITHOUT a `rescue` clause: its main statements and its `ensure` clause;
22
+ # - the receiver and arguments of a certain call, so `private def x` and `memoize def x` stay certain.
23
+ #
24
+ # Everything else is `possible`: the branches of `if` / `unless` / `case` and the modifiers, loop bodies, the
25
+ # right operand of `&&` / `||`, every other block and every lambda, `BEGIN` / `END`, a `class X … end if Y`
26
+ # reopening, and the WHOLE of a `begin` that has a `rescue` clause, its main statements included. That last
27
+ # rule refines WD3's wording: a rescued raise skips every statement after the raising one, so the main body of
28
+ # a rescuing `begin` is not certain either, while a `begin` with no rescue either runs its main statements
29
+ # and `ensure` or propagates the raise out of the file's top level. A `def` inside a method body is never
30
+ # recorded by the walks (they return at a `DefNode`), so the classifier does not descend there either.
31
+ #
32
+ # The answer is an identity Set of the POSSIBLE contribution nodes — the nodes a producer records from — and
33
+ # {EMPTY} when the file has none, so a file written entirely in certain regions allocates nothing. It is
34
+ # separate from {MixinAccumulator}, which answers a different question (whether an edge's POSITION is a fact).
35
+ module Certainty
36
+ # The answer for a file with no possible contribution.
37
+ EMPTY = Set.new.compare_by_identity.freeze
38
+
39
+ # The nodes a def-contribution producer records from: a `def`, an `alias` / `undef`, a call (`attr_*`,
40
+ # `define_method`, `alias_method`, `module_function :x`, a factory's members), and the declarations whose
41
+ # header records members (`class X < Struct.new(:a)`, `X = Struct.new(:a) do … end`).
42
+ CONTRIBUTIONS = Set[
43
+ Prism::DefNode, Prism::AliasMethodNode, Prism::UndefNode, Prism::CallNode, Prism::ClassNode,
44
+ Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
45
+ Prism::ConstantPathOrWriteNode
46
+ ].freeze
47
+
48
+ CONSTANT_WRITES = Set[
49
+ Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
50
+ Prism::ConstantPathOrWriteNode
51
+ ].freeze
52
+
53
+ # The nodes whose whole subtree is possible.
54
+ POSSIBLE_ROOTS = Set[
55
+ Prism::BlockNode, Prism::LambdaNode, Prism::PreExecutionNode, Prism::PostExecutionNode,
56
+ Prism::RescueModifierNode
57
+ ].freeze
58
+ private_constant :CONSTANT_WRITES, :POSSIBLE_ROOTS
59
+
60
+ module_function
61
+
62
+ # The possible contribution nodes of the tree under `root`, an identity Set ({EMPTY} when none).
63
+ def possible_nodes(root)
64
+ classifier = Classifier.new
65
+ classifier.certain(root)
66
+ classifier.result
67
+ end
68
+
69
+ # Whether `node` was classified possible in `possible` (a {possible_nodes} answer).
70
+ def possible?(possible, node)
71
+ !possible.empty? && possible.include?(node)
72
+ end
73
+
74
+ # One classification walk. `certain` descends a certain region; `possible` marks every contribution in a
75
+ # subtree, since nothing below a possible node is certain.
76
+ class Classifier
77
+ def initialize
78
+ @possible = nil
79
+ end
80
+
81
+ def result
82
+ @possible ? @possible.freeze : EMPTY
83
+ end
84
+
85
+ def certain(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
86
+ return unless node.is_a?(Prism::Node)
87
+ return possible(node) if POSSIBLE_ROOTS.include?(node.class)
88
+
89
+ case node
90
+ when Prism::DefNode then return
91
+ when Prism::IfNode then return branch(node.predicate, node.statements, node.subsequent)
92
+ when Prism::UnlessNode then return branch(node.predicate, node.statements, node.else_clause)
93
+ when Prism::CaseNode, Prism::CaseMatchNode then return case_branches(node)
94
+ when Prism::WhileNode, Prism::UntilNode then return branch(node.predicate, node.statements)
95
+ when Prism::ForNode then return branch(node.collection, node.index, node.statements)
96
+ when Prism::AndNode, Prism::OrNode then return branch(node.left, node.right)
97
+ when Prism::BeginNode then return begin_block(node)
98
+ when Prism::CallNode then return call(node)
99
+ end
100
+ return if CONSTANT_WRITES.include?(node.class) && meta_new_write?(node)
101
+
102
+ node.rigor_each_child { |child| certain(child) }
103
+ end
104
+
105
+ # Marks every contribution of the subtree.
106
+ def possible(node)
107
+ return unless node.is_a?(Prism::Node)
108
+
109
+ (@possible ||= Set.new.compare_by_identity) << node if CONTRIBUTIONS.include?(node.class)
110
+ return if node.is_a?(Prism::DefNode)
111
+
112
+ node.rigor_each_child { |child| possible(child) }
113
+ end
114
+
115
+ private
116
+
117
+ # The first node runs whenever the construct does; the rest may not.
118
+ def branch(first, *rest)
119
+ certain(first)
120
+ rest.each { |node| possible(node) }
121
+ end
122
+
123
+ def case_branches(node)
124
+ certain(node.predicate)
125
+ node.conditions.each { |clause| possible(clause) }
126
+ possible(node.else_clause)
127
+ end
128
+
129
+ # Without a rescue the main statements run (or the raise leaves the file) and `ensure` runs; with one, a
130
+ # rescued raise skips every statement after the raising one, so all of it is possible.
131
+ def begin_block(node)
132
+ return possible_children(node) if node.rescue_clause
133
+
134
+ certain(node.statements)
135
+ possible(node.else_clause)
136
+ certain(node.ensure_clause)
137
+ end
138
+
139
+ def possible_children(node)
140
+ node.rigor_each_child { |child| possible(child) }
141
+ end
142
+
143
+ # The receiver and arguments of a certain call run; its block runs as a class body only for a bare
144
+ # factory call. A safe-navigation call may skip its arguments.
145
+ def call(node)
146
+ certain(node.receiver)
147
+ if node.safe_navigation?
148
+ possible(node.arguments)
149
+ possible(node.block)
150
+ return
151
+ end
152
+ certain(node.arguments)
153
+ block = node.block
154
+ if block.is_a?(Prism::BlockNode) && AnonymousMetaClass.block_form_receiver(node)
155
+ certain(block.body)
156
+ else
157
+ certain(block)
158
+ end
159
+ end
160
+
161
+ # A meta-new constant write: the factory's receiver and arguments, and its block as the class body.
162
+ def meta_new_write?(node)
163
+ call = ScopeIndexer.meta_new_block_call(node)
164
+ return false unless call
165
+
166
+ certain(call.receiver)
167
+ certain(call.arguments)
168
+ block = call.block
169
+ certain(block.is_a?(Prism::BlockNode) ? block.body : block)
170
+ true
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../declaration_walk/traversal"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module ScopeIndexer
8
+ # The `class_cvars` discovery table (slice 7 phase 6) as a {DeclarationWalk} collector: every
9
+ # `@@x = …` a `def` body writes, typed under the census scope and unioned per class. Ruby cvars are
10
+ # shared by the instance and singleton facets, so every `def` counts, `def self.x` included.
11
+ class ClassCvarsCollector
12
+ include DeclarationWalk::Collector
13
+
14
+ def initialize
15
+ @table = {}
16
+ end
17
+
18
+ # `@@x` in a `def` resolves through the LEXICAL cref: `Module.nesting` is the same inside a meta-new or
19
+ # eval block, so the context's `prefix` keys the write and its rebound `self` never does —
20
+ # `K = Class.new { def m = @@x }` inside `class C` writes `C::@@x`. The walk goes no further in: the
21
+ # gather reads the body with `def`, `class` and `module` as barriers, and nothing below a `def` reaches
22
+ # this table.
23
+ def on_def(node, context)
24
+ ScopeIndexer.collect_def_cvar_writes(node, context.prefix, context.scope, @table)
25
+ DeclarationWalk::DECLINE
26
+ end
27
+
28
+ # The table as the discovery index holds it: each class's entry frozen, then the whole.
29
+ def table
30
+ @table.transform_values(&:freeze).freeze
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../declaration_walk/traversal"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module ScopeIndexer
8
+ # Issue #681's `{Prism::DefNode => Module.nesting}` table as a {DeclarationWalk} collector: the chain every
9
+ # `def` is declared under, keyed by node identity. A top-level `def` records the empty chain (#716); a `def`
10
+ # below a header that renders no name records nothing, because no chain survives it.
11
+ class DefNestingsCollector
12
+ include DeclarationWalk::Collector
13
+
14
+ # The rules `walk_def_nestings` answered differently from the walk, kept so the table stays
15
+ # byte-identical (ADR-116 WD5). Each is a legacy answer, and #1521 tracks converging it:
16
+ #
17
+ # - `factory_block: :ordinary_call` — no bare-factory arm, so a `self::` header in a `Class.new { … }`
18
+ # block anchors on the enclosing `self` and the block's parameters are walked (#1521 item 8).
19
+ # - `lexical_prefix: :nesting_head` — the walker threads no qualified prefix; its meta-new and eval
20
+ # splits resolve against `nesting.first` split into segments, which differs from the walk's prefix
21
+ # below a compact header and below an unnameable cref (#1521 item 1).
22
+ # - `unrendered_header: :body_with_lost_nesting` — a header that renders no name (a parse error) walks
23
+ # its body alone, with the chain lost unless an unnameable cref encloses it (#1521 item 3).
24
+ VARIANTS = {
25
+ factory_block: :ordinary_call,
26
+ lexical_prefix: :nesting_head,
27
+ unrendered_header: :body_with_lost_nesting
28
+ }.freeze
29
+
30
+ def initialize
31
+ @table = {}.compare_by_identity
32
+ end
33
+
34
+ # Nothing below a `def` declares a class or module, so the walk goes no further in for this table.
35
+ def on_def(node, context)
36
+ nesting = context.nesting
37
+ @table[node] = nesting unless nesting.nil?
38
+ DeclarationWalk::DECLINE
39
+ end
40
+
41
+ def table
42
+ @table.freeze
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ module ScopeIndexer
6
+ # Issue #1507 — a file's issue #681 def-nesting table read over the cross-file seed rather than merged into a
7
+ # copy of it. The copy inserted every `def` in the PROJECT once per analysed file (263 ms of an 18.95 s cold
8
+ # Mastodon run); a lookup probes at most both layers, and only for a `def` whose body is re-walked.
9
+ #
10
+ # Both layers are keyed by node identity, so a same-file declaration and its cross-file twin are distinct keys
11
+ # and the order cannot change an answer. The file layer is read first all the same, which is what the merge it
12
+ # replaces did (`merge!(seed)`, then `merge!(file)`), so the answer holds even if a shared parse ever made the
13
+ # keys overlap.
14
+ #
15
+ # It answers what the table's readers ask — `[]` ({ExpressionTyper#recorded_def_nesting}) and `empty?` — and
16
+ # is deliberately not a Hash: `each`, `key?`, `size` or `==` would have to see both layers, and a Hash holding
17
+ # one of them would answer those wrong instead of raising.
18
+ class LayeredDefNestings
19
+ def initialize(file, seed)
20
+ @file = file
21
+ @seed = seed
22
+ freeze
23
+ end
24
+
25
+ # `fetch` rather than `||`, so presence in the file layer decides, as the later `merge!` did.
26
+ def [](def_node)
27
+ @file.fetch(def_node) { @seed[def_node] }
28
+ end
29
+
30
+ def empty?
31
+ @file.empty? && @seed.empty?
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../declaration_walk/traversal"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module ScopeIndexer
8
+ # ADR-48's two member-layout tables as one {DeclarationWalk} collector: the ordered members of each
9
+ # `Data.define` class and of each `Struct.new` class (with its `keyword_init:` flag), in both the subclass
10
+ # form (`class Point < Data.define(:x, :y)`) and the constant form (`Point = Data.define(:x, :y)`). The
11
+ # legacy walkers built the two tables in two walks with the same rules; one collector builds both.
12
+ class MemberLayoutsCollector
13
+ include DeclarationWalk::Collector
14
+
15
+ # The rules the member-layout walkers answered differently from the walk, kept so both tables stay
16
+ # byte-identical (ADR-116 WD5). Each is a legacy answer, and #1521 tracks converging it:
17
+ #
18
+ # - `factory_block: :ordinary_call` — no bare-factory arm, so a `self::` header in a `Class.new { … }`
19
+ # block anchors on the enclosing `self` and the block's parameters are walked (#1521 item 8).
20
+ # - `unrendered_header: :skip` — a header that renders no name (a parse error) stops the walk there:
21
+ # neither its parts nor its body are walked (#1521 item 3).
22
+ VARIANTS = { factory_block: :ordinary_call, unrendered_header: :skip }.freeze
23
+
24
+ def initialize
25
+ @data = {}
26
+ @struct = {}
27
+ end
28
+
29
+ # A `class Point < Data.define(:x, :y)` header, recorded under the class it names; an unnameable
30
+ # header (`class D` below `class <<` opens `#<Class:C>::D`, which no name spells) records nothing.
31
+ def on_declaration(node, _context, body)
32
+ if node.is_a?(Prism::ClassNode) && !body.singleton_cref
33
+ ScopeIndexer.record_data_member_layout(@data, body.prefix, node.superclass, allow_outer_block: false)
34
+ ScopeIndexer.record_struct_member_layout(@struct, body.prefix, node.superclass, allow_outer_block: false)
35
+ end
36
+ DeclarationWalk::DESCEND
37
+ end
38
+
39
+ # A `Point = Data.define(:x, :y)` write, recorded under the class the write names. Below an unnameable
40
+ # cref only a path write still names one.
41
+ def on_constant_write(node, context)
42
+ self_owner = context.self_owner
43
+ unless context.singleton_cref && !ScopeIndexer.meta_new_path_target_nameable?(node, self_owner)
44
+ child_prefix = ScopeIndexer.meta_new_child_prefix(node, context.prefix, self_owner)
45
+ end
46
+ if child_prefix
47
+ rvalue = ScopeIndexer.meta_new_rvalue(node)
48
+ ScopeIndexer.record_data_member_layout(@data, child_prefix, rvalue)
49
+ ScopeIndexer.record_struct_member_layout(@struct, child_prefix, rvalue)
50
+ end
51
+ DeclarationWalk::DESCEND
52
+ end
53
+
54
+ # `[data_member_layouts, struct_member_layouts]`, each frozen.
55
+ def tables
56
+ [@data.freeze, @struct.freeze]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ module ScopeIndexer
6
+ # The accumulator the mixin walks ({ScopeIndexer#mixin_tables}, {ScopeIndexer#extend_tables}) fill: the
7
+ # per-class module lists as before, plus which mixin edges have no position the tables can vouch for
8
+ # (`Scope::DiscoveryIndex#unpositioned_mixins`).
9
+ #
10
+ # The mixin tables store each class's modules in the order the statements that wrote them run, and that
11
+ # order is a fact only for a DIRECT statement of the class's own body (or of its `class << self` body),
12
+ # in a declaration that is itself a direct statement (a top-level statement, or a direct statement of a
13
+ # direct body): `include M` there runs where it is written, unconditionally. An `include` inside a
14
+ # method, a block (`class_eval`, `Class.new`, `included do`), a conditional, or a declaration wrapped in
15
+ # a conditional or a block (`class C; include B; end if X`) runs when that code runs, if it runs at
16
+ # all, and the receiver form (`Base.prepend(M)`) runs wherever it is called from, so the order the
17
+ # tables give such an edge is a guess. The walks record those edges as they always did and name them
18
+ # here, so a reader that depends on ORDER can decline where the order is not known. An edge written
19
+ # both ways is unpositioned: one guessed copy is enough to move the answer.
20
+ #
21
+ # A mixin call the walks CANNOT record (`include helper`, `include(*MODS)`, `send(:include, M)`,
22
+ # `self.include M`, `C.include(M)`, `singleton_class.include M`, `base.extend M` or
23
+ # `base.class_eval { include M }` in a `self.included(base)` hook, a name in an argument list beside one
24
+ # the walk cannot name) still reshapes the ancestry of the class it belongs to, and
25
+ # leaves the edges the tables did record looking positioned. So it taints the whole owner side with the
26
+ # {WILDCARD} name instead: an owner side that lists {WILDCARD} has no known order at all.
27
+ #
28
+ # The value is `{owner => {include: [names], extend: [names]}}`. `:include` is the instance side
29
+ # (`include` and `prepend`), `:extend` the singleton side (`extend`, and `include` / `prepend` inside
30
+ # `class << self`), so an unknown `extend` never taints a class's instance ancestry.
31
+ #
32
+ # A Hash (owner => lists), so every walk helper that indexes the accumulator keeps working unchanged.
33
+ class MixinAccumulator < Hash
34
+ # The name an owner side lists when a mixin call the walk could not record was written on it.
35
+ WILDCARD = "*"
36
+
37
+ # What {#unpositioned} answers when nothing was noted: one shared frozen table, so the common file
38
+ # allocates none.
39
+ EMPTY = {}.freeze
40
+
41
+ DIRECT_STATEMENTS = [Prism::CallNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
42
+ private_constant :DIRECT_STATEMENTS
43
+
44
+ def initialize
45
+ super
46
+ @direct = nil
47
+ @unpositioned = nil
48
+ @hook_params = nil
49
+ @body_blocks = nil
50
+ end
51
+
52
+ # Marks the statements of a body whose own position is a fact — the calls and declarations in them
53
+ # are the only ones whose position is. The caller marks a declaration's body only when the
54
+ # declaration was itself marked (or is the file's top level).
55
+ def direct_body(body)
56
+ return unless body.is_a?(Prism::StatementsNode)
57
+
58
+ body.body.each do |statement|
59
+ next unless DIRECT_STATEMENTS.any? { |kind| statement.is_a?(kind) }
60
+
61
+ (@direct ||= Set.new.compare_by_identity) << statement
62
+ end
63
+ end
64
+
65
+ # Marks the block-carrying calls that are statements of a class or module body, whatever that declaration
66
+ # sits in: the only blocks whose `self` the extends walk takes to be the body's own (#1592).
67
+ def body_statements(body)
68
+ return unless body.is_a?(Prism::StatementsNode)
69
+
70
+ body.body.each do |statement|
71
+ if statement.is_a?(Prism::CallNode) && statement.block
72
+ (@body_blocks ||= Set.new.compare_by_identity) << statement
73
+ end
74
+ end
75
+ end
76
+
77
+ def body_block?(node)
78
+ !@body_blocks.nil? && @body_blocks.include?(node)
79
+ end
80
+
81
+ # Runs the block with the parameters of a `self.included(base)`-style hook in scope: a mixin call on
82
+ # one of them reshapes the class that reached the hook, which this walk cannot name.
83
+ def with_hook_params(names)
84
+ saved = @hook_params
85
+ @hook_params = saved ? saved + names : names
86
+ yield
87
+ ensure
88
+ @hook_params = saved
89
+ end
90
+
91
+ def hook_local?(name)
92
+ !@hook_params.nil? && @hook_params.include?(name)
93
+ end
94
+
95
+ def direct?(node)
96
+ !@direct.nil? && @direct.include?(node)
97
+ end
98
+
99
+ # Notes the edges `node` recorded for `owner` on `side`. `complete` is false when an argument named
100
+ # nothing the walk could record, which leaves the recorded names' neighbours unknown.
101
+ def note(node, owner, side, targets, complete:)
102
+ return taint(owner, side) unless complete
103
+ return if node.receiver.nil? && direct?(node)
104
+
105
+ add(owner, side, targets)
106
+ end
107
+
108
+ # Names `owner`'s `side` as having no known order.
109
+ def taint(owner, side)
110
+ add(owner, side, [WILDCARD])
111
+ end
112
+
113
+ # `{owner => {side => [names]}}`, frozen, each list de-duplicated.
114
+ def unpositioned
115
+ return EMPTY if @unpositioned.nil?
116
+
117
+ @unpositioned.transform_values do |sides|
118
+ sides.transform_values { |names| names.uniq.freeze }.freeze
119
+ end.freeze
120
+ end
121
+
122
+ private
123
+
124
+ def add(owner, side, names)
125
+ ((@unpositioned ||= {})[owner] ||= {})[side] = ((@unpositioned[owner][side] || []) + names)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../declaration_walk/traversal"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module ScopeIndexer
8
+ # The as-written superclass table and its issue #682 header-nesting twin (ADR-24 slice 2), as a
9
+ # {DeclarationWalk} collector: each `class Foo < Bar` header's superclass name and the `Module.nesting`
10
+ # its header sits in, plus the superclass of a `Class.new(Parent) { … }` block under that anonymous
11
+ # class's synthetic name (#319). One walk builds both tables, so a caller never pairs a superclass
12
+ # with a nesting from a different parse.
13
+ class SuperclassesCollector
14
+ include DeclarationWalk::Collector
15
+
16
+ # The two rules `walk_class_superclasses` answered differently from the walk, kept so both tables stay
17
+ # byte-identical (ADR-116 WD5). Each is a legacy answer, and #1521 tracks converging it:
18
+ #
19
+ # - `factory_block: :ordinary_call` — the legacy walker had no bare-factory arm, so it walked
20
+ # `Class.new { … }` as any call, block parameters included, with the enclosing `self`. A
21
+ # `class self::E < S` in that block is filed under the enclosing class (`C::E`), a class Ruby never
22
+ # creates (#1521 item 8).
23
+ # - `anonymous_class_path: :outside_class_bodies` — the legacy walker dropped the file path when it
24
+ # entered a class/module, meta-new or eval-family body, but not a `class <<` body, so an anonymous
25
+ # class created inside a class body is keyed without the path the other tables give it (#1521 item
26
+ # 11).
27
+ VARIANTS = { factory_block: :ordinary_call, anonymous_class_path: :outside_class_bodies }.freeze
28
+
29
+ def initialize
30
+ @accumulator = { superclasses: {}, header_nestings: {} }
31
+ # Read through the class, as the walk reads `factory_block`, so a subclass that overrides `VARIANTS`
32
+ # gets one answer from both.
33
+ @path_variant = DeclarationWalk::Collector.variant_of(self.class, :anonymous_class_path)
34
+ end
35
+
36
+ # A header that still names its class records its ancestry: an unnameable one (`class D` below
37
+ # `class <<`) would publish facts for a `C::D` Ruby never creates. The nesting recorded is the one
38
+ # OUTSIDE the header, because the header is evaluated before its body is entered.
39
+ def on_declaration(node, context, body)
40
+ unless body.singleton_cref
41
+ ScopeIndexer.record_declaration_ancestry(node, context.nesting, body.prefix, @accumulator)
42
+ end
43
+ DeclarationWalk::DESCEND
44
+ end
45
+
46
+ # Only a `Class.new(Parent) { … }` with a literal block records anything, so a call without one is
47
+ # passed over before the path is looked up.
48
+ def on_call(node, context)
49
+ if node.block.is_a?(Prism::BlockNode)
50
+ path = context.anonymous_class_path(@path_variant)
51
+ ScopeIndexer.record_anonymous_meta_superclass(node, @accumulator[:superclasses], path)
52
+ end
53
+ DeclarationWalk::DESCEND
54
+ end
55
+
56
+ # `[superclasses, header_nestings]`, each frozen, as {ScopeIndexer.declaration_walk_tables} returns
57
+ # them.
58
+ def tables
59
+ [@accumulator[:superclasses].freeze, @accumulator[:header_nestings].freeze]
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end