rigortype 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  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/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -25,6 +25,9 @@ require_relative "check_rules/ivar_write_collector"
25
25
  require_relative "check_rules/main_pass_collector"
26
26
  require_relative "check_rules/void_value_use_collector"
27
27
  require_relative "check_rules/self_closedness_scanner"
28
+ require_relative "check_rules/source_arity"
29
+ require_relative "check_rules/lexical_method_sites"
30
+ require_relative "check_rules/special_global_setters"
28
31
 
29
32
  module Rigor
30
33
  module Analysis
@@ -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,7 +214,7 @@ 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
219
  shadow_verify_node_collectors(path, root, scope_index, collectors) if ENV["RIGOR_SHADOW_RULE_WALK"]
201
220
  collectors
@@ -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
1602
 
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
-
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
@@ -2503,7 +2903,7 @@ module Rigor
2503
2903
  return nil if method_def.nil?
2504
2904
 
2505
2905
  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)
2906
+ mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides, scope_index)
2507
2907
  return nil if mismatch.nil?
2508
2908
  return nil if inferred_param_mismatch_verdict?(call_node, mismatch, scope)
2509
2909
 
@@ -2529,12 +2929,15 @@ module Rigor
2529
2929
  # overload rejects) plus, on non-coerce methods, the non-nil channel
2530
2930
  # (a single-concrete-class argument every overload rejects). See
2531
2931
  # {#multi_overload_argument_mismatch}.
2532
- def argument_mismatch(method_types, call_node, scope, param_overrides)
2932
+ #
2933
+ # Each argument is typed from its own index entry, which is the call's unless an earlier operand moved
2934
+ # the scope it was entered from (issue #1256): `(k = 0; 5)[k]` checks `k` as the `0` it holds there.
2935
+ def argument_mismatch(method_types, call_node, scope, param_overrides, scope_index)
2533
2936
  mismatch =
2534
2937
  if method_types.size == 1
2535
- first_argument_mismatch(method_types.first, call_node, scope, param_overrides)
2938
+ first_argument_mismatch(method_types.first, call_node, scope, param_overrides, scope_index)
2536
2939
  else
2537
- multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides)
2940
+ multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides, scope_index)
2538
2941
  end
2539
2942
  return nil if mismatch && keyword_bearing?(method_types) && nil_tainted?(mismatch[:actual])
2540
2943
 
@@ -2586,28 +2989,35 @@ module Rigor
2586
2989
  # stays deferred), and decides acceptance on the RBS param type
2587
2990
  # ({#param_accepts_arg_class?}) so it sees through the `int` / `string`
2588
2991
  # interface-aliases the translator degrades.
2589
- def multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides)
2992
+ def multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides, scope_index)
2590
2993
  functions = method_types.map(&:type)
2591
2994
  return nil unless functions.all? { |function| argument_check_eligible?(function) }
2592
2995
 
2593
2996
  coerce_method = COERCE_DISPATCH_METHODS.include?(call_node.name)
2594
2997
  arguments = call_node.arguments&.arguments || []
2595
2998
  arguments.each_with_index do |arg, index|
2596
- arg_type = scope.type_of(arg)
2597
2999
  params = checkable_overload_params(method_types, index, param_overrides, scope)
2598
3000
  next if params.nil?
2599
3001
 
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
3002
+ mismatch = overload_argument_mismatch(arg, params, param_overrides,
3003
+ argument_scope(arg, scope, scope_index), coerce_method)
2606
3004
  return mismatch if mismatch
2607
3005
  end
2608
3006
  nil
2609
3007
  end
2610
3008
 
3009
+ # The mismatch (or nil) for one argument against the parameter every overload has at its position. Issue
3010
+ # #1362 — a builtin global's seed, or its copy, is judged by the file's writes to the global.
3011
+ def overload_argument_mismatch(arg, params, param_overrides, arg_scope, coerce_method)
3012
+ written = DeclarationSourcedGuard.written_type(arg, arg_scope)
3013
+ arg_type = written || arg_scope.type_of(arg)
3014
+ if nil_member?(arg_type) # pure nil only — not a `T | nil` union
3015
+ nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, arg_scope, written: !written.nil?)
3016
+ elsif !coerce_method
3017
+ non_nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, arg_scope)
3018
+ end
3019
+ end
3020
+
2611
3021
  # The parameter set at `index` that a verdict may rest on, or nil when there is none — the two
