rigortype 0.3.6 → 0.3.7

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 (213) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/date.yml +6 -3
  4. data/data/builtins/ruby_core/enumerable.yml +1 -0
  5. data/data/builtins/ruby_core/exception.yml +2 -1
  6. data/data/builtins/ruby_core/file.yml +28 -14
  7. data/data/builtins/ruby_core/hash.yml +5 -2
  8. data/data/builtins/ruby_core/io.yml +33 -16
  9. data/data/builtins/ruby_core/random.yml +2 -1
  10. data/data/builtins/ruby_core/re.yml +6 -3
  11. data/data/builtins/ruby_core/struct.yml +2 -0
  12. data/data/builtins/ruby_core/time.yml +2 -1
  13. data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
  14. data/docs/manual/02-cli-reference.md +16 -7
  15. data/docs/manual/05-inspecting-types.md +20 -8
  16. data/docs/manual/07-plugins.md +4 -0
  17. data/docs/manual/plugins/rigor-actionpack.md +61 -0
  18. data/docs/manual/plugins/rigor-activerecord.md +74 -1
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
  20. data/docs/manual/plugins/rigor-sidekiq.md +22 -0
  21. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
  22. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
  23. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
  24. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
  25. data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
  26. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
  27. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
  28. data/lib/rigor/analysis/check_rules.rb +383 -48
  29. data/lib/rigor/analysis/crash_signature.rb +116 -0
  30. data/lib/rigor/analysis/dependency_recorder.rb +18 -3
  31. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  32. data/lib/rigor/analysis/incremental.rb +28 -0
  33. data/lib/rigor/analysis/incremental_session.rb +44 -9
  34. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
  35. data/lib/rigor/analysis/reachability/graph.rb +14 -5
  36. data/lib/rigor/analysis/reachability/scan.rb +7 -3
  37. data/lib/rigor/analysis/result.rb +32 -0
  38. data/lib/rigor/analysis/run_cache_key.rb +41 -1
  39. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
  40. data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
  41. data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
  42. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
  43. data/lib/rigor/analysis/runner.rb +147 -10
  44. data/lib/rigor/analysis/worker_session.rb +10 -1
  45. data/lib/rigor/bleeding_edge.rb +7 -6
  46. data/lib/rigor/builtins/hkt_builtins.rb +45 -6
  47. data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
  48. data/lib/rigor/builtins/static_return_refinements.rb +20 -1
  49. data/lib/rigor/cache/descriptor.rb +42 -2
  50. data/lib/rigor/cache/incremental_snapshot.rb +40 -3
  51. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
  52. data/lib/rigor/cache/store.rb +10 -1
  53. data/lib/rigor/cli/coverage_command.rb +23 -18
  54. data/lib/rigor/cli/coverage_mutation.rb +13 -17
  55. data/lib/rigor/cli/coverage_scan.rb +47 -6
  56. data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
  57. data/lib/rigor/cli/fused_protection_report.rb +23 -2
  58. data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
  59. data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
  60. data/lib/rigor/cli/mutation_protection_report.rb +31 -2
  61. data/lib/rigor/cli/sig_gen_command.rb +18 -0
  62. data/lib/rigor/cli/type_of_command.rb +188 -41
  63. data/lib/rigor/cli/type_of_renderer.rb +72 -3
  64. data/lib/rigor/cli/type_scan_command.rb +10 -1
  65. data/lib/rigor/cli.rb +1 -1
  66. data/lib/rigor/configuration.rb +7 -4
  67. data/lib/rigor/effects/plugin_facts.rb +1 -1
  68. data/lib/rigor/effects/scanner.rb +2 -3
  69. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
  70. data/lib/rigor/environment/lockfile_resolver.rb +15 -5
  71. data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
  72. data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
  73. data/lib/rigor/environment/rbs_loader.rb +525 -46
  74. data/lib/rigor/environment.rb +38 -15
  75. data/lib/rigor/inference/acceptance.rb +172 -20
  76. data/lib/rigor/inference/captured_locals.rb +62 -0
  77. data/lib/rigor/inference/content_join.rb +347 -0
  78. data/lib/rigor/inference/def_handle.rb +15 -3
  79. data/lib/rigor/inference/def_node_resolver.rb +62 -3
  80. data/lib/rigor/inference/expression_typer.rb +1020 -141
  81. data/lib/rigor/inference/fork_map.rb +6 -1
  82. data/lib/rigor/inference/hkt_reducer.rb +12 -1
  83. data/lib/rigor/inference/hkt_registry.rb +46 -0
  84. data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
  85. data/lib/rigor/inference/index_write_widening.rb +48 -0
  86. data/lib/rigor/inference/indexed_narrowing.rb +14 -0
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
  88. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
  89. data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
  90. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
  91. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
  93. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
  94. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
  95. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
  96. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
  97. data/lib/rigor/inference/method_dispatcher.rb +155 -14
  98. data/lib/rigor/inference/method_parameter_binder.rb +3 -3
  99. data/lib/rigor/inference/mutation_widening.rb +244 -174
  100. data/lib/rigor/inference/narrowing.rb +331 -27
  101. data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
  102. data/lib/rigor/inference/pre_eval_constants.rb +15 -0
  103. data/lib/rigor/inference/precision_scanner.rb +5 -2
  104. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  105. data/lib/rigor/inference/rbs_type_translator.rb +89 -39
  106. data/lib/rigor/inference/scope_indexer.rb +1493 -140
  107. data/lib/rigor/inference/statement_evaluator.rb +276 -91
  108. data/lib/rigor/inference/struct_fold_safety.rb +186 -27
  109. data/lib/rigor/inference/version_guard.rb +229 -0
  110. data/lib/rigor/language_server/selection_range_provider.rb +1 -1
  111. data/lib/rigor/plugin/base.rb +3 -2
  112. data/lib/rigor/plugin/inflector.rb +14 -5
  113. data/lib/rigor/plugin/io_boundary.rb +107 -4
  114. data/lib/rigor/plugin/loader.rb +14 -0
  115. data/lib/rigor/protection/analysis_guard.rb +60 -0
  116. data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
  117. data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
  118. data/lib/rigor/protection/discovery_seed.rb +1 -1
  119. data/lib/rigor/protection/measurement_integrity.rb +36 -0
  120. data/lib/rigor/protection/mutation_scanner.rb +61 -6
  121. data/lib/rigor/protection/mutator.rb +121 -18
  122. data/lib/rigor/reflection.rb +182 -30
  123. data/lib/rigor/scope/discovery_index.rb +39 -1
  124. data/lib/rigor/scope.rb +385 -4
  125. data/lib/rigor/sig_gen/classification.rb +12 -1
  126. data/lib/rigor/sig_gen/generator.rb +254 -5
  127. data/lib/rigor/sig_gen/renderer.rb +1 -6
  128. data/lib/rigor/sig_gen/writer.rb +3 -0
  129. data/lib/rigor/source/constant_path.rb +79 -2
  130. data/lib/rigor/triage/catalogue.rb +1 -1
  131. data/lib/rigor/type/combinator.rb +10 -0
  132. data/lib/rigor/type/maybe.rb +47 -0
  133. data/lib/rigor/type/refined.rb +1 -2
  134. data/lib/rigor/type/result.rb +53 -0
  135. data/lib/rigor/type.rb +2 -0
  136. data/lib/rigor/version.rb +1 -1
  137. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
  138. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
  139. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
  140. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
  141. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
  142. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
  143. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
  144. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
  145. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
  146. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
  147. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
  148. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
  149. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
  150. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
  151. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
  152. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
  153. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
  154. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
  156. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
  157. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
  158. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
  159. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
  160. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
  163. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
  164. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
  165. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
  166. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
  167. data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
  168. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
  169. data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
  170. data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
  171. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
  172. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
  173. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
  174. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
  175. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
  176. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
  177. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
  178. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
  179. data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
  180. data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
  181. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
  182. data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
  183. data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
  184. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
  185. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
  186. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  187. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
  188. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
  189. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
  190. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
  191. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
  192. data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
  193. data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
  194. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
  195. data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
  196. data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
  197. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
  198. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
  199. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
  200. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
  201. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
  202. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
  203. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
  204. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
  205. data/sig/rigor/analysis/baseline.rbs +1 -1
  206. data/sig/rigor/environment.rbs +3 -0
  207. data/sig/rigor/inference.rbs +4 -1
  208. data/sig/rigor/plugin/io_boundary.rbs +2 -0
  209. data/sig/rigor/reflection.rbs +3 -1
  210. data/sig/rigor/scope.rbs +25 -1
  211. data/sig/rigor/type.rbs +95 -1
  212. data/sig/rigor.rbs +16 -0
  213. metadata +69 -3
