rigortype 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -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
@@ -83,11 +86,8 @@ module Rigor
83
86
  # have already produced `scope_index` through
84
87
  # `Rigor::Inference::ScopeIndexer.index(root, default_scope:)`.
85
88
  #
86
- # @param path [String] used to populate
89
+ # @param path — used to populate
87
90
  # `Diagnostic#path`; the rule does not open files.
88
- # @param root [Prism::Node]
89
- # @param scope_index [Hash{Prism::Node => Rigor::Scope}]
90
- # @return [Array<Rigor::Analysis::Diagnostic>]
91
91
  #
92
92
  # ADR-53 B4 — when `node_collectors` is supplied, the converged
93
93
  # {Plugin::NodeRuleWalk} traversal has already populated the built-in
@@ -138,24 +138,37 @@ module Rigor
138
138
  # (`diagnose`'s `Source::NodeWalker.each` `case`), now invoked by
139
139
  # {MainPassCollector} on the shared {RuleWalk}. Returns the
140
140
  # diagnostics for one node, in the same emission order as before.
141
- 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)
142
142
  case node
143
143
  when Prism::CallNode
144
- call_node_diagnostics(path, node, scope_index)
144
+ call_node_diagnostics(path, node, scope_index, eval_ranges, lexical_sites)
145
145
  when Prism::DefNode
146
146
  [
147
- return_type_mismatch_diagnostic(path, node, scope_index),
147
+ refinement_aware_return_type_mismatch(path, node, scope_index, lexical_sites),
148
148
  override_visibility_diagnostic(path, node, scope_index),
149
149
  override_return_widened_diagnostic(path, node, scope_index),
150
150
  override_param_narrowed_diagnostic(path, node, scope_index)
151
151
  ].compact
152
152
  when Prism::IfNode, Prism::UnlessNode
153
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)
154
156
  else
155
157
  []
156
158
  end
157
159
  end
158
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
+
159
172
  # Constructs the fresh, unpopulated built-in collector set keyed by
160
173
  # role, including the main pass. Split out so the converged walk
161
174
  # (ADR-53 B4) can build the collectors, drive them via a
@@ -163,9 +176,12 @@ module Rigor
163
176
  # traversal, and hand the populated set back to {.diagnose} as
164
177
  # `node_collectors:`. The main pass needs `path` because its per-node
165
178
  # diagnostics carry it (ADR-53 B3c hosts it on the same walk).
166
- 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) }
167
183
  {
168
- main_pass: MainPassCollector.new(->(node) { main_pass_node_diagnostics(path, node, scope_index) }),
184
+ main_pass: MainPassCollector.new(main_pass),
169
185
  void_value_use: VoidValueUseCollector.new(scope_index),
170
186
  always_truthy: AlwaysTruthyConditionCollector.new(scope_index),
171
187
  unreachable_clauses: UnreachableClauseCollector.new(scope_index),
@@ -198,7 +214,7 @@ module Rigor
198
214
  # divergence aborts the run — the corpus-scale half of the
199
215
  # equivalence harness (the curated half is `rule_walk_equivalence_spec`).
200
216
  def run_node_collectors(path, root, scope_index)
201
- collectors = build_node_collectors(path, scope_index)
217
+ collectors = build_node_collectors(path, scope_index, root)
202
218
  RuleWalk.run(root, collectors.values)
203
219
  shadow_verify_node_collectors(path, root, scope_index, collectors) if ENV["RIGOR_SHADOW_RULE_WALK"]
204
220
  collectors
@@ -242,8 +258,10 @@ module Rigor
242
258
  # produces them on the shared walk.
243
259
  def main_pass_oracle(path, root, scope_index)
244
260
  diagnostics = []
261
+ eval_ranges = receiver_eval_block_ranges(root)
262
+ lexical_sites = LexicalMethodSites.new(root)
245
263
  Source::NodeWalker.each(root) do |node|
246
- 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))
247
265
  end
248
266
  diagnostics
249
267
  end
@@ -263,10 +281,10 @@ module Rigor
263
281
  shadow_verify_node_collectors(path, root, scope_index, collectors)
264
282
  end
265
283
 
266
- def call_node_diagnostics(path, node, scope_index)
284
+ def call_node_diagnostics(path, node, scope_index, eval_ranges = nil, lexical_sites = nil)
267
285
  [
268
- undefined_method_diagnostic(path, node, scope_index),
269
- 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),
270
288
  wrong_arity_diagnostic(path, node, scope_index),
271
289
  argument_type_diagnostic(path, node, scope_index),
272
290
  nil_receiver_diagnostic(path, node, scope_index),
@@ -490,7 +508,7 @@ module Rigor
490
508
  /\A#\s*rigor:(?<marker>disable-(?!file(?![\w-]))[\w-]+|enable(?:-[\w-]+)?)(?![\w-])(?<rest>.*)/
