rigortype 0.4.2 → 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 +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +26 -9
- 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 +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- 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 +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- 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-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -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
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +21 -1
|
@@ -35,11 +35,12 @@ module Rigor
|
|
|
35
35
|
found.freeze
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
# `<bundle>/ruby/X.Y.Z/gems/<name>-<version
|
|
38
|
+
# `<bundle>/ruby/X.Y.Z/gems/<name>-<version>[-<platform>]/` — the same pure-filesystem layout
|
|
39
39
|
# {MissingGemConstantIndex.bundle_gem_dirs} walks, preferred over the host's gems so a project that
|
|
40
40
|
# vendored its bundle without committing the lockfile is read from its own tree. A platform-tagged
|
|
41
|
-
# directory (`
|
|
42
|
-
#
|
|
41
|
+
# directory (`commonmarker-2.10.0-arm64-darwin`) is kept with its platform as a variant: those builds
|
|
42
|
+
# ship the same `lib/<name>.rb` and top-level namespace as the `ruby` one, and the index looks the
|
|
43
|
+
# directory up by that variant. A name installed more than once keeps every directory as a variant.
|
|
43
44
|
def from_bundle_tree(bundle_path)
|
|
44
45
|
return {} if bundle_path.nil?
|
|
45
46
|
|
|
@@ -47,10 +48,15 @@ module Rigor
|
|
|
47
48
|
return {} unless base.directory?
|
|
48
49
|
|
|
49
50
|
Dir.glob(base.join("ruby", "*", "gems", "*")).each_with_object({}) do |dir, acc|
|
|
50
|
-
name, version =
|
|
51
|
+
name, version, platform = split_install_dirname(File.basename(dir))
|
|
51
52
|
next unless name
|
|
52
53
|
|
|
53
|
-
acc[name]
|
|
54
|
+
prior = acc[name]
|
|
55
|
+
acc[name] = if prior
|
|
56
|
+
prior.with(variants: [*prior.variants, [version, platform]])
|
|
57
|
+
else
|
|
58
|
+
locked_gem(name, version, platform)
|
|
59
|
+
end
|
|
54
60
|
end
|
|
55
61
|
end
|
|
56
62
|
|
|
@@ -70,15 +76,36 @@ module Rigor
|
|
|
70
76
|
{}
|
|
71
77
|
end
|
|
72
78
|
|
|
73
|
-
def locked_gem(name, version)
|
|
74
|
-
LockfileResolver::LockedGem.new(name: name, version: version, platform:
|
|
79
|
+
def locked_gem(name, version, platform = "ruby")
|
|
80
|
+
LockfileResolver::LockedGem.new(name: name, version: version, platform: platform)
|
|
75
81
|
end
|
|
76
82
|
|
|
77
|
-
# Splits a gem install directory's basename
|
|
78
|
-
#
|
|
79
|
-
|
|
83
|
+
# Splits a gem install directory's basename into `[name, version, platform]`, or nil. The version
|
|
84
|
+
# always starts with a digit and carries no hyphen; a platform tail is accepted only when RubyGems
|
|
85
|
+
# recognises its OS (`arm64-darwin`, `x86_64-linux-gnu`, `java`), so `foo-2fa-1.0` stays the plain gem
|
|
86
|
+
# `foo-2fa`. The platform split is tried first, because a plain split of `foo-1.0-universal-darwin-23`
|
|
87
|
+
# would claim the name `foo-1.0-universal-darwin`; and from the rightmost version-shaped segment,
|
|
88
|
+
# because RubyGems also reads `1.0-x86_64-linux` as a linux platform, which would cut
|
|
89
|
+
# `foo-2fa-1.0-x86_64-linux` at `foo`.
|
|
90
|
+
def split_install_dirname(basename)
|
|
91
|
+
platform_split(basename) || plain_split(basename)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def platform_split(basename)
|
|
95
|
+
cuts = []
|
|
96
|
+
basename.scan(/-(\d[^-]*)(?=-.)/) { cuts << Regexp.last_match }
|
|
97
|
+
cuts.reverse_each do |match|
|
|
98
|
+
platform = match.post_match.delete_prefix("-")
|
|
99
|
+
# `current` makes `Gem::Platform.new` read the host's platform, which a non-main Ractor may not touch.
|
|
100
|
+
next if platform == "current"
|
|
101
|
+
return [match.pre_match, match[1], platform] unless Gem::Platform.new(platform).os == "unknown"
|
|
102
|
+
end
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def plain_split(basename)
|
|
80
107
|
match = /\A(.+)-(\d[^-]*)\z/.match(basename)
|
|
81
|
-
match ? [match[1], match[2]] : nil
|
|
108
|
+
match ? [match[1], match[2], "ruby"] : nil
|
|
82
109
|
end
|
|
83
110
|
end
|
|
84
111
|
end
|
|
@@ -30,13 +30,29 @@ module Rigor
|
|
|
30
30
|
# dependency that moved from a `git:` source to a released version without a `bundle clean`, so its
|
|
31
31
|
# git-installed directory is still sitting there under the OLD source's name). Defaults `false` so
|
|
32
32
|
# existing callers that construct a `LockedGem` without an opinion stay conservative.
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
#
|
|
34
|
+
# `variants` is every `[version, platform]` pair the lockfile locks under this name, in Bundler's parsed
|
|
35
|
+
# order. A multi-platform lockfile (`bundle lock --add-platform`) lists one spec per platform — `ffi
|
|
36
|
+
# (1.17.4)`, `ffi (1.17.4-arm64-darwin)`, … — and Bundler installs whichever matches the host, so
|
|
37
|
+
# {BundleSigDiscovery} must admit all of them (#1844). `version` / `platform` name one representative
|
|
38
|
+
# variant for the readers that want a single version (coverage report, doctor): the `ruby` one when the
|
|
39
|
+
# lockfile has it, otherwise the last parsed. Defaults to the single `[version, platform]` pair, and the
|
|
40
|
+
# pair must be one of `variants`.
|
|
41
|
+
LockedGem = Data.define(:name, :version, :platform, :git_source, :variants) do
|
|
42
|
+
def initialize(name:, version:, platform:, git_source: false, variants: nil)
|
|
43
|
+
version = -version.to_s
|
|
44
|
+
platform = -platform.to_s
|
|
45
|
+
variants = (variants || [[version, platform]]).map { |v, p| [-v.to_s, -p.to_s].freeze }.uniq.freeze
|
|
46
|
+
unless variants.include?([version, platform])
|
|
47
|
+
raise ArgumentError, "LockedGem #{name}: [#{version}, #{platform}] is not one of its variants"
|
|
48
|
+
end
|
|
49
|
+
|
|
35
50
|
super(
|
|
36
51
|
name: -name.to_s,
|
|
37
|
-
version:
|
|
38
|
-
platform:
|
|
39
|
-
git_source: git_source ? true : false
|
|
52
|
+
version: version,
|
|
53
|
+
platform: platform,
|
|
54
|
+
git_source: git_source ? true : false,
|
|
55
|
+
variants: variants
|
|
40
56
|
)
|
|
41
57
|
end
|
|
42
58
|
end
|
|
@@ -162,10 +178,13 @@ module Rigor
|
|
|
162
178
|
# `#source` is the `Bundler::Source::*` this spec resolved from — `Git` for a `GIT` lockfile
|
|
163
179
|
# stanza, `Rubygems` for a `GEM` one, `Path`/`Gemspec` for `path:`. Only `Git` sources land under
|
|
164
180
|
# {BundleSigDiscovery}'s `bundler/gems/` walk.
|
|
165
|
-
|
|
181
|
+
# A multi-platform lockfile repeats the name once per platform; fold the repeats into `variants`
|
|
182
|
+
# instead of letting the last one overwrite the rest (#1844).
|
|
183
|
+
entry = LockedGem.new(
|
|
166
184
|
name: spec.name, version: spec.version.to_s, platform: platform,
|
|
167
185
|
git_source: spec.source.is_a?(Bundler::Source::Git)
|
|
168
186
|
)
|
|
187
|
+
h[entry.name] = (prior = h[entry.name]) ? merge_variant(prior, entry) : entry
|
|
169
188
|
end
|
|
170
189
|
locked.freeze
|
|
171
190
|
rescue ::Ractor::IsolationError => e
|
|
@@ -177,6 +196,18 @@ module Rigor
|
|
|
177
196
|
end
|
|
178
197
|
private_class_method :do_parse
|
|
179
198
|
|
|
199
|
+
# Folds a further platform variant of an already-parsed name into it. The representative keeps a `ruby`
|
|
200
|
+
# variant once seen; otherwise the last variant wins, as it did before variants were kept.
|
|
201
|
+
def self.merge_variant(prior, entry)
|
|
202
|
+
rep = prior.platform == "ruby" ? prior : entry
|
|
203
|
+
prior.with(
|
|
204
|
+
variants: [*prior.variants, [entry.version, entry.platform]],
|
|
205
|
+
version: rep.version, platform: rep.platform,
|
|
206
|
+
git_source: prior.git_source || entry.git_source
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
private_class_method :merge_variant
|
|
210
|
+
|
|
180
211
|
# #1064 — Bundler's lockfile parser reads RubyGems module state a non-main Ractor may not touch, so the
|
|
181
212
|
# parse raises `Ractor::IsolationError` there whatever the lockfile says. That is an engine constraint, not
|
|
182
213
|
# a malformed lockfile, and the warning must not send the user to fix a file that is fine. The Ractor pool
|
|
@@ -37,8 +37,8 @@ module Rigor
|
|
|
37
37
|
module MissingGemConstantIndex
|
|
38
38
|
module_function
|
|
39
39
|
|
|
40
|
-
# @param gems — `[gem_name, version]`
|
|
41
|
-
#
|
|
40
|
+
# @param gems — `[gem_name, version, variants]` entries (the `:missing` rows of {RbsCoverageReport},
|
|
41
|
+
# with the gem's `LockedGem#variants`); `variants` may be omitted or nil, meaning the `ruby` one.
|
|
42
42
|
# @param bundle_path — the target's resolved bundler install root, or nil.
|
|
43
43
|
# @param spec_resolver — `(name, version) -> String?` fallback dir resolver. Injectable for
|
|
44
44
|
# specs; the default is the RubyGems-metadata lookup (no code load).
|
|
@@ -49,8 +49,9 @@ module Rigor
|
|
|
49
49
|
bundle_dirs = bundle_gem_dirs(bundle_path)
|
|
50
50
|
git_dirs = git_gem_dirs(bundle_path)
|
|
51
51
|
index = {}
|
|
52
|
-
gems.each do |gem_name, version|
|
|
53
|
-
dir = bundle_dirs
|
|
52
|
+
gems.each do |gem_name, version, variants|
|
|
53
|
+
dir = bundle_dir(bundle_dirs, gem_name, version, variants) ||
|
|
54
|
+
git_dirs[gem_name] || spec_resolver.call(gem_name, version)
|
|
54
55
|
next unless dir
|
|
55
56
|
|
|
56
57
|
entry_files(dir, gem_name).each do |file|
|
|
@@ -62,9 +63,8 @@ module Rigor
|
|
|
62
63
|
|
|
63
64
|
# `{"<name>-<version>" => gem_dir}` for the target's bundle, or `{}` when no bundle is resolvable. The
|
|
64
65
|
# glob covers the RUBYGEMS-sourced layout only, `<bundle>/ruby/X.Y.Z/gems/<name>-<version>/`; the
|
|
65
|
-
# git-sourced layout is {.git_gem_dirs}'s. Keyed on the dir basename
|
|
66
|
-
#
|
|
67
|
-
# native code, not the pure-Ruby constants this indexes.
|
|
66
|
+
# git-sourced layout is {.git_gem_dirs}'s. Keyed on the dir basename, which {.bundle_dir} reconstructs
|
|
67
|
+
# per locked variant.
|
|
68
68
|
def bundle_gem_dirs(bundle_path)
|
|
69
69
|
return {} if bundle_path.nil?
|
|
70
70
|
|
|
@@ -76,6 +76,21 @@ module Rigor
|
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# The installed directory of one of the gem's locked variants: `<name>-<version>` for the `ruby` one,
|
|
80
|
+
# `<name>-<version>-<platform>` for a platform-tagged one. Bundler installs only the variant matching
|
|
81
|
+
# the host, and a multi-platform lockfile's representative is usually the `ruby` one, so every variant
|
|
82
|
+
# is tried. A platform-tagged gem is not a native-only gem: the commonmarker, nokogiri and sqlite3 builds
|
|
83
|
+
# each ship `lib/<name>.rb` declaring the gem's top-level namespace, exactly as the `ruby` build does.
|
|
84
|
+
def bundle_dir(bundle_dirs, gem_name, version, variants)
|
|
85
|
+
[[version, "ruby"], *variants].each do |variant_version, platform|
|
|
86
|
+
basename = "#{gem_name}-#{variant_version}"
|
|
87
|
+
basename = "#{basename}-#{platform}" unless platform == "ruby"
|
|
88
|
+
dir = bundle_dirs[basename]
|
|
89
|
+
return dir if dir
|
|
90
|
+
end
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
79
94
|
# Issue #763 — `{gem_name => gem_dir}` for the target's GIT-sourced gems,
|
|
80
95
|
# `<bundle>/ruby/X.Y.Z/bundler/gems/<repo>-<12-hex-revision>/`. Before this, a `git:`-sourced gem was
|
|
81
96
|
# invisible to the index, so every constant reaching into it recorded the generic engine-gap cause
|
|
@@ -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
|
|
@@ -2321,6 +2330,39 @@ module Rigor
|
|
|
2321
2330
|
[].freeze
|
|
2322
2331
|
end
|
|
2323
2332
|
|
|
2333
|
+
# Issue #1752 — the lookup of `class_name`'s class object as RBS declares it, the singleton twin of
|
|
2334
|
+
# {#ancestor_names_for}: `[:singleton, name]` for each singleton class on it (`C`'s, then each superclass's)
|
|
2335
|
+
# and `[:instance, name]` for each module or class whose instance methods it reaches (the declared `extend`s,
|
|
2336
|
+
# then `Class`, `Module`, `Object`, `Kernel` and `BasicObject` for a class, `Module` onwards for a module), in
|
|
2337
|
+
# lookup order. A refinement of `Object` or `Module` answers a call on a class object through the second kind.
|
|
2338
|
+
#
|
|
2339
|
+
# Marked as an internal demand, as {#ancestor_names_for} is (issue #696). Memoised per name: the environment is
|
|
2340
|
+
# immutable for the loader's lifetime.
|
|
2341
|
+
#
|
|
2342
|
+
# @return `::`-stripped pairs, or `[]` for an unknown class and on any RBS build error.
|
|
2343
|
+
def singleton_ancestors_for(class_name)
|
|
2344
|
+
key = class_name.to_s.delete_prefix("::")
|
|
2345
|
+
memo = (@state[:singleton_ancestors] ||= {})
|
|
2346
|
+
return memo[key] if memo.key?(key)
|
|
2347
|
+
|
|
2348
|
+
memo[key] = build_singleton_ancestors(key)
|
|
2349
|
+
end
|
|
2350
|
+
|
|
2351
|
+
def build_singleton_ancestors(key)
|
|
2352
|
+
during_internal_demand do
|
|
2353
|
+
definition = singleton_definition(key)
|
|
2354
|
+
next [].freeze if definition.nil?
|
|
2355
|
+
|
|
2356
|
+
definition.ancestors.ancestors.map do |ancestor|
|
|
2357
|
+
kind = ancestor.is_a?(::RBS::Definition::Ancestor::Singleton) ? :singleton : :instance
|
|
2358
|
+
[kind, ancestor.name.to_s.delete_prefix("::")].freeze
|
|
2359
|
+
end.uniq.freeze
|
|
2360
|
+
end
|
|
2361
|
+
rescue ::RBS::BaseError, StandardError
|
|
2362
|
+
[].freeze
|
|
2363
|
+
end
|
|
2364
|
+
private :build_singleton_ancestors
|
|
2365
|
+
|
|
2324
2366
|
# @return the class method on `class_name`. For example,
|
|
2325
2367
|
# `singleton_method(class_name: "Integer", method_name: :sqrt)` returns the definition for
|
|
2326
2368
|
# `Integer.sqrt`, while `singleton_method(class_name: "Foo", method_name: :new)` returns Class#new
|
|
@@ -2438,6 +2480,18 @@ module Rigor
|
|
|
2438
2480
|
# fail-soft: a broken RBS environment yields no entries.
|
|
2439
2481
|
end
|
|
2440
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
|
+
|
|
2441
2495
|
# Slice A constant-value lookup. Returns the translated `Rigor::Type` for a non-class constant
|
|
2442
2496
|
# declaration (`BUCKETS: Array[Symbol]`, `DEFAULT_PATH: String`, ...) or `nil` when no constant entry
|
|
2443
2497
|
# exists for `name` in the loaded RBS environment. Callers MUST treat the return value as authoritative
|
|
@@ -2871,15 +2925,16 @@ module Rigor
|
|
|
2871
2925
|
#
|
|
2872
2926
|
# So the rescue only remembers, keyed by the `::`-stripped name (the eager walk spells it `::Acme`,
|
|
2873
2927
|
# a dispatch site spells it `Acme`, and they are one class — the {#synthesized_namespaces}
|
|
2874
|
-
# convention), and {#report_definition_build_failure} decides.
|
|
2875
|
-
#
|
|
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.
|
|
2876
2933
|
def store_definition_build_detail(class_name, error)
|
|
2877
2934
|
key = class_name.to_s.delete_prefix("::")
|
|
2878
|
-
details = (@state[:definition_build_details] ||= {})
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
details[key] = [key, error.class.name.to_s, definition_build_member(error),
|
|
2882
|
-
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
|
|
2883
2938
|
end
|
|
2884
2939
|
|
|
2885
2940
|
# Issue #997 — the `RBS::NoTypeFoundError` twin of {#definition_build_member}: `nil` for every other
|
|
@@ -2954,14 +3009,28 @@ module Rigor
|
|
|
2954
3009
|
return if details.nil?
|
|
2955
3010
|
|
|
2956
3011
|
key = class_name.to_s.delete_prefix("::")
|
|
2957
|
-
|
|
2958
|
-
return if detail.nil?
|
|
3012
|
+
return if details[key].nil?
|
|
2959
3013
|
|
|
2960
3014
|
reported = (@state[:definition_build_reported] ||= {})
|
|
2961
3015
|
return if reported[key]
|
|
2962
3016
|
|
|
2963
3017
|
reported[key] = true
|
|
2964
|
-
(
|
|
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
|
|
2965
3034
|
end
|
|
2966
3035
|
|
|
2967
3036
|
# Marks a definition demand that is RIGOR'S OWN, not the analysed program's, so
|
|
@@ -51,7 +51,8 @@ module Rigor
|
|
|
51
51
|
#
|
|
52
52
|
# @param sources — path => source text, read in place of that path's file (an editor buffer).
|
|
53
53
|
def for_configuration(configuration, extra_files = [], sources: {})
|
|
54
|
-
files = Analysis::PathExpansion.ruby_files(configuration.paths, configuration.exclude_patterns
|
|
54
|
+
files = Analysis::PathExpansion.ruby_files(configuration.paths, configuration.exclude_patterns,
|
|
55
|
+
base: configuration.exclude_base)
|
|
55
56
|
scan(files + Array(extra_files).map(&:to_s), sources: sources)
|
|
56
57
|
rescue StandardError
|
|
57
58
|
[]
|
|
@@ -77,7 +78,8 @@ module Rigor
|
|
|
77
78
|
# @return path (absolute) => the features that file requires, for every file under the configured paths
|
|
78
79
|
# that requires any. A long-lived reader (the language server) keeps this and overlays its open buffers.
|
|
79
80
|
def feature_files(configuration)
|
|
80
|
-
listed = Analysis::PathExpansion.ruby_files(configuration.paths, configuration.exclude_patterns
|
|
81
|
+
listed = Analysis::PathExpansion.ruby_files(configuration.paths, configuration.exclude_patterns,
|
|
82
|
+
base: configuration.exclude_base)
|
|
81
83
|
files = listed.map { |path| File.expand_path(path.to_s) }
|
|
82
84
|
result = files.each_with_object({}) do |path, acc|
|
|
83
85
|
features = memoized_features_in(path)
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -97,7 +97,7 @@ module Rigor
|
|
|
97
97
|
# for the overlay, instead of aborting the run outright (see {#hkt_registry}).
|
|
98
98
|
@hkt_scan_failure = FailureSlot.new
|
|
99
99
|
@constant_type_cache = ConstantTypeCacheHolder.new
|
|
100
|
-
# ADR-82 WD9 — `[gem_name, version]`
|
|
100
|
+
# ADR-82 WD9 — `[gem_name, version, variants]` entries for the locked gems with no resolvable RBS. The
|
|
101
101
|
# root-constant ownership index over them is built lazily (first unresolved constant read) so runs
|
|
102
102
|
# whose constants all resolve never pay the entry-file scan.
|
|
103
103
|
@missing_rbs_gems = missing_rbs_gems.freeze
|
|
@@ -444,8 +444,8 @@ module Rigor
|
|
|
444
444
|
end
|
|
445
445
|
|
|
446
446
|
# The `:missing` classification (locked gems with no RBS through any resolution path), computed once
|
|
447
|
-
# and consumed twice: as `[gem_name, version]`
|
|
448
|
-
# as the eligible-gem set for the ADR-72 overlay resolution. Returns `[
|
|
447
|
+
# and consumed twice: as `[gem_name, version, variants]` entries for the ADR-82 WD9 constant-ownership
|
|
448
|
+
# index, and as the eligible-gem set for the ADR-72 overlay resolution. Returns `[entries, overlay_paths]`.
|
|
449
449
|
def missing_gems_and_overlay_paths(locked:, default_libraries:, bundle_sig_paths:,
|
|
450
450
|
rbs_collection_paths:, plugin_registry:, signature_paths: [],
|
|
451
451
|
bundle_root: nil)
|
|
@@ -458,7 +458,7 @@ module Rigor
|
|
|
458
458
|
missing_gem_names: rows.map(&:gem_name), plugin_registry: plugin_registry,
|
|
459
459
|
signature_paths: signature_paths
|
|
460
460
|
)
|
|
461
|
-
[rows
|
|
461
|
+
[index_entries(rows, locked), overlays]
|
|
462
462
|
end
|
|
463
463
|
|
|
464
464
|
# Issue #530 — a project with no `Gemfile.lock` still has gems on disk, and before this it handed the
|
|
@@ -472,7 +472,14 @@ module Rigor
|
|
|
472
472
|
return [[], []] if installed.empty?
|
|
473
473
|
|
|
474
474
|
rows = missing_rows(installed, default_libraries, bundle_sig_paths, rbs_collection_paths)
|
|
475
|
-
[rows
|
|
475
|
+
[index_entries(rows, installed), []]
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# The constant-ownership index's input: each `:missing` row with every `[version, platform]` variant its
|
|
479
|
+
# gem was locked (or found installed) under, because Bundler installs the variant matching the host —
|
|
480
|
+
# `commonmarker-2.10.0-arm64-darwin/` under a lockfile whose representative is the `ruby` one (#1844).
|
|
481
|
+
def index_entries(rows, gems)
|
|
482
|
+
rows.map { |row| [row.gem_name, row.version, gems[row.gem_name]&.variants] }
|
|
476
483
|
end
|
|
477
484
|
|
|
478
485
|
def missing_rows(gems, default_libraries, bundle_sig_paths, rbs_collection_paths)
|
|
@@ -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.
|
|
@@ -16,6 +16,9 @@ module Rigor
|
|
|
16
16
|
# (`Integer#quo` and `File#to_path`, whose CRuby owners are `Numeric` and `IO`), and declares a few CRuby no
|
|
17
17
|
# longer defines (`Process::Status#&`). A class no catalogue covers answers false, as does a method its
|
|
18
18
|
# catalogue does not list: the caller reads both as "not proven". The catalogue is one CRuby snapshot.
|
|
19
|
+
#
|
|
20
|
+
# Issue #1752 — the singleton side reads the catalogue's `singleton_methods` (each `rb_define_singleton_method`
|
|
21
|
+
# on the class), the record that `Integer.sqrt` is CRuby's own method of `Integer`'s singleton class.
|
|
19
22
|
module CRubyDefiners
|
|
20
23
|
module_function
|
|
21
24
|
|
|
@@ -25,7 +28,15 @@ module Rigor
|
|
|
25
28
|
!names.nil? && names.include?(method_name.to_s)
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
# Does the catalogue prove CRuby defines `method_name` on `class_name`'s singleton class itself?
|
|
32
|
+
def singleton_defines?(class_name, method_name)
|
|
33
|
+
names = SINGLETON_INDEX[class_name.to_s.delete_prefix("::")]
|
|
34
|
+
!names.nil? && names.include?(method_name.to_s)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# `{class name => Set[method name]}` from each catalogued class's instance methods and aliases, or from its
|
|
38
|
+
# `singleton_methods` when `side` is `:singleton`.
|
|
39
|
+
def build_index(side = :instance)
|
|
29
40
|
index = Hash.new { |hash, key| hash[key] = Set.new }
|
|
30
41
|
MethodCatalog.topic_paths.each do |path|
|
|
31
42
|
data = YAML.safe_load_file(path, permitted_classes: [Symbol])
|
|
@@ -35,9 +46,7 @@ module Rigor
|
|
|
35
46
|
classes.each do |name, klass|
|
|
36
47
|
next if klass["rdoc_only"]
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
names.merge(defined_names(klass["instance_methods"]))
|
|
40
|
-
names.merge(defined_names(klass["aliases"]))
|
|
49
|
+
merge_names(index[name], klass, side)
|
|
41
50
|
end
|
|
42
51
|
end
|
|
43
52
|
Ractor.make_shareable(index.to_h { |name, names| [name, names.freeze] })
|
|
@@ -45,6 +54,13 @@ module Rigor
|
|
|
45
54
|
Ractor.make_shareable({})
|
|
46
55
|
end
|
|
47
56
|
|
|
57
|
+
def merge_names(names, klass, side)
|
|
58
|
+
return names.merge(defined_names(klass["singleton_methods"])) if side == :singleton
|
|
59
|
+
|
|
60
|
+
names.merge(defined_names(klass["instance_methods"]))
|
|
61
|
+
names.merge(defined_names(klass["aliases"]))
|
|
62
|
+
end
|
|
63
|
+
|
|
48
64
|
# The names of `entries` CRuby defines at runtime: one the catalogue marks `rdoc_only` was registered only for
|
|
49
65
|
# RDoc, inside an `#if 0` block.
|
|
50
66
|
def defined_names(entries)
|
|
@@ -54,7 +70,8 @@ module Rigor
|
|
|
54
70
|
end
|
|
55
71
|
|
|
56
72
|
INDEX = build_index
|
|
57
|
-
|
|
73
|
+
SINGLETON_INDEX = build_index(:singleton)
|
|
74
|
+
private_constant :INDEX, :SINGLETON_INDEX
|
|
58
75
|
end
|
|
59
76
|
end
|
|
60
77
|
end
|
|
@@ -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`
|
|
@@ -310,23 +333,63 @@ module Rigor
|
|
|
310
333
|
|
|
311
334
|
# The mutation sites under `body` whose reads name a variable {.captured_target?} accepts, keyed by name.
|
|
312
335
|
# The block yields the names the enclosing construct introduces, asked only when a local site is found.
|
|
336
|
+
#
|
|
337
|
+
# A site whose receiver is a body local that aliases a capture (`l2 = log; l2 << e`) mutates that capture's
|
|
338
|
+
# object, so it is attributed to the capture as well ({.aliased_captures}): left out, the next iteration
|
|
339
|
+
# read `log` at its entry `[]` and `[1, 2].inject(0) { |a, e| r = a + log.size; l2 = log; l2 << e; r }`
|
|
340
|
+
# folded to `0` where Ruby answers `1` (issue #1332).
|
|
313
341
|
def body_mutation_sites(body, base_scope, non_locals, &)
|
|
314
342
|
sites = nil
|
|
315
343
|
evaluator = nil
|
|
344
|
+
aliases = nil
|
|
316
345
|
Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
|
|
317
346
|
# A callee is resolved in the call-site scope, as the straight-line callee floor resolves it.
|
|
318
347
|
site = mutation_site(descendant) { evaluator ||= StatementEvaluator.new(scope: base_scope) }
|
|
319
348
|
next if site.nil?
|
|
320
349
|
|
|
321
350
|
site_reads(site).each do |read|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
351
|
+
targets = captured_target?(read, ancestors, base_scope, non_locals, &) ? [read] : []
|
|
352
|
+
if read.is_a?(Prism::LocalVariableReadNode)
|
|
353
|
+
aliases ||= local_aliases(body)
|
|
354
|
+
targets.concat(aliased_captures(read.name, aliases, base_scope, non_locals, Set.new, &))
|
|
355
|
+
end
|
|
356
|
+
targets.each { |target| ((sites ||= {})[target.name] ||= []) << site }
|
|
325
357
|
end
|
|
326
358
|
end
|
|
327
359
|
sites || NO_SITES
|
|
328
360
|
end
|
|
329
361
|
|
|
362
|
+
# `{ name => [[read, ancestors], ...] }`: every variable read a local write under `body` may store
|
|
363
|
+
# ({ReceiverAlias.candidates} of its value), with the ancestors {.captured_target?} resolves it against.
|
|
364
|
+
def local_aliases(body)
|
|
365
|
+
aliases = {}
|
|
366
|
+
Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
|
|
367
|
+
next unless node.is_a?(Prism::LocalVariableWriteNode) || node.is_a?(Prism::LocalVariableOrWriteNode)
|
|
368
|
+
|
|
369
|
+
ReceiverAlias.candidates(node.value).each do |read|
|
|
370
|
+
(aliases[node.name] ||= []) << [read, ancestors + [node]]
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
aliases
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# The captured reads the local `name` may alias, followed through local-to-local copies (`l3 = l2 = log`).
|
|
377
|
+
# It over-approximates — a write in any branch, or in a nested scope reusing the name, counts — which only
|
|
378
|
+
# widens a capture's binding.
|
|
379
|
+
def aliased_captures(name, aliases, base_scope, non_locals, visited, &)
|
|
380
|
+
return [] unless visited.add?(name)
|
|
381
|
+
|
|
382
|
+
aliases.fetch(name, NO_NAMES).flat_map do |read, ancestors|
|
|
383
|
+
if captured_target?(read, ancestors, base_scope, non_locals, &)
|
|
384
|
+
[read]
|
|
385
|
+
elsif read.is_a?(Prism::LocalVariableReadNode)
|
|
386
|
+
aliased_captures(read.name, aliases, base_scope, non_locals, visited, &)
|
|
387
|
+
else
|
|
388
|
+
NO_NAMES
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
330
393
|
# The mutation site `node` is — the node itself, or a {UnknownStoreWidening::CalleeStore} for a self-call
|
|
331
394
|
# whose callee content-mutates a local it is passed — or nil. A self-call is resolved by the
|
|
332
395
|
# `StatementEvaluator` the block yields, which the caller builds only when one is needed. A mutator name
|