@@ -12,6 +12,8 @@ require_relative "dependency_recorder"
12
12
  require_relative "check_rules/rule_ids"
13
13
  require_relative "check_rules/inferred_param_guard"
14
14
  require_relative "check_rules/declaration_sourced_guard"
15
+ require_relative "check_rules/published_constant_guard"
16
+ require_relative "check_rules/dead_version_guard_arms"
15
17
  require_relative "check_rules/rule_walk"
16
18
  require_relative "check_rules/always_truthy_condition_collector"
17
19
  require_relative "check_rules/unreachable_clause_collector"
@@ -50,7 +52,7 @@ module Rigor
50
52
  # resolves these to their runtime class for dispatch.
51
53
  # - receivers whose class name is NOT registered in the
52
54
  # loader (RBS-blind environments, unknown stdlib).
53
- # rubocop:disable Metrics/ModuleLength
55
+ # rubocop:disable-next Metrics/ModuleLength
54
56
  module CheckRules
55
57
  # ADR-87 WD4 — the canonical rule-id table lives in the light `check_rules/rule_ids.rb` (required at the
56
58
  # top of this file) so {Analysis::RuleCatalog} can read it without loading the engine.
@@ -102,6 +104,11 @@ module Rigor
102
104
  COLLECTOR_DIAGNOSTIC_BUILDERS.each do |role, builder|
103
105
  diagnostics.concat(send(builder, path, collectors[role].results))
104
106
  end
107
+ # ADR-47 WD5 — the dead arm of a decidable version guard (#627) reports nothing: it cannot run on
108
+ # the Ruby being checked with, so a diagnostic there is a false positive. Applied before
109
+ # `suppression.*` joins the list, which stays reportable inside a dead arm — a malformed
110
+ # `# rigor:disable` marker is an authoring error regardless of whether its code runs.
111
+ diagnostics = DeadVersionGuardArms.filter(diagnostics, root)
105
112
  # Suppression-marker validation (`suppression.*`) runs BEFORE the filter so its own diagnostics are
106
113
  # suppressible like any other rule — `# rigor:disable suppression.unknown-rule` on the offending
107
114
  # comment's line works, with no regress (the token itself is known, so it never re-fires).
@@ -649,7 +656,7 @@ module Rigor
649
656
  resolved.nil? || resolved.empty? ? [token] : resolved
650
657
  end
651
658
 
652
- # rubocop:disable Metrics/ClassLength
659
+ # rubocop:disable-next Metrics/ClassLength
653
660
  class << self
654
661
  private
655
662
 
@@ -659,9 +666,14 @@ module Rigor
659
666
  scope = scope_index[call_node]
660
667
  return nil if scope.nil?
661
668
 
662
- # ADR-67 WD6b an inferred-parameter receiver's type is an open-call-site lower bound; firing
663
- # undefined-method against it is an FP by construction. Decline.
664
- return nil if inferred_param_receiver?(call_node, scope)
669
+ # The two exemptions that hold whatever shape the receiver has. Ahead of the receiver-shape
670
+ # 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.
675
+ # See {#call_site_exempt?}.
676
+ return nil if call_site_exempt?(call_node, scope)
665
677
 
666
678
  # N3 — a safe-navigation call (`recv&.m`) never dispatches on the
667
679
  # nil edge of its receiver: at runtime it short-circuits to nil.
@@ -705,7 +717,7 @@ module Rigor
705
717
  # because the real type is empty (the real `DRb` has
706
718
  # `start_service`), so enumerating it to prove a call
707
719
  # "undefined" would be a false positive.
708
- return nil if unbounded_receiver_surface?(class_name, scope)
720
+ return nil if unenumerable_receiver?(class_name, scope)
709
721
 
710
722
  # Slice 7 phase 12 — suppress when the user has
711
723
  # declared the method in source (`def` /
@@ -730,21 +742,73 @@ module Rigor
730
742
  method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
731
743
  return nil if method_def
732
744
 
