rigortype 0.1.19 → 0.2.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
  3. data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
  4. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  5. data/lib/rigor/analysis/check_rules.rb +492 -71
  6. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  8. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  9. data/lib/rigor/analysis/fact_store.rb +5 -4
  10. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
  12. data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
  13. data/lib/rigor/analysis/runner.rb +17 -6
  14. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  15. data/lib/rigor/analysis/worker_session.rb +10 -14
  16. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  17. data/lib/rigor/cache/store.rb +5 -3
  18. data/lib/rigor/cli/annotate_command.rb +28 -7
  19. data/lib/rigor/cli/baseline_command.rb +4 -3
  20. data/lib/rigor/cli/check_command.rb +115 -16
  21. data/lib/rigor/cli/coverage_command.rb +148 -16
  22. data/lib/rigor/cli/coverage_scan.rb +57 -0
  23. data/lib/rigor/cli/explain_command.rb +2 -0
  24. data/lib/rigor/cli/lsp_command.rb +3 -7
  25. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  26. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  27. data/lib/rigor/cli/options.rb +9 -0
  28. data/lib/rigor/cli/plugins_command.rb +2 -1
  29. data/lib/rigor/cli/protection_renderer.rb +63 -0
  30. data/lib/rigor/cli/protection_report.rb +68 -0
  31. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  32. data/lib/rigor/cli/trace_command.rb +2 -1
  33. data/lib/rigor/cli/triage_command.rb +2 -1
  34. data/lib/rigor/cli/type_of_command.rb +1 -1
  35. data/lib/rigor/cli/type_scan_command.rb +2 -1
  36. data/lib/rigor/cli.rb +3 -2
  37. data/lib/rigor/configuration/dependencies.rb +2 -4
  38. data/lib/rigor/configuration.rb +45 -7
  39. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  40. data/lib/rigor/environment/class_registry.rb +4 -3
  41. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  42. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  43. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  44. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  45. data/lib/rigor/environment/rbs_loader.rb +49 -5
  46. data/lib/rigor/environment.rb +17 -7
  47. data/lib/rigor/flow_contribution/fact.rb +1 -1
  48. data/lib/rigor/flow_contribution.rb +3 -5
  49. data/lib/rigor/inference/acceptance.rb +17 -9
  50. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  51. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  52. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  53. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  54. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  55. data/lib/rigor/inference/expression_typer.rb +20 -28
  56. data/lib/rigor/inference/hkt_body.rb +8 -11
  57. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  58. data/lib/rigor/inference/hkt_registry.rb +10 -11
  59. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  60. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
  61. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  62. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  63. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  64. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  65. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  66. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  67. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  68. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
  69. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  70. data/lib/rigor/inference/method_dispatcher.rb +40 -48
  71. data/lib/rigor/inference/mutation_widening.rb +5 -11
  72. data/lib/rigor/inference/narrowing.rb +14 -16
  73. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  74. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  75. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  76. data/lib/rigor/inference/protection_scanner.rb +86 -0
  77. data/lib/rigor/inference/scope_indexer.rb +129 -55
  78. data/lib/rigor/inference/statement_evaluator.rb +244 -114
  79. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  80. data/lib/rigor/inference/synthetic_method.rb +7 -7
  81. data/lib/rigor/language_server/completion_provider.rb +6 -12
  82. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  83. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  84. data/lib/rigor/language_server/hover_provider.rb +2 -3
  85. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  86. data/lib/rigor/language_server/server.rb +9 -17
  87. data/lib/rigor/language_server.rb +4 -5
  88. data/lib/rigor/plugin/base.rb +10 -8
  89. data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
  90. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  91. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  92. data/lib/rigor/plugin/macro.rb +4 -5
  93. data/lib/rigor/plugin/manifest.rb +45 -66
  94. data/lib/rigor/plugin/registry.rb +6 -7
  95. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  96. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  97. data/lib/rigor/protection/mutator.rb +246 -0
  98. data/lib/rigor/rbs_extended.rb +24 -36
  99. data/lib/rigor/reflection.rb +4 -7
  100. data/lib/rigor/scope/discovery_index.rb +14 -2
  101. data/lib/rigor/scope.rb +54 -11
  102. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  103. data/lib/rigor/sig_gen/writer.rb +40 -2
  104. data/lib/rigor/source/constant_path.rb +62 -0
  105. data/lib/rigor/source.rb +1 -0
  106. data/lib/rigor/type/bound_method.rb +2 -11
  107. data/lib/rigor/type/combinator.rb +16 -3
  108. data/lib/rigor/type/constant.rb +2 -11
  109. data/lib/rigor/type/data_class.rb +2 -11
  110. data/lib/rigor/type/data_instance.rb +2 -11
  111. data/lib/rigor/type/hash_shape.rb +2 -11
  112. data/lib/rigor/type/integer_range.rb +2 -11
  113. data/lib/rigor/type/intersection.rb +2 -11
  114. data/lib/rigor/type/nominal.rb +2 -11
  115. data/lib/rigor/type/plain_lattice.rb +37 -0
  116. data/lib/rigor/type/refined.rb +72 -13
  117. data/lib/rigor/type/singleton.rb +2 -11
  118. data/lib/rigor/type/struct_class.rb +75 -0
  119. data/lib/rigor/type/struct_instance.rb +93 -0
  120. data/lib/rigor/type/tuple.rb +5 -15
  121. data/lib/rigor/type.rb +2 -0
  122. data/lib/rigor/version.rb +1 -1
  123. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  124. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  125. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
  126. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  127. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  128. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
  129. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  130. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  131. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
  132. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  133. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
  134. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  135. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  136. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  137. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  138. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  139. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  140. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  141. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
  142. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  143. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  144. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
  145. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  146. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  147. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  148. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
  149. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  150. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  151. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
  152. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  153. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  154. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  155. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
  156. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  157. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  158. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  159. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  160. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  161. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  162. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  163. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
  164. data/sig/rigor/scope.rbs +9 -1
  165. data/sig/rigor/type.rbs +36 -1
  166. metadata +19 -1
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../reflection"
6
6
  require_relative "../source/node_walker"
