rigortype 0.1.6 → 0.1.8
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 +60 -3
- 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/builtins/static_return_refinements.rb +23 -1
- data/lib/rigor/cli/baseline_command.rb +377 -0
- data/lib/rigor/cli/triage_command.rb +83 -0
- data/lib/rigor/cli/triage_renderer.rb +77 -0
- data/lib/rigor/cli.rb +78 -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 +152 -14
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +57 -11
- data/lib/rigor/inference/method_dispatcher.rb +50 -5
- data/lib/rigor/inference/narrowing.rb +103 -1
- data/lib/rigor/inference/scope_indexer.rb +209 -13
- data/lib/rigor/inference/statement_evaluator.rb +91 -10
- data/lib/rigor/plugin/macro/heredoc_template.rb +2 -2
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- 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/version.rb +1 -1
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- data/sig/rigor.rbs +1 -0
- metadata +8 -1
data/lib/rigor/configuration.rb
CHANGED
|
@@ -59,6 +59,14 @@ module Rigor
|
|
|
59
59
|
# the dispatcher tier consuming the registry lands in
|
|
60
60
|
# slice 2.
|
|
61
61
|
"pre_eval" => [],
|
|
62
|
+
# ADR-22 — baseline file path. nil (default) means no
|
|
63
|
+
# baseline is loaded; the `false` literal is treated as
|
|
64
|
+
# the explicit-disable form for `.rigor.yml`-side override
|
|
65
|
+
# of an upstream `.rigor.dist.yml` `baseline:` declaration.
|
|
66
|
+
# The presence of `.rigor-baseline.yml` on disk alone does
|
|
67
|
+
# NOT activate filtering — the path must be named here
|
|
68
|
+
# (WD2 (b) of ADR-22).
|
|
69
|
+
"baseline" => nil,
|
|
62
70
|
"fold_platform_specific_paths" => false,
|
|
63
71
|
"cache" => {
|
|
64
72
|
"path" => ".rigor/cache"
|
|
@@ -166,7 +174,7 @@ module Rigor
|
|
|
166
174
|
:dependencies, :parallel_workers,
|
|
167
175
|
:bundler_bundle_path, :bundler_auto_detect, :bundler_lockfile,
|
|
168
176
|
:rbs_collection_lockfile, :rbs_collection_auto_detect,
|
|
169
|
-
:pre_eval
|
|
177
|
+
:pre_eval, :baseline_path
|
|
170
178
|
|
|
171
179
|
# Loads a configuration file.
|
|
172
180
|
#
|
|
@@ -321,6 +329,7 @@ module Rigor
|
|
|
321
329
|
@pre_eval = expand_pre_eval_entries(
|
|
322
330
|
Array(data.fetch("pre_eval", DEFAULTS.fetch("pre_eval"))).map(&:to_s)
|
|
323
331
|
)
|
|
332
|
+
@baseline_path = coerce_baseline_path(data.fetch("baseline", DEFAULTS.fetch("baseline")))
|
|
324
333
|
@fold_platform_specific_paths = data.fetch(
|
|
325
334
|
"fold_platform_specific_paths", DEFAULTS.fetch("fold_platform_specific_paths")
|
|
326
335
|
) == true
|
|
@@ -488,6 +497,17 @@ module Rigor
|
|
|
488
497
|
raise ArgumentError, "parallel.workers must be a non-negative Integer, got #{value.inspect} (#{e.message})"
|
|
489
498
|
end
|
|
490
499
|
|
|
500
|
+
# ADR-22 WD2 (b) — `baseline: <path>` activates the file;
|
|
501
|
+
# `baseline: false` is the explicit-disable form (useful in
|
|
502
|
+
# `.rigor.yml` to override an upstream `.rigor.dist.yml`
|
|
503
|
+
# that names a baseline). `nil` (default / absent key) is
|
|
504
|
+
# also "no baseline".
|
|
505
|
+
def coerce_baseline_path(value)
|
|
506
|
+
return nil if value.nil? || value == false
|
|
507
|
+
|
|
508
|
+
value.to_s
|
|
509
|
+
end
|
|
510
|
+
|
|
491
511
|
def coerce_network_policy(value)
|
|
492
512
|
sym = value.to_sym
|
|
493
513
|
unless VALID_NETWORK_POLICIES.include?(sym)
|
|
@@ -42,7 +42,7 @@ module Rigor
|
|
|
42
42
|
# enough that hard-coding is acceptable; a directory walk
|
|
43
43
|
# at every call would add stat-cost to no benefit.)
|
|
44
44
|
VENDORED_GEM_NAMES = Set[
|
|
45
|
-
"bcrypt", "idn-ruby", "mysql2", "nokogiri", "pg", "prism", "redis"
|
|
45
|
+
"bcrypt", "bundler", "cgi", "did_you_mean", "idn-ruby", "mysql2", "nokogiri", "pg", "prism", "redis", "rubygems"
|
|
46
46
|
].freeze
|
|
47
47
|
|
|
48
48
|
# @param locked_gems [Hash{String => LockfileResolver::LockedGem}]
|
|
@@ -160,6 +160,28 @@ module Rigor
|
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
162
|
|
|
163
|
+
# Returns true when the named RBS declaration is a Module
|
|
164
|
+
# (`RBS::AST::Declarations::Module`) rather than a Class. The
|
|
165
|
+
# `user_class_fallback_receiver` tier consults this to route
|
|
166
|
+
# `Nominal[M].some_kernel_method` (where M is a module mixin
|
|
167
|
+
# like `PP::ObjectMixin`) through the `Nominal[Object]`
|
|
168
|
+
# fallback, because every concrete includer of M sees Kernel
|
|
169
|
+
# / Object instance methods as part of its own ancestor chain.
|
|
170
|
+
#
|
|
171
|
+
# Returns false for classes, for unknown names, and when the
|
|
172
|
+
# RBS environment failed to build (fail-soft).
|
|
173
|
+
def rbs_module?(name)
|
|
174
|
+
return false if env.nil?
|
|
175
|
+
|
|
176
|
+
rbs_name = parse_type_name(name)
|
|
177
|
+
return false if rbs_name.nil?
|
|
178
|
+
|
|
179
|
+
entry = env.class_decls[rbs_name]
|
|
180
|
+
entry.is_a?(::RBS::Environment::ModuleEntry)
|
|
181
|
+
rescue ::RBS::BaseError
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
|
|
163
185
|
# Yields every known class / module / alias name (top-level
|
|
164
186
|
# prefixed) currently loaded into the environment. The cache
|
|
165
187
|
# producer that materialises the known-name set uses this so
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -354,6 +354,19 @@ module Rigor
|
|
|
354
354
|
@rbs_loader&.reflection
|
|
355
355
|
end
|
|
356
356
|
|
|
357
|
+
# Returns true when the RBS environment carries the named
|
|
358
|
+
# declaration as a Module (not a Class). Used by the
|
|
359
|
+
# `user_class_fallback_receiver` tier to detect a module-mixin
|
|
360
|
+
# receiver (e.g. `PP::ObjectMixin`) so the dispatcher can route
|
|
361
|
+
# unresolved method calls through the `Nominal[Object]`
|
|
362
|
+
# fallback — every concrete includer of M honours Kernel /
|
|
363
|
+
# Object instance methods through its own ancestor chain.
|
|
364
|
+
def rbs_module?(name)
|
|
365
|
+
return false unless rbs_loader
|
|
366
|
+
|
|
367
|
+
rbs_loader.rbs_module?(name)
|
|
368
|
+
end
|
|
369
|
+
|
|
357
370
|
# Compares two class/module names using analyzer-owned class data.
|
|
358
371
|
# Returns `:equal`, `:subclass`, `:superclass`, `:disjoint`, or
|
|
359
372
|
# `:unknown`. The static registry handles built-ins cheaply; the RBS
|
|
@@ -31,14 +31,20 @@ module Rigor
|
|
|
31
31
|
#
|
|
32
32
|
# ## Field set
|
|
33
33
|
#
|
|
34
|
-
# - `target_kind`: `:parameter` (call-site argument)
|
|
35
|
-
# `:
|
|
36
|
-
#
|
|
37
|
-
#
|
|
34
|
+
# - `target_kind`: `:parameter` (call-site argument), `:self`
|
|
35
|
+
# (receiver), or `:local` (a named local in the surrounding
|
|
36
|
+
# scope). v0.1.8 Pillar 2 Slice 1 added `:local` so plugins
|
|
37
|
+
# recognising bespoke call shapes (`expect(x).to be_a(T)`)
|
|
38
|
+
# can narrow a specific scope-bound local without routing
|
|
39
|
+
# through the parameter-name lookup that requires an
|
|
40
|
+
# authoritative RBS sig on the called method. Future slices
|
|
41
|
+
# may extend further (`:ivar`, `:result`). The merger is
|
|
42
|
+
# agnostic to the concrete kinds and only requires equality.
|
|
38
43
|
# - `target_name`: a `Symbol`. For `:parameter` it's the
|
|
39
44
|
# declared parameter name. For `:self` it is the literal
|
|
40
45
|
# `:self` symbol so the field stays non-nil and the merge
|
|
41
|
-
# key is well-defined.
|
|
46
|
+
# key is well-defined. For `:local` it's the local-variable
|
|
47
|
+
# name (e.g. `:x` for `expect(x).to be_a(T)`).
|
|
42
48
|
# - `type`: a `Rigor::Type::*` (Nominal, Refined,
|
|
43
49
|
# IntegerRange, Difference, …) the fact narrows the
|
|
44
50
|
# target toward (when `negative` is false) or away from
|
|
@@ -53,7 +59,7 @@ module Rigor
|
|
|
53
59
|
# value {Element#target} keys on, so two facts that narrow
|
|
54
60
|
# the same parameter from different contribution sources
|
|
55
61
|
# land in the same merge bucket.
|
|
56
|
-
FACT_VALID_TARGET_KINDS = %i[parameter self].freeze
|
|
62
|
+
FACT_VALID_TARGET_KINDS = %i[parameter self local].freeze
|
|
57
63
|
|
|
58
64
|
class Fact < Data.define(:target_kind, :target_name, :type, :negative)
|
|
59
65
|
def initialize(target_kind:, target_name:, type:, negative: false)
|
|
@@ -72,10 +78,14 @@ module Rigor
|
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
# Composite target identifier the merger keys on. `:self`
|
|
75
|
-
# for self-targeted facts; otherwise `[
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# bucket.
|
|
81
|
+
# for self-targeted facts; otherwise `[kind, name]` so two
|
|
82
|
+
# contributions that narrow the same `(kind, name)` pair —
|
|
83
|
+
# regardless of source family — land in the same merge
|
|
84
|
+
# bucket. `:local` and `:parameter` facts that name the
|
|
85
|
+
# same symbol stay in separate buckets, which is the
|
|
86
|
+
# correct semantics: a `:local` fact narrows the surrounding
|
|
87
|
+
# scope's named local, a `:parameter` fact narrows the
|
|
88
|
+
# call-site argument matching the parameter declaration.
|
|
79
89
|
def target
|
|
80
90
|
target_kind == :self ? :self : [target_kind, target_name]
|
|
81
91
|
end
|
|
@@ -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)
|
|
@@ -1010,11 +1025,15 @@ module Rigor
|
|
|
1010
1025
|
local_def = node.receiver.nil? ? scope.top_level_def_for(node.name) : nil
|
|
1011
1026
|
if local_def
|
|
1012
1027
|
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
1013
|
-
return local_inference if local_inference
|
|
1014
|
-
|
|
1015
|
-
# The local def matches by name but the
|
|
1016
|
-
#
|
|
1017
|
-
#
|
|
1028
|
+
return local_inference if local_inference && adoptable_self_call_result?(local_inference)
|
|
1029
|
+
|
|
1030
|
+
# The local def matches by name but the inference
|
|
1031
|
+
# was disqualified — either the parameter shape is
|
|
1032
|
+
# too complex for the first-iteration binder
|
|
1033
|
+
# (kwargs / optionals / rest), or ADR-24 slice 1's
|
|
1034
|
+
# conservative gate declined the resolved return
|
|
1035
|
+
# type inside a class body (see
|
|
1036
|
+
# `adoptable_self_call_result?`).
|
|
1018
1037
|
# Returning `Dynamic[Top]` is the safest answer:
|
|
1019
1038
|
# we know RBS dispatch would be wrong (the
|
|
1020
1039
|
# method is user-defined and shadows whatever
|
|
@@ -1054,7 +1073,11 @@ module Rigor
|
|
|
1054
1073
|
# the body with the call's argument types bound and
|
|
1055
1074
|
# return the body's last-expression type.
|
|
1056
1075
|
user_inference = try_user_method_inference(receiver, node, arg_types)
|
|
1057
|
-
|
|
1076
|
+
if user_inference
|
|
1077
|
+
return user_inference if adoptable_self_call_result?(user_inference)
|
|
1078
|
+
|
|
1079
|
+
return dynamic_top
|
|
1080
|
+
end
|
|
1058
1081
|
|
|
1059
1082
|
# Dynamic-origin propagation: when the receiver is Dynamic[T] and
|
|
1060
1083
|
# no positive rule resolves the call, the result inherits the
|
|
@@ -1097,10 +1120,39 @@ module Rigor
|
|
|
1097
1120
|
nil
|
|
1098
1121
|
end
|
|
1099
1122
|
|
|
1123
|
+
# ADR-24 slice 1 — implicit-self method-call resolution.
|
|
1124
|
+
# `discovered_def_nodes` is now carried into method /
|
|
1125
|
+
# class body scopes (see `StatementEvaluator#build_fresh_body_scope`),
|
|
1126
|
+
# so a call written with no explicit receiver inside a
|
|
1127
|
+
# method body resolves against the enclosing class's own
|
|
1128
|
+
# definitions and the file's top-level defs. Before
|
|
1129
|
+
# slice 1 every such call typed `Dynamic[top]`.
|
|
1130
|
+
#
|
|
1131
|
+
# The adoption of the resolved return type is gated:
|
|
1132
|
+
#
|
|
1133
|
+
# - At top-level / inside a DSL block (`scope.self_type`
|
|
1134
|
+
# is nil) the result is adopted unchanged — this is
|
|
1135
|
+
# the pre-slice-1 surface (the v0.0.3 A local-`def`
|
|
1136
|
+
# shortcut) and MUST keep working.
|
|
1137
|
+
# - Inside a class body / method body (`self_type` set)
|
|
1138
|
+
# the result is adopted ONLY when it is `Bot`. A `Bot`
|
|
1139
|
+
# return is an always-diverging guard helper; adopting
|
|
1140
|
+
# it can only ever enable correct terminating-branch
|
|
1141
|
+
# narrowing, never a new `undefined-method` /
|
|
1142
|
+
# argument-type false positive. A non-`Bot` resolved
|
|
1143
|
+
# return is kept as `Dynamic[top]` (WD3) — adopting
|
|
1144
|
+
# precise non-`Bot` returns project-wide awaits the
|
|
1145
|
+
# callee-return-inference precision a later slice
|
|
1146
|
+
# brings (measured: unconditional adoption regressed
|
|
1147
|
+
# `rigor check lib` by 16 diagnostics).
|
|
1148
|
+
def adoptable_self_call_result?(type)
|
|
1149
|
+
scope.self_type.nil? || type.is_a?(Type::Bot)
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1100
1152
|
def try_user_method_inference(receiver, call_node, arg_types)
|
|
1101
1153
|
return nil unless receiver.is_a?(Type::Nominal)
|
|
1102
1154
|
|
|
1103
|
-
def_node =
|
|
1155
|
+
def_node = resolve_user_def_through_ancestors(receiver.class_name, call_node.name)
|
|
1104
1156
|
return nil if def_node.nil?
|
|
1105
1157
|
|
|
1106
1158
|
infer_user_method_return(def_node, receiver, arg_types)
|
|
@@ -1108,6 +1160,81 @@ module Rigor
|
|
|
1108
1160
|
nil
|
|
1109
1161
|
end
|
|
1110
1162
|
|
|
1163
|
+
# ADR-24 slice 2 — resolves `method_name` against
|
|
1164
|
+
# `class_name`'s own `def`s, then walks the user-class
|
|
1165
|
+
# ancestor chain: included / prepended modules (transitive)
|
|
1166
|
+
# and the superclass chain. RBS-known ancestors are NOT
|
|
1167
|
+
# walked here — the `MethodDispatcher` RBS tier runs before
|
|
1168
|
+
# `try_user_method_inference` and already covers them; an
|
|
1169
|
+
# ancestor name that resolves to no project-discovered
|
|
1170
|
+
# class/module ends that branch. Cross-file: the chain is
|
|
1171
|
+
# followed through `Scope#discovered_superclasses` /
|
|
1172
|
+
# `#discovered_includes` / `#discovered_def_nodes`, which
|
|
1173
|
+
# the runner seeds from the project-wide pre-pass. The walk
|
|
1174
|
+
# is breadth-first, cycle-guarded, and node-count-capped.
|
|
1175
|
+
ANCESTOR_WALK_LIMIT = 100
|
|
1176
|
+
private_constant :ANCESTOR_WALK_LIMIT
|
|
1177
|
+
|
|
1178
|
+
def resolve_user_def_through_ancestors(class_name, method_name)
|
|
1179
|
+
queue = [class_name.to_s]
|
|
1180
|
+
seen = {}
|
|
1181
|
+
visited = 0
|
|
1182
|
+
until queue.empty?
|
|
1183
|
+
current = queue.shift
|
|
1184
|
+
next if current.nil? || seen[current]
|
|
1185
|
+
|
|
1186
|
+
seen[current] = true
|
|
1187
|
+
visited += 1
|
|
1188
|
+
return nil if visited > ANCESTOR_WALK_LIMIT
|
|
1189
|
+
|
|
1190
|
+
found = scope.user_def_for(current, method_name)
|
|
1191
|
+
return found if found
|
|
1192
|
+
|
|
1193
|
+
enqueue_ancestors(current, queue)
|
|
1194
|
+
end
|
|
1195
|
+
nil
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
# Pushes `current`'s direct ancestors onto the BFS queue:
|
|
1199
|
+
# included / prepended modules first (Ruby places mixins
|
|
1200
|
+
# nearer than the superclass), then the superclass. Each
|
|
1201
|
+
# as-written name is resolved against `current`'s lexical
|
|
1202
|
+
# nesting; names that resolve to no project class/module
|
|
1203
|
+
# are dropped (RBS-known / third-party ancestors).
|
|
1204
|
+
def enqueue_ancestors(current, queue)
|
|
1205
|
+
scope.includes_of(current).each do |raw|
|
|
1206
|
+
resolved = resolve_ancestor_class_name(current, raw)
|
|
1207
|
+
queue.push(resolved) if resolved
|
|
1208
|
+
end
|
|
1209
|
+
raw_super = scope.superclass_of(current)
|
|
1210
|
+
return if raw_super.nil?
|
|
1211
|
+
|
|
1212
|
+
resolved_super = resolve_ancestor_class_name(current, raw_super)
|
|
1213
|
+
queue.push(resolved_super) if resolved_super
|
|
1214
|
+
end
|
|
1215
|
+
|
|
1216
|
+
# Resolves a superclass name AS WRITTEN (`"Base"`, or a
|
|
1217
|
+
# qualified `"A::B"`) to a project-discovered class,
|
|
1218
|
+
# following Ruby's `Module.nesting` constant lookup: try
|
|
1219
|
+
# the raw name under each enclosing namespace of the
|
|
1220
|
+
# subclass, innermost first, then bare. Returns nil when
|
|
1221
|
+
# no candidate names a discovered user class (e.g. the
|
|
1222
|
+
# superclass is an RBS-known or third-party class).
|
|
1223
|
+
def resolve_ancestor_class_name(subclass_qualified, raw_superclass)
|
|
1224
|
+
segments = subclass_qualified.split("::")
|
|
1225
|
+
(segments.length - 1).downto(0) do |i|
|
|
1226
|
+
candidate = (segments[0, i] + [raw_superclass]).join("::")
|
|
1227
|
+
return candidate if known_user_class?(candidate)
|
|
1228
|
+
end
|
|
1229
|
+
nil
|
|
1230
|
+
end
|
|
1231
|
+
|
|
1232
|
+
def known_user_class?(name)
|
|
1233
|
+
scope.discovered_superclasses.key?(name) ||
|
|
1234
|
+
scope.discovered_def_nodes.key?(name) ||
|
|
1235
|
+
scope.discovered_includes.key?(name)
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1111
1238
|
INFERENCE_GUARD_KEY = :__rigor_user_method_inference_stack__
|
|
1112
1239
|
private_constant :INFERENCE_GUARD_KEY
|
|
1113
1240
|
|
|
@@ -1117,11 +1244,20 @@ module Rigor
|
|
|
1117
1244
|
body_scope = build_user_method_body_scope(def_node, receiver, arg_types)
|
|
1118
1245
|
return nil if body_scope.nil?
|
|
1119
1246
|
|
|
1120
|
-
# Recursion-guard signature.
|
|
1121
|
-
#
|
|
1122
|
-
#
|
|
1123
|
-
#
|
|
1124
|
-
|
|
1247
|
+
# Recursion-guard signature. Keyed on `(receiver,
|
|
1248
|
+
# method)` only — NOT the argument types. ADR-24 WD5:
|
|
1249
|
+
# a method whose summary is still being computed
|
|
1250
|
+
# resolves to `Dynamic[top]` for that cycle. Keying on
|
|
1251
|
+
# arg types would let mutual recursion through a
|
|
1252
|
+
# `module_function` module (`Acceptance#accepts` →
|
|
1253
|
+
# `accepts_one` → `accepts_dynamic` → `accepts`)
|
|
1254
|
+
# recurse unboundedly whenever the carried argument
|
|
1255
|
+
# types differ at each level — observed as a
|
|
1256
|
+
# `SystemStackError` once implicit-self calls began
|
|
1257
|
+
# resolving during the main walk. `describe(:short)`
|
|
1258
|
+
# keeps non-Nominal receivers (the implicit `Object`
|
|
1259
|
+
# carrier for top-level / DSL-block defs) printable.
|
|
1260
|
+
signature = [receiver.describe(:short), def_node.name]
|
|
1125
1261
|
stack = (Thread.current[INFERENCE_GUARD_KEY] ||= [])
|
|
1126
1262
|
return Type::Combinator.untyped if stack.include?(signature)
|
|
1127
1263
|
|
|
@@ -1159,6 +1295,8 @@ module Rigor
|
|
|
1159
1295
|
.with_program_globals(scope.program_globals)
|
|
1160
1296
|
.with_discovered_methods(scope.discovered_methods)
|
|
1161
1297
|
.with_discovered_def_nodes(scope.discovered_def_nodes)
|
|
1298
|
+
.with_discovered_superclasses(scope.discovered_superclasses)
|
|
1299
|
+
.with_discovered_includes(scope.discovered_includes)
|
|
1162
1300
|
.with_self_type(receiver)
|
|
1163
1301
|
|
|
1164
1302
|
required.each_with_index do |param, index|
|
|
@@ -74,10 +74,28 @@ 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).
|
|
88
|
+
# @param public_only [Boolean] when true, a method whose RBS
|
|
89
|
+
# accessibility is `:private` does not resolve (the call
|
|
90
|
+
# yields `nil`, i.e. "no rule"). Set by the explicit-
|
|
91
|
+
# non-`self`-receiver user-class fallback so a call like
|
|
92
|
+
# `Favourite.select(...)` does not adopt the private
|
|
93
|
+
# `Kernel#select` signature.
|
|
77
94
|
# @return [Rigor::Type, nil] inferred return type, or `nil`
|
|
78
95
|
# when no rule resolves (no class name, no method, dispatch
|
|
79
96
|
# on a Top/Dynamic[Top] receiver, etc.).
|
|
80
|
-
def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil
|
|
97
|
+
def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil, self_type_override: nil,
|
|
98
|
+
public_only: false)
|
|
81
99
|
return nil if environment.nil?
|
|
82
100
|
return nil unless environment.rbs_loader
|
|
83
101
|
|
|
@@ -86,7 +104,9 @@ module Rigor
|
|
|
86
104
|
method_name: method_name,
|
|
87
105
|
args: args,
|
|
88
106
|
environment: environment,
|
|
89
|
-
block_type: block_type
|
|
107
|
+
block_type: block_type,
|
|
108
|
+
self_type_override: self_type_override,
|
|
109
|
+
public_only: public_only
|
|
90
110
|
)
|
|
91
111
|
end
|
|
92
112
|
|
|
@@ -128,32 +148,39 @@ module Rigor
|
|
|
128
148
|
class << self
|
|
129
149
|
private
|
|
130
150
|
|
|
131
|
-
def dispatch_for(receiver:, method_name:, args:, environment:, block_type:
|
|
151
|
+
def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil,
|
|
152
|
+
public_only: false)
|
|
132
153
|
args ||= []
|
|
133
154
|
case receiver
|
|
134
155
|
when Type::Union
|
|
135
|
-
dispatch_union(receiver, method_name, args, environment, block_type
|
|
156
|
+
dispatch_union(receiver, method_name, args, environment, block_type, self_type_override,
|
|
157
|
+
public_only: public_only)
|
|
136
158
|
else
|
|
137
|
-
dispatch_one(receiver, method_name, args, environment, block_type
|
|
159
|
+
dispatch_one(receiver, method_name, args, environment, block_type, self_type_override,
|
|
160
|
+
public_only: public_only)
|
|
138
161
|
end
|
|
139
162
|
end
|
|
140
163
|
|
|
141
|
-
def dispatch_union(receiver, method_name, args, environment, block_type
|
|
164
|
+
def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil,
|
|
165
|
+
public_only: false)
|
|
142
166
|
results = receiver.members.map do |member|
|
|
143
|
-
dispatch_one(member, method_name, args, environment, block_type
|
|
167
|
+
dispatch_one(member, method_name, args, environment, block_type, self_type_override,
|
|
168
|
+
public_only: public_only)
|
|
144
169
|
end
|
|
145
170
|
return nil if results.any?(&:nil?)
|
|
146
171
|
|
|
147
172
|
Type::Combinator.union(*results)
|
|
148
173
|
end
|
|
149
174
|
|
|
150
|
-
def dispatch_one(receiver, method_name, args, environment, block_type
|
|
175
|
+
def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil,
|
|
176
|
+
public_only: false)
|
|
151
177
|
descriptor = receiver_descriptor(receiver)
|
|
152
178
|
return nil unless descriptor
|
|
153
179
|
|
|
154
180
|
class_name, kind, receiver_args = descriptor
|
|
155
181
|
method_definition = lookup_method(environment, class_name, kind, method_name)
|
|
156
182
|
return nil unless method_definition
|
|
183
|
+
return nil if public_only && method_private?(method_definition)
|
|
157
184
|
|
|
158
185
|
type_vars = build_type_vars(environment, class_name, receiver_args)
|
|
159
186
|
translate_return_type(
|
|
@@ -163,7 +190,8 @@ module Rigor
|
|
|
163
190
|
args: args,
|
|
164
191
|
type_vars: type_vars,
|
|
165
192
|
block_type: block_type,
|
|
166
|
-
environment: environment
|
|
193
|
+
environment: environment,
|
|
194
|
+
self_type_override: self_type_override
|
|
167
195
|
)
|
|
168
196
|
rescue StandardError
|
|
169
197
|
# Defensive: if RBS' definition builder raises on a broken
|
|
@@ -229,6 +257,16 @@ module Rigor
|
|
|
229
257
|
]
|
|
230
258
|
end
|
|
231
259
|
|
|
260
|
+
# True when the RBS method definition is `private`. A call
|
|
261
|
+
# with an explicit, non-`self` receiver cannot reach a
|
|
262
|
+
# private method (Ruby raises `NoMethodError`), so the
|
|
263
|
+
# explicit-receiver user-class fallback uses this to reject
|
|
264
|
+
# private signatures rather than return a wrong type.
|
|
265
|
+
def method_private?(method_definition)
|
|
266
|
+
method_definition.respond_to?(:accessibility) &&
|
|
267
|
+
method_definition.accessibility == :private
|
|
268
|
+
end
|
|
269
|
+
|
|
232
270
|
def lookup_method(environment, class_name, kind, method_name)
|
|
233
271
|
case kind
|
|
234
272
|
when :instance
|
|
@@ -254,8 +292,10 @@ module Rigor
|
|
|
254
292
|
param_names.zip(receiver_args).to_h
|
|
255
293
|
end
|
|
256
294
|
|
|
295
|
+
# rubocop:disable Metrics/ParameterLists
|
|
257
296
|
def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:,
|
|
258
|
-
environment: nil)
|
|
297
|
+
environment: nil, self_type_override: nil)
|
|
298
|
+
# rubocop:enable Metrics/ParameterLists
|
|
259
299
|
# Slice 4b-3 (ADR-7 § "Slice 4-A/4-B") — read the
|
|
260
300
|
# return-type override through the merger so future
|
|
261
301
|
# plugin / `:rbs_extended` bundles that also assert a
|
|
@@ -266,11 +306,17 @@ module Rigor
|
|
|
266
306
|
return override if override
|
|
267
307
|
|
|
268
308
|
instance_type = Type::Combinator.nominal_of(class_name)
|
|
269
|
-
|
|
309
|
+
resolved_self_type =
|
|
270
310
|
case kind
|
|
271
311
|
when :singleton then Type::Combinator.singleton_of(class_name)
|
|
272
312
|
else instance_type
|
|
273
313
|
end
|
|
314
|
+
# `self_type_override` lets the user-class fallback
|
|
315
|
+
# path preserve the ORIGINAL receiver as the substitute
|
|
316
|
+
# for `Bases::Self` — so `Kernel#dup: () -> self`
|
|
317
|
+
# resolved through the Object fallback returns the
|
|
318
|
+
# caller's type, not Object.
|
|
319
|
+
self_type = self_type_override || resolved_self_type
|
|
274
320
|
|
|
275
321
|
method_type = OverloadSelector.select(
|
|
276
322
|
method_definition,
|
|
@@ -191,7 +191,7 @@ module Rigor
|
|
|
191
191
|
# introspection (`attr_reader`, `private`, ...) on
|
|
192
192
|
# user classes without requiring the user to author
|
|
193
193
|
# their own RBS.
|
|
194
|
-
try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
194
|
+
try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type, call_node)
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
# v0.1.3 — discovered-method dispatch tier. `scope` carries
|
|
@@ -651,27 +651,72 @@ module Rigor
|
|
|
651
651
|
)
|
|
652
652
|
end
|
|
653
653
|
|
|
654
|
-
def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
654
|
+
def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type, call_node = nil)
|
|
655
655
|
return nil if environment.nil?
|
|
656
656
|
|
|
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.
|
|
668
|
+
#
|
|
669
|
+
# `public_only:` — when the call has an EXPLICIT, non-`self`
|
|
670
|
+
# receiver (`Favourite.select(...)`), suppress the private
|
|
671
|
+
# `Object`/`Kernel`/`Class` methods the fallback would
|
|
672
|
+
# otherwise resolve. Ruby raises `NoMethodError` for a
|
|
673
|
+
# private method called with an explicit receiver, so
|
|
674
|
+
# resolving `Favourite.select` to the private `Kernel#select`
|
|
675
|
+
# (`-> Array[String]`) is a confidently-wrong type. Implicit-
|
|
676
|
+
# self / `self.`-receiver calls (`puts`, `raise`, `require`)
|
|
677
|
+
# keep resolving — those are the fallback's intended targets.
|
|
660
678
|
RbsDispatch.try_dispatch(
|
|
661
679
|
receiver: fallback_receiver,
|
|
662
680
|
method_name: method_name,
|
|
663
681
|
args: arg_types,
|
|
664
682
|
environment: environment,
|
|
665
|
-
block_type: block_type
|
|
683
|
+
block_type: block_type,
|
|
684
|
+
self_type_override: receiver_type,
|
|
685
|
+
public_only: explicit_non_self_receiver?(call_node)
|
|
666
686
|
)
|
|
667
687
|
end
|
|
668
688
|
|
|
689
|
+
# True when the call node carries an explicit receiver that is
|
|
690
|
+
# not the literal `self`. Such a call cannot legally dispatch to
|
|
691
|
+
# a private method, so the user-class fallback must skip private
|
|
692
|
+
# signatures rather than return a confidently-wrong type. Returns
|
|
693
|
+
# false for implicit-self calls and `self.`-receiver calls (both
|
|
694
|
+
# may legally reach a private method in modern Ruby), and false
|
|
695
|
+
# when no `call_node` is supplied (internal dispatcher callers).
|
|
696
|
+
def explicit_non_self_receiver?(call_node)
|
|
697
|
+
return false if call_node.nil?
|
|
698
|
+
return false unless call_node.respond_to?(:receiver)
|
|
699
|
+
|
|
700
|
+
receiver = call_node.receiver
|
|
701
|
+
return false if receiver.nil?
|
|
702
|
+
|
|
703
|
+
!receiver.is_a?(Prism::SelfNode)
|
|
704
|
+
end
|
|
705
|
+
|
|
669
706
|
def user_class_fallback_receiver(receiver_type, environment)
|
|
670
707
|
case receiver_type
|
|
671
708
|
when Type::Nominal
|
|
672
|
-
|
|
709
|
+
# Modules: even when RBS knows the module, an instance
|
|
710
|
+
# method on a mixin-only module (e.g. `PP::ObjectMixin`)
|
|
711
|
+
# observes Kernel / Object methods through every concrete
|
|
712
|
+
# includer's ancestor chain. Route through the
|
|
713
|
+
# `Nominal[Object]` fallback so `self.inspect` /
|
|
714
|
+
# `self.respond_to?` / `self.class` resolve cleanly when
|
|
715
|
+
# the module itself does not declare them.
|
|
716
|
+
known = Rigor::Reflection.rbs_class_known?(receiver_type.class_name, environment: environment)
|
|
717
|
+
return environment.nominal_for_name("Object") if !known || environment.rbs_module?(receiver_type.class_name)
|
|
673
718
|
|
|
674
|
-
|
|
719
|
+
nil
|
|
675
720
|
when Type::Singleton
|
|
676
721
|
return nil if Rigor::Reflection.rbs_class_known?(receiver_type.class_name, environment: environment)
|
|
677
722
|
|