2612
3022
  # reasons being arity divergence (an overload with no param there, which is `call.wrong-arity`'s
2613
3023
  # concern) and an overload whose param names a class carrying no definition ({#stub_typed_param?},
@@ -2623,8 +3033,8 @@ module Rigor
2623
3033
 
2624
3034
  # The nil channel: a pure `nil` argument no overload admits (ADR-58
2625
3035
  # 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)
3036
+ def nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope, written: false)
3037
+ return nil if !written && declaration_sourced_nil_argument?(arg, scope)
2628
3038
  return nil if params.any? { |param| param_admits_nil?(param, param_overrides, scope) }
2629
3039
 
2630
3040
  { node: arg, name: nil, expected: overload_param_expected_label(params), actual: arg_type }
@@ -2891,7 +3301,7 @@ module Rigor
2891
3301
  DeclarationSourcedGuard.marked?(arg, scope)
2892
3302
  end
2893
3303
 
2894
- def first_argument_mismatch(method_type, call_node, scope, param_overrides)
3304
+ def first_argument_mismatch(method_type, call_node, scope, param_overrides, scope_index)
2895
3305
  function = method_type.type
2896
3306
  return nil unless argument_check_eligible?(function)
2897
3307
 
@@ -2901,12 +3311,17 @@ module Rigor
2901
3311
  param = params[index]
2902
3312
  next if param.nil? # arity mismatch is the wrong-arity rule's concern.
2903
3313
 
2904
- mismatch = single_argument_mismatch(param, arg, scope, param_overrides)
3314
+ mismatch = single_argument_mismatch(param, arg, argument_scope(arg, scope, scope_index), param_overrides)
2905
3315
  return mismatch if mismatch
2906
3316
  end
2907
3317
  nil
2908
3318
  end
2909
3319
 
3320
+ # The scope `arg` is checked from: its own index entry ({#argument_mismatch}), else the call's.
3321
+ def argument_scope(arg, call_scope, scope_index)
3322
+ scope_index[arg] || call_scope
3323
+ end
3324
+
2910
3325
  # The mismatch (or nil) for one positional argument against one
2911
3326
  # parameter. The nil channel decides a pure `nil` argument on the RBS
2912
3327
  # parameter type — seeing through the `string` / `int` interface-alias
@@ -2917,6 +3332,17 @@ module Rigor
2917
3332
  def single_argument_mismatch(param, arg, scope, param_overrides)
2918
3333
  return nil if stub_typed_param?(param, param_overrides, scope)
2919
3334
 
3335
+ # Issue #1362 (ADR-58 parity, ADR-117 Decision point 2) — a read of a builtin global still on its declared
3336
+ # seed, or a local copied from one, is judged by the file's writes to the global, as the argument was
3337
+ # before the join: the declared members are not diagnostic fuel.
3338
+ written = DeclarationSourcedGuard.written_type(arg, scope)
3339
+ return written_argument_mismatch(param, arg, written, scope, param_overrides) if written
3340
+
3341
+ read_argument_mismatch(param, arg, scope, param_overrides)
3342
+ end
3343
+
3344
+ # {#single_argument_mismatch} for an argument that reads no builtin global's seed directly.
3345
+ def read_argument_mismatch(param, arg, scope, param_overrides)
2920
3346
  arg_type = scope.type_of(arg)
2921
3347
 
2922
3348
  if nil_member?(arg_type)
@@ -2930,10 +3356,42 @@ module Rigor
2930
3356
  return nil if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
2931
3357
  return nil if arg_type.is_a?(Type::Dynamic) || arg_type.is_a?(Type::Top)
2932
3358
  return nil unless argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
