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
@@ -15,6 +15,7 @@ module Rigor
15
15
  :class_ivars,
16
16
  :class_cvars,
17
17
  :program_globals,
18
+ :program_global_seeds,
18
19
  :discovered_classes,
19
20
  :in_source_constants,
20
21
  :discovered_methods,
@@ -24,12 +25,20 @@ module Rigor
24
25
  :discovered_def_sources,
25
26
  :discovered_singleton_def_sources,
26
27
  :discovered_method_visibilities,
28
+ :discovered_parameter_envelopes,
27
29
  :discovered_superclasses,
30
+ :discovered_deferred_ranges,
31
+ :discovered_refinements,
32
+ :discovered_global_write_census,
28
33
  :discovered_header_nestings,
29
34
  :discovered_includes,
35
+ :discovered_prepends,
30
36
  :discovered_extends,
37
+ :unpositioned_mixins,
31
38
  :discovered_class_sources,
32
39
  :constant_sources,
40
+ :constant_writers,
41
+ :constant_shadowers,
33
42
  :published_constant_names,
34
43
  :local_constant_names,
35
44
  :published_constant_alias_names,
@@ -37,7 +46,17 @@ module Rigor
37
46
  :data_member_layouts,
38
47
  :struct_member_layouts,
39
48
  :param_inferred_types,
40
- :run_generation
49
+ :run_generation,
50
+ :patched_line_readers,
51
+ :clears_last_status,
52
+ :defines_case_equality,
53
+ :implicit_self_evidence,
54
+ :possible_discovered_methods,
55
+ :possible_discovered_deferred_ranges,
56
+ :contested_discovered_def_nodes,
57
+ :contested_discovered_singleton_def_nodes,
58
+ :contested_discovered_method_visibilities,
59
+ :contested_discovered_parameter_envelopes
41
60
  )
42
61
 
43
62
  class DiscoveryIndex
@@ -46,6 +65,114 @@ module Rigor
46
65
  EMPTY_NAME_SET = Set.new.freeze
47
66
  private_constant :EMPTY_NODE_TABLE, :EMPTY_TABLE, :EMPTY_NAME_SET
48
67
 