7
+ require_relative "../source/constant_path"
7
8
  require_relative "../type"
8
9
  require_relative "diagnostic"
9
10
  require_relative "dependency_recorder"
@@ -17,37 +18,30 @@ require_relative "check_rules/self_closedness_scanner"
17
18
 
18
19
  module Rigor
19
20
  module Analysis
20
- # First-preview catalogue of `rigor check` diagnostic rules.
21
+ # Catalogue of `rigor check` diagnostic rules.
21
22
  #
22
- # The rules are intentionally narrow: they fire ONLY when the
23
- # engine is confident enough to make a useful claim, and they
24
- # MUST NOT raise on unrecognised AST shapes, RBS gaps, or
25
- # missing scope information. Each rule consumes the per-node
26
- # scope index produced by
23
+ # Rules fire ONLY when the engine is confident enough to make a
24
+ # useful claim and MUST NOT raise on unrecognised AST shapes,
25
+ # RBS gaps, or missing scope information. Each rule consumes
26
+ # the per-node scope index produced by
27
27
  # `Rigor::Inference::ScopeIndexer.index` and yields zero or
28
28
  # more `Rigor::Analysis::Diagnostic` values.
29
29
  #
30
- # The first shipped rule, `UndefinedMethodOnTypedReceiver`,
31
- # flags an explicit-receiver `Prism::CallNode` whose receiver
32
- # statically resolves to a `Type::Nominal` or `Type::Singleton`
33
- # known to the analyzer's RBS environment AND whose method
34
- # name does not appear on that class's instance / singleton
35
- # method table. This is the canonical "type check" signal
36
- # ("Foo has no method bar"), but it explicitly does NOT fire
37
- # for:
30
+ # The primary rule (`call.undefined-method`) flags an
31
+ # explicit-receiver `Prism::CallNode` whose receiver statically
32
+ # resolves to a class known to the RBS environment and whose
33
+ # method name does not appear on that class's method table.
34
+ # It does NOT fire for:
38
35
  #
39
36
  # - implicit-self calls (no `node.receiver`) — too noisy
40
37
  # without per-method RBS for every helper in the class.
41
38
  # - dynamic / unknown receivers (`Dynamic[T]`, `Top`, `Union`)
42
39
  # — by definition we cannot enumerate the method set.
43
- # - shape carriers (`Tuple`, `HashShape`, `Constant`) their
44
- # dispatch goes through `ShapeDispatch` / `ConstantFolding`
45
- # which the rule does not yet model.
40
+ # - shape carriers: `Tuple` → "Array", `HashShape` "Hash",
41
+ # `Constant` the constant's class `concrete_class_name`
42
+ # resolves these to their runtime class for dispatch.
46
43
  # - receivers whose class name is NOT registered in the
47
44
  # loader (RBS-blind environments, unknown stdlib).
48
- #
49
- # The above list is the deliberate conservative envelope of
50
- # the first preview; later slices broaden it.
51
45
  # rubocop:disable Metrics/ModuleLength
52
46
  module CheckRules
53
47
  # Canonical identifiers for each rule. Per ADR-8 §
@@ -525,7 +519,11 @@ module Rigor
525
519
  # be a working-code false positive).
526
520
  receiver_type = safe_navigation_receiver(call_node, scope)
527
521
  class_name = concrete_class_name(receiver_type)
528
- return nil if class_name.nil?
522
+ # A union receiver has no single concrete class. The scalar path
523
+ # below cannot reason about it, but the call is still definitely
524
+ # undefined when EVERY arm lacks the method — see
525
+ # `union_undefined_method_diagnostic`.
526
+ return union_undefined_method_diagnostic(path, call_node, receiver_type, scope) if class_name.nil?
529
527
 
530
528
  # ADR-26 — a plugin may declare a class "open": one
531
529
  # known to respond beyond its RBS-declared method
@@ -704,6 +702,20 @@ module Rigor
704
702
  when Type::Tuple then "Array"
705
703
  when Type::HashShape then "Hash"
706
704
  when Type::Constant then constant_class_name(type.value)
705
+ # A refinement IS its base class for method dispatch — its method
706
+ # surface is the base's. Resolve to the base so the call rules
707
+ # (undefined-method / wrong-arity / argument-type-mismatch) reason
708
+ # about it instead of bailing. `Type::Refined` carries string-family
709
+ # refinements (`lowercase-string`, …) over an explicit `.base`;
710
+ # `Type::IntegerRange` carries the bounded-int refinements
711
+ # (`non-negative-int`, `positive-int`, `int<1,5>`), every one an
712
+ # Integer; `Type::Difference` (`A - B`) carries the non-empty /
713
+ # non-zero refinements (`non-empty-string` = `String - ""`,
714
+ # `non-empty-array` = `Array - []`, `non-zero-int` = `Integer - 0`)
715
+ # — subtracting values never changes the method surface, so the
716
+ # base (minuend) class dispatches.
717
+ when Type::Refined, Type::Difference then concrete_class_name(type.base)
718
+ when Type::IntegerRange then "Integer"
707
719
  end
708
720
  end
709
721
 
@@ -780,12 +792,90 @@ module Rigor
780
792
  scope = scope_index[miss.node]
781
793
  next if scope.nil?
782
794
  next unless confidently_closed_self_class?(miss.class_name, scope)
795
+ next if method_defined_on_known_subclass?(miss.class_name, miss.method_name, scope)
783
796
 
