rigortype 0.4.3 → 0.4.4
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 +1 -1
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +16 -0
- data/docs/manual/02-cli-reference.md +4 -3
- data/docs/manual/03-configuration.md +16 -5
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/18-removing-dead-code.md +11 -1
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +57 -0
- data/lib/rigor/analysis/check_rules.rb +46 -36
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_session.rb +121 -21
- data/lib/rigor/analysis/reachability/graph.rb +23 -3
- data/lib/rigor/analysis/reachability/scan.rb +51 -9
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +110 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +35 -9
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +57 -1
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/incremental_snapshot.rb +20 -4
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/environment/rbs_loader.rb +46 -10
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/captured_locals.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +59 -4
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/scope_indexer.rb +439 -70
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +84 -29
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +15 -0
- data/lib/rigor/lens/document.rb +2 -2
- data/lib/rigor/lens/file_reader.rb +442 -73
- data/lib/rigor/lens/members.rb +65 -15
- data/lib/rigor/lens/run.rb +44 -2
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/scope.rb +11 -6
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- metadata +4 -1
|
@@ -82,7 +82,12 @@ module Rigor
|
|
|
82
82
|
# an unchanged file would fold as "no possible fact" where a cold walk records one.
|
|
83
83
|
# v18: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`; a pre-18 bundle
|
|
84
84
|
# carries it empty, so an unchanged file's conditional visibility would fold as certain.
|
|
85
|
-
|
|
85
|
+
# v19: #1555 / #1962 — the run-result dependency descriptor now carries a `:kind` row per analysis root,
|
|
86
|
+
# `pre_eval:` entry and project signature root (configured, or the auto-detected `sig`), so one that
|
|
87
|
+
# appears, vanishes or changes kind invalidates the warm entry. A
|
|
88
|
+
# pre-19 entry carries none of them and would validate fresh across exactly those edits, so cached
|
|
89
|
+
# entries must read as misses once and rebuild carrying the rows.
|
|
90
|
+
SCHEMA_VERSION = 19
|
|
86
91
|
|
|
87
92
|
# Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
|
|
88
93
|
# struct so no caller can mutate after the entry is in a Descriptor.
|
|
@@ -96,7 +101,7 @@ module Rigor
|
|
|
96
101
|
# It MUST NOT be placed in a descriptor used as a cache KEY (its value carries machine-local,
|
|
97
102
|
# per-run-nondeterministic stat data); {RbsDescriptor.build}'s env-cache `files` therefore stay
|
|
98
103
|
# `:digest`, while the {Runner} run-dependency descriptor and plugin {IoBoundary} reads use `:stat`.
|
|
99
|
-
VALID_COMPARATORS = %i[digest stat mtime exists].freeze
|
|
104
|
+
VALID_COMPARATORS = %i[digest stat mtime kind exists].freeze
|
|
100
105
|
|
|
101
106
|
# The two `:exists` values — `File.exist?`'s answer rendered with `to_s`, which is what
|
|
102
107
|
# {Descriptor#file_entry_fresh?} compares the live answer against.
|
|
@@ -163,6 +168,32 @@ module Rigor
|
|
|
163
168
|
new(path: path, comparator: :exists, value: PRESENT)
|
|
164
169
|
end
|
|
165
170
|
|
|
171
|
+
# The four `:kind` values, in the order {Analysis::Runner}'s path expansion asks: a directory, a regular
|
|
172
|
+
# file, something else that exists (a FIFO, a socket), nothing (a missing path or a dangling symlink).
|
|
173
|
+
# Each predicate follows symlinks, as the expansion's do.
|
|
174
|
+
KIND_DIRECTORY = "directory"
|
|
175
|
+
KIND_FILE = "file"
|
|
176
|
+
KIND_OTHER = "other"
|
|
177
|
+
KIND_ABSENT = "absent"
|
|
178
|
+
|
|
179
|
+
# #1962 — the kind row: fresh while `path` is still the same kind of thing, stale once it appears,
|
|
180
|
+
# vanishes, or a directory and a file trade places. It is the row for an analysis root, whose
|
|
181
|
+
# diagnostics (`no such file or directory`, `not a Ruby file`) answer to exactly that and to nothing
|
|
182
|
+
# else about the path, and for a `pre_eval:` entry and a signature root: an existence row would read a
|
|
183
|
+
# directory replaced by a file of the same name as fresh (ADR-45 WD1b's bound), and what the path holds
|
|
184
|
+
# has rows of its own (the analysed-file set the key carries, a signature root's `.rbs` rows).
|
|
185
|
+
def self.kind(path:)
|
|
186
|
+
new(path: path, comparator: :kind, value: kind_of(path))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.kind_of(path)
|
|
190
|
+
if File.directory?(path) then KIND_DIRECTORY
|
|
191
|
+
elsif File.file?(path) then KIND_FILE
|
|
192
|
+
elsif File.exist?(path) then KIND_OTHER
|
|
193
|
+
else KIND_ABSENT
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
166
197
|
# @return whether this row records the path as absent (see {.absent}).
|
|
167
198
|
def absent?
|
|
168
199
|
comparator == :exists && value == ABSENT
|
|
@@ -413,10 +444,11 @@ module Rigor
|
|
|
413
444
|
|
|
414
445
|
# File-comparator strictness ordering. `:stat` is strictest (it carries the content digest AND a stat
|
|
415
446
|
# tuple, so it validates content-authoritatively while short-circuiting on an unmoved stat); `:digest` is
|
|
416
|
-
# deterministic across machines; `:mtime` is cheaper but local; `:
|
|
417
|
-
# `:
|
|
418
|
-
#
|
|
419
|
-
|
|
447
|
+
# deterministic across machines; `:mtime` is cheaper but local; `:kind` answers what the path is and
|
|
448
|
+
# `:exists` only whether it is there, the weakest signal. Ranking `:stat` and `:digest` apart guarantees a
|
|
449
|
+
# path contributed under both never raises a value {Conflict} (their `value` strings differ by
|
|
450
|
+
# construction) — the stricter one wins and its value is used.
|
|
451
|
+
COMPARATOR_STRICTNESS = { stat: 4, digest: 3, mtime: 2, kind: 1, exists: 0 }.freeze
|
|
420
452
|
private_constant :COMPARATOR_STRICTNESS
|
|
421
453
|
|
|
422
454
|
# Composes any number of descriptors into a single descriptor whose slots are the union of the inputs'
|
|
@@ -505,6 +537,8 @@ module Rigor
|
|
|
505
537
|
FileDigest.stat_fresh?(entry.path, entry.value)
|
|
506
538
|
when :mtime
|
|
507
539
|
File.exist?(entry.path) && File.mtime(entry.path).to_i.to_s == entry.value
|
|
540
|
+
when :kind
|
|
541
|
+
FileEntry.kind_of(entry.path) == entry.value
|
|
508
542
|
when :exists
|
|
509
543
|
File.exist?(entry.path).to_s == entry.value
|
|
510
544
|
else
|
|
@@ -169,7 +169,10 @@ module Rigor
|
|
|
169
169
|
# file's conditional `def` as certain and a warm run would answer a definer a cold run declines.
|
|
170
170
|
# 37: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`, which a pre-37 bundle
|
|
171
171
|
# carries empty.
|
|
172
|
-
|
|
172
|
+
# 38: issue #1898 — the payload gains `effect_plugin_digest`, the plugin effect-fact digest the collections were
|
|
173
|
+
# stamped under, which a recheck now checks before its served collections reach the envelope pass. A pre-38
|
|
174
|
+
# blob carries none, so the gate rejects it and the next run is cold.
|
|
175
|
+
SCHEMA = 38
|
|
173
176
|
|
|
174
177
|
# The persisted per-file state.
|
|
175
178
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -224,11 +227,22 @@ module Rigor
|
|
|
224
227
|
# Issues #796 / #794 — the run-level rows a narrowed run replays instead of re-deriving; see
|
|
225
228
|
# docs/internal-spec/cache.md § "Run-level rows the snapshot carries" for why exactly these two are
|
|
226
229
|
# fresh under the fingerprint and why no other `.rigor.yml`-level row may join them.
|
|
230
|
+
# Issue #1898 — `effect_plugin_digest` is {Rigor::Effects::PluginFacts#digest} at the run that collected
|
|
231
|
+
# `effect_collections` (nil when collection was off). A plugin stamps its attributions and edges into a file's
|
|
232
|
+
# collection, and `effects_identity` is plugin-blind (it is compared before any plugin loads), so the session
|
|
233
|
+
# compares this one after the recheck has loaded them.
|
|
227
234
|
Payload = Data.define(:cache, :sources, :digests, :analyzed,
|
|
228
235
|
:symbol_sources, :ancestry_sources, :symbol_fingerprints,
|
|
229
236
|
:missing, :class_decls, :constant_decls, :seed_bundles, :plugin_fact_digest,
|
|
230
237
|
:return_summaries, :param_table,
|
|
231
|
-
:effect_collections, :effects_identity, :run_level_rows
|
|
238
|
+
:effect_collections, :effects_identity, :run_level_rows,
|
|
239
|
+
:effect_plugin_digest) do
|
|
240
|
+
# Defaults to nil so a payload built without it (an older caller, a spec fixture) reads as "unknown",
|
|
241
|
+
# which the session treats as moved.
|
|
242
|
+
def initialize(effect_plugin_digest: nil, **rest)
|
|
243
|
+
super
|
|
244
|
+
end
|
|
245
|
+
end
|
|
232
246
|
|
|
233
247
|
# Issues #796 / #794 — the two run-level rows the snapshot carries. `definition_build_failures` is the
|
|
234
248
|
# reported per-class detail list ({Environment::RbsLoader#definition_build_failures}); `hkt_scan_failure`
|
|
@@ -434,7 +448,8 @@ module Rigor
|
|
|
434
448
|
param_table: data[:param_table] || {},
|
|
435
449
|
effect_collections: data[:effect_collections] || {},
|
|
436
450
|
effects_identity: data[:effects_identity],
|
|
437
|
-
run_level_rows: data[:run_level_rows]
|
|
451
|
+
run_level_rows: data[:run_level_rows],
|
|
452
|
+
effect_plugin_digest: data[:effect_plugin_digest]
|
|
438
453
|
)
|
|
439
454
|
end
|
|
440
455
|
private :payload_from
|
|
@@ -459,7 +474,8 @@ module Rigor
|
|
|
459
474
|
param_table: payload.param_table,
|
|
460
475
|
effect_collections: payload.effect_collections,
|
|
461
476
|
effects_identity: payload.effects_identity,
|
|
462
|
-
run_level_rows: payload.run_level_rows
|
|
477
|
+
run_level_rows: payload.run_level_rows,
|
|
478
|
+
effect_plugin_digest: payload.effect_plugin_digest
|
|
463
479
|
)
|
|
464
480
|
# #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
|
|
465
481
|
# on the edit path; see {Store::DEFLATE_LEVEL}.
|
|
@@ -12,7 +12,7 @@ module Rigor
|
|
|
12
12
|
# Starts a long-running MCP (Model Context Protocol) server over stdio. The server exposes Rigor's analysis tools as
|
|
13
13
|
# MCP tool calls over a newline-delimited JSON-RPC 2.0 stream. See ADR-33.
|
|
14
14
|
#
|
|
15
|
-
#
|
|
15
|
+
# The stdio transport serves eight read-only tools: rigor_check, rigor_type_of, rigor_triage, rigor_lens,
|
|
16
16
|
# rigor_annotate, rigor_sig_gen, rigor_explain, rigor_coverage.
|
|
17
17
|
class McpCommand < Command
|
|
18
18
|
USAGE = "Usage: rigor mcp [options]"
|
data/lib/rigor/config_audit.rb
CHANGED
|
@@ -26,11 +26,12 @@ module Rigor
|
|
|
26
26
|
#
|
|
27
27
|
# Every check is held to the same bar that {SignaturePathAudit} set: it mirrors the
|
|
28
28
|
# loader's own acceptance test, so a warning means the loader really did load nothing, and
|
|
29
|
-
# it never fires on a setup that works. In particular the
|
|
30
|
-
#
|
|
31
|
-
# (whose family Rigor cannot statically enumerate, since a `node_rule` block emits
|
|
32
|
-
# `rule:` string it likes) is left alone
|
|
33
|
-
# a
|
|
29
|
+
# it never fires on a setup that works. In particular the `severity_overrides:` check only
|
|
30
|
+
# flags a key under a built-in family (`call`, `flow`, …) — a plugin- or `rbs_extended.*`
|
|
31
|
+
# rule id (whose family Rigor cannot statically enumerate, since a `node_rule` block emits
|
|
32
|
+
# any `rule:` string it likes) is left alone. The `disable:` check goes further because the
|
|
33
|
+
# list's reach is enumerable (issue #1953): a plugin's row answers only to its qualified
|
|
34
|
+
# `plugin.<id>.<rule>` id, so a bare plugin rule there is inert (see {.disable_token_warning}).
|
|
34
35
|
module ConfigAudit
|
|
35
36
|
# One config-level finding. `kind` discriminates the source key (`:signature_path`,
|
|
36
37
|
# `:bundled_plugin_signature_path`, `:library`, `:disabled_rule`, `:severity_override`,
|
|
@@ -146,18 +147,13 @@ module Rigor
|
|
|
146
147
|
[]
|
|
147
148
|
end
|
|
148
149
|
|
|
149
|
-
# `disable:` tokens
|
|
150
|
-
#
|
|
151
|
-
# docstring).
|
|
150
|
+
# `disable:` tokens that reach no row ({.disable_token_warning}) and `severity_overrides:` keys
|
|
151
|
+
# that name no rule. An override key is flagged only under a built-in family, so a plugin
|
|
152
|
+
# rule id is never mis-flagged (see the module docstring).
|
|
152
153
|
def self.rule_token_warnings(configuration)
|
|
154
|
+
plugin_ids = configured_plugin_ids(configuration.plugins)
|
|
153
155
|
disable = Array(configuration.disabled_rules).filter_map do |token|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Warning.new(
|
|
157
|
-
kind: :disabled_rule,
|
|
158
|
-
message: "disable: #{token.to_s.inspect} is not a recognized rule id; the suppression has no effect",
|
|
159
|
-
fields: { "token" => token.to_s }
|
|
160
|
-
)
|
|
156
|
+
disable_token_warning(token.to_s, plugin_ids)
|
|
161
157
|
end
|
|
162
158
|
overrides = configuration.severity_overrides.keys.filter_map do |key|
|
|
163
159
|
next unless inert_builtin_token?(key.to_s)
|
|
@@ -171,6 +167,106 @@ module Rigor
|
|
|
171
167
|
disable + overrides
|
|
172
168
|
end
|
|
173
169
|
|
|
170
|
+
# Issue #1953 — the run-level rows the diagnostic policy keeps on `severity_overrides:` and the
|
|
171
|
+
# baseline. `disable:` does not reach them by any spelling, so naming one there is inert.
|
|
172
|
+
RESERVED_RUN_LEVEL_FAMILIES = %w[plugin_loader plugin_trust rbs.coverage dynamic.dependency-source].freeze
|
|
173
|
+
RESERVED_RUN_LEVEL_IDS = %w[load-error runtime-error pool-degraded].freeze
|
|
174
|
+
private_constant :RESERVED_RUN_LEVEL_FAMILIES, :RESERVED_RUN_LEVEL_IDS
|
|
175
|
+
|
|
176
|
+
# A warning for a `disable:` token that can reach no row, or nil. Since issue #1953 the list's
|
|
177
|
+
# reach is exactly enumerable for three shapes, and those are what this flags:
|
|
178
|
+
#
|
|
179
|
+
# - a token naming a reserved run-level row ({RESERVED_RUN_LEVEL_FAMILIES} /
|
|
180
|
+
# {RESERVED_RUN_LEVEL_IDS}), which the list never reaches;
|
|
181
|
+
# - a `plugin.`-qualified token naming no plugin `plugins:` configures — checked only when
|
|
182
|
+
# every entry's manifest id is known without loading it ({.configured_plugin_ids});
|
|
183
|
+
# - any other token that is not `all`, a family, a legacy alias or a catalogue / `pre-eval.*`
|
|
184
|
+
# id. A bare id never reaches a plugin's row, so a plugin's bare rule is inert here too.
|
|
185
|
+
#
|
|
186
|
+
# A token under a non-check engine family (`rbs_extended.*`, `rbs.*`, `dynamic.*`) or a known
|
|
187
|
+
# non-catalogue engine id stays unflagged: under-warning is this module's FP-safe direction.
|
|
188
|
+
def self.disable_token_warning(token, plugin_ids)
|
|
189
|
+
return nil if effective_disable_token?(token)
|
|
190
|
+
|
|
191
|
+
reason = disable_token_problem(token, plugin_ids)
|
|
192
|
+
return nil if reason.nil?
|
|
193
|
+
|
|
194
|
+
Warning.new(kind: :disabled_rule, message: "disable: #{token.inspect} #{reason}",
|
|
195
|
+
fields: { "token" => token })
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.disable_token_problem(token, plugin_ids)
|
|
199
|
+
if reserved_run_level_token?(token)
|
|
200
|
+
"names a run-level row, which `disable:` does not reach; use `severity_overrides:` or a baseline"
|
|
201
|
+
elsif token.start_with?(Analysis::CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
202
|
+
unless plugin_ids.nil? || plausible_plugin_token?(token, plugin_ids)
|
|
203
|
+
"names no plugin `plugins:` configures; the suppression has no effect"
|
|
204
|
+
end
|
|
205
|
+
elsif inert_builtin_token?(token)
|
|
206
|
+
"is not a recognized rule id; the suppression has no effect"
|
|
207
|
+
elsif !non_check_engine_token?(token)
|
|
208
|
+
"is not a recognized rule id; the suppression has no effect " \
|
|
209
|
+
"(if this is a plugin's rule, write it as `plugin.<id>.<rule>`)"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
private_class_method :disable_token_problem
|
|
213
|
+
|
|
214
|
+
def self.effective_disable_token?(token)
|
|
215
|
+
return true if token == "all"
|
|
216
|
+
|
|
217
|
+
!Array(Analysis::CheckRules.resolve_rule_token(token)).empty?
|
|
218
|
+
end
|
|
219
|
+
private_class_method :effective_disable_token?
|
|
220
|
+
|
|
221
|
+
def self.reserved_run_level_token?(token)
|
|
222
|
+
RESERVED_RUN_LEVEL_IDS.include?(token) ||
|
|
223
|
+
RESERVED_RUN_LEVEL_FAMILIES.any? { |family| token == family || token.start_with?("#{family}.") }
|
|
224
|
+
end
|
|
225
|
+
private_class_method :reserved_run_level_token?
|
|
226
|
+
|
|
227
|
+
def self.non_check_engine_token?(token)
|
|
228
|
+
Analysis::CheckRules::NON_CHECK_DIAGNOSTIC_IDS.include?(token) ||
|
|
229
|
+
Analysis::CheckRules::NON_CHECK_DIAGNOSTIC_FAMILIES.include?(token.split(".").first)
|
|
230
|
+
end
|
|
231
|
+
private_class_method :non_check_engine_token?
|
|
232
|
+
|
|
233
|
+
# `plugin.<id>` or `plugin.<id>.<rule>` for a configured id. A manifest id may itself hold a
|
|
234
|
+
# dot, so the token is matched against each id rather than split.
|
|
235
|
+
def self.plausible_plugin_token?(token, plugin_ids)
|
|
236
|
+
rest = token.delete_prefix(Analysis::CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
237
|
+
plugin_ids.any? { |id| rest == id || rest.start_with?("#{id}.") }
|
|
238
|
+
end
|
|
239
|
+
private_class_method :plausible_plugin_token?
|
|
240
|
+
|
|
241
|
+
# The manifest id of every `plugins:` entry, or nil when one cannot be known without loading
|
|
242
|
+
# the plugin. An explicit `id:` is the id; a bundled plugin's id is its gem name minus the
|
|
243
|
+
# `rigor-` prefix ({Plugin::FirstParty}); a third-party gem named without `id:` may register
|
|
244
|
+
# any id, so its presence turns the plugin-token check off. An `enabled: false` entry counts:
|
|
245
|
+
# the user has found the plugin, and under-warning is the FP-safe direction.
|
|
246
|
+
def self.configured_plugin_ids(plugins)
|
|
247
|
+
Array(plugins).each_with_object(Set.new) do |raw, ids|
|
|
248
|
+
entry = raw.is_a?(Hash) ? raw.to_h { |key, value| [key.to_s, value] } : { "gem" => raw.to_s }
|
|
249
|
+
if entry["id"]
|
|
250
|
+
ids << entry["id"].to_s
|
|
251
|
+
elsif bundled_plugin_gem?(entry["gem"].to_s)
|
|
252
|
+
ids << entry["gem"].to_s.delete_prefix(BUNDLED_GEM_PREFIX)
|
|
253
|
+
else
|
|
254
|
+
return nil
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
private_class_method :configured_plugin_ids
|
|
259
|
+
|
|
260
|
+
BUNDLED_GEM_PREFIX = "rigor-"
|
|
261
|
+
private_constant :BUNDLED_GEM_PREFIX
|
|
262
|
+
|
|
263
|
+
def self.bundled_plugin_gem?(gem)
|
|
264
|
+
return false unless gem.start_with?(BUNDLED_GEM_PREFIX)
|
|
265
|
+
|
|
266
|
+
File.file?(File.join(SignaturePathAudit::BUNDLED_PLUGINS_ROOT, gem, "lib", "#{gem}.rb"))
|
|
267
|
+
end
|
|
268
|
+
private_class_method :bundled_plugin_gem?
|
|
269
|
+
|
|
174
270
|
# True when `token` looks like a built-in rule id but matches none — its first segment
|
|
175
271
|
# is a built-in family yet it is neither a bare family wildcard nor a known canonical id.
|
|
176
272
|
# A token whose family is not built-in (a plugin / `rbs_extended.*` rule, or a bare
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../configuration_error"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
class Configuration
|
|
5
7
|
# Parsed `dependencies:` section of `.rigor.yml`. Per
|
|
@@ -56,7 +58,7 @@ module Rigor
|
|
|
56
58
|
# Hash with `source_inference:` and / or `budget_per_gem:` / `budget_overrun_strategy:` present.
|
|
57
59
|
def self.from_h(data)
|
|
58
60
|
return new([]) if data.nil?
|
|
59
|
-
raise
|
|
61
|
+
raise ConfigurationError, "dependencies: must be a Hash, got #{data.inspect}" unless data.is_a?(Hash)
|
|
60
62
|
|
|
61
63
|
raw_entries = coerce_source_inference(data["source_inference"])
|
|
62
64
|
entries, warnings = dedupe_entries(raw_entries)
|
|
@@ -139,7 +141,7 @@ module Rigor
|
|
|
139
141
|
return [] if value.nil? || value == false
|
|
140
142
|
|
|
141
143
|
unless value.is_a?(Array)
|
|
142
|
-
raise
|
|
144
|
+
raise ConfigurationError,
|
|
143
145
|
"dependencies.source_inference: must be a list of entries " \
|
|
144
146
|
"(or false / omitted to disable), got #{value.inspect}"
|
|
145
147
|
end
|
|
@@ -149,7 +151,7 @@ module Rigor
|
|
|
149
151
|
|
|
150
152
|
def coerce_entry(raw)
|
|
151
153
|
unless raw.is_a?(Hash)
|
|
152
|
-
raise
|
|
154
|
+
raise ConfigurationError,
|
|
153
155
|
"dependencies.source_inference[] entry must be a Hash, got #{raw.inspect}"
|
|
154
156
|
end
|
|
155
157
|
|
|
@@ -162,7 +164,7 @@ module Rigor
|
|
|
162
164
|
|
|
163
165
|
def coerce_gem(value)
|
|
164
166
|
unless value.is_a?(String) && !value.empty?
|
|
165
|
-
raise
|
|
167
|
+
raise ConfigurationError,
|
|
166
168
|
"dependencies.source_inference[].gem must be a non-empty String, got #{value.inspect}"
|
|
167
169
|
end
|
|
168
170
|
|
|
@@ -170,28 +172,34 @@ module Rigor
|
|
|
170
172
|
end
|
|
171
173
|
|
|
172
174
|
def coerce_mode(value)
|
|
173
|
-
mode = (value || "when_missing")
|
|
175
|
+
mode = config_symbol(value || "when_missing")
|
|
174
176
|
return mode if VALID_MODES.include?(mode)
|
|
175
177
|
|
|
176
|
-
raise
|
|
178
|
+
raise ConfigurationError,
|
|
177
179
|
"dependencies.source_inference[].mode must be one of " \
|
|
178
180
|
"#{VALID_MODES.inspect}, got #{value.inspect}"
|
|
179
181
|
end
|
|
180
182
|
|
|
183
|
+
# The Symbol a String / Symbol value names, or nil for any other YAML shape (a number, a list, a mapping), which
|
|
184
|
+
# the caller then reports as not one of its valid values rather than crashing on `#to_sym`.
|
|
185
|
+
def config_symbol(value)
|
|
186
|
+
value.to_sym if value.is_a?(String) || value.is_a?(Symbol)
|
|
187
|
+
end
|
|
188
|
+
|
|
181
189
|
def coerce_roots(raw)
|
|
182
190
|
roots = Array(raw.fetch("roots", DEFAULT_ROOTS)).map(&:to_s).freeze
|
|
183
191
|
return roots unless roots.empty?
|
|
184
192
|
|
|
185
|
-
raise
|
|
193
|
+
raise ConfigurationError,
|
|
186
194
|
"dependencies.source_inference[].roots must not be empty when supplied " \
|
|
187
195
|
"(omit the key to fall back to the default #{DEFAULT_ROOTS.inspect})"
|
|
188
196
|
end
|
|
189
197
|
|
|
190
198
|
def coerce_budget_overrun_strategy(value)
|
|
191
|
-
symbol = value
|
|
199
|
+
symbol = config_symbol(value)
|
|
192
200
|
return symbol if VALID_BUDGET_OVERRUN_STRATEGIES.include?(symbol)
|
|
193
201
|
|
|
194
|
-
raise
|
|
202
|
+
raise ConfigurationError,
|
|
195
203
|
"dependencies.budget_overrun_strategy must be one of " \
|
|
196
204
|
"#{VALID_BUDGET_OVERRUN_STRATEGIES.inspect}, got #{value.inspect}"
|
|
197
205
|
end
|
|
@@ -201,13 +209,13 @@ module Rigor
|
|
|
201
209
|
# MIN_BUDGET_PER_GEM .. MAX_BUDGET_PER_GEM (i.e. 0.25× – 4× of the default).
|
|
202
210
|
def coerce_budget_per_gem(value)
|
|
203
211
|
unless value.is_a?(Integer)
|
|
204
|
-
raise
|
|
212
|
+
raise ConfigurationError,
|
|
205
213
|
"dependencies.budget_per_gem must be an Integer, " \
|
|
206
214
|
"got #{value.inspect}"
|
|
207
215
|
end
|
|
208
216
|
|
|
209
217
|
unless value.between?(MIN_BUDGET_PER_GEM, MAX_BUDGET_PER_GEM)
|
|
210
|
-
raise
|
|
218
|
+
raise ConfigurationError,
|
|
211
219
|
"dependencies.budget_per_gem must be in the range " \
|
|
212
220
|
"#{MIN_BUDGET_PER_GEM}..#{MAX_BUDGET_PER_GEM}, " \
|
|
213
221
|
"got #{value.inspect}"
|
|
@@ -66,6 +66,15 @@ module Rigor
|
|
|
66
66
|
QUARANTINE_WARN_LIMIT = 10
|
|
67
67
|
|
|
68
68
|
class << self
|
|
69
|
+
# Issue #1902 — the fixed total order that picks ONE detail tuple for a class out of several: its two
|
|
70
|
+
# sides in {#report_definition_build_failure}, or a fresh and a replayed record in
|
|
71
|
+
# {Analysis::Runner::PoolCoordinator}. Content-only (the tuple carries no side), so both places choose
|
|
72
|
+
# the same tuple from the same candidates, whatever order those candidates arrived in.
|
|
73
|
+
def definition_build_detail_order(detail)
|
|
74
|
+
_, error_class, member, buffers, unresolved = detail
|
|
75
|
+
[error_class.to_s, member.to_s, Array(buffers).map(&:to_s), Array(unresolved).map(&:to_s)]
|
|
76
|
+
end
|
|
77
|
+
|
|
69
78
|
def default
|
|
70
79
|
@default ||= new.freeze
|
|
71
80
|
end
|
|
@@ -2471,6 +2480,18 @@ module Rigor
|
|
|
2471
2480
|
# fail-soft: a broken RBS environment yields no entries.
|
|
2472
2481
|
end
|
|
2473
2482
|
|
|
2483
|
+
# ADR-113 — the RBS declaration of the constant `name` (qualified) as `[type, buffer name]`, or nil when no
|
|
2484
|
+
# signature declares it. `rigor lens` reports the declared type with the provenance the buffer names.
|
|
2485
|
+
def constant_declaration(name)
|
|
2486
|
+
rbs_name = parse_type_name(name)
|
|
2487
|
+
entry = rbs_name && env&.constant_decls&.[](rbs_name)
|
|
2488
|
+
return nil unless entry
|
|
2489
|
+
|
|
2490
|
+
[entry.decl.type.to_s, entry.decl.location&.buffer&.name.to_s]
|
|
2491
|
+
rescue ::RBS::BaseError
|
|
2492
|
+
nil
|
|
2493
|
+
end
|
|
2494
|
+
|
|
2474
2495
|
# Slice A constant-value lookup. Returns the translated `Rigor::Type` for a non-class constant
|
|
2475
2496
|
# declaration (`BUCKETS: Array[Symbol]`, `DEFAULT_PATH: String`, ...) or `nil` when no constant entry
|
|
2476
2497
|
# exists for `name` in the loaded RBS environment. Callers MUST treat the return value as authoritative
|
|
@@ -2904,15 +2925,16 @@ module Rigor
|
|
|
2904
2925
|
#
|
|
2905
2926
|
# So the rescue only remembers, keyed by the `::`-stripped name (the eager walk spells it `::Acme`,
|
|
2906
2927
|
# a dispatch site spells it `Acme`, and they are one class — the {#synthesized_namespaces}
|
|
2907
|
-
# convention), and {#report_definition_build_failure} decides.
|
|
2908
|
-
#
|
|
2928
|
+
# convention), and {#report_definition_build_failure} decides. Every side's detail is kept, not the
|
|
2929
|
+
# first one written (issue #1902): the instance and singleton sides can fail for DIFFERENT reasons
|
|
2930
|
+
# (a duplicated `def self.s` and a duplicated `def i` on one class), and which side a run demands first
|
|
2931
|
+
# depends on which files it analysed, so first-writer-wins made the reported tuple — and with it the
|
|
2932
|
+
# message's "First failure", advice and file list — differ between a cold run and a warm recheck.
|
|
2909
2933
|
def store_definition_build_detail(class_name, error)
|
|
2910
2934
|
key = class_name.to_s.delete_prefix("::")
|
|
2911
|
-
details = (@state[:definition_build_details] ||= {})
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
details[key] = [key, error.class.name.to_s, definition_build_member(error),
|
|
2915
|
-
definition_build_conflict_buffers(error), unresolved_type_name_detail(error)].freeze
|
|
2935
|
+
details = ((@state[:definition_build_details] ||= {})[key] ||= [])
|
|
2936
|
+
details << [key, error.class.name.to_s, definition_build_member(error),
|
|
2937
|
+
definition_build_conflict_buffers(error), unresolved_type_name_detail(error)].freeze
|
|
2916
2938
|
end
|
|
2917
2939
|
|
|
2918
2940
|
# Issue #997 — the `RBS::NoTypeFoundError` twin of {#definition_build_member}: `nil` for every other
|
|
@@ -2987,14 +3009,28 @@ module Rigor
|
|
|
2987
3009
|
return if details.nil?
|
|
2988
3010
|
|
|
2989
3011
|
key = class_name.to_s.delete_prefix("::")
|
|
2990
|
-
|
|
2991
|
-
return if detail.nil?
|
|
3012
|
+
return if details[key].nil?
|
|
2992
3013
|
|
|
2993
3014
|
reported = (@state[:definition_build_reported] ||= {})
|
|
2994
3015
|
return if reported[key]
|
|
2995
3016
|
|
|
2996
3017
|
reported[key] = true
|
|
2997
|
-
(
|
|
3018
|
+
attempt_both_definition_sides(class_name)
|
|
3019
|
+
canonical = details[key].min_by { |candidate| self.class.definition_build_detail_order(candidate) }
|
|
3020
|
+
(@state[:definition_build_failures] ||= []) << canonical
|
|
3021
|
+
end
|
|
3022
|
+
|
|
3023
|
+
# Issue #1902 — a reported class's tuple is chosen from BOTH sides' failures, so the side this demand
|
|
3024
|
+
# did not ask for is built here too. Without it, the candidate set would be whatever sides the run
|
|
3025
|
+
# happened to demand, and a recheck whose closure touches only `Berry.s` would report the singleton
|
|
3026
|
+
# side's reason where the full run, which also demanded `Berry.new.i`, reported the instance side's.
|
|
3027
|
+
# An internal demand: it decides WHICH reason is shown for a class the analysis already demanded, never
|
|
3028
|
+
# WHETHER a class is reported, so the set #696 binds to the analysis's own demand is unchanged.
|
|
3029
|
+
def attempt_both_definition_sides(class_name)
|
|
3030
|
+
during_internal_demand do
|
|
3031
|
+
instance_definition(class_name)
|
|
3032
|
+
singleton_definition(class_name)
|
|
3033
|
+
end
|
|
2998
3034
|
end
|
|
2999
3035
|
|
|
3000
3036
|
# Marks a definition demand that is RIGOR'S OWN, not the analysed program's, so
|
|
@@ -165,7 +165,12 @@ module Rigor
|
|
|
165
165
|
# nothing for names this rare, and the block-return pass is no independent check here: it types a
|
|
166
166
|
# self-call to an overridden `raise` as Kernel's `bot` too, so a missed override is a wrong `bot`.
|
|
167
167
|
# {LastLine.reads_line?} declines on it the same way for `gets` and `readline`.
|
|
168
|
+
#
|
|
169
|
+
# Issue #1459 — the answer reads every project file's tables whole, so it records a `defines:<name>` edge
|
|
170
|
+
# whichever way it answers: an edit that makes a file start or stop defining the name re-checks the reader
|
|
171
|
+
# (`Incremental.changed_defined_names`). A `pre_eval:` patch moves the snapshot's own fingerprint.
|
|
168
172
|
def project_defines_anywhere?(method_name, scope)
|
|
173
|
+
Analysis::DependencyRecorder.read_missing(:defines, method_name) if Analysis::DependencyRecorder.active?
|
|
169
174
|
return true if scope.top_level_def_for(method_name)
|
|
170
175
|
# `discovered_methods` withholds a plain cross-file `def` (the ADR-17 monkey-patch contract); the
|
|
171
176
|
# def-node tables still carry it, on both sides.
|
|
@@ -9,6 +9,7 @@ require_relative "index_write_widening"
|
|
|
9
9
|
require_relative "mutation_widening"
|
|
10
10
|
require_relative "optimistic_origin"
|
|
11
11
|
require_relative "receiver_alias"
|
|
12
|
+
require_relative "reflective_ivar_write"
|
|
12
13
|
require_relative "unknown_store_widening"
|
|
13
14
|
|
|
14
15
|
module Rigor
|
|
@@ -129,6 +130,9 @@ module Rigor
|
|
|
129
130
|
if LOCAL_WRITE_NODES.include?(descendant.class)
|
|
130
131
|
captured_local_write(descendant, ancestors, base_scope) { introduced ||= introduced_locals(block_node) }
|
|
131
132
|
elsif outliving
|
|
133
|
+
if ReflectiveIvarWrite.unknown_target?(descendant)
|
|
134
|
+
(names ||= []).concat(reflective_unknown_ivars(base_scope))
|
|
135
|
+
end
|
|
132
136
|
rebindable_outliving_write(descendant, base_scope, non_locals)
|
|
133
137
|
end
|
|
134
138
|
(names ||= []) << name if name
|
|
@@ -136,6 +140,13 @@ module Rigor
|
|
|
136
140
|
names ? names.uniq : NO_NAMES
|
|
137
141
|
end
|
|
138
142
|
|
|
143
|
+
# Issue #1889 — every ivar `base_scope` binds: an `instance_variable_set` on `self` whose target no literal
|
|
144
|
+
# names ({ReflectiveIvarWrite.unknown_target?}) may write any of them. One on its ADR-58 declaration seed
|
|
145
|
+
# counts too, since the census behind the seed sees no reflective write; {.bind} keeps its mark.
|
|
146
|
+
def reflective_unknown_ivars(base_scope)
|
|
147
|
+
base_scope.ivars.keys
|
|
148
|
+
end
|
|
149
|
+
|
|
139
150
|
def rebindable_outliving_write(node, base_scope, non_locals)
|
|
140
151
|
name = outliving_write_name(node, non_locals)
|
|
141
152
|
name if name && rebindable_non_local?(base_scope, name)
|
|
@@ -151,8 +162,12 @@ module Rigor
|
|
|
151
162
|
yield.include?(name) ? nil : name
|
|
152
163
|
end
|
|
153
164
|
|
|
154
|
-
# The non-local `node` rebinds under `non_locals:` — or, without it, the instance variable only.
|
|
165
|
+
# The non-local `node` rebinds under `non_locals:` — or, without it, the instance variable only. A literal-name
|
|
166
|
+
# `instance_variable_set` on `self` ({ReflectiveIvarWrite.literal_name}) is a write of that ivar under either.
|
|
155
167
|
def outliving_write_name(node, non_locals)
|
|
168
|
+
reflective = ReflectiveIvarWrite.literal_name(node)
|
|
169
|
+
return reflective if reflective
|
|
170
|
+
|
|
156
171
|
name = non_local_write_name(node)
|
|
157
172
|
non_locals || (name && ivar_name?(name) && NON_LOCAL_WRITE_NODES.include?(node.class)) ? name : nil
|
|
158
173
|
end
|
|
@@ -201,13 +216,21 @@ module Rigor
|
|
|
201
216
|
# it the miss answer the mark records (issue #1302).
|
|
202
217
|
def bind(scope, name, type, optimistic: nil)
|
|
203
218
|
case variable_kind(name)
|
|
204
|
-
when :ivar then
|
|
219
|
+
when :ivar then bind_ivar(scope, name, type, optimistic)
|
|
205
220
|
when :cvar then scope.with_cvar(name, type)
|
|
206
221
|
when :global then scope.with_global(name, type)
|
|
207
222
|
else OptimisticOrigin.carry_local_mark(scope, scope.with_local(name, type), name, optimistic)
|
|
208
223
|
end
|
|
209
224
|
end
|
|
210
225
|
|
|
226
|
+
# An ivar on its ADR-58 declaration seed reaches {.bind} through {.reflective_unknown_ivars}, and through
|
|
227
|
+
# `ExpressionTyper#prefix_answered_scope`; rebinding it re-stamps the declaration mark, as
|
|
228
|
+
# `ReflectiveIvarWrite.apply` does, so a nil in the seed fuels no nil-receiver diagnostic.
|
|
229
|
+
def bind_ivar(scope, name, type, optimistic)
|
|
230
|
+
bound = OptimisticOrigin.carry_ivar_mark(scope, scope.with_ivar(name, type), name, optimistic)
|
|
231
|
+
scope.declaration_sourced?(:ivar, name) ? bound.seed_declaration_sourced_ivar(name, type) : bound
|
|
232
|
+
end
|
|
233
|
+
|
|
211
234
|
# The optimistic nil-freeness mark `scope` holds for a name from {.writes}, or nil.
|
|
212
235
|
def optimistic_mark(scope, name)
|
|
213
236
|
case variable_kind(name)
|
|
@@ -290,7 +313,7 @@ module Rigor
|
|
|
290
313
|
receiver = node.receiver
|
|
291
314
|
return true if receiver && MutationWidening::SHAPE_MUTATORS.include?(node.name)
|
|
292
315
|
|
|
293
|
-
callee_call?(node)
|
|
316
|
+
ReflectiveIvarWrite.self_write?(node) || callee_call?(node)
|
|
294
317
|
end
|
|
295
318
|
|
|
296
319
|
# Issue #1412 — {.content_mutations} for a `while` / `until` / `for` body: the locals bound in `base_scope`
|
|
@@ -39,6 +39,9 @@ module Rigor
|
|
|
39
39
|
Hit = Data.define(:answer, :owner, :index, :side)
|
|
40
40
|
|
|
41
41
|
QUESTIONS = %i[definer visibility arity override own_side].freeze
|
|
42
|
+
# The questions whose answer reads a recorded visibility: only they treat a contested visibility slot as a
|
|
43
|
+
# possible definer ({#possible?}).
|
|
44
|
+
VISIBILITY_QUESTIONS = %i[visibility override].freeze
|
|
42
45
|
NONE = :rigor_definer_resolution_none
|
|
43
46
|
STRAY = :rigor_definer_resolution_stray
|
|
44
47
|
private_constant :NONE, :STRAY
|
|
@@ -58,12 +61,13 @@ module Rigor
|
|
|
58
61
|
flavor = question == :arity ? :arity : :methods
|
|
59
62
|
chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, flavor)
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
reads_visibility = VISIBILITY_QUESTIONS.include?(question)
|
|
65
|
+
hits = candidates(scope, chain, method_name, from, answer_in, reads_visibility)
|
|
62
66
|
return UNKNOWN if hits.nil?
|
|
63
67
|
|
|
64
68
|
verdict = chain.settle(scope, outcomes(hits), owner: hits.first&.owner, unknown_for: method_name) do |retro|
|
|
65
69
|
retro_from = retro_position(chain, retro, from)
|
|
66
|
-
outcomes(retro_from && candidates(scope, retro, method_name, retro_from, answer_in))
|
|
70
|
+
outcomes(retro_from && candidates(scope, retro, method_name, retro_from, answer_in, reads_visibility))
|
|
67
71
|
end
|
|
68
72
|
return UNKNOWN unless verdict == :chain
|
|
69
73
|
return UNKNOWN if side == :singleton && SingletonHookDecline.decline?(scope, chain, method_name, from, hits)
|
|
@@ -101,7 +105,7 @@ module Rigor
|
|
|
101
105
|
# follows (the absent candidate). A cut chain, and a chain on which an external ancestor ahead of the
|
|
102
106
|
# candidate (or, with no candidate, the implicit `Object`) may define the name, is a decline, answered by
|
|
103
107
|
# nil instead of a set.
|
|
104
|
-
def candidates(scope, chain, method_name, from, answer_in)
|
|
108
|
+
def candidates(scope, chain, method_name, from, answer_in, reads_visibility)
|
|
105
109
|
hits = []
|
|
106
110
|
position = from
|
|
107
111
|
loop do
|
|
@@ -113,7 +117,9 @@ module Rigor
|
|
|
113
117
|
return nil if hit.nil? && chain.truncated?
|
|
114
118
|
|
|
115
119
|
hits << hit
|
|
116
|
-
|
|
120
|
+
if hit.nil? || !possible?(scope, hit, method_name, reads_visibility)
|
|
121
|
+
return decided(scope, chain, method_name, from, hits)
|
|
122
|
+
end
|
|
117
123
|
|
|
118
124
|
position = hit.index + 1
|
|
119
125
|
end
|
|
@@ -188,15 +194,18 @@ module Rigor
|
|
|
188
194
|
end
|
|
189
195
|
|
|
190
196
|
# A definer that rests on a fact the walk could not prove executes: the member's `possible` sibling, or a
|
|
191
|
-
# contested slot of one of the single-valued members.
|
|
192
|
-
|
|
197
|
+
# contested slot of one of the single-valued members. A contested visibility slot counts only for a question
|
|
198
|
+
# that reads the visibility (`reads_visibility`): a slot is contested there for a visibility call the walk
|
|
199
|
+
# cannot place (an uncertain toggle, `private(*names)`, `private def self.x`), which leaves the definer and its
|
|
200
|
+
# `def` certain; a possible `def` or `attr_*` call contests its def-node slot or its name's `possible` side too.
|
|
201
|
+
def possible?(scope, hit, method_name, reads_visibility)
|
|
193
202
|
discovery = scope.discovery
|
|
194
203
|
owner = hit.owner
|
|
195
204
|
kind = hit.side == :singleton ? :singleton : :instance
|
|
196
205
|
nodes = kind == :singleton ? :discovered_singleton_def_nodes : :discovered_def_nodes
|
|
197
206
|
discovery.possible_method?(owner, method_name, kind) ||
|
|
198
207
|
discovery.contested?(nodes, [owner, method_name.to_sym]) ||
|
|
199
|
-
discovery.contested?(:discovered_method_visibilities, [owner, method_name.to_sym]) ||
|
|
208
|
+
(reads_visibility && discovery.contested?(:discovered_method_visibilities, [owner, method_name.to_sym])) ||
|
|
200
209
|
discovery.contested?(:discovered_parameter_envelopes, [owner, [kind, method_name.to_sym]])
|
|
201
210
|
end
|
|
202
211
|
|