733
- # Module-mixin fallback (mirror of
734
- # `MethodDispatcher#user_class_fallback_receiver`'s module
735
- # path): an instance method on a module-mixin like
736
- # `PP::ObjectMixin` observes Kernel / Object methods
737
- # through every concrete includer's ancestor chain, so an
738
- # unresolved `self.inspect` / `self.respond_to?` /
739
- # `self.class` MUST NOT fire `undefined-method`. Retry
740
- # against Object before the rule fires.
741
- return nil if module_mixin_receiver?(receiver_type, scope) &&
742
- lookup_method(receiver_type, "Object", call_node.name, scope)
745
+ return nil if last_resort_surface_answers?(receiver_type, class_name, call_node, scope, kind)
743
746
 
744
747
  definition_site = project_definition_site(scope, class_name, call_node.name, kind)
748
+ # Issue #735 — the site is evidence, and whose RBS this is decides what it is evidence OF. On a
749
+ # BUNDLED declaration (core / stdlib / a gem) the class is not the project's, so a project `def`
750
+ # on it is the ADR-17 monkey-patch this rule surfaces with the `pre_eval:` nudge. On the
751
+ # project's OWN sidecar `sig/` the declaration describes the source being analysed, the "patch"
752
+ # is just another of the project's files, and the nudge is not something a user can act on: it
753
+ # asks them to `pre_eval:` their own application. `rigor sig-gen --write` on redmine turned 29
754
+ # `call.undefined-method` into 171 through exactly this reading, 49 of them here.
755
+ return nil if project_sidecar_owns_method?(scope, class_name, call_node.name, kind)
756
+
745
757
  build_undefined_method_diagnostic(path, call_node, receiver_type, definition_site, class_name)
746
758
  end
747
759
 
760
+ # The two probes that run only once every cheaper answer has come back "absent", kept together
761
+ # because they share that position and nothing else.
762
+ #
763
+ # - Issue #739 — an instance-side MODULE receiver. A value typed as a mixin module is an instance of
764
+ # some class that includes it, and that class contributes an arbitrary surface: inside
765
+ # `module Attachable::InstanceMethods`, `self.project` is defined by the ActiveRecord model that
766
+ # includes the module, and in RBS a parameter typed `Taggable` means "something whose class
767
+ # includes Taggable", not "something whose methods are Taggable's". The surface is not
768
+ # enumerable, so nothing here can prove a method absent. This probe used to RETRY against Object
769
+ # — enough for `self.inspect` / `self.class`, and nothing else — which left 45 firings on
770
+ # redmine's `acts_as_*` mixins the moment a `sig/` declared them.
771
+ #
772
+ # The union twin already declines these arms outright (`union_arm_blocks_undefined_fire?`, "a
773
+ # module mixin whose Object-inherited methods the per-arm lookup would miss"). The two rules
774
+ # answering the same question differently about the same receiver is the fork this closes; the
775
+ # union's reading is the correct one.
776
+ #
777
+ # Singleton-side module receivers are NOT covered and must not be: `M.typo` on a namespace module
778
+ # with `module_function` / `def self.` has a real, enumerable surface and keeps firing.
779
+ # - Issue #723 — the project's own ancestry. Everything above answered "the RBS does not have
780
+ # it"; a project ancestor still might, and the typer has already resolved the site through
781
+ # exactly this walk. It is last because it is the only probe here that walks the class graph
782
+ # (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)
784
+ return true if module_mixin_receiver?(receiver_type, scope)
785
+ return true if mixin_self_class_receiver?(call_node, scope)
786
+ return true if unknown_mixin_includer?(class_name, scope)
787
+
788
+ ancestry_declares_method?(scope, class_name, call_node.name, kind)
789
+ end
790
+
791
+ # Issue #739 — the singleton twin of the module-mixin receiver, and the only shape that reaches the
792
+ # singleton side legitimately. `self.class` inside a mixin's instance method is the INCLUDER's class
793
+ # at runtime; the engine types it `Singleton[<the module>]`, one hop along from the same wrong
794
+ # `self`. redmine's `acts_as_*` mixins reach their configuration that way
795
+ # (`self.class.attachable_options`), 17 sites of it.
796
+ #
797
+ # Keyed on the call-site SYNTAX rather than on the receiver type, because `Singleton[M]` is also
798
+ # what a namespace module's own `M.helper` produces, and that surface is real and enumerable — a
799
+ # type-only test would silence every `M.typo` in the project. Here the question is narrower: was
800
+ # this singleton produced by `.class` on a value whose type is a mixin module?
801
+ def mixin_self_class_receiver?(call_node, scope)
802
+ receiver = call_node.receiver
803
+ return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :class
804
+ return false unless receiver.arguments.nil?
805
+
806
+ inner = receiver.receiver
807
+ return false if inner.nil?
808
+
809
+ module_mixin_receiver?(scope.type_of(inner), scope)
810
+ end
811
+
748
812
  # ADR-17 — when the project itself defines this method on the
749
813
  # receiver class somewhere in the analyzed file set (a reopened
750
814
  # core/stdlib/gem class the dispatcher does not apply cross-
@@ -761,6 +825,38 @@ module Rigor
761
825
  scope.user_def_site_for(class_name, method_name)
762
826
  end
763
827
 