784
797
  build_self_undefined_method_diagnostic(path, miss)
785
798
  end
786
799
  end
787
800
 
801
+ # ADR-24 slice 4 — subclass-aware gating (the abstract / template-method
802
+ # base-class false positive the WD4 corpus eval surfaced). A base class
803
+ # legitimately calls a method its subclasses implement
804
+ # (`Mail::CommonField#decoded` calls `do_decode`, which
805
+ # `Mail::UnstructuredField < CommonField` and its siblings define; the
806
+ # same shape covers `Mail::Retriever#find` → POP3 / IMAP). When the
807
+ # missed method is discovered on ANY known subclass of the self-class,
808
+ # the call is a template-method hook, not a typo — suppress. Walks the
809
+ # project subclass closure (the `discovered_superclasses` child→parent
810
+ # map inverted, cycle-guarded). A pure narrowing — it only ever
811
+ # suppresses a firing the closed-class gate would otherwise emit.
812
+ def method_defined_on_known_subclass?(class_name, method_name, scope)
813
+ supers = scope.discovered_superclasses
814
+ seen = {}
815
+ queue = direct_subclasses(class_name, supers)
816
+ until queue.empty?
817
+ subclass = queue.shift
818
+ next if seen[subclass]
819
+
820
+ seen[subclass] = true
821
+ return true if method_known_on_class?(subclass, method_name, scope)
822
+
823
+ queue.concat(direct_subclasses(subclass, supers))
824
+ end
825
+ false
826
+ end
827
+
828
+ # The directly-recorded subclasses of `class_name`. `discovered_superclasses`
829
+ # keys the child fully-qualified (`Mail::POP3`) but records the parent
830
+ # *as written* (`Retriever`), so a qualified miss class (`Mail::Retriever`)
831
+ # is matched by resolving the parent name in the child's namespace.
832
+ def direct_subclasses(class_name, discovered_superclasses)
833
+ discovered_superclasses.filter_map { |child, parent| child if parent_matches?(child, parent, class_name) }
834
+ end
835
+
836
+ # Ruby constant lookup: a recorded parent `Retriever` on child
837
+ # `Mail::POP3` resolves to `Mail::Retriever` (walk the child's namespace
838
+ # prefixes, longest first), matched against the miss's fully-qualified
839
+ # class name. Namespace-anchored, so it cannot match a same-named base in
840
+ # an unrelated namespace.
841
+ def parent_matches?(child, parent, class_name)
842
+ parent_name = parent.to_s
843
+ return true if parent_name == class_name
844
+
845
+ segments = child.to_s.split("::")[0...-1]
846
+ until segments.empty?
847
+ return true if "#{segments.join('::')}::#{parent_name}" == class_name
848
+
849
+ segments.pop
850
+ end
851
+ false
852
+ end
853
+
854
+ # Whether `method_name` is defined on `class_name` in the project — a
855
+ # plain `def` (the def-node table) or a dynamic definition
856
+ # (`define_method` / `attr_*` / `alias`). `discovered_method?` alone
857
+ # misses plain defs, which is exactly what the abstract hooks are.
858
+ def method_known_on_class?(class_name, method_name, scope)
859
+ !scope.user_def_for(class_name, method_name).nil? ||
860
+ scope.discovered_method?(class_name, method_name, :instance)
861
+ end
862
+
863
+ # ADR-24 slice 4 — the universal bases. A recorded self-call miss tagged
864
+ # with one of these means the engine fell back to the root self-type
865
+ # because it could NOT resolve the real class (a class-body macro context
866
+ # where self is the Class object, top-level `main`, `instance_eval`, an
867
+ # FFI / `define_method` metaprogramming surface). Their instance method
868
+ # set is never "project-known and complete" — every object also responds
869
+ # to whatever the unresolved real class adds — so a miss there is a
870
+ # resolution gap, not a typo. This is the dominant false-positive class
871
+ # the WD4 corpus eval surfaced (protobuf 73 / tdiary 199 / pycall 10 /
872
+ # … FFI + class-macro calls, 287 firings across the corpus); excluding
873
+ # it is a pure narrowing.
874
+ SELF_UNDEFINED_UNIVERSAL_BASES = %w[Object BasicObject Kernel].to_set.freeze
875
+ private_constant :SELF_UNDEFINED_UNIVERSAL_BASES
876
+
788
877
  def confidently_closed_self_class?(class_name, scope)
878
+ return false if SELF_UNDEFINED_UNIVERSAL_BASES.include?(class_name)
789
879
  return false if unbounded_receiver_surface?(class_name, scope)
790
880
  return false if scope.discovered_method?(class_name, :method_missing, :instance)
791
881
  # A superclass or mixin extends the surface beyond what this file
@@ -1074,6 +1164,71 @@ module Rigor
1074
1164
  !definition.methods[method_name.to_sym].nil?
1075
1165
  end
1076
1166
 
