rigortype 0.4.0 → 0.4.1
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 +4 -3
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +5 -0
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
- data/lib/rigor/analysis/check_rules.rb +118 -87
- data/lib/rigor/analysis/dependency_recorder.rb +200 -18
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +273 -49
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -8
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner.rb +337 -46
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +4 -3
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +70 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +5 -0
- data/lib/rigor/configuration.rb +6 -5
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +1 -1
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +13 -70
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +136 -45
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/guard_rebinding.rb +53 -12
- data/lib/rigor/inference/macro_block_self_type.rb +14 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
- data/lib/rigor/inference/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +147 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +1443 -295
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +80 -14
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +3 -3
- data/lib/rigor/reflection/constant_ancestors.rb +96 -47
- data/lib/rigor/reflection/constant_path.rb +22 -7
- data/lib/rigor/reflection.rb +2 -3
- data/lib/rigor/scope/discovery_index.rb +218 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +295 -322
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +22 -16
- data/lib/rigor/type/combinator.rb +25 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -0
- data/sig/rigor/scope.rbs +43 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +25 -1
|
@@ -33,12 +33,19 @@ module Rigor
|
|
|
33
33
|
# The accepted gap is implicit conversion: a core method that calls back into a project method the program
|
|
34
34
|
# does not spell (`puts obj` runs `obj.to_s`, `hash[obj]` runs `obj.hash`, `a.sort` runs `<=>`) is read as the
|
|
35
35
|
# core method alone.
|
|
36
|
+
#
|
|
37
|
+
# Issue #1446 — a class guard's narrowing of an instance variable is restored at the same code, since code that
|
|
38
|
+
# may rebind a global may reach `self` and rebind the variable too, and at two more: `instance_variable_set` and
|
|
39
|
+
# `remove_instance_variable` on any receiver, and a write to the narrowed variable in code the scan reads (a
|
|
40
|
+
# block that may run again, a loop's next pass).
|
|
36
41
|
module GuardRebinding
|
|
37
42
|
# Calls that run code chosen by name or by a String.
|
|
38
43
|
CODE_RUNNING_NAMES = Set[
|
|
39
44
|
:send, :__send__, :public_send, :eval, :require, :require_relative, :load,
|
|
40
45
|
# These rebind a constant on any receiver (`Object.const_set(:SEP, nil)`).
|
|
41
|
-
:const_set, :remove_const
|
|
46
|
+
:const_set, :remove_const,
|
|
47
|
+
# These rebind an instance variable of a receiver that may be `self` (#1446).
|
|
48
|
+
:instance_variable_set, :remove_instance_variable
|
|
42
49
|
].freeze
|
|
43
50
|
# These run their literal block, which the scan reads as any block, or a String of code, which it cannot read.
|
|
44
51
|
BLOCK_OR_CODE_NAMES = Set[
|
|
@@ -109,10 +116,11 @@ module Rigor
|
|
|
109
116
|
(call_node.block.is_a?(Prism::BlockArgumentNode) && may_rebind?(call_node.block, scope))
|
|
110
117
|
end
|
|
111
118
|
|
|
112
|
-
# True when running `node` may rebind one: it writes a global
|
|
113
|
-
# call, spelled or implicit ({.implicit_call_may_rebind?}), whose
|
|
114
|
-
# lambda literal run nothing where they are written. Each node is
|
|
115
|
-
# reached as one of its children, so a nested block chain costs its
|
|
119
|
+
# True when running `node` may rebind one: it writes a global, a constant or an instance variable a class guard
|
|
120
|
+
# narrowed, yields, calls `super`, or holds a call, spelled or implicit ({.implicit_call_may_rebind?}), whose
|
|
121
|
+
# method may run foreign code. A `def` and a lambda literal run nothing where they are written. Each node is
|
|
122
|
+
# visited once: a call's literal block is reached as one of its children, so a nested block chain costs its
|
|
123
|
+
# size, not its depth's power.
|
|
116
124
|
#
|
|
117
125
|
# A local the scanned code writes has no binding in `scope` yet, so the scan reads it as the code writes it
|
|
118
126
|
# ({ScanScope.with_scanned_locals}): `copy = $sep; copy.length` is a `String` call. A literal block's parameters
|
|
@@ -126,6 +134,7 @@ module Rigor
|
|
|
126
134
|
def scan(node, scope)
|
|
127
135
|
return false unless node.is_a?(Prism::Node)
|
|
128
136
|
return true if REBINDING_NODES.include?(node.class)
|
|
137
|
+
return true if guarded_ivar_write?(node, scope)
|
|
129
138
|
return false if node.is_a?(Prism::DefNode) || node.is_a?(Prism::LambdaNode)
|
|
130
139
|
return scan_call(node, scope) if node.is_a?(Prism::CallNode)
|
|
131
140
|
return true if IMPLICIT_CALL_NODES.include?(node.class) && implicit_call_may_rebind?(node, scope)
|
|
@@ -135,6 +144,18 @@ module Rigor
|
|
|
135
144
|
found
|
|
136
145
|
end
|
|
137
146
|
|
|
147
|
+
# Issue #1446 — true when `node` writes an instance variable a class guard narrowed
|
|
148
|
+
# ({Scope#guard_narrowed_ivar?}). A write to any other instance variable rebinds nothing a guard recorded.
|
|
149
|
+
def guarded_ivar_write?(node, scope)
|
|
150
|
+
case node
|
|
151
|
+
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
152
|
+
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
153
|
+
scope.guard_narrowed_ivar?(node.name)
|
|
154
|
+
else
|
|
155
|
+
false
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
138
159
|
def scan_call(node, scope)
|
|
139
160
|
return true if call_runs_foreign_code?(node, scope)
|
|
140
161
|
return true if scan(node.receiver, scope) || scan(node.arguments, scope)
|
|
@@ -156,7 +177,9 @@ module Rigor
|
|
|
156
177
|
def implicit_call_may_rebind?(node, scope)
|
|
157
178
|
kind = VARIABLE_OPERATOR_WRITES[node.class]
|
|
158
179
|
return type_method_foreign?(variable_type(kind, node.name, scope), node.binary_operator, scope) if kind
|
|
159
|
-
|
|
180
|
+
if node.is_a?(Prism::ForNode)
|
|
181
|
+
return type_method_foreign?(ScanScope.dispatch_type(node.collection, scope), :each, scope)
|
|
182
|
+
end
|
|
160
183
|
|
|
161
184
|
compound_write_foreign?(node, scope)
|
|
162
185
|
rescue StandardError
|
|
@@ -174,7 +197,7 @@ module Rigor
|
|
|
174
197
|
end
|
|
175
198
|
|
|
176
199
|
def compound_receiver_type(node, scope)
|
|
177
|
-
return
|
|
200
|
+
return ScanScope.dispatch_type(node.receiver, scope) if node.receiver
|
|
178
201
|
|
|
179
202
|
scope.self_type || Type::Combinator.nominal_of("Object")
|
|
180
203
|
end
|
|
@@ -264,7 +287,7 @@ module Rigor
|
|
|
264
287
|
return ProjectMethodOwnership.targets(self_type)
|
|
265
288
|
end
|
|
266
289
|
|
|
267
|
-
ProjectMethodOwnership.targets(
|
|
290
|
+
ProjectMethodOwnership.targets(ScanScope.dispatch_type(receiver, scope))
|
|
268
291
|
end
|
|
269
292
|
|
|
270
293
|
def foreign_target?(class_name, method_name, kind, scope)
|
|
@@ -300,8 +323,8 @@ module Rigor
|
|
|
300
323
|
owner = definition.respond_to?(:defined_in) ? definition.defined_in : nil
|
|
301
324
|
owner&.to_s&.delete_prefix("::")
|
|
302
325
|
end
|
|
303
|
-
private_class_method :scan, :scan_call, :receiver_targets, :foreign_target?,
|
|
304
|
-
:method_owner, :deferred_block_call?,
|
|
326
|
+
private_class_method :scan, :guarded_ivar_write?, :scan_call, :receiver_targets, :foreign_target?,
|
|
327
|
+
:universal_delegate_foreign?, :method_owner, :deferred_block_call?,
|
|
305
328
|
:compound_write_foreign?,
|
|
306
329
|
:compound_receiver_type, :compound_accessors, :compound_read_type, :variable_type,
|
|
307
330
|
:type_method_foreign?
|
|
@@ -376,9 +399,10 @@ module Rigor
|
|
|
376
399
|
bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
377
400
|
end
|
|
378
401
|
|
|
379
|
-
# What the method `call_node` calls yields its block, by position, or `[]` when that cannot be read.
|
|
402
|
+
# What the method `call_node` calls yields its block, by position, or `[]` when that cannot be read. A receiver
|
|
403
|
+
# a class guard left `bot` yields what the guarded class does ({.dispatch_type}, #1446).
|
|
380
404
|
def yielded_types(call_node, scope)
|
|
381
|
-
receiver = call_node.receiver ?
|
|
405
|
+
receiver = call_node.receiver ? dispatch_type(call_node.receiver, scope) : scope.self_type
|
|
382
406
|
arguments = call_node.arguments&.arguments || []
|
|
383
407
|
MethodDispatcher.expected_block_param_types(
|
|
384
408
|
receiver_type: receiver, method_name: call_node.name, environment: scope.environment, scope: scope,
|
|
@@ -388,6 +412,23 @@ module Rigor
|
|
|
388
412
|
[]
|
|
389
413
|
end
|
|
390
414
|
|
|
415
|
+
# Issue #1446 — the type a call on the receiver `node` dispatches on: its type in `scope`, or, where a class
|
|
416
|
+
# guard disjoint from the receiver's binding left it `bot` (`return unless @io.is_a?(StringIO)` on an `IO`),
|
|
417
|
+
# the classes the guard named ({Narrowing.bot_guard_classes_of}), since the value that passes the guard is one
|
|
418
|
+
# of them. So `@io.rewind` reads as `StringIO#rewind` and keeps the narrowing, while `@cb.call` past
|
|
419
|
+
# `@cb.is_a?(Proc)` reads as a call on a code object, and a method a project subclass or reopening defines on
|
|
420
|
+
# the class still counts. A `bot` no class guard produced stays `bot`, which no target resolves, so a call on
|
|
421
|
+
# it counts as an unresolved callee.
|
|
422
|
+
def dispatch_type(node, scope)
|
|
423
|
+
type = scope.type_of(node)
|
|
424
|
+
return type unless type.is_a?(Type::Bot)
|
|
425
|
+
|
|
426
|
+
classes = Narrowing.bot_guard_classes_of(node, scope)
|
|
427
|
+
return type if classes.nil?
|
|
428
|
+
|
|
429
|
+
Type::Combinator.union(*classes.map { |name| Type::Combinator.nominal_of(name) })
|
|
430
|
+
end
|
|
431
|
+
|
|
391
432
|
# The names a block's parameter list declares: every kind of parameter, a destructured one's parts included,
|
|
392
433
|
# and a `;`-block-local. A default value is not read.
|
|
393
434
|
def collect_parameter_names(node, names)
|
|
@@ -168,11 +168,13 @@ module Rigor
|
|
|
168
168
|
seen[current] = true
|
|
169
169
|
# The class's own singleton defs sit ahead of EVERY `extend` in its singleton ancestry —
|
|
170
170
|
# `class F; extend T::Sig; def self.sig(&b); end; end` calls F's method, not T::Sig's —
|
|
171
|
-
# once the def has executed; a `def` written after this call has not run yet.
|
|
171
|
+
# once the def has executed; a `def` written after this call has not run yet. Shadowing here
|
|
172
|
+
# WITHHOLDS the narrowing for every class but the constraint itself, so a possible def
|
|
173
|
+
# (ADR-119) keeps the same answer and needs no certainty check.
|
|
172
174
|
return current == constraint if scope.singleton_def_shadows_call?(current, method_name, call_node)
|
|
173
175
|
|
|
174
176
|
owner = extended_module_call_owner(current, extends, method_name, call_node, scope, environment)
|
|
175
|
-
return owner
|
|
177
|
+
return extended_owner_reaches?(owner, constraint, method_name, call_node, scope, environment) if owner
|
|
176
178
|
|
|
177
179
|
raw = supers[current]
|
|
178
180
|
scope.ancestor_name_candidates(current, raw).each { |c| queue << c } if raw
|
|
@@ -217,6 +219,16 @@ module Rigor
|
|
|
217
219
|
environment: environment).nil?
|
|
218
220
|
end
|
|
219
221
|
|
|
222
|
+
# Whether the module that answers the call is `constraint`. ADR-119 WD1 errata: here "shadows" NARROWS the
|
|
223
|
+
# block's self to the owner, so an owner whose decisive source def is only possible (`if X; def sig ...; end`)
|
|
224
|
+
# answers false — the module may not answer at runtime.
|
|
225
|
+
def extended_owner_reaches?(owner, constraint, method_name, call_node, scope, environment)
|
|
226
|
+
return false if scope.instance_def_shadows_call?(owner, method_name, call_node) &&
|
|
227
|
+
scope.discovery.possible_method?(owner, method_name, :instance)
|
|
228
|
+
|
|
229
|
+
owner == constraint || rbs_inherits?(owner, constraint, environment)
|
|
230
|
+
end
|
|
231
|
+
|
|
220
232
|
def rbs_inherits?(class_name, constraint, environment)
|
|
221
233
|
%i[equal subclass].include?(environment.class_ordering(class_name, constraint))
|
|
222
234
|
end
|
|
@@ -564,11 +564,11 @@ module Rigor
|
|
|
564
564
|
|
|
565
565
|
memo = core_stdlib_memo(environment, scope)
|
|
566
566
|
key = [class_name.to_s, method_name.to_sym]
|
|
567
|
-
return memo[key] if memo
|
|
567
|
+
return memo[key] if memo.key?(key)
|
|
568
568
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
569
|
+
store_unless_recorded(memo, key) do
|
|
570
|
+
compute_core_stdlib_ancestor_method(environment, class_name, method_name, scope)
|
|
571
|
+
end
|
|
572
572
|
end
|
|
573
573
|
|
|
574
574
|
# Issue #1173 — the include-edge sibling of {#core_stdlib_ancestor_method}: a discovered
|
|
@@ -596,11 +596,11 @@ module Rigor
|
|
|
596
596
|
|
|
597
597
|
memo = mixin_ancestor_memo(environment, scope)
|
|
598
598
|
key = [class_name.to_s, method_name.to_sym]
|
|
599
|
-
return memo[key] if memo
|
|
599
|
+
return memo[key] if memo.key?(key)
|
|
600
600
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
store_unless_recorded(memo, key) do
|
|
602
|
+
compute_included_module_method(environment, class_name, method_name, scope)
|
|
603
|
+
end
|
|
604
604
|
end
|
|
605
605
|
|
|
606
606
|
# The declines are conjunctive and ordered as {compute_core_stdlib_ancestor_method}'s are:
|
|
@@ -626,20 +626,12 @@ module Rigor
|
|
|
626
626
|
end
|
|
627
627
|
|
|
628
628
|
# The same one-slot memo shape as {#core_stdlib_memo}; see there for why it is one slot keyed
|
|
629
|
-
# on the discovery index's identity, and
|
|
629
|
+
# on the discovery index's identity, and what a recording run may store in it.
|
|
630
630
|
MIXIN_ANCESTOR_MEMO_KEY = :__rigor_mixin_ancestor_dispatch__
|
|
631
631
|
private_constant :MIXIN_ANCESTOR_MEMO_KEY
|
|
632
632
|
|
|
633
633
|
def mixin_ancestor_memo(environment, scope)
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
discovery = scope.discovery
|
|
637
|
-
slot = Thread.current[MIXIN_ANCESTOR_MEMO_KEY]
|
|
638
|
-
unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
|
|
639
|
-
slot = [discovery, environment, {}]
|
|
640
|
-
Thread.current[MIXIN_ANCESTOR_MEMO_KEY] = slot
|
|
641
|
-
end
|
|
642
|
-
slot[2]
|
|
634
|
+
ancestor_memo_slot(MIXIN_ANCESTOR_MEMO_KEY, environment, scope)
|
|
643
635
|
end
|
|
644
636
|
|
|
645
637
|
# The declines are conjunctive, so their ORDER is free — and it is chosen so the two that walk
|
|
@@ -800,8 +792,10 @@ module Rigor
|
|
|
800
792
|
# Memo for the whole decision. Every call site of a class asks the same `(class, method)`
|
|
801
793
|
# question, and the answer is a pure function of the frozen discovery index and the
|
|
802
794
|
# environment, so it is cacheable on their identity. A run that is RECORDING ADR-46
|
|
803
|
-
# dependency edges
|
|
804
|
-
#
|
|
795
|
+
# dependency edges stores only the answers whose computation recorded nothing
|
|
796
|
+
# ({#store_unless_recorded}): the shadow probes above read the project's method tables, and
|
|
797
|
+
# serving an answer that read them from the memo would swallow those edges for every later
|
|
798
|
+
# caller.
|
|
805
799
|
#
|
|
806
800
|
# ONE slot, replaced rather than accumulated — see {ExternalAncestorResolution}'s twin for
|
|
807
801
|
# the measurement. A `Scope` hands each analysed file its own discovery index, so an
|
|
@@ -811,15 +805,38 @@ module Rigor
|
|
|
811
805
|
private_constant :CORE_STDLIB_ANCESTOR_MEMO_KEY
|
|
812
806
|
|
|
813
807
|
def core_stdlib_memo(environment, scope)
|
|
814
|
-
|
|
808
|
+
ancestor_memo_slot(CORE_STDLIB_ANCESTOR_MEMO_KEY, environment, scope)
|
|
809
|
+
end
|
|
815
810
|
|
|
811
|
+
# The slot behind both memos. Whether this thread is recording is part of its key, so a slot
|
|
812
|
+
# filled with every answer is never read by a recording caller, which may only reuse the
|
|
813
|
+
# answers {#store_unless_recorded} lets through.
|
|
814
|
+
def ancestor_memo_slot(memo_key, environment, scope)
|
|
816
815
|
discovery = scope.discovery
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
816
|
+
recording = Rigor::Analysis::DependencyRecorder.recording?
|
|
817
|
+
slot = Thread.current[memo_key]
|
|
818
|
+
unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment) && slot[2] == recording
|
|
819
|
+
slot = [discovery, environment, recording, {}]
|
|
820
|
+
Thread.current[memo_key] = slot
|
|
821
821
|
end
|
|
822
|
-
slot[
|
|
822
|
+
slot[3]
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Computes an answer, stores it under `key` when it may be served again, and returns it.
|
|
826
|
+
# Outside recording every answer is stored. Under ADR-46 recording the block runs in an
|
|
827
|
+
# ADR-84 WD2 capture window, which sees every edge the block files for the current consumer
|
|
828
|
+
# (the reads it withholds are not among them). An answer whose window stayed empty is stored:
|
|
829
|
+
# computing it again would read the same frozen tables and file nothing again, so serving it
|
|
830
|
+
# from the memo records exactly what recomputing does. That is most of them, since both
|
|
831
|
+
# computations withhold their ancestry walk and return before any recorded probe unless an
|
|
832
|
+
# RBS declaration answered. Any other answer is recomputed on every call, which files its
|
|
833
|
+
# edges each time, as a recording run always did.
|
|
834
|
+
def store_unless_recorded(memo, key, &)
|
|
835
|
+
return memo[key] = yield unless Rigor::Analysis::DependencyRecorder.recording?
|
|
836
|
+
|
|
837
|
+
answer, read_set = Rigor::Analysis::DependencyRecorder.capture(&)
|
|
838
|
+
memo[key] = answer if read_set.reads.empty? && read_set.missing.empty?
|
|
839
|
+
answer
|
|
823
840
|
end
|
|
824
841
|
|
|
825
842
|
# BFS over the scope's as-written ancestry tables — include edges first, then the
|
|
@@ -889,6 +906,7 @@ module Rigor
|
|
|
889
906
|
# The class's own `def self.x` sits ahead of every `extend` — once it has run.
|
|
890
907
|
# `singleton_def_shadows_call?` orders the def against the call site, so a `def self.sig`
|
|
891
908
|
# written AFTER this `sig {}` does not suppress the bridge.
|
|
909
|
+
# ADR-119: shadowing already withholds the bridge, so a possible def withholds too.
|
|
892
910
|
return nil if scope.singleton_def_shadows_call?(current, method_name, call_node)
|
|
893
911
|
|
|
894
912
|
resolved = rbs_complete_extended_module_for(current, extends, environment, scope,
|
|
@@ -927,6 +945,8 @@ module Rigor
|
|
|
927
945
|
nil
|
|
928
946
|
end
|
|
929
947
|
|
|
948
|
+
# ADR-119: "defines" here stops the allow-list walk (a withholding answer), so a possible def stays
|
|
949
|
+
# a definer — treating it as absent would let a later extend bridge a call the def may own.
|
|
930
950
|
def extend_owner_defines?(owner, method_name, call_node, scope, environment)
|
|
931
951
|
return true if scope.instance_def_shadows_call?(owner, method_name, call_node)
|
|
932
952
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../source/node_children"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# The one home for what a receiverless `module_function` call does to the methods around it
|
|
9
|
+
# ([#1507](https://github.com/rigortype/rigor/issues/1507),
|
|
10
|
+
# [#1197](https://github.com/rigortype/rigor/issues/1197)). Four readers used to compute it in four
|
|
11
|
+
# places. They disagree with each other and, in places, with Ruby. Each entry point below reproduces
|
|
12
|
+
# one reader's answer exactly: this module gives the readings one place to live, and does not yet make
|
|
13
|
+
# them agree. A change to one reading lands here, beside the other three. Examples are #1550's
|
|
14
|
+
# named-form snapshot and the reset a bare `public`, `private` or `protected` makes.
|
|
15
|
+
#
|
|
16
|
+
# - The singleton def-node table (`ScopeIndexer#walk_singleton_body`) reads
|
|
17
|
+
# {.each_singleton_sibling} and {.each_singleton_copy}. A bare call covers the later direct
|
|
18
|
+
# statements of its own body and is never reset.
|
|
19
|
+
# - The deferred ranges (`ScopeIndexer#walk_deferred_body`) read {.prescan_deferred} and
|
|
20
|
+
# {.deferred_module_function?}. A bare call covers every later def by offset, and a call inside
|
|
21
|
+
# control flow or a block counts.
|
|
22
|
+
# - The extends table (`ScopeIndexer#record_extend_call`) reads {.extends_self?}. A bare call covers
|
|
23
|
+
# every instance def of the module, in any order.
|
|
24
|
+
# - sig-gen (`SigGen::Generator#walk_statements`) reads {.each_sig_gen_statement} and
|
|
25
|
+
# {.sig_gen_module_function?}. A bare call covers the later statements of its own statement list and
|
|
26
|
+
# what nests in them.
|
|
27
|
+
#
|
|
28
|
+
# A fifth reader does not come here yet: `rigor-activerecord`'s `ModelDiscoverer#table_name_decorators`
|
|
29
|
+
# (`plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb`) reads a module body's
|
|
30
|
+
# direct statements for its table-name readers. It also accepts `self.module_function`, and there a bare
|
|
31
|
+
# call covers the instance methods written before it as well as after it, while a named call takes only a
|
|
32
|
+
# def written before it. A change to what `module_function` means must decide whether that reader follows.
|
|
33
|
+
#
|
|
34
|
+
# The deferred-ranges prescan asks `ScopeIndexer.receiver_eval_call?` and `ScopeIndexer.def_singleton?`, so
|
|
35
|
+
# `rigor/inference/scope_indexer` must be loaded before it runs. This file does not require it:
|
|
36
|
+
# `scope_indexer.rb` requires this file, and a require back would be circular. The other entry points need
|
|
37
|
+
# only Prism.
|
|
38
|
+
#
|
|
39
|
+
# `spec/rigor/inference/module_function_state_spec.rb` pins each reader's answers, and marks the ones
|
|
40
|
+
# Ruby contradicts with a "flip this when" note.
|
|
41
|
+
module ModuleFunctionState
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# A receiverless `module_function` call, bare or named.
|
|
45
|
+
def call?(node)
|
|
46
|
+
node.name == :module_function && node.receiver.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The toggle form: a `module_function` call with no arguments.
|
|
50
|
+
def bare?(node)
|
|
51
|
+
node.arguments.nil? || node.arguments.arguments.empty?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The singleton def-node table's reading, over one class-ish body's direct statements in source
|
|
55
|
+
# order. `ScopeIndexer#statements_of` builds that list, folding a body-level `rescue`, `else` or
|
|
56
|
+
# `ensure` clause into it. Yields `stmt, module_function_on, named_call` for every statement except a
|
|
57
|
+
# bare call:
|
|
58
|
+
#
|
|
59
|
+
# - `module_function_on` says whether a bare call precedes the statement. The toggle never switches
|
|
60
|
+
# off, although a later bare `public`, `private` or `protected` resets it in Ruby.
|
|
61
|
+
# - `named_call` is true for a named call (`module_function :a` or `module_function def x`), which
|
|
62
|
+
# {.each_singleton_copy} resolves; the caller records nothing else for it.
|
|
63
|
+
#
|
|
64
|
+
# A call nested in control flow or a block is not seen.
|
|
65
|
+
def each_singleton_sibling(statements)
|
|
66
|
+
module_function_on = false
|
|
67
|
+
statements.each do |stmt|
|
|
68
|
+
if stmt.is_a?(Prism::CallNode) && call?(stmt)
|
|
69
|
+
if bare?(stmt)
|
|
70
|
+
module_function_on = true
|
|
71
|
+
else
|
|
72
|
+
yield stmt, module_function_on, true
|
|
73
|
+
end
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
yield stmt, module_function_on, false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The defs a named call copies onto the module's singleton, for the singleton def-node table. Yields
|
|
81
|
+
# `name, def_node` for each Symbol or String argument that names a receiverless def among the body's
|
|
82
|
+
# direct `statements`. The last def of that name wins wherever it sits. A def written after the call
|
|
83
|
+
# is therefore taken, although Ruby copies the one in effect at the call (#1550), and a name defined
|
|
84
|
+
# only after the call resolves, although Ruby raises `NameError` there. A `module_function def x`
|
|
85
|
+
# argument names nothing here.
|
|
86
|
+
def each_singleton_copy(call, statements)
|
|
87
|
+
defs_by_name = statements.each_with_object({}) do |stmt, acc|
|
|
88
|
+
acc[stmt.name] = stmt if stmt.is_a?(Prism::DefNode) && stmt.receiver.nil?
|
|
89
|
+
end
|
|
90
|
+
call.arguments&.arguments&.each do |arg|
|
|
91
|
+
name = symbol_argument_name(arg)
|
|
92
|
+
def_node = name && defs_by_name[name]
|
|
93
|
+
yield name, def_node if def_node
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The deferred-ranges reading, over one class-ish body's subtree. Appends to `offsets` the start of
|
|
98
|
+
# every bare call; each def that starts after one is a module function
|
|
99
|
+
# ({.deferred_module_function?}). Appends to `ranges` one row per named call, in the table's
|
|
100
|
+
# `[start, end, name, kind, owner]` shape:
|
|
101
|
+
#
|
|
102
|
+
# - a `:name` argument gives a `:singleton` row over the call itself, because the copy happens at
|
|
103
|
+
# the call and not at the def;
|
|
104
|
+
# - a `module_function def x` argument gives a `:both` row over the def, or a `:singleton` row for
|
|
105
|
+
# `def self.x`.
|
|
106
|
+
#
|
|
107
|
+
# The prescan looks through control flow and blocks, because a call that ran there still flips the
|
|
108
|
+
# later defs. It stops at a nested class, module, `class <<` or def body, where the call targets
|
|
109
|
+
# another module. It skips an `END` body, which runs after every def, and a `*_eval` / `*_exec`
|
|
110
|
+
# block, whose self is the receiver's module; that block gets its own prescan as a body. Nothing
|
|
111
|
+
# resets it.
|
|
112
|
+
#
|
|
113
|
+
# `qualified_prefix` names the owner, and `in_singleton_class` says whether the body is a
|
|
114
|
+
# `class <<` body; both feed `ScopeIndexer.def_singleton?`.
|
|
115
|
+
def prescan_deferred(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
116
|
+
return unless node.is_a?(Prism::Node)
|
|
117
|
+
return if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode) ||
|
|
118
|
+
node.is_a?(Prism::SingletonClassNode) || node.is_a?(Prism::DefNode) ||
|
|
119
|
+
node.is_a?(Prism::PostExecutionNode)
|
|
120
|
+
|
|
121
|
+
if node.is_a?(Prism::CallNode)
|
|
122
|
+
return prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges) if call?(node)
|
|
123
|
+
if ScopeIndexer.receiver_eval_call?(node)
|
|
124
|
+
return prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
node.rigor_each_child do |child|
|
|
129
|
+
prescan_deferred(child, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Whether the deferred-ranges reading makes a def starting at `start` a module function: a bare call
|
|
134
|
+
# starts before it, at any depth the prescan entered.
|
|
135
|
+
def deferred_module_function?(offsets, start)
|
|
136
|
+
offsets.any? { |offset| offset < start }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The extends table's reading. A bare call makes the module extend itself, so every instance def the
|
|
140
|
+
# module has also answers on its singleton. That holds for defs before and after the call, and
|
|
141
|
+
# whatever a later `public` or `private` says. The over-approximation is deliberate (#526): the
|
|
142
|
+
# extra names only silence `call.undefined-method` on calls that raise. The walk reaches a call in
|
|
143
|
+
# control flow, and one in a `class_eval`-family block under that block's receiver. It does not reach
|
|
144
|
+
# one in an ordinary block, an `END` body or a `class << self` body.
|
|
145
|
+
def extends_self?(call)
|
|
146
|
+
call?(call) && bare?(call)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# sig-gen's reading, over one statement list. Yields every statement except a bare call, with
|
|
150
|
+
# whether a bare call came before it in this list or `active` was already true on entry. The
|
|
151
|
+
# generator passes the enclosing list's answer into an `if`, a block or a `begin`, but not into a
|
|
152
|
+
# nested class, module or `class << self`. A bare call in a nested list covers only that list's
|
|
153
|
+
# later statements. Nothing resets it, and a named call is not a directive at all.
|
|
154
|
+
def each_sig_gen_statement(statements, active)
|
|
155
|
+
statements.each do |stmt|
|
|
156
|
+
if sig_gen_directive?(stmt)
|
|
157
|
+
active = true
|
|
158
|
+
next
|
|
159
|
+
end
|
|
160
|
+
yield stmt, active
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# A bare receiverless call, as sig-gen's statement-level directive.
|
|
165
|
+
def sig_gen_directive?(node)
|
|
166
|
+
node.is_a?(Prism::CallNode) && call?(node) && bare?(node)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Whether sig-gen renders a def as `def self?.name`: an instance-side def under an active directive,
|
|
170
|
+
# in a class or a module alike. `kind` is `:singleton` for `def self.x` and a `class << self` body.
|
|
171
|
+
def sig_gen_module_function?(active, kind)
|
|
172
|
+
active && kind == :instance
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# The prescan's call arm.
|
|
176
|
+
def prescan_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
177
|
+
if bare?(node)
|
|
178
|
+
offsets << node.location.start_offset
|
|
179
|
+
else
|
|
180
|
+
owner = qualified_prefix.empty? ? nil : qualified_prefix.join("::")
|
|
181
|
+
node.arguments&.arguments&.each do |arg|
|
|
182
|
+
if arg.is_a?(Prism::DefNode)
|
|
183
|
+
kind = ScopeIndexer.def_singleton?(arg, qualified_prefix, in_singleton_class) ? :singleton : :both
|
|
184
|
+
ranges << [arg.location.start_offset, arg.location.end_offset, arg.name, kind, owner]
|
|
185
|
+
elsif (name = symbol_argument_name(arg))
|
|
186
|
+
ranges << [node.location.start_offset, node.location.end_offset, name, :singleton, owner]
|
|
187
|
+
else
|
|
188
|
+
prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
return unless node.block
|
|
193
|
+
|
|
194
|
+
prescan_deferred(node.block, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# The prescan's eval arm: only the call's receiver and arguments keep this module's context.
|
|
198
|
+
def prescan_eval_call(node, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
199
|
+
prescan_deferred(node.receiver, qualified_prefix, in_singleton_class, offsets, ranges) if node.receiver
|
|
200
|
+
node.arguments&.arguments&.each do |arg|
|
|
201
|
+
prescan_deferred(arg, qualified_prefix, in_singleton_class, offsets, ranges)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# The Symbol a `:name` or `"name"` literal argument names, or nil.
|
|
206
|
+
def symbol_argument_name(arg)
|
|
207
|
+
arg.unescaped.to_sym if arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
private_class_method :prescan_call, :prescan_eval_call, :symbol_argument_name
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -108,31 +108,36 @@ module Rigor
|
|
|
108
108
|
# `index_targets` maps each `Prism::IndexTargetNode` to the value it stores; {#apply_to}
|
|
109
109
|
# binds nothing for it.
|
|
110
110
|
Result = Data.define(:types, :optimistic, :ivars, :optimistic_ivars, :index_targets) do
|
|
111
|
-
def initialize(types:, optimistic:, ivars: NO_BINDINGS, optimistic_ivars: NO_NAMES,
|
|
112
|
-
index_targets: NO_BINDINGS)
|
|
111
|
+
def initialize(types:, optimistic:, ivars: NO_BINDINGS, optimistic_ivars: NO_NAMES, index_targets: NO_BINDINGS)
|
|
113
112
|
super
|
|
114
113
|
end
|
|
115
114
|
|
|
116
|
-
# Binds every name into `scope`
|
|
117
|
-
# `Scope#with_local` / `Scope#with_ivar` drop any mark the name carried before. `miss` is what
|
|
118
|
-
# each marked slot answers on a miss (issue #1302): `nil` for every mark the binder makes itself —
|
|
119
|
-
# a short array pads with `nil`, and a `nil` destructures to `nil` — so only a caller whose
|
|
120
|
-
# right-hand side a miss can make something else passes another answer
|
|
121
|
-
# ({OptimisticOrigin.destructuring_miss}).
|
|
115
|
+
# Binds every name into `scope` with its mark ({MultiTargetBinder.apply_bindings}).
|
|
122
116
|
def apply_to(scope, miss: nil)
|
|
123
|
-
|
|
124
|
-
bound = ivars.reduce(bound) { |acc, (name, type)| acc.with_ivar(name, type) }
|
|
125
|
-
bound = optimistic.reduce(bound) do |acc, name|
|
|
126
|
-
acc.with_optimistic_local(name, OptimisticOrigin::IMPLICITLY_RETURNS_NIL, miss: miss)
|
|
127
|
-
end
|
|
128
|
-
optimistic_ivars.reduce(bound) do |acc, name|
|
|
129
|
-
acc.with_optimistic_ivar(name, OptimisticOrigin::IMPLICITLY_RETURNS_NIL, miss: miss)
|
|
130
|
-
end
|
|
117
|
+
MultiTargetBinder.apply_bindings(scope, types, optimistic, ivars:, optimistic_ivars:, miss:)
|
|
131
118
|
end
|
|
132
119
|
end
|
|
133
120
|
|
|
134
121
|
module_function
|
|
135
122
|
|
|
123
|
+
# The one place bindings and their optimistic marks are applied, shared by {Result#apply_to} and
|
|
124
|
+
# `BlockParameterBinder#bind_onto`. Every name is bound into the scope `bound` and the mark recorded
|
|
125
|
+
# after the binding, since `Scope#with_local` / `Scope#with_ivar` drop any mark the name carried
|
|
126
|
+
# before; the answer is the bound scope. `miss` is what each marked slot answers on a miss (issue
|
|
127
|
+
# #1302): `nil` for every mark the binder makes itself — a short array pads with `nil`, and a `nil`
|
|
128
|
+
# destructures to `nil` — so only a caller whose right-hand side a miss can make something else passes
|
|
129
|
+
# another answer ({OptimisticOrigin.destructuring_miss}). It loops with `each`, since `reduce`
|
|
130
|
+
# allocates even over the empty collections most of the four are
|
|
131
|
+
# ([#1504](https://github.com/rigortype/rigor/issues/1504)).
|
|
132
|
+
def apply_bindings(bound, types, optimistic, ivars: NO_BINDINGS, optimistic_ivars: NO_NAMES, miss: nil)
|
|
133
|
+
types.each { |name, type| bound = bound.with_local(name, type) }
|
|
134
|
+
ivars.each { |name, type| bound = bound.with_ivar(name, type).without_ivar_guard(name) }
|
|
135
|
+
origin = OptimisticOrigin::IMPLICITLY_RETURNS_NIL
|
|
136
|
+
optimistic.each { |name| bound = bound.with_optimistic_local(name, origin, miss: miss) }
|
|
137
|
+
optimistic_ivars.each { |name| bound = bound.with_optimistic_ivar(name, origin, miss: miss) }
|
|
138
|
+
bound
|
|
139
|
+
end
|
|
140
|
+
|
|
136
141
|
# @param rhs_type — type of the right-hand side
|
|
137
142
|
# @param scope — the scope the destructure is evaluated in, which answers the `to_ary`
|
|
138
143
|
# question for a class outside the core list; without one only the core list wraps.
|