rigortype 0.1.0 → 0.1.2
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 +7 -2
- data/data/builtins/ruby_core/array.yml +6 -6
- data/data/builtins/ruby_core/hash.yml +1 -1
- data/data/builtins/ruby_core/io.yml +3 -3
- data/data/builtins/ruby_core/numeric.yml +1 -1
- data/data/builtins/ruby_core/pathname.yml +100 -100
- data/data/builtins/ruby_core/proc.yml +1 -1
- data/data/builtins/ruby_core/range.yml +6 -4
- data/data/builtins/ruby_core/string.yml +15 -10
- data/data/builtins/ruby_core/time.yml +3 -3
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +116 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +123 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +118 -0
- data/lib/rigor/analysis/check_rules.rb +346 -18
- data/lib/rigor/analysis/rule_catalog.rb +343 -0
- data/lib/rigor/analysis/runner.rb +90 -6
- data/lib/rigor/builtins/regex_refinement.rb +104 -0
- data/lib/rigor/cli/diff_command.rb +169 -0
- data/lib/rigor/cli/explain_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +3 -3
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli.rb +29 -5
- data/lib/rigor/configuration/severity_profile.rb +18 -3
- data/lib/rigor/configuration.rb +186 -13
- data/lib/rigor/environment.rb +12 -4
- data/lib/rigor/inference/expression_typer.rb +3 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +43 -2
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +104 -12
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
- data/lib/rigor/inference/method_dispatcher.rb +50 -1
- data/lib/rigor/inference/narrowing.rb +150 -6
- data/lib/rigor/inference/scope_indexer.rb +220 -17
- data/lib/rigor/inference/statement_evaluator.rb +29 -0
- data/lib/rigor/plugin/base.rb +43 -0
- data/lib/rigor/plugin/fact_store.rb +92 -0
- data/lib/rigor/plugin/io_boundary.rb +92 -19
- data/lib/rigor/plugin/load_error.rb +14 -2
- data/lib/rigor/plugin/loader.rb +116 -0
- data/lib/rigor/plugin/manifest.rb +75 -6
- data/lib/rigor/plugin/services.rb +14 -2
- data/lib/rigor/plugin/trust_policy.rb +30 -7
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/scope.rb +30 -5
- data/lib/rigor/trinary.rb +1 -1
- data/lib/rigor/type/integer_range.rb +6 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +8 -2
- metadata +9 -1
|
@@ -122,10 +122,25 @@ module Rigor
|
|
|
122
122
|
Type::Nominal => :dispatch_nominal_size,
|
|
123
123
|
Type::Difference => :dispatch_difference,
|
|
124
124
|
Type::Refined => :dispatch_refined,
|
|
125
|
-
Type::Intersection => :dispatch_intersection
|
|
125
|
+
Type::Intersection => :dispatch_intersection,
|
|
126
|
+
Type::IntegerRange => :dispatch_integer_range
|
|
126
127
|
}.freeze
|
|
127
128
|
private_constant :RECEIVER_HANDLERS
|
|
128
129
|
|
|
130
|
+
# v0.1.1 Track 1 slice 5b — `Integer#to_s(base)` on a
|
|
131
|
+
# non-negative `IntegerRange` receiver. The output of
|
|
132
|
+
# `n.to_s(b)` for `n >= 0` is digit-string-only (no
|
|
133
|
+
# leading sign), so when the base is in this table the
|
|
134
|
+
# result lifts to the matching imported refinement.
|
|
135
|
+
# Bases not listed (2, 36, ...) keep the v0.1.0 baseline
|
|
136
|
+
# since Rigor has no carrier for the resulting alphabet.
|
|
137
|
+
TO_S_BASE_REFINEMENTS = {
|
|
138
|
+
10 => :decimal_int_string,
|
|
139
|
+
8 => :octal_int_string,
|
|
140
|
+
16 => :hex_int_string
|
|
141
|
+
}.freeze
|
|
142
|
+
private_constant :TO_S_BASE_REFINEMENTS
|
|
143
|
+
|
|
129
144
|
def try_dispatch(receiver:, method_name:, args:)
|
|
130
145
|
args ||= []
|
|
131
146
|
handler = RECEIVER_HANDLERS[receiver.class]
|
|
@@ -184,6 +199,42 @@ module Rigor
|
|
|
184
199
|
Type::Combinator.non_negative_int
|
|
185
200
|
end
|
|
186
201
|
|
|
202
|
+
# `IntegerRange#to_s` precision (v0.1.1 Track 1 slice 5b).
|
|
203
|
+
# When the range's lower bound is `>= 0`, every member is
|
|
204
|
+
# a non-negative integer and `to_s(base)` returns a
|
|
205
|
+
# digit-string with no leading sign. The result lifts to
|
|
206
|
+
# the matching imported refinement (`decimal-int-string`
|
|
207
|
+
# for base 10, `octal-int-string` for 8, `hex-int-string`
|
|
208
|
+
# for 16). Signed ranges fall through (the result could
|
|
209
|
+
# carry a `-` sign that no Rigor refinement currently
|
|
210
|
+
# captures), as do bases without a digit-only refinement.
|
|
211
|
+
def dispatch_integer_range(range, method_name, args)
|
|
212
|
+
return nil unless method_name == :to_s
|
|
213
|
+
return nil unless range.lower >= 0
|
|
214
|
+
|
|
215
|
+
base = base_argument(args)
|
|
216
|
+
return nil if base.nil?
|
|
217
|
+
|
|
218
|
+
refinement = TO_S_BASE_REFINEMENTS[base]
|
|
219
|
+
return nil if refinement.nil?
|
|
220
|
+
|
|
221
|
+
Type::Combinator.public_send(refinement)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# `to_s` with no argument defaults to base 10. With one
|
|
225
|
+
# argument, the value MUST be a `Constant<Integer>` to
|
|
226
|
+
# be statically known. Anything else (Nominal[Integer]
|
|
227
|
+
# arg, multi-arg, etc.) declines.
|
|
228
|
+
def base_argument(args)
|
|
229
|
+
return 10 if args.empty?
|
|
230
|
+
return nil unless args.size == 1
|
|
231
|
+
|
|
232
|
+
arg = args.first
|
|
233
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
|
|
234
|
+
|
|
235
|
+
arg.value
|
|
236
|
+
end
|
|
237
|
+
|
|
187
238
|
# Refinement-aware projections over a `Difference[base,
|
|
188
239
|
# removed]` receiver. When the removed value is the
|
|
189
240
|
# empty witness of the base (`Constant[""]` for
|
|
@@ -289,7 +340,21 @@ module Rigor
|
|
|
289
340
|
%i[octal_int downcase] => :refined_self,
|
|
290
341
|
%i[octal_int upcase] => :refined_self,
|
|
291
342
|
%i[hex_int downcase] => :refined_self,
|
|
292
|
-
%i[hex_int upcase] => :refined_self
|
|
343
|
+
%i[hex_int upcase] => :refined_self,
|
|
344
|
+
# v0.1.1 Track 1 slice 2 — `to_i` / `to_int` on a
|
|
345
|
+
# known digit-only string. `decimal-int-string`
|
|
346
|
+
# (`/\A\d+\z/`) and `numeric-string` (Rigor's
|
|
347
|
+
# numeric-string predicate, ASCII digits) are
|
|
348
|
+
# predicates over digit-only strings, so the parse
|
|
349
|
+
# is total over the carrier domain and the result
|
|
350
|
+
# is always `>= 0`. `non-negative-int` is the
|
|
351
|
+
# tightest carrier that captures both the lower
|
|
352
|
+
# bound and the integer-ness without inventing a
|
|
353
|
+
# narrower carrier.
|
|
354
|
+
%i[decimal_int to_i] => :non_negative_int,
|
|
355
|
+
%i[decimal_int to_int] => :non_negative_int,
|
|
356
|
+
%i[numeric to_i] => :non_negative_int,
|
|
357
|
+
%i[numeric to_int] => :non_negative_int
|
|
293
358
|
}.freeze
|
|
294
359
|
private_constant :REFINED_STRING_PROJECTIONS
|
|
295
360
|
|
|
@@ -313,6 +378,7 @@ module Rigor
|
|
|
313
378
|
when :refined_self then refined
|
|
314
379
|
when :uppercase_string then Type::Combinator.uppercase_string
|
|
315
380
|
when :lowercase_string then Type::Combinator.lowercase_string
|
|
381
|
+
when :non_negative_int then Type::Combinator.non_negative_int
|
|
316
382
|
end
|
|
317
383
|
end
|
|
318
384
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../reflection"
|
|
4
4
|
require_relative "../type"
|
|
5
|
+
require_relative "../flow_contribution"
|
|
6
|
+
require_relative "../flow_contribution/merger"
|
|
5
7
|
require_relative "method_dispatcher/constant_folding"
|
|
6
8
|
require_relative "method_dispatcher/literal_string_folding"
|
|
7
9
|
require_relative "method_dispatcher/shape_dispatch"
|
|
@@ -59,12 +61,25 @@ module Rigor
|
|
|
59
61
|
# @param environment [Rigor::Environment, nil] required for
|
|
60
62
|
# RBS-backed dispatch; when nil only constant folding can fire.
|
|
61
63
|
# @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
|
|
62
|
-
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
64
|
+
def dispatch(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/ParameterLists
|
|
65
|
+
block_type: nil, environment: nil,
|
|
66
|
+
call_node: nil, scope: nil)
|
|
63
67
|
return nil if receiver_type.nil?
|
|
64
68
|
|
|
65
69
|
precise = dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type)
|
|
66
70
|
return precise if precise
|
|
67
71
|
|
|
72
|
+
# v0.1.1 Track 2 slice 7 — plugin return-type contribution
|
|
73
|
+
# tier. Sits ahead of `RbsDispatch` so a plugin that
|
|
74
|
+
# understands a domain-specific dispatch (e.g. an
|
|
75
|
+
# `ActiveRecord::Base.find` returning `Nominal[<resolved
|
|
76
|
+
# model>]`) wins over the RBS-projected envelope. Only
|
|
77
|
+
# consults the registry when both `call_node` and `scope`
|
|
78
|
+
# are supplied — the dispatcher's own internal callers
|
|
79
|
+
# (per-element block fold, etc.) skip this tier.
|
|
80
|
+
plugin_result = try_plugin_contribution(call_node, scope)
|
|
81
|
+
return plugin_result if plugin_result
|
|
82
|
+
|
|
68
83
|
rbs_result = RbsDispatch.try_dispatch(
|
|
69
84
|
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
70
85
|
environment: environment, block_type: block_type
|
|
@@ -85,6 +100,40 @@ module Rigor
|
|
|
85
100
|
try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
86
101
|
end
|
|
87
102
|
|
|
103
|
+
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
104
|
+
# slice 7. Walks every loaded plugin's
|
|
105
|
+
# `#flow_contribution_for(call_node:, scope:)` hook,
|
|
106
|
+
# collects the non-nil `FlowContribution` bundles, merges
|
|
107
|
+
# them through `FlowContribution::Merger`, and returns
|
|
108
|
+
# the merged `return_type` slot (or nil when no plugin
|
|
109
|
+
# contributed a return type).
|
|
110
|
+
#
|
|
111
|
+
# Plugins whose hook raises have their contribution
|
|
112
|
+
# silently dropped for this call so the dispatch chain
|
|
113
|
+
# keeps moving — the run-level diagnostic envelope (per
|
|
114
|
+
# ADR-2 § "Plugin Trust and I/O Policy") is owned by
|
|
115
|
+
# `Analysis::Runner#plugin_emitted_diagnostics`.
|
|
116
|
+
def try_plugin_contribution(call_node, scope)
|
|
117
|
+
return nil if call_node.nil? || scope.nil?
|
|
118
|
+
|
|
119
|
+
registry = scope.environment&.plugin_registry
|
|
120
|
+
return nil if registry.nil? || registry.empty?
|
|
121
|
+
|
|
122
|
+
contributions = collect_plugin_contributions(registry, call_node, scope)
|
|
123
|
+
return nil if contributions.empty?
|
|
124
|
+
|
|
125
|
+
FlowContribution::Merger.merge(contributions).return_type
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def collect_plugin_contributions(registry, call_node, scope)
|
|
129
|
+
registry.plugins.filter_map do |plugin|
|
|
130
|
+
contribution = plugin.flow_contribution_for(call_node: call_node, scope: scope)
|
|
131
|
+
contribution.is_a?(FlowContribution) ? contribution : nil
|
|
132
|
+
rescue StandardError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
88
137
|
# Runs the precision tiers (constant fold, shape dispatch,
|
|
89
138
|
# file-path fold, block fold) in order and returns the first
|
|
90
139
|
# non-nil answer. Each tier owns its own receiver/argument
|
|
@@ -7,6 +7,7 @@ require_relative "../type"
|
|
|
7
7
|
require_relative "../environment"
|
|
8
8
|
require_relative "../rbs_extended"
|
|
9
9
|
require_relative "../analysis/fact_store"
|
|
10
|
+
require_relative "../builtins/regex_refinement"
|
|
10
11
|
|
|
11
12
|
module Rigor
|
|
12
13
|
module Inference
|
|
@@ -746,20 +747,51 @@ module Rigor
|
|
|
746
747
|
# `nil`. Subtract the dead half on each edge so callers
|
|
747
748
|
# like `year.upcase` inside the truthy branch no longer
|
|
748
749
|
# fire `possible-nil-receiver`.
|
|
750
|
+
#
|
|
751
|
+
# v0.1.1 Track 1 slice 1 — when the regex source is a
|
|
752
|
+
# statically known `RegularExpressionNode` and a named
|
|
753
|
+
# capture's body matches one of the curated shapes in
|
|
754
|
+
# {Rigor::Builtins::RegexRefinement::RULES}, the truthy
|
|
755
|
+
# branch narrows further than `String` to the matching
|
|
756
|
+
# imported refinement (e.g. `decimal-int-string` for
|
|
757
|
+
# `\d+`). Bodies outside the table fall back to the
|
|
758
|
+
# v0.1.0 baseline (plain `String`).
|
|
749
759
|
def analyse_match_write(node, scope)
|
|
750
760
|
string_t = Type::Combinator.nominal_of("String")
|
|
751
761
|
nil_t = Type::Combinator.constant_of(nil)
|
|
762
|
+
refinements = match_write_capture_refinements(node)
|
|
752
763
|
truthy = scope
|
|
753
764
|
falsey = scope
|
|
754
765
|
node.targets.each do |target|
|
|
755
766
|
next unless target.is_a?(Prism::LocalVariableTargetNode)
|
|
756
767
|
|
|
757
|
-
truthy = truthy.with_local(target.name, string_t)
|
|
768
|
+
truthy = truthy.with_local(target.name, refinements[target.name] || string_t)
|
|
758
769
|
falsey = falsey.with_local(target.name, nil_t)
|
|
759
770
|
end
|
|
760
771
|
[truthy, falsey]
|
|
761
772
|
end
|
|
762
773
|
|
|
774
|
+
# Extracts `{ capture_name => Refinement }` for every named
|
|
775
|
+
# capture group in the `MatchWriteNode`'s wrapped `=~` call
|
|
776
|
+
# whose body the recogniser table accepts. Bodies that
|
|
777
|
+
# contain nested groups, anchors, alternation, or anything
|
|
778
|
+
# else outside the curated forms drop out and the caller
|
|
779
|
+
# falls back to plain `String`.
|
|
780
|
+
NAMED_CAPTURE_BODY_RE = /\(\?<([A-Za-z_][A-Za-z0-9_]*)>([^()|]*)\)/
|
|
781
|
+
private_constant :NAMED_CAPTURE_BODY_RE
|
|
782
|
+
|
|
783
|
+
def match_write_capture_refinements(node)
|
|
784
|
+
regex = node.call.is_a?(Prism::CallNode) ? node.call.receiver : nil
|
|
785
|
+
return {} unless regex.is_a?(Prism::RegularExpressionNode)
|
|
786
|
+
|
|
787
|
+
refinements = {}
|
|
788
|
+
regex.unescaped.scan(NAMED_CAPTURE_BODY_RE) do |name, body|
|
|
789
|
+
type = ::Rigor::Builtins::RegexRefinement.for_capture_body(body)
|
|
790
|
+
refinements[name.to_sym] = type if type
|
|
791
|
+
end
|
|
792
|
+
refinements
|
|
793
|
+
end
|
|
794
|
+
|
|
763
795
|
# Recognised CallNode predicates:
|
|
764
796
|
# - `recv.nil?` (Slice 6 phase 1, no args, no block)
|
|
765
797
|
# - unary `!recv` (`name == :!`, no args, no block)
|
|
@@ -772,19 +804,82 @@ module Rigor
|
|
|
772
804
|
# through to the no-narrowing fallback.
|
|
773
805
|
def analyse_call(node, scope)
|
|
774
806
|
return nil if node.block
|
|
775
|
-
return nil if node.receiver.nil?
|
|
776
807
|
|
|
777
|
-
|
|
778
|
-
|
|
808
|
+
unless node.receiver.nil?
|
|
809
|
+
shape_result = dispatch_call(node, scope, node.name)
|
|
810
|
+
return shape_result if shape_result
|
|
811
|
+
|
|
812
|
+
# v0.1.1 Track 1 slice 4 — String predicate flow facts.
|
|
813
|
+
string_predicate_result = analyse_string_predicate(node, scope)
|
|
814
|
+
return string_predicate_result if string_predicate_result
|
|
815
|
+
end
|
|
779
816
|
|
|
780
817
|
# Slice 7 phase 15 — RBS::Extended predicate
|
|
781
818
|
# effects. When the method's RBS signature carries
|
|
782
819
|
# `rigor:v1:predicate-if-true` / `predicate-if-false`
|
|
783
820
|
# annotations, apply them to narrow the corresponding
|
|
784
|
-
# local-variable arguments on each edge.
|
|
821
|
+
# local-variable arguments on each edge. v0.1.1 Track
|
|
822
|
+
# 1 slice 3 — implicit-self calls (`recv == nil`,
|
|
823
|
+
# e.g. `admin?` inside an instance method body) flow
|
|
824
|
+
# through here too so `self`-targeted facts can edit
|
|
825
|
+
# `scope.self_type`.
|
|
785
826
|
analyse_rbs_extended_contribution(node, scope)
|
|
786
827
|
end
|
|
787
828
|
|
|
829
|
+
# v0.1.1 Track 1 slice 4 — `String#start_with?` /
|
|
830
|
+
# `#end_with?` / `#include?` against a `Constant<String>`
|
|
831
|
+
# needle attaches a relational `FactStore::Fact` to the
|
|
832
|
+
# local on each edge. The receiver's type does NOT
|
|
833
|
+
# change — Rigor has no "starts-with-X" carrier today —
|
|
834
|
+
# so the fact carries the predicate semantics for any
|
|
835
|
+
# downstream consumer that wants to read it (e.g. a
|
|
836
|
+
# plugin's `prepare(services)` hook in v0.1.x).
|
|
837
|
+
# Truthy edge: positive polarity. Falsey edge: negative
|
|
838
|
+
# polarity. Mirrors the equality-predicate fact pattern
|
|
839
|
+
# already used by `analyse_equality_predicate`.
|
|
840
|
+
STRING_PREDICATE_NAMES = %i[start_with? end_with? include?].freeze
|
|
841
|
+
private_constant :STRING_PREDICATE_NAMES
|
|
842
|
+
|
|
843
|
+
def analyse_string_predicate(node, scope)
|
|
844
|
+
return nil unless string_predicate_call?(node)
|
|
845
|
+
|
|
846
|
+
needle = string_predicate_needle(node, scope)
|
|
847
|
+
return nil if needle.nil?
|
|
848
|
+
|
|
849
|
+
local_name = node.receiver.name
|
|
850
|
+
return nil if scope.local(local_name).nil?
|
|
851
|
+
|
|
852
|
+
[
|
|
853
|
+
scope.with_fact(string_predicate_fact(local_name, node.name, needle, polarity: :positive)),
|
|
854
|
+
scope.with_fact(string_predicate_fact(local_name, node.name, needle, polarity: :negative))
|
|
855
|
+
]
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
def string_predicate_call?(node)
|
|
859
|
+
STRING_PREDICATE_NAMES.include?(node.name) &&
|
|
860
|
+
node.receiver.is_a?(Prism::LocalVariableReadNode) &&
|
|
861
|
+
!node.arguments.nil? &&
|
|
862
|
+
node.arguments.arguments.size == 1
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def string_predicate_needle(node, scope)
|
|
866
|
+
needle_type = static_literal_type(node.arguments.arguments.first, scope)
|
|
867
|
+
return nil unless needle_type.is_a?(Type::Constant) && needle_type.value.is_a?(String)
|
|
868
|
+
|
|
869
|
+
needle_type.value
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
def string_predicate_fact(name, predicate, needle, polarity:)
|
|
873
|
+
Analysis::FactStore::Fact.new(
|
|
874
|
+
bucket: :relational,
|
|
875
|
+
target: Analysis::FactStore::Target.local(name),
|
|
876
|
+
predicate: predicate,
|
|
877
|
+
payload: needle,
|
|
878
|
+
polarity: polarity,
|
|
879
|
+
stability: :local_binding
|
|
880
|
+
)
|
|
881
|
+
end
|
|
882
|
+
|
|
788
883
|
ZERO_CLASS_PREDICATES = %i[positive? negative? zero? nonzero?].freeze
|
|
789
884
|
COMPARISON_OPERATORS = %i[< <= > >=].freeze
|
|
790
885
|
private_constant :ZERO_CLASS_PREDICATES, :COMPARISON_OPERATORS
|
|
@@ -1267,6 +1362,7 @@ module Rigor
|
|
|
1267
1362
|
|
|
1268
1363
|
def apply_fact_to_scope(fact, call_node, entry_scope, target_scope, method_def)
|
|
1269
1364
|
target_node = fact_target_node(fact, call_node, method_def)
|
|
1365
|
+
return apply_self_fact(fact, target_node, entry_scope, target_scope) if fact.target_kind == :self
|
|
1270
1366
|
return target_scope unless target_node.is_a?(Prism::LocalVariableReadNode)
|
|
1271
1367
|
|
|
1272
1368
|
local_name = target_node.name
|
|
@@ -1277,6 +1373,45 @@ module Rigor
|
|
|
1277
1373
|
target_scope.with_local(local_name, narrowed)
|
|
1278
1374
|
end
|
|
1279
1375
|
|
|
1376
|
+
# v0.1.1 Track 1 slice 3 — `target_kind == :self` facts
|
|
1377
|
+
# (`predicate-if-true self is T`, `predicate-if-false
|
|
1378
|
+
# self is T`) narrow whichever scope binding is bound to
|
|
1379
|
+
# the call's receiver. Four receiver shapes participate:
|
|
1380
|
+
#
|
|
1381
|
+
# `recv.method?` where recv is...
|
|
1382
|
+
# - `LocalVariableReadNode` -> narrow that local
|
|
1383
|
+
# - `InstanceVariableReadNode` -> narrow that ivar
|
|
1384
|
+
# - `Prism::SelfNode` (explicit self) -> narrow `self_type`
|
|
1385
|
+
# - nil (implicit self call inside a method body)
|
|
1386
|
+
# -> narrow `self_type`
|
|
1387
|
+
#
|
|
1388
|
+
# Other receiver shapes (method chains, expressions) have
|
|
1389
|
+
# no scope binding to narrow against and fall through.
|
|
1390
|
+
def apply_self_fact(fact, receiver_node, entry_scope, target_scope)
|
|
1391
|
+
case receiver_node
|
|
1392
|
+
when nil, Prism::SelfNode
|
|
1393
|
+
current = entry_scope.self_type
|
|
1394
|
+
return target_scope if current.nil?
|
|
1395
|
+
|
|
1396
|
+
narrowed = narrow_for_fact(current, fact, entry_scope.environment)
|
|
1397
|
+
target_scope.with_self_type(narrowed)
|
|
1398
|
+
when Prism::LocalVariableReadNode
|
|
1399
|
+
current = entry_scope.local(receiver_node.name)
|
|
1400
|
+
return target_scope if current.nil?
|
|
1401
|
+
|
|
1402
|
+
narrowed = narrow_for_fact(current, fact, entry_scope.environment)
|
|
1403
|
+
target_scope.with_local(receiver_node.name, narrowed)
|
|
1404
|
+
when Prism::InstanceVariableReadNode
|
|
1405
|
+
current = entry_scope.ivar(receiver_node.name)
|
|
1406
|
+
return target_scope if current.nil?
|
|
1407
|
+
|
|
1408
|
+
narrowed = narrow_for_fact(current, fact, entry_scope.environment)
|
|
1409
|
+
target_scope.with_ivar(receiver_node.name, narrowed)
|
|
1410
|
+
else
|
|
1411
|
+
target_scope
|
|
1412
|
+
end
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1280
1415
|
# Resolves a Fact's target node. Mirrors the earlier
|
|
1281
1416
|
# `effect_target_node` helper but reads from the
|
|
1282
1417
|
# canonical Fact carrier; `:self` routes to the call
|
|
@@ -1291,7 +1426,9 @@ module Rigor
|
|
|
1291
1426
|
end
|
|
1292
1427
|
|
|
1293
1428
|
def resolve_rbs_extended_method(node, scope)
|
|
1294
|
-
receiver_type =
|
|
1429
|
+
receiver_type = receiver_type_for_resolve(node, scope)
|
|
1430
|
+
return nil if receiver_type.nil?
|
|
1431
|
+
|
|
1295
1432
|
class_name = rbs_extended_class_name(receiver_type)
|
|
1296
1433
|
return nil if class_name.nil?
|
|
1297
1434
|
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
@@ -1305,6 +1442,13 @@ module Rigor
|
|
|
1305
1442
|
nil
|
|
1306
1443
|
end
|
|
1307
1444
|
|
|
1445
|
+
# Implicit self call (`admin?` with no receiver inside an
|
|
1446
|
+
# instance method body) — read the method definition from
|
|
1447
|
+
# `scope.self_type` per v0.1.1 Track 1 slice 3.
|
|
1448
|
+
def receiver_type_for_resolve(node, scope)
|
|
1449
|
+
node.receiver.nil? ? scope.self_type : scope.type_of(node.receiver)
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1308
1452
|
def rbs_extended_class_name(receiver_type)
|
|
1309
1453
|
case receiver_type
|
|
1310
1454
|
when Type::Nominal, Type::Singleton then receiver_type.class_name
|