1167
+ # Teeth on a *union* receiver. The scalar `undefined_method_diagnostic`
1168
+ # bails when the receiver has no single concrete class; here we fire
1169
+ # when EVERY non-nil arm is a fully-known, bounded, instance class on
1170
+ # which the method is absent — the call is then undefined regardless of
1171
+ # which arm the value takes at runtime (`A | B` responds to `m` only if
1172
+ # both `A` and `B` do). FP-safe by construction: `method_present_anywhere?`
1173
+ # returns "present" for any Dynamic / unknown / unbuildable /
1174
+ # source-declared arm, so the `any?` short-circuits and we never fire on
1175
+ # uncertainty; `union_arm_blocks_undefined_fire?` additionally bails on
1176
+ # any open (ADR-26) / synthesized / singleton / module-mixin arm. This is
1177
+ # no more aggressive than the scalar rule — it just applies the same
1178
+ # certainty test to each arm.
1179
+ #
1180
+ # Nil-bearing unions are deferred: their nil arm interacts with the
1181
+ # `possible-nil-receiver` rule, safe-navigation, and ADR-58
1182
+ # declaration-sourced nil. Slice 1 handles pure non-nil unions
1183
+ # (e.g. `String | Symbol`).
1184
+ def union_undefined_method_diagnostic(path, call_node, receiver_type, scope)
1185
+ return nil unless receiver_type.is_a?(Type::Union)
1186
+ return nil if call_node.safe_navigation?
1187
+
1188
+ members = receiver_type.members
1189
+ # Nil-bearing unions (`T | nil`) stay silent — the deliberate N3
1190
+ # decision (`safe_navigation_undefined_method_spec.rb`). A corpus FP
1191
+ # study of a bundled-arm-narrowed candidate (12 projects incl.
1192
+ # ActiveSupport-heavy) found ~zero real firings yet a demonstrated
1193
+ # loss-of-specificity false positive, so the silence is kept. See
1194
+ # ADR-62 and `docs/notes/20260613-mutation-teeth-harness.md`.
1195
+ return nil if members.any? { |member| nil_member?(member) }
1196
+ return nil if members.any? { |member| union_arm_blocks_undefined_fire?(member, scope) }
1197
+ # Only a genuinely multi-class union ("the value is an A or a B") gains
1198
+ # from this rule. A union whose arms all resolve to ONE class
1199
+ # (`Hash[K1, V1] | Hash[K2, V2]`) is a shape-join artifact — checking
1200
+ # method existence there is the scalar rule's job, and when the join is
1201
+ # a misinference it is a false positive (a corpus probe caught mail's
1202
+ # `compose_codepoints` typed `Hash | Hash` for an `Array`, flagging
1203
+ # `.pack`). Require at least two distinct arm classes.
1204
+ return nil if members.map { |member| concrete_class_name(member) }.uniq.size < 2
1205
+ return nil if members.any? { |member| method_present_anywhere?(member, call_node.name, scope) }
1206
+
1207
+ build_undefined_method_diagnostic(path, call_node, receiver_type)
1208
+ end
1209
+
1210
+ # An arm that makes a sound "undefined on every arm" verdict
1211
+ # impossible: a non-class surface (Dynamic / Top / Bot), a singleton
1212
+ # (slice 1 reasons about instance arms only), the generic metaclass
1213
+ # `Class` / `Module` (a value typed as one is *some* class/module object
1214
+ # whose singleton methods cannot be enumerated from the metaclass — e.g.
1215
+ # `plugin_class : Class` really holds a `Plugin` subclass with
1216
+ # `.manifest`), an unbounded receiver (ADR-26 open class or a synthesized
1217
+ # stub), or a module mixin whose Object-inherited methods the per-arm
1218
+ # lookup would miss.
1219
+ METACLASS_ARMS = %w[Class Module].to_set.freeze
1220
+ private_constant :METACLASS_ARMS
1221
+
1222
+ def union_arm_blocks_undefined_fire?(member, scope)
1223
+ class_name = concrete_class_name(member)
1224
+ return true if class_name.nil?
1225
+ return true if member.is_a?(Type::Singleton)
1226
+ return true if METACLASS_ARMS.include?(class_name)
1227
+ return true if unbounded_receiver_surface?(class_name, scope)
1228
+
1229
+ module_mixin_receiver?(member, scope)
1230
+ end
1231
+
1077
1232
  # Slice 7 phase 19 — PHPStan-style `dump_type(value)`.
1078
1233
  # When the engine recognises a call to `dump_type` (with
1079
1234
  # any of the supported receiver shapes — implicit self
@@ -1166,27 +1321,12 @@ module Rigor
1166
1321
  receiver = call_node.receiver
1167
1322
  return true if receiver.nil?
1168
1323
 
1169
- name = constant_name_of(receiver)
1324
+ name = Source::ConstantPath.qualified_name_or_nil(receiver)
1170
1325
  return false if name.nil?
1171
1326
 
1172
1327
  RIGOR_TESTING_RECEIVERS.include?(name)
1173
1328
  end
1174
1329
 
1175
- def constant_name_of(node)
1176
- case node
1177
- when Prism::ConstantReadNode then node.name.to_s
1178
- when Prism::ConstantPathNode then render_constant_path(node)
1179
- end
1180
- end
1181
-
1182
- def render_constant_path(node)
1183
- parent = node.parent
1184
- base = constant_name_of(parent)
1185
- return nil if parent && base.nil?
1186
-
1187
- parent ? "#{base}::#{node.name}" : node.name.to_s
1188
- end
1189
-
1190
1330
  def build_assert_type_diagnostic(path, call_node, expected, actual)
