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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# ADR-119 WD3 (errata 2026-10-08, PR C2-a) — the singleton side's positional hook decline. A hook's edge on
|
|
6
|
+
# the singleton side is recorded on no includer, so no chain state carries it (WD3); what the read CAN say
|
|
7
|
+
# is where such an edge could land. A hook touches the singleton chain of a class `K` in two ways only:
|
|
8
|
+
#
|
|
9
|
+
# - (U1) `K.extend(X)`, `class_methods do`, a concern's `ClassMethods`: Ruby inserts `X` after `#<Class:K>`
|
|
10
|
+
# and before `#<Class:K.superclass>`, inside `K`'s own level;
|
|
11
|
+
# - (U2) `included do def self.x end`, `define_singleton_method`, `singleton_class.class_eval`: a definition ON
|
|
12
|
+
# `#<Class:K>`, at the level's head.
|
|
13
|
+
#
|
|
14
|
+
# So a hook at level `i` cannot move a definer at an index at or before the level's head, and a definer found
|
|
15
|
+
# at `#<Class:K_j>` itself, written in `K_j`'s own body, is immune to every hook of levels `j` and deeper. The
|
|
16
|
+
# read's last candidate gives a BOUND: its index when it is that class entry and its `def` was written in
|
|
17
|
+
# `K_j`'s body; one past it for an extended module's entry, or for a copy the extends fold put on `K_j`
|
|
18
|
+
# (the `def`'s recorded nesting does not start with `K_j`), and every entry when nothing answers. The levels
|
|
19
|
+
# tested are those that start before the bound and do not end before `from`. The read declines when a tested
|
|
20
|
+
# level's own instance level (the class, its prepends and includes with their closures) or its singleton
|
|
21
|
+
# segment (the class's singleton and its extends' closures) holds a HOOK-CAPABLE entry, or when a level
|
|
22
|
+
# deeper than the shallowest tested level is hook-capable by the same test: its `inherited` defines on
|
|
23
|
+
# every subclass's singleton, and it need not be the class's own `def`. A deeper external superclass RBS
|
|
24
|
+
# does not know counts, its `inherited` being unknown by construction.
|
|
25
|
+
#
|
|
26
|
+
# Hook-capable, for a project entry or a declared module the chain holds as external (a candidate of its
|
|
27
|
+
# spelling is a `discovered_class_sources`, `discovered_classes`, `discovered_includes` or
|
|
28
|
+
# `discovered_extends` key; an ambiguous spelling tests every declared candidate and any capable one
|
|
29
|
+
# declines): lists `"*"` on either side, records a {Scope::ResolutionChain::Relevance::HOOKS} name on
|
|
30
|
+
# either side or as an envelope key, or extends `ActiveSupport::Concern`. An undeclared external is capable
|
|
31
|
+
# unless RBS knows it (WD2(i)'s limit: an RBS-known external is clean). The dynamic mark is not a hook signal.
|
|
32
|
+
# The verdict does not depend on the name asked, so it is memoised per entry in the chain's flavor bucket
|
|
33
|
+
# with the ADR-46 edges it read (a class edge and the negative class edge on the unqualified name per tested
|
|
34
|
+
# project entry, the negative class edge on the spelling's last segment and the candidates' class edges per
|
|
35
|
+
# tested external), replayed on every call while a recording is active.
|
|
36
|
+
#
|
|
37
|
+
# Known remainders, pinned in `spec/integration/definer_resolution_witness_spec.rb`: `class << self; prepend
|
|
38
|
+
# P` is recorded as an `extend` (the chain places `P` after the singleton, so an own `def self.x` is trusted
|
|
39
|
+
# against it); a concern is capable for every name; an own `def self.x` is trusted against its own level's
|
|
40
|
+
# U2 hooks and same-class redefinitions; a `class Class; def inherited` monkeypatch is unseen; and a
|
|
41
|
+
# superclass that only `extend`s adds two forks, so every read on its subclasses declines.
|
|
42
|
+
module SingletonHookDecline
|
|
43
|
+
WILDCARD = "*"
|
|
44
|
+
CONCERN = "ActiveSupport::Concern"
|
|
45
|
+
SIDES = %i[include extend].freeze
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
# True when a hook may have placed a definer of `method_name` ahead of the read's last candidate on the
|
|
50
|
+
# singleton `chain` (the candidate set `hits`, read from `from`).
|
|
51
|
+
def decline?(scope, chain, method_name, from, hits)
|
|
52
|
+
bound = bound_of(scope, chain, method_name, hits.last)
|
|
53
|
+
tested = tested_levels(chain, from, bound)
|
|
54
|
+
return false if tested.empty?
|
|
55
|
+
return true if chain.truncated?
|
|
56
|
+
|
|
57
|
+
context = Context.new(scope, chain.flavor)
|
|
58
|
+
tested.any? { |level| context.level_capable?(chain, level) } || context.deeper_hook?(chain, tested.first)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def bound_of(scope, chain, method_name, hit)
|
|
62
|
+
return chain.entries.size if hit.nil?
|
|
63
|
+
|
|
64
|
+
index = hit.index
|
|
65
|
+
entry = chain.entries[index]
|
|
66
|
+
# A singleton project entry on a singleton chain is always a level's class entry.
|
|
67
|
+
own = !entry.external? && entry.side == :singleton && def_head(scope, entry.name, method_name) == entry.name
|
|
68
|
+
own ? index : index + 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# What a class entry's answer is when its `def` was written in another module's body: `:copy` when that
|
|
72
|
+
# module is on the same level's singleton segment (the extends fold copied it there; the copy is not where
|
|
73
|
+
# Ruby finds the method — the module's own entry is, and an entry between them may answer first — so the
|
|
74
|
+
# read asks again past it), `:stray` when it is not (the fold and the chain resolved the `extend`'s name to
|
|
75
|
+
# different modules, `extend X` inside `module A` with both `X` and `A::X` declared; or the `def` is not the
|
|
76
|
+
# node that module's instance table holds, a `def C.x` written in its body: the read declines), and
|
|
77
|
+
# nil otherwise (the class's own `def`, no `def`, or one with no recorded nesting).
|
|
78
|
+
def copy_kind(scope, chain, method_name, hit)
|
|
79
|
+
entry = chain.entries[hit.index]
|
|
80
|
+
return nil if entry.external? || entry.side != :singleton
|
|
81
|
+
|
|
82
|
+
head = def_head(scope, entry.name, method_name)
|
|
83
|
+
return nil if head.nil? || head == entry.name
|
|
84
|
+
|
|
85
|
+
level = chain.level_starts.rindex { |start| start <= hit.index }
|
|
86
|
+
return :stray if level.nil? || level >= chain.level_count
|
|
87
|
+
|
|
88
|
+
found = chain.level_entries(level).any? { |candidate| candidate.side == :instance && candidate.name == head }
|
|
89
|
+
found && copied_from?(scope, head, method_name, entry.name) ? :copy : :stray
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Whether the class's singleton `def` is the very node `head`'s instance table holds, the one the fold
|
|
93
|
+
# copied: a receiverless `def`. A `def C.x` or `class << C; def x; end` written inside `head`'s body has the
|
|
94
|
+
# same nesting head and is the class's own, not a copy. (The walk records a `def C.x` written in `module X`
|
|
95
|
+
# as `X#x` too, so identity alone does not tell it from a copy; its receiver does.)
|
|
96
|
+
def copied_from?(scope, head, method_name, owner)
|
|
97
|
+
node = scope.singleton_def_for(owner, method_name)
|
|
98
|
+
return false if node.nil? || !node.receiver.nil?
|
|
99
|
+
|
|
100
|
+
scope.user_def_for(head, method_name).equal?(node)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The innermost `Module.nesting` entry `owner`'s singleton `def` of the name was written in, or nil.
|
|
104
|
+
def def_head(scope, owner, method_name)
|
|
105
|
+
node = scope.singleton_def_for(owner, method_name)
|
|
106
|
+
return nil if node.nil?
|
|
107
|
+
|
|
108
|
+
nesting = scope.discovery.discovered_def_nestings[node] || DefNodeResolver.rehydrated_nesting(node)
|
|
109
|
+
nesting&.first
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def tested_levels(chain, from, bound)
|
|
113
|
+
starts = chain.level_starts
|
|
114
|
+
size = chain.entries.size
|
|
115
|
+
(0...chain.level_count).select { |level| starts[level] < bound && (starts[level + 1] || size) >= from }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The verdicts' working state: the scope, the flavor and the memo the verdicts share.
|
|
119
|
+
class Context
|
|
120
|
+
def initialize(scope, flavor)
|
|
121
|
+
@scope = scope
|
|
122
|
+
@flavor = flavor
|
|
123
|
+
@memo = Scope::ResolutionChain.hook_memo(scope, flavor)
|
|
124
|
+
@recording = Analysis::DependencyRecorder.active?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A tested level: its singleton segment, and its class's own instance level.
|
|
128
|
+
def level_capable?(chain, level)
|
|
129
|
+
return true if chain.level_entries(level).any? { |entry| capable?(entry) }
|
|
130
|
+
|
|
131
|
+
class_name = chain.level_classes[level]
|
|
132
|
+
!class_name.nil? && instance_level_capable?(class_name)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A level deeper than `level` whose `inherited` can define on every subclass's singleton: one that is
|
|
136
|
+
# hook-capable as a tested level is. Its class records `inherited` (own, folded, or a literal
|
|
137
|
+
# `define_method`), or an entry of its own instance level or singleton segment can define it unseen (a
|
|
138
|
+
# concern's `class_methods do def inherited`, a hook that extends the class with a module defining it),
|
|
139
|
+
# or it is an external superclass that is hook-capable.
|
|
140
|
+
def deeper_hook?(chain, level)
|
|
141
|
+
((level + 1)...chain.level_count).any? { |deeper| level_capable?(chain, deeper) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def capable?(entry)
|
|
145
|
+
return verdict(:project, entry.name) { project_capable?(entry.name) } unless entry.external?
|
|
146
|
+
|
|
147
|
+
verdict(:external, entry) { external_capable?(entry) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def instance_level_capable?(class_name)
|
|
153
|
+
verdict(:instance_level, class_name) do |edges|
|
|
154
|
+
chain = Scope::ResolutionChain.for(@scope, class_name, :instance, @flavor)
|
|
155
|
+
next true if chain.truncated?
|
|
156
|
+
|
|
157
|
+
entries = chain.level_count.zero? ? chain.entries : chain.level_entries(0)
|
|
158
|
+
entries.any? do |entry|
|
|
159
|
+
edges.concat(entry_edges(entry))
|
|
160
|
+
capable?(entry)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The memoised `[verdict, edges]` of one test, its edges replayed while a recording is active.
|
|
166
|
+
def verdict(kind, key)
|
|
167
|
+
verdict, edges = @memo[[kind, key]] ||= begin
|
|
168
|
+
edges = []
|
|
169
|
+
value = yield(edges)
|
|
170
|
+
[value ? true : false, edges.concat(own_edges(kind, key)).uniq.freeze].freeze
|
|
171
|
+
end
|
|
172
|
+
replay(edges) if @recording
|
|
173
|
+
verdict
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def own_edges(kind, key)
|
|
177
|
+
return entry_edges(key) if kind == :external
|
|
178
|
+
return EMPTY_EDGES if kind == :instance_level
|
|
179
|
+
|
|
180
|
+
[[:class, key], [:missing, key.to_s.split("::").last]]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def entry_edges(entry)
|
|
184
|
+
return [[:class, entry.name], [:missing, entry.last_segment]] unless entry.external?
|
|
185
|
+
|
|
186
|
+
entry.candidates.map { |candidate| [:class, candidate] } << [:missing, entry.raw.to_s.split("::").last]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def replay(edges)
|
|
190
|
+
edges.each do |kind, value|
|
|
191
|
+
case kind
|
|
192
|
+
when :class then Scope::ResolutionChain.record_class(@scope, value)
|
|
193
|
+
when :missing then Analysis::DependencyRecorder.read_missing(:class, value)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def project_capable?(name)
|
|
199
|
+
sides = @scope.discovery.unpositioned_mixins[name]
|
|
200
|
+
return true if sides && SIDES.any? { |side| sides[side]&.include?(WILDCARD) }
|
|
201
|
+
|
|
202
|
+
return true if records_hook?(name)
|
|
203
|
+
|
|
204
|
+
extends_concern?(name)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Whether `name` records a hook name on either side, or in its envelope table: a `def self.inherited`, a
|
|
208
|
+
# module's instance `def inherited` (a hook once extended), `singleton_class.define_method(:inherited)`
|
|
209
|
+
# (recorded as an instance method) and `define_singleton_method(:inherited)` (recorded only as an opaque
|
|
210
|
+
# envelope, the name-literal evidence `ScopeIndexer#record_surface_evidence` files).
|
|
211
|
+
def records_hook?(name)
|
|
212
|
+
envelopes = @scope.parameter_envelopes_of(name)
|
|
213
|
+
Scope::ResolutionChain::Relevance::HOOKS.any? do |hook|
|
|
214
|
+
@scope.discovered_method?(name, hook, :singleton) || @scope.discovered_method?(name, hook, :instance) ||
|
|
215
|
+
envelopes.key?([:singleton, hook]) || envelopes.key?([:instance, hook])
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Whether `name`'s own singleton segment (its `extend`s, as the chain places them) holds
|
|
220
|
+
# `ActiveSupport::Concern` — the concern shape's only signal, a framework name the follow-up ADR moves
|
|
221
|
+
# behind the plugin API (WD3, Q11).
|
|
222
|
+
def extends_concern?(name)
|
|
223
|
+
chain = Scope::ResolutionChain.for(@scope, name, :singleton, @flavor)
|
|
224
|
+
entries = chain.level_count.zero? ? chain.entries : chain.level_entries(0)
|
|
225
|
+
entries.any? do |entry|
|
|
226
|
+
entry.external? ? entry.candidates.include?(CONCERN) : entry.name == CONCERN
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# A declared module the chain holds as external is tested as that module; an undeclared one is capable
|
|
231
|
+
# unless RBS knows it.
|
|
232
|
+
def external_capable?(entry)
|
|
233
|
+
declared = entry.candidates.select { |candidate| declared?(candidate) }
|
|
234
|
+
return declared.any? { |name| verdict(:project, name) { project_capable?(name) } } unless declared.empty?
|
|
235
|
+
|
|
236
|
+
entry.candidates.none? { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: @scope) }
|
|
237
|
+
rescue StandardError
|
|
238
|
+
true
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def declared?(name)
|
|
242
|
+
discovery = @scope.discovery
|
|
243
|
+
discovery.discovered_class_sources.key?(name) || discovery.discovered_classes.key?(name) ||
|
|
244
|
+
discovery.discovered_includes.key?(name) || discovery.discovered_extends.key?(name)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
private_constant :Context
|
|
248
|
+
|
|
249
|
+
EMPTY_EDGES = [].freeze
|
|
250
|
+
private_constant :EMPTY_EDGES
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -9,6 +9,7 @@ require_relative "../source/node_walker"
|
|
|
9
9
|
require_relative "../source/node_children"
|
|
10
10
|
require_relative "../source/constant_path"
|
|
11
11
|
require_relative "anonymous_meta_class"
|
|
12
|
+
require_relative "definer_resolution"
|
|
12
13
|
require_relative "block_repetition"
|
|
13
14
|
require_relative "block_parameter_binder"
|
|
14
15
|
require_relative "body_fixpoint"
|
|
@@ -272,10 +273,16 @@ module Rigor
|
|
|
272
273
|
# {OperandWalk} rooted inside an operand still records its later operands ({#walk_recorder}).
|
|
273
274
|
# @param operand_types — the later operands' own values ({OperandWalk#types}) for the evaluator that runs a
|
|
274
275
|
# call from the scope its operands left, read by {#type_operand}.
|
|
276
|
+
# @param opaque_class — true while the walk is inside the body of a class nothing names (issue #1518:
|
|
277
|
+
# a declined `class self::X`, a parse-error header), where `class_context` is empty without the code
|
|
278
|
+
# being at the top level. A `def` (or `class << self`) there runs under an untyped `self` instead of
|
|
279
|
+
# the top level's nil one, so its implicit-self calls answer `Dynamic` and no top-level rule fires.
|
|
275
280
|
def initialize(scope:, tracer: nil, on_enter: nil, class_context: [].freeze, # rubocop:disable Metrics/ParameterLists
|
|
276
281
|
lexical_nesting: EMPTY_NESTING, converged_loop_recording: false, next_scope_sink: nil,
|
|
277
|
-
operand_scope: nil, in_operand: false, operand_recorder: nil, operand_types: nil
|
|
282
|
+
operand_scope: nil, in_operand: false, operand_recorder: nil, operand_types: nil,
|
|
283
|
+
opaque_class: false)
|
|
278
284
|
@scope = scope
|
|
285
|
+
@opaque_class = opaque_class
|
|
279
286
|
@tracer = tracer
|
|
280
287
|
@on_enter = on_enter
|
|
281
288
|
@class_context = class_context.freeze
|
|
@@ -625,7 +632,8 @@ module Rigor
|
|
|
625
632
|
# lands.
|
|
626
633
|
def eval_ivar_write(node)
|
|
627
634
|
rhs_type, post_rhs = sub_eval(node.value, scope)
|
|
628
|
-
|
|
635
|
+
# Issue #1446 — a write ends a class guard's narrowing of the variable, as it does a global's.
|
|
636
|
+
bound = post_rhs.with_ivar(node.name, rhs_type).without_ivar_guard(node.name)
|
|
629
637
|
bound = bound.with_ivar_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
|
|
630
638
|
cause = optimistic_rhs_origin(node.value, post_rhs)
|
|
631
639
|
bound = bound.with_optimistic_ivar(node.name, cause, miss: optimistic_rhs_miss(node.value, post_rhs)) if cause
|
|
@@ -806,7 +814,8 @@ module Rigor
|
|
|
806
814
|
end
|
|
807
815
|
|
|
808
816
|
def rebind_variable(target_scope, kind, name, type)
|
|
809
|
-
target_scope.public_send(VAR_KIND_BUILDERS.fetch(kind), name, type)
|
|
817
|
+
bound = target_scope.public_send(VAR_KIND_BUILDERS.fetch(kind), name, type)
|
|
818
|
+
kind == :ivar ? bound.without_ivar_guard(name) : bound
|
|
810
819
|
end
|
|
811
820
|
|
|
812
821
|
def compound_result_type(current, rhs, operator)
|
|
@@ -2431,6 +2440,12 @@ module Rigor
|
|
|
2431
2440
|
case index_node
|
|
2432
2441
|
when Prism::LocalVariableTargetNode
|
|
2433
2442
|
scope.with_local(index_node.name, element_type)
|
|
2443
|
+
when Prism::InstanceVariableTargetNode
|
|
2444
|
+
# Issue #1446 — `for @io in xs` writes the ivar each iteration, which ends a class guard's narrowing of it,
|
|
2445
|
+
# and `for $out in xs` the global, which ends any guard's narrowing of it.
|
|
2446
|
+
scope.with_ivar(index_node.name, element_type).without_ivar_guard(index_node.name)
|
|
2447
|
+
when Prism::GlobalVariableTargetNode
|
|
2448
|
+
scope.with_global(index_node.name, element_type)
|
|
2434
2449
|
when Prism::IndexTargetNode
|
|
2435
2450
|
widen_index_target(index_node, element_type, scope, type_scope: scope)
|
|
2436
2451
|
when Prism::MultiTargetNode
|
|
@@ -2698,13 +2713,31 @@ module Rigor
|
|
|
2698
2713
|
# that half rather than this one deriving it from the frames.
|
|
2699
2714
|
def eval_class_or_module(node)
|
|
2700
2715
|
path = node.constant_path
|
|
2701
|
-
|
|
2716
|
+
name = Source::ConstantPath.qualified_name(path)
|
|
2717
|
+
# A header Prism recovered without a constant (`class foo`, a parse error) names no class: its body
|
|
2718
|
+
# runs under an untyped `self` and no class frame, so nothing is filed under an empty name.
|
|
2719
|
+
return eval_unnameable_class_body(node) if name.nil?
|
|
2720
|
+
# `class self::X` the indexer declined (opaque `self`, e.g. `REGISTRY.first.class_eval`) is not the
|
|
2721
|
+
# lexical `X` that `qualified_name` renders; a nameable `self::` header is in the identity table.
|
|
2722
|
+
return eval_unnameable_class_body(node) if declined_self_header?(path)
|
|
2723
|
+
|
|
2724
|
+
frame = ClassFrame.new(name: name, singleton: false)
|
|
2702
2725
|
new_context = Source::ConstantPath.rooted?(path) ? [frame] : @class_context + [frame]
|
|
2703
2726
|
body_type, _body_scope = eval_class_body(node, new_context,
|
|
2704
2727
|
Source::ConstantPath.pushed_nesting(@lexical_nesting, path))
|
|
2705
2728
|
[body_type, scope]
|
|
2706
2729
|
end
|
|
2707
2730
|
|
|
2731
|
+
def declined_self_header?(path)
|
|
2732
|
+
path.is_a?(Prism::ConstantPathNode) && path.parent.is_a?(Prism::SelfNode) &&
|
|
2733
|
+
!scope.declared_types.key?(path)
|
|
2734
|
+
end
|
|
2735
|
+
|
|
2736
|
+
def eval_unnameable_class_body(node)
|
|
2737
|
+
body_type, _body_scope = eval_class_body(node, [], @lexical_nesting, opaque_self: true)
|
|
2738
|
+
[body_type, scope]
|
|
2739
|
+
end
|
|
2740
|
+
|
|
2708
2741
|
# `class << expr; body; end`. When `expr` is `self`, the body defines class methods on the immediate enclosing
|
|
2709
2742
|
# class — the innermost frame flips to `singleton: true` so a nested `def foo` resolves through `singleton_method`
|
|
2710
2743
|
# rather than `instance_method`. For non-`self` expressions we cannot statically resolve the receiver, so we keep
|
|
@@ -5175,7 +5208,7 @@ module Rigor
|
|
|
5175
5208
|
|
|
5176
5209
|
# ----- def/class helpers -----
|
|
5177
5210
|
|
|
5178
|
-
def eval_class_body(node, new_context, new_nesting = @lexical_nesting)
|
|
5211
|
+
def eval_class_body(node, new_context, new_nesting = @lexical_nesting, opaque_self: false)
|
|
5179
5212
|
return [Type::Combinator.constant_of(nil), scope] if node.body.nil?
|
|
5180
5213
|
|
|
5181
5214
|
# Class/module bodies run in a fresh scope: the outer scope's locals are NOT visible inside `class Foo; ...
|
|
@@ -5183,7 +5216,10 @@ module Rigor
|
|
|
5183
5216
|
# A-engine: `self` inside a class body is the class object itself, so we set `self_type` to
|
|
5184
5217
|
# `Singleton[<qualified>]`.
|
|
5185
5218
|
fresh = build_fresh_body_scope
|
|
5186
|
-
|
|
5219
|
+
# Issue #1518 — `class << self` inside an unnameable class body keeps that body's opacity: it opens the
|
|
5220
|
+
# singleton of a class nothing names, and its empty frame stack is not the top level.
|
|
5221
|
+
opaque_self ||= node.is_a?(Prism::SingletonClassNode) && @opaque_class && new_context.empty?
|
|
5222
|
+
body_self = opaque_self ? Type::Combinator.untyped : self_type_for_class_body(new_context)
|
|
5187
5223
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
5188
5224
|
# Issue #963 — `self` in a `class << ...` body is the SINGLETON class, which shares the `Singleton[X]`
|
|
5189
5225
|
# carrier a `class X` body gets. The mark is the only thing that tells the two apart downstream, and a
|
|
@@ -5191,7 +5227,7 @@ module Rigor
|
|
|
5191
5227
|
fresh = fresh.with_singleton_class_body(node.is_a?(Prism::SingletonClassNode))
|
|
5192
5228
|
fresh = fresh.with_match_frame(node.body)
|
|
5193
5229
|
fresh = stamp_nesting(fresh, new_nesting)
|
|
5194
|
-
sub_eval(node.body, fresh, class_context: new_context, lexical_nesting: new_nesting)
|
|
5230
|
+
sub_eval(node.body, fresh, class_context: new_context, lexical_nesting: new_nesting, opaque_class: opaque_self)
|
|
5195
5231
|
end
|
|
5196
5232
|
|
|
5197
5233
|
def build_method_entry_scope(def_node) # rubocop:disable Metrics/AbcSize
|
|
@@ -5410,7 +5446,9 @@ module Rigor
|
|
|
5410
5446
|
# Returns nil for top-level defs that have no enclosing class.
|
|
5411
5447
|
def self_type_for_method_body(singleton:)
|
|
5412
5448
|
path = current_class_path
|
|
5413
|
-
|
|
5449
|
+
# Issue #1518 — a `def` in a class nothing names has a receiver, just not one Rigor can type; nil would
|
|
5450
|
+
# read its body as the top level and its implicit-self calls as unresolved top-level calls.
|
|
5451
|
+
return (Type::Combinator.untyped if @opaque_class) if path.nil?
|
|
5414
5452
|
|
|
5415
5453
|
if singleton
|
|
5416
5454
|
Type::Combinator.singleton_of(path)
|
|
@@ -5549,10 +5587,14 @@ module Rigor
|
|
|
5549
5587
|
# `on_enter: nil` evaluates without recording into the per-node scope index — for a pass whose scopes are not
|
|
5550
5588
|
# the ones the index should keep. `next_scope_sink:` is replaced only by {#evaluate_invocation} and
|
|
5551
5589
|
# {#loop_iteration}.
|
|
5552
|
-
|
|
5553
|
-
|
|
5590
|
+
# `opaque_class:` survives only while the frame stack stays empty: a named class opened inside an
|
|
5591
|
+
# unnameable body gets a frame, and its `def`s a nominal `self`, as they did before #1518.
|
|
5592
|
+
def sub_eval(node, with_scope, class_context: @class_context, lexical_nesting: @lexical_nesting, # rubocop:disable Metrics/ParameterLists
|
|
5593
|
+
on_enter: @on_enter, next_scope_sink: @next_scope_sink, operand_recorder: @operand_recorder,
|
|
5594
|
+
opaque_class: @opaque_class && class_context.empty?)
|
|
5554
5595
|
evaluator_at(with_scope, class_context: class_context, lexical_nesting: lexical_nesting, on_enter: on_enter,
|
|
5555
|
-
next_scope_sink: next_scope_sink, operand_recorder: operand_recorder
|
|
5596
|
+
next_scope_sink: next_scope_sink, operand_recorder: operand_recorder,
|
|
5597
|
+
opaque_class: opaque_class).evaluate(node)
|
|
5556
5598
|
end
|
|
5557
5599
|
|
|
5558
5600
|
# An evaluator over `with_scope` that inherits everything else from this one. `operand_scope:` and
|
|
@@ -5560,7 +5602,8 @@ module Rigor
|
|
|
5560
5602
|
# operands left.
|
|
5561
5603
|
def evaluator_at(with_scope, class_context: @class_context, lexical_nesting: @lexical_nesting, # rubocop:disable Metrics/ParameterLists
|
|
5562
5604
|
on_enter: @on_enter, next_scope_sink: @next_scope_sink, operand_scope: nil,
|
|
5563
|
-
in_operand: @in_operand, operand_recorder: @operand_recorder, operand_types: nil
|
|
5605
|
+
in_operand: @in_operand, operand_recorder: @operand_recorder, operand_types: nil,
|
|
5606
|
+
opaque_class: @opaque_class)
|
|
5564
5607
|
StatementEvaluator.new(
|
|
5565
5608
|
scope: with_scope,
|
|
5566
5609
|
tracer: tracer,
|
|
@@ -5572,7 +5615,8 @@ module Rigor
|
|
|
5572
5615
|
operand_scope: operand_scope,
|
|
5573
5616
|
in_operand: in_operand,
|
|
5574
5617
|
operand_recorder: operand_recorder,
|
|
5575
|
-
operand_types: operand_types
|
|
5618
|
+
operand_types: operand_types,
|
|
5619
|
+
opaque_class: opaque_class
|
|
5576
5620
|
)
|
|
5577
5621
|
end
|
|
5578
5622
|
|
|
@@ -5675,6 +5719,12 @@ module Rigor
|
|
|
5675
5719
|
case ref
|
|
5676
5720
|
when Prism::LocalVariableTargetNode
|
|
5677
5721
|
scope.with_local(ref.name, exception_type)
|
|
5722
|
+
when Prism::InstanceVariableTargetNode
|
|
5723
|
+
# Issue #1446 — `rescue => @io` writes the ivar, which ends a class guard's narrowing of it, and `rescue =>
|
|
5724
|
+
# $out` the global, which ends any guard's narrowing of it.
|
|
5725
|
+
scope.with_ivar(ref.name, exception_type).without_ivar_guard(ref.name)
|
|
5726
|
+
when Prism::GlobalVariableTargetNode
|
|
5727
|
+
scope.with_global(ref.name, exception_type)
|
|
5678
5728
|
when Prism::IndexTargetNode
|
|
5679
5729
|
widen_index_target(ref, exception_type, scope, type_scope: scope)
|
|
5680
5730
|
else
|
|
@@ -5818,7 +5868,7 @@ module Rigor
|
|
|
5818
5868
|
def source_decomposition_projection(subject_type, method_name, arg_types, scope)
|
|
5819
5869
|
return nil unless subject_type.is_a?(Type::Nominal)
|
|
5820
5870
|
|
|
5821
|
-
def_node = scope
|
|
5871
|
+
def_node = own_definer_node(scope, subject_type.class_name, method_name)
|
|
5822
5872
|
return nil if def_node.nil?
|
|
5823
5873
|
|
|
5824
5874
|
result = scope.user_method_return(def_node, subject_type, arg_types)
|
|
@@ -5827,6 +5877,22 @@ module Rigor
|
|
|
5827
5877
|
result
|
|
5828
5878
|
end
|
|
5829
5879
|
|
|
5880
|
+
# The `def` node the receiver's OWN class defines `method_name` with, or nil. ADR-119 C2-e (#1615): asked of
|
|
5881
|
+
# `DefinerResolution`, so a slot rebuilt from an ADR-85 seed bundle is resolved to a node and the read files
|
|
5882
|
+
# its ADR-46 edge. UNKNOWN answers nil: an unsettled or forked chain, or a contested slot once ADR-119 C1d
|
|
5883
|
+
# fills the siblings. Conditional or reopened defs are not contested until C1d. A definer further up the
|
|
5884
|
+
# chain (a prepended or included module, a superclass) is not the own class's, so the owner check declines it.
|
|
5885
|
+
def own_definer_node(scope, class_name, method_name)
|
|
5886
|
+
case DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :definer)
|
|
5887
|
+
in DefinerResolution::Known(answer: [node, owner])
|
|
5888
|
+
owner == class_name ? node : nil
|
|
5889
|
+
in DefinerResolution::UNKNOWN # the case/in contract needs one arm per answer
|
|
5890
|
+
nil
|
|
5891
|
+
in DefinerResolution::ABSENT # rubocop:disable Lint/DuplicateBranch -- see above
|
|
5892
|
+
nil
|
|
5893
|
+
end
|
|
5894
|
+
end
|
|
5895
|
+
|
|
5830
5896
|
# A `StructInstance`'s own projection — `Tuple` of its member values for `deconstruct`, `HashShape` of
|
|
5831
5897
|
# its members for `deconstruct_keys` — or nil when the subject is another carrier or the projection would
|
|
5832
5898
|
# be unsound.
|
data/lib/rigor/plugin/base.rb
CHANGED
|
@@ -882,7 +882,7 @@ module Rigor
|
|
|
882
882
|
|
|
883
883
|
before, separator, = file.rpartition("/lib/")
|
|
884
884
|
root = separator.empty? ? File.dirname(file) : before
|
|
885
|
-
relative.map { |rel| File.
|
|
885
|
+
relative.map { |rel| File.absolute_path(rel, root) }
|
|
886
886
|
end
|
|
887
887
|
|
|
888
888
|
# ADR-28 — the path-scoped method-protocol contracts this plugin contributes. Defaults to the
|
|
@@ -940,6 +940,17 @@ module Rigor
|
|
|
940
940
|
# changed. The producer id is auto-prefixed `plugin.<manifest.id>.` per ADR-7 § "Slice 6-C" so plugin
|
|
941
941
|
# caches stay sandboxed from built-in producers.
|
|
942
942
|
#
|
|
943
|
+
# #1558 — whichever way the value arrives, the boundary ends up holding every row of the producer's
|
|
944
|
+
# dependency descriptor. A hit ran no block and read nothing, so the served entry's stored rows are
|
|
945
|
+
# replayed into it ({IoBoundary#replay}); a miss read its inputs itself, and replaying the descriptor it
|
|
946
|
+
# just recorded adds the evaluated `watch:` rows, the only rows the block did not record. The boundary
|
|
947
|
+
# is per plugin and accumulates for the run, so the rows reach the ADR-45 run-result descriptor
|
|
948
|
+
# (`Runner#build_run_dependency_descriptor`) and the descriptor of every producer of this plugin
|
|
949
|
+
# computed after it, whether its block asked this one or it composes a value this one returned
|
|
950
|
+
# earlier. Without the replay a producer recomputed after a served one stored no row for the served
|
|
951
|
+
# value's inputs, and neither did the run: rigor-activerecord's `:model_index`, recomputed after a
|
|
952
|
+
# model edit while `:schema_table` hit, kept its stale columns after `db/schema.rb` changed.
|
|
953
|
+
#
|
|
943
954
|
# When `services.cache_store` is `nil` (e.g. CLI `--no-cache`), the callable bypasses the cache and
|
|
944
955
|
# runs the producer block every time — same semantics as the v0.0.9 cache surface for built-in
|
|
945
956
|
# producers.
|
|
@@ -974,11 +985,9 @@ module Rigor
|
|
|
974
985
|
generation_cap: producer[:generation_cap],
|
|
975
986
|
params: params,
|
|
976
987
|
serialize: pair_serializer(producer[:serialize]),
|
|
977
|
-
deserialize: pair_deserializer(producer[:deserialize])
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
[value, producer_dependency_descriptor(producer)]
|
|
981
|
-
end
|
|
988
|
+
deserialize: pair_deserializer(producer[:deserialize]),
|
|
989
|
+
on_hit: ->(dependencies) { io_boundary.replay(dependencies) }
|
|
990
|
+
) { computed_producer_pair(producer, compute) }
|
|
982
991
|
end
|
|
983
992
|
end
|
|
984
993
|
|
|
@@ -1151,7 +1160,7 @@ module Rigor
|
|
|
1151
1160
|
|
|
1152
1161
|
def collect_glob_files(roots, patterns)
|
|
1153
1162
|
matched = roots.flat_map do |root|
|
|
1154
|
-
absolute = File.
|
|
1163
|
+
absolute = File.absolute_path(root.to_s)
|
|
1155
1164
|
next [] unless File.directory?(absolute)
|
|
1156
1165
|
|
|
1157
1166
|
patterns.flat_map { |pattern| Dir.glob(File.join(absolute, pattern.to_s)) }
|
|
@@ -1196,6 +1205,17 @@ module Rigor
|
|
|
1196
1205
|
Cache::Descriptor.compose(auto_built, extra)
|
|
1197
1206
|
end
|
|
1198
1207
|
|
|
1208
|
+
# #1558 — the miss half of {#cache_for}. The block's own reads are in the boundary already, so replaying
|
|
1209
|
+
# the descriptor it just recorded adds the evaluated `watch:` rows, as a hit's replay of the stored
|
|
1210
|
+
# descriptor does; a producer computing around this one then records one row set whichever way this
|
|
1211
|
+
# value arrived.
|
|
1212
|
+
def computed_producer_pair(producer, compute)
|
|
1213
|
+
value = compute.call
|
|
1214
|
+
dependencies = producer_dependency_descriptor(producer)
|
|
1215
|
+
io_boundary.replay(dependencies)
|
|
1216
|
+
[value, dependencies]
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1199
1219
|
# ADR-60 WD3 — the dependency descriptor stored beside the producer's value, built AFTER the block ran
|
|
1200
1220
|
# so every in-block `io_boundary` read is captured, plus the evaluated `watch:` glob rows.
|
|
1201
1221
|
#
|
|
@@ -1224,7 +1244,7 @@ module Rigor
|
|
|
1224
1244
|
Array(tuples).flat_map do |tuple|
|
|
1225
1245
|
roots, *patterns = Array(tuple)
|
|
1226
1246
|
Array(roots).flat_map do |root|
|
|
1227
|
-
absolute = File.
|
|
1247
|
+
absolute = File.absolute_path(root.to_s)
|
|
1228
1248
|
patterns.map { |pattern| Cache::Descriptor::GlobEntry.compute(root: absolute, pattern: pattern.to_s) }
|
|
1229
1249
|
end
|
|
1230
1250
|
end.uniq
|