68
+ # ADR-119 WD1 — each member that may one day admit `possible` or `contested` facts, and the sibling that
69
+ # holds them. Every copy path reads this one declaration, so a member never travels without its sibling:
70
+ # {#with} raises on a half pair and {.compact_pairs} drops a pair only when BOTH halves are empty. No
71
+ # producer fills a sibling yet (the lane-1 carriage), so every sibling is empty and each member keeps its
72
+ # meaning, the union of its certain and possible facts.
73
+ #
74
+ # - `possible_*` has its member's shape and is a subset of it (a `discovered_methods` pair is a
75
+ # `{class => {name => kind}}` table).
76
+ # - `contested_*` is a Set of key paths into its member, one per entry whose value depends on a possible
77
+ # fact: `[class, method]`, or `[class, [kind, method]]` for an envelope.
78
+ #
79
+ # The members without a sibling here (`discovered_def_sources`, `discovered_singleton_def_sources`) are site
80
+ # tables (dependency edges, positions, fingerprints): no read takes a definer, visibility, arity or type from
81
+ # them, and certainty does not move a site. The reads whose answer depends on the site are
82
+ # `Scope#same_file_top_level_def?` (file identity, independent of certainty), `#singleton_def_shadows_call?`
83
+ # and `#instance_def_shadows_call?` (a conditional def's site answers "shadows"). Where "shadows" narrows an
84
+ # answer, the caller pairs it with {#possible_method?}: existence is the method table, certainty is the possible
85
+ # table alone (ADR-119 C1d-c).
86
+ SIBLINGS = {
87
+ discovered_methods: :possible_discovered_methods,
88
+ discovered_deferred_ranges: :possible_discovered_deferred_ranges,
89
+ discovered_def_nodes: :contested_discovered_def_nodes,
90
+ discovered_singleton_def_nodes: :contested_discovered_singleton_def_nodes,
91
+ discovered_method_visibilities: :contested_discovered_method_visibilities,
92
+ discovered_parameter_envelopes: :contested_discovered_parameter_envelopes
93
+ }.freeze
94
+
95
+ # Either half of a pair to the other.
96
+ PARTNERS = SIBLINGS.flat_map { |member, sibling| [[member, sibling], [sibling, member]] }.to_h.freeze
97
+ private_constant :PARTNERS
98
+
99
+ # [#1507](https://github.com/rigortype/rigor/issues/1507) — the kind of fact each member holds, which decides
100
+ # what a reader may conclude from an entry and which gate covers the member (ADR-119 WD4, proposed). Each
101
+ # reason describes the table as the code builds it today. `spec/rigor/declaration_facts/member_classes_spec.rb`
102
+ # fails on a member that is unclassified or classified twice, and checks each class's shape on an index built
103
+ # from a fixture project; it pins the few misfilings those checks accept.
104
+ #
105
+ # - `:set_valued` — names, edges, files or rows whose presence is the fact. A reader asks whether an entry is
106
+ # there.
107
+ # - `:single_valued` — one value per key, which readers use; disagreeing contributions fold by a fixed rule.
108
+ # - `:typed` — the values are types, which already carry uncertainty in the type lattice.
109
+ # - `:syntactic` — read off the parse of the analysed file alone, which is their reference.
110
+ # - `:run_state` — the run's own state, not a fact about the program.
111
+ # - `:sibling` — the `possible_*` / `contested_*` half of a pair in {SIBLINGS}, which travels with its member.
112
+ MEMBER_CLASSES = {
113
+ set_valued: {
114
+ discovered_classes: "the class and module names the project declares; the value is the name's singleton",
115
+ discovered_methods: "which names each class answers, and on which side",
116
+ discovered_refinements: "the methods a refine block adds, keyed by the refined class",
117
+ discovered_global_write_census: "the names any file aliases, defines or mixes in at the top level",
118
+ discovered_includes: "the modules each class includes, prepends among them",
119
+ discovered_prepends: "the modules each class prepends",
120
+ discovered_extends: "the modules each singleton mixes in; the module itself for module_function",
121
+ unpositioned_mixins: "per side, the mixin edges each class records whose order is not a fact, or \"*\"",
122
+ discovered_class_sources: "the files that declare each class, reopenings included",
123
+ discovered_deferred_ranges: "each file's def and block ranges; def rows add owner and module_function side",
124
+ constant_sources: "the files that write each constant",
125
+ constant_writers: "the constant names some write binds, by last segment",
126
+ constant_shadowers: "the constant names a non-memo write assigns, by last segment",
127
+ published_constant_names: "the last segments of the constants the project publishes",
128
+ local_constant_names: "the last segments of the constants the analysed file assigns",
129
+ published_constant_alias_names: "the file's constants copied straight from a published one",
130
+ published_constant_ivars: "the ivars each class seeds from a published constant"
131
+ }.freeze,
132
+ single_valued: {
133
+ discovered_def_nodes: "the def node each instance method resolves to",
134
+ discovered_singleton_def_nodes: "the def node each singleton method resolves to",
135
+ discovered_def_sources: "the first-seen path:line of each instance method, which readers use",
136
+ discovered_singleton_def_sources: "the first-seen path:line of each singleton method, which readers use",
137
+ discovered_method_visibilities: "each method's visibility",
138
+ discovered_parameter_envelopes: "each method's parameter shape, opaque when contributions disagree",
139
+ discovered_superclasses: "each class's superclass as written",
140
+ discovered_header_nestings: "the cref each ancestor name resolves in, an alternatives list when ambiguous",
141
+ data_member_layouts: "each Data class's member layout",
142
+ struct_member_layouts: "each Struct class's member layout"
143
+ }.freeze,
144
+ typed: {
145
+ declared_types: "the type each declaration node binds",
146
+ class_ivars: "the joined type of each ivar's writes, per class",
147
+ class_cvars: "the joined type of each cvar's writes, per class",
148
+ program_globals: "the joined type of each global's writes",
149
+ program_global_seeds: "a declared global's signature type joined with its writes",
150
+ in_source_constants: "the type each constant's write binds",
151
+ param_inferred_types: "the union of each parameter's call-site argument types"
152
+ }.freeze,
153
+ syntactic: {
154
+ discovered_def_nestings: "the Module.nesting each def is written in, keyed by node identity",
155
+ patched_line_readers: "the gets / readline names the file defines through the define_method family",
156
+ clears_last_status: "whether the analysed file holds a call that may set $? to nil",
157
+ defines_case_equality: "whether the analysed file holds a define_method naming ===",
158
+ implicit_self_evidence: "where the file's implicit-self readers sit; built lazily from its tree, never seeded"
159
+ }.freeze,
160
+ run_state: {
161
+ run_generation: "the identity token of the current run"
162
+ }.freeze,
163
+ sibling: {
164
+ possible_discovered_methods: "the names and sides of discovered_methods that only a possible definer gives",
165
+ possible_discovered_deferred_ranges: "the rows of discovered_deferred_ranges that only a possible fact gives",
166
+ contested_discovered_def_nodes: "the key paths of discovered_def_nodes that a possible definer may change",
167
+ contested_discovered_singleton_def_nodes:
168
+ "the key paths of discovered_singleton_def_nodes that a possible definer may change",
169
+ contested_discovered_method_visibilities:
170
+ "the key paths of discovered_method_visibilities that a possible fact may change",
171
+ contested_discovered_parameter_envelopes:
172
+ "the key paths of discovered_parameter_envelopes that a possible fact may change"
173
+ }.freeze
174
+ }.freeze
175
+
49
176
  # The third value a `discovered_methods` entry can hold, beside `:instance` and `:singleton`. One name may