1191
1331
  Diagnostic.from_message_loc(
1192
1332
  call_node,
@@ -1540,6 +1680,21 @@ module Rigor
1540
1680
  UNIVERSAL_EQUALITY_METHODS = %i[== != eql? equal? <=>].to_set.freeze
1541
1681
  private_constant :UNIVERSAL_EQUALITY_METHODS
1542
1682
 
1683
+ # ADR-64 WD1 — the binary arithmetic / bit / ordering operators
1684
+ # dispatch through Ruby's `coerce` protocol (and `<=>` for the
1685
+ # comparisons): `5 + Money.new` is valid at runtime because
1686
+ # `Integer#+` calls `Money#coerce(5)`, even though no RBS `Integer#+`
1687
+ # overload lists `Money`. A non-`Numeric` argument to them is therefore
1688
+ # NOT statically refutable — any user type may define `coerce` — so the
1689
+ # *non-nil* argument-type-mismatch channel excludes them (a fixed
1690
+ # allow-list, modelled on {UNIVERSAL_EQUALITY_METHODS}, not `coerce`
1691
+ # detection). `nil` never coerces, so the nil channel stays in force
1692
+ # here; the exclusion applies to the non-nil case only. `<=>` and the
1693
+ # `==` family are already excluded wholesale by
1694
+ # {UNIVERSAL_EQUALITY_METHODS}.
1695
+ COERCE_DISPATCH_METHODS = %i[+ - * / % ** & | ^ << >> < > <= >=].to_set.freeze
1696
+ private_constant :COERCE_DISPATCH_METHODS
1697
+
1543
1698
  def argument_type_diagnostic(path, call_node, scope_index)
1544
1699
  return nil if call_node.receiver.nil?
1545
1700
  return nil if UNIVERSAL_EQUALITY_METHODS.include?(call_node.name)
@@ -1563,15 +1718,266 @@ module Rigor
1563
1718
 
1564
1719
  method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
1565
1720
  return nil if method_def.nil? || method_def == true
1566
- return nil unless method_def.method_types.size == 1
1567
1721
 
1568
1722
  param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
1569
- mismatch = first_argument_mismatch(method_def.method_types.first, call_node, scope, param_overrides)
1723
+ mismatch = argument_mismatch(method_def.method_types, call_node, scope, param_overrides)
1570
1724
  return nil if mismatch.nil?
1571
1725
 
1572
1726
  build_argument_type_diagnostic(path, call_node, class_name, mismatch)
1573
1727
  end
1574
1728
 
1729
+ # Single overload → the exact per-argument acceptance (unchanged).
1730
+ # Multiple overloads → the nil channel (a pure-`nil` argument every
1731
+ # overload rejects) plus, on non-coerce methods, the non-nil channel
1732
+ # (a single-concrete-class argument every overload rejects). See
1733
+ # {#multi_overload_argument_mismatch}.
1734
+ def argument_mismatch(method_types, call_node, scope, param_overrides)
1735
+ if method_types.size == 1
1736
+ first_argument_mismatch(method_types.first, call_node, scope, param_overrides)
1737
+ else
1738
+ multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides)
1739
+ end
1740
+ end
1741
+
1742
+ # Multi-overload argument-type-mismatch. The dispatcher's per-overload
1743
+ # acceptance plumbing is not run here; instead the FP-safe shape mirrors
1744
+ # the "absent on every arm" union-undefined-method rule: an argument is
1745
+ # a mismatch only when EVERY overload's matching positional param
1746
+ # rejects it.
1747
+ #
1748
+ # Two channels, both gated on a positively-refuted argument:
1749
+ # - **nil** (any method): a pure `nil` no overload admits is a
1750
+ # guaranteed `TypeError` — `nil` never coerces.
1751
+ # - **non-nil** (ADR-64, non-coerce methods only): an argument that
1752
+ # types to a single concrete RBS-known class that no overload admits.
1753
+ # Excludes {COERCE_DISPATCH_METHODS} (`5 + Money.new` is valid via
1754
+ # `coerce`), restricts to a single concrete class (WD3 — a union arg
1755
+ # stays deferred), and decides acceptance on the RBS param type
1756
+ # ({#param_accepts_arg_class?}) so it sees through the `int` / `string`
1757
+ # interface-aliases the translator degrades.
1758
+ def multi_overload_argument_mismatch(method_types, call_node, scope, param_overrides)
1759
+ functions = method_types.map(&:type)
1760
+ return nil unless functions.all? { |function| argument_check_eligible?(function) }
1761
+
1762
+ coerce_method = COERCE_DISPATCH_METHODS.include?(call_node.name)
1763
+ arguments = call_node.arguments&.arguments || []
1764
+ arguments.each_with_index do |arg, index|
1765
+ arg_type = scope.type_of(arg)
1766
+ params = overload_positional_params(functions, index)
1767
+ next if params.nil? # arity divergence — some overload lacks a param here
1768
+
1769
+ mismatch =
1770
+ if nil_member?(arg_type) # pure nil only — not a `T | nil` union
1771
+ nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
1772
+ elsif !coerce_method
1773
+ non_nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
1774
+ end
1775
+ return mismatch if mismatch
1776
+ end
1777
+ nil
1778
+ end
1779
+
1780
+ # The nil channel: a pure `nil` argument no overload admits (ADR-58
1781
+ # parity excuses a declaration-sourced ivar nil).
1782
+ def nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
1783
+ return nil if declaration_sourced_nil_argument?(arg, scope)
1784
+ return nil if params.any? { |param| param_admits_nil?(param, param_overrides, scope) }
1785
+
1786
+ { node: arg, name: nil, expected: overload_param_expected_label(params), actual: arg_type }
1787
+ end
1788
+
1789
+ # The non-nil channel (ADR-64 WD2/WD3): a single-concrete-class
1790
+ # argument no overload admits, on a non-coerce method.
1791
+ def non_nil_arg_overload_mismatch(arg, arg_type, params, param_overrides, scope)
1792
+ return nil unless single_concrete_arg_class?(arg_type, scope)
1793
+ return nil if params.any? { |param| param_accepts_arg_class?(param, arg_type, param_overrides, scope) }
1794
+
1795
+ { node: arg, name: nil, expected: overload_param_expected_label(params), actual: arg_type }
1796
+ end
1797
+
1798
+ # The matching positional RBS param across every overload, or nil when
1799
+ # any overload has no param at `index` (arity divergence — the
1800
+ # wrong-arity rule's concern, not this one's).
1801
+ def overload_positional_params(functions, index)
1802
+ params = functions.map { |function| (function.required_positionals + function.optional_positionals)[index] }
1803
+ params.any?(&:nil?) ? nil : params
1804
+ end
1805
+
1806
+ # The class names whose instances `nil` IS — `NilClass` and every
1807
+ # ancestor. A parameter typed as any other class instance rejects nil.
1808
+ NIL_COMPATIBLE_CLASS_NAMES = %w[NilClass Object BasicObject Kernel].to_set.freeze
1809
+ private_constant :NIL_COMPATIBLE_CLASS_NAMES
1810
+
1811
+ # Does this parameter admit a `nil` argument? Decided on the RBS
1812
+ # parameter type (a `rigor:v1:param` override takes precedence).
1813
+ # Conservative throughout: any case we cannot decide returns true
1814
+ # (admits → do not fire), so the rule never fires on uncertainty.
1815
+ def param_admits_nil?(param, param_overrides, scope)
1816
+ override = param_overrides[param.name]
1817
+ return rigor_type_admits_nil?(override) if override
1818
+
1819
+ rbs_type_admits_nil?(param.type, scope)
1820
+ end
1821
+
1822
+ # The `rigor:v1:param` override variant — a refinement
1823
+ # (`non-empty-string`) rejects nil; an explicit nil / nilable union /
1824
+ # gradual override admits it.
1825
+ def rigor_type_admits_nil?(type)
1826
+ return true if type.is_a?(Type::Dynamic) || type.is_a?(Type::Top)
1827
+ return true if nil_member?(type)
1828
+ return union_contains_nil?(type) if type.is_a?(Type::Union)
1829
+
1830
+ false
1831
+ end
1832
+
1833
+ # Walks the RBS parameter type. The load-bearing cases are `Alias`
1834
+ # (`string` = `String | _ToStr`) and `Interface` (`_ToStr`), which
1835
+ # {Inference::RbsTypeTranslator} degrades to `untyped` — the reason a
1836
+ # `nil` argument is invisible after translation (the interface-alias
1837
+ # gap). Resolving them here recovers the rejection. Only a concrete
1838
+ # class instance that is not a `nil` ancestor, and an interface
1839
+ # NilClass does not satisfy, return false; everything else admits.
1840
+ def rbs_type_admits_nil?(rbs_type, scope)
1841
+ case rbs_type
1842
+ when RBS::Types::Union then rbs_type.types.any? { |member| rbs_type_admits_nil?(member, scope) }
1843
+ when RBS::Types::Alias
1844
+ expanded = scope.environment&.rbs_loader&.expand_type_alias(rbs_type)
1845
+ expanded.nil? || rbs_type_admits_nil?(expanded, scope)
1846
+ when RBS::Types::ClassInstance
1847
+ NIL_COMPATIBLE_CLASS_NAMES.include?(rbs_type.name.to_s.delete_prefix("::"))
1848
+ when RBS::Types::Interface then interface_admits_nil?(rbs_type, scope)
1849
+ else true # Optional / bases / variable / tuple / record / proc / literal / intersection → conservative admit
1850
+ end
1851
+ end
1852
+
1853
+ # An interface parameter (`_ToStr`) admits nil only when NilClass
1854
+ # implements every method it requires (`to_str`, `to_int`, … — which
1855
+ # NilClass does not, so `string` / `int` params reject nil; a
1856
+ # hypothetical `_ToS` would admit, since NilClass#to_s exists).
1857
+ # Unresolvable → conservative true.
1858
+ def interface_admits_nil?(rbs_type, scope)
1859
+ loader = scope.environment&.rbs_loader
1860
+ return true if loader.nil?
1861
+
1862
+ methods = loader.interface_method_names(rbs_type.name.to_s)
1863
+ return true if methods.nil? || methods.empty?
1864
+
1865
+ methods.all? { |method_name| nil_class_has_method?(method_name, scope) }
1866
+ end
1867
+
1868
+ # ADR-64 WD3 — the non-nil channel fires only on an argument that types
1869
+ # to a single concrete, RBS-known class. A union arg mirrors the
1870
+ # union-receiver story and stays deferred; a class/module object
1871
+ # (`Singleton`) has a special acceptance surface and is skipped; a
1872
+ # non-RBS project class is skipped because its conversion protocol (a
1873
+ # duck-typed `to_int` / `to_str`) is invisible to us, so we cannot
1874
+ # refute acceptance.
1875
+ def single_concrete_arg_class?(arg_type, scope)
1876
+ return false if arg_type.is_a?(Type::Union)
1877
+ return false if arg_type.is_a?(Type::Singleton)
1878
+
1879
+ class_name = concrete_class_name(arg_type)
1880
+ return false if class_name.nil?
1881
+
1882
+ Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
1883
+ end
1884
+
1885
+ # ADR-64 WD2 — does this parameter accept the (non-nil) argument?
1886
+ # The non-nil generalization of {#param_admits_nil?}: decided on the RBS
1887
+ # parameter type (a `rigor:v1:param` override takes precedence) so it
1888
+ # sees through the `int` / `string` interface-aliases the translator
1889
+ # degrades to gradual. Conservative throughout — any case we cannot
1890
+ # decide returns true (accepts → do not fire).
1891
+ def param_accepts_arg_class?(param, arg_type, param_overrides, scope)
1892
+ override = param_overrides[param.name]
1893
+ return rigor_type_accepts_arg?(override, arg_type) if override
1894
+
1895
+ rbs_type_accepts_arg?(param.type, arg_type, scope)
1896
+ end
1897
+
1898
+ # The `rigor:v1:param` override variant — a Rigor `Type`, so the
1899
+ # acceptance engine decides directly (gradual; only a proven rejection
1900
+ # refutes). Dynamic / Top admit unconditionally.
1901
+ def rigor_type_accepts_arg?(param_type, arg_type)
1902
+ return true if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
1903
+
1904
+ !Inference::Acceptance.accepts(param_type, arg_type, mode: :gradual).no?
1905
+ end
1906
+
1907
+ # Walks the RBS parameter type, mirroring {#rbs_type_admits_nil?}. The
1908
+ # load-bearing cases are `Alias` / `Interface` (`int` = `Integer |
1909
+ # _ToInt`), which the translator degrades to gradual — resolving them
1910
+ # here recovers the rejection. A faithfully-translated `ClassInstance`
1911
+ # is handed to the acceptance engine; everything undecidable admits.
1912
+ def rbs_type_accepts_arg?(rbs_type, arg_type, scope)
1913
+ case rbs_type
1914
+ when RBS::Types::Union then rbs_type.types.any? { |member| rbs_type_accepts_arg?(member, arg_type, scope) }
1915
+ when RBS::Types::Alias
1916
+ expanded = scope.environment&.rbs_loader&.expand_type_alias(rbs_type)
1917
+ expanded.nil? || rbs_type_accepts_arg?(expanded, arg_type, scope)
1918
+ when RBS::Types::ClassInstance then class_instance_accepts_arg?(rbs_type, arg_type, scope)
1919
+ when RBS::Types::Interface then interface_accepts_arg?(rbs_type, arg_type, scope)
1920
+ else true # bases / variable / tuple / record / proc / literal / intersection / optional → conservative admit
1921
+ end
1922
+ end
1923
+
1924
+ # A `ClassInstance` param (`Integer`, `Numeric`, …) is translated
1925
+ # faithfully (no interface degradation), so the acceptance engine — the
1926
+ # canonical RBS-ancestry / generic-aware subtype check — decides it.
1927
+ # Only a proven rejection refutes; an unresolvable class is `:maybe`,
1928
+ # which admits.
1929
+ def class_instance_accepts_arg?(rbs_type, arg_type, scope)
1930
+ translated = translate_param_type(rbs_type, scope.environment)
1931
+ return true if translated.is_a?(Type::Dynamic) || translated.is_a?(Type::Top)
1932
+
1933
+ !Inference::Acceptance.accepts(translated, arg_type, mode: :gradual).no?
1934
+ end
1935
+
1936
+ # An interface param (`_ToInt`) accepts the arg only when the arg's
1937
+ # class implements every method it requires (`to_int`, …). The non-nil
1938
+ # mirror of {#interface_admits_nil?}: ask the arg class, not NilClass.
1939
+ # Unresolvable anywhere → conservative true (admit).
1940
+ def interface_accepts_arg?(rbs_type, arg_type, scope)
1941
+ loader = scope.environment&.rbs_loader
1942
+ return true if loader.nil?
1943
+
1944
+ methods = loader.interface_method_names(rbs_type.name.to_s)
1945
+ return true if methods.nil? || methods.empty?
1946
+
1947
+ class_name = concrete_class_name(arg_type)
1948
+ return true if class_name.nil?
1949
+
1950
+ methods.all? { |method_name| arg_class_has_method?(class_name, method_name, scope) }
1951
+ end
1952
+
1953
+ # The non-nil mirror of {#nil_class_has_method?}, but conservative on
1954
+ # the unknown side: an unresolvable definition returns true (the class
1955
+ # *might* implement the method — e.g. a metaprogrammed conversion), so
1956
+ # the channel never fires on uncertainty.
1957
+ def arg_class_has_method?(class_name, method_name, scope)
1958
+ definition = Rigor::Reflection.instance_definition(class_name, scope: scope)
1959
+ return true if definition.nil?
1960
+
1961
+ !definition.methods[method_name.to_sym].nil?
1962
+ end
1963
+
1964
+ # A readable "expected" label for a multi-overload mismatch — the RBS
1965
+ # parameter type(s) as written (`string`, or the per-overload set), since
1966
+ # the translated Rigor type degrades the interface-alias the rejection
1967
+ # hinges on. Shared by the nil and non-nil channels.
1968
+ def overload_param_expected_label(params)
1969
+ params.map { |param| param.type.to_s.delete_prefix("::") }.uniq.join(" | ")
1970
+ end
1971
+
1972
+ # ADR-58 parity for the nil channel: a declaration-sourced ivar read
1973
+ # that types as nil is the same not-diagnostic-fuel case the union
1974
+ # path gates in {#declaration_sourced_nil_only_mismatch?}; suppress it
1975
+ # here too so a ctor-seeded `@x = nil` read passed as an argument does
1976
+ # not fire on a working program's cross-method invariant.
1977
+ def declaration_sourced_nil_argument?(arg, scope)
1978
+ arg.is_a?(Prism::InstanceVariableReadNode) && scope.declaration_sourced?(:ivar, arg.name)
1979
+ end
1980
+
1575
1981
  def first_argument_mismatch(method_type, call_node, scope, param_overrides)