828
+ # Issue #735 — the site is evidence, and whose RBS the class has decides what it is evidence OF. On
829
+ # a BUNDLED declaration (core / stdlib / a gem) the class is not the project's, so a project `def`
830
+ # on it is the ADR-17 monkey-patch this rule surfaces with the `pre_eval:` nudge. On the project's
831
+ # OWN sidecar `sig/` the declaration describes the source being analysed, the "patch" is just
832
+ # another of the project's files, and the nudge is not something a user can act on: it asks them to
833
+ # `pre_eval:` their own application. `rigor sig-gen --write` on redmine turned 29
834
+ # `call.undefined-method` into 171 through exactly this reading, 49 of them here.
835
+ def project_sidecar_owns_method?(scope, class_name, method_name, kind)
836
+ Rigor::Reflection.project_declared_class?(class_name, scope: scope) &&
837
+ project_defines_method?(scope, class_name, method_name, kind)
838
+ end
839
+
840
+ # Issue #735 — "does the project define this method, on either side, somewhere in its own files".
841
+ # Distinct from {#project_definition_site}, which answers the narrower question the ADR-17 MESSAGE
842
+ # asks (instance-side only, because that is the monkey-patch shape whose `pre_eval:` advice applies).
843
+ # The suppression needs both sides: `def self.available_search_types` in one file and its call in
844
+ # another is the same "second file of my own class" shape as the instance case, and on redmine it is
845
+ # the larger half — modules with class-method surfaces are how that codebase is organised.
846
+ def project_defines_method?(scope, class_name, method_name, kind)
847
+ if kind == :singleton
848
+ # The singleton side has no ancestor walk to ask (#731); it answers for the receiver's own name.
849
+ !scope.user_singleton_def_site_for(class_name, method_name).nil?
850
+ else
851
+ # Through the project's ancestry, not the receiver's name alone: a base class defining a helper
852
+ # its subclasses call is ordinary Ruby, and a sidecar `sig/` that declares the SUBCLASS without
853
+ # that helper was reporting it (`AbstractAdapter#url`, called from `FilesystemAdapter#target`).
854
+ # The def-node walk is what has to answer here — `discovered_methods` deliberately withholds a
855
+ # plain cross-file `def` (`ScopeIndexer#finalize_def_index`), which is exactly this shape.
856
+ !scope.user_def_through_ancestors(class_name, method_name).first.nil?
857
+ end
858
+ end
859
+
764
860
  def module_mixin_receiver?(receiver_type, scope)
765
861
  return false unless receiver_type.is_a?(Type::Nominal)
766
862
  return false if scope.environment.nil?
@@ -783,6 +879,22 @@ module Rigor
783
879
  project_patched_method?(scope, class_name, method_name, kind)
784
880
  end
785
881
 
882
+ # Issue #723 — the same question as {#source_declared_method?}, asked through the project's own
883
+ # ANCESTRY rather than the receiver's name alone. A class whose `sig/` declares it without its
884
+ # project superclass typed `:admin_val` under `dump_type` and drew `undefined method 'admin_val'`
885
+ # at the same column of the same run: the probe read a name-keyed table while the typer resolved
886
+ # the call through the class graph.
887
+ #
888
+ # It is deliberately NOT folded into `source_declared_method?`, which every `call.*` rule consults
889
+ # early on its hot path. The walk reads `Scope#superclass_of` / `#includes_of`, and those record an
890
+ # ADR-46 file-level ancestry edge; running it for every call would have made `Widget.new` — a call
891
+ # RBS answers, that never reaches a verdict — record an ancestry dependency on `Widget`'s
892
+ # declaring file, coarsening incremental invalidation for the whole project. Asked HERE, one step
893
+ # before the diagnostic is built, the edge is recorded exactly where the answer depended on it.
894
+ def ancestry_declares_method?(scope, class_name, method_name, kind)
895
+ scope.discovered_method_through_ancestors?(class_name, method_name, kind)
896
+ end
897
+
786
898
  # ADR-17 § "Inference contract" — consults
787
899
  # `Environment#project_patched_methods` so a `def` declared
788
900
  # in a `pre_eval:` file suppresses the diagnostic at the
@@ -853,6 +965,52 @@ module Rigor
853
965
  )
854
966
  end
855
967
 
968
+ # Issue #742 — the receivers whose method surface this rule cannot enumerate, in one question.
969
+ #
970
+ # {#unbounded_receiver_surface?} covers the ADR-26 open receivers and Rigor's own synthesized stubs.
971
+ # The generic metaclasses join it here: a value typed `Class` or `Module` is SOME class or module
972
+ # object, and its singleton methods cannot be read off the metaclass — `def self.included(base)`
973
+ # receives the includer, so `base.class_attribute :main_menu` and `base.main_menu = true` are calls
974
+ # on whatever included the module. The union twin has declined these arms since it was written
975
+ # (`METACLASS_ARMS`, "a `plugin_class : Class` really holds a `Plugin` subclass with `.manifest`");
976
+ # the scalar rule enumerated `Module`'s own RBS instead and reported them.
977
+ #
978
+ # 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)
983
+ end
984
+
985
+ # Issue #746 — a class that includes a module the environment does not know. `SudoMode::Form`
986
+ # includes `ActiveModel::Validations`, which no RBS in redmine's environment declares, and `valid?`
987
+ # comes from it: the ancestors are not fully known, so the method table is not enumerable and a
988
+ # verdict read off the part we can see is unsound. Same reasoning `unbounded_receiver_surface?`
989
+ # already applies to an ADR-26 open receiver and to Rigor's own synthesized stubs.
990
+ #
991
+ # Asked LATE, with {#ancestry_declares_method?} and for the same reason: `Scope#includes_of` records
992
+ # an ADR-46 file-level ancestry edge, so running it on the hot path made `Widget.new` — a call RBS
993
+ # answers, that never reaches a verdict — depend on `Widget`'s declaring file.
994
+ # `dependency_recorder_spec` caught it here exactly as it caught #723's walk.
995
+ #
996
+ # The receiver's OWN includes only. A project ancestor's unknown include makes the surface just as
997
+ # unknown; walking the chain is a deliberate deferral rather than an oversight.
998
+ def unknown_mixin_includer?(class_name, scope)
999
+ includes = scope.includes_of(class_name)
1000
+ return false if includes.empty?
1001
+
1002
+ includes.any? { |raw| !known_mixin?(raw, class_name, scope) }
1003
+ end
1004
+
1005
+ def known_mixin?(raw_name, class_name, scope)
1006
+ scope.ancestor_name_candidates(class_name, raw_name).any? do |candidate|
1007
+ next true if scope.discovered_classes.key?(candidate)
1008
+
1009
+ Rigor::Reflection.rbs_class_known?(candidate, scope: scope) &&
1010
+ !synthesized_stub_receiver?(candidate, scope)
1011
+ end
1012
+ end
1013
+
856
1014
  # Returns a qualified class name for the in-scope check.
857
1015
  # Nominal / Singleton carry a single-class identity
858
1016
  # directly. Constant projects to its value's class.
@@ -918,16 +1076,43 @@ module Rigor
918
1076
  nil
919
1077
  end
920
1078
 