491
509
  private_constant :UNKNOWN_SUPPRESSION_MARKER
492
510
 
493
- # @return [Array<(Hash{Integer => Set}, Set)>] pair of
511
+ # @return pair of
494
512
  # `(line_suppressions, file_suppressions)`. Line
495
513
  # suppressions are keyed by source line number; file
496
514
  # suppressions apply to every line.
@@ -660,7 +678,7 @@ module Rigor
660
678
  class << self
661
679
  private
662
680
 
663
- 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
664
682
  return nil if call_node.receiver.nil?
665
683
 
666
684
  scope = scope_index[call_node]
@@ -668,10 +686,20 @@ module Rigor
668
686
 
669
687
  # The two exemptions that hold whatever shape the receiver has. Ahead of the receiver-shape
670
688
  # branch below because a plugin is consulted ONCE for the whole receiver, union included, so the
671
- # gate belongs where that one consult is rather than duplicated into each shape's path. On the
672
- # union side that is placement, not demonstrated coverage: no fixture reaches the gate through
673
- # `union_undefined_method_diagnostic` today — every plugin-typed union anyone has built carries a
674
- # `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.
675
703
  # See {#call_site_exempt?}.
676
704
  return nil if call_site_exempt?(call_node, scope)
677
705
 
@@ -717,7 +745,7 @@ module Rigor
717
745
  # because the real type is empty (the real `DRb` has
718
746
  # `start_service`), so enumerating it to prove a call
719
747
  # "undefined" would be a false positive.
720
- return nil if unenumerable_receiver?(class_name, scope)
748
+ return nil if unenumerable_receiver?(receiver_type, class_name, scope)
721
749
 
722
750
  # Slice 7 phase 12 — suppress when the user has
723
751
  # declared the method in source (`def` /
@@ -742,7 +770,7 @@ module Rigor
742
770
  method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
743
771
  return nil if method_def
744
772
 
745
- 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)
746
774
 
747
775
  definition_site = project_definition_site(scope, class_name, call_node.name, kind)
748
776
  # Issue #735 — the site is evidence, and whose RBS this is decides what it is evidence OF. On a
@@ -757,9 +785,54 @@ module Rigor
757
785
  build_undefined_method_diagnostic(path, call_node, receiver_type, definition_site, class_name)
758
786
  end
759
787
 
760
- # 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
761
832
  # because they share that position and nothing else.
762
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?}).
763
836
  # - Issue #739 — an instance-side MODULE receiver. A value typed as a mixin module is an instance of
764
837
  # some class that includes it, and that class contributes an arbitrary surface: inside
765
838
  # `module Attachable::InstanceMethods`, `self.project` is defined by the ActiveRecord model that
@@ -780,7 +853,8 @@ module Rigor
780
853
  # it"; a project ancestor still might, and the typer has already resolved the site through
781
854
  # exactly this walk. It is last because it is the only probe here that walks the class graph
782
855
  # (see {#ancestry_declares_method?} for why that placement is load-bearing, not tidiness).
783
- 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)
784
858
  return true if module_mixin_receiver?(receiver_type, scope)
785
859
  return true if mixin_self_class_receiver?(call_node, scope)
786
860
  return true if unknown_mixin_includer?(class_name, scope)
@@ -936,7 +1010,7 @@ module Rigor
936
1010
  # Authored severity is `:warning`; the severity profile
937
1011
  # remaps it (`strict` → `:error`, `balanced` →
938
1012
  # `:warning`, `lenient` → `:off` / suppressed).
939
- def unresolved_toplevel_diagnostic(path, call_node, scope_index)
1013
+ def unresolved_toplevel_diagnostic(path, call_node, scope_index, eval_ranges = nil)
940
1014
  return nil unless call_node.receiver.nil?
941
1015
 
942
1016
  scope = scope_index[call_node]
@@ -947,10 +1021,51 @@ module Rigor
947
1021
  return nil if scope.top_level_def_for(name)
948
1022
  return nil if source_declared_method?(scope, "Object", name, :instance)
949
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)
950
1029
 
951
1030
  build_unresolved_toplevel_diagnostic(path, call_node)
952
1031
  end
953
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
+
954
1069
  def build_unresolved_toplevel_diagnostic(path, call_node)