1576
1982
  function = method_type.type
1577
1983
  return nil unless argument_check_eligible?(function)
@@ -1582,22 +1988,35 @@ module Rigor
1582
1988
  param = params[index]
1583
1989
  next if param.nil? # arity mismatch is the wrong-arity rule's concern.
1584
1990
 
1585
- # `rigor:v1:param: <name> <refinement>` annotations
1586
- # tighten the RBS-declared parameter type. The
1587
- # override is the authoritative contract when
1588
- # present; otherwise we translate the RBS type as
1589
- # before.
1590
- param_type = param_overrides[param.name] || translate_param_type(param.type, scope.environment)
1591
- next if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
1991
+ mismatch = single_argument_mismatch(param, arg, scope, param_overrides)
1992
+ return mismatch if mismatch
1993
+ end
1994
+ nil
1995
+ end
1592
1996
 
1593
- arg_type = scope.type_of(arg)
1594
- next if arg_type.is_a?(Type::Dynamic) || arg_type.is_a?(Type::Top)
1997
+ # The mismatch (or nil) for one positional argument against one
1998
+ # parameter. The nil channel decides a pure `nil` argument on the RBS
1999
+ # parameter type — seeing through the `string` / `int` interface-alias
2000
+ # the translator degrades to gradual (so `"a" + nil` fires), excusing a
2001
+ # declaration-sourced ivar nil (ADR-58 parity). The non-nil channel is
2002
+ # the original translated-acceptance check, with a `rigor:v1:param`
2003
+ # override taking precedence over the RBS-declared type.
2004
+ def single_argument_mismatch(param, arg, scope, param_overrides)
2005
+ arg_type = scope.type_of(arg)
1595
2006
 