1079
+ # ADR-26's own counterpart, not a use of it — issue #660 tracks whether this deserves a first-class
1080
+ # mechanism of its own rather than living here as a constant. `open_receivers:` (the manifest field,
1081
+ # below) only takes effect once a project LOADS the plugin that names it, but a gem overlay
1082
+ # (`data/gem_overlay/`, ADR-72) is auto-applied to every project that locks the gem WITHOUT opting
1083
+ # into a plugin — there is no manifest in that path for an `open_receivers:` entry to live on. This
1084
+ # is that path's counterpart: the class names a BUNDLED overlay declares knowing the declaration is
1085
+ # partial, protected independent of any plugin manifest.
1086
+ #
1087
+ # MEMBERSHIP ALONE is not enough, though: `unbounded_receiver_surface?` (below) also requires the
1088
+ # overlay directory that made the entry true to have actually LOADED this run — otherwise a project
1089
+ # that never locks the gem, and happens to own a class of the same name itself, would silently lose
1090
+ # `call.undefined-method` (and inherited-name `wrong-arity`) on that class, the exact regression this
1091
+ # gate exists to avoid (ADR-26 WD1: the protection is active exactly when the RBS it protects is).
1092
+ #
1093
+ # `ActiveSupport::Duration` is the first (and, as of #632, only) member: it forwards any method its
1094
+ # own class doesn't define to the wrapped numeric via `method_missing` (audited against
1095
+ # ActiveSupport 8.1.3.1's `lib/active_support/duration.rb` — see
1096
+ # `data/gem_overlay/activesupport/core_ext.rbs`'s `ActiveSupport::Duration` comment for the full
1097
+ # audit), so declaring even its reader surface makes it RBS-known — and, without this, every member
1098
+ # the declaration omits would become a false `call.undefined-method` for every project that locks
1099
+ # activesupport, the overwhelming majority of which never opt into the plugin at all.
1100
+ GEM_OVERLAY_OPEN_RECEIVERS = Set["ActiveSupport::Duration"].freeze
1101
+ private_constant :GEM_OVERLAY_OPEN_RECEIVERS
1102
+
921
1103
  # ADR-26 — whether `class_name` is declared "open" by a
922
1104
  # loaded plugin (manifest `open_receivers:`). An open
923
1105
  # class responds beyond its RBS surface, so the
924
1106
  # `call.undefined-method` rule must not fire for it.
925
1107
  # True when the receiver class responds beyond an enumerable
926
1108
  # RBS method table, so proving a call "undefined" against it is
927
- # unsound: a plugin-declared open receiver, or a Rigor-
928
- # synthesized stub type (see `RbsLoader#synthesized_type_names`).
1109
+ # unsound: a plugin-declared open receiver, a Rigor-bundled-
1110
+ # overlay open receiver ({GEM_OVERLAY_OPEN_RECEIVERS}) whose
1111
+ # bundled directory this run actually loaded, or a
1112
+ # Rigor-synthesized stub type (see `RbsLoader#synthesized_type_names`).
929
1113
  def unbounded_receiver_surface?(class_name, scope)
930
- open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope)
1114
+ open_receiver?(class_name, scope) || synthesized_stub_receiver?(class_name, scope) ||
1115
+ (GEM_OVERLAY_OPEN_RECEIVERS.include?(class_name.to_s.sub(/\A::/, "")) && gem_overlay_loaded?(scope))
931
1116
  end
932
1117
 
933
1118
  def open_receiver?(class_name, scope)
@@ -937,6 +1122,79 @@ module Rigor
937
1122
  registry.open_receiver?(class_name)
938
1123
  end
939
1124
 
1125
+ # Whether THIS run's RBS environment actually includes one of Rigor's OWN partial declarations of the
1126
+ # overlaid gem — {GEM_OVERLAY_OPEN_RECEIVERS} membership alone must not grant the exemption (see that
1127
+ # constant's comment). `RbsLoader#signature_paths` is the same list `Environment.for_project` builds
1128
+ # from `resolved_paths + plugin_sig_paths + gem_sig_paths + collection_paths + overlay_paths`, and
1129
+ # two of its members can carry that declaration:
1130
+ #
1131
+ # - a `data/gem_overlay/<gem>/` directory, which is a member if and only if `gem_overlay_paths`
1132
+ # picked it (the gem is locked, `:missing` coverage, and its plugin twin is NOT reachable —
1133
+ # ADR-72), so the `RbsLoader.under_gem_overlay_root?` prefix match is exact: no other signature
1134
+ # source lives under that root;
1135
+ # - the bundled plugin twin's own `sig/`, when a project wires it through `signature_paths:` instead
1136
+ # of `plugins:`. Before #672 that route loaded BOTH halves and `Duration` collapsed outright, so
1137
+ # nothing fired and nothing needed protecting; now the overlay stands down and the twin's
1138
+ # declaration is the one that loads — with no manifest behind it, so `open_receiver?` above stays
1139
+ # false and this is the only thing standing between a real `Duration` receiver and a false
1140
+ # `call.undefined-method` on every member the declaration omits.
1141
+ #
1142
+ # When the PLUGIN itself is loaded the first arm goes false (the overlay stands down) and the second
1143
+ # goes true — its `sig/` rides in as a plugin signature path — but nothing here decides that case:
1144
+ # `open_receiver?`'s manifest-driven check short-circuits above it. The two answers agree, which is
1145
+ # the point; this predicate asks only whether a Rigor-authored partial declaration is in the
1146
+ # environment, never how the project asked for it.
1147
+ #
1148
+ # The two arms are asked in that order, over the WHOLE list each time, rather than interleaved per
1149
+ # path: the overlay arm is a string prefix and the twin arm canonicalises paths on the filesystem,
1150
+ # so the overwhelmingly common overlay-only run never reaches a syscall here.
1151
+ def gem_overlay_loaded?(scope)
1152
+ loader = scope.environment&.rbs_loader
1153
+ return false if loader.nil? || !loader.respond_to?(:signature_paths)
1154
+
1155
+ paths = loader.signature_paths
1156
+ paths.any? { |path| Rigor::Environment::RbsLoader.under_gem_overlay_root?(path) } ||
1157
+ Rigor::Environment::RbsLoader.gem_overlay_twin_signatures_loaded?(paths)
1158
+ end
1159
+
1160
+ # The `call.undefined-method` exemptions that depend only on the CALL SITE, never on the receiver's
1161
+ # type or class — so they are decided before the rule looks at the receiver at all:
1162
+ #
1163
+ # - ADR-67 WD6b — an inferred-parameter receiver's type is an open-call-site lower bound, so firing
1164
+ # undefined-method against it is a false positive by construction.
1165
+ # - Issue #653 — a plugin answered this call site ({#plugin_typed_call?}).
1166
+ def call_site_exempt?(call_node, scope)
1167
+ inferred_param_receiver?(call_node, scope) || plugin_typed_call?(call_node, scope)
1168
+ end
1169
+
1170
+ # Issue #653 — true when a plugin's `dynamic_return` supplied the return type for THIS call node
1171
+ # (recorded by `MethodDispatcher` on `Scope#plugin_typed_calls` during the typing pass, exactly the
1172
+ # way `Scope#void_origins` is recorded and consumed).
1173
+ #
1174
+ # Why the rule declines. The plugin tier sits ABOVE `RbsDispatch` in `MethodDispatcher#resolve`, so
1175
+ # a plugin answer means the RBS never dispatched the call: the type at the site is the plugin's.
1176
+ # Reading that same receiver's RBS afterwards to prove the call undefined puts the two subsystems in
1177
+ # direct contradiction on one line of one run — the plugin says `Rails.logger` is an
1178
+ # `ActiveSupport::BroadcastLogger`, the rule says `logger` does not exist. Under ADR-5 a
1179
+ # partially-declared receiver is not a closed world, and incompleteness is exactly what a plugin is
1180
+ # loaded to cover, so the plugin's answer is the honest one. Without this, adding a four-line
1181
+ # partial `sig/` for a class a plugin already covered turned every covered call site into an error
1182
+ # — writing more RBS made a project's diagnostics worse.
1183
+ #
1184
+ # Deliberately NOT scoped to "the RBS is silent about this method": the precedence recorded here is
1185
+ # the dispatcher's own, and the dispatcher does not consult the RBS before letting a plugin answer.
1186
+ # For THIS rule the two readings coincide anyway — a method the RBS declares is found by
1187
+ # `lookup_method` below and never reaches the diagnostic — so the broader statement costs no
1188
+ # diagnostic and keeps the contract stated in one direction. `docs/internal-spec/plugin.md` §
1189
+ # "Return-type and narrowing contributions" is where it binds.
1190
+ #
1191
+ # Unlike {#unbounded_receiver_surface?} this is per CALL SITE, not per receiver class: a plugin
1192
+ # answering `Rails.logger` exempts that call and nothing else, so `Rails.no_such_reader` on the same
1193
+ # receiver still fires.
1194
+ def plugin_typed_call?(call_node, scope)
1195
+ scope.plugin_typed_call?(call_node)
1196
+ end
1197
+
940
1198
  def synthesized_stub_receiver?(class_name, scope)