3359
+ # Issue #1362 — a value that mixes such a global with something else (`c ? $stdout : StringIO.new`) is judged
3360
+ # without the declared members the file never writes, as it was before the join.
3361
+ return nil if DeclarationSourcedGuard.declared_only_rejection?(arg_type, param_type, scope) do |stripped|
3362
+ !plain_argument_rejects?(param, stripped, scope, param_overrides)
3363
+ end
2933
3364
 
2934
3365
  { node: arg, name: param.name, expected: param_type, actual: arg_type }
2935
3366
  end
2936
3367
 
3368
+ # The mismatch for an argument judged by `written`, the file's writes to the builtin globals it reads the
3369
+ # seeds of, through the channels {#single_argument_mismatch} runs, without the ADR-58 withholding: before
3370
+ # the join the global carried no mark.
3371
+ def written_argument_mismatch(param, arg, written, scope, param_overrides)
3372
+ return nil unless plain_argument_rejects?(param, written, scope, param_overrides)
3373
+
3374
+ expected =
3375
+ if nil_member?(written)
3376
+ overload_param_expected_label([param])
3377
+ else
3378
+ param_overrides[param.name] || translate_param_type(param.type, scope.environment)
3379
+ end
3380
+ { node: arg, name: param.name, expected: expected, actual: written }
3381
+ end
3382
+
3383
+ # True when `param` rejects an argument of type `type` on the terms of {#single_argument_mismatch}'s nil and
3384
+ # non-nil channels, without the ADR-58 withholding.
3385
+ def plain_argument_rejects?(param, type, scope, param_overrides)
3386
+ return !param_admits_nil?(param, param_overrides, scope) if nil_member?(type)
3387
+
3388
+ param_type = param_overrides[param.name] || translate_param_type(param.type, scope.environment)
3389
+ return false if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
3390
+ return false if type.is_a?(Type::Dynamic) || type.is_a?(Type::Top)
3391
+
3392
+ Inference::Acceptance.accepts(param_type, type, mode: :gradual).no?
3393
+ end
3394
+
2937
3395
  # The parameter rejects the argument AND the rejection is not a
2938
3396
  # withheld declaration-sourced-nil case.
2939
3397
  def argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
@@ -3100,15 +3558,28 @@ module Rigor
3100
3558
  declared = declared_return_type(def_node, scope_index)
3101
3559
  return nil if declared.nil?
3102
3560
 
3103
- inferred = inner_scope.type_of(last_expr)
3561
+ # Issue #1362 — a body that ends on a builtin global still on its declared seed, or on a local copied from
3562
+ # one, is judged by the file's writes to the global, as for an argument ({#single_argument_mismatch}).
3563
+ inferred = DeclarationSourcedGuard.written_type(last_expr, inner_scope) || inner_scope.type_of(last_expr)
3104
3564
  return nil if dynamic_top?(inferred)
3105
3565
 
3106
3566
  severity = compare_return(declared, inferred)
3107
3567
  return nil if severity.nil?
3568
+ return nil if declared_only_return?(last_expr, inferred, declared, inner_scope)
3108
3569
 
3109
3570
  build_return_type_mismatch_diagnostic(path, def_node, declared, inferred, severity)
3110
3571
  end
3111
3572
 
3573
+ # Issue #1362 — a body that ends on a value mixing a builtin global's seed with something else is judged
3574
+ # without the declared members the file never writes, as for an argument.
3575
+ def declared_only_return?(last_expr, inferred, declared, scope)
3576
+ return false unless DeclarationSourcedGuard.global_sources(last_expr, scope).empty?
3577
+
3578
+ DeclarationSourcedGuard.declared_only_rejection?(inferred, declared, scope) do |stripped|
3579
+ compare_return(declared, stripped).nil?
3580
+ end
3581
+ end
3582
+
3112
3583
  # The body of a `def` is the last `Prism::StatementsNode` child (or a single expression for
3113
3584
  # one-liner defs). Take the last statement; that's the implicit return.
3114
3585
  def body_last_expression(body)