955
1070
  Diagnostic.from_message_loc(
956
1071
  call_node,
@@ -976,10 +1091,32 @@ module Rigor
976
1091
  # the scalar rule enumerated `Module`'s own RBS instead and reported them.
977
1092
  #
978
1093
  # Kept beside `unbounded_receiver_surface?` rather than folded into it: that predicate has eight
979
- # other callers (arity, the `raise` verdicts), and widening all of them is a separate change with
980
- # its own evidence to gather.
981
- def unenumerable_receiver?(class_name, scope)
982
- METACLASS_ARMS.include?(class_name) || unbounded_receiver_surface?(class_name, scope)
1094
+ # other callers, and #742 had no evidence that any of them spoke on these receivers.
1095
+ #
1096
+ # Issue #879 supplied it for exactly one — `call.wrong-arity`, which decided `v.hexdigest(1, 2, 3)`
1097
+ # against `Digest::Instance`'s declaration on the same run where `v.no_such_method_zzz` was declined
1098
+ # as unenumerable. One receiver, one run, two answers about whether that surface can be read. The
1099
+ # reporter could make no other caller speak: `raise` on a module- or `Class`-typed operand is silent,
1100
+ # and `Class`-typed `.new` with a wrong arity is silent too, so the other seven stay on
1101
+ # `unbounded_receiver_surface?` until something demonstrates otherwise.
1102
+ #
1103
+ # Arity takes {#unenumerable_receiver_shape?}, not this whole predicate: ADR-26's open receivers are
1104
+ # handled for the signature-reading rules by {#unauthoritative_inherited_signature?}, which is
1105
+ # deliberately narrower — a method the open class DECLARES itself keeps its arity check.
1106
+ def unenumerable_receiver?(receiver_type, class_name, scope)
1107
+ unenumerable_receiver_shape?(receiver_type, class_name, scope) ||
1108
+ unbounded_receiver_surface?(class_name, scope)
1109
+ end
1110
+
1111
+ # The receiver shapes whose method surface cannot be enumerated at all, whatever the environment
1112
+ # declares: a generic metaclass value, and an instance-side mixin-module type — an instance of some
1113
+ # unknown class that includes the module, which may declare the name with any arity of its own
1114
+ # (#739's reasoning, applied to the signature rather than to the method's existence).
1115
+ #
1116
+ # Instance-side only, for #739's reason: `Singleton[M]` on a namespace module is a real, enumerable
1117
+ # surface, and `M.helper(1, 2, 3)` must keep firing.
1118
+ def unenumerable_receiver_shape?(receiver_type, class_name, scope)
1119
+ METACLASS_ARMS.include?(class_name) || module_mixin_receiver?(receiver_type, scope)
983
1120
  end
984
1121
 
985
1122
  # Issue #746 — a class that includes a module the environment does not know. `SudoMode::Form`
@@ -1030,7 +1167,7 @@ module Rigor
1030
1167
  # about it instead of bailing. `Type::Refined` carries string-family
1031
1168
  # refinements (`lowercase-string`, …) over an explicit `.base`;
1032
1169
  # `Type::IntegerRange` carries the bounded-int refinements
1033
- # (`non-negative-int`, `positive-int`, `int<1,5>`), every one an
1170
+ # (`non-negative-int`, `positive-int`, `Integer[1..5]`), every one an
1034
1171
  # Integer; `Type::Difference` (`A - B`) carries the non-empty /
1035
1172
  # non-zero refinements (`non-empty-string` = `String - ""`,
1036
1173
  # `non-empty-array` = `Array - []`, `non-zero-int` = `Integer - 0`)
@@ -1038,6 +1175,7 @@ module Rigor
1038
1175
  # base (minuend) class dispatches.
1039
1176
  when Type::Refined, Type::Difference then concrete_class_name(type.base)
1040
1177
  when Type::IntegerRange then "Integer"
1178
+ when Type::FloatRange then "Float"
1041
1179
  end
1042
1180
  end
1043
1181
 
@@ -1097,7 +1235,11 @@ module Rigor
1097
1235
  # audit), so declaring even its reader surface makes it RBS-known — and, without this, every member
1098
1236
  # the declaration omits would become a false `call.undefined-method` for every project that locks
1099
1237
  # activesupport, the overwhelming majority of which never opt into the plugin at all.
1100
- GEM_OVERLAY_OPEN_RECEIVERS = Set["ActiveSupport::Duration"].freeze
1238
+ # `ActiveSupport::Concern` joined it in #534 item 7. `class_methods do … end` synthesizes a
1239
+ # `ClassMethods` constant and `append_features` / `prepend_features` are part of the real surface,
1240
+ # none of which the two-line declaration enumerates — and a concern module is the receiver of that
1241
+ # surface in every Rails app, so the same argument applies with the same force.
1242
+ GEM_OVERLAY_OPEN_RECEIVERS = Set["ActiveSupport::Duration", "ActiveSupport::Concern"].freeze
1101
1243
  private_constant :GEM_OVERLAY_OPEN_RECEIVERS
1102
1244
 
1103
1245
  # ADR-26 — whether `class_name` is declared "open" by a
@@ -1425,7 +1567,7 @@ module Rigor
1425
1567
  # by `undefined_method_diagnostic`; it returns nil
1426
1568
  # when the call's receiver / RBS coverage / call shape
1427
1569
  # disqualifies the rule.
1428
- # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1570
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1429
1571
  def wrong_arity_diagnostic(path, call_node, scope_index)
1430
1572
  return nil if call_node.receiver.nil?
1431
1573
  return nil unless plain_positional_call?(call_node)
@@ -1441,6 +1583,12 @@ module Rigor
1441
1583
  class_name = concrete_class_name(receiver_type)
1442
1584
  return nil if class_name.nil?
1443
1585
 
1586
+ # Issue #879 — the receiver whose surface `call.undefined-method` already refuses to enumerate.
1587
+ # Deciding an arity against a declaration this rule cannot prove the receiver actually runs is the
1588
+ # same unsoundness in signature form: the includer may declare the name with a wider arity, and
1589
+ # then the call is correct code.
1590
+ return nil if unenumerable_receiver_shape?(receiver_type, class_name, scope)
1591
+
1444
1592
  kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
1445
1593
  # `Struct.new(:a, :b).new(...)` chained: the inner
1446
1594
  # `Struct.new(...)` is an anonymous Struct *subclass* whose
@@ -1451,24 +1599,129 @@ module Rigor
1451
1599
  # `.new` against the real `Struct.new(*Symbol)` signature is a
1452
1600
  # false positive. Skip arity-checking the chained position.
1453
1601
  return nil if anonymous_struct_new_call?(call_node, class_name, kind)
1454
- return nil if scope.discovered_method?(class_name, call_node.name, kind)
1455
-
1456
- return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
1457
- return nil unless definition_available?(receiver_type, class_name, scope)
1458
1602
 
1459
- method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
1460
- return nil if method_def.nil?
1461
-
1462
- arity_envelope = compute_arity_envelope(method_def)
1603
+ arity_envelope = arity_envelope_for(receiver_type, class_name, call_node, scope, kind)
1463
1604
  return nil if arity_envelope.nil?
1464
1605
 
1465
1606
  actual = (call_node.arguments&.arguments || []).size
1466
- min, max = arity_envelope
1467
- 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)
1468
1615
 