941
1199
  loader = scope.environment&.rbs_loader
942
1200
  return false if loader.nil? || !loader.respond_to?(:synthesized_type_names)
@@ -944,6 +1202,49 @@ module Rigor
944
1202
  loader.synthesized_type_names.include?(class_name.to_s.sub(/\A::/, ""))
945
1203
  end
946
1204
 
1205
+ # ADR-26 — the SIGNATURE half of the open-receiver policy. `call.undefined-method` already declines
1206
+ # on an unbounded surface: a plugin vouches that the class answers names its RBS cannot enumerate
1207
+ # (an `ActiveRecord::Relation` delegates every user-declared `scope` to its model). The rules that
1208
+ # read a RESOLVED SIGNATURE — wrong-arity and argument-type-mismatch — must decline on the same
1209
+ # ground, but only where the name resolved through an ANCESTOR rather than the open class itself.
1210
+ #
1211
+ # The failure it closes: `Issue.visible.open` is a declared scope taking `*args`, but `open` is also
1212
+ # `Kernel#open`, so resolution walks the Relation's ancestry, finds the IO opener and checks the
1213
+ # call against `(String, ...)` — `wrong number of arguments (given 0, expected 1..3)` on correct
1214
+ # code, plus `argument type mismatch: expected String, got false` on `open(false)`. The collision
1215
+ # set is every Object/Kernel name, so any project whose scope is called `open`, `select`, `test`,
1216
+ # `format`, `p`, `system`, … inherits the error the moment the first hop types. That is a wrong
1217
+ # signature applied to a method the receiver does not actually run — not a strict reading of a real
1218
+ # one — and it fires on code that works.
1219
+ #
1220
+ # A method the open class DECLARES stays checked: `Relation#limit` is in the plugin's own bundled
1221
+ # RBS, so `relation.limit` with no arguments still fires. The suppression is therefore bounded to
1222
+ # names the open class inherited, where the resolution was never authoritative to begin with.
1223
+ # The RBS method definition this call may be validated against, or nil when there is none worth
1224
+ # trusting: the lookup found nothing, it returned {#lookup_method}'s defensive `true` sentinel, or
1225
+ # the name is one an open receiver INHERITED rather than declared. Shared by the two rules that read
1226
+ # a resolved signature — `call.wrong-arity` and `call.argument-type-mismatch` — so they cannot drift
1227
+ # on which signatures are authoritative.
1228
+ def trustworthy_signature(receiver_type, class_name, call_node, scope)
1229
+ method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
1230
+ return nil if method_def.nil? || method_def == true
1231
+ return nil if unauthoritative_inherited_signature?(class_name, method_def, scope)
1232
+
1233
+ method_def
1234
+ end
1235
+
1236
+ def unauthoritative_inherited_signature?(class_name, method_def, scope)
1237
+ return false unless unbounded_receiver_surface?(class_name, scope)
1238
+ return true unless method_def.respond_to?(:defined_in)
1239
+
1240
+ defined_in = method_def.defined_in
1241
+ return true if defined_in.nil?
1242
+
1243
+ strip_root(defined_in.to_s) != strip_root(class_name)
1244
+ end
1245
+
1246
+ def strip_root(name) = name.to_s.sub(/\A::/, "")
1247
+
947
1248
  def definition_available?(receiver_type, class_name, scope)
948
1249
  if receiver_type.is_a?(Type::Singleton)
949
1250
  !Rigor::Reflection.singleton_definition(class_name, scope: scope).nil?
@@ -1026,6 +1327,10 @@ module Rigor
1026
1327
  # an unrelated namespace.
1027
1328
  def parent_matches?(child, parent, class_name)
1028
1329
  parent_name = parent.to_s
