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.
- checksums.yaml +4 -4
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +115 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +49 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +20 -28
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +244 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +14 -2
- data/lib/rigor/scope.rb +54 -11
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- 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
|
-
#
|
|
21
|
+
# Catalogue of `rigor check` diagnostic rules.
|
|
21
22
|
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
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
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
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
|
|
44
|
-
#
|
|
45
|
-
#
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
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
|
-
|
|
1594
|
-
|
|
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
|
-
|
|
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:
|
|
2011
|
+
return { node: arg, name: param.name, expected: overload_param_expected_label([param]), actual: arg_type }
|
|
1599
2012
|
end
|
|
1600
|
-
|
|
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 #{
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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?
|