1469
1616
  build_arity_diagnostic(path, call_node, class_name, min, max, actual)
1470
1617
  end
1471
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
+
1706
+ # Issue #917 — no loaded RBS declares a constructor for this class. The definition builder
1707
+ # synthesizes `.new` from the nearest `initialize` in the ancestry, and when nothing declares
1708
+ # one that is `BasicObject#initialize: () -> void`, so `.new` derives a nullary envelope that
1709
+ # is an artifact of the fallback rather than a statement about the class. `Gem::Specification`
1710
+ # is declared in full by the vendored rubygems sigs except for its constructor, and every
1711
+ # argument at the real one was reported. The rule needs "this class declares a nullary
1712
+ # constructor", and only a `defined_in` naming a class other than `BasicObject` is that
1713
+ # evidence — except on `BasicObject` and `Object` themselves, where the declaration is their
1714
+ # own and `.new` really does take no arguments.
1715
+ def undeclared_constructor?(class_name, call_node, kind, method_def)
1716
+ return false unless kind == :singleton && call_node.name == :new
1717
+ return false unless method_def.respond_to?(:defined_in)
1718
+
1719
+ defined_in = method_def.defined_in
1720
+ return false unless defined_in.nil? || strip_root(defined_in.to_s) == "BasicObject"
1721
+
1722
+ !%w[BasicObject Object].include?(strip_root(class_name))
1723
+ end
1724
+
1472
1725
  # True for the outer `.new` of a chained `Struct.new(...).new`:
1473
1726
  # `class_name`/`kind` already pin the receiver to
1474
1727
  # `Singleton[Struct]`, and the receiver node is itself a
@@ -1590,7 +1843,10 @@ module Rigor
1590
1843
  # {DeclarationSourcedGuard} exactly what it asked before — but it
1591
1844
  # now asks it through the predicate the argument-type gates share
1592
1845
  # (issue #324), which is what keeps the two rules from drifting.
1593
- 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)
1594
1850
 
1595
1851
  # ADR-67 WD6b — an inferred-parameter receiver's type (incl. any nil constituent unioned in from a
1596
1852
  # nil call site) is an open-call-site lower bound; a possible-nil firing against it is an FP by
@@ -2115,6 +2371,200 @@ module Rigor
2115
2371
  )
2116
2372
  end
2117
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
+
2118
2568
  # v0.1.2 — `flow.unreachable-branch`. Fires when an `IfNode` / `UnlessNode` whose predicate is a
2119
2569
  # literal `true` / `false` / `nil` (or a literal numeric / string / symbol whose Ruby truthiness is