1330
+ # Issue #722 residue 1 — a rooted parent (`< ::Base`) names the top level, so the namespace walk
1331
+ # below must not run for it. This peel is one of the two that predate
1332
+ # `Scope#ancestor_name_candidates` and resolve their own pair.
1333
+ return parent_name.delete_prefix("::") == class_name if parent_name.start_with?("::")
1029
1334
  return true if parent_name == class_name
1030
1335
 
1031
1336
  segments = child.to_s.split("::")[0...-1]
@@ -1120,7 +1425,7 @@ module Rigor
1120
1425
  # by `undefined_method_diagnostic`; it returns nil
1121
1426
  # when the call's receiver / RBS coverage / call shape
1122
1427
  # disqualifies the rule.
1123
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1428
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1124
1429
  def wrong_arity_diagnostic(path, call_node, scope_index)
1125
1430
  return nil if call_node.receiver.nil?
1126
1431
  return nil unless plain_positional_call?(call_node)
@@ -1151,8 +1456,8 @@ module Rigor
1151
1456
  return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
1152
1457
  return nil unless definition_available?(receiver_type, class_name, scope)
1153
1458
 
1154
- method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
1155
- return nil if method_def.nil? || method_def == true
1459
+ method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
1460
+ return nil if method_def.nil?
1156
1461
 
1157
1462
  arity_envelope = compute_arity_envelope(method_def)
1158
1463
  return nil if arity_envelope.nil?
@@ -1163,7 +1468,6 @@ module Rigor
1163
1468
 
1164
1469
  build_arity_diagnostic(path, call_node, class_name, min, max, actual)
1165
1470
  end
1166
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1167
1471
 
1168
1472
  # True for the outer `.new` of a chained `Struct.new(...).new`:
1169
1473
  # `class_name`/`kind` already pin the receiver to
@@ -1313,7 +1617,7 @@ module Rigor
1313
1617
  # method must be absent from `NilClass` (so the nil path really raises).
1314
1618
  def nil_bearing_union_witnesses?(receiver_type, method_name, scope)
1315
1619
  union_contains_nil?(receiver_type) &&
1316
- union_has_nameable_non_nil_arm?(receiver_type) &&
1620
+ union_has_nameable_non_nil_arm?(receiver_type, scope) &&
1317
1621
  union_method_present_on_non_nil?(receiver_type, method_name, scope) &&
1318
1622
  !nil_class_has_method?(method_name, scope)
1319
1623
  end
@@ -1355,8 +1659,22 @@ module Rigor
1355
1659
  # `String | nil` keeps firing, and so does `Dynamic | String | nil` —
1356
1660
  # the nameless arm stays permissive inside the all-arms check, which is
1357
1661
  # only about the arms' method surface.
1358
- def union_has_nameable_non_nil_arm?(union)
1359
- union.members.any? { |m| !nil_member?(m) && !concrete_class_name(m).nil? }
1662
+ def union_has_nameable_non_nil_arm?(union, scope)
1663
+ union.members.any? do |m|
1664
+ next false if nil_member?(m)
1665
+
1666
+ class_name = concrete_class_name(m)
1667
+ next false if class_name.nil?
1668
+
1669
+ # Issue #574 — nameable is not the same as ANSWERABLE. `method_present_anywhere?` reports
1670
+ # "present" for a named arm nothing knows the surface of, purely because nothing is known —
1671
+ # which is the same vacuity this gate's docstring says it exists to prevent, one level in. A
1672
+ # plugin-minted nominal like `ActionController::Parameters` ships no RBS and has no project
1673
+ # source, so it counted as a full witness and `q = params[:q]; q.strip` fired at ERROR severity
1674
+ # on correct Rails code.
1675
+ Rigor::Reflection.rbs_class_known?(class_name, scope: scope) ||
1676
+ scope.discovered_classes.key?(class_name)
1677
+ end
1360
1678
  end
1361
1679
 
1362
1680
  # The non-nil members must collectively support the
@@ -1379,8 +1697,11 @@ module Rigor
1379
1697
  return true if scope.discovered_method?(class_name, method_name, :instance)
1380
1698
  return true unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
1381
1699
  return true unless definition_available?(member, class_name, scope)
1700
+ return true if lookup_method(member, class_name, method_name, scope)
1382
1701
 
1383
- !lookup_method(member, class_name, method_name, scope).nil?
1702
+ # Issue #723 — the arm's own RBS does not have it; its project ancestry still might. Last, for
1703
+ # the reason {#ancestry_declares_method?} carries: this is the probe that walks the class graph.
1704
+ ancestry_declares_method?(scope, class_name, method_name, :instance)
1384
1705
  end
1385
1706
 
1386
1707
  def nil_class_has_method?(method_name, scope)
@@ -2126,8 +2447,8 @@ module Rigor
2126
2447
  return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
2127
2448
  return nil unless definition_available?(receiver_type, class_name, scope)
2128
2449
 
2129
- method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
2130
- return nil if method_def.nil? || method_def == true
2450
+ method_def = trustworthy_signature(receiver_type, class_name, call_node, scope)
2451
+ return nil if method_def.nil?
2131
2452
 
2132
2453
  param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
2133
2454
  mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides)
@@ -2516,8 +2837,8 @@ module Rigor
2516
2837
  function.trailing_positionals.empty?
2517
2838
  end
2518
2839
 
2519
- def translate_param_type(rbs_type, _environment)
2520
- Inference::RbsTypeTranslator.translate(rbs_type)
2840
+ def translate_param_type(rbs_type, environment)
2841
+ Inference::RbsTypeTranslator.translate(rbs_type, alias_expander: environment&.rbs_loader)
2521
2842
  rescue StandardError
2522
2843
  Type::Combinator.untyped
2523
2844
  end
@@ -2651,7 +2972,7 @@ module Rigor
2651
2972
  return nil unless self_type.respond_to?(:class_name)
2652
2973
 
2653
2974
  method_def =
2654
- if def_node.receiver.nil?
2975
+ if def_node.receiver.nil? && !singleton_context_def?(scope, self_type.class_name, def_node.name)
2655
2976
  Reflection.instance_method_definition(self_type.class_name, def_node.name, scope: scope)
2656
2977
  else
2657
2978
  Reflection.singleton_method_definition(self_type.class_name, def_node.name, scope: scope)
@@ -2664,16 +2985,27 @@ module Rigor
2664
2985
  declared_return_union(method_def, scope.environment)
2665
2986
  end
2666
2987
 
