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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../reflection"
|
|
4
|
+
require_relative "../../inference/definer_resolution"
|
|
4
5
|
require_relative "../../source/parameter_envelope"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
@@ -14,9 +15,10 @@ module Rigor
|
|
|
14
15
|
# Asked in two stages, because every condition past the first can only WITHHOLD a firing:
|
|
15
16
|
#
|
|
16
17
|
# 1. {#owner_envelope} walks the receiver's superclass chain to the first class whose own body, or one of
|
|
17
|
-
# whose project mixins, records the name. Every record at that level must agree —
|
|
18
|
-
#
|
|
19
|
-
# opaque. A call that fits this envelope
|
|
18
|
+
# whose project mixins, records the name. Every record at that level must agree — deliberately wider
|
|
19
|
+
# than Ruby's order, so a mixin whose definition may shadow the class's own, or a load-order-dependent
|
|
20
|
+
# pair (#986), declines instead of answering — and none may be opaque. A call that fits this envelope
|
|
21
|
+
# is silent, and nothing below is consulted.
|
|
20
22
|
# 2. {#authoritative?} runs only for a call that would fire. It declines when a `method_missing` /
|
|
21
23
|
# `respond_to_missing?` hook or a dynamic-surface mark sits anywhere in the chain; when a mixin the
|
|
22
24
|
# project does not declare is not a known RBS module that lacks the name; when a `pre_eval:` patch or
|
|
@@ -24,20 +26,45 @@ module Rigor
|
|
|
24
26
|
# project subclass of the receiver — whose instances the receiver's type admits — records a different
|
|
25
27
|
# or opaque envelope for it.
|
|
26
28
|
#
|
|
29
|
+
# ADR-119 C1b / C2-c — on both sides stage 1 and the subclass check ask `Inference::DefinerResolution`, and a
|
|
30
|
+
# chain it declines (`UNKNOWN`) answers no envelope: where Ruby's order depends on a fact the tables cannot
|
|
31
|
+
# prove, the rule is silent instead of checking the call against master's order (#1570, #1607).
|
|
32
|
+
#
|
|
27
33
|
# Deliberately NOT in scope: a constructor reached through `Class#new` (nothing records `initialize` as
|
|
28
34
|
# `[:singleton, :new]`, so `.new` finds no owner unless the class writes `def self.new` itself); keyword
|
|
29
35
|
# arity (the envelope's required-keyword flag declines, as `compute_arity_envelope` declines an RBS
|
|
30
36
|
# function with required keywords); and a `class_eval` whose receiver is not a constant, which the
|
|
31
37
|
# discovery walk cannot name.
|
|
32
|
-
class SourceArity
|
|
38
|
+
class SourceArity # rubocop:disable Metrics/ClassLength
|
|
33
39
|
MISSING_HOOKS = %i[method_missing respond_to_missing?].freeze
|
|
34
40
|
private_constant :MISSING_HOOKS
|
|
35
41
|
|
|
36
42
|
# One class on the walk: its own name, the project modules its mixins resolve to (transitively through
|
|
37
43
|
# their own `include`s), and the candidate-name lists of the mixins that resolve to no project module.
|
|
44
|
+
#
|
|
45
|
+
# Issue #1570 — the level is a SEGMENT of the receiver's `Scope::ResolutionChain` (`:arity` flavor): the
|
|
46
|
+
# class with the modules Ruby inserts around it before the superclass's entries begin. Ruby skips an
|
|
47
|
+
# `include` of a module the superclass chain already carries, so `class C < Base; include M` where
|
|
48
|
+
# `Base` includes `M` leaves `M` at `Base`'s level; collecting every module a class's own `include`s
|
|
49
|
+
# name put it at `C`'s level instead, where its `def` answered a call `Base#foo` receives.
|
|
38
50
|
Level = Data.define(:class_name, :modules, :externals)
|
|
39
51
|
private_constant :Level
|
|
40
52
|
|
|
53
|
+
# ADR-119 C1b — the answer of a level whose records are not one non-opaque envelope. It equals itself in
|
|
54
|
+
# every world, so two worlds that both reach such a level agree, and no envelope equals it.
|
|
55
|
+
AMBIGUOUS = Object.new.freeze
|
|
56
|
+
private_constant :AMBIGUOUS
|
|
57
|
+
|
|
58
|
+
# What `Inference::DefinerResolution` reads off a candidate: the answer, the entry that gave it, its
|
|
59
|
+
# position on the chain and its side. The same four readers as `DefinerResolution::Hit`, which a call site
|
|
60
|
+
# may not name (the case/in contract spec allows no other reference to the module).
|
|
61
|
+
ArityHit = Data.define(:answer, :owner, :index, :side)
|
|
62
|
+
private_constant :ArityHit
|
|
63
|
+
|
|
64
|
+
# A subclass level the walk cannot read: `subclasses_agree?` reads the nil as a decline.
|
|
65
|
+
NO_LEVELS = [nil].freeze
|
|
66
|
+
private_constant :NO_LEVELS
|
|
67
|
+
|
|
41
68
|
def initialize(scope, method_name, kind)
|
|
42
69
|
@scope = scope
|
|
43
70
|
@method_name = method_name.to_sym
|
|
@@ -53,6 +80,7 @@ module Rigor
|
|
|
53
80
|
def owner_envelope(class_name)
|
|
54
81
|
@owner_entries = []
|
|
55
82
|
@ambiguous = false
|
|
83
|
+
@unknown = false
|
|
56
84
|
envelope, @withheld = Analysis::DependencyRecorder.withhold { walk_to_owner(class_name) }
|
|
57
85
|
envelope
|
|
58
86
|
end
|
|
@@ -67,6 +95,11 @@ module Rigor
|
|
|
67
95
|
# withhold, so none of these needs the file-level class edge the bucket reads would otherwise record.
|
|
68
96
|
def settle_by_definitions
|
|
69
97
|
return if @withheld.nil?
|
|
98
|
+
# ADR-119 C1b — a declined chain (`@unknown`) that some level records the name on is silent only until
|
|
99
|
+
# another file's edit lifts the decline, so its verdict depends on everything the walk read, as an opaque
|
|
100
|
+
# level's does. A decline over a name no project level records is not `@unknown` (see
|
|
101
|
+
# {#decline_unless_recorded_nowhere}): it settles by definitions, as an `ABSENT` read does.
|
|
102
|
+
return settle_by_walk if @unknown
|
|
70
103
|
|
|
71
104
|
@owner_entries.each do |name, kind|
|
|
72
105
|
if kind == :singleton
|
|
@@ -95,20 +128,88 @@ module Rigor
|
|
|
95
128
|
|
|
96
129
|
private
|
|
97
130
|
|
|
131
|
+
# ADR-119 C1b / C2-c, #1570 / #1607 — the nearest level is asked of {Inference::DefinerResolution}, on the
|
|
132
|
+
# receiver's own side: the chain stands only where every candidate level gives one envelope in every
|
|
133
|
+
# world Ruby may have run (a fork, an unproven mixin order, a `possible`-only definer, and on the
|
|
134
|
+
# singleton side a hook that could have put a definer ahead). Where it does not, the answer is NO
|
|
135
|
+
# envelope and `@unknown` — the rule stays silent — instead of master's order, which is what #1570 and
|
|
136
|
+
# #1607 fired on. The levels the walk keeps for {#authoritative?} are the chain's.
|
|
98
137
|
def walk_to_owner(class_name)
|
|
99
138
|
@levels = []
|
|
100
139
|
@passed = []
|
|
101
|
-
|
|
102
|
-
|
|
140
|
+
@recorded = false
|
|
141
|
+
|
|
142
|
+
case Inference::DefinerResolution.resolve(@scope, class_name, @method_name, side,
|
|
143
|
+
question: :arity) { |chain, from| arity_hit(chain, from) }
|
|
144
|
+
in Inference::DefinerResolution::Known(answer: _answer, owner: _owner) then nearest_envelope(class_name)
|
|
145
|
+
in Inference::DefinerResolution::ABSENT then nearest_envelope(class_name) # rubocop:disable Lint/DuplicateBranch
|
|
146
|
+
in Inference::DefinerResolution::UNKNOWN then decline_unless_recorded_nowhere(class_name)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# A declined read of a name no project level records (`arity_hit` found no level that does, in any world it
|
|
151
|
+
# was asked) answers no envelope whether or not it stands, so the walk's reads are not the verdict's
|
|
152
|
+
# dependency: only a `def` appearing can change it, which {#settle_by_definitions} files, as for an
|
|
153
|
+
# `ABSENT` read. It can leave a stale false negative in a warm run, never a firing. Without this, every
|
|
154
|
+
# `Widget.new` (the implicit `Class` tail may answer it) would replay the whole walk as a file-level
|
|
155
|
+
# class edge.
|
|
156
|
+
def decline_unless_recorded_nowhere(class_name)
|
|
157
|
+
nowhere = !@recorded && !arity_chain(class_name).truncated?
|
|
158
|
+
nowhere ? nearest_envelope(class_name) : decline
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The chain's nearest level's envelope. `Known#answer` is that envelope, but {#authoritative?} and
|
|
162
|
+
# {#settle_by_definitions} also read the levels, passed names and owner entries this walk fills, so the
|
|
163
|
+
# answer is not used on its own. Run only once the candidate-set read has answered, so the chain stands
|
|
164
|
+
# for the name.
|
|
165
|
+
def nearest_envelope(class_name) = owner_in(chain_levels(arity_chain(class_name)).first)
|
|
166
|
+
|
|
167
|
+
def decline
|
|
168
|
+
@levels = []
|
|
169
|
+
@passed = []
|
|
170
|
+
@owner_entries = []
|
|
171
|
+
@envelope = nil
|
|
172
|
+
@unknown = true
|
|
173
|
+
nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The first level at or after entry position `from` whose records name the method: its envelope (or
|
|
177
|
+
# {AMBIGUOUS}), the entry that recorded it and that entry's position and side on the chain. On the
|
|
178
|
+
# singleton side the class's own record is a `:singleton` entry and an extended module's an `:instance`
|
|
179
|
+
# one (`owner_entries` names the kind of each).
|
|
180
|
+
def arity_hit(chain, from)
|
|
181
|
+
start = 0
|
|
182
|
+
chain.levels.each_with_index do |raw, index|
|
|
183
|
+
level = Level.new(*raw)
|
|
184
|
+
entries = chain.level_entries(index)
|
|
185
|
+
first = start
|
|
186
|
+
start += entries.size
|
|
187
|
+
next if first < from
|
|
188
|
+
|
|
189
|
+
found = level_envelopes(level)
|
|
190
|
+
next if found.empty?
|
|
191
|
+
|
|
192
|
+
envelope = found.first
|
|
193
|
+
answer = found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope) ? envelope : AMBIGUOUS
|
|
194
|
+
owner, owner_kind = owner_entries(level).first
|
|
195
|
+
position = first + (entries.index { |entry| entry.name == owner && entry.side == owner_kind } || 0)
|
|
196
|
+
@recorded = true
|
|
197
|
+
return ArityHit.new(answer, owner, position, owner_kind)
|
|
198
|
+
end
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def owner_in(levels)
|
|
203
|
+
levels.each do |level|
|
|
103
204
|
@levels << level
|
|
104
205
|
found = level_envelopes(level)
|
|
105
206
|
if found.empty?
|
|
106
|
-
@passed.concat([
|
|
207
|
+
@passed.concat([level.class_name] + level.modules)
|
|
107
208
|
next
|
|
108
209
|
end
|
|
109
210
|
|
|
110
211
|
envelope = found.first
|
|
111
|
-
@owner_entries
|
|
212
|
+
@owner_entries |= owner_entries(level)
|
|
112
213
|
unless found.all?(envelope) && !Source::ParameterEnvelope.opaque?(envelope)
|
|
113
214
|
@ambiguous = true
|
|
114
215
|
return nil
|
|
@@ -155,56 +256,30 @@ module Rigor
|
|
|
155
256
|
@scope.discovered_classes.key?(name) || Rigor::Reflection.rbs_class_known?(name, scope: @scope)
|
|
156
257
|
end
|
|
157
258
|
|
|
158
|
-
# False when the walk stopped at the ADR-41 budget rather than
|
|
159
|
-
# exhaustion is uncertainty, and every caller reads it as a reason to decline.
|
|
160
|
-
def walked_whole_chain?(class_name)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return false if seen.size >= Scope::ANCESTOR_WALK_LIMIT
|
|
165
|
-
|
|
166
|
-
seen[current] = true
|
|
167
|
-
yield current
|
|
168
|
-
current = resolve(current, @scope.superclass_of(current))
|
|
169
|
-
end
|
|
170
|
-
true
|
|
259
|
+
# Yields each level of `class_name`'s chain. False when the walk stopped at the ADR-41 budget rather than
|
|
260
|
+
# ending: budget exhaustion is uncertainty, and every caller reads it as a reason to decline.
|
|
261
|
+
def walked_whole_chain?(class_name, &)
|
|
262
|
+
levels, whole = chain_levels(arity_chain(class_name))
|
|
263
|
+
levels.each(&)
|
|
264
|
+
whole
|
|
171
265
|
end
|
|
172
266
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
externals = []
|
|
176
|
-
collect_mixins(class_name, own_mixins(class_name), modules, externals, {})
|
|
177
|
-
Level.new(class_name: class_name, modules: modules, externals: externals)
|
|
178
|
-
end
|
|
267
|
+
# `[levels, whole]` for one world of the chain, up to the first superclass the project does not declare.
|
|
268
|
+
def chain_levels(chain) = [chain.levels.map { |level| Level.new(*level) }, !chain.truncated?]
|
|
179
269
|
|
|
180
|
-
|
|
181
|
-
# an extended module's own `include`s reach the same singleton.
|
|
182
|
-
def own_mixins(class_name)
|
|
183
|
-
@kind == :singleton ? (@scope.discovered_extends[class_name] || []) : @scope.includes_of(class_name)
|
|
184
|
-
end
|
|
270
|
+
def side = @kind == :singleton ? :singleton : :instance
|
|
185
271
|
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
next externals << @scope.ancestor_name_candidates(owner, raw) if names.empty?
|
|
198
|
-
|
|
199
|
-
names.each do |name|
|
|
200
|
-
next if seen[name]
|
|
201
|
-
|
|
202
|
-
seen[name] = true
|
|
203
|
-
modules << name
|
|
204
|
-
collect_mixins(name, @scope.includes_of(name), modules, externals, seen)
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
end
|
|
272
|
+
# Instance methods reach a receiver through `include` / `prepend`; class methods through `extend`, and
|
|
273
|
+
# an extended module's own `include`s reach the same singleton — the chain's singleton side.
|
|
274
|
+
#
|
|
275
|
+
# Issue #986 — the `:arity` flavor expands a name the compact-header rename collision left ambiguous
|
|
276
|
+
# to BOTH classes it names: both are ancestors at runtime and only their MRO order is unknowable. Taking
|
|
277
|
+
# both into the level is what keeps this rule alive for the rest of the receiver: a method only one of
|
|
278
|
+
# them declares still has one envelope and still fires, a method they disagree about lands two on the
|
|
279
|
+
# level's join and declines there, and the class's own `def`s, its superclass's and its unambiguous
|
|
280
|
+
# mixins are untouched. Letting the decline arrive as an unknown EXTERNAL mixin instead suppressed the
|
|
281
|
+
# rule for every level of the receiver.
|
|
282
|
+
def arity_chain(class_name) = Scope::ResolutionChain.for(@scope, class_name.to_s, side, :arity)
|
|
208
283
|
|
|
209
284
|
def resolve(owner, raw)
|
|
210
285
|
return nil if raw.nil?
|
|
@@ -273,8 +348,8 @@ module Rigor
|
|
|
273
348
|
# A hook anywhere in the chain, including above the owner, and on either side: a class that answers
|
|
274
349
|
# names it does not define is a class whose `def`s are not the whole story of what a call reaches.
|
|
275
350
|
def chain_free_of_hooks?(class_name)
|
|
276
|
-
walked_whole_chain?(class_name) do |
|
|
277
|
-
([
|
|
351
|
+
walked_whole_chain?(class_name) do |level|
|
|
352
|
+
([level.class_name] + level.modules).each do |name|
|
|
278
353
|
envelopes = @scope.parameter_envelopes_of(name)
|
|
279
354
|
return false if dynamic_surface?(name)
|
|
280
355
|
return false if MISSING_HOOKS.any? { |hook| hook_recorded?(envelopes, hook) }
|
|
@@ -290,13 +365,42 @@ module Rigor
|
|
|
290
365
|
# receiver may be any of them, and each one's own definition of the name is what runs for it.
|
|
291
366
|
def subclasses_agree?(class_name, envelope)
|
|
292
367
|
each_subclass(class_name) do |subclass|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
368
|
+
subclass_levels(subclass).each do |level|
|
|
369
|
+
# A subclass whose own level the budget cut cannot be read, which is a reason to decline.
|
|
370
|
+
return false if level.nil?
|
|
371
|
+
return false unless clean_level?(level)
|
|
372
|
+
return false unless level_envelopes(level).all?(envelope)
|
|
373
|
+
end
|
|
296
374
|
end
|
|
297
375
|
true
|
|
298
376
|
end
|
|
299
377
|
|
|
378
|
+
# A subclass's own level (nil where the budget cut it or the chain does not stand for the name). It asks
|
|
379
|
+
# {Inference::DefinerResolution} on the receiver's side (ADR-119 C1b, C2-c): the subclass's own level
|
|
380
|
+
# answers, and a fork, an unproven mixin order, a `possible`-only definer or a hook there is `UNKNOWN`,
|
|
381
|
+
# which `subclasses_agree?` reads as a reason to decline.
|
|
382
|
+
def subclass_levels(subclass)
|
|
383
|
+
case Inference::DefinerResolution.resolve(@scope, subclass, @method_name, side, question: :arity,
|
|
384
|
+
&own_level_answer(subclass))
|
|
385
|
+
in Inference::DefinerResolution::Known(answer: _answer, owner: _owner) then [first_level(subclass)]
|
|
386
|
+
in Inference::DefinerResolution::ABSENT then [nil]
|
|
387
|
+
in Inference::DefinerResolution::UNKNOWN then NO_LEVELS
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# The subclass's own level, always a candidate (an empty answer where it records nothing), so that the
|
|
392
|
+
# read asks whether the chain stands for the subclass and never whether an ancestor defines the name.
|
|
393
|
+
def own_level_answer(subclass)
|
|
394
|
+
lambda do |chain, from|
|
|
395
|
+
next nil unless from.zero? && !chain.levels.empty?
|
|
396
|
+
|
|
397
|
+
level = Level.new(*chain.levels.first)
|
|
398
|
+
ArityHit.new(level_envelopes(level).uniq, subclass, 0, side)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def first_level(class_name) = chain_levels(arity_chain(class_name)).first.first
|
|
403
|
+
|
|
300
404
|
def each_subclass(class_name)
|
|
301
405
|
children = children_by_parent
|
|
302
406
|
queue = children.fetch(class_name.to_s, []).dup
|
|
@@ -6,6 +6,7 @@ require_relative "../reflection"
|
|
|
6
6
|
require_relative "../source/node_walker"
|
|
7
7
|
require_relative "../source/constant_path"
|
|
8
8
|
require_relative "../inference/singleton_object_constant"
|
|
9
|
+
require_relative "shadow_harness"
|
|
9
10
|
require_relative "../type"
|
|
10
11
|
require_relative "diagnostic"
|
|
11
12
|
require_relative "dependency_recorder"
|
|
@@ -60,10 +61,9 @@ module Rigor
|
|
|
60
61
|
# ADR-87 WD4 — the canonical rule-id table lives in the light `check_rules/rule_ids.rb` (required at the
|
|
61
62
|
# top of this file) so {Analysis::RuleCatalog} can read it without loading the engine.
|
|
62
63
|
|
|
63
|
-
# ADR-35 slice 1 —
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
OVERRIDE_ANCESTOR_WALK_LIMIT = 100
|
|
64
|
+
# ADR-35 slice 1 — the public > protected > private ordering used to decide whether an override reduces
|
|
65
|
+
# visibility. The override rules' ancestor walk is bounded by `Scope::ANCESTOR_WALK_LIMIT`, the
|
|
66
|
+
# resolution chain's.
|
|
67
67
|
VISIBILITY_RANK = { public: 2, protected: 1, private: 0 }.freeze
|
|
68
68
|
|
|
69
69
|
# Resolves a user-supplied rule token (`undefined-method`,
|
|
@@ -216,7 +216,7 @@ module Rigor
|
|
|
216
216
|
def run_node_collectors(path, root, scope_index)
|
|
217
217
|
collectors = build_node_collectors(path, scope_index, root)
|
|
218
218
|
RuleWalk.run(root, collectors.values)
|
|
219
|
-
shadow_verify_node_collectors(path, root, scope_index, collectors) if
|
|
219
|
+
shadow_verify_node_collectors(path, root, scope_index, collectors) if ShadowHarness.enabled?
|
|
220
220
|
collectors
|
|
221
221
|
end
|
|
222
222
|
|
|
@@ -2631,6 +2631,9 @@ module Rigor
|
|
|
2631
2631
|
# `Prism::SelfNode` are also skipped — Ruby 2.7+ permits `self.foo` for private methods.
|
|
2632
2632
|
# - Receiver MUST resolve to a `Type::Nominal` so the rule has a single class identity to query.
|
|
2633
2633
|
# Unions / Dynamic / shape carriers are skipped.
|
|
2634
|
+
# - Issue #1568 — a module the class PREPENDS sits ahead of it in Ruby's order, so a definition there
|
|
2635
|
+
# is what the call reaches; the class's own private `def` answers only when nothing prepended
|
|
2636
|
+
# defines the name ({#reached_definer_visibility}).
|
|
2634
2637
|
def visibility_mismatch_diagnostic(path, call_node, scope_index)
|
|
2635
2638
|
return nil unless explicit_non_self_receiver?(call_node.receiver)
|
|
2636
2639
|
|
|
@@ -2647,9 +2650,33 @@ module Rigor
|
|
|
2647
2650
|
visibility = scope.discovered_method_visibility(receiver_type.class_name, call_node.name)
|
|
2648
2651
|
return nil unless visibility == :private
|
|
2649
2652
|
|
|
2653
|
+
reached = reached_definer_visibility(scope, receiver_type.class_name, call_node.name)
|
|
2654
|
+
return nil if reached == :undecided
|
|
2655
|
+
return nil unless reached.nil? || reached == :private
|
|
2656
|
+
|
|
2650
2657
|
build_visibility_mismatch_diagnostic(path, call_node, receiver_type)
|
|
2651
2658
|
end
|
|
2652
2659
|
|
|
2660
|
+
# Issue #1568, ADR-119 C1c — the recorded visibility of the definition a call on `class_name` reaches: the
|
|
2661
|
+
# first project entry on the class's {Scope::ResolutionChain} from its head that records the name, so a
|
|
2662
|
+
# module the class PREPENDS answers ahead of the class. `Inference::DefinerResolution` reads it with the
|
|
2663
|
+
# `:visibility` question's default answer. It is `:undecided` wherever the chain does not settle the order
|
|
2664
|
+
# (an unpositioned or forked mixin, a conditional include) or an ancestor ahead of the definer may define
|
|
2665
|
+
# the name (one RBS does not know), and the caller declines; a definer whose visibility slot ADR-119's
|
|
2666
|
+
# producers contest is not alone (`DefinerResolution` asks the next candidate too), so it is `:undecided`
|
|
2667
|
+
# there without a read of the table here. nil is nothing recording the name at all, and the class's own
|
|
2668
|
+
# visibility stands. An ancestor the project does not declare is passed over, as the method lookup passes it.
|
|
2669
|
+
def reached_definer_visibility(scope, class_name, method_name)
|
|
2670
|
+
case Inference::DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :visibility)
|
|
2671
|
+
in Inference::DefinerResolution::Known(answer:)
|
|
2672
|
+
answer
|
|
2673
|
+
in Inference::DefinerResolution::UNKNOWN
|
|
2674
|
+
:undecided
|
|
2675
|
+
in Inference::DefinerResolution::ABSENT
|
|
2676
|
+
nil
|
|
2677
|
+
end
|
|
2678
|
+
end
|
|
2679
|
+
|
|
2653
2680
|
def explicit_non_self_receiver?(receiver)
|
|
2654
2681
|
return false if receiver.nil?
|
|
2655
2682
|
return false if receiver.is_a?(Prism::SelfNode)
|
|
@@ -3609,8 +3636,11 @@ module Rigor
|
|
|
3609
3636
|
self_type = scope.self_type
|
|
3610
3637
|
return nil unless self_type.respond_to?(:class_name)
|
|
3611
3638
|
|
|
3639
|
+
side = def_node.receiver.nil? ? receiverless_def_side(scope, self_type.class_name, def_node) : :singleton
|
|
3640
|
+
return nil if side.nil?
|
|
3641
|
+
|
|
3612
3642
|
method_def =
|
|
3613
|
-
if
|
|
3643
|
+
if side == :instance
|
|
3614
3644
|
Reflection.instance_method_definition(self_type.class_name, def_node.name, scope: scope)
|
|
3615
3645
|
else
|
|
3616
3646
|
Reflection.singleton_method_definition(self_type.class_name, def_node.name, scope: scope)
|
|
@@ -3624,14 +3654,26 @@ module Rigor
|
|
|
3624
3654
|
declared_return_union(method_def, scope.environment)
|
|
3625
3655
|
end
|
|
3626
3656
|
|
|
3627
|
-
#
|
|
3628
|
-
#
|
|
3629
|
-
#
|
|
3630
|
-
#
|
|
3631
|
-
#
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3657
|
+
# Which side a receiverless `def` defines, from the indexer's own record of THAT node: the instance table
|
|
3658
|
+
# holding it makes it an instance def, the singleton table (`class << self` bodies, and a `def` under
|
|
3659
|
+
# `Const.instance_eval` that the indexer attributes to the singleton) a singleton def. A node neither table
|
|
3660
|
+
# holds (a `def` in a block whose `self` the engine does not model, such as `E.class_eval do` inside
|
|
3661
|
+
# `class << self`, a redefinition a later `def` replaced) or both do is `nil`, and the caller declines: no
|
|
3662
|
+
# world is consulted, but the record cannot say which signature the body answers to.
|
|
3663
|
+
def receiverless_def_side(scope, class_name, def_node)
|
|
3664
|
+
instance = same_def_node?(scope.user_def_for(class_name, def_node.name), def_node)
|
|
3665
|
+
singleton = same_def_node?(scope.singleton_def_for(class_name, def_node.name), def_node)
|
|
3666
|
+
return :instance if instance && !singleton
|
|
3667
|
+
return :singleton if singleton && !instance
|
|
3668
|
+
|
|
3669
|
+
nil
|
|
3670
|
+
end
|
|
3671
|
+
|
|
3672
|
+
# The tables hold the live node or one re-resolved from a `DefHandle`, so identity is the name and span.
|
|
3673
|
+
def same_def_node?(recorded, def_node)
|
|
3674
|
+
recorded.is_a?(Prism::DefNode) && recorded.name == def_node.name &&
|
|
3675
|
+
recorded.location.start_offset == def_node.location.start_offset &&
|
|
3676
|
+
recorded.location.end_offset == def_node.location.end_offset
|
|
3635
3677
|
end
|
|
3636
3678
|
|
|
3637
3679
|
# `type_vars:` — ADR-35 WD9 tier 1. When the caller supplies a generic-instantiation
|
|
@@ -3739,82 +3781,57 @@ module Rigor
|
|
|
3739
3781
|
override_rank < parent_rank
|
|
3740
3782
|
end
|
|
3741
3783
|
|
|
3742
|
-
#
|
|
3743
|
-
#
|
|
3744
|
-
#
|
|
3745
|
-
#
|
|
3746
|
-
#
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3784
|
+
# The project ancestors whose definitions `class_name`'s own `def` OVERRIDES are the entries AFTER the
|
|
3785
|
+
# class on its {Scope::ResolutionChain}, nearest first; {#nearest_ancestor_visibility} and
|
|
3786
|
+
# {#nearest_ancestor_method_def} each read the first of them that answers their question through
|
|
3787
|
+
# `Inference::DefinerResolution` (ADR-119 C1c), `from:` the position after the class. Both are instance
|
|
3788
|
+
# side only: the singleton override rules do not exist, and the singleton side's own read lands with
|
|
3789
|
+
# ADR-119's singleton design.
|
|
3790
|
+
#
|
|
3791
|
+
# Issue #1568 — the walk this replaced was breadth-first from the class's direct ancestors, and those
|
|
3792
|
+
# include the modules the class PREPENDS. A prepended module sits BEFORE the class in Ruby's order: its
|
|
3793
|
+
# `def` overrides the class's, not the other way round, so `prepend P` with a public `P#foo` and a
|
|
3794
|
+
# private `C#foo` reported `C#foo` as reducing `P#foo`'s visibility. It also reached `Base` before a
|
|
3795
|
+
# module included through an included module (#1567's order).
|
|
3796
|
+
#
|
|
3797
|
+
# ADR-119 WD3 — for the two override lints *absent* always counts: a chain that does not settle (a mixin
|
|
3798
|
+
# whose position the tables cannot vouch for, a fork whose worlds disagree, a conditional or
|
|
3799
|
+
# block-scoped `include`), or an external ancestor ahead of the parent that RBS does not know or knows
|
|
3800
|
+
# and declares the name in, is `UNKNOWN`, and the lint stays silent rather than answer from the
|
|
3801
|
+
# breadth-first order the walk used before (`Scope::ResolutionChain::MasterOrder`) where the order is
|
|
3802
|
+
# not known. The two lints read only the `[owner, value]` of a `Known`.
|
|
3803
|
+
#
|
|
3804
|
+
# ADR-46 slice 3 — an ancestor the project does not declare ends the walk's reach into it, and a class
|
|
3805
|
+
# defined LATER under that name would change the answer; `DefinerResolution` files the negative class
|
|
3806
|
+
# edge of each external entry it tests.
|
|
3807
|
+
def overridden_ancestors_start(scope, class_name)
|
|
3808
|
+
own = Scope::ResolutionChain.for(scope, class_name.to_s, :instance, :methods).index_of(class_name.to_s)
|
|
3809
|
+
own && (own + 1)
|
|
3765
3810
|
end
|
|
3766
3811
|
|
|
3767
3812
|
# `[defining_class, visibility]` for the nearest user-source ancestor that defines an instance
|
|
3768
3813
|
# method `method_name`, or nil.
|
|
3769
3814
|
def nearest_ancestor_visibility(scope, class_name, method_name)
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
end
|
|
3790
|
-
raw_super = scope.superclass_of(class_name)
|
|
3791
|
-
if raw_super
|
|
3792
|
-
resolved_super = resolve_override_ancestor_name(scope, class_name, raw_super)
|
|
3793
|
-
names << resolved_super if resolved_super
|
|
3815
|
+
from = overridden_ancestors_start(scope, class_name)
|
|
3816
|
+
return nil if from.nil?
|
|
3817
|
+
|
|
3818
|
+
case Inference::DefinerResolution.resolve(scope, class_name, method_name, :instance,
|
|
3819
|
+
question: :override, from: from) do |ancestor|
|
|
3820
|
+
# Stop at the nearest ancestor that DEFINES the method; its visibility may be nil (unknown) —
|
|
3821
|
+
# the caller treats unknown as "cannot prove a reduction" and stays silent. Never fabricate
|
|
3822
|
+
# `:public` from a missing entry (that produced a large false-positive cluster on
|
|
3823
|
+
# cross-file Rails concerns). The `:unrecorded` marker stands for that nil in the answer.
|
|
3824
|
+
next unless scope.user_def_for(ancestor, method_name)
|
|
3825
|
+
|
|
3826
|
+
scope.discovered_method_visibility(ancestor, method_name) || :unrecorded
|
|
3827
|
+
end
|
|
3828
|
+
in Inference::DefinerResolution::Known(answer: [owner, visibility])
|
|
3829
|
+
[owner, visibility == :unrecorded ? nil : visibility]
|
|
3830
|
+
in Inference::DefinerResolution::UNKNOWN # the case/in contract needs one arm per answer
|
|
3831
|
+
nil
|
|
3832
|
+
in Inference::DefinerResolution::ABSENT # rubocop:disable Lint/DuplicateBranch -- see above
|
|
3833
|
+
nil
|
|
3794
3834
|
end
|
|
3795
|
-
names
|
|
3796
|
-
end
|
|
3797
|
-
|
|
3798
|
-
# Issue #732 — the predicate is asked of the SCOPE, not re-implemented here. This rule kept its own
|
|
3799
|
-
# three-table copy until #723 widened the scope's with `discovered_methods` and left this one behind,
|
|
3800
|
-
# so the two disagreed about exactly one shape (a project class with no instance def, no superclass
|
|
3801
|
-
# and no include — `class Base; def self.build = :built; end`) with nothing observing the fork. #682
|
|
3802
|
-
# made `Scope` the single owner of the candidate ORDER; this makes it the single owner of the
|
|
3803
|
-
# predicate that filters those candidates too.
|
|
3804
|
-
def resolve_override_ancestor_name(scope, subclass_qualified, raw_ancestor)
|
|
3805
|
-
resolved = scope.ancestor_name_candidates(subclass_qualified, raw_ancestor)
|
|
3806
|
-
.find { |candidate| scope.known_user_class?(candidate) }
|
|
3807
|
-
return resolved if resolved
|
|
3808
|
-
|
|
3809
|
-
# ADR-46 slice 3 — the override checker reads the class graph
|
|
3810
|
-
# directly (not through the recorder's `Scope` choke points), and
|
|
3811
|
-
# short-circuits when the ancestor resolves to no project class, so
|
|
3812
|
-
# an incremental re-check has no edge telling it to re-check this
|
|
3813
|
-
# subclass when that ancestor is later defined. Record a negative
|
|
3814
|
-
# class edge (keyed on the unqualified name) so the appeared-class
|
|
3815
|
-
# widening picks it up.
|
|
3816
|
-
DependencyRecorder.read_missing(:class, raw_ancestor.to_s.split("::").last) if DependencyRecorder.active?
|
|
3817
|
-
nil
|
|
3818
3835
|
end
|
|
3819
3836
|
|
|
3820
3837
|
def build_override_visibility_diagnostic(path, def_node, parent_class, parent_visibility, override_visibility)
|
|
@@ -3894,9 +3911,23 @@ module Rigor
|
|
|
3894
3911
|
# `[defining_class, RBS::Definition::Method]` for the nearest project-discovered ancestor whose
|
|
3895
3912
|
# RBS declares `method_name` (not the starting class's own declaration), or nil.
|
|
3896
3913
|
def nearest_ancestor_method_def(scope, class_name, method_name)
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3914
|
+
from = overridden_ancestors_start(scope, class_name)
|
|
3915
|
+
return nil if from.nil?
|
|
3916
|
+
|
|
3917
|
+
case Inference::DefinerResolution.resolve(scope, class_name, method_name, :instance,
|
|
3918
|
+
question: :override, from: from) do |ancestor|
|
|
3919
|
+
method_def = safe_instance_method_definition(ancestor, method_name, scope)
|
|
3920
|
+
# The signature text is the answer two worlds compare: an `RBS::Definition::Method` has no
|
|
3921
|
+
# structural equality of its own.
|
|
3922
|
+
method_def.method_types.map(&:to_s) if method_def && !defined_on?(method_def, class_name)
|
|
3923
|
+
end
|
|
3924
|
+
in Inference::DefinerResolution::Known(owner:)
|
|
3925
|
+
method_def = safe_instance_method_definition(owner, method_name, scope)
|
|
3926
|
+
method_def && [owner, method_def]
|
|
3927
|
+
in Inference::DefinerResolution::UNKNOWN # the case/in contract needs one arm per answer
|
|
3928
|
+
nil
|
|
3929
|
+
in Inference::DefinerResolution::ABSENT # rubocop:disable Lint/DuplicateBranch -- see above
|
|
3930
|
+
nil
|
|
3900
3931
|
end
|
|
3901
3932
|
end
|
|
3902
3933
|
|