2120
2570
  # known at-a-glance) has an observable dead branch. The diagnostic points at the dead branch (not
@@ -2385,8 +2835,10 @@ module Rigor
2385
2835
  # overload requires the dispatcher's full
2386
2836
  # acceptance plumbing.
2387
2837
  # - The selected overload must have NO
2388
- # rest_positionals, NO required keywords, NO
2389
- # trailing positionals.
2838
+ # rest_positionals and NO trailing positionals —
2839
+ # the two shapes under which argument N no longer
2840
+ # binds to positional parameter N. Keyword
2841
+ # parameters do not disqualify it (#673).
2390
2842
  # - The call must use plain positional arguments
2391
2843
  # (no splat / kw / block-pass / forwarded).
2392
2844
  # - Per-argument: skip when EITHER side is `Dynamic`
@@ -2451,7 +2903,7 @@ module Rigor
2451
2903
  return nil if method_def.nil?
2452
2904
 
2453
2905
  param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
2454
- 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)
2455
2907
  return nil if mismatch.nil?
2456
2908
  return nil if inferred_param_mismatch_verdict?(call_node, mismatch, scope)
2457
2909
 
@@ -2477,14 +2929,50 @@ module Rigor
2477
2929
  # overload rejects) plus, on non-coerce methods, the non-nil channel
2478
2930
  # (a single-concrete-class argument every overload rejects). See
2479
2931
  # {#multi_overload_argument_mismatch}.
2480
- def argument_mismatch(method_types, call_node, scope, param_overrides)
2481
- if method_types.size == 1
2482
- first_argument_mismatch(method_types.first, call_node, scope, param_overrides)
2483
- else
2484
- multi_overload_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)
2936
+ mismatch =
2937
+ if method_types.size == 1
2938
+ first_argument_mismatch(method_types.first, call_node, scope, param_overrides, scope_index)
2939
+ else
2940
+ multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides, scope_index)
2941
+ end
2942
+ return nil if mismatch && keyword_bearing?(method_types) && nil_tainted?(mismatch[:actual])
2943
+
2944
+ mismatch
2945
+ end
2946
+
2947
+ # Issue #673 widened {#argument_check_eligible?} to admit keyword-bearing signatures, whose
2948
+ # POSITIONAL arguments had gone unchecked. The widening is staged, and this is the half held
2949
+ # back: on such a signature only an argument refuted on its CLASS fires; one carrying `nil`
2950
+ # does not.
2951
+ #
2952
+ # Measured, not assumed. Across redmine, mastodon, tdiary-core, faraday, liquid and
2953
+ # rubocop-ast the widening produced exactly two new verdicts, both on correct code and both
2954
+ # resting on nullability: `Addrinfo.foreach(host, nil, nil, :STREAM)` (redmine — a nil service
2955
+ # is legal at runtime; rbs declares the parameter `String | ::Integer`) and
2956
+ # `Nokogiri::HTML5(html)` guarded by `return if html.nil?` one line above (mastodon — the guard
2957
+ # is on a method call, so the type stays `Dynamic[top]?`). Zero class-refuted new verdicts were
2958
+ # false. Nullability is the property a hand-written signature most often over-declares, so it
2959
+ # stays behind the old envelope until it has evidence of its own.
2960
+ #
2961
+ # Signatures WITHOUT keywords are untouched: `"a" + nil` fires exactly as before.
2962
+ def keyword_bearing?(method_types)
2963
+ method_types.any? do |method_type|
2964
+ function = method_type.type
2965
+ next false unless function.respond_to?(:required_keywords)
2966
+
2967
+ !function.required_keywords.empty? || !function.optional_keywords.empty? ||
2968
+ !function.rest_keywords.nil?
2485
2969
  end
2486
2970
  end
2487
2971
 
2972
+ def nil_tainted?(actual)
2973
+ nil_member?(actual) || (actual.is_a?(Type::Union) && union_contains_nil?(actual))
2974
+ end
2975
+
2488
2976
  # Multi-overload argument-type-mismatch. The dispatcher's per-overload
2489
2977
  # acceptance plumbing is not run here; instead the FP-safe shape mirrors
2490
2978
  # the "absent on every arm" union-undefined-method rule: an argument is
@@ -2501,32 +2989,52 @@ module Rigor
2501
2989
  # stays deferred), and decides acceptance on the RBS param type
2502
2990
  # ({#param_accepts_arg_class?}) so it sees through the `int` / `string`
2503
2991
  # interface-aliases the translator degrades.
2504
- 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)
2505
2993
  functions = method_types.map(&:type)
2506
2994
  return nil unless functions.all? { |function| argument_check_eligible?(function) }
2507
2995
 
2508
2996
  coerce_method = COERCE_DISPATCH_METHODS.include?(call_node.name)