50
177
  # legitimately be defined on both sides of the same class (`def helper` plus a `class << self` twin), and the
51
178
  # table is keyed by name alone — so before this existed the second `def` overwrote the first's kind and
@@ -60,6 +187,55 @@ module Rigor
60
187
  # as written, and no ancestor name can be nil.
61
188
  UNKEYED_HEADER_NESTING = nil
62
189
 
190
+ # Issue #986 — a bucket entry is normally ONE chain (`["Wrap"]`, innermost first). When the
191
+ # compact-header rename pass lands two declarations of one class on a single key and both wrote the
192
+ # same raw ancestor name in DIFFERENT crefs, the entry is instead the LIST of those chains —
193
+ # `[["Wrap"], []]` — because no union of them is Ruby's answer for either site. `Scope` resolves each
194
+ # alternative and declines outright when they name two different project classes: which one wins
195
+ # depends on the runtime load order of the two `include`s, which static analysis cannot know, so
196
+ # picking either is unsound. A chain's entries are Strings and an alternatives list's are Arrays, so
197
+ # the two shapes cannot be confused.
198
+ def self.ambiguous_header_nesting?(entries)
199
+ entries.first.is_a?(Array)
200
+ end
201
+
202
+ # Issue #992 — the two class-wide keys a `discovered_parameter_envelopes` bucket can carry beside its
203
+ # `[kind, method_name]` entries. Their PRESENCE is the fact; the value is always
204
+ # `Source::ParameterEnvelope::OPAQUE`, so a bucket folds under the one join every other entry does.
205
+ # Symbols, because every per-method key is an Array and neither can collide with the other.
206
+ #
207
+ # The class marks below and {ENVELOPE_PROJECT_WIDE} are presence facts read only to decline a read; they are
208
+ # never a contested path. Contested envelope paths are always `[class, [kind, name]]`.
209
+ #
210
+ # - {ENVELOPE_MODULE_MARK}: the name is declared with `module` (or `Const = Module.new do … end`) somewhere
211
+ # in the project. An instance of it is an instance of an unknown includer.
212
+ # - {ENVELOPE_DYNAMIC_MARK}: the class body rewrites its method table in a way no literal argument names
213
+ # (`class_eval`, a computed `define_method`, `send`, a non-constant mixin), or a constant-receiver form
214
+ # of those names it from outside.
215
+ # - {ENVELOPE_OBJECT_EXTENDED_MARK}: some method body passes the module to `extend`, so an object of any
216
+ # class may carry its instance methods ahead of that class's own.
217
+ # - {ENVELOPE_REFINED_MARK}: a `refine` block refines the class, which redefines its methods wherever a `using`
218
+ # is in effect. Issue #1367 splits it from {ENVELOPE_DYNAMIC_MARK}, which it used to share: an implicit
219
+ # conversion (`to_str`, `to_int`) ignores a refinement, so the `global.*` rules must tell the two apart.
220
+ # Every reader that asks whether a surface is rewritten reads both ({.rewritten_surface?}).
221
+ ENVELOPE_MODULE_MARK = :"<module>"
222
+ ENVELOPE_DYNAMIC_MARK = :"<dynamic>"
223
+ ENVELOPE_OBJECT_EXTENDED_MARK = :"<object-extended>"
224
+ ENVELOPE_REFINED_MARK = :"<refined>"
225
+
226
+ # Whether an envelope bucket says the project rewrites the class's method table beyond its literal names: a
227
+ # dynamic rewrite, or a refinement.
228
+ def self.rewritten_surface?(bucket)
229
+ bucket.key?(ENVELOPE_DYNAMIC_MARK) || bucket.key?(ENVELOPE_REFINED_MARK)
230
+ end
231
+
232
+ # Issue #992 — the class key a WHOLE-PROJECT discovery pass adds to `discovered_parameter_envelopes`
233
+ # (`ScopeIndexer#finalize_def_index`), with an empty bucket. A single file's walk alone — `run_source`,
234
+ # the LSP `prebuilt:` scope — sees one `def` and cannot see the reopening, subclass or `class_eval` in
235
+ # another file that would make it opaque, so `call.wrong-arity` reads an envelope only when this key is
236
+ # present. Not a constant character, so no class can be named it.
237
+ ENVELOPE_PROJECT_WIDE = "<project-wide>"
238
+
63
239
  # The shared all-empty index `Scope.empty` (and every scope that never sees a seeding pass) points at — one
64
240
  # allocation per process.