1596
- next unless argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
2007
+ if nil_member?(arg_type)
2008
+ return nil if declaration_sourced_nil_argument?(arg, scope)
2009
+ return nil if param_admits_nil?(param, param_overrides, scope)
1597
2010
 
1598
- return { node: arg, name: param.name, expected: param_type, actual: arg_type }
2011
+ return { node: arg, name: param.name, expected: overload_param_expected_label([param]), actual: arg_type }
1599
2012
  end
1600
- nil
2013
+
2014
+ param_type = param_overrides[param.name] || translate_param_type(param.type, scope.environment)
2015
+ return nil if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
2016
+ return nil if arg_type.is_a?(Type::Dynamic) || arg_type.is_a?(Type::Top)
2017
+ return nil unless argument_genuinely_mismatches?(arg, arg_type, param_type, scope)
2018
+
2019
+ { node: arg, name: param.name, expected: param_type, actual: arg_type }
1601
2020
  end
1602
2021
 
1603
2022
  # The parameter rejects the argument AND the rejection is not a
@@ -1658,8 +2077,10 @@ module Rigor
1658
2077
  def build_argument_type_diagnostic(path, call_node, class_name, mismatch)
1659
2078
  method_label = "`#{call_node.name}' on #{class_name}"
1660
2079
  parameter_label = mismatch[:name] ? "parameter `#{mismatch[:name]}' of #{method_label}" : method_label