2509
2997
  arguments = call_node.arguments&.arguments || []
2510
2998
  arguments.each_with_index do |arg, index|
2511
- arg_type = scope.type_of(arg)
2512
- params = overload_positional_params(method_types, index)
2513
- next if params.nil? # arity divergence — some overload lacks a param here
2514
-
2515
- mismatch =
2516
- if nil_member?(arg_type) # pure nil only — not a `T | nil` union
2517
- nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
2518
- elsif !coerce_method
2519
- non_nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
2520
- end
2999
+ params = checkable_overload_params(method_types, index, param_overrides, scope)
3000
+ next if params.nil?
3001
+
3002
+ mismatch = overload_argument_mismatch(arg, params, param_overrides,
3003
+ argument_scope(arg, scope, scope_index), coerce_method)
2521
3004
  return mismatch if mismatch
2522
3005
  end
2523
3006
  nil
2524
3007
  end
2525
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
+
3021
+ # The parameter set at `index` that a verdict may rest on, or nil when there is none — the two
3022
+ # reasons being arity divergence (an overload with no param there, which is `call.wrong-arity`'s
3023
+ # concern) and an overload whose param names a class carrying no definition ({#stub_typed_param?},
3024
+ # issue #661). Both channels below fire only when EVERY overload rejects, so either one is enough
3025
+ # to unseat that premise.
3026
+ def checkable_overload_params(method_types, index, param_overrides, scope)
3027
+ params = overload_positional_params(method_types, index)
3028
+ return nil if params.nil?
3029
+ return nil if params.any? { |param| stub_typed_param?(param, param_overrides, scope) }
3030
+
3031
+ params
3032
+ end
3033
+
2526
3034
  # The nil channel: a pure `nil` argument no overload admits (ADR-58
2527
3035
  # parity excuses a declaration-sourced ivar nil).
2528
- def nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
2529
- 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)
2530
3038
  return nil if params.any? { |param| param_admits_nil?(param, param_overrides, scope) }
2531
3039
 
2532
3040
  { node: arg, name: nil, expected: overload_param_expected_label(params), actual: arg_type }
@@ -2568,6 +3076,53 @@ module Rigor
2568
3076
  param.map_type { |type| type.sub(substitution) }
2569
3077
  end
2570
3078
 
3079
+ # ADR-26 WD8's PARAMETER-side twin (issue #661). `sig/app.rbs` declaring
3080
+ # `(ActiveSupport::TimeWithZone) -> void` in a project that never brings ActiveSupport's RBS in
3081
+ # names a class that carries no definition anywhere in the environment. Rigor still refuted
3082
+ # `App.take_twz(Time.now)` against it — `expected ActiveSupport::TimeWithZone, got Time` on a call
3083
+ # that works — because the acceptance walk compares nominals and `Time`'s ancestry does not list a
3084
+ # name nothing declares.
3085
+ #
3086
+ # WD8 already declines the two signature rules on such a RECEIVER, on the ground that there is
3087
+ # nothing authoritative to check the call against. It is the same one position over: with no
3088
+ # ancestry there is no fact that could make an argument satisfy the parameter either, so the
3089
+ # rejection is the missing signature restated as an error rather than a finding about the program.
3090
+ # Under AGENTS.md's false-positives-outrank-worst-case rule the honest answer is to decline, and
3091
+ # the report it costs is one the analyzer never had evidence for.
3092
+ #
3093
+ # Both shapes the missing name arrives in are covered, deliberately as ONE criterion. A name an
3094
+ # INSTANCE-method signature references is minted as an empty stub so the class builds
3095
+ # ({Environment::RbsLoader.stub_missing_referenced_types}) and reads back as RBS-known; the same
3096
+ # name in a SINGLETON signature is not, because that pass mirrors rbs's own membership test and
3097
+ # `validate_type_params` never reaches singleton members. That asymmetry is an artifact of which
3098
+ # pass ran, not of what is knowable, and the issue's own repro is the singleton half.
3099
+ #
3100
+ # A `rigor:v1:param` override is a real Rigor type the author wrote against the call site's own
3101
+ # method, so it is consulted first and checked normally.
3102
+ def stub_typed_param?(param, param_overrides, scope)
3103
+ return false if param_overrides[param.name]
3104
+
3105
+ undefined_param_class?(param.type, scope)
3106
+ end
3107
+
3108
+ # Only the arms a verdict can rest on directly: a union member, an optional's inner type, an
3109
+ # expanded alias, and the class instance / singleton itself. Deliberately NOT recursive into type
3110
+ # ARGUMENTS — `Array[Missing]` is still refuted by an `Integer` argument on the `Array` alone, and
3111
+ # declining there would be a lost report bought for nothing.
3112
+ def undefined_param_class?(rbs_type, scope)
3113
+ case rbs_type
3114
+ when RBS::Types::Union then rbs_type.types.any? { |member| undefined_param_class?(member, scope) }
3115
+ when RBS::Types::Optional then undefined_param_class?(rbs_type.type, scope)
3116
+ when RBS::Types::Alias
3117
+ expanded = scope.environment&.rbs_loader&.expand_type_alias(rbs_type)
3118
+ !expanded.nil? && undefined_param_class?(expanded, scope)
3119
+ when RBS::Types::ClassInstance, RBS::Types::ClassSingleton
3120
+ name = rbs_type.name
3121
+ synthesized_stub_receiver?(name, scope) || !Rigor::Reflection.rbs_class_known?(name.to_s, scope: scope)
3122
+ else false
3123
+ end
3124
+ end
3125
+
2571
3126
  # The class names whose instances `nil` IS — `NilClass` and every
