rigortype 0.1.7 → 0.1.9
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/README.md +186 -513
- data/lib/rigor/analysis/check_rules.rb +23 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -3
- data/lib/rigor/analysis/runner.rb +178 -3
- data/lib/rigor/analysis/worker_session.rb +14 -3
- data/lib/rigor/cli/annotate_command.rb +224 -0
- data/lib/rigor/cli/baseline_command.rb +36 -16
- data/lib/rigor/cli/prism_colorizer.rb +111 -0
- data/lib/rigor/cli/triage_command.rb +83 -0
- data/lib/rigor/cli/triage_renderer.rb +77 -0
- data/lib/rigor/cli.rb +71 -5
- data/lib/rigor/environment.rb +9 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
- data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
- data/lib/rigor/inference/expression_typer.rb +300 -18
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +173 -10
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +149 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +20 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +33 -8
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +316 -2
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +126 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +179 -4
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/scope_indexer.rb +156 -6
- data/lib/rigor/inference/statement_evaluator.rb +43 -21
- data/lib/rigor/plugin/base.rb +39 -0
- data/lib/rigor/plugin/loader.rb +22 -1
- data/lib/rigor/plugin/manifest.rb +73 -10
- data/lib/rigor/plugin/protocol_contract.rb +185 -0
- data/lib/rigor/plugin/registry.rb +66 -0
- data/lib/rigor/scope.rb +46 -0
- data/lib/rigor/triage/catalogue.rb +296 -0
- data/lib/rigor/triage/hint.rb +27 -0
- data/lib/rigor/triage.rb +89 -0
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- metadata +16 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Folds the zone-pinned `Time` class constructors on statically
|
|
9
|
+
# known arguments.
|
|
10
|
+
#
|
|
11
|
+
# Only `Time.utc` / `Time.gm` are folded. They pin the result
|
|
12
|
+
# to UTC, so the literal is machine-independent — every reader
|
|
13
|
+
# (`year`, `hour`, `utc_offset`, `strftime`, …) yields the same
|
|
14
|
+
# answer on any analysis host.
|
|
15
|
+
#
|
|
16
|
+
# `Time.now` (non-deterministic), `Time.at` / `Time.local` /
|
|
17
|
+
# `Time.mktime` / `Time.new` (local-zone — the result's wall
|
|
18
|
+
# clock and `utc_offset` depend on the analysis machine's
|
|
19
|
+
# timezone) are deliberately NOT folded; they keep their
|
|
20
|
+
# `Nominal[Time]` RBS answer. For the same reason `Time#getlocal`
|
|
21
|
+
# is blocklisted in `TIME_CATALOG` so a folded `Constant[Time]`
|
|
22
|
+
# cannot produce a machine-dependent local-zone copy.
|
|
23
|
+
module TimeFolding
|
|
24
|
+
TIME_UTC_METHODS = Set[:utc, :gm].freeze
|
|
25
|
+
private_constant :TIME_UTC_METHODS
|
|
26
|
+
|
|
27
|
+
# `Time.utc` accepts the 1–7 positional (year-first) form and
|
|
28
|
+
# a 10-arg (sec-first) form; cap the arity so a malformed
|
|
29
|
+
# call declines cheaply rather than reaching the constructor.
|
|
30
|
+
MAX_TIME_ARITY = 10
|
|
31
|
+
private_constant :MAX_TIME_ARITY
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
36
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
37
|
+
return nil unless dispatch_target?(receiver)
|
|
38
|
+
return nil unless TIME_UTC_METHODS.include?(method_name)
|
|
39
|
+
return nil unless args.size.between?(1, MAX_TIME_ARITY)
|
|
40
|
+
return nil unless args.all?(Type::Constant)
|
|
41
|
+
|
|
42
|
+
values = args.map(&:value)
|
|
43
|
+
return nil unless values.all? { |v| v.is_a?(Integer) || v.is_a?(String) }
|
|
44
|
+
|
|
45
|
+
Type::Combinator.constant_of(Time.utc(*values))
|
|
46
|
+
rescue StandardError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def dispatch_target?(receiver)
|
|
51
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "Time"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require_relative "../../type"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module MethodDispatcher
|
|
9
|
+
# Folds `URI` module-function calls on statically known
|
|
10
|
+
# string constants.
|
|
11
|
+
#
|
|
12
|
+
# `URI.encode_www_form_component` / `decode_www_form_component`
|
|
13
|
+
# and the newer `encode_uri_component` / `decode_uri_component`
|
|
14
|
+
# are pure, deterministic functions over their string inputs.
|
|
15
|
+
# When the argument is a `Constant[String]`, the analyzer can
|
|
16
|
+
# evaluate the call at inference time and return the concrete
|
|
17
|
+
# `Constant[String]` result.
|
|
18
|
+
#
|
|
19
|
+
# === Supported methods
|
|
20
|
+
#
|
|
21
|
+
# * `encode_www_form_component(str)` / `decode_www_form_component(str)` —
|
|
22
|
+
# RFC 3986 percent-encode / decode. Returns `Constant[String]`.
|
|
23
|
+
# * `encode_uri_component(str)` / `decode_uri_component(str)` —
|
|
24
|
+
# Same encoding but may preserve additional reserved chars
|
|
25
|
+
# (Ruby 3.2+). Returns `Constant[String]`.
|
|
26
|
+
#
|
|
27
|
+
# === Non-constant / unsupported cases
|
|
28
|
+
#
|
|
29
|
+
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
30
|
+
# - the receiver is not `Singleton[URI]`,
|
|
31
|
+
# - the first argument is not a `Constant[String]`,
|
|
32
|
+
# - the method is not in the supported set.
|
|
33
|
+
module URIFolding
|
|
34
|
+
URI_COMPONENT_METHODS = Set[
|
|
35
|
+
:encode_www_form_component, :decode_www_form_component,
|
|
36
|
+
:encode_uri_component, :decode_uri_component
|
|
37
|
+
].freeze
|
|
38
|
+
private_constant :URI_COMPONENT_METHODS
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
43
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
44
|
+
return nil unless dispatch_target?(receiver)
|
|
45
|
+
return nil unless URI_COMPONENT_METHODS.include?(method_name)
|
|
46
|
+
|
|
47
|
+
fold_uri_call(method_name, args)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def dispatch_target?(receiver)
|
|
51
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "URI"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fold_uri_call(method_name, args)
|
|
55
|
+
return nil unless args.size == 1
|
|
56
|
+
|
|
57
|
+
arg = args.first
|
|
58
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
59
|
+
|
|
60
|
+
Type::Combinator.constant_of(URI.public_send(method_name, arg.value))
|
|
61
|
+
rescue StandardError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -13,6 +13,13 @@ require_relative "method_dispatcher/rbs_dispatch"
|
|
|
13
13
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
14
14
|
require_relative "method_dispatcher/block_folding"
|
|
15
15
|
require_relative "method_dispatcher/file_folding"
|
|
16
|
+
require_relative "method_dispatcher/shellwords_folding"
|
|
17
|
+
require_relative "method_dispatcher/math_folding"
|
|
18
|
+
require_relative "method_dispatcher/time_folding"
|
|
19
|
+
require_relative "method_dispatcher/regexp_folding"
|
|
20
|
+
require_relative "method_dispatcher/cgi_folding"
|
|
21
|
+
require_relative "method_dispatcher/uri_folding"
|
|
22
|
+
require_relative "method_dispatcher/set_folding"
|
|
16
23
|
require_relative "method_dispatcher/kernel_dispatch"
|
|
17
24
|
require_relative "method_dispatcher/method_folding"
|
|
18
25
|
|
|
@@ -191,7 +198,7 @@ module Rigor
|
|
|
191
198
|
# introspection (`attr_reader`, `private`, ...) on
|
|
192
199
|
# user classes without requiring the user to author
|
|
193
200
|
# their own RBS.
|
|
194
|
-
try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
201
|
+
try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type, call_node)
|
|
195
202
|
end
|
|
196
203
|
|
|
197
204
|
# v0.1.3 — discovered-method dispatch tier. `scope` carries
|
|
@@ -643,7 +650,7 @@ module Rigor
|
|
|
643
650
|
ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
644
651
|
LiteralStringFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
645
652
|
ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
646
|
-
|
|
653
|
+
dispatch_stdlib_module_tiers(receiver_type, method_name, arg_types) ||
|
|
647
654
|
KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
648
655
|
MethodFolding.try_forward(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
649
656
|
BlockFolding.try_fold(
|
|
@@ -651,7 +658,22 @@ module Rigor
|
|
|
651
658
|
)
|
|
652
659
|
end
|
|
653
660
|
|
|
654
|
-
|
|
661
|
+
# Stdlib module singleton-folding tiers: File, Shellwords, Math,
|
|
662
|
+
# Time, Regexp, CGI, URI, Set. Extracted from
|
|
663
|
+
# `dispatch_precise_tiers` to keep the parent method within the
|
|
664
|
+
# cyclomatic-complexity limit.
|
|
665
|
+
def dispatch_stdlib_module_tiers(receiver_type, method_name, arg_types)
|
|
666
|
+
FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
667
|
+
ShellwordsFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
668
|
+
MathFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
669
|
+
TimeFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
670
|
+
RegexpFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
671
|
+
CGIFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
672
|
+
URIFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
673
|
+
SetFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type, call_node = nil)
|
|
655
677
|
return nil if environment.nil?
|
|
656
678
|
|
|
657
679
|
fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
|
|
@@ -665,16 +687,44 @@ module Rigor
|
|
|
665
687
|
# `Bundler::URI::Generic` instance method types `base`
|
|
666
688
|
# as `Object` because `Bundler::URI::Generic` is not in
|
|
667
689
|
# RBS and the fallback's `self` resolves to Object.
|
|
690
|
+
#
|
|
691
|
+
# `public_only:` — when the call has an EXPLICIT, non-`self`
|
|
692
|
+
# receiver (`Favourite.select(...)`), suppress the private
|
|
693
|
+
# `Object`/`Kernel`/`Class` methods the fallback would
|
|
694
|
+
# otherwise resolve. Ruby raises `NoMethodError` for a
|
|
695
|
+
# private method called with an explicit receiver, so
|
|
696
|
+
# resolving `Favourite.select` to the private `Kernel#select`
|
|
697
|
+
# (`-> Array[String]`) is a confidently-wrong type. Implicit-
|
|
698
|
+
# self / `self.`-receiver calls (`puts`, `raise`, `require`)
|
|
699
|
+
# keep resolving — those are the fallback's intended targets.
|
|
668
700
|
RbsDispatch.try_dispatch(
|
|
669
701
|
receiver: fallback_receiver,
|
|
670
702
|
method_name: method_name,
|
|
671
703
|
args: arg_types,
|
|
672
704
|
environment: environment,
|
|
673
705
|
block_type: block_type,
|
|
674
|
-
self_type_override: receiver_type
|
|
706
|
+
self_type_override: receiver_type,
|
|
707
|
+
public_only: explicit_non_self_receiver?(call_node)
|
|
675
708
|
)
|
|
676
709
|
end
|
|
677
710
|
|
|
711
|
+
# True when the call node carries an explicit receiver that is
|
|
712
|
+
# not the literal `self`. Such a call cannot legally dispatch to
|
|
713
|
+
# a private method, so the user-class fallback must skip private
|
|
714
|
+
# signatures rather than return a confidently-wrong type. Returns
|
|
715
|
+
# false for implicit-self calls and `self.`-receiver calls (both
|
|
716
|
+
# may legally reach a private method in modern Ruby), and false
|
|
717
|
+
# when no `call_node` is supplied (internal dispatcher callers).
|
|
718
|
+
def explicit_non_self_receiver?(call_node)
|
|
719
|
+
return false if call_node.nil?
|
|
720
|
+
return false unless call_node.respond_to?(:receiver)
|
|
721
|
+
|
|
722
|
+
receiver = call_node.receiver
|
|
723
|
+
return false if receiver.nil?
|
|
724
|
+
|
|
725
|
+
!receiver.is_a?(Prism::SelfNode)
|
|
726
|
+
end
|
|
727
|
+
|
|
678
728
|
def user_class_fallback_receiver(receiver_type, environment)
|
|
679
729
|
case receiver_type
|
|
680
730
|
when Type::Nominal
|
|
@@ -743,6 +793,18 @@ module Rigor
|
|
|
743
793
|
array_lift = array_new_lift(receiver_type.class_name, arg_types)
|
|
744
794
|
return array_lift if array_lift
|
|
745
795
|
|
|
796
|
+
range_lift = range_new_lift(receiver_type.class_name, arg_types)
|
|
797
|
+
return range_lift if range_lift
|
|
798
|
+
|
|
799
|
+
set_lift = set_new_lift(receiver_type.class_name, arg_types)
|
|
800
|
+
return set_lift if set_lift
|
|
801
|
+
|
|
802
|
+
regexp_lift = regexp_new_lift(receiver_type.class_name, arg_types)
|
|
803
|
+
return regexp_lift if regexp_lift
|
|
804
|
+
|
|
805
|
+
date_lift = date_new_lift(receiver_type.class_name, arg_types)
|
|
806
|
+
return date_lift if date_lift
|
|
807
|
+
|
|
746
808
|
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
747
809
|
end
|
|
748
810
|
|
|
@@ -812,6 +874,119 @@ module Rigor
|
|
|
812
874
|
type
|
|
813
875
|
end
|
|
814
876
|
|
|
877
|
+
# `Range.new(b, e)` / `Range.new(b, e, excl)` — folds to
|
|
878
|
+
# `Constant[Range]` when both endpoints are `Constant[Integer]`
|
|
879
|
+
# or both are `Constant[String]`, and the optional third argument
|
|
880
|
+
# is a `Constant[true/false]`. Nil endpoints (beginless /
|
|
881
|
+
# endless ranges) are not folded because the useful instance
|
|
882
|
+
# methods (`to_a`, `first`, `last`) are not defined for them;
|
|
883
|
+
# the RBS tier answers `Nominal[Range]` for those forms.
|
|
884
|
+
def range_new_lift(class_name, arg_types)
|
|
885
|
+
return nil unless class_name == "Range"
|
|
886
|
+
return nil if arg_types.size < 2 || arg_types.size > 3
|
|
887
|
+
|
|
888
|
+
b_type = arg_types[0]
|
|
889
|
+
e_type = arg_types[1]
|
|
890
|
+
|
|
891
|
+
return nil unless b_type.is_a?(Type::Constant) && e_type.is_a?(Type::Constant)
|
|
892
|
+
|
|
893
|
+
b_val = b_type.value
|
|
894
|
+
e_val = e_type.value
|
|
895
|
+
|
|
896
|
+
# Only fold homogeneous Integer or String endpoint pairs.
|
|
897
|
+
return nil unless b_val.instance_of?(e_val.class)
|
|
898
|
+
return nil unless b_val.is_a?(Integer) || b_val.is_a?(String)
|
|
899
|
+
|
|
900
|
+
excl = range_new_excl(arg_types[2])
|
|
901
|
+
return nil if excl.nil?
|
|
902
|
+
|
|
903
|
+
Type::Combinator.constant_of(Range.new(b_val, e_val, excl))
|
|
904
|
+
rescue StandardError
|
|
905
|
+
nil
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
# Resolves the optional `exclude_end` argument for `range_new_lift`.
|
|
909
|
+
# Returns `false` (no arg), `true`/`false` (Constant[bool] arg),
|
|
910
|
+
# or `nil` to signal "decline" (wrong type / wrong value class).
|
|
911
|
+
def range_new_excl(excl_type)
|
|
912
|
+
case excl_type
|
|
913
|
+
when nil then false
|
|
914
|
+
when Type::Constant
|
|
915
|
+
excl_type.value if [true, false].include?(excl_type.value)
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
# `Set.new` / `Set.new(tuple_of_constants)` — folds to `Constant[Set]`
|
|
920
|
+
# when zero arguments are given or the single argument is a `Tuple`
|
|
921
|
+
# whose every element is a `Constant[T]`. Mirrors `SetFolding#fold_new`
|
|
922
|
+
# but lives here so the `:new` path in `try_meta_introspection` can
|
|
923
|
+
# reach it before the RBS tier answers `Nominal[Set]`.
|
|
924
|
+
def set_new_lift(class_name, arg_types)
|
|
925
|
+
return nil unless class_name == "Set"
|
|
926
|
+
return Type::Combinator.constant_of(::Set.new) if arg_types.empty?
|
|
927
|
+
return nil if arg_types.size > 1
|
|
928
|
+
|
|
929
|
+
arg = arg_types.first
|
|
930
|
+
return nil unless arg.is_a?(Type::Tuple)
|
|
931
|
+
return nil unless arg.elements.all?(Type::Constant)
|
|
932
|
+
|
|
933
|
+
values = arg.elements.map(&:value)
|
|
934
|
+
Type::Combinator.constant_of(::Set.new(values))
|
|
935
|
+
rescue StandardError
|
|
936
|
+
nil
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
# `Regexp.new(pattern)` / `Regexp.new(pattern, opts)` — folds to
|
|
940
|
+
# `Constant[Regexp]` when the pattern is a `Constant[String]`.
|
|
941
|
+
# Mirrors `RegexpFolding#fold_new` but lives here so the `:new` path
|
|
942
|
+
# in `try_meta_introspection` can reach it.
|
|
943
|
+
def regexp_new_lift(class_name, arg_types)
|
|
944
|
+
return nil unless class_name == "Regexp"
|
|
945
|
+
return nil if arg_types.empty? || arg_types.size > 2
|
|
946
|
+
|
|
947
|
+
pattern_arg = arg_types.first
|
|
948
|
+
return nil unless pattern_arg.is_a?(Type::Constant) && pattern_arg.value.is_a?(String)
|
|
949
|
+
|
|
950
|
+
opts = if arg_types.size == 2
|
|
951
|
+
opt_type = arg_types[1]
|
|
952
|
+
return nil unless opt_type.is_a?(Type::Constant)
|
|
953
|
+
|
|
954
|
+
opt_type.value
|
|
955
|
+
else
|
|
956
|
+
0
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
Type::Combinator.constant_of(Regexp.new(pattern_arg.value, opts))
|
|
960
|
+
rescue StandardError
|
|
961
|
+
nil
|
|
962
|
+
end
|
|
963
|
+
|
|
964
|
+
# `Date.new(y, m, d)` / `DateTime.new(y, m, d, h, min, s, off)`
|
|
965
|
+
# — folds to `Constant[Date]` / `Constant[DateTime]` when every
|
|
966
|
+
# argument is a `Constant` carrying an Integer (or, for the
|
|
967
|
+
# `DateTime` offset / `start` slots, a Rational or String).
|
|
968
|
+
# `Date.new` carries no timezone, and `DateTime.new`'s offset
|
|
969
|
+
# defaults to UTC, so the literal is machine-independent.
|
|
970
|
+
# `Date.today` / `Date.parse` are not constructors here — they
|
|
971
|
+
# are non-deterministic / string-dependent and stay
|
|
972
|
+
# `Nominal[Date]`.
|
|
973
|
+
DATE_NEW_CLASSES = %w[Date DateTime].freeze
|
|
974
|
+
private_constant :DATE_NEW_CLASSES
|
|
975
|
+
|
|
976
|
+
def date_new_lift(class_name, arg_types)
|
|
977
|
+
return nil unless DATE_NEW_CLASSES.include?(class_name)
|
|
978
|
+
return nil unless arg_types.size.between?(1, 8)
|
|
979
|
+
return nil unless arg_types.all?(Type::Constant)
|
|
980
|
+
|
|
981
|
+
values = arg_types.map(&:value)
|
|
982
|
+
return nil unless values.all? { |v| v.is_a?(Integer) || v.is_a?(Rational) || v.is_a?(String) }
|
|
983
|
+
|
|
984
|
+
klass = class_name == "Date" ? Date : DateTime
|
|
985
|
+
Type::Combinator.constant_of(klass.new(*values))
|
|
986
|
+
rescue StandardError
|
|
987
|
+
nil
|
|
988
|
+
end
|
|
989
|
+
|
|
815
990
|
CONSTANT_METACLASSES = {
|
|
816
991
|
Integer => "Integer", Float => "Float", String => "String",
|
|
817
992
|
Symbol => "Symbol", Range => "Range",
|
|
@@ -43,10 +43,15 @@ module Rigor
|
|
|
43
43
|
# method (either `def self.foo` or a `def foo` inside
|
|
44
44
|
# `class << self`); routes the lookup through
|
|
45
45
|
# `RbsLoader#singleton_method`.
|
|
46
|
-
|
|
46
|
+
# @param source_path [String, nil] the project-relative path of
|
|
47
|
+
# the file the method is defined in. Used to match ADR-28
|
|
48
|
+
# path-scoped protocol contracts; `nil` (the default for
|
|
49
|
+
# synthetic / probe scopes) disables the contract tier.
|
|
50
|
+
def initialize(environment:, class_path:, singleton:, source_path: nil)
|
|
47
51
|
@environment = environment
|
|
48
52
|
@class_path = class_path
|
|
49
53
|
@singleton = singleton
|
|
54
|
+
@source_path = source_path
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
# @param def_node [Prism::DefNode]
|
|
@@ -58,15 +63,22 @@ module Rigor
|
|
|
58
63
|
types = default_types_for(slots)
|
|
59
64
|
|
|
60
65
|
rbs_method = lookup_rbs_method(def_node)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
if rbs_method
|
|
67
|
+
apply_rbs_overloads(types, slots, rbs_method.method_types) unless rbs_method.method_types.empty?
|
|
68
|
+
# `rigor:v1:param: <name> <refinement>` annotations
|
|
69
|
+
# tighten the bound type for matching slots. Applied
|
|
70
|
+
# after the RBS-overload pass so the override is the
|
|
71
|
+
# authoritative answer regardless of what the RBS
|
|
72
|
+
# signature declared.
|
|
73
|
+
apply_param_overrides(types, slots, rbs_method)
|
|
74
|
+
end
|
|
75
|
+
# ADR-28 — a path-scoped protocol contract supplies the
|
|
76
|
+
# parameter type for a matching `def`. Applied last (most
|
|
77
|
+
# authoritative) and regardless of RBS presence: the
|
|
78
|
+
# methods a contract targets — controller actions and the
|
|
79
|
+
# like — typically have no RBS signature at all, so this
|
|
80
|
+
# tier must run even when `rbs_method` is nil.
|
|
81
|
+
apply_protocol_contract(types, slots, def_node)
|
|
70
82
|
types
|
|
71
83
|
end
|
|
72
84
|
|
|
@@ -75,6 +87,10 @@ module Rigor
|
|
|
75
87
|
ParamSlot = Data.define(:kind, :name, :index)
|
|
76
88
|
private_constant :ParamSlot
|
|
77
89
|
|
|
90
|
+
# Slot kinds a contract's positional `index` can address.
|
|
91
|
+
POSITIONAL_KINDS = %i[required_positional optional_positional].freeze
|
|
92
|
+
private_constant :POSITIONAL_KINDS
|
|
93
|
+
|
|
78
94
|
# Walk the Prism `ParametersNode` and emit one slot per named
|
|
79
95
|
# parameter, in declaration order. Anonymous slots (rest /
|
|
80
96
|
# keyword-rest with no name) are skipped because we have no
|
|
@@ -187,6 +203,47 @@ module Rigor
|
|
|
187
203
|
end
|
|
188
204
|
end
|
|
189
205
|
|
|
206
|
+
# ADR-28 — when a path-scoped protocol contract targets this
|
|
207
|
+
# `def` (file path matches the contract's `path_glob`, method
|
|
208
|
+
# name + singleton-ness match), replace each contracted
|
|
209
|
+
# positional slot's binding with the contract's declared type.
|
|
210
|
+
# The type name resolves against the environment lazily here;
|
|
211
|
+
# an unresolvable name (the protocol's RBS not loaded) falls
|
|
212
|
+
# through to whatever the prior tiers bound, fail-soft.
|
|
213
|
+
def apply_protocol_contract(types, slots, def_node)
|
|
214
|
+
return if @source_path.nil?
|
|
215
|
+
|
|
216
|
+
registry = @environment.respond_to?(:plugin_registry) ? @environment.plugin_registry : nil
|
|
217
|
+
return if registry.nil?
|
|
218
|
+
|
|
219
|
+
contract = matching_contract(registry, def_node)
|
|
220
|
+
return if contract.nil?
|
|
221
|
+
|
|
222
|
+
contract.param_types.each do |param_type|
|
|
223
|
+
slot = positional_slot_at(slots, param_type.index)
|
|
224
|
+
next if slot.nil? || slot.name.nil?
|
|
225
|
+
|
|
226
|
+
resolved = @environment.nominal_for_name(param_type.type_name)
|
|
227
|
+
next if resolved.nil?
|
|
228
|
+
|
|
229
|
+
types[slot.name] = resolved
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def matching_contract(registry, def_node)
|
|
234
|
+
contracts = registry.contracts_for_path(@source_path)
|
|
235
|
+
return nil if contracts.empty?
|
|
236
|
+
|
|
237
|
+
singleton = def_node.receiver.is_a?(Prism::SelfNode) || @singleton
|
|
238
|
+
contracts.find do |contract|
|
|
239
|
+
contract.method_name == def_node.name && contract.singleton == singleton
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def positional_slot_at(slots, index)
|
|
244
|
+
slots.find { |slot| POSITIONAL_KINDS.include?(slot.kind) && slot.index == index }
|
|
245
|
+
end
|
|
246
|
+
|
|
190
247
|
def collect_translated_types(method_types, slot)
|
|
191
248
|
rbs_types = method_types.flat_map do |mt|
|
|
192
249
|
t = rbs_type_for_slot(mt.type, slot)
|
|
@@ -386,8 +386,8 @@ module Rigor
|
|
|
386
386
|
analyse_local_read(node, scope)
|
|
387
387
|
when Prism::LocalVariableWriteNode
|
|
388
388
|
analyse_local_write(node, scope)
|
|
389
|
-
when Prism::InstanceVariableWriteNode
|
|
390
|
-
|
|
389
|
+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode
|
|
390
|
+
analyse_ivar(node, scope)
|
|
391
391
|
when Prism::ClassVariableWriteNode
|
|
392
392
|
analyse_cvar_write(node, scope)
|
|
393
393
|
when Prism::GlobalVariableWriteNode
|
|
@@ -767,7 +767,17 @@ module Rigor
|
|
|
767
767
|
]
|
|
768
768
|
end
|
|
769
769
|
|
|
770
|
-
|
|
770
|
+
# Truthy guard on an instance variable. Covers both the bare
|
|
771
|
+
# read — `if @ivar`, the left arm of `@ivar && @ivar.foo`,
|
|
772
|
+
# the receiver of `unless @ivar.nil?` once `!` reverses it —
|
|
773
|
+
# and the assignment-in-condition write `if @ivar = expr`.
|
|
774
|
+
# Mirrors `analyse_local_read` / `analyse_local_write`: the
|
|
775
|
+
# truthy edge narrows the ivar by `narrow_truthy` (dropping
|
|
776
|
+
# `nil` / `false`), the falsey edge by `narrow_falsey`. The
|
|
777
|
+
# narrowing is scoped to the guarded branch only, so it is
|
|
778
|
+
# purely additive — it never widens or re-narrows the ivar
|
|
779
|
+
# binding seen elsewhere.
|
|
780
|
+
def analyse_ivar(node, scope)
|
|
771
781
|
current = scope.ivar(node.name)
|
|
772
782
|
return nil if current.nil?
|
|
773
783
|
|
|
@@ -2024,15 +2034,24 @@ module Rigor
|
|
|
2024
2034
|
end
|
|
2025
2035
|
|
|
2026
2036
|
def analyse_nil_predicate(receiver, scope)
|
|
2027
|
-
|
|
2037
|
+
case receiver
|
|
2038
|
+
when Prism::LocalVariableReadNode
|
|
2039
|
+
current = scope.local(receiver.name)
|
|
2040
|
+
return nil if current.nil?
|
|
2028
2041
|
|
|
2029
|
-
|
|
2030
|
-
|
|
2042
|
+
[
|
|
2043
|
+
scope.with_local(receiver.name, narrow_nil(current)),
|
|
2044
|
+
scope.with_local(receiver.name, narrow_non_nil(current))
|
|
2045
|
+
]
|
|
2046
|
+
when Prism::InstanceVariableReadNode
|
|
2047
|
+
current = scope.ivar(receiver.name)
|
|
2048
|
+
return nil if current.nil?
|
|
2031
2049
|
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2050
|
+
[
|
|
2051
|
+
scope.with_ivar(receiver.name, narrow_nil(current)),
|
|
2052
|
+
scope.with_ivar(receiver.name, narrow_non_nil(current))
|
|
2053
|
+
]
|
|
2054
|
+
end
|
|
2036
2055
|
end
|
|
2037
2056
|
|
|
2038
2057
|
# `a && b` short-circuits: the truthy edge is the truthy edge
|