65
241
  EMPTY = new(
@@ -67,6 +243,12 @@ module Rigor
67
243
  class_ivars: EMPTY_TABLE,
68
244
  class_cvars: EMPTY_TABLE,
69
245
  program_globals: EMPTY_TABLE,
246
+ # Issue #1362 — for each global in `program_globals` that Ruby's own signatures declare (`$VERBOSE: bool?`,
247
+ # `$stdout: IO`), its declared type joined with its writes in the file: what every method body and the top
248
+ # level start from, under the ADR-58 `:global` mark. Any other global starts from its `program_globals` entry,
249
+ # which keeps the writes alone for the consumers that compare against them (the mark's gates, and `$;` for
250
+ # `split`). Filled by `Inference::ScopeIndexer.index` from the file's own tree only.
251
+ program_global_seeds: EMPTY_TABLE,
70
252
  discovered_classes: EMPTY_TABLE,
71
253
  in_source_constants: EMPTY_TABLE,
72
254
  discovered_methods: EMPTY_TABLE,
@@ -87,7 +269,37 @@ module Rigor
87
269
  discovered_def_sources: EMPTY_TABLE,
88
270
  discovered_singleton_def_sources: EMPTY_TABLE,
89
271
  discovered_method_visibilities: EMPTY_TABLE,
272
+ # Issue #992 — `{qualified class name => {[kind, method_name] => Source::ParameterEnvelope}}`, the
273
+ # parameter envelope of every method the declaration walk records, plus {ENVELOPE_MODULE_MARK} /
274
+ # {ENVELOPE_DYNAMIC_MARK}. Written beside `discovered_methods` by one recorder, so an `alias` / `attr_*`
275
+ # / `define_method` name is always present here as `OPAQUE`, and folded across files and reopenings
276
+ # with `Source::ParameterEnvelope.merge`: one value per name, a real envelope only while every
277
+ # contribution agrees. Read by `call.wrong-arity` for a method no signature declares.
278
+ discovered_parameter_envelopes: EMPTY_TABLE,
90
279
  discovered_superclasses: EMPTY_TABLE,
280
+ # Issue #1097 — `{file path => [[start_offset, end_offset, name, kind, owner], ...]}`, every
281
+ # `def` / block / lambda body range in the file. `Scope#*_def_shadows_call?` reads it to tell
282
+ # an eager class-body call (orderable by offset against a same-name, same-owner def entry)
283
+ # from a deferred one (contained in any range — it runs at invocation time, when every
284
+ # class-body def exists). Def entries carry the method name, the `:instance` / `:singleton` /
285
+ # `:both` (module_function) kind, and the qualified owner; block / lambda rows and defs nested
286
+ # inside another deferred range carry nils and answer only the containment half. Plain data,
287
+ # so the ADR-85 seed bundle round-trips it unchanged.
288
+ discovered_deferred_ranges: EMPTY_TABLE,
289
+ # Issue #1120 — `{refined class name => {method name => [refining module names]}}`, the instance
290
+ # methods a `refine X do … end` block defines. They are not X's methods everywhere: Ruby activates them
291
+ # only lexically after a `using` of the refining module, so `call.undefined-method` reads this table
292
+ # together with the call site's file ({Analysis::CheckRules::LexicalMethodSites}) and never through
293
+ # `discovered_methods`. A refinement a `Module.new { … }` block defines has no nameable module; it is
294
+ # keyed by the name the walk gives the block's owner. Plain data, so the ADR-85 seed bundle
295
+ # round-trips it unchanged.
296
+ discovered_refinements: EMPTY_TABLE,
297
+ # Issue #1367 — the project's `Inference::GlobalWriteCensus`: the globals any `alias` names, the
298
+ # `write` / `to_str` / `to_int` / hatch names any file defines (inside a refinement or not), and the
299
+ # modules a top-level mixin names, over every file and inside method bodies too. The `global.*` write rules
300
+ # read it. The project pre-pass seeds the whole project's census and `Inference::ScopeIndexer.index` adds the
301
+ # analysed file's own. Plain data, so the ADR-85 seed bundle round-trips it unchanged.
302
+ discovered_global_write_census: EMPTY_NAME_SET,
91
303
  # Issue #682 — `{qualified class name => Module.nesting where its declaration HEADER is written}`,
92
304
  # innermost first and EXCLUDING the declaration's own entry. Read by `Scope#ancestor_name_candidates`,
93
305
  # which resolves a superclass / include name in that cref instead of peeling the subclass's qualified
@@ -102,18 +314,54 @@ module Rigor
102
314
  # a name no site recorded under its own key.
103
315
  discovered_header_nestings: EMPTY_TABLE,
104
316
  discovered_includes: EMPTY_TABLE,
317
+ # Issue #1123 — `{qualified class or module name => [module names it `prepend`s, as written]}`,
318
+ # stored in instance-ancestor SEARCH order (nearest prepend first), the `discovered_extends`
319
+ # convention. The instance-side twin `discovered_includes` holds the same names — prepends
320
+ # included — in that order too since #1173: prepends ahead of includes, each nearest-first. The
321
+ # kind still cannot be told apart there, so `Scope#user_def_through_ancestors` reads the wedge
322
+ # from here. An absent entry means "prepends nothing", which is the un-preprended behaviour
323
+ # every scope had before it existed.
324
+ discovered_prepends: EMPTY_TABLE,
105
325
  # Issue #898 — the singleton-side twin of `discovered_includes`: `{qualified class or module name =>
106
326
  # [module names it `extend`s, as written]}`, built by the same `ScopeIndexer` walk that #526 already
107
327
  # ran to fold an extended module's instance defs onto the extending class's singleton. #526 consumed
108
328
  # the table inside the indexer and threw it away; `Narrowing` needs it to survive onto the scope,
109
329
  # because `Singleton[C]`'s ancestry is exactly what `extend` writes and nothing else records it.
110
330
  discovered_extends: EMPTY_TABLE,
331
+ # `{qualified class or module name => {include: [names], extend: [names]}}` — the mixin edges of the three
332
+ # tables above whose ORDER is not a fact, per side (`:include` is the instance side, `include` and
333
+ # `prepend`; `:extend` the singleton side). An edge is listed when it is written anywhere but as a direct
334
+ # statement of the class's own body (or its `class << self` body) in a declaration that is itself a
335
+ # direct statement — inside a `def`, a block, a conditional, or a conditional or deferred declaration —
336
+ # or in the receiver form, and when it shares an argument list with a name the walk could not record
337
+ # (`Inference::ScopeIndexer::MixinAccumulator`). A mixin call the walk cannot record at all (`include
338
+ # helper`, `include(*MODS)`, `send(:include, M)`, `self.include M`, `C.include(M)`,
339
+ # `singleton_class.include M`, a call on a `self.included(base)`-style hook's parameter, an `*_eval` block on
340
+ # one or on an opaque receiver whose body mixes a module in) lists the name
341
+ # `"*"` on the side it reshapes: that side of the owner has no known order. An edge written both ways
342
+ # is listed. Folded by union. A reader that depends on the order of a class's ancestors MUST decline
343
+ # when the owner it asks about, or any ancestor it walks through, lists anything on the side it reads;
344
+ # the three tables themselves are unchanged.
345
+ unpositioned_mixins: EMPTY_TABLE,
346
+ # The files that declare each class, reopenings included. Seeded on every run: the ADR-46 recording
347
+ # accessors read it for their class edges, and an ancestor-order reader reads whether a class is
348
+ # declared in more than one file, where the order of its mixins across the files is load order.
111
349
  discovered_class_sources: EMPTY_TABLE,
112
350
  # Issue #644 — `{qualified constant name => Set[declaring file]}`, the write attribution behind the
113
351
  # cross-file value-constant table. Read only by `Scope#record_constant_dependency` during ADR-46
114
352
  # dependency recording, so the runner seeds it only on a recording run; every other run leaves it
115
353
  # empty and the edge costs one nil check.
116
354
  constant_sources: EMPTY_TABLE,
355
+ # Issue #617 — the censused names that bind (any write other than a memo `||=`, or memos of the
356
+ # segment in two files), grouped by LAST SEGMENT, wildcard keys (`*::LIMIT`) under their segment.
357
+ # `Scope#bound_constant_names` reads it for a constant compound write whose plain read resolves to
358
+ # nothing: such a name is bound, just not to a value the analyzer carries. Seeded on every run, because
359
+ # the question is a typing one rather than a recording one.
360
+ constant_writers: EMPTY_TABLE,
361
+ # Issue #1290 — the censused names some write other than a memo `||=` assigns, grouped by LAST SEGMENT.
362
+ # `Scope#shadowing_constant_names` reads it for the lexical ladder: a candidate no source types but this
363
+ # table holds is where Ruby's lookup stops. Seeded on every run beside `constant_writers`.
364
+ constant_shadowers: EMPTY_TABLE,
117
365
  # Issue #644 — the two halves of `Scope#published_constant?`, the question
118
366
  # {Analysis::CheckRules::PublishedConstantGuard} asks. `published_constant_names` is the LAST
119
367
  # SEGMENTS of the project-wide published table (run-wide, seeded by the runner);
@@ -147,8 +395,109 @@ module Rigor
147
395
  # cross a run boundary (LSP / ADR-62 warm-loop re-runs land in a fresh bucket). Nil on scopes that
148
396
  # never see the runner seed (single-file probes, `run_source` before the seed applies): the memo
149
397
  # falls back to today's per-file `discovered_def_nodes` identity for those.
150
- run_generation: nil
398
+ run_generation: nil,
399
+ # Issue #1359 — the `gets` / `readline` names this file patches in through the `define_method` family
400
+ # (`$stdin.define_singleton_method(:gets) { … }`, `IO.define_method(:gets)`, `alias_method :gets, :x`, or a
401
+ # computed name, which may be either), which `Inference::LastLine.reads_line?` declines on, as it does on a
402
+ # name `BlockCallTiming.project_defines_anywhere?` finds. Filled by `Inference::ScopeIndexer.index` from the
403
+ # file's own tree only.
404
+ patched_line_readers: EMPTY_NAME_SET,
405
+ # Issue #1360 — true when this file holds a call that may set `$?` to nil (`Inference::LastStatus.clears?`: a
406
+ # `wait`-family call with a flags argument, or `waitall`), which may run between any subprocess and a read of
407
+ # `$?`, so `Inference::LastStatus.after` binds it nowhere in the file. Filled by `Inference::ScopeIndexer.index`
408
+ # from the file's own tree only.
409
+ clears_last_status: false,
410
+ # Issue #1360 — true when this file holds a `define_method` or `define_singleton_method` call whose literal
411
+ # name is `===` (`Inference::ErrorInfo.defines_case_equality?`), which may give a class the singleton `===`
412
+ # `rescue` matches with; the class it lands on is not recorded, so no rescued class binds `$!` in the file.
413
+ # Filled by `Inference::ScopeIndexer.index` from the file's own tree only.
414
+ defines_case_equality: false,
415
+ # Issue #1415 — the file's evidence about the `self` its implicit-self `gets` / `readline` readers run with
416
+ # (`Inference::LastLine::SelfEvidence`, read on the first ask), which `Inference::LastLine.reads_line?` reads
417
+ # them by. Filled by `Inference::ScopeIndexer.index` from the file's own tree only; nil, where no file was
418
+ # indexed, declines every such reader.
419
+ implicit_self_evidence: nil,
420
+ # ADR-119 WD1 — the siblings of the members {SIBLINGS} pairs, empty until a producer admits a possible fact.
421
+ possible_discovered_methods: EMPTY_TABLE,
422
+ possible_discovered_deferred_ranges: EMPTY_TABLE,
423
+ contested_discovered_def_nodes: EMPTY_NAME_SET,
424
+ contested_discovered_singleton_def_nodes: EMPTY_NAME_SET,
425
+ contested_discovered_method_visibilities: EMPTY_NAME_SET,
426
+ contested_discovered_parameter_envelopes: EMPTY_NAME_SET
151
427
  )