2572
3127
  # ancestor. A parameter typed as any other class instance rejects nil.
2573
3128
  NIL_COMPATIBLE_CLASS_NAMES = %w[NilClass Object BasicObject Kernel].to_set.freeze
@@ -2746,7 +3301,7 @@ module Rigor
2746
3301
  DeclarationSourcedGuard.marked?(arg, scope)
2747
3302
  end
2748
3303
 
2749
- 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)
2750
3305
  function = method_type.type
2751
3306
  return nil unless argument_check_eligible?(function)
2752
3307
 
@@ -2756,12 +3311,17 @@ module Rigor
2756
3311
  param = params[index]
2757
3312
  next if param.nil? # arity mismatch is the wrong-arity rule's concern.
2758
3313
 
2759
- mismatch = single_argument_mismatch(param, arg, scope, param_overrides)
3314
+ mismatch = single_argument_mismatch(param, arg, argument_scope(arg, scope, scope_index), param_overrides)
2760
3315
  return mismatch if mismatch
2761
3316
  end
2762
3317
  nil
2763
3318
  end
2764
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
+
2765
3325
  # The mismatch (or nil) for one positional argument against one
2766
3326
  # parameter. The nil channel decides a pure `nil` argument on the RBS
2767
3327
  # parameter type — seeing through the `string` / `int` interface-alias
@@ -2770,6 +3330,19 @@ module Rigor
2770
3330
  # the original translated-acceptance check, with a `rigor:v1:param`
2771
3331
  # override taking precedence over the RBS-declared type.
2772
3332
  def single_argument_mismatch(param, arg, scope, param_overrides)
3333
+ return nil if stub_typed_param?(param, param_overrides, scope)
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)
2773
3346
  arg_type = scope.type_of(arg)
2774
3347
 
2775
3348
  if nil_member?(arg_type)
@@ -2783,10 +3356,42 @@ module Rigor
2783
3356
  return nil if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
2784
3357
  return nil if arg_type.is_a?(Type::Dynamic) || arg_type.is_a?(Type::Top)
2785
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
2786
3364
 
2787
3365
  { node: arg, name: param.name, expected: param_type, actual: arg_type }
2788
3366
  end
2789
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
+
2790
3395
  # The parameter rejects the argument AND the rejection is not a
2791
3396
  # withheld declaration-sourced-nil case.
2792
3397
  def argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
@@ -2824,17 +3429,29 @@ module Rigor
2824
3429
  Inference::Acceptance.accepts(param_type, non_nil, mode: :gradual).yes?
2825
3430
  end
2826
3431
 
3432
+ # What this rule needs is that argument N binds to positional
3433
+ # parameter N, so the only disqualifiers are the two shapes that
3434
+ # break that mapping: a rest positional (arguments past it belong
3435
+ # to the rest, not to a later parameter) and trailing positionals
3436
+ # (the last arguments bind backwards from the end).
3437
+ #
3438
+ # Issue #673 — a keyword parameter used to disqualify the whole
3439
+ # signature, which silently unchecked the POSITIONAL arguments of
3440
+ # every keyword-bearing signature in the bundle:
3441
+ # `time.beginning_of_week("monday")` fired while
3442
+ # `time.next_week("monday")`, `(?Symbol, ?same_time: bool)`, did not,
3443
+ # though both raise the same way at runtime. Keywords cannot shift a
3444
+ # positional argument's index, and a call that PASSES keywords never
3445
+ # reaches here at all — `plain_positional_call?` rejects a
3446
+ # `KeywordHashNode` argument (`**opts` included) before the signature
3447
+ # is consulted.
2827
3448
  def argument_check_eligible?(function)
2828
3449
  # See `arity_eligible?`: `UntypedFunction` lacks
2829
3450
  # the per-arity accessors. Treat it as ineligible
2830
3451
  # for argument-type-mismatch diagnostics.
