rigortype 0.1.7 → 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/lib/rigor/analysis/check_rules.rb +3 -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/triage_command.rb +83 -0
- data/lib/rigor/cli/triage_renderer.rb +77 -0
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +135 -12
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +33 -8
- data/lib/rigor/inference/method_dispatcher.rb +31 -3
- data/lib/rigor/inference/scope_indexer.rb +156 -6
- data/lib/rigor/inference/statement_evaluator.rb +40 -20
- 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/inference.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- metadata +6 -1
|
@@ -1025,11 +1025,15 @@ module Rigor
|
|
|
1025
1025
|
local_def = node.receiver.nil? ? scope.top_level_def_for(node.name) : nil
|
|
1026
1026
|
if local_def
|
|
1027
1027
|
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
1028
|
-
return local_inference if local_inference
|
|
1029
|
-
|
|
1030
|
-
# The local def matches by name but the
|
|
1031
|
-
#
|
|
1032
|
-
#
|
|
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?`).
|
|
1033
1037
|
# Returning `Dynamic[Top]` is the safest answer:
|
|
1034
1038
|
# we know RBS dispatch would be wrong (the
|
|
1035
1039
|
# method is user-defined and shadows whatever
|
|
@@ -1069,7 +1073,11 @@ module Rigor
|
|
|
1069
1073
|
# the body with the call's argument types bound and
|
|
1070
1074
|
# return the body's last-expression type.
|
|
1071
1075
|
user_inference = try_user_method_inference(receiver, node, arg_types)
|
|
1072
|
-
|
|
1076
|
+
if user_inference
|
|
1077
|
+
return user_inference if adoptable_self_call_result?(user_inference)
|
|
1078
|
+
|
|
1079
|
+
return dynamic_top
|
|
1080
|
+
end
|
|
1073
1081
|
|
|
1074
1082
|
# Dynamic-origin propagation: when the receiver is Dynamic[T] and
|
|
1075
1083
|
# no positive rule resolves the call, the result inherits the
|
|
@@ -1112,10 +1120,39 @@ module Rigor
|
|
|
1112
1120
|
nil
|
|
1113
1121
|
end
|
|
1114
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
|
+
|
|
1115
1152
|
def try_user_method_inference(receiver, call_node, arg_types)
|
|
1116
1153
|
return nil unless receiver.is_a?(Type::Nominal)
|
|
1117
1154
|
|
|
1118
|
-
def_node =
|
|
1155
|
+
def_node = resolve_user_def_through_ancestors(receiver.class_name, call_node.name)
|
|
1119
1156
|
return nil if def_node.nil?
|
|
1120
1157
|
|
|
1121
1158
|
infer_user_method_return(def_node, receiver, arg_types)
|
|
@@ -1123,6 +1160,81 @@ module Rigor
|
|
|
1123
1160
|
nil
|
|
1124
1161
|
end
|
|
1125
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
|
+
|
|
1126
1238
|
INFERENCE_GUARD_KEY = :__rigor_user_method_inference_stack__
|
|
1127
1239
|
private_constant :INFERENCE_GUARD_KEY
|
|
1128
1240
|
|
|
@@ -1132,11 +1244,20 @@ module Rigor
|
|
|
1132
1244
|
body_scope = build_user_method_body_scope(def_node, receiver, arg_types)
|
|
1133
1245
|
return nil if body_scope.nil?
|
|
1134
1246
|
|
|
1135
|
-
# Recursion-guard signature.
|
|
1136
|
-
#
|
|
1137
|
-
#
|
|
1138
|
-
#
|
|
1139
|
-
|
|
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]
|
|
1140
1261
|
stack = (Thread.current[INFERENCE_GUARD_KEY] ||= [])
|
|
1141
1262
|
return Type::Combinator.untyped if stack.include?(signature)
|
|
1142
1263
|
|
|
@@ -1174,6 +1295,8 @@ module Rigor
|
|
|
1174
1295
|
.with_program_globals(scope.program_globals)
|
|
1175
1296
|
.with_discovered_methods(scope.discovered_methods)
|
|
1176
1297
|
.with_discovered_def_nodes(scope.discovered_def_nodes)
|
|
1298
|
+
.with_discovered_superclasses(scope.discovered_superclasses)
|
|
1299
|
+
.with_discovered_includes(scope.discovered_includes)
|
|
1177
1300
|
.with_self_type(receiver)
|
|
1178
1301
|
|
|
1179
1302
|
required.each_with_index do |param, index|
|
|
@@ -85,10 +85,17 @@ module Rigor
|
|
|
85
85
|
# `Bundler::URI::Generic` per `Kernel#dup: () -> self`
|
|
86
86
|
# rather than `Object`. Defaults to nil (compute self
|
|
87
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.
|
|
88
94
|
# @return [Rigor::Type, nil] inferred return type, or `nil`
|
|
89
95
|
# when no rule resolves (no class name, no method, dispatch
|
|
90
96
|
# on a Top/Dynamic[Top] receiver, etc.).
|
|
91
|
-
def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil, self_type_override: nil
|
|
97
|
+
def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil, self_type_override: nil,
|
|
98
|
+
public_only: false)
|
|
92
99
|
return nil if environment.nil?
|
|
93
100
|
return nil unless environment.rbs_loader
|
|
94
101
|
|
|
@@ -98,7 +105,8 @@ module Rigor
|
|
|
98
105
|
args: args,
|
|
99
106
|
environment: environment,
|
|
100
107
|
block_type: block_type,
|
|
101
|
-
self_type_override: self_type_override
|
|
108
|
+
self_type_override: self_type_override,
|
|
109
|
+
public_only: public_only
|
|
102
110
|
)
|
|
103
111
|
end
|
|
104
112
|
|
|
@@ -140,32 +148,39 @@ module Rigor
|
|
|
140
148
|
class << self
|
|
141
149
|
private
|
|
142
150
|
|
|
143
|
-
def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil
|
|
151
|
+
def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil,
|
|
152
|
+
public_only: false)
|
|
144
153
|
args ||= []
|
|
145
154
|
case receiver
|
|
146
155
|
when Type::Union
|
|
147
|
-
dispatch_union(receiver, method_name, args, environment, block_type, self_type_override
|
|
156
|
+
dispatch_union(receiver, method_name, args, environment, block_type, self_type_override,
|
|
157
|
+
public_only: public_only)
|
|
148
158
|
else
|
|
149
|
-
dispatch_one(receiver, method_name, args, environment, block_type, self_type_override
|
|
159
|
+
dispatch_one(receiver, method_name, args, environment, block_type, self_type_override,
|
|
160
|
+
public_only: public_only)
|
|
150
161
|
end
|
|
151
162
|
end
|
|
152
163
|
|
|
153
|
-
def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil
|
|
164
|
+
def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil,
|
|
165
|
+
public_only: false)
|
|
154
166
|
results = receiver.members.map do |member|
|
|
155
|
-
dispatch_one(member, method_name, args, environment, block_type, self_type_override
|
|
167
|
+
dispatch_one(member, method_name, args, environment, block_type, self_type_override,
|
|
168
|
+
public_only: public_only)
|
|
156
169
|
end
|
|
157
170
|
return nil if results.any?(&:nil?)
|
|
158
171
|
|
|
159
172
|
Type::Combinator.union(*results)
|
|
160
173
|
end
|
|
161
174
|
|
|
162
|
-
def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil
|
|
175
|
+
def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil,
|
|
176
|
+
public_only: false)
|
|
163
177
|
descriptor = receiver_descriptor(receiver)
|
|
164
178
|
return nil unless descriptor
|
|
165
179
|
|
|
166
180
|
class_name, kind, receiver_args = descriptor
|
|
167
181
|
method_definition = lookup_method(environment, class_name, kind, method_name)
|
|
168
182
|
return nil unless method_definition
|
|
183
|
+
return nil if public_only && method_private?(method_definition)
|
|
169
184
|
|
|
170
185
|
type_vars = build_type_vars(environment, class_name, receiver_args)
|
|
171
186
|
translate_return_type(
|
|
@@ -242,6 +257,16 @@ module Rigor
|
|
|
242
257
|
]
|
|
243
258
|
end
|
|
244
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
|
+
|
|
245
270
|
def lookup_method(environment, class_name, kind, method_name)
|
|
246
271
|
case kind
|
|
247
272
|
when :instance
|
|
@@ -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,7 +651,7 @@ 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)
|
|
@@ -665,16 +665,44 @@ module Rigor
|
|
|
665
665
|
# `Bundler::URI::Generic` instance method types `base`
|
|
666
666
|
# as `Object` because `Bundler::URI::Generic` is not in
|
|
667
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.
|
|
668
678
|
RbsDispatch.try_dispatch(
|
|
669
679
|
receiver: fallback_receiver,
|
|
670
680
|
method_name: method_name,
|
|
671
681
|
args: arg_types,
|
|
672
682
|
environment: environment,
|
|
673
683
|
block_type: block_type,
|
|
674
|
-
self_type_override: receiver_type
|
|
684
|
+
self_type_override: receiver_type,
|
|
685
|
+
public_only: explicit_non_self_receiver?(call_node)
|
|
675
686
|
)
|
|
676
687
|
end
|
|
677
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
|
+
|
|
678
706
|
def user_class_fallback_receiver(receiver_type, environment)
|
|
679
707
|
case receiver_type
|
|
680
708
|
when Type::Nominal
|
|
@@ -109,12 +109,11 @@ module Rigor
|
|
|
109
109
|
discovered_methods = build_discovered_methods(root)
|
|
110
110
|
seeded_scope = seeded_scope.with_discovered_methods(discovered_methods)
|
|
111
111
|
|
|
112
|
-
# v0.0.2 #5
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
seeded_scope = seeded_scope.with_discovered_def_nodes(discovered_def_nodes)
|
|
112
|
+
# v0.0.2 #5 + ADR-24 slice 2 — record per-instance-method
|
|
113
|
+
# def nodes, the class -> superclass map, and the
|
|
114
|
+
# class/module -> included-modules map, each merged under
|
|
115
|
+
# the cross-file pre-pass seed (see below).
|
|
116
|
+
seeded_scope = merge_project_method_indexes(seeded_scope, default_scope, root)
|
|
118
117
|
|
|
119
118
|
# v0.1.2 — per-class table of method visibilities
|
|
120
119
|
# (`:public` / `:private` / `:protected`). The
|
|
@@ -134,6 +133,31 @@ module Rigor
|
|
|
134
133
|
table
|
|
135
134
|
end
|
|
136
135
|
|
|
136
|
+
# v0.0.2 #5 + ADR-24 slice 2 — seeds the three
|
|
137
|
+
# project-method indexes onto `seeded_scope`: the
|
|
138
|
+
# per-instance-method def-node table, the class ->
|
|
139
|
+
# superclass map, and the class/module -> included-modules
|
|
140
|
+
# map. Each per-file table is merged UNDER the cross-file
|
|
141
|
+
# `discovered_def_index_for_paths` seed carried on
|
|
142
|
+
# `default_scope` — same-file declarations win per entry,
|
|
143
|
+
# the cross-file seed supplies sibling-file ancestors.
|
|
144
|
+
def merge_project_method_indexes(seeded_scope, default_scope, root)
|
|
145
|
+
def_nodes = default_scope.discovered_def_nodes.merge(
|
|
146
|
+
build_discovered_def_nodes(root)
|
|
147
|
+
) { |_class, cross_file, per_file| cross_file.merge(per_file) }
|
|
148
|
+
superclasses = default_scope.discovered_superclasses.merge(
|
|
149
|
+
build_discovered_superclasses(root)
|
|
150
|
+
)
|
|
151
|
+
includes = default_scope.discovered_includes.merge(
|
|
152
|
+
build_discovered_includes(root)
|
|
153
|
+
) { |_class, cross_file, per_file| (cross_file + per_file).uniq }
|
|
154
|
+
|
|
155
|
+
seeded_scope
|
|
156
|
+
.with_discovered_def_nodes(def_nodes)
|
|
157
|
+
.with_discovered_superclasses(superclasses)
|
|
158
|
+
.with_discovered_includes(includes)
|
|
159
|
+
end
|
|
160
|
+
|
|
137
161
|
# Slice 7 phase 2. Builds the class-level ivar accumulator
|
|
138
162
|
# by walking every `Prism::ClassNode` / `Prism::ModuleNode`
|
|
139
163
|
# body, descending into each nested `Prism::DefNode`, and
|
|
@@ -580,6 +604,94 @@ module Rigor
|
|
|
580
604
|
accumulator[class_name][def_node.name] = def_node
|
|
581
605
|
end
|
|
582
606
|
|
|
607
|
+
# ADR-24 slice 2 — per-class table mapping a fully
|
|
608
|
+
# qualified user class to its superclass name AS WRITTEN
|
|
609
|
+
# at the `class Foo < Bar` declaration. Only constant
|
|
610
|
+
# superclasses are recorded (`class Foo < Struct.new(...)`
|
|
611
|
+
# and other non-constant superclasses produce no entry).
|
|
612
|
+
# The as-written name is resolved to a qualified class at
|
|
613
|
+
# the call site against the subclass's lexical nesting —
|
|
614
|
+
# see `ExpressionTyper#resolve_ancestor_class_name`.
|
|
615
|
+
def build_discovered_superclasses(root)
|
|
616
|
+
accumulator = {}
|
|
617
|
+
walk_class_superclasses(root, [], accumulator)
|
|
618
|
+
accumulator.freeze
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def walk_class_superclasses(node, qualified_prefix, accumulator)
|
|
622
|
+
return unless node.is_a?(Prism::Node)
|
|
623
|
+
|
|
624
|
+
case node
|
|
625
|
+
when Prism::ClassNode
|
|
626
|
+
name = qualified_name_for(node.constant_path)
|
|
627
|
+
if name
|
|
628
|
+
full = (qualified_prefix + [name]).join("::")
|
|
629
|
+
superclass = node.superclass && qualified_name_for(node.superclass)
|
|
630
|
+
accumulator[full] = superclass if superclass
|
|
631
|
+
walk_class_superclasses(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
632
|
+
return
|
|
633
|
+
end
|
|
634
|
+
when Prism::ModuleNode
|
|
635
|
+
name = qualified_name_for(node.constant_path)
|
|
636
|
+
if name
|
|
637
|
+
walk_class_superclasses(node.body, qualified_prefix + [name], accumulator) if node.body
|
|
638
|
+
return
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
node.compact_child_nodes.each do |child|
|
|
643
|
+
walk_class_superclasses(child, qualified_prefix, accumulator)
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
MIXIN_CALL_NAMES = %i[include prepend].freeze
|
|
648
|
+
|
|
649
|
+
# ADR-24 slice 2 — per-class/module table mapping a fully
|
|
650
|
+
# qualified user class or module to the list of module
|
|
651
|
+
# names it `include`s / `prepend`s, AS WRITTEN at the
|
|
652
|
+
# mixin call (`include Foo` / `include Foo::Bar`). Only
|
|
653
|
+
# constant arguments are recorded; dynamic mixins
|
|
654
|
+
# (`include some_method`) produce no entry. `prepend` is
|
|
655
|
+
# bucketed with `include` — both contribute instance
|
|
656
|
+
# methods to the ancestor chain. `extend` is NOT tracked
|
|
657
|
+
# (it adds singleton methods; ADR-24 slice 2 resolves the
|
|
658
|
+
# instance-side chain).
|
|
659
|
+
def build_discovered_includes(root)
|
|
660
|
+
accumulator = {}
|
|
661
|
+
walk_class_includes(root, [], nil, accumulator)
|
|
662
|
+
accumulator.transform_values { |mods| mods.uniq.freeze }.freeze
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def walk_class_includes(node, qualified_prefix, current_class, accumulator)
|
|
666
|
+
return unless node.is_a?(Prism::Node)
|
|
667
|
+
|
|
668
|
+
case node
|
|
669
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
670
|
+
name = qualified_name_for(node.constant_path)
|
|
671
|
+
if name
|
|
672
|
+
full = (qualified_prefix + [name]).join("::")
|
|
673
|
+
walk_class_includes(node.body, qualified_prefix + [name], full, accumulator) if node.body
|
|
674
|
+
return
|
|
675
|
+
end
|
|
676
|
+
when Prism::CallNode
|
|
677
|
+
record_mixin_call(node, current_class, accumulator)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
node.compact_child_nodes.each do |child|
|
|
681
|
+
walk_class_includes(child, qualified_prefix, current_class, accumulator)
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def record_mixin_call(node, current_class, accumulator)
|
|
686
|
+
return unless current_class && node.receiver.nil?
|
|
687
|
+
return unless MIXIN_CALL_NAMES.include?(node.name)
|
|
688
|
+
|
|
689
|
+
node.arguments&.arguments&.each do |arg|
|
|
690
|
+
mod = qualified_name_for(arg)
|
|
691
|
+
(accumulator[current_class] ||= []) << mod if mod
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
583
695
|
VISIBILITY_MODIFIERS = %i[public private protected].freeze
|
|
584
696
|
|
|
585
697
|
# v0.1.2 — per-class method-visibility table for the
|
|
@@ -845,6 +957,44 @@ module Rigor
|
|
|
845
957
|
accumulator.freeze
|
|
846
958
|
end
|
|
847
959
|
|
|
960
|
+
# ADR-24 slice 2 — cross-file companion to
|
|
961
|
+
# `discovered_classes_for_paths`. Walks every project
|
|
962
|
+
# file once and returns both the merged
|
|
963
|
+
# `discovered_def_nodes` table (a class reopened across
|
|
964
|
+
# files has its method tables merged) and the merged
|
|
965
|
+
# class -> superclass-name map. The engine consults these
|
|
966
|
+
# so an implicit-self call inside a subclass resolves
|
|
967
|
+
# against a superclass `def` declared in a sibling file
|
|
968
|
+
# (`Mastodon::CLI::Accounts` calling a helper defined in
|
|
969
|
+
# `Mastodon::CLI::Base`).
|
|
970
|
+
#
|
|
971
|
+
# @param paths [Array<String>] project file paths.
|
|
972
|
+
# @param buffer [Rigor::Analysis::BufferBinding, nil]
|
|
973
|
+
# @return [Hash{Symbol => Hash}] `{ def_nodes:, superclasses: }`
|
|
974
|
+
def discovered_def_index_for_paths(paths, buffer: nil)
|
|
975
|
+
def_nodes = {}
|
|
976
|
+
superclasses = {}
|
|
977
|
+
includes = {}
|
|
978
|
+
paths.each do |path|
|
|
979
|
+
physical = buffer ? buffer.resolve(path) : path
|
|
980
|
+
root = Prism.parse(File.read(physical), filepath: path).value
|
|
981
|
+
build_discovered_def_nodes(root).each do |class_name, methods|
|
|
982
|
+
(def_nodes[class_name] ||= {}).merge!(methods)
|
|
983
|
+
end
|
|
984
|
+
superclasses.merge!(build_discovered_superclasses(root))
|
|
985
|
+
build_discovered_includes(root).each do |class_name, mods|
|
|
986
|
+
includes[class_name] = ((includes[class_name] || []) + mods).uniq
|
|
987
|
+
end
|
|
988
|
+
rescue StandardError
|
|
989
|
+
# Skip files that fail to parse or read; the per-file
|
|
990
|
+
# analyzer surfaces the parse error separately.
|
|
991
|
+
next
|
|
992
|
+
end
|
|
993
|
+
def_nodes.each_value(&:freeze)
|
|
994
|
+
includes.each_value(&:freeze)
|
|
995
|
+
{ def_nodes: def_nodes.freeze, superclasses: superclasses.freeze, includes: includes.freeze }
|
|
996
|
+
end
|
|
997
|
+
|
|
848
998
|
# Class-only variant of `record_declarations` — descends
|
|
849
999
|
# into nested module bodies (so `module Foo; class Bar`
|
|
850
1000
|
# registers `Foo::Bar`) but never registers the module
|
|
@@ -358,9 +358,9 @@ module Rigor
|
|
|
358
358
|
# is the falsey edge of the predicate (subsequent
|
|
359
359
|
# statements observe the predicate-was-false world).
|
|
360
360
|
return [Type::Combinator.union(then_type, else_type), falsey_scope] \
|
|
361
|
-
if
|
|
361
|
+
if branch_terminates?(node.statements, then_type) && node.subsequent.nil?
|
|
362
362
|
return [Type::Combinator.union(then_type, else_type), truthy_scope] \
|
|
363
|
-
if
|
|
363
|
+
if branch_terminates?(node.subsequent, else_type) && node.statements
|
|
364
364
|
|
|
365
365
|
[
|
|
366
366
|
Type::Combinator.union(then_type, else_type),
|
|
@@ -385,9 +385,9 @@ module Rigor
|
|
|
385
385
|
# `if`: when the body unconditionally exits and there
|
|
386
386
|
# is no else, the post-scope is the truthy edge.
|
|
387
387
|
return [Type::Combinator.union(then_type, else_type), truthy_scope] \
|
|
388
|
-
if
|
|
388
|
+
if branch_terminates?(node.statements, then_type) && node.else_clause.nil?
|
|
389
389
|
return [Type::Combinator.union(then_type, else_type), falsey_scope] \
|
|
390
|
-
if
|
|
390
|
+
if branch_terminates?(node.else_clause, else_type) && node.statements
|
|
391
391
|
|
|
392
392
|
[
|
|
393
393
|
Type::Combinator.union(then_type, else_type),
|
|
@@ -709,24 +709,25 @@ module Rigor
|
|
|
709
709
|
# `a` when the RHS is skipped, while `a || b` can only produce
|
|
710
710
|
# the truthy fragment of `a` when the RHS is skipped.
|
|
711
711
|
#
|
|
712
|
-
# When the RHS
|
|
713
|
-
# `
|
|
714
|
-
#
|
|
715
|
-
# `a or
|
|
716
|
-
#
|
|
717
|
-
#
|
|
718
|
-
#
|
|
719
|
-
#
|
|
712
|
+
# When the RHS is a terminating branch — it `raise`s /
|
|
713
|
+
# `return`s / `throw`s / `exit`s / `break`s / `next`s, OR its
|
|
714
|
+
# inferred type is `Bot` (ADR-24 WD6: a divergent helper such
|
|
715
|
+
# as `a or fail_with_message(...)`, recognised via
|
|
716
|
+
# `branch_terminates?`) — the post-OR / post-AND scope is the
|
|
717
|
+
# LHS-skipped edge alone: `a or raise` only survives when `a`
|
|
718
|
+
# was truthy, so subsequent statements observe `a` narrowed to
|
|
719
|
+
# its truthy fragment; the symmetric `a and raise` survives
|
|
720
|
+
# only when `a` was falsey. Same shape as the `eval_if` /
|
|
721
|
+
# `eval_unless` early-return narrowing.
|
|
720
722
|
def eval_and_or(node)
|
|
721
723
|
left_type, left_scope = sub_eval(node.left, scope)
|
|
722
724
|
truthy_left, falsey_left = Narrowing.predicate_scopes(node.left, left_scope)
|
|
723
725
|
rhs_entry = node.is_a?(Prism::AndNode) ? truthy_left : falsey_left
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
#
|
|
728
|
-
#
|
|
729
|
-
sub_eval(node.right, rhs_entry)
|
|
726
|
+
right_type, right_scope = sub_eval(node.right, rhs_entry)
|
|
727
|
+
|
|
728
|
+
if branch_terminates?(node.right, right_type)
|
|
729
|
+
# Control never reaches any statement after `a or raise`
|
|
730
|
+
# via the RHS edge — the RHS scope is discarded.
|
|
730
731
|
surviving_type =
|
|
731
732
|
if node.is_a?(Prism::AndNode)
|
|
732
733
|
Narrowing.narrow_falsey(left_type)
|
|
@@ -737,7 +738,6 @@ module Rigor
|
|
|
737
738
|
return [surviving_type, surviving_scope]
|
|
738
739
|
end
|
|
739
740
|
|
|
740
|
-
right_type, right_scope = sub_eval(node.right, rhs_entry)
|
|
741
741
|
skipped_type =
|
|
742
742
|
if node.is_a?(Prism::AndNode)
|
|
743
743
|
Narrowing.narrow_falsey(left_type)
|
|
@@ -1484,7 +1484,7 @@ module Rigor
|
|
|
1484
1484
|
# ScopeIndexer-populated declaration overrides
|
|
1485
1485
|
# (`Prism::ConstantReadNode` for `module Foo` headers, etc.)
|
|
1486
1486
|
# remain reachable from inside nested bodies.
|
|
1487
|
-
def build_fresh_body_scope
|
|
1487
|
+
def build_fresh_body_scope # rubocop:disable Metrics/AbcSize
|
|
1488
1488
|
Scope.empty(environment: scope.environment)
|
|
1489
1489
|
.with_declared_types(scope.declared_types)
|
|
1490
1490
|
.with_discovered_classes(scope.discovered_classes)
|
|
@@ -1493,6 +1493,9 @@ module Rigor
|
|
|
1493
1493
|
.with_class_cvars(scope.class_cvars)
|
|
1494
1494
|
.with_program_globals(scope.program_globals)
|
|
1495
1495
|
.with_discovered_methods(scope.discovered_methods)
|
|
1496
|
+
.with_discovered_def_nodes(scope.discovered_def_nodes)
|
|
1497
|
+
.with_discovered_superclasses(scope.discovered_superclasses)
|
|
1498
|
+
.with_discovered_includes(scope.discovered_includes)
|
|
1496
1499
|
.with_discovered_method_visibilities(scope.discovered_method_visibilities)
|
|
1497
1500
|
end
|
|
1498
1501
|
|
|
@@ -1692,6 +1695,23 @@ module Rigor
|
|
|
1692
1695
|
end
|
|
1693
1696
|
end
|
|
1694
1697
|
|
|
1698
|
+
# ADR-24 WD6 / slice 3 — generalised terminating-branch
|
|
1699
|
+
# detection. `branch_unconditionally_exits?` recognises a
|
|
1700
|
+
# branch SYNTACTICALLY (return / next / break / a call
|
|
1701
|
+
# named raise / throw / exit / abort / fail). A branch
|
|
1702
|
+
# whose *inferred type is `Bot`* also terminates — it
|
|
1703
|
+
# cannot produce a value, so control never falls through
|
|
1704
|
+
# it — regardless of how it is spelled. The canonical
|
|
1705
|
+
# case is a resolved guard helper (`fail_with_message(...)`)
|
|
1706
|
+
# whose body always raises: ADR-24 slice 1 types the call
|
|
1707
|
+
# `bot`, and this OR-test makes `helper(...) if x.nil?`
|
|
1708
|
+
# narrow exactly like `raise ... if x.nil?`. The branch
|
|
1709
|
+
# type is already computed by `eval_if` / `eval_unless`.
|
|
1710
|
+
def branch_terminates?(branch_node, branch_type)
|
|
1711
|
+
branch_unconditionally_exits?(branch_node) ||
|
|
1712
|
+
branch_type.is_a?(Type::Bot)
|
|
1713
|
+
end
|
|
1714
|
+
|
|
1695
1715
|
def eval_branch_or_nil(branch_node, branch_scope)
|
|
1696
1716
|
return [Type::Combinator.constant_of(nil), branch_scope] if branch_node.nil?
|
|
1697
1717
|
|