rigortype 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +40 -29
- data/lib/rigor/analysis/baseline.rb +347 -0
- data/lib/rigor/analysis/check_rules.rb +57 -2
- data/lib/rigor/builtins/static_return_refinements.rb +23 -1
- data/lib/rigor/cli/baseline_command.rb +377 -0
- data/lib/rigor/cli.rb +70 -3
- data/lib/rigor/configuration.rb +21 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +22 -0
- data/lib/rigor/environment.rb +13 -0
- data/lib/rigor/flow_contribution/fact.rb +20 -10
- data/lib/rigor/inference/expression_typer.rb +17 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
- data/lib/rigor/inference/method_dispatcher.rb +20 -3
- data/lib/rigor/inference/narrowing.rb +103 -1
- data/lib/rigor/inference/scope_indexer.rb +53 -7
- data/lib/rigor/inference/statement_evaluator.rb +66 -5
- data/lib/rigor/plugin/macro/heredoc_template.rb +2 -2
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor.rbs +1 -0
- metadata +3 -1
|
@@ -196,8 +196,8 @@ module Rigor
|
|
|
196
196
|
Prism::UntilNode => :type_of_loop,
|
|
197
197
|
Prism::ForNode => :type_of_dynamic_top,
|
|
198
198
|
Prism::DefinedNode => :type_of_defined,
|
|
199
|
-
Prism::NumberedReferenceReadNode => :
|
|
200
|
-
Prism::BackReferenceReadNode => :
|
|
199
|
+
Prism::NumberedReferenceReadNode => :type_of_numbered_reference,
|
|
200
|
+
Prism::BackReferenceReadNode => :type_of_back_reference,
|
|
201
201
|
Prism::MatchPredicateNode => :type_of_match_predicate,
|
|
202
202
|
Prism::MatchRequiredNode => :type_of_match_required,
|
|
203
203
|
Prism::MatchWriteNode => :type_of_dynamic_top,
|
|
@@ -347,6 +347,21 @@ module Rigor
|
|
|
347
347
|
)
|
|
348
348
|
end
|
|
349
349
|
|
|
350
|
+
# `$1` / `$2` / ... — numbered match-data globals. When the
|
|
351
|
+
# narrowing tier has bound a tighter type for this number
|
|
352
|
+
# (typically `String` after a `=~`-success guard like `unless
|
|
353
|
+
# /(\d+)/ =~ s; raise; end`), prefer the scope-bound type.
|
|
354
|
+
# Falls back to the default `String | nil`.
|
|
355
|
+
def type_of_numbered_reference(node)
|
|
356
|
+
scope.global(:"$#{node.number}") || type_of_string_or_nil(node)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# `$&` / `$'` / `$\`` / `$+` — symbolic back-references. Same
|
|
360
|
+
# narrowing model as numbered references.
|
|
361
|
+
def type_of_back_reference(node)
|
|
362
|
+
scope.global(node.name) || type_of_string_or_nil(node)
|
|
363
|
+
end
|
|
364
|
+
|
|
350
365
|
# `expr in pattern` — pattern-match predicate. Returns `true`
|
|
351
366
|
# when the pattern matches, `false` otherwise.
|
|
352
367
|
def type_of_match_predicate(_node)
|
|
@@ -74,10 +74,21 @@ module Rigor
|
|
|
74
74
|
# and binds the method-level type parameter that the
|
|
75
75
|
# block's return type references to `block_type` (Slice 6
|
|
76
76
|
# phase C sub-phase 2).
|
|
77
|
+
# @param self_type_override [Rigor::Type, nil] when set,
|
|
78
|
+
# the substitution for `Bases::Self` in the method's
|
|
79
|
+
# return type. Used by `MethodDispatcher#try_user_class_fallback`
|
|
80
|
+
# to preserve the ORIGINAL receiver as the substitute
|
|
81
|
+
# for `self` even though the dispatch is routed through
|
|
82
|
+
# `Nominal[Object]` — so that `Bundler::URI::Generic.dup`
|
|
83
|
+
# (which resolves through the `Object` fallback because
|
|
84
|
+
# `Bundler::URI::Generic` lacks RBS) returns
|
|
85
|
+
# `Bundler::URI::Generic` per `Kernel#dup: () -> self`
|
|
86
|
+
# rather than `Object`. Defaults to nil (compute self
|
|
87
|
+
# from the resolved class_name as before).
|
|
77
88
|
# @return [Rigor::Type, nil] inferred return type, or `nil`
|
|
78
89
|
# when no rule resolves (no class name, no method, dispatch
|
|
79
90
|
# on a Top/Dynamic[Top] receiver, etc.).
|
|
80
|
-
def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil)
|
|
91
|
+
def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil, self_type_override: nil)
|
|
81
92
|
return nil if environment.nil?
|
|
82
93
|
return nil unless environment.rbs_loader
|
|
83
94
|
|
|
@@ -86,7 +97,8 @@ module Rigor
|
|
|
86
97
|
method_name: method_name,
|
|
87
98
|
args: args,
|
|
88
99
|
environment: environment,
|
|
89
|
-
block_type: block_type
|
|
100
|
+
block_type: block_type,
|
|
101
|
+
self_type_override: self_type_override
|
|
90
102
|
)
|
|
91
103
|
end
|
|
92
104
|
|
|
@@ -128,26 +140,26 @@ module Rigor
|
|
|
128
140
|
class << self
|
|
129
141
|
private
|
|
130
142
|
|
|
131
|
-
def dispatch_for(receiver:, method_name:, args:, environment:, block_type:)
|
|
143
|
+
def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil)
|
|
132
144
|
args ||= []
|
|
133
145
|
case receiver
|
|
134
146
|
when Type::Union
|
|
135
|
-
dispatch_union(receiver, method_name, args, environment, block_type)
|
|
147
|
+
dispatch_union(receiver, method_name, args, environment, block_type, self_type_override)
|
|
136
148
|
else
|
|
137
|
-
dispatch_one(receiver, method_name, args, environment, block_type)
|
|
149
|
+
dispatch_one(receiver, method_name, args, environment, block_type, self_type_override)
|
|
138
150
|
end
|
|
139
151
|
end
|
|
140
152
|
|
|
141
|
-
def dispatch_union(receiver, method_name, args, environment, block_type)
|
|
153
|
+
def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil)
|
|
142
154
|
results = receiver.members.map do |member|
|
|
143
|
-
dispatch_one(member, method_name, args, environment, block_type)
|
|
155
|
+
dispatch_one(member, method_name, args, environment, block_type, self_type_override)
|
|
144
156
|
end
|
|
145
157
|
return nil if results.any?(&:nil?)
|
|
146
158
|
|
|
147
159
|
Type::Combinator.union(*results)
|
|
148
160
|
end
|
|
149
161
|
|
|
150
|
-
def dispatch_one(receiver, method_name, args, environment, block_type)
|
|
162
|
+
def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil)
|
|
151
163
|
descriptor = receiver_descriptor(receiver)
|
|
152
164
|
return nil unless descriptor
|
|
153
165
|
|
|
@@ -163,7 +175,8 @@ module Rigor
|
|
|
163
175
|
args: args,
|
|
164
176
|
type_vars: type_vars,
|
|
165
177
|
block_type: block_type,
|
|
166
|
-
environment: environment
|
|
178
|
+
environment: environment,
|
|
179
|
+
self_type_override: self_type_override
|
|
167
180
|
)
|
|
168
181
|
rescue StandardError
|
|
169
182
|
# Defensive: if RBS' definition builder raises on a broken
|
|
@@ -254,8 +267,10 @@ module Rigor
|
|
|
254
267
|
param_names.zip(receiver_args).to_h
|
|
255
268
|
end
|
|
256
269
|
|
|
270
|
+
# rubocop:disable Metrics/ParameterLists
|
|
257
271
|
def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:,
|
|
258
|
-
environment: nil)
|
|
272
|
+
environment: nil, self_type_override: nil)
|
|
273
|
+
# rubocop:enable Metrics/ParameterLists
|
|
259
274
|
# Slice 4b-3 (ADR-7 § "Slice 4-A/4-B") — read the
|
|
260
275
|
# return-type override through the merger so future
|
|
261
276
|
# plugin / `:rbs_extended` bundles that also assert a
|
|
@@ -266,11 +281,17 @@ module Rigor
|
|
|
266
281
|
return override if override
|
|
267
282
|
|
|
268
283
|
instance_type = Type::Combinator.nominal_of(class_name)
|
|
269
|
-
|
|
284
|
+
resolved_self_type =
|
|
270
285
|
case kind
|
|
271
286
|
when :singleton then Type::Combinator.singleton_of(class_name)
|
|
272
287
|
else instance_type
|
|
273
288
|
end
|
|
289
|
+
# `self_type_override` lets the user-class fallback
|
|
290
|
+
# path preserve the ORIGINAL receiver as the substitute
|
|
291
|
+
# for `Bases::Self` — so `Kernel#dup: () -> self`
|
|
292
|
+
# resolved through the Object fallback returns the
|
|
293
|
+
# caller's type, not Object.
|
|
294
|
+
self_type = self_type_override || resolved_self_type
|
|
274
295
|
|
|
275
296
|
method_type = OverloadSelector.select(
|
|
276
297
|
method_definition,
|
|
@@ -657,21 +657,38 @@ module Rigor
|
|
|
657
657
|
fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
|
|
658
658
|
return nil if fallback_receiver.nil?
|
|
659
659
|
|
|
660
|
+
# Preserve the ORIGINAL receiver type as the `self`
|
|
661
|
+
# substitution so `Kernel#dup: () -> self` and other
|
|
662
|
+
# `self`-returning methods route through Object's RBS
|
|
663
|
+
# while still returning the caller's type rather than
|
|
664
|
+
# `Object`. Without this, `base = self.dup` inside a
|
|
665
|
+
# `Bundler::URI::Generic` instance method types `base`
|
|
666
|
+
# as `Object` because `Bundler::URI::Generic` is not in
|
|
667
|
+
# RBS and the fallback's `self` resolves to Object.
|
|
660
668
|
RbsDispatch.try_dispatch(
|
|
661
669
|
receiver: fallback_receiver,
|
|
662
670
|
method_name: method_name,
|
|
663
671
|
args: arg_types,
|
|
664
672
|
environment: environment,
|
|
665
|
-
block_type: block_type
|
|
673
|
+
block_type: block_type,
|
|
674
|
+
self_type_override: receiver_type
|
|
666
675
|
)
|
|
667
676
|
end
|
|
668
677
|
|
|
669
678
|
def user_class_fallback_receiver(receiver_type, environment)
|
|
670
679
|
case receiver_type
|
|
671
680
|
when Type::Nominal
|
|
672
|
-
|
|
681
|
+
# Modules: even when RBS knows the module, an instance
|
|
682
|
+
# method on a mixin-only module (e.g. `PP::ObjectMixin`)
|
|
683
|
+
# observes Kernel / Object methods through every concrete
|
|
684
|
+
# includer's ancestor chain. Route through the
|
|
685
|
+
# `Nominal[Object]` fallback so `self.inspect` /
|
|
686
|
+
# `self.respond_to?` / `self.class` resolve cleanly when
|
|
687
|
+
# the module itself does not declare them.
|
|
688
|
+
known = Rigor::Reflection.rbs_class_known?(receiver_type.class_name, environment: environment)
|
|
689
|
+
return environment.nominal_for_name("Object") if !known || environment.rbs_module?(receiver_type.class_name)
|
|
673
690
|
|
|
674
|
-
|
|
691
|
+
nil
|
|
675
692
|
when Type::Singleton
|
|
676
693
|
return nil if Rigor::Reflection.rbs_class_known?(receiver_type.class_name, environment: environment)
|
|
677
694
|
|
|
@@ -950,7 +950,7 @@ module Rigor
|
|
|
950
950
|
end
|
|
951
951
|
|
|
952
952
|
def simple_dispatch_name?(name)
|
|
953
|
-
%i[nil? ! is_a? kind_of? instance_of? == != ===].include?(name)
|
|
953
|
+
%i[nil? ! is_a? kind_of? instance_of? == != === =~].include?(name)
|
|
954
954
|
end
|
|
955
955
|
|
|
956
956
|
def dispatch_call_simple(node, scope, name)
|
|
@@ -960,9 +960,111 @@ module Rigor
|
|
|
960
960
|
when :instance_of? then analyse_class_predicate(node, scope, exact: true)
|
|
961
961
|
when :==, :!= then analyse_equality_predicate(node, scope, equality: name)
|
|
962
962
|
when :=== then analyse_case_equality_predicate(node, scope)
|
|
963
|
+
when :=~ then analyse_regex_match_predicate(node, scope)
|
|
963
964
|
end
|
|
964
965
|
end
|
|
965
966
|
|
|
967
|
+
# Survey item (b): `/regex/ =~ str` and `str =~ /regex/`
|
|
968
|
+
# bind the regex match-data globals on each edge.
|
|
969
|
+
#
|
|
970
|
+
# - Truthy edge (`=~` returned an Integer position — the
|
|
971
|
+
# match succeeded): `$~` to `Nominal[MatchData]`; `$&`
|
|
972
|
+
# and `$1..$N` (where N is the number of capture groups
|
|
973
|
+
# in the regex source) to `Nominal[String]`. This is the
|
|
974
|
+
# same optimistic-narrowing shape the existing
|
|
975
|
+
# `analyse_match_write` uses for named captures inside
|
|
976
|
+
# `if /(?<x>...)/ =~ str` — optional groups in the
|
|
977
|
+
# regex source (`(\d+)?`) would bind `$N` to `nil` at
|
|
978
|
+
# runtime, but the floor here matches the common idiom
|
|
979
|
+
# (required captures) and lets `unless /(\d+)/ =~ s;
|
|
980
|
+
# raise; end; $1.to_i` resolve cleanly.
|
|
981
|
+
# - Falsey edge (`=~` returned nil — no match): `$~` and
|
|
982
|
+
# every numbered / back-reference global bound to
|
|
983
|
+
# `Constant<nil>`.
|
|
984
|
+
#
|
|
985
|
+
# Returns nil (no narrowing) when the receiver / argument
|
|
986
|
+
# pair does not include a `RegularExpressionNode` literal
|
|
987
|
+
# we can count.
|
|
988
|
+
def analyse_regex_match_predicate(node, scope)
|
|
989
|
+
return nil if node.arguments.nil?
|
|
990
|
+
return nil unless node.arguments.arguments.size == 1
|
|
991
|
+
|
|
992
|
+
regex_node = regex_match_literal(node.receiver, node.arguments.arguments.first)
|
|
993
|
+
return nil if regex_node.nil?
|
|
994
|
+
|
|
995
|
+
group_count = count_regex_capture_groups(regex_node.unescaped)
|
|
996
|
+
regex_match_predicate_scopes(scope, group_count)
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
def regex_match_literal(left, right)
|
|
1000
|
+
return left if left.is_a?(Prism::RegularExpressionNode)
|
|
1001
|
+
return right if right.is_a?(Prism::RegularExpressionNode)
|
|
1002
|
+
|
|
1003
|
+
nil
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
# Curated set of back-reference globals bound by every
|
|
1007
|
+
# `=~`. Numbered references (`$1..$N`) are handled
|
|
1008
|
+
# separately because N depends on the regex source.
|
|
1009
|
+
REGEX_MATCH_GLOBALS = %i[$~ $& $` $' $+].freeze
|
|
1010
|
+
private_constant :REGEX_MATCH_GLOBALS
|
|
1011
|
+
|
|
1012
|
+
def regex_match_predicate_scopes(scope, group_count)
|
|
1013
|
+
string_t = Type::Combinator.nominal_of("String")
|
|
1014
|
+
match_data_t = Type::Combinator.nominal_of("MatchData")
|
|
1015
|
+
nil_t = Type::Combinator.constant_of(nil)
|
|
1016
|
+
|
|
1017
|
+
truthy = scope
|
|
1018
|
+
falsey = scope
|
|
1019
|
+
truthy = truthy.with_global(:$~, match_data_t)
|
|
1020
|
+
falsey = falsey.with_global(:$~, nil_t)
|
|
1021
|
+
REGEX_MATCH_GLOBALS.each do |name|
|
|
1022
|
+
next if name == :$~
|
|
1023
|
+
|
|
1024
|
+
truthy = truthy.with_global(name, string_t)
|
|
1025
|
+
falsey = falsey.with_global(name, nil_t)
|
|
1026
|
+
end
|
|
1027
|
+
group_count.times do |i|
|
|
1028
|
+
name = :"$#{i + 1}"
|
|
1029
|
+
truthy = truthy.with_global(name, string_t)
|
|
1030
|
+
falsey = falsey.with_global(name, nil_t)
|
|
1031
|
+
end
|
|
1032
|
+
[truthy, falsey]
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
# Counts capture groups (numbered + named — both
|
|
1036
|
+
# contribute to `$1..$N`) in a regex source. Backslash
|
|
1037
|
+
# escapes are skipped; non-capturing `(?:...)`, lookahead
|
|
1038
|
+
# `(?=...)` / `(?!...)`, and lookbehind `(?<=...)` /
|
|
1039
|
+
# `(?<!...)` do NOT count. Named groups `(?<name>...)`
|
|
1040
|
+
# DO count. The walker is intentionally light — it does
|
|
1041
|
+
# not parse the regex AST, just scans char-by-char — so
|
|
1042
|
+
# exotic constructs that overlap the lookaround syntax
|
|
1043
|
+
# may miscount; the unsoundness is bounded (over- or
|
|
1044
|
+
# under-binding a few `$N` globals) and we already accept
|
|
1045
|
+
# the same shape of unsoundness for `analyse_match_write`.
|
|
1046
|
+
def count_regex_capture_groups(source)
|
|
1047
|
+
i = 0
|
|
1048
|
+
total = 0
|
|
1049
|
+
length = source.length
|
|
1050
|
+
while i < length
|
|
1051
|
+
c = source[i]
|
|
1052
|
+
if c == "\\"
|
|
1053
|
+
i += 2
|
|
1054
|
+
next
|
|
1055
|
+
end
|
|
1056
|
+
if c == "("
|
|
1057
|
+
if source[i + 1] == "?"
|
|
1058
|
+
total += 1 if source[i + 2] == "<" && source[i + 3] != "=" && source[i + 3] != "!"
|
|
1059
|
+
else
|
|
1060
|
+
total += 1
|
|
1061
|
+
end
|
|
1062
|
+
end
|
|
1063
|
+
i += 1
|
|
1064
|
+
end
|
|
1065
|
+
total
|
|
1066
|
+
end
|
|
1067
|
+
|
|
966
1068
|
def dispatch_call_numeric(node, scope, name)
|
|
967
1069
|
if COMPARISON_OPERATORS.include?(name)
|
|
968
1070
|
analyse_comparison_predicate(node, scope, comparator: name)
|
|
@@ -343,11 +343,26 @@ module Rigor
|
|
|
343
343
|
unless qualified_prefix.empty?
|
|
344
344
|
body_scope = body_scope.with_self_type(Type::Combinator.singleton_of(qualified_prefix.join("::")))
|
|
345
345
|
end
|
|
346
|
-
rvalue_type = body_scope.type_of(node.value)
|
|
346
|
+
rvalue_type = meta_new_constant_type(node, full) || body_scope.type_of(node.value)
|
|
347
347
|
existing = accumulator[full]
|
|
348
348
|
accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type
|
|
349
349
|
end
|
|
350
350
|
|
|
351
|
+
# Survey item (e): when the rvalue is a recognised
|
|
352
|
+
# `Module.new do ... end` / `Class.new do ... end` /
|
|
353
|
+
# `Struct.new(*sym) do ... end` / `Data.define(*sym) do
|
|
354
|
+
# ... end` form, type the named constant as
|
|
355
|
+
# `Singleton[<full>]` so the discovered-method table
|
|
356
|
+
# registered under `full` becomes reachable through
|
|
357
|
+
# singleton-side dispatch (`Const.[]=` etc.). Returns nil
|
|
358
|
+
# for non-meta-new rvalues so the caller falls back to the
|
|
359
|
+
# default `body_scope.type_of(node.value)` shape.
|
|
360
|
+
def meta_new_constant_type(node, full)
|
|
361
|
+
return nil unless meta_new_block_body(node)
|
|
362
|
+
|
|
363
|
+
Type::Combinator.singleton_of(full)
|
|
364
|
+
end
|
|
365
|
+
|
|
351
366
|
# Slice 7 phase 12 — in-source method discovery pre-pass.
|
|
352
367
|
# Walks every class/module body and records the methods
|
|
353
368
|
# introduced via `Prism::DefNode` (instance + singleton)
|
|
@@ -429,16 +444,22 @@ module Rigor
|
|
|
429
444
|
# v0.1.2 — when a `Const = Data.define(*sym) do ... end`
|
|
430
445
|
# / `Const = Struct.new(*sym) do ... end` constant write
|
|
431
446
|
# carries a block, the block body holds method overrides
|
|
432
|
-
# whose canonical class is `Const`.
|
|
433
|
-
#
|
|
434
|
-
#
|
|
435
|
-
#
|
|
436
|
-
#
|
|
447
|
+
# whose canonical class is `Const`. Survey item (e) extended
|
|
448
|
+
# the recognition to `Const = Module.new do ... end` and
|
|
449
|
+
# `Const = Class.new(?super) do ... end` — the
|
|
450
|
+
# ADR-16 Tier A "block-as-method" idiom at constant-write
|
|
451
|
+
# position. Returns the block body node (a
|
|
452
|
+
# `Prism::StatementsNode`) when the rvalue matches; nil
|
|
453
|
+
# otherwise. Used by `walk_methods` / `walk_def_nodes` to
|
|
454
|
+
# push `Const` onto the qualified prefix before recursing.
|
|
437
455
|
def meta_new_block_body(node)
|
|
438
456
|
return nil unless node.is_a?(Prism::ConstantWriteNode)
|
|
439
457
|
|
|
440
458
|
rvalue = node.value
|
|
441
|
-
return nil unless data_define_call?(rvalue) ||
|
|
459
|
+
return nil unless data_define_call?(rvalue) ||
|
|
460
|
+
struct_new_call?(rvalue) ||
|
|
461
|
+
module_new_call?(rvalue) ||
|
|
462
|
+
class_new_call?(rvalue)
|
|
442
463
|
|
|
443
464
|
rvalue.block&.body
|
|
444
465
|
end
|
|
@@ -946,6 +967,31 @@ module Rigor
|
|
|
946
967
|
positional.all?(Prism::SymbolNode)
|
|
947
968
|
end
|
|
948
969
|
|
|
970
|
+
# Recognises `Module.new` and `Module.new(&block)` /
|
|
971
|
+
# `Module.new do ... end` at constant-write rvalue
|
|
972
|
+
# position. The block body is the anonymous module's
|
|
973
|
+
# `module_eval` body; defs inside it bind methods on the
|
|
974
|
+
# named constant (`Const = Module.new do ...; def foo; ...; end; end`).
|
|
975
|
+
# Arguments are NOT inspected because `Module.new` accepts
|
|
976
|
+
# no positionals — Ruby raises ArgumentError if any are
|
|
977
|
+
# passed — so a malformed call falls through the walker
|
|
978
|
+
# without affecting analysis.
|
|
979
|
+
def module_new_call?(node)
|
|
980
|
+
meta_call_with_name?(node, :Module, :new)
|
|
981
|
+
end
|
|
982
|
+
|
|
983
|
+
# Recognises `Class.new`, `Class.new(super_class)`, and the
|
|
984
|
+
# block form `Class.new { ... }`. Like `module_new_call?`,
|
|
985
|
+
# the block body is walked as the anonymous class's body.
|
|
986
|
+
# The optional `super_class` positional is accepted but does
|
|
987
|
+
# NOT route through `ancestor` discovery in this slice — the
|
|
988
|
+
# synthesised class still answers method lookups via its
|
|
989
|
+
# own body's defs, mirroring how `Struct.new` / `Data.define`
|
|
990
|
+
# are handled.
|
|
991
|
+
def class_new_call?(node)
|
|
992
|
+
meta_call_with_name?(node, :Class, :new)
|
|
993
|
+
end
|
|
994
|
+
|
|
949
995
|
def meta_call_with_name?(node, receiver_name, method_name)
|
|
950
996
|
return false unless node.is_a?(Prism::CallNode)
|
|
951
997
|
return false unless node.name == method_name
|
|
@@ -497,13 +497,25 @@ module Rigor
|
|
|
497
497
|
def eval_begin(node)
|
|
498
498
|
primary_type, primary_scope = eval_begin_primary(node)
|
|
499
499
|
rescue_chain = collect_rescue_chain_results(node.rescue_clause, scope)
|
|
500
|
-
|
|
501
|
-
|
|
500
|
+
# Rescue arms whose body unconditionally exits (`return`,
|
|
501
|
+
# `next`, `break`, `raise`, `throw`, `exit`, `abort`,
|
|
502
|
+
# `fail`) contribute neither a type fragment NOR a scope
|
|
503
|
+
# to the post-begin flow — control left the `begin` via
|
|
504
|
+
# that arm. Mirrors the `eval_if` / `eval_unless` /
|
|
505
|
+
# `eval_and_or` early-return narrowing. Without this
|
|
506
|
+
# filter, a `rescue ... return` on a local bound only in
|
|
507
|
+
# the primary body nil-injects that local across the
|
|
508
|
+
# join, defeating the rescue arm's whole point of guaranteeing
|
|
509
|
+
# the primary local is in scope for downstream statements.
|
|
510
|
+
live_rescues = rescue_chain.reject { |_pair, arm_node| branch_unconditionally_exits?(arm_node.statements) }
|
|
511
|
+
.map(&:first)
|
|
512
|
+
|
|
513
|
+
if live_rescues.empty?
|
|
502
514
|
exit_type = primary_type
|
|
503
515
|
exit_scope = primary_scope
|
|
504
516
|
else
|
|
505
|
-
exit_type = Type::Combinator.union(primary_type, *
|
|
506
|
-
exit_scope = reduce_scopes_with_nil_injection([primary_scope, *
|
|
517
|
+
exit_type = Type::Combinator.union(primary_type, *live_rescues.map(&:first))
|
|
518
|
+
exit_scope = reduce_scopes_with_nil_injection([primary_scope, *live_rescues.map(&:last)])
|
|
507
519
|
end
|
|
508
520
|
|
|
509
521
|
if node.ensure_clause
|
|
@@ -540,7 +552,7 @@ module Rigor
|
|
|
540
552
|
current = rescue_node
|
|
541
553
|
while current
|
|
542
554
|
rescue_scope = bind_rescue_reference(current, entry_scope)
|
|
543
|
-
results << eval_branch_or_nil(current.statements, rescue_scope)
|
|
555
|
+
results << [eval_branch_or_nil(current.statements, rescue_scope), current]
|
|
544
556
|
current = current.subsequent
|
|
545
557
|
end
|
|
546
558
|
results
|
|
@@ -696,10 +708,35 @@ module Rigor
|
|
|
696
708
|
# edge-aware: `a && b` can only produce the falsey fragment of
|
|
697
709
|
# `a` when the RHS is skipped, while `a || b` can only produce
|
|
698
710
|
# the truthy fragment of `a` when the RHS is skipped.
|
|
711
|
+
#
|
|
712
|
+
# When the RHS unconditionally exits (`raise` / `return` /
|
|
713
|
+
# `throw` / `exit` / `abort` / `fail` / `next` / `break`), the
|
|
714
|
+
# post-OR / post-AND scope is the LHS-skipped edge alone:
|
|
715
|
+
# `a or raise` only survives when `a` was truthy, so subsequent
|
|
716
|
+
# statements observe `a` narrowed to its truthy fragment; the
|
|
717
|
+
# symmetric `a and raise` survives only when `a` was falsey.
|
|
718
|
+
# Same shape as the `eval_if` / `eval_unless` early-return
|
|
719
|
+
# narrowing.
|
|
699
720
|
def eval_and_or(node)
|
|
700
721
|
left_type, left_scope = sub_eval(node.left, scope)
|
|
701
722
|
truthy_left, falsey_left = Narrowing.predicate_scopes(node.left, left_scope)
|
|
702
723
|
rhs_entry = node.is_a?(Prism::AndNode) ? truthy_left : falsey_left
|
|
724
|
+
if branch_unconditionally_exits?(node.right)
|
|
725
|
+
# Walk the RHS for side-effects (on_enter callbacks,
|
|
726
|
+
# diagnostic dispatch on the raise / return expression
|
|
727
|
+
# itself) but discard its scope: control never reaches
|
|
728
|
+
# any statement after `a or raise` via that edge.
|
|
729
|
+
sub_eval(node.right, rhs_entry)
|
|
730
|
+
surviving_type =
|
|
731
|
+
if node.is_a?(Prism::AndNode)
|
|
732
|
+
Narrowing.narrow_falsey(left_type)
|
|
733
|
+
else
|
|
734
|
+
Narrowing.narrow_truthy(left_type)
|
|
735
|
+
end
|
|
736
|
+
surviving_scope = node.is_a?(Prism::AndNode) ? falsey_left : truthy_left
|
|
737
|
+
return [surviving_type, surviving_scope]
|
|
738
|
+
end
|
|
739
|
+
|
|
703
740
|
right_type, right_scope = sub_eval(node.right, rhs_entry)
|
|
704
741
|
skipped_type =
|
|
705
742
|
if node.is_a?(Prism::AndNode)
|
|
@@ -1106,7 +1143,16 @@ module Rigor
|
|
|
1106
1143
|
# narrowing logic via `Narrowing.narrow_for_fact` so the
|
|
1107
1144
|
# predicate / assert / plugin paths all converge on the
|
|
1108
1145
|
# same hierarchy-aware narrowing rules.
|
|
1146
|
+
#
|
|
1147
|
+
# v0.1.8 Pillar 2 Slice 1 added the `:local` target_kind
|
|
1148
|
+
# branch so plugins recognising bespoke call shapes
|
|
1149
|
+
# (`expect(x).to be_a(T)`) can directly narrow a named
|
|
1150
|
+
# local in the surrounding scope, bypassing the
|
|
1151
|
+
# parameter-name lookup that requires an authoritative RBS
|
|
1152
|
+
# sig on the called method (which RSpec matchers lack).
|
|
1109
1153
|
def apply_post_return_fact(fact, call_node, current_scope, method_def)
|
|
1154
|
+
return apply_local_post_return_fact(fact, current_scope) if fact.target_kind == :local
|
|
1155
|
+
|
|
1110
1156
|
target_node = fact_target_node(fact, call_node, method_def)
|
|
1111
1157
|
return apply_self_post_return_fact(fact, target_node, current_scope) if fact.target_kind == :self
|
|
1112
1158
|
return current_scope unless target_node.is_a?(Prism::LocalVariableReadNode)
|
|
@@ -1119,6 +1165,21 @@ module Rigor
|
|
|
1119
1165
|
current_scope.with_local(local_name, narrowed)
|
|
1120
1166
|
end
|
|
1121
1167
|
|
|
1168
|
+
# v0.1.8 Pillar 2 Slice 1 — narrows the named local directly
|
|
1169
|
+
# without consulting the call node's argument list. The fact's
|
|
1170
|
+
# `target_name` is the local-variable name as written in
|
|
1171
|
+
# source. Silently no-ops when the local is unbound in the
|
|
1172
|
+
# current scope (the plugin's named local may have already
|
|
1173
|
+
# gone out of scope when the contribution fires).
|
|
1174
|
+
def apply_local_post_return_fact(fact, current_scope)
|
|
1175
|
+
local_name = fact.target_name
|
|
1176
|
+
current_type = current_scope.local(local_name)
|
|
1177
|
+
return current_scope if current_type.nil?
|
|
1178
|
+
|
|
1179
|
+
narrowed = Narrowing.narrow_for_fact(current_type, fact, current_scope.environment)
|
|
1180
|
+
current_scope.with_local(local_name, narrowed)
|
|
1181
|
+
end
|
|
1182
|
+
|
|
1122
1183
|
# v0.1.1 Track 1 slice 3 — `assert self is T` post-return
|
|
1123
1184
|
# narrowing for the four supported receiver shapes (mirrors
|
|
1124
1185
|
# `Narrowing#apply_self_fact`).
|
|
@@ -73,8 +73,8 @@ module Rigor
|
|
|
73
73
|
# This file ships the value class only. Slice 2b wires the
|
|
74
74
|
# pre-pass that scans Tier C call sites + the
|
|
75
75
|
# `SyntheticMethodIndex` the dispatcher consults; slice 2c
|
|
76
|
-
# authors `
|
|
77
|
-
# `
|
|
76
|
+
# authors `plugins/rigor-dry-struct/` and
|
|
77
|
+
# `plugins/rigor-dry-types/` as the worked consumers.
|
|
78
78
|
class HeredocTemplate
|
|
79
79
|
NAME_PLACEHOLDER = "\#{name}"
|
|
80
80
|
|
|
@@ -83,7 +83,7 @@ module Rigor
|
|
|
83
83
|
# This file ships the value class only. Slice 3b wires the
|
|
84
84
|
# scanner that walks Tier B call sites + the per-method
|
|
85
85
|
# explosion via `SyntheticMethodIndex`; slice 3c authors
|
|
86
|
-
# `
|
|
86
|
+
# `plugins/rigor-devise/` model side as the worked consumer.
|
|
87
87
|
class TraitRegistry
|
|
88
88
|
REST_POSITION = :rest
|
|
89
89
|
|
data/lib/rigor/version.rb
CHANGED
data/sig/rigor/environment.rbs
CHANGED
|
@@ -25,6 +25,7 @@ module Rigor
|
|
|
25
25
|
def singleton_for_name: (String | Symbol name) -> Type::Singleton?
|
|
26
26
|
def constant_for_name: (String | Symbol name) -> Type::t?
|
|
27
27
|
def class_known?: (String | Symbol name) -> bool
|
|
28
|
+
def rbs_module?: (String | Symbol name) -> bool
|
|
28
29
|
def class_ordering: (String | Symbol lhs, String | Symbol rhs) -> ordering
|
|
29
30
|
def reflection: () -> untyped?
|
|
30
31
|
|
|
@@ -52,6 +53,7 @@ module Rigor
|
|
|
52
53
|
|
|
53
54
|
def initialize: (?libraries: Array[String], ?signature_paths: Array[String | _ToPath], ?cache_store: untyped?) -> void
|
|
54
55
|
def class_known?: (String | Symbol name) -> bool
|
|
56
|
+
def rbs_module?: (String | Symbol name) -> bool
|
|
55
57
|
def instance_definition: (String | Symbol class_name) -> untyped?
|
|
56
58
|
def instance_method: (class_name: String | Symbol, method_name: String | Symbol) -> untyped?
|
|
57
59
|
def uncached_instance_definition: (String | Symbol class_name) -> untyped?
|
data/sig/rigor.rbs
CHANGED
|
@@ -9,6 +9,7 @@ module Rigor
|
|
|
9
9
|
attr_reader paths: Array[String]
|
|
10
10
|
attr_reader plugins: Array[String]
|
|
11
11
|
attr_reader cache_path: String
|
|
12
|
+
attr_reader baseline_path: String?
|
|
12
13
|
|
|
13
14
|
def self.load: (?String path) -> Configuration
|
|
14
15
|
def initialize: (?Hash[String, untyped] data) -> void
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rigortype
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rigor contributors
|
|
@@ -223,6 +223,7 @@ files:
|
|
|
223
223
|
- data/builtins/ruby_core/time.yml
|
|
224
224
|
- exe/rigor
|
|
225
225
|
- lib/rigor.rb
|
|
226
|
+
- lib/rigor/analysis/baseline.rb
|
|
226
227
|
- lib/rigor/analysis/buffer_binding.rb
|
|
227
228
|
- lib/rigor/analysis/check_rules.rb
|
|
228
229
|
- lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb
|
|
@@ -260,6 +261,7 @@ files:
|
|
|
260
261
|
- lib/rigor/cache/rbs_known_class_names.rb
|
|
261
262
|
- lib/rigor/cache/store.rb
|
|
262
263
|
- lib/rigor/cli.rb
|
|
264
|
+
- lib/rigor/cli/baseline_command.rb
|
|
263
265
|
- lib/rigor/cli/diff_command.rb
|
|
264
266
|
- lib/rigor/cli/explain_command.rb
|
|
265
267
|
- lib/rigor/cli/lsp_command.rb
|