2831
3452
  return false unless function.respond_to?(:required_keywords)
2832
3453
 
2833
- function.rest_positionals.nil? &&
2834
- function.required_keywords.empty? &&
2835
- function.optional_keywords.empty? &&
2836
- function.rest_keywords.nil? &&
2837
- function.trailing_positionals.empty?
3454
+ function.rest_positionals.nil? && function.trailing_positionals.empty?
2838
3455
  end
2839
3456
 
2840
3457
  def translate_param_type(rbs_type, environment)
@@ -2911,6 +3528,14 @@ module Rigor
2911
3528
  #
2912
3529
  # - Skips methods without an RBS declaration. The rule has no contract to compare against for
2913
3530
  # source-only methods.
3531
+ # - Skips a `def` whose only declaration was resolved through an ANCESTOR. `Reflection`'s lookup
3532
+ # reads the fully resolved method table, so a signature written about a base class answers for
3533
+ # every subclass that inherits the name; comparing an override's body against it reports the
3534
+ # override wrong for disagreeing with a contract that was never written about it (#744, and
3535
+ # the four redmine sites in #856). ADR-35 already specifies this rule as comparing a method's
3536
+ # OWN declared return, and the sibling override rules gate on `defined_on?` — this one did not.
3537
+ # The inherited-declaration signal belongs to the ADR-35 family, which requires both sides
3538
+ # authored. Decided in ADR-110 WD3.
2914
3539
  # - Skips methods whose enclosing class isn't a `Type::Singleton` self_type that we can name
2915
3540
  # (top-level / module-level methods land outside the rule).
2916
3541
  # - Skips methods whose body's last expression is absent or types as `Dynamic[top]` (the
@@ -2933,15 +3558,28 @@ module Rigor
2933
3558
  declared = declared_return_type(def_node, scope_index)
2934
3559
  return nil if declared.nil?
2935
3560
 
2936
- 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)
2937
3564
  return nil if dynamic_top?(inferred)
2938
3565
 
2939
3566
  severity = compare_return(declared, inferred)
2940
3567
  return nil if severity.nil?
3568
+ return nil if declared_only_return?(last_expr, inferred, declared, inner_scope)
2941
3569
 
2942
3570
  build_return_type_mismatch_diagnostic(path, def_node, declared, inferred, severity)
2943
3571
  end
2944
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
+
2945
3583
  # The body of a `def` is the last `Prism::StatementsNode` child (or a single expression for
2946
3584
  # one-liner defs). Take the last statement; that's the implicit return.
2947
3585
  def body_last_expression(body)
@@ -2978,6 +3616,7 @@ module Rigor
2978
3616
  Reflection.singleton_method_definition(self_type.class_name, def_node.name, scope: scope)
2979
3617
  end
2980
3618
  return nil if method_def.nil?
3619
+ return nil unless defined_on?(method_def, self_type.class_name)
2981
3620
 
2982
3621
  override = Rigor::RbsExtended.read_return_type_override(method_def, environment: scope.environment)
2983
3622
  return override if override
@@ -3156,9 +3795,15 @@ module Rigor
3156
3795
  names
3157
3796
  end
3158
3797
 
3798
+ # Issue #732 — the predicate is asked of the SCOPE, not re-implemented here. This rule kept its own
3799
+ # three-table copy until #723 widened the scope's with `discovered_methods` and left this one behind,
3800
+ # so the two disagreed about exactly one shape (a project class with no instance def, no superclass
3801
+ # and no include — `class Base; def self.build = :built; end`) with nothing observing the fork. #682
3802
+ # made `Scope` the single owner of the candidate ORDER; this makes it the single owner of the
3803
+ # predicate that filters those candidates too.
3159
3804
  def resolve_override_ancestor_name(scope, subclass_qualified, raw_ancestor)
3160
3805
  resolved = scope.ancestor_name_candidates(subclass_qualified, raw_ancestor)
3161
- .find { |candidate| known_user_class?(scope, candidate) }
3806
+ .find { |candidate| scope.known_user_class?(candidate) }
3162
3807
  return resolved if resolved
3163
3808
 
3164
3809
  # ADR-46 slice 3 — the override checker reads the class graph
@@ -3172,12 +3817,6 @@ module Rigor
3172
3817
  nil
3173
3818
  end
3174
3819
 
3175
- def known_user_class?(scope, name)
3176
- scope.discovered_superclasses.key?(name) ||
3177
- scope.discovered_def_nodes.key?(name) ||
3178
- scope.discovered_includes.key?(name)
3179
- end
3180
-
3181
3820
  def build_override_visibility_diagnostic(path, def_node, parent_class, parent_visibility, override_visibility)
3182
3821
  Diagnostic.from_name_loc(
3183
3822
  def_node,