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
@@ -6,6 +6,7 @@ require_relative "../reflection"
6
6
  require_relative "../source/node_walker"
7
7
  require_relative "../source/constant_path"
8
8
  require_relative "../inference/singleton_object_constant"
9
+ require_relative "shadow_harness"
9
10
  require_relative "../type"
10
11
  require_relative "diagnostic"
11
12
  require_relative "dependency_recorder"
@@ -25,6 +26,9 @@ require_relative "check_rules/ivar_write_collector"
25
26
  require_relative "check_rules/main_pass_collector"
26
27
  require_relative "check_rules/void_value_use_collector"
27
28
  require_relative "check_rules/self_closedness_scanner"
29
+ require_relative "check_rules/source_arity"
30
+ require_relative "check_rules/lexical_method_sites"
31
+ require_relative "check_rules/special_global_setters"
28
32
 
29
33
  module Rigor
30
34
  module Analysis
@@ -57,10 +61,9 @@ module Rigor
57
61
  # ADR-87 WD4 — the canonical rule-id table lives in the light `check_rules/rule_ids.rb` (required at the
58
62
  # top of this file) so {Analysis::RuleCatalog} can read it without loading the engine.
59
63
 
60
- # ADR-35 slice 1 — bound for the `def.override-visibility-reduced`
61
- # ancestor walk, and the public > protected > private ordering
62
- # used to decide whether an override reduces visibility.
63
- OVERRIDE_ANCESTOR_WALK_LIMIT = 100
64
+ # ADR-35 slice 1 — the public > protected > private ordering used to decide whether an override reduces
65
+ # visibility. The override rules' ancestor walk is bounded by `Scope::ANCESTOR_WALK_LIMIT`, the
66
+ # resolution chain's.
64
67
  VISIBILITY_RANK = { public: 2, protected: 1, private: 0 }.freeze
65
68
 
66
69
  # Resolves a user-supplied rule token (`undefined-method`,
@@ -135,24 +138,37 @@ module Rigor
135
138
  # (`diagnose`'s `Source::NodeWalker.each` `case`), now invoked by
136
139
  # {MainPassCollector} on the shared {RuleWalk}. Returns the
137
140
  # diagnostics for one node, in the same emission order as before.
138
- def main_pass_node_diagnostics(path, node, scope_index)
141
+ def main_pass_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil)
139
142
  case node
140
143
  when Prism::CallNode
141
- call_node_diagnostics(path, node, scope_index)
144
+ call_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites)
142
145
  when Prism::DefNode
143
146
  [
144
- return_type_mismatch_diagnostic(path, node, scope_index),
147
+ refinement_aware_return_type_mismatch(path, node, scope_index, lexical_sites),
145
148
  override_visibility_diagnostic(path, node, scope_index),
146
149
  override_return_widened_diagnostic(path, node, scope_index),
147
150
  override_param_narrowed_diagnostic(path, node, scope_index)
148
151
  ].compact
149
152
  when Prism::IfNode, Prism::UnlessNode
150
153
  [unreachable_branch_diagnostic(path, node, scope_index)].compact
154
+ when Prism::GlobalVariableWriteNode, Prism::GlobalVariableOperatorWriteNode, Prism::MultiWriteNode
155
+ special_global_write_diagnostics(path, node, scope_index, lexical_sites)
151
156
  else
152
157
  []
153
158
  end
154
159
  end
155
160
 
161
+ # Issue #1120, maintainer ruling a′ — a `def` in a `refine X do … end` body redefines X's method by design, so
162
+ # X's declared return for the name is not its contract and a mismatch against it is not reported. Every other
163
+ # check in the body stands. The refine-body test runs only once a mismatch would report, so a file pays the
164
+ # {LexicalMethodSites} walk only then.
165
+ def refinement_aware_return_type_mismatch(path, node, scope_index, lexical_sites)
166
+ diagnostic = return_type_mismatch_diagnostic(path, node, scope_index)
167
+ return diagnostic if diagnostic.nil? || lexical_sites.nil?
168
+
169
+ lexical_sites.refinement_def?(node) ? nil : diagnostic
170
+ end
171
+
156
172
  # Constructs the fresh, unpopulated built-in collector set keyed by
157
173
  # role, including the main pass. Split out so the converged walk
158
174
  # (ADR-53 B4) can build the collectors, drive them via a
@@ -160,9 +176,12 @@ module Rigor
160
176
  # traversal, and hand the populated set back to {.diagnose} as
161
177
  # `node_collectors:`. The main pass needs `path` because its per-node
162
178
  # diagnostics carry it (ADR-53 B3c hosts it on the same walk).
163
- def build_node_collectors(path, scope_index)
179
+ def build_node_collectors(path, scope_index, root = nil)
180
+ eval_ranges = receiver_eval_block_ranges(root)
181
+ lexical_sites = LexicalMethodSites.new(root)
182
+ main_pass = ->(node) { main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites) }
164
183
  {
165
- main_pass: MainPassCollector.new(->(node) { main_pass_node_diagnostics(path, node, scope_index) }),
184
+ main_pass: MainPassCollector.new(main_pass),
166
185
  void_value_use: VoidValueUseCollector.new(scope_index),
167
186
  always_truthy: AlwaysTruthyConditionCollector.new(scope_index),
168
187
  unreachable_clauses: UnreachableClauseCollector.new(scope_index),
@@ -195,9 +214,9 @@ module Rigor
195
214
  # divergence aborts the run — the corpus-scale half of the
196
215
  # equivalence harness (the curated half is `rule_walk_equivalence_spec`).
197
216
  def run_node_collectors(path, root, scope_index)
198
- collectors = build_node_collectors(path, scope_index)
217
+ collectors = build_node_collectors(path, scope_index, root)
199
218
  RuleWalk.run(root, collectors.values)
200
- shadow_verify_node_collectors(path, root, scope_index, collectors) if ENV["RIGOR_SHADOW_RULE_WALK"]
219
+ shadow_verify_node_collectors(path, root, scope_index, collectors) if ShadowHarness.enabled?
201
220
  collectors
202
221
  end
203
222
 
@@ -239,8 +258,10 @@ module Rigor
239
258
  # produces them on the shared walk.
240
259
  def main_pass_oracle(path, root, scope_index)
241
260
  diagnostics = []
261
+ eval_ranges = receiver_eval_block_ranges(root)
262
+ lexical_sites = LexicalMethodSites.new(root)
242
263
  Source::NodeWalker.each(root) do |node|
243
- diagnostics.concat(main_pass_node_diagnostics(path, node, scope_index))
264
+ diagnostics.concat(main_pass_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites))
244
265
  end
245
266
  diagnostics
246
267
  end
@@ -260,10 +281,10 @@ module Rigor
260
281
  shadow_verify_node_collectors(path, root, scope_index, collectors)
261
282
  end
262
283
 