428
+
429
+ # The siblings of an all-empty index, `{sibling name => empty value}`, frozen: what a def-index with no
430
+ # possible fact carries, and a Runner's initial state. Copy it (`dup`) before mutating.
431
+ EMPTY_SIBLINGS = SIBLINGS.each_value.to_h { |sibling| [sibling, EMPTY.public_send(sibling)] }.freeze
432
+
433
+ # The all-empty value of a paired member or sibling.
434
+ def self.empty_value(name)
435
+ EMPTY.public_send(name)
436
+ end
437
+
438
+ def self.empty_siblings = EMPTY_SIBLINGS
439
+
440
+ # ADR-119 WD1 — `tables` (a `with(**)` argument Hash) with every pair either complete or absent: a pair whose
441
+ # halves are both missing, nil or empty is dropped, any other pair carries BOTH halves (the half that was
442
+ # absent or nil becomes its empty value). Dropping members one at a time would strand a non-empty sibling,
443
+ # and passing a half would make {#with} raise. Entries outside {SIBLINGS} pass through untouched.
444
+ def self.compact_pairs(tables)
445
+ tables.each_with_object({}) do |(name, table), out|
446
+ partner = PARTNERS[name]
447
+ next out[name] = table if partner.nil?
448
+ next if out.key?(name)
449
+
450
+ other = tables[partner]
451
+ next if blank_table?(table) && blank_table?(other)
452
+
453
+ out[name] = table.nil? ? empty_value(name) : table
454
+ out[partner] = other.nil? ? empty_value(partner) : other
455
+ end
456
+ end
457
+
458
+ # Whether `name` is a member or sibling of {SIBLINGS}.
459
+ def self.paired?(name)
460
+ PARTNERS.key?(name)
461
+ end
462
+
463
+ def self.blank_table?(table)
464
+ table.nil? || table.empty?
465
+ end
466
+ private_class_method :blank_table?
467
+
468
+ # The six siblings as `{sibling name => table}`, for a caller that rebuilds the paired members and carries
469
+ # their siblings over unchanged.
470
+ def sibling_tables
471
+ SIBLINGS.each_value.to_h { |sibling| [sibling, public_send(sibling)] }
472
+ end
473
+
474
+ # ADR-119 WD1/WD2 — whether `class_name` answers `method_name` on `kind` only through a `possible` fact
475
+ # (`possible_discovered_methods`, empty until a producer admits one). Internal to the candidate-set read.
476
+ def possible_method?(class_name, method_name, kind)
477
+ recorded = possible_discovered_methods[class_name.to_s]&.[](method_name.to_sym)
478
+ recorded == kind || (!recorded.nil? && recorded == METHOD_KIND_BOTH)
479
+ end
480
+
481
+ # ADR-119 WD1 — whether `path` (`[class, name]`, or `[class, [kind, name]]` for an envelope) is a contested
482
+ # entry of `member`, one of the single-valued members {SIBLINGS} pairs. Internal to the candidate-set read.
483
+ def contested?(member, path)
484
+ public_send(SIBLINGS.fetch(member)).include?(path)
485
+ end
486
+
487
+ # Data's `with`, except that a paired member and its sibling are accepted only TOGETHER (ADR-119 WD1): a
488
+ # caller that rebuilt one without the other would leave the sibling describing the old table, so it
489
+ # raises instead. A call with no change returns the receiver.
490
+ def with(**changes)
491
+ return self if changes.empty?
492
+
493
+ changes.each_key do |name|
494
+ partner = PARTNERS[name]
495
+ next if partner.nil? || changes.key?(partner)
496
+
497
+ raise ArgumentError, "DiscoveryIndex#with: #{name} and #{partner} change together; #{partner} is missing"
498
+ end
499
+ super
500
+ end
152
501
  end
153
502
  end
154
503
  end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Scope
5
+ class ResolutionChain
6
+ # ADR-119 WD2 — per-name relevance, the narrowing of an unsettled chain (#1591). A {Mark} says a node's mixin
7
+ # edges have no known order; the chain still answers a question about ONE method name when the module the
8
+ # mark names cannot answer that name, because with no fork every entry reached the chain by a single route,
9
+ # so a module of unknown presence or position adds or removes only its own closure's entries and moves no
10
+ # other entry's first occurrence. A closure that cannot answer the name therefore cannot move its first
11
+ # definer. {ResolutionChain#settle} asks it under `unknown_for:`; nothing else does.
12
+ #
13
+ # - A named entry `Q` (not `"*"`) is discharged for `n` when every entry of `Q`'s own instance chain (its
14
+ # closure) is clean: (i) a project entry, or an external one RBS knows whose declaration lacks `n`
15
+ # (the test `SourceArity#external_mixin_lacks_method?` applies); (ii) no project entry carries the dynamic
16
+ # mark, lists `"*"` on the instance side or the mark's, or records `method_missing`; (iii) no project
17
+ # entry records `n` in `discovered_methods` (either kind) or in `discovered_method_visibilities`.
18
+ # - A multi-file mark names no entry: it is discharged when at most one of the node's edges' closures is
19
+ # not clean. A mark that is both listed and multi-file must pass both rules.
20
+ # - An ambiguous spelling (`:arity` only: several project modules a compact-header collision left as the
21
+ # spelling's meaning, #986) is discharged when every one of them is clean; any candidate that fails fails it.
22
+ # - `"*"` and a truncated closure are never discharged.
23
+ #
24
+ # The verdict and the edges it read are memoised per `[mark, name]` in the flavor's bucket and the edges are
25
+ # replayed on every call while a dependency recording is active (ADR-46): per tested project entry its class
26
+ # edge and the negative method edge on `Owner#name`, per tested external the negative class edge on the last
27
+ # segment of its spelling, so a new file declaring the module re-checks the consumer.
28
+ module Relevance
29
+ WILDCARD = "*"
30
+
31
+ # The singleton hooks Ruby calls while a module enters (or is asked to enter) another's ancestry. A module
32
+ # that defines one can add methods to the includer, extender or prepender with nothing the tables record
33
+ # on the module (`def self.included(base) = base.attr_reader(:foo)`), so no closure holding one can be
34
+ # said not to answer a name.
35
+ HOOKS = %i[included extended prepended inherited append_features extend_object prepend_features].freeze
36
+
37
+ module_function
38
+
39
+ def discharged?(scope, chain, mark, name)
40
+ name = name.to_sym
41
+ memo = ResolutionChain.relevance_memo(scope, chain.flavor)
42
+ verdict, edges = memo[[mark, name]] ||= compute(scope, chain.flavor, mark, name)
43
+ replay(scope, edges) if Analysis::DependencyRecorder.active?
44
+ verdict
45
+ end
46
+
47
+ def compute(scope, flavor, mark, name)
48
+ context = Context.new(scope, flavor, mark, name)
49
+ verdict = !mark.listed.include?(WILDCARD) && listed_discharged?(context) && multi_file_discharged?(context)
50
+ [verdict, context.edges.uniq.freeze].freeze
51
+ end
52
+
53
+ def listed_discharged?(context)
54
+ mark = context.mark
55
+ mark.listed.all? { |raw| context.closure_clean?(raw) }
56
+ end
57
+
58
+ def multi_file_discharged?(context)
59
+ mark = context.mark
60
+ return true unless mark.multi_file
61
+
62
+ edges = context.edge_names
63
+ edges.count { |raw| !context.closure_clean?(raw) } <= 1
64
+ end
65
+
66
+ def replay(scope, edges)
67
+ edges.each do |kind, value|
68
+ case kind
69
+ when :class then ResolutionChain.record_class(scope, value)
70
+ when :method then Analysis::DependencyRecorder.read_missing(:method, value)
71
+ when :external then Analysis::DependencyRecorder.read_missing(:class, value)
72
+ end
73
+ end
74
+ end
75
+
76
+ # True only for an ancestor the project does not declare that RBS knows and whose declaration lacks `name`
77
+ # (`SourceArity#external_mixin_lacks_method?`); an unknown one, or one that has it, may answer.
78
+ def external_lacks?(scope, candidates, name)
79
+ known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
80
+ return false if known.nil?
81
+
82
+ Rigor::Reflection.instance_method_definition(known, name, scope: scope).nil?
83
+ rescue StandardError
84
+ false
85
+ end
86
+
87
+ # One verdict's working state: the scope, the resolver, and the edges the tests read.
88
+ class Context
89
+ attr_reader :mark, :edges
90
+
91
+ def initialize(scope, flavor, mark, name)
92
+ @scope = scope
93
+ @flavor = flavor
94
+ @mark = mark
95
+ @name = name
96
+ @resolver = ResolutionChain.resolver_for(scope, flavor)
97
+ @edges = []
98
+ end
99
+
100
+ # The raw names of the node's own edges on the side the mark covers.
101
+ def edge_names
102
+ discovery = @scope.discovery
103
+ table = @mark.kind == :extend ? discovery.discovered_extends : discovery.discovered_includes
104
+ table[@mark.node] || EMPTY_NAMES
105
+ end
106
+
107
+ # Whether the closure of the module `raw` names from the marked node cannot answer the name.
108
+ def closure_clean?(raw)
109
+ resolved = @resolver.resolve(@mark.node, raw)
110
+ case resolved
111
+ when String then project_closure_clean?(resolved)
112
+ when Array then resolved.all? { |name| project_closure_clean?(name) }
113
+ else external_clean?(@resolver.candidates(@mark.node, raw), raw)
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def project_closure_clean?(module_name)
120
+ chain = ResolutionChain.for(@scope, module_name, :instance, @flavor)
121
+ return false if chain.truncated?
122
+
123
+ chain.entries.all? do |entry|
124
+ entry.external? ? external_clean?(entry.candidates, entry.raw) : project_clean?(entry)
125
+ end
126
+ end
127
+
128
+ def project_clean?(entry)
129
+ owner = entry.name
130
+ @edges << [:class, owner] << [:method, "#{owner}##{@name}"]
131
+ return false if Scope::DiscoveryIndex.rewritten_surface?(@scope.parameter_envelopes_of(owner))
132
+ return false if wildcard_listed?(owner)
133
+ return false if @scope.discovered_method?(owner, :method_missing, :instance)
134
+ return false if HOOKS.any? { |hook| @scope.discovered_method?(owner, hook, :singleton) }
135
+
136
+ !records_name?(owner)
137
+ end
138
+
139
+ def wildcard_listed?(owner)
140
+ sides = @scope.discovery.unpositioned_mixins[owner]
141
+ return false if sides.nil?
142
+
143
+ [:include, @mark.kind].uniq.any? { |side| sides[side]&.include?(WILDCARD) }
144
+ end
145
+
146
+ def records_name?(owner)
147
+ @scope.discovered_method?(owner, @name, :instance) || @scope.discovered_method?(owner, @name, :singleton) ||
148
+ !@scope.discovered_method_visibility(owner, @name).nil?
149
+ end
150
+
151
+ # An ancestor the project does not declare: only a module RBS knows, whose declaration lacks the name,
152
+ # is evidence it does not define it (`SourceArity#external_mixin_lacks_method?`).
153
+ def external_clean?(candidates, raw)
154
+ @edges << [:external, raw.to_s.split("::").last]
155
+ Relevance.external_lacks?(@scope, candidates, @name)
156
+ end
157
+ end
158
+ private_constant :Context
159
+
160
+ EMPTY_NAMES = [].freeze
161
+ private_constant :EMPTY_NAMES
162
+ end
163
+ end
164
+ end
165
+ end