2080
+ expected = mismatch[:expected]
2081
+ expected_description = expected.is_a?(String) ? expected : expected.describe(:short)
1661
2082
  message = "argument type mismatch at #{parameter_label}: " \
1662
- "expected #{mismatch[:expected].describe(:short)}, " \
2083
+ "expected #{expected_description}, " \
1663
2084
  "got #{mismatch[:actual].describe(:short)}"
1664
2085
  Diagnostic.from_node(
1665
2086
  mismatch[:node],
@@ -2034,7 +2455,14 @@ module Rigor
2034
2455
  # everything, so they stay silent (FP-safe). `self`/`instance`
2035
2456
  # are translated with `self_type: nil` on both sides, so a
2036
2457
  # parent `-> self` and an override `-> self` never fire.
2037
- def override_return_widened_diagnostic(path, def_node, scope_index)
2458
+ # The authored-override resolution shared by the Liskov override
2459
+ # rules (`def.override-return-widened` and
2460
+ # `def.override-param-narrowed`): the def must be an instance method
2461
+ # whose own class declares it in RBS, and a project-discovered
2462
+ # ancestor must also declare it. Returns
2463
+ # `[scope, override_method, parent_class, parent_method]`, or nil
2464
+ # (the rule does not fire) when any gate is unmet.
2465
+ def resolve_authored_override(def_node, scope_index)
2038
2466
  return nil unless def_node.receiver.nil? # instance methods only (singleton: follow-on)
2039
2467
 
2040
2468
  scope = scope_index[def_node]
@@ -2054,6 +2482,14 @@ module Rigor
2054
2482
  return nil if parent.nil?
2055
2483
 
2056
2484
  parent_class, parent_method = parent
2485
+ [scope, override_method, parent_class, parent_method]
2486
+ end
2487
+
2488
+ def override_return_widened_diagnostic(path, def_node, scope_index)
2489
+ resolved = resolve_authored_override(def_node, scope_index)
2490
+ return nil if resolved.nil?
2491
+
2492
+ scope, override_method, parent_class, parent_method = resolved
2057
2493
  override_return = declared_return_union(override_method, scope.environment)
2058
2494
  parent_return = declared_return_union(parent_method, scope.environment)
2059
2495
  return nil if override_return.nil? || parent_return.nil?
@@ -2126,25 +2562,10 @@ module Rigor
2126
2562
  # overload-arm ambiguity, both sides must have exactly one
2127
2563
  # method type.
2128
2564
  def override_param_narrowed_diagnostic(path, def_node, scope_index)
2129
- return nil unless def_node.receiver.nil? # instance methods only
2130
-
2131
- scope = scope_index[def_node]
2132
- return nil if scope.nil?
2133
-
2134
- self_type = scope.self_type
2135
- return nil unless self_type.respond_to?(:class_name)
2136
-
2137
- class_name = self_type.class_name.to_s
2138
- method_name = def_node.name
2139
-
2140
- override_method = safe_instance_method_definition(class_name, method_name, scope)
2141
- return nil if override_method.nil?
2142
- return nil unless defined_on?(override_method, class_name)
2143
-
2144
- parent = nearest_ancestor_method_def(scope, class_name, method_name)
2145
- return nil if parent.nil?
2565
+ resolved = resolve_authored_override(def_node, scope_index)
2566
+ return nil if resolved.nil?
2146
2567
 
2147
- parent_class, parent_method = parent
2568
+ _scope, override_method, parent_class, parent_method = resolved
2148
2569
  override_params = positional_param_types(override_method)
2149
2570
  parent_params = positional_param_types(parent_method)
2150
2571
  return nil if override_params.nil? || parent_params.nil?