2988
+ # A receiverless `def` inside `class << self` is a singleton def; the def NODE alone cannot say
2989
+ # so, but the discovery walk recorded it under the singleton kind (and not the instance kind).
2990
+ # Without this, `class << self; def load` compared its body against `Kernel#load: -> bool` — the
2991
+ # inherited INSTANCE method — instead of its own `def self.load` signature. A name defined on
2992
+ # both facets stays on the instance lookup (the pre-existing conservative reading).
2993
+ def singleton_context_def?(scope, class_name, method_name)
2994
+ scope.discovered_method?(class_name, method_name, :singleton) &&
2995
+ !scope.discovered_method?(class_name, method_name, :instance)
2996
+ end
2997
+
2667
2998
  # `type_vars:` — ADR-35 WD9 tier 1. When the caller supplies a generic-instantiation
2668
2999
  # substitution map (parent-side, keyed by the parent class's declared type-parameter names),
2669
3000
  # a `-> T` return is translated at its instantiated type (`-> Integer`) rather than degrading
2670
3001
  # to `Dynamic[Top]`. The default empty map is the pre-WD9 behaviour, under which any type
2671
3002
  # variable degrades to `Dynamic[Top]` and the rule stays silent.
2672
- def declared_return_union(method_def, _environment, type_vars: {})
3003
+ def declared_return_union(method_def, environment, type_vars: {})
2673
3004
  translated = method_def.method_types.filter_map do |mt|
2674
3005
  Inference::RbsTypeTranslator.translate(
2675
3006
  mt.type.return_type,
2676
- self_type: nil, instance_type: nil, type_vars: type_vars
3007
+ self_type: nil, instance_type: nil, type_vars: type_vars,
3008
+ alias_expander: environment&.rbs_loader
2677
3009
  )
2678
3010
  rescue StandardError
2679
3011
  nil
@@ -2807,8 +3139,9 @@ module Rigor
2807
3139
  end
2808
3140
 
2809
3141
  # Direct ancestors of `class_name` as project-discovered, qualified names: included / prepended
2810
- # modules first, then the superclass. As-written names are resolved against the subclass's lexical
2811
- # nesting; names that resolve to no project class/module (RBS-known / third-party) are dropped.
3142
+ # modules first, then the superclass. As-written names are resolved against the nesting the
3143
+ # subclass's declaration header is written in (`Scope#ancestor_name_candidates`, the single owner of
3144
+ # that order); names that resolve to no project class/module (RBS-known / third-party) are dropped.
2812
3145
  def ancestor_class_names(scope, class_name)
2813
3146
  names = []
2814
3147
  scope.includes_of(class_name).each do |raw|
@@ -2824,11 +3157,10 @@ module Rigor
2824
3157
  end
2825
3158
 
2826
3159
  def resolve_override_ancestor_name(scope, subclass_qualified, raw_ancestor)
2827
- segments = subclass_qualified.to_s.split("::")
2828
- (segments.length - 1).downto(0) do |i|
2829
- candidate = (segments[0, i] + [raw_ancestor]).join("::")
2830
- return candidate if known_user_class?(scope, candidate)
2831
- end
3160
+ resolved = scope.ancestor_name_candidates(subclass_qualified, raw_ancestor)
3161
+ .find { |candidate| known_user_class?(scope, candidate) }
3162
+ return resolved if resolved
3163
+
2832
3164
  # ADR-46 slice 3 — the override checker reads the class graph
2833
3165
  # directly (not through the recorder's `Scope` choke points), and
2834
3166
  # short-circuits when the ancestor resolves to no project class, so
@@ -2971,7 +3303,10 @@ module Rigor
2971
3303
  return {} if param_names.empty? || param_names.size != args.size
2972
3304
 
2973
3305
  translated = args.map do |arg|
2974
- Inference::RbsTypeTranslator.translate(arg, self_type: nil, instance_type: nil, type_vars: {})
3306
+ Inference::RbsTypeTranslator.translate(
3307
+ arg, self_type: nil, instance_type: nil, type_vars: {},
3308
+ alias_expander: scope.environment&.rbs_loader
3309
+ )
2975
3310
  rescue StandardError
2976
3311
  nil
2977
3312
  end
@@ -3028,8 +3363,9 @@ module Rigor
3028
3363
 
3029
3364
  scope, override_method, parent_class, parent_method = resolved
3030
3365
  parent_type_vars = ancestor_instantiation_type_vars(scope, parent_class)
3031
- override_params = positional_param_types(override_method)
3032
- parent_params = positional_param_types(parent_method, type_vars: parent_type_vars)
3366
+ override_params = positional_param_types(override_method, environment: scope.environment)
3367
+ parent_params = positional_param_types(parent_method, type_vars: parent_type_vars,
3368
+ environment: scope.environment)
3033
3369
  return nil if override_params.nil? || parent_params.nil?
3034
3370
 
3035
3371
  index = first_narrowed_param_index(override_params, parent_params)
@@ -3049,7 +3385,7 @@ module Rigor
3049
3385
  # map (parent class's type params → the subclass's instantiation) so a parent `(T)` parameter
3050
3386
  # is compared at its instantiated type. Empty (the default, and the override side) keeps the
3051
3387
  # pre-WD9 degrade-to-`Dynamic[Top]` behaviour.
3052
- def positional_param_types(method_def, type_vars: {})
3388
+ def positional_param_types(method_def, type_vars: {}, environment: nil)
3053
3389
  method_types = method_def.method_types
3054
3390
  return nil unless method_types.size == 1
3055
3391
 
@@ -3058,7 +3394,8 @@ module Rigor
3058
3394
 
3059
3395
  (func.required_positionals + func.optional_positionals).map do |param|
3060
3396
  Inference::RbsTypeTranslator.translate(
3061
- param.type, self_type: nil, instance_type: nil, type_vars: type_vars
3397
+ param.type, self_type: nil, instance_type: nil, type_vars: type_vars,
3398
+ alias_expander: environment&.rbs_loader
3062
3399
  )
3063
3400
  rescue StandardError
3064
3401
  nil
@@ -3096,8 +3433,6 @@ module Rigor
3096
3433
  )
3097
3434
  end
3098
3435
  end
3099
- # rubocop:enable Metrics/ClassLength
3100
3436
  end
3101
- # rubocop:enable Metrics/ModuleLength
3102
3437
  end
3103
3438
  end