263
- def call_node_diagnostics(path, node, scope_index)
284
+ def call_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil)
264
285
  [
265
- undefined_method_diagnostic(path, node, scope_index),
266
- unresolved_toplevel_diagnostic(path, node, scope_index),
286
+ undefined_method_diagnostic(path, node, scope_index, lexical_sites),
287
+ unresolved_toplevel_diagnostic(path, node, scope_index, eval_ranges),
267
288
  wrong_arity_diagnostic(path, node, scope_index),
268
289
  argument_type_diagnostic(path, node, scope_index),
269
290
  nil_receiver_diagnostic(path, node, scope_index),
@@ -657,7 +678,7 @@ module Rigor
657
678
  class << self
658
679
  private
659
680
 
660
- def undefined_method_diagnostic(path, call_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
681
+ def undefined_method_diagnostic(path, call_node, scope_index, lexical_sites = nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
661
682
  return nil if call_node.receiver.nil?
662
683
 
663
684
  scope = scope_index[call_node]
@@ -665,10 +686,20 @@ module Rigor
665
686
 
666
687
  # The two exemptions that hold whatever shape the receiver has. Ahead of the receiver-shape
667
688
  # branch below because a plugin is consulted ONCE for the whole receiver, union included, so the
668
- # gate belongs where that one consult is rather than duplicated into each shape's path. On the
669
- # union side that is placement, not demonstrated coverage: no fixture reaches the gate through
670
- # `union_undefined_method_diagnostic` today — every plugin-typed union anyone has built carries a
671
- # `Singleton` arm, which `union_arm_blocks_undefined_fire?` declines on outright.
689
+ # gate belongs where that one consult is rather than duplicated into each shape's path.
690
+ #
691
+ # The union side of that placement IS reachable, contrary to what this comment used to say. The
692
+ # shape needs a non-nil union of at least two RBS-DECLARED classes — an undeclared arm is an
693
+ # ADR-26 open receiver and `union_arm_blocks_undefined_fire?` bails before the method lookup,
694
+ # which is why the earlier probes read silent and were misread as the gate being unreachable.
695
+ # `spec/integration/composite_receiver_plugin_typed_suppression_spec.rb` builds it. What keeps
696
+ # the gate from firing there today is that the dispatcher no longer records a plugin-typed call
697
+ # it declined (#1101), not that no such union can be built.
698
+ #
699
+ # Note what the gate would do if it were reached: the scalar `Ledger#settle` a plugin declares
700
+ # is exempt here, while `(Ledger | Invoice)#settle` is not, because the union rule's own
701
+ # `method_present_anywhere?` reads RBS and project source and never asks the registry. Closing
702
+ # that asymmetry is #653's union coverage, not this rule's placement.
672
703
  # See {#call_site_exempt?}.
673
704
  return nil if call_site_exempt?(call_node, scope)
674
705
 
@@ -739,7 +770,7 @@ module Rigor
739
770
  method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
740
771
  return nil if method_def
741
772
 
742
- return nil if last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind)
773
+ return nil if last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind, lexical_sites)
743
774
 
744
775
  definition_site = project_definition_site(scope, class_name, call_node.name, kind)
745
776
  # Issue #735 — the site is evidence, and whose RBS this is decides what it is evidence OF. On a
@@ -754,9 +785,54 @@ module Rigor
754
785
  build_undefined_method_diagnostic(path, call_node, receiver_type, definition_site, class_name)
755
786
  end
756
787
 
757
- # The two probes that run only once every cheaper answer has come back "absent", kept together
788
+ # Issue #1120 — the method exists at THIS call site but not everywhere: a refinement whose `using` is in
789
+ # effect here, or a `def o.m` on the receiver local in this scope. Asked from {#last_resort_surface_answers?},
790
+ # once every project-wide table has come back "absent", so the file walk behind {LexicalMethodSites} runs
791
+ # only for a call about to fire.
792
+ # A refinement answers instance-side receivers only: a refined singleton (`refine X.singleton_class`) names
793
+ # no constant target, so nothing records it.
794
+ def lexically_defined_method?(class_name, call_node, scope, kind, lexical_sites)
795
+ return false if lexical_sites.nil?
796
+ return true if lexical_sites.singleton_local_def?(call_node)
797
+ return false unless kind == :instance
798
+
799
+ # ADR-46 — the answer below is a function of every refinement of this name in the project, so the
800
+ # consumer depends on the name whichever way it answers; a refine body edited in another file must
801
+ # re-check it (`IncrementalSession#refinement_affected`).
802
+ DependencyRecorder.read_name(:refinement, call_node.name) if DependencyRecorder.active?
803
+ modules = refining_modules(scope, class_name, call_node.name)
804
+ !modules.nil? && lexical_sites.refinement_active?(call_node, modules)
805
+ end
806
+
807
+ # Issue #1120 — every module that refines `method_name` into `class_name` or one of its ancestors (a
808
+ # refinement of `Object` reaches a `String` receiver), or nil when none does. The table is empty on a
809
+ # project that refines nothing, which answers without a lookup.
810
+ def refining_modules(scope, class_name, method_name)
811
+ refinements = scope.discovered_refinements
812
+ return nil if refinements.empty?
813
+
814
+ modules = nil
815
+ refinements.each do |refined, methods|
816
+ names = methods[method_name]
817
+ next if names.nil? || !refined_receiver_class?(scope, class_name, refined)
818
+
819
+ (modules ||= []).concat(names)
820
+ end
821
+ modules
822
+ end
823
+
824
+ def refined_receiver_class?(scope, class_name, refined)
825
+ return true if class_name == refined
826
+
827
+ environment = scope.environment
828
+ !environment.nil? && environment.class_ordering(class_name, refined) == :subclass
829
+ end
830
+
831
+ # The probes that run only once every cheaper answer has come back "absent", kept together
758
832
  # because they share that position and nothing else.
759
833
  #
834
+ # - Issue #1120 — a method that exists at this call site only: a refinement in effect here, or a
835
+ # singleton `def` on the receiver local ({#lexically_defined_method?}).
760
836
  # - Issue #739 — an instance-side MODULE receiver. A value typed as a mixin module is an instance of
761
837
  # some class that includes it, and that class contributes an arbitrary surface: inside
762
838
  # `module Attachable::InstanceMethods`, `self.project` is defined by the ActiveRecord model that
@@ -777,7 +853,8 @@ module Rigor
777
853
  # it"; a project ancestor still might, and the typer has already resolved the site through
778
854
  # exactly this walk. It is last because it is the only probe here that walks the class graph
779
855
  # (see {#ancestry_declares_method?} for why that placement is load-bearing, not tidiness).
780
- def last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind)
856
+ def last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind, lexical_sites = nil)
857
+ return true if lexically_defined_method?(class_name, call_node, scope, kind, lexical_sites)
781
858
  return true if module_mixin_receiver?(receiver_type, scope)
782
859
  return true if mixin_self_class_receiver?(call_node, scope)
783
860
  return true if unknown_mixin_includer?(class_name, scope)
@@ -933,7 +1010,7 @@ module Rigor
933
1010
  # Authored severity is `:warning`; the severity profile
934
1011
  # remaps it (`strict` → `:error`, `balanced` →
935
1012
  # `:warning`, `lenient` → `:off` / suppressed).
936
- def unresolved_toplevel_diagnostic(path, call_node, scope_index)
1013
+ def unresolved_toplevel_diagnostic(path, call_node, scope_index, eval_ranges = nil)
937
1014
  return nil unless call_node.receiver.nil?
938
1015
 
939
1016
  scope = scope_index[call_node]
@@ -944,10 +1021,51 @@ module Rigor
944
1021
  return nil if scope.top_level_def_for(name)
945
1022
  return nil if source_declared_method?(scope, "Object", name, :instance)
946
1023
  return nil if Rigor::Reflection.instance_method_definition("Object", name, scope: scope)
1024
+ # `Target.class_eval { def added = 1; def use_added = added }` files the defs on Target
1025
+ # but leaves the eval body with a nil `self_type`, so `toplevel?` still holds. An eval
1026
+ # body is morally a class body, so ADR-34 stays silent there — including on a genuinely
1027
+ # undefined name inside the block. Ranges are computed once per file.
1028
+ return nil if call_inside_receiver_eval_ranges?(eval_ranges, call_node)
947
1029
 
948
1030
  build_unresolved_toplevel_diagnostic(path, call_node)
949
1031
  end
950
1032
 
1033
+ # ScopeIndexer keeps RECEIVER_EVAL_CALLS private; this list is the same family
1034
+ # (`class_eval` / `module_eval` / `class_exec` / `module_exec` / `instance_eval` /
1035
+ # `instance_exec`). Used only to decline ADR-34 inside those blocks.
1036
+ RECEIVER_EVAL_CALL_NAMES = %i[
1037
+ class_eval module_eval class_exec module_exec instance_eval instance_exec
1038
+ ].freeze
1039
+ private_constant :RECEIVER_EVAL_CALL_NAMES
1040
+ EMPTY_EVAL_RANGES = [].freeze
1041
+ private_constant :EMPTY_EVAL_RANGES
1042
+
1043
+ def receiver_eval_block_ranges(root)
1044
+ return EMPTY_EVAL_RANGES if root.nil?
1045
+
1046
+ ranges = []
1047
+ Source::NodeWalker.each(root) do |node|
1048
+ next unless node.is_a?(Prism::CallNode)
1049
+ next unless RECEIVER_EVAL_CALL_NAMES.include?(node.name)
1050
+
1051
+ block = node.block
1052
+ next unless block.is_a?(Prism::BlockNode)
1053
+
1054
+ loc = block.location
1055
+ ranges << [loc.start_offset, loc.end_offset]
1056
+ end
1057
+ ranges
1058
+ end
1059
+
1060
+ def call_inside_receiver_eval_ranges?(eval_ranges, call_node)
1061
+ return false if eval_ranges.nil? || eval_ranges.empty?
1062
+
1063
+ loc = call_node.location
1064
+ eval_ranges.any? do |start_offset, end_offset|
1065
+ loc.start_offset >= start_offset && loc.end_offset <= end_offset
1066
+ end
1067
+ end
1068
+
951
1069
  def build_unresolved_toplevel_diagnostic(path, call_node)
952
1070
  Diagnostic.from_message_loc(
953
1071
  call_node,
@@ -1449,7 +1567,7 @@ module Rigor
1449
1567
  # by `undefined_method_diagnostic`; it returns nil
1450
1568
  # when the call's receiver / RBS coverage / call shape
1451
1569
  # disqualifies the rule.
1452
- # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1570
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1453
1571
  def wrong_arity_diagnostic(path, call_node, scope_index)
1454
1572
  return nil if call_node.receiver.nil?
1455
1573
  return nil unless plain_positional_call?(call_node)
@@ -1481,25 +1599,110 @@ module Rigor
1481
1599
  # `.new` against the real `Struct.new(*Symbol)` signature is a
1482
1600
  # false positive. Skip arity-checking the chained position.
1483
1601
  return nil if anonymous_struct_new_call?(call_node, class_name, kind)
1484
- return nil if scope.discovered_method?(class_name, call_node.name, kind)
1485
-
1486
- return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
1487
- return nil unless definition_available?(receiver_type, class_name, scope)
1488
1602
 
1489
- method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
1490
- return nil if method_def.nil?
1491
- return nil if undeclared_constructor?(class_name, call_node, kind, method_def)
1492
-
1493
- arity_envelope = compute_arity_envelope(method_def)
1603
+ arity_envelope = arity_envelope_for(receiver_type, class_name, call_node, scope, kind)
1494
1604
  return nil if arity_envelope.nil?
1495
1605
 
1496
1606
  actual = (call_node.arguments&.arguments || []).size
1497
- min, max = arity_envelope
1498
- return nil if actual.between?(min, max)
1607
+ min, max, source_arity = arity_envelope
1608
+ if actual.between?(min, max)
1609
+ source_arity&.settle_by_definitions
1610
+ return nil
1611
+ end
1612
+ # Issue #992 — a source envelope's remaining declines can only withhold, so they run only now.
1613
+ source_arity&.settle_by_walk
1614
+ return nil if source_arity && !source_arity.authoritative?(class_name)
1499
1615
 
1500
1616
  build_arity_diagnostic(path, call_node, class_name, min, max, actual)
1501
1617
  end
1502
1618
 
1619
+ # The `[min, max]` the call is checked against — a declared signature's, or (issue #992) the project
1620
+ # `def`'s own when no signature declares the method, with the {SourceArity} that found it as a third
1621
+ # element — or nil when neither may be trusted.
1622
+ #
1623
+ # Issue #991 — `discovered_method?` used to exempt the call outright before ever asking for a
1624
+ # declaration. `trustworthy_signature` is the same lookup `argument_type_diagnostic` treats as
1625
+ # authoritative over a source `def` (see its own comment beside that call), and the two rules must agree
1626
+ # on whose contract binds, so a declared method is checked against its declaration.
1627
+ def arity_envelope_for(receiver_type, class_name, call_node, scope, kind)
1628
+ if Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
1629
+ return nil unless definition_available?(receiver_type, class_name, scope)
1630
+
1631
+ method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
1632
+ if method_def && !Rigor::RbsExtended.inferred_signature?(method_def)
1633
+ return nil if undeclared_constructor?(class_name, call_node, kind, method_def)
1634
+
1635
+ return compute_arity_envelope(method_def)
1636
+ end
1637
+ return nil unless source_lane_open?(method_def, receiver_type, class_name, call_node, scope)
1638
+ end
1639
+
1640
+ source_arity_envelope(class_name, call_node, scope, kind)
1641
+ end
1642
+
1643
+ # Issue #992 — whether a class RBS knows may still be checked against its project `def`. Two lanes:
1644
+ #
1645
+ # - the member resolved and carries `%a{rigor:v1:inferred-signature}`: under ADR-93 the bundled
1646
+ # `rigor-rbs-inline` plugin emits a full skeleton for EVERY `def` in a file carrying at least one
1647
+ # annotation (#823), with every type slot defaulted, so nobody asserted anything about it. Its arity
1648
+ # is NOT read here — the `def`'s envelope is, through the same {SourceArity} walk as a method with no
1649
+ # member at all, so one `def` is never judged by two readings of its parameter list;
1650
+ # - no member resolved, and the class's declaration is the project's own `sig/`
1651
+ # (`project_declared_class?`): a partial sidecar of a project class whose `def` lives in source.
1652
+ #
1653
+ # Everything else declines. A bundled declaration (core / stdlib / gem) describes a class the project
1654
+ # does not own, so a project `def` on it is an ADR-17 monkey-patch that another library may patch too;
1655
+ # a lookup that raised, or a name an open receiver only inherited (`trustworthy_signature` answered
1656
+ # nil for those while a raw lookup does not), is not "no member".
1657
+ def source_lane_open?(method_def, receiver_type, class_name, call_node, scope)
1658
+ return true if method_def
1659
+
1660
+ lookup_method(receiver_type, class_name, call_node.name, scope).nil? &&
1661
+ Rigor::Reflection.project_declared_class?(class_name, scope: scope)
1662
+ end
1663
+
1664
+ # Issue #992 — stage 1 of {SourceArity}: the nearest project definition's envelope, after the
1665
+ # receiver-level declines that need no walk. Stage 2 (`SourceArity#authoritative?`) runs only once the
1666
+ # count is known to fall outside it.
1667
+ def source_arity_envelope(class_name, call_node, scope, kind)
1668
+ return nil unless scope.discovered_parameter_envelopes.key?(Scope::DiscoveryIndex::ENVELOPE_PROJECT_WIDE)
1669
+ return nil if unbounded_receiver_surface?(class_name, scope)
1670
+ return nil if plugin_typed_call?(call_node, scope)
1671
+ # An instance of a project module is an instance of whatever includes it (#739's reasoning, which
1672
+ # `module_mixin_receiver?` can only apply to a module RBS declares).
1673
+ return nil if kind == :instance && project_module?(class_name, scope)
1674
+ return nil if project_module_class_receiver?(call_node, scope)
1675
+
1676
+ arity = SourceArity.new(scope, call_node.name, kind)
1677
+ envelope = arity.owner_envelope(class_name)
1678
+ if envelope.nil? || envelope[2]
1679
+ envelope.nil? && arity.ambiguous? ? arity.settle_by_walk : arity.settle_by_definitions
1680
+ return nil
1681
+ end
1682
+
1683
+ [envelope[0], envelope[1] || Float::INFINITY, arity]
1684
+ end
1685
+
1686
+ # `self.class.f(1)` inside a project module's instance method: the engine types `self.class` as the
1687
+ # module's singleton, but at runtime it is the includer's class, whose class methods are its own. The
1688
+ # source-lane twin of {#mixin_self_class_receiver?}, which can only see a module RBS declares.
1689
+ def project_module_class_receiver?(call_node, scope)
1690
+ receiver = call_node.receiver
1691
+ return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :class && receiver.arguments.nil?
1692
+ return false if receiver.receiver.nil?
1693
+
1694
+ inner = concrete_class_name(scope.type_of(receiver.receiver))
1695
+ !inner.nil? && project_module?(inner, scope)
1696
+ end
1697
+
1698
+ # Read off the table directly rather than through `Scope#parameter_envelopes_of`: a class does not turn
1699
+ # into a module without its declaration (and so the receiver's own typing) changing, so this answer
1700
+ # needs no ADR-46 class edge of its own.
1701
+ def project_module?(class_name, scope)
1702
+ bucket = scope.discovered_parameter_envelopes[class_name.to_s]
1703
+ !bucket.nil? && bucket.key?(Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
1704
+ end
1705
+
1503
1706
  # Issue #917 — no loaded RBS declares a constructor for this class. The definition builder
1504
1707
  # synthesizes `.new` from the nearest `initialize` in the ancestry, and when nothing declares
1505
1708
  # one that is `BasicObject#initialize: () -> void`, so `.new` derives a nullary envelope that
@@ -1640,7 +1843,10 @@ module Rigor
1640
1843
  # {DeclarationSourcedGuard} exactly what it asked before — but it
1641
1844
  # now asks it through the predicate the argument-type gates share
1642
1845
  # (issue #324), which is what keeps the two rules from drifting.
1643
- return nil if DeclarationSourcedGuard.marked?(call_node.receiver, scope)
1846
+ #
1847
+ # Issue #1362 — a local copied from a global still on its declared seed (`sep = $/`) is asked against the
1848
+ # file's writes to that global instead: its declared `nil` is withheld, and one the file writes still fires.
1849
+ return nil if DeclarationSourcedGuard.withholds_nil?(call_node.receiver, scope)
1644
1850
 
1645
1851
  # ADR-67 WD6b — an inferred-parameter receiver's type (incl. any nil constituent unioned in from a
1646
1852
  # nil call site) is an open-call-site lower bound; a possible-nil firing against it is an FP by
@@ -2165,6 +2371,200 @@ module Rigor
2165
2371
  )
2166
2372
  end
2167
2373
 
2374
+ # Issue #1367 (ADR-117 Decision point 1, WD2) — `global.write-type-mismatch` and `global.readonly-write`: a
2375
+ # write to a special global that the interpreter's setter rejects, so the write raises every time it runs.
2376
+ # The envelope is {SpecialGlobalSetters}: the setter, not the global's RBS declaration.
2377
+ #
2378
+ # - `global.readonly-write` needs no type. `$g = v`, `$g op= v` and a target of a multiple assignment
2379
+ # (`$g, x = …`, `*$g`) always write, so a write to a read-only special raises.
2380
+ # - `global.write-type-mismatch` checks `$g = v` only when `v` is a literal node
2381
+ # ({SpecialGlobalSetters.literal_class}), whose class the syntax fixes. Any other value is never reported,
2382
+ # whatever its inferred type: a class RBS declares without `write` or `to_str` does not prove the object
2383
+ # lacks it (a singleton `def`, an `extend`, or a run-time `alias to_str to_s`, as URI has).
2384
+ # - `$g ||= v` and `$g &&= v` write only when the current value is falsy (truthy), so neither rule checks
2385
+ # them, nor a `for` index or a `rescue => $g` reference.
2386
+ # - A special that any project file aliases (`alias $stdout $out`, either side) is exempt from both.
2387
+ def special_global_write_diagnostics(path, node, scope_index, lexical_sites)
2388
+ case node
2389
+ when Prism::GlobalVariableWriteNode
2390
+ [readonly_global_write_diagnostic(path, node, scope_index) ||
2391
+ global_write_type_diagnostic(path, node, scope_index, lexical_sites)].compact
2392
+ when Prism::GlobalVariableOperatorWriteNode
2393
+ [readonly_global_write_diagnostic(path, node, scope_index)].compact
2394
+ when Prism::MultiWriteNode
2395
+ multi_write_global_targets(node).filter_map do |target|
2396
+ readonly_global_write_diagnostic(path, target, scope_index)
2397
+ end
2398
+ else
2399
+ []
2400
+ end
2401
+ end
2402
+
2403
+ # The `GlobalVariableTargetNode`s a multiple assignment writes, through a splat (`*$g`) and nested
2404
+ # destructuring (`(a, $g), b = …`).
2405
+ def multi_write_global_targets(node)
2406
+ [*node.lefts, node.rest, *node.rights].flat_map do |target|
2407
+ case target
2408
+ when Prism::GlobalVariableTargetNode then [target]
2409
+ when Prism::SplatNode
2410
+ target.expression.is_a?(Prism::GlobalVariableTargetNode) ? [target.expression] : []
2411
+ when Prism::MultiTargetNode then multi_write_global_targets(target)
2412
+ else []
2413
+ end
2414
+ end
2415
+ end
2416
+
2417
+ def readonly_global_write_diagnostic(path, node, scope_index)
2418
+ return nil unless SpecialGlobalSetters.read_only?(node.name)
2419
+
2420
+ scope = scope_index[node]
2421
+ return nil if scope.nil? || global_aliased?(scope, node.name)
2422
+
2423
+ location = node.respond_to?(:name_loc) ? node.name_loc : node.location
2424
+ Diagnostic.from_location(
2425
+ location,
2426
+ rule: RULE_GLOBAL_READONLY_WRITE,
2427
+ path: path,
2428
+ message: "`#{node.name}' is a read-only variable; this write raises NameError at runtime",
2429
+ severity: :error
2430
+ )
2431
+ end
2432
+
2433
+ def global_write_type_diagnostic(path, node, scope_index, lexical_sites)
2434
+ contract = SpecialGlobalSetters.contract_for(node.name)
2435
+ return nil if contract.nil?
2436
+
2437
+ class_name = SpecialGlobalSetters.literal_class(node.value)
2438
+ return nil if class_name.nil?
2439
+
2440
+ scope = scope_index[node]
2441
+ return nil if scope.nil?
2442
+ return nil unless literal_rejected?(class_name, contract, node, scope, lexical_sites)
2443
+ return nil if global_aliased?(scope, node.name)
2444
+
2445
+ build_global_write_type_diagnostic(path, node, contract, class_name)
2446
+ end
2447
+
2448
+ # The project's {Inference::GlobalWriteCensus} with the `pre_eval:` files' own.
2449
+ def global_write_census(scope)
2450
+ census = scope.discovered_global_write_census
2451
+ pre_eval = scope.environment&.project_patched_methods&.write_census
2452
+ pre_eval.nil? || pre_eval.empty? ? census : census | pre_eval
2453
+ end
2454
+
2455
+ # Whether any project or `pre_eval:` file aliases `name`, on either side, which can make it another variable.
2456
+ def global_aliased?(scope, name)
2457
+ Inference::GlobalWriteCensus.aliased?(global_write_census(scope), name)
2458
+ end
2459
+
2460
+ # A literal is rejected when its class is none the setter takes and, for a setter that also converts
2461
+ # (`to_str`, `to_int`) or asks for `write`, the literal's object cannot answer that method.
2462
+ def literal_rejected?(class_name, contract, node, scope, lexical_sites)
2463
+ return false if contract.accepts_class?(class_name)
2464
+ return true if contract.conversion.nil?
2465
+
2466
+ !literal_may_answer?(class_name, contract.conversion, node, scope, lexical_sites)
2467
+ end
2468
+
2469
+ # The escape hatches through which an object answers a method its class does not define: the setter's
2470
+ # `respond_to?` and implicit conversion both consult them.
2471
+ GLOBAL_WRITE_HATCHES = %i[method_missing respond_to_missing? respond_to?].freeze
2472
+ private_constant :GLOBAL_WRITE_HATCHES
2473
+
2474
+ # Where RBS core declares the hatches every object inherits unchanged.
2475
+ DEFAULT_HATCH_OWNERS = %w[::BasicObject ::Kernel ::Object].freeze
2476
+ private_constant :DEFAULT_HATCH_OWNERS
2477
+
2478
+ # Whether the literal's object may answer `method_name` after all:
2479
+ #
2480
+ # - RBS gives its class the method, or a hatch of its own (a project `sig/` reopening or `include` counts);
2481
+ # - the program defines the method or a hatch anywhere, in any spelling, on any receiver
2482
+ # ({Inference::GlobalWriteCensus}): no census of where a definition lands can be complete
2483
+ # (`class << nil`, `K = Integer; class K`, `[Integer].each { |k| k.define_method(:write) }`);
2484
+ # - an ancestor's surface is rewritten from outside (`Integer.include(M)`, `Object.include(M)`, a string
2485
+ # `class_eval`: the `ENVELOPE_DYNAMIC_MARK`), or it or `Object` mixes in a module RBS does not rule out;
2486
+ # - for `write` only, a refinement that may add it is in effect at the write ({#refined_writer_in_effect?}).
2487
+ def literal_may_answer?(class_name, method_name, node, scope, lexical_sites)
2488
+ definition = Rigor::Reflection.instance_definition(class_name, scope: scope)
2489
+ return true if definition.nil?
2490
+ return true if definition.methods.key?(method_name) || overridden_hatch?(definition)
2491
+
2492
+ ancestors = definition.ancestors.ancestors.to_set { |ancestor| ancestor.name.to_s.delete_prefix("::") }
2493
+ names = [method_name, *GLOBAL_WRITE_HATCHES]
2494
+ census = global_write_census(scope)
2495
+ program_may_answer?(ancestors, names, census, scope) ||
2496
+ (method_name == :write && refined_writer_in_effect?(census, node, lexical_sites))
2497
+ end
2498
+
2499
+ def overridden_hatch?(definition)
2500
+ GLOBAL_WRITE_HATCHES.any? do |name|
2501
+ method = definition.methods[name]
2502
+ method && !DEFAULT_HATCH_OWNERS.include?(method.defined_in.to_s)
2503
+ end
2504
+ end
2505
+
2506
+ def program_may_answer?(ancestors, names, census, scope)
2507
+ return true if Inference::GlobalWriteCensus.defines_any_of?(census, names)
2508
+
2509
+ dynamic_mark = Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK
2510
+ return true if ancestors.any? { |owner| scope.parameter_envelopes_of(owner).key?(dynamic_mark) }
2511
+
2512
+ main_mixins = Inference::GlobalWriteCensus.main_mixins(census)
2513
+ main_mixins.any? { |mixin| rbs_mixin_may_answer?(mixin, names, scope) } ||
2514
+ mixin_may_answer?(ancestors, names, scope)
2515
+ end
2516
+
2517
+ # A module an ancestor of the literal's class mixes in through a class body (`class Integer; include
2518
+ # Writable; end`), followed through the modules it mixes in in turn. A project module answers when its
2519
+ # surface is rewritten beyond literal names (a `send(:include, …)`, a non-constant mixin); its literal
2520
+ # definitions are the census's. Any other module answers when RBS does not know it or declares one of
2521
+ # `names`.
2522
+ def mixin_may_answer?(ancestors, names, scope)
2523
+ queue = ancestors.flat_map { |owner| scope.includes_of(owner) }
2524
+ seen = Set.new
2525
+ until queue.empty?
2526
+ mixin = queue.shift
2527
+ next unless seen.add?(mixin)
2528
+
2529
+ bucket = scope.parameter_envelopes_of(mixin)
2530
+ if bucket.key?(Scope::DiscoveryIndex::ENVELOPE_MODULE_MARK)
2531
+ return true if bucket.key?(Scope::DiscoveryIndex::ENVELOPE_DYNAMIC_MARK)
2532
+
2533
+ queue.concat(scope.includes_of(mixin))
2534
+ elsif rbs_mixin_may_answer?(mixin, names, scope)
2535
+ return true
2536
+ end
2537
+ end
2538
+ false
2539
+ end
2540
+
2541
+ def rbs_mixin_may_answer?(mixin, names, scope)
2542
+ definition = Rigor::Reflection.instance_definition(mixin, scope: scope)
2543
+ definition.nil? || definition.methods.key?(names.first) || overridden_hatch?(definition)
2544
+ end
2545
+
2546
+ # For `write` only: some refinement may add `write` (a `def`, `define_method` or `alias_method` in a `refine`
2547
+ # block, or an `import_methods`), and a `using` is in effect at the write. Which class it refines is not asked:
2548
+ # the target may be computed (`[Array].each { |k| refine(k) { … } }`) or a constant alias (`T = Array;
2549
+ # refine(T)`), so a refined `write` on any class declines every literal. `respond_to?(:write)` honours an
2550
+ # active refinement on Ruby 4.0.5; a refined `respond_to_missing?` or `respond_to?`, and every implicit
2551
+ # conversion, do not, so a refinement that adds only those leaves the write reported.
2552
+ def refined_writer_in_effect?(census, node, lexical_sites)
2553
+ Inference::GlobalWriteCensus.refines_write?(census) &&
2554
+ (lexical_sites.nil? || lexical_sites.using_in_effect?(node))
2555
+ end
2556
+
2557
+ def build_global_write_type_diagnostic(path, node, contract, class_name)
2558
+ Diagnostic.from_name_loc(
2559
+ node,
2560
+ rule: RULE_GLOBAL_WRITE_TYPE_MISMATCH,
2561
+ path: path,
2562
+ message: "`#{node.name}' accepts only #{contract.accepts}; this write assigns " \
2563
+ "#{SpecialGlobalSetters.literal_description(class_name)}, which raises TypeError at runtime",
2564
+ severity: :error
2565
+ )
2566
+ end
2567
+
2168
2568
  # v0.1.2 — `flow.unreachable-branch`. Fires when an `IfNode` / `UnlessNode` whose predicate is a
2169
2569
  # literal `true` / `false` / `nil` (or a literal numeric / string / symbol whose Ruby truthiness is
2170
2570
  # known at-a-glance) has an observable dead branch. The diagnostic points at the dead branch (not
@@ -2231,6 +2631,9 @@ module Rigor
2231
2631
  # `Prism::SelfNode` are also skipped — Ruby 2.7+ permits `self.foo` for private methods.
2232
2632
  # - Receiver MUST resolve to a `Type::Nominal` so the rule has a single class identity to query.
2233
2633
  # Unions / Dynamic / shape carriers are skipped.
2634
+ # - Issue #1568 — a module the class PREPENDS sits ahead of it in Ruby's order, so a definition there
2635
+ # is what the call reaches; the class's own private `def` answers only when nothing prepended
2636
+ # defines the name ({#reached_definer_visibility}).
2234
2637
  def visibility_mismatch_diagnostic(path, call_node, scope_index)
2235
2638
  return nil unless explicit_non_self_receiver?(call_node.receiver)
2236
2639
 
@@ -2247,9 +2650,33 @@ module Rigor
2247
2650
  visibility = scope.discovered_method_visibility(receiver_type.class_name, call_node.name)
2248
2651
  return nil unless visibility == :private
2249
2652
 
2653
+ reached = reached_definer_visibility(scope, receiver_type.class_name, call_node.name)
2654
+ return nil if reached == :undecided
2655
+ return nil unless reached.nil? || reached == :private
2656
+
2250
2657
  build_visibility_mismatch_diagnostic(path, call_node, receiver_type)
2251
2658
  end
2252
2659
 
2660
+ # Issue #1568, ADR-119 C1c — the recorded visibility of the definition a call on `class_name` reaches: the
2661
+ # first project entry on the class's {Scope::ResolutionChain} from its head that records the name, so a
2662
+ # module the class PREPENDS answers ahead of the class. `Inference::DefinerResolution` reads it with the
2663
+ # `:visibility` question's default answer. It is `:undecided` wherever the chain does not settle the order
2664
+ # (an unpositioned or forked mixin, a conditional include) or an ancestor ahead of the definer may define
2665
+ # the name (one RBS does not know), and the caller declines; a definer whose visibility slot ADR-119's
2666
+ # producers contest is not alone (`DefinerResolution` asks the next candidate too), so it is `:undecided`
2667
+ # there without a read of the table here. nil is nothing recording the name at all, and the class's own
2668
+ # visibility stands. An ancestor the project does not declare is passed over, as the method lookup passes it.
2669
+ def reached_definer_visibility(scope, class_name, method_name)
2670
+ case Inference::DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :visibility)
2671
+ in Inference::DefinerResolution::Known(answer:)
2672
+ answer
2673
+ in Inference::DefinerResolution::UNKNOWN
2674
+ :undecided
2675
+ in Inference::DefinerResolution::ABSENT
2676
+ nil
2677
+ end
2678
+ end
2679
+
2253
2680
  def explicit_non_self_receiver?(receiver)
2254
2681
  return false if receiver.nil?
2255
2682
  return false if receiver.is_a?(Prism::SelfNode)
@@ -2503,7 +2930,7 @@ module Rigor
2503
2930
  return nil if method_def.nil?
2504
2931
 
2505
2932
  param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
2506
- mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides)
2933
+ mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides, scope_index)
2507
2934
  return nil if mismatch.nil?
2508
2935
  return nil if inferred_param_mismatch_verdict?(call_node, mismatch, scope)
2509
2936
 
@@ -2529,12 +2956,15 @@ module Rigor
2529
2956
  # overload rejects) plus, on non-coerce methods, the non-nil channel
2530
2957
  # (a single-concrete-class argument every overload rejects). See
2531
2958
  # {#multi_overload_argument_mismatch}.
2532
- def argument_mismatch(method_types, call_node, scope, param_overrides)
2959
+ #
2960
+ # Each argument is typed from its own index entry, which is the call's unless an earlier operand moved
2961
+ # the scope it was entered from (issue #1256): `(k = 0; 5)[k]` checks `k` as the `0` it holds there.
2962
+ def argument_mismatch(method_types, call_node, scope, param_overrides, scope_index)
2533
2963
  mismatch =
2534
2964
  if method_types.size == 1
2535
- first_argument_mismatch(method_types.first, call_node, scope, param_overrides)
2965
+ first_argument_mismatch(method_types.first, call_node, scope, param_overrides, scope_index)
2536
2966
  else
2537
- multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides)
2967
+ multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides, scope_index)
2538
2968
  end
2539
2969
  return nil if mismatch && keyword_bearing?(method_types) && nil_tainted?(mismatch[:actual])
2540
2970
 
@@ -2586,28 +3016,35 @@ module Rigor
2586
3016
  # stays deferred), and decides acceptance on the RBS param type
2587
3017
  # ({#param_accepts_arg_class?}) so it sees through the `int` / `string`
2588
3018
  # interface-aliases the translator degrades.
2589
- def multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides)
3019
+ def multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides, scope_index)
2590
3020
  functions = method_types.map(&:type)
2591
3021
  return nil unless functions.all? { |function| argument_check_eligible?(function) }
2592
3022
 
2593
3023
  coerce_method = COERCE_DISPATCH_METHODS.include?(call_node.name)
2594
3024
  arguments = call_node.arguments&.arguments || []
2595
3025
  arguments.each_with_index do |arg, index|
2596
- arg_type = scope.type_of(arg)
2597
3026
  params = checkable_overload_params(method_types, index, param_overrides, scope)
2598
3027
  next if params.nil?
2599
3028
 
2600
- mismatch =
2601
- if nil_member?(arg_type) # pure nil only — not a `T | nil` union
2602
- nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
2603
- elsif !coerce_method
2604
- non_nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
2605
- end
3029
+ mismatch = overload_argument_mismatch(arg, params, param_overrides,
3030
+ argument_scope(arg, scope, scope_index), coerce_method)
2606
3031
  return mismatch if mismatch
2607
3032
  end
2608
3033
  nil
2609
3034
  end
2610
3035
 
3036
+ # The mismatch (or nil) for one argument against the parameter every overload has at its position. Issue
3037
+ # #1362 — a builtin global's seed, or its copy, is judged by the file's writes to the global.
3038
+ def overload_argument_mismatch(arg, params, param_overrides, arg_scope, coerce_method)
3039
+ written = DeclarationSourcedGuard.written_type(arg, arg_scope)
3040
+ arg_type = written || arg_scope.type_of(arg)
3041
+ if nil_member?(arg_type) # pure nil only — not a `T | nil` union
3042
+ nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, arg_scope, written: !written.nil?)
3043
+ elsif !coerce_method
3044
+ non_nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, arg_scope)
3045
+ end
3046
+ end
3047
+
2611
3048
  # The parameter set at `index` that a verdict may rest on, or nil when there is none — the two
2612
3049
  # reasons being arity divergence (an overload with no param there, which is `call.wrong-arity`'s
2613
3050
  # concern) and an overload whose param names a class carrying no definition ({#stub_typed_param?},
@@ -2623,8 +3060,8 @@ module Rigor
2623
3060
 
2624
3061
  # The nil channel: a pure `nil` argument no overload admits (ADR-58
2625
3062
  # parity excuses a declaration-sourced ivar nil).
2626
- def nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
2627
- return nil if declaration_sourced_nil_argument?(arg, scope)
3063
+ def nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope, written: false)
3064
+ return nil if !written && declaration_sourced_nil_argument?(arg, scope)
2628
3065
  return nil if params.any? { |param| param_admits_nil?(param, param_overrides, scope) }
2629
3066
 
2630
3067
  { node: arg, name: nil, expected: overload_param_expected_label(params), actual: arg_type }
@@ -2891,7 +3328,7 @@ module Rigor
2891
3328
  DeclarationSourcedGuard.marked?(arg, scope)
2892
3329
  end
2893
3330
 
2894
- def first_argument_mismatch(method_type, call_node, scope, param_overrides)
3331
+ def first_argument_mismatch(method_type, call_node, scope, param_overrides, scope_index)
2895
3332
  function = method_type.type
2896
3333
  return nil unless argument_check_eligible?(function)
2897
3334
 
@@ -2901,12 +3338,17 @@ module Rigor
2901
3338
  param = params[index]
2902
3339
  next if param.nil? # arity mismatch is the wrong-arity rule's concern.
2903
3340
 
2904
- mismatch = single_argument_mismatch(param, arg, scope, param_overrides)
3341
+ mismatch = single_argument_mismatch(param, arg, argument_scope(arg, scope, scope_index), param_overrides)
2905
3342
  return mismatch if mismatch
2906
3343
  end
2907
3344
  nil
2908
3345
  end
2909
3346
 
3347
+ # The scope `arg` is checked from: its own index entry ({#argument_mismatch}), else the call's.
3348
+ def argument_scope(arg, call_scope, scope_index)
3349
+ scope_index[arg] || call_scope
3350
+ end
3351
+
2910
3352
  # The mismatch (or nil) for one positional argument against one
2911
3353
  # parameter. The nil channel decides a pure `nil` argument on the RBS
2912
3354
  # parameter type — seeing through the `string` / `int` interface-alias
@@ -2917,6 +3359,17 @@ module Rigor
2917
3359
  def single_argument_mismatch(param, arg, scope, param_overrides)
2918
3360
  return nil if stub_typed_param?(param, param_overrides, scope)
2919
3361
 
3362
+ # Issue #1362 (ADR-58 parity, ADR-117 Decision point 2) — a read of a builtin global still on its declared
3363
+ # seed, or a local copied from one, is judged by the file's writes to the global, as the argument was
3364
+ # before the join: the declared members are not diagnostic fuel.
3365
+ written = DeclarationSourcedGuard.written_type(arg, scope)
3366
+ return written_argument_mismatch(param, arg, written, scope, param_overrides) if written
3367
+
3368
+ read_argument_mismatch(param, arg, scope, param_overrides)
3369
+ end
3370
+
3371
+ # {#single_argument_mismatch} for an argument that reads no builtin global's seed directly.
3372
+ def read_argument_mismatch(param, arg, scope, param_overrides)
2920
3373
  arg_type = scope.type_of(arg)
2921
3374
 
2922
3375
  if nil_member?(arg_type)
@@ -2930,10 +3383,42 @@ module Rigor
2930
3383
  return nil if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
2931
3384
  return nil if arg_type.is_a?(Type::Dynamic) || arg_type.is_a?(Type::Top)
2932
3385
  return nil unless argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
3386
+ # Issue #1362 — a value that mixes such a global with something else (`c ? $stdout : StringIO.new`) is judged
3387
+ # without the declared members the file never writes, as it was before the join.
3388
+ return nil if DeclarationSourcedGuard.declared_only_rejection?(arg_type, param_type, scope) do |stripped|
3389
+ !plain_argument_rejects?(param, stripped, scope, param_overrides)
3390
+ end
2933
3391
 
2934
3392
  { node: arg, name: param.name, expected: param_type, actual: arg_type }
2935
3393
  end
2936
3394
 
3395
+ # The mismatch for an argument judged by `written`, the file's writes to the builtin globals it reads the
3396
+ # seeds of, through the channels {#single_argument_mismatch} runs, without the ADR-58 withholding: before
3397
+ # the join the global carried no mark.
3398
+ def written_argument_mismatch(param, arg, written, scope, param_overrides)
3399
+ return nil unless plain_argument_rejects?(param, written, scope, param_overrides)
3400
+
3401
+ expected =
3402
+ if nil_member?(written)
3403
+ overload_param_expected_label([param])
3404
+ else
3405
+ param_overrides[param.name] || translate_param_type(param.type, scope.environment)
3406
+ end
3407
+ { node: arg, name: param.name, expected: expected, actual: written }
3408
+ end
3409
+
3410
+ # True when `param` rejects an argument of type `type` on the terms of {#single_argument_mismatch}'s nil and
3411
+ # non-nil channels, without the ADR-58 withholding.
3412
+ def plain_argument_rejects?(param, type, scope, param_overrides)
3413
+ return !param_admits_nil?(param, param_overrides, scope) if nil_member?(type)
3414
+
3415
+ param_type = param_overrides[param.name] || translate_param_type(param.type, scope.environment)
3416
+ return false if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
3417
+ return false if type.is_a?(Type::Dynamic) || type.is_a?(Type::Top)
3418
+
3419
+ Inference::Acceptance.accepts(param_type, type, mode: :gradual).no?
3420
+ end
3421
+
2937
3422
  # The parameter rejects the argument AND the rejection is not a
2938
3423
  # withheld declaration-sourced-nil case.
2939
3424
  def argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
@@ -3100,15 +3585,28 @@ module Rigor
3100
3585
  declared = declared_return_type(def_node, scope_index)
3101
3586
  return nil if declared.nil?
3102
3587
 
3103
- inferred = inner_scope.type_of(last_expr)
3588
+ # Issue #1362 — a body that ends on a builtin global still on its declared seed, or on a local copied from
3589
+ # one, is judged by the file's writes to the global, as for an argument ({#single_argument_mismatch}).
3590
+ inferred = DeclarationSourcedGuard.written_type(last_expr, inner_scope) || inner_scope.type_of(last_expr)
3104
3591
  return nil if dynamic_top?(inferred)
3105
3592
 
3106
3593
  severity = compare_return(declared, inferred)
3107
3594
  return nil if severity.nil?
3595
+ return nil if declared_only_return?(last_expr, inferred, declared, inner_scope)
3108
3596
 
3109
3597
  build_return_type_mismatch_diagnostic(path, def_node, declared, inferred, severity)
3110
3598
  end
3111
3599
 
3600
+ # Issue #1362 — a body that ends on a value mixing a builtin global's seed with something else is judged
3601
+ # without the declared members the file never writes, as for an argument.
3602
+ def declared_only_return?(last_expr, inferred, declared, scope)
3603
+ return false unless DeclarationSourcedGuard.global_sources(last_expr, scope).empty?
3604
+
3605
+ DeclarationSourcedGuard.declared_only_rejection?(inferred, declared, scope) do |stripped|
3606
+ compare_return(declared, stripped).nil?
3607
+ end
3608
+ end
3609
+
3112
3610
  # The body of a `def` is the last `Prism::StatementsNode` child (or a single expression for
3113
3611
  # one-liner defs). Take the last statement; that's the implicit return.
3114
3612
  def body_last_expression(body)
@@ -3138,8 +3636,11 @@ module Rigor
3138
3636
  self_type = scope.self_type
3139
3637
  return nil unless self_type.respond_to?(:class_name)
3140
3638
 
3639
+ side = def_node.receiver.nil? ? receiverless_def_side(scope, self_type.class_name, def_node) : :singleton
3640
+ return nil if side.nil?
3641
+
3141
3642
  method_def =
3142
- if def_node.receiver.nil? && !singleton_context_def?(scope, self_type.class_name, def_node.name)
3643
+ if side == :instance
3143
3644
  Reflection.instance_method_definition(self_type.class_name, def_node.name, scope: scope)
3144
3645
  else
3145
3646
  Reflection.singleton_method_definition(self_type.class_name, def_node.name, scope: scope)
@@ -3153,14 +3654,26 @@ module Rigor
3153
3654
  declared_return_union(method_def, scope.environment)
3154
3655
  end
3155
3656
 
3156
- # A receiverless `def` inside `class << self` is a singleton def; the def NODE alone cannot say
3157
- # so, but the discovery walk recorded it under the singleton kind (and not the instance kind).
3158
- # Without this, `class << self; def load` compared its body against `Kernel#load: -> bool` — the
3159
- # inherited INSTANCE method — instead of its own `def self.load` signature. A name defined on
3160
- # both facets stays on the instance lookup (the pre-existing conservative reading).
3161
- def singleton_context_def?(scope, class_name, method_name)
3162
- scope.discovered_method?(class_name, method_name, :singleton) &&
3163
- !scope.discovered_method?(class_name, method_name, :instance)
3657
+ # Which side a receiverless `def` defines, from the indexer's own record of THAT node: the instance table
3658
+ # holding it makes it an instance def, the singleton table (`class << self` bodies, and a `def` under
3659
+ # `Const.instance_eval` that the indexer attributes to the singleton) a singleton def. A node neither table
3660
+ # holds (a `def` in a block whose `self` the engine does not model, such as `E.class_eval do` inside
3661
+ # `class << self`, a redefinition a later `def` replaced) or both do is `nil`, and the caller declines: no
3662
+ # world is consulted, but the record cannot say which signature the body answers to.
3663
+ def receiverless_def_side(scope, class_name, def_node)
3664
+ instance = same_def_node?(scope.user_def_for(class_name, def_node.name), def_node)
3665
+ singleton = same_def_node?(scope.singleton_def_for(class_name, def_node.name), def_node)
3666
+ return :instance if instance && !singleton
3667
+ return :singleton if singleton && !instance
3668
+
3669
+ nil
3670
+ end
3671
+
3672
+ # The tables hold the live node or one re-resolved from a `DefHandle`, so identity is the name and span.
3673
+ def same_def_node?(recorded, def_node)
3674
+ recorded.is_a?(Prism::DefNode) && recorded.name == def_node.name &&
3675
+ recorded.location.start_offset == def_node.location.start_offset &&
3676
+ recorded.location.end_offset == def_node.location.end_offset
3164
3677
  end
3165
3678
 
3166
3679
  # `type_vars:` — ADR-35 WD9 tier 1. When the caller supplies a generic-instantiation
@@ -3268,82 +3781,57 @@ module Rigor
3268
3781
  override_rank < parent_rank
3269
3782
  end
3270
3783
 
3271
- # Breadth-first walk of the project-discovered ancestor chain (included / prepended modules first,
3272
- # then the superclass — Ruby's MRO ordering), yielding each resolved ancestor class name
3273
- # nearest-first. Returns the first truthy value the block produces, or nil. Cross-file: the chain
3274
- # is followed through the scope tables the runner seeds from the project pre-pass (ADR-24 WD1).
3275
- # Cycle-guarded and node-count-capped. Mirrors `ExpressionTyper#resolve_user_def_through_ancestors`.
3276
- def each_project_ancestor(scope, class_name)
3277
- queue = ancestor_class_names(scope, class_name)
3278
- seen = { class_name.to_s => true }
3279
- visited = 0
3280
- until queue.empty?
3281
- current = queue.shift
3282
- next if current.nil? || seen[current]
3283
-
3284
- seen[current] = true
3285
- visited += 1
3286
- return nil if visited > OVERRIDE_ANCESTOR_WALK_LIMIT
3287
-
3288
- result = yield current
3289
- return result if result
3290
-
3291
- ancestor_class_names(scope, current).each { |name| queue.push(name) }
3292
- end
3293
- nil
3784
+ # The project ancestors whose definitions `class_name`'s own `def` OVERRIDES are the entries AFTER the
3785
+ # class on its {Scope::ResolutionChain}, nearest first; {#nearest_ancestor_visibility} and
3786
+ # {#nearest_ancestor_method_def} each read the first of them that answers their question through
3787
+ # `Inference::DefinerResolution` (ADR-119 C1c), `from:` the position after the class. Both are instance
3788
+ # side only: the singleton override rules do not exist, and the singleton side's own read lands with
3789
+ # ADR-119's singleton design.
3790
+ #
3791
+ # Issue #1568 — the walk this replaced was breadth-first from the class's direct ancestors, and those
3792
+ # include the modules the class PREPENDS. A prepended module sits BEFORE the class in Ruby's order: its
3793
+ # `def` overrides the class's, not the other way round, so `prepend P` with a public `P#foo` and a
3794
+ # private `C#foo` reported `C#foo` as reducing `P#foo`'s visibility. It also reached `Base` before a
3795
+ # module included through an included module (#1567's order).
3796
+ #
3797
+ # ADR-119 WD3 — for the two override lints *absent* always counts: a chain that does not settle (a mixin
3798
+ # whose position the tables cannot vouch for, a fork whose worlds disagree, a conditional or
3799
+ # block-scoped `include`), or an external ancestor ahead of the parent that RBS does not know or knows
3800
+ # and declares the name in, is `UNKNOWN`, and the lint stays silent rather than answer from the
3801
+ # breadth-first order the walk used before (`Scope::ResolutionChain::MasterOrder`) where the order is
3802
+ # not known. The two lints read only the `[owner, value]` of a `Known`.
3803
+ #
3804
+ # ADR-46 slice 3 — an ancestor the project does not declare ends the walk's reach into it, and a class
3805
+ # defined LATER under that name would change the answer; `DefinerResolution` files the negative class
3806
+ # edge of each external entry it tests.
3807
+ def overridden_ancestors_start(scope, class_name)
3808
+ own = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :methods).index_of(class_name.to_s)
3809
+ own && (own + 1)
3294
3810
  end
3295
3811
 
3296
3812
  # `[defining_class, visibility]` for the nearest user-source ancestor that defines an instance
3297
3813
  # method `method_name`, or nil.
3298
3814
  def nearest_ancestor_visibility(scope, class_name, method_name)
3299
- each_project_ancestor(scope, class_name) do |ancestor|
3300
- # Stop at the nearest ancestor that DEFINES the method; its visibility may be nil (unknown) —
3301
- # the caller treats unknown as "cannot prove a reduction" and stays silent. Never fabricate
3302
- # `:public` from a missing entry (that produced a large false-positive cluster on cross-file
3303
- # Rails concerns).
3304
- [ancestor, scope.discovered_method_visibility(ancestor, method_name)] if scope.user_def_for(ancestor,
3305
- method_name)
3306
- end
3307
- end
3308
-
3309
- # Direct ancestors of `class_name` as project-discovered, qualified names: included / prepended
3310
- # modules first, then the superclass. As-written names are resolved against the nesting the
3311
- # subclass's declaration header is written in (`Scope#ancestor_name_candidates`, the single owner of
3312
- # that order); names that resolve to no project class/module (RBS-known / third-party) are dropped.
3313
- def ancestor_class_names(scope, class_name)
3314
- names = []
3315
- scope.includes_of(class_name).each do |raw|
3316
- resolved = resolve_override_ancestor_name(scope, class_name, raw)
3317
- names << resolved if resolved
3318
- end
3319
- raw_super = scope.superclass_of(class_name)
3320
- if raw_super
3321
- resolved_super = resolve_override_ancestor_name(scope, class_name, raw_super)
3322
- names << resolved_super if resolved_super
3815
+ from = overridden_ancestors_start(scope, class_name)
3816
+ return nil if from.nil?
3817
+
3818
+ case Inference::DefinerResolution.resolve(scope, class_name, method_name, :instance,
3819
+ question: :override, from: from) do |ancestor|
3820
+ # Stop at the nearest ancestor that DEFINES the method; its visibility may be nil (unknown) —
3821
+ # the caller treats unknown as "cannot prove a reduction" and stays silent. Never fabricate
3822
+ # `:public` from a missing entry (that produced a large false-positive cluster on
3823
+ # cross-file Rails concerns). The `:unrecorded` marker stands for that nil in the answer.
3824
+ next unless scope.user_def_for(ancestor, method_name)
3825
+
3826
+ scope.discovered_method_visibility(ancestor, method_name) || :unrecorded
3827
+ end
3828
+ in Inference::DefinerResolution::Known(answer: [owner, visibility])
3829
+ [owner, visibility == :unrecorded ? nil : visibility]
3830
+ in Inference::DefinerResolution::UNKNOWN # the case/in contract needs one arm per answer
3831
+ nil
3832
+ in Inference::DefinerResolution::ABSENT # rubocop:disable Lint/DuplicateBranch -- see above
3833
+ nil
3323
3834
  end
3324
- names
3325
- end
3326
-
3327
- # Issue #732 — the predicate is asked of the SCOPE, not re-implemented here. This rule kept its own
3328
- # three-table copy until #723 widened the scope's with `discovered_methods` and left this one behind,
3329
- # so the two disagreed about exactly one shape (a project class with no instance def, no superclass
3330
- # and no include — `class Base; def self.build = :built; end`) with nothing observing the fork. #682
3331
- # made `Scope` the single owner of the candidate ORDER; this makes it the single owner of the
3332
- # predicate that filters those candidates too.
3333
- def resolve_override_ancestor_name(scope, subclass_qualified, raw_ancestor)
3334
- resolved = scope.ancestor_name_candidates(subclass_qualified, raw_ancestor)
3335
- .find { |candidate| scope.known_user_class?(candidate) }
3336
- return resolved if resolved
3337
-
3338
- # ADR-46 slice 3 — the override checker reads the class graph
3339
- # directly (not through the recorder's `Scope` choke points), and
3340
- # short-circuits when the ancestor resolves to no project class, so
3341
- # an incremental re-check has no edge telling it to re-check this
3342
- # subclass when that ancestor is later defined. Record a negative
3343
- # class edge (keyed on the unqualified name) so the appeared-class
3344
- # widening picks it up.
3345
- DependencyRecorder.read_missing(:class, raw_ancestor.to_s.split("::").last) if DependencyRecorder.active?
3346
- nil
3347
3835
  end
3348
3836
 
3349
3837
  def build_override_visibility_diagnostic(path, def_node, parent_class, parent_visibility, override_visibility)
@@ -3423,9 +3911,23 @@ module Rigor
3423
3911
  # `[defining_class, RBS::Definition::Method]` for the nearest project-discovered ancestor whose
3424
3912
  # RBS declares `method_name` (not the starting class's own declaration), or nil.
3425
3913
  def nearest_ancestor_method_def(scope, class_name, method_name)
3426
- each_project_ancestor(scope, class_name) do |ancestor|
3427
- method_def = safe_instance_method_definition(ancestor, method_name, scope)
3428
- [ancestor, method_def] if method_def && !defined_on?(method_def, class_name)
3914
+ from = overridden_ancestors_start(scope, class_name)
3915
+ return nil if from.nil?
3916
+
3917
+ case Inference::DefinerResolution.resolve(scope, class_name, method_name, :instance,
3918
+ question: :override, from: from) do |ancestor|
3919
+ method_def = safe_instance_method_definition(ancestor, method_name, scope)
3920
+ # The signature text is the answer two worlds compare: an `RBS::Definition::Method` has no
3921
+ # structural equality of its own.
3922
+ method_def.method_types.map(&:to_s) if method_def && !defined_on?(method_def, class_name)
3923
+ end
3924
+ in Inference::DefinerResolution::Known(owner:)
3925
+ method_def = safe_instance_method_definition(owner, method_name, scope)
3926
+ method_def && [owner, method_def]
3927
+ in Inference::DefinerResolution::UNKNOWN # the case/in contract needs one arm per answer
3928
+ nil
3929
+ in Inference::DefinerResolution::ABSENT # rubocop:disable Lint/DuplicateBranch -- see above
3930
+ nil
3429
3931
  end
3430
3932
  end
3431
3933