rigortype 0.4.4 → 0.4.5
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 +5 -5
- data/docs/handbook/11-sig-gen.md +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +26 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +60 -15
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +45 -2
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +13 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../rooted_glob"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Effects
|
|
5
7
|
# Everything an effect envelope may be written in, as `[buffer name, RBS source]` pairs
|
|
@@ -55,7 +57,7 @@ module Rigor
|
|
|
55
57
|
# @param signature_paths — the configured roots; empty / nil takes the default.
|
|
56
58
|
# @param virtual_rbs — the loader's virtual entries.
|
|
57
59
|
def collect(signature_paths:, virtual_rbs: nil)
|
|
58
|
-
files = roots(signature_paths).flat_map { |root|
|
|
60
|
+
files = roots(signature_paths).flat_map { |root| RootedGlob.glob(root, "**/*.rbs").sort }
|
|
59
61
|
sources = files.filter_map do |path|
|
|
60
62
|
[path, File.read(path)]
|
|
61
63
|
rescue StandardError
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
5
|
require_relative "../config_path"
|
|
6
|
+
require_relative "../rooted_glob"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
class Environment
|
|
@@ -91,8 +92,8 @@ module Rigor
|
|
|
91
92
|
# all (`Bundler::Source::Path#path` is the user's own `path:` directory, used in place), so there is
|
|
92
93
|
# no bundle-relative location to walk. A `path:`-sourced gem's `sig/` is only reachable by adding it
|
|
93
94
|
# directly to the project's `signature_paths:` — which already works today and needs no discovery.
|
|
94
|
-
all = (
|
|
95
|
-
|
|
95
|
+
all = (RootedGlob.glob(resolved, "ruby/*/gems/*/sig") +
|
|
96
|
+
RootedGlob.glob(resolved, "ruby/*/bundler/gems/*/sig")).map { |d| Pathname.new(d) }
|
|
96
97
|
filtered = all.reject { |sig_dir| skip_gems.include?(gem_name_from_sig_path(sig_dir)) }
|
|
97
98
|
return filtered if locked_gems.nil? || locked_gems.empty?
|
|
98
99
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rbs"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class Environment
|
|
@@ -15,7 +16,7 @@ module Rigor
|
|
|
15
16
|
# @param dir — the vendored directory (Pathname).
|
|
16
17
|
# @return the absolute-less names (`"Prime::PseudoPrimeGenerator"`) of every class and module it declares.
|
|
17
18
|
def type_names(dir)
|
|
18
|
-
|
|
19
|
+
RootedGlob.glob(dir, "*.rbs").flat_map do |file|
|
|
19
20
|
_, _, decls = RBS::Parser.parse_signature(File.read(file))
|
|
20
21
|
names(decls, nil)
|
|
21
22
|
rescue RBS::BaseError, SystemCallError
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rbs"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class Environment
|
|
@@ -132,7 +133,7 @@ module Rigor
|
|
|
132
133
|
end
|
|
133
134
|
|
|
134
135
|
def signature_files(roots)
|
|
135
|
-
roots.flat_map { |root|
|
|
136
|
+
roots.flat_map { |root| RootedGlob.glob(root, "*.rbs").sort }
|
|
136
137
|
end
|
|
137
138
|
|
|
138
139
|
def shell_buffer
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "lockfile_resolver"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class Environment
|
|
@@ -47,7 +48,7 @@ module Rigor
|
|
|
47
48
|
base = Pathname.new(bundle_path)
|
|
48
49
|
return {} unless base.directory?
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
RootedGlob.glob(base, "ruby/*/gems/*").each_with_object({}) do |dir, acc|
|
|
51
52
|
name, version, platform = split_install_dirname(File.basename(dir))
|
|
52
53
|
next unless name
|
|
53
54
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
|
+
require_relative "../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
class Environment
|
|
@@ -71,7 +72,7 @@ module Rigor
|
|
|
71
72
|
base = Pathname.new(bundle_path)
|
|
72
73
|
return {} unless base.directory?
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
RootedGlob.glob(base, "ruby/*/gems/*").each_with_object({}) do |dir, acc|
|
|
75
76
|
acc[File.basename(dir)] ||= dir
|
|
76
77
|
end
|
|
77
78
|
end
|
|
@@ -114,7 +115,7 @@ module Rigor
|
|
|
114
115
|
base = Pathname.new(bundle_path)
|
|
115
116
|
return {} unless base.directory?
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
RootedGlob.glob(base, "ruby/*/bundler/gems/*").each_with_object({}) do |dir, acc|
|
|
118
119
|
name = gemspec_gem_name(dir)
|
|
119
120
|
acc[name] ||= dir if name
|
|
120
121
|
end
|
|
@@ -124,7 +125,7 @@ module Rigor
|
|
|
124
125
|
# exactly one. Bundler requires a git-sourced gem to ship one, so "not exactly one" means this is not
|
|
125
126
|
# the shape we think it is — and a guess from the directory basename is what this exists to avoid.
|
|
126
127
|
def gemspec_gem_name(dir)
|
|
127
|
-
specs =
|
|
128
|
+
specs = RootedGlob.glob(dir, "*.gemspec")
|
|
128
129
|
return nil unless specs.size == 1
|
|
129
130
|
|
|
130
131
|
File.basename(specs.first, ".gemspec")
|
|
@@ -10,6 +10,7 @@ require_relative "member_consistency"
|
|
|
10
10
|
require_relative "required_features"
|
|
11
11
|
require_relative "gated_signature_guard"
|
|
12
12
|
require_relative "gated_signature_plan"
|
|
13
|
+
require_relative "../rooted_glob"
|
|
13
14
|
|
|
14
15
|
module Rigor
|
|
15
16
|
class Environment
|
|
@@ -549,7 +550,7 @@ module Rigor
|
|
|
549
550
|
path = Pathname(path) unless path.is_a?(Pathname)
|
|
550
551
|
next [] unless path.directory?
|
|
551
552
|
|
|
552
|
-
|
|
553
|
+
RootedGlob.glob(path, "**/*.rbs").map { |p| File.expand_path(p) }
|
|
553
554
|
end.to_set
|
|
554
555
|
end
|
|
555
556
|
|
|
@@ -1916,6 +1917,25 @@ module Rigor
|
|
|
1916
1917
|
(@state[:definition_build_failures] || []).dup.freeze
|
|
1917
1918
|
end
|
|
1918
1919
|
|
|
1920
|
+
# Issue #1929 — re-ask for `class_name`'s definition on behalf of a PREVIOUS run's analysis, which
|
|
1921
|
+
# demanded it and found it failing. An `--incremental` recheck replays that run's failures, and one
|
|
1922
|
+
# whose conflicting buffer is a file the recheck re-synthesized cannot be replayed as it stands (the
|
|
1923
|
+
# edit may have fixed it), while the file that demanded the class may sit outside the recheck's
|
|
1924
|
+
# closure and never ask again. Re-asking here re-derives the answer over the current signatures: a
|
|
1925
|
+
# class that still fails is reported with its current tuple, one that now builds is not.
|
|
1926
|
+
#
|
|
1927
|
+
# A real demand, not {#during_internal_demand}: it stands in for the analysis's own, which is what
|
|
1928
|
+
# #696 binds the reported set to. Both sides are asked, since the replayed tuple does not record which
|
|
1929
|
+
# side the analysis demanded ({#report_definition_build_failure} builds both anyway).
|
|
1930
|
+
#
|
|
1931
|
+
# @return the class's current tuple, or nil when it now builds.
|
|
1932
|
+
def redemand_definition(class_name)
|
|
1933
|
+
key = class_name.to_s.delete_prefix("::")
|
|
1934
|
+
instance_definition(key)
|
|
1935
|
+
singleton_definition(key)
|
|
1936
|
+
definition_build_failures.find { |failure| failure.first == key }
|
|
1937
|
+
end
|
|
1938
|
+
|
|
1919
1939
|
# Virtual (inline-synthesized) contributions dropped by the collision quarantine
|
|
1920
1940
|
# ({.resolve_quarantining_virtual_collisions}): buffer names absent from the built env even though the
|
|
1921
1941
|
# entry's content is non-empty and parses (a parse failure is the synthesizer's own WD6 skip, reported
|
|
@@ -72,6 +72,12 @@ module Rigor
|
|
|
72
72
|
|
|
73
73
|
module_function
|
|
74
74
|
|
|
75
|
+
# Whether `call_node` is spelled to reach the private `Kernel#loop` (see {.loop_may_complete?}), which runs its
|
|
76
|
+
# block until it breaks or raises.
|
|
77
|
+
def kernel_loop?(call_node)
|
|
78
|
+
call_node.name == :loop && kernel_spelled_receiver?(call_node.receiver)
|
|
79
|
+
end
|
|
80
|
+
|
|
75
81
|
# Issue #1107 — whether a `loop` call can complete normally although its declared return is `bot`.
|
|
76
82
|
#
|
|
77
83
|
# `Kernel#loop` is `() { () -> void } -> bot` in core RBS, yet it rescues a `StopIteration` its block
|
|
@@ -86,7 +92,7 @@ module Rigor
|
|
|
86
92
|
# promise. A project redefinition of `loop` itself is not excluded: widening its `bot` costs precision,
|
|
87
93
|
# never a false positive.
|
|
88
94
|
def loop_may_complete?(call_node)
|
|
89
|
-
return false unless
|
|
95
|
+
return false unless kernel_loop?(call_node)
|
|
90
96
|
|
|
91
97
|
block = call_node.block
|
|
92
98
|
return false if block.nil?
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
|
+
require_relative "../../rooted_glob"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Inference
|
|
@@ -49,7 +50,7 @@ module Rigor
|
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
# Every catalogue file under {DATA_ROOT}, in a stable order.
|
|
52
|
-
def self.topic_paths =
|
|
53
|
+
def self.topic_paths = RootedGlob.glob(DATA_ROOT, "*.yml")
|
|
53
54
|
|
|
54
55
|
def initialize(path:, mutating_selectors: {})
|
|
55
56
|
@path = path
|
|
@@ -6,6 +6,7 @@ require_relative "../source/node_walker"
|
|
|
6
6
|
require_relative "block_parameter_binder"
|
|
7
7
|
require_relative "element_read_widening"
|
|
8
8
|
require_relative "index_write_widening"
|
|
9
|
+
require_relative "intervening_ivar_reset"
|
|
9
10
|
require_relative "mutation_widening"
|
|
10
11
|
require_relative "optimistic_origin"
|
|
11
12
|
require_relative "receiver_alias"
|
|
@@ -109,16 +110,22 @@ module Rigor
|
|
|
109
110
|
# `base_scope`), except that one still on its class-wide binding does not: ADR-58's declaration seed is
|
|
110
111
|
# the union of every write in the class, this body's included, so nothing the body stores can move it,
|
|
111
112
|
# and rebinding it would only drop the declaration mark that keeps its nil from being diagnostic fuel.
|
|
112
|
-
#
|
|
113
|
-
# `
|
|
114
|
-
#
|
|
113
|
+
# Issue #1402 — an instance variable written in a nested block that runs on an object provably not `self`
|
|
114
|
+
# (`Other.class_eval { @x = 1 }`, {InterveningIvarReset.foreign_self_block?}, its receiver typed in
|
|
115
|
+
# `base_scope`) is that object's, and does not count; one on a receiver that may be `self` does. A class
|
|
116
|
+
# variable or global written there counts as anywhere else: Ruby resolves a class variable lexically, and
|
|
117
|
+
# a global is the same one whatever `self` is. A nested `def` runs only when called; it still counts,
|
|
118
|
+
# because a call to it inside the body writes this one.
|
|
119
|
+
# @param foreign_self — the block itself runs on an object provably not `self`
|
|
120
|
+
# ({InterveningIvarReset.foreign_self_block?} of its call): no instance variable it writes is the caller's.
|
|
121
|
+
# Its locals, class variables and globals still count.
|
|
115
122
|
# @param non_locals — the per-element fold's superset of `ivars`: the class variables and globals the body
|
|
116
123
|
# rebinds count too, on the same terms, and so does an attribute setter called on `self` (`self.w = v`)
|
|
117
124
|
# as a rebind of the instance variable it is named after, `@w`: the `attr_writer` / `attr_accessor`
|
|
118
125
|
# convention stores there, and no write node shows it. A hand-written setter storing elsewhere is not
|
|
119
126
|
# seen.
|
|
120
127
|
# @return the captured names the body writes, each once, in first-write order.
|
|
121
|
-
def writes(block_node, base_scope, ivars: false, non_locals: false)
|
|
128
|
+
def writes(block_node, base_scope, ivars: false, non_locals: false, foreign_self: false)
|
|
122
129
|
body = block_node.body
|
|
123
130
|
return NO_NAMES if body.nil?
|
|
124
131
|
|
|
@@ -126,20 +133,44 @@ module Rigor
|
|
|
126
133
|
names = nil
|
|
127
134
|
outliving = non_locals || ivars
|
|
128
135
|
Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
elsif outliving
|
|
133
|
-
if ReflectiveIvarWrite.unknown_target?(descendant)
|
|
134
|
-
(names ||= []).concat(reflective_unknown_ivars(base_scope))
|
|
135
|
-
end
|
|
136
|
-
rebindable_outliving_write(descendant, base_scope, non_locals)
|
|
136
|
+
if LOCAL_WRITE_NODES.include?(descendant.class)
|
|
137
|
+
name = captured_local_write(descendant, ancestors, base_scope) do
|
|
138
|
+
introduced ||= introduced_locals(block_node)
|
|
137
139
|
end
|
|
138
|
-
|
|
140
|
+
(names ||= []) << name if name
|
|
141
|
+
elsif outliving
|
|
142
|
+
found = outliving_writes(descendant, ancestors, base_scope, non_locals, foreign_self)
|
|
143
|
+
(names ||= []).concat(found) unless found.empty?
|
|
144
|
+
end
|
|
139
145
|
end
|
|
140
146
|
names ? names.uniq : NO_NAMES
|
|
141
147
|
end
|
|
142
148
|
|
|
149
|
+
# The non-local names `node` writes ({.reflective_unknown_ivars} for an `instance_variable_set` on `self` whose
|
|
150
|
+
# target no literal names, then {.rebindable_outliving_write}), less the instance variables when the write
|
|
151
|
+
# runs on an object provably not `self` (`foreign_self`, or a nested block {.under_foreign_self_block?}).
|
|
152
|
+
def outliving_writes(node, ancestors, base_scope, non_locals, foreign_self)
|
|
153
|
+
found = ReflectiveIvarWrite.unknown_target?(node) ? reflective_unknown_ivars(base_scope) : NO_NAMES
|
|
154
|
+
written = rebindable_outliving_write(node, base_scope, non_locals)
|
|
155
|
+
return found if written.nil? && found.empty?
|
|
156
|
+
|
|
157
|
+
names = written ? found + [written] : found
|
|
158
|
+
return names unless foreign_self || under_foreign_self_block?(ancestors, base_scope)
|
|
159
|
+
|
|
160
|
+
names.reject { |name| ivar_name?(name) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Issue #1402 — whether a block on the path `ancestors` runs on an object provably not `self`: its call is a
|
|
164
|
+
# {InterveningIvarReset.foreign_self_block?}, the receiver typed in `base_scope`.
|
|
165
|
+
def under_foreign_self_block?(ancestors, base_scope)
|
|
166
|
+
(1...ancestors.size).any? do |index|
|
|
167
|
+
child = ancestors[index]
|
|
168
|
+
parent = ancestors[index - 1]
|
|
169
|
+
child.is_a?(Prism::BlockNode) && parent.is_a?(Prism::CallNode) && parent.block.equal?(child) &&
|
|
170
|
+
InterveningIvarReset.foreign_self_block?(parent, base_scope)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
143
174
|
# Issue #1889 — every ivar `base_scope` binds: an `instance_variable_set` on `self` whose target no literal
|
|
144
175
|
# names ({ReflectiveIvarWrite.unknown_target?}) may write any of them. One on its ADR-58 declaration seed
|
|
145
176
|
# counts too, since the census behind the seed sees no reflective write; {.bind} keeps its mark.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../source/node_children"
|
|
6
|
+
require_relative "../type"
|
|
7
|
+
require_relative "captured_locals"
|
|
8
|
+
|
|
9
|
+
module Rigor
|
|
10
|
+
module Inference
|
|
11
|
+
# Issues #1231 and #1839 — a rescue clause runs after the `begin` body raised from any point in it, and an `ensure`
|
|
12
|
+
# clause after the body, its `else` or a rescue clause raised, so each local or instance variable those wrote may
|
|
13
|
+
# hold any value one of their writes gave it, or the one it entered with. The engine does not track where the body
|
|
14
|
+
# raises; a clause enters with such a name rebound to `Dynamic[top]` instead ({.widen}).
|
|
15
|
+
#
|
|
16
|
+
# Not every name the body writes: one the clause mentions, whose binding the clause reads, and one the body
|
|
17
|
+
# writes more than once, which can hold a value neither the entry nor the body's exit binds. A name written once
|
|
18
|
+
# and never mentioned leaves the clause at its entry binding, which the join past the `begin` already pairs with
|
|
19
|
+
# the body's exit binding, the value of that one write (a write in a block or loop body included: the scope a
|
|
20
|
+
# block call or loop leaves joins every iteration's).
|
|
21
|
+
module ClauseWidening
|
|
22
|
+
WRITE_NODES = Set[
|
|
23
|
+
*CapturedLocals::LOCAL_WRITE_NODES,
|
|
24
|
+
Prism::InstanceVariableWriteNode,
|
|
25
|
+
Prism::InstanceVariableOperatorWriteNode,
|
|
26
|
+
Prism::InstanceVariableOrWriteNode,
|
|
27
|
+
Prism::InstanceVariableAndWriteNode,
|
|
28
|
+
Prism::InstanceVariableTargetNode
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
MENTION_NODES = Set[Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, *WRITE_NODES].freeze
|
|
32
|
+
|
|
33
|
+
SCOPE_BODY_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
34
|
+
|
|
35
|
+
# The names `sources` write: `names` all of them, `repeated` those written more than once, a nested block's
|
|
36
|
+
# included (it may write an outer local). A `def` or class body writes nothing here.
|
|
37
|
+
Writes = Data.define(:names, :repeated)
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# The {Writes} of `sources`, or nil for none.
|
|
42
|
+
def writes(sources)
|
|
43
|
+
counts = nil
|
|
44
|
+
sources.each { |source| counts = count_writes(source, counts) if source }
|
|
45
|
+
return nil if counts.nil?
|
|
46
|
+
|
|
47
|
+
Writes.new(names: counts.keys.to_set, repeated: counts.filter_map { |name, count| name if count > 1 }.to_set)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# `scope` with each name of `writes` that `clause` mentions, or that is repeated, rebound to `Dynamic[top]` where
|
|
51
|
+
# `scope` binds it, or nil when none changes. `clause` may be nil (an empty rescue clause).
|
|
52
|
+
def widen(scope, writes, clause)
|
|
53
|
+
return nil if writes.nil?
|
|
54
|
+
|
|
55
|
+
names = mentions(clause, writes.names, writes.repeated.dup)
|
|
56
|
+
untyped = Type::Combinator.untyped
|
|
57
|
+
widened = names.reduce(scope) do |acc, name|
|
|
58
|
+
ivar = name.start_with?("@")
|
|
59
|
+
current = ivar ? scope.ivar(name) : scope.local(name)
|
|
60
|
+
next acc if current.nil? || current == untyped
|
|
61
|
+
|
|
62
|
+
ivar ? acc.with_ivar(name, untyped).without_ivar_guard(name) : acc.with_local(name, untyped)
|
|
63
|
+
end
|
|
64
|
+
widened.equal?(scope) ? nil : widened
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# `counts` with each write under `node` counted.
|
|
68
|
+
def count_writes(node, counts)
|
|
69
|
+
return counts if SCOPE_BODY_NODES.any? { |klass| node.is_a?(klass) }
|
|
70
|
+
|
|
71
|
+
if WRITE_NODES.include?(node.class)
|
|
72
|
+
counts ||= Hash.new(0)
|
|
73
|
+
counts[node.name] += 1
|
|
74
|
+
end
|
|
75
|
+
node.rigor_each_child { |child| counts = count_writes(child, counts) }
|
|
76
|
+
counts
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# `found` with each name of `among` that `node` mentions added.
|
|
80
|
+
def mentions(node, among, found)
|
|
81
|
+
return found if node.nil? || SCOPE_BODY_NODES.any? { |klass| node.is_a?(klass) }
|
|
82
|
+
|
|
83
|
+
found << node.name if MENTION_NODES.include?(node.class) && among.include?(node.name)
|
|
84
|
+
node.rigor_each_child { |child| found = mentions(child, among, found) }
|
|
85
|
+
found
|
|
86
|
+
end
|
|
87
|
+
private_class_method :count_writes, :mentions
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -61,7 +61,7 @@ module Rigor
|
|
|
61
61
|
flavor = question == :arity ? :arity : :methods
|
|
62
62
|
chain = Scope::ResolutionChain.for(scope, class_name.to_s, side, flavor)
|
|
63
63
|
|
|
64
|
-
reads_visibility = VISIBILITY_QUESTIONS.include?(question)
|
|
64
|
+
reads_visibility = VISIBILITY_QUESTIONS.include?(question) ? question : nil
|
|
65
65
|
hits = candidates(scope, chain, method_name, from, answer_in, reads_visibility)
|
|
66
66
|
return UNKNOWN if hits.nil?
|
|
67
67
|
|
|
@@ -159,23 +159,39 @@ module Rigor
|
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
# The implicit tail after a chain's last project superclass: `Object` (with `Kernel` and `BasicObject`) on the
|
|
162
|
-
# instance side, `#<Class:Object>` through `Class`, `Module` and `Kernel` on the singleton side.
|
|
162
|
+
# instance side, `#<Class:Object>` through `Class`, `Module` and `Kernel` on the singleton side. It may answer
|
|
163
|
+
# where RBS declares the name there, where the last class's superclass is not a constant the tables can follow
|
|
164
|
+
# (#1624, {Scope::ResolutionChain#opaque_tail?}: `class C < Struct.new(:a)`, `C = Class.new(Base)`), and where
|
|
165
|
+
# the project itself reopens a tail class with the name (`class Object; def self.x`), which is no entry on the
|
|
166
|
+
# chain because the class names no superclass.
|
|
163
167
|
def implicit_object_answers?(scope, chain, method_name)
|
|
164
|
-
!external_lacks?(scope, ["Object"], chain.side, method_name)
|
|
168
|
+
!external_lacks?(scope, ["Object"], chain.side, method_name) || chain.opaque_tail?(scope) ||
|
|
169
|
+
project_tail_defines?(scope, chain.side, method_name)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The implicit tail's classes and the side each is read on, per chain side: a class object reaches the instance
|
|
173
|
+
# methods of `Class`, `Module`, `Object`, `Kernel` and `BasicObject` and the singleton methods of `Object` and
|
|
174
|
+
# `BasicObject`.
|
|
175
|
+
CLASS_OBJECT_TAIL = %w[Class Module Object Kernel BasicObject].product([:instance]) +
|
|
176
|
+
%w[Object BasicObject].product([:singleton])
|
|
177
|
+
IMPLICIT_TAIL = Ractor.make_shareable({ instance: %w[Object Kernel BasicObject].product([:instance]),
|
|
178
|
+
singleton: CLASS_OBJECT_TAIL })
|
|
179
|
+
private_constant :CLASS_OBJECT_TAIL, :IMPLICIT_TAIL
|
|
180
|
+
|
|
181
|
+
# Whether a project reopening of a tail class records the name: a `def`, whose read files the method edge, or
|
|
182
|
+
# an `attr_*` / `define_method` the existence table holds.
|
|
183
|
+
def project_tail_defines?(scope, side, method_name)
|
|
184
|
+
IMPLICIT_TAIL.fetch(side).any? do |name, kind|
|
|
185
|
+
(kind == :singleton ? scope.singleton_def_for(name, method_name) : scope.user_def_for(name, method_name)) ||
|
|
186
|
+
scope.discovered_method?(name, method_name, kind)
|
|
187
|
+
end
|
|
165
188
|
end
|
|
166
189
|
|
|
167
190
|
# True only for an ancestor RBS knows whose declaration lacks the name, on the side the entry stands for: an
|
|
168
191
|
# external superclass on a singleton chain (`:singleton`, its whole tail) is asked for a singleton method, an
|
|
169
192
|
# extended module (`:instance`) and every instance-side entry for an instance method.
|
|
170
193
|
def external_lacks?(scope, candidates, side, method_name)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
|
|
174
|
-
return false if known.nil?
|
|
175
|
-
|
|
176
|
-
Rigor::Reflection.singleton_method_definition(known, method_name, scope: scope).nil?
|
|
177
|
-
rescue StandardError
|
|
178
|
-
false
|
|
194
|
+
Scope::ResolutionChain::Relevance.external_lacks_on?(scope, candidates, side, method_name)
|
|
179
195
|
end
|
|
180
196
|
|
|
181
197
|
# What a candidate set says, for {Scope::ResolutionChain#settle} to compare across worlds.
|
|
@@ -198,14 +214,23 @@ module Rigor
|
|
|
198
214
|
# that reads the visibility (`reads_visibility`): a slot is contested there for a visibility call the walk
|
|
199
215
|
# cannot place (an uncertain toggle, `private(*names)`, `private def self.x`), which leaves the definer and its
|
|
200
216
|
# `def` certain; a possible `def` or `attr_*` call contests its def-node slot or its name's `possible` side too.
|
|
217
|
+
#
|
|
218
|
+
# `reads_visibility` is the question when it reads one. The `:visibility` question does not read a contested
|
|
219
|
+
# instance def-node slot (#1989): it answers from the visibility slot and the name's existence, not from the
|
|
220
|
+
# `def` body, and a possible `def` that contests the slot also makes its name `possible`. The slot is contested
|
|
221
|
+
# alone where a later `attr_*` replaced the `def`: the attr writes the name's visibility slot, so the reader's
|
|
222
|
+
# visibility is certain. A `define_method` or alias that replaced it contests the visibility slot as well, which
|
|
223
|
+
# this question does read.
|
|
201
224
|
def possible?(scope, hit, method_name, reads_visibility)
|
|
202
225
|
discovery = scope.discovery
|
|
203
226
|
owner = hit.owner
|
|
204
227
|
kind = hit.side == :singleton ? :singleton : :instance
|
|
205
228
|
nodes = kind == :singleton ? :discovered_singleton_def_nodes : :discovered_def_nodes
|
|
206
229
|
discovery.possible_method?(owner, method_name, kind) ||
|
|
207
|
-
|
|
208
|
-
|
|
230
|
+
(!(reads_visibility == :visibility && kind == :instance) &&
|
|
231
|
+
discovery.contested?(nodes, [owner, method_name.to_sym])) ||
|
|
232
|
+
(reads_visibility && discovery.contested?(:discovered_method_visibilities,
|
|
233
|
+
[owner, visibility_slot(method_name, kind)])) ||
|
|
209
234
|
discovery.contested?(:discovered_parameter_envelopes, [owner, [kind, method_name.to_sym]])
|
|
210
235
|
end
|
|
211
236
|
|
|
@@ -235,20 +260,61 @@ module Rigor
|
|
|
235
260
|
|
|
236
261
|
# The first project entry that records the name (a discovered method, an instance `def` the def table
|
|
237
262
|
# holds, or a visibility change such as `private :foo` recorded for it), with its visibility (`:public` where
|
|
238
|
-
# none was stated).
|
|
263
|
+
# none was stated). Issue #1984 — a singleton-side entry reads its class's singleton slot
|
|
264
|
+
# (`private_class_method :new` records `new` there with no `def`), never the instance one.
|
|
239
265
|
def visibility_answer(scope, method_name)
|
|
240
266
|
lambda do |chain, from|
|
|
241
267
|
first_hit(chain, scope, from) do |entry|
|
|
242
268
|
kind = entry.side == :singleton ? :singleton : :instance
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
269
|
+
recorded = side_visibility(scope, entry.name, method_name, kind)
|
|
270
|
+
next unless scope.discovered_method?(entry.name, method_name, kind) || recorded ||
|
|
271
|
+
(kind == :instance && scope.user_def_for(entry.name, method_name))
|
|
272
|
+
|
|
273
|
+
recorded || :public
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def side_visibility(scope, owner, method_name, kind)
|
|
279
|
+
if kind == :singleton
|
|
280
|
+
scope.discovered_method_visibilities.dig(owner.to_s, visibility_slot(method_name, :singleton)) ||
|
|
281
|
+
self_extended_copy_visibility(scope, owner, method_name)
|
|
282
|
+
else
|
|
283
|
+
scope.discovered_method_visibility(owner, method_name)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
246
286
|
|
|
247
|
-
|
|
287
|
+
# Issue #2059 — a module that `extend`s itself reaches its own instance method from the singleton side, and the
|
|
288
|
+
# extends fold copies that receiverless `def` onto the module's singleton entry, where no singleton slot records
|
|
289
|
+
# it: the instance slot's visibility is what Ruby applies (`module ES; extend self; private def x` makes `ES.x`
|
|
290
|
+
# private). A module function's singleton copy is public whatever its instance copy is, so a `module_function`
|
|
291
|
+
# row for the name in any file answers nil, as does a contested instance slot.
|
|
292
|
+
def self_extended_copy_visibility(scope, owner, method_name)
|
|
293
|
+
node = scope.singleton_def_for(owner, method_name)
|
|
294
|
+
return nil if node.nil? || !node.receiver.nil?
|
|
295
|
+
return nil unless scope.discovered_extends[owner.to_s]&.include?(owner.to_s)
|
|
296
|
+
return nil if scope.discovery.contested?(:discovered_method_visibilities, [owner.to_s, method_name.to_sym])
|
|
297
|
+
return nil if module_function_row?(scope, owner, method_name)
|
|
298
|
+
|
|
299
|
+
scope.discovered_method_visibility(owner, method_name)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Whether any file's deferred ranges carry a `module_function` row (`:both` or `:singleton`) for the name on
|
|
303
|
+
# `owner` (`ModuleFunctionState`).
|
|
304
|
+
def module_function_row?(scope, owner, method_name)
|
|
305
|
+
name = method_name.to_sym
|
|
306
|
+
scope.discovered_deferred_ranges.each_value.any? do |rows|
|
|
307
|
+
rows.any? do |(_start, _finish, row_name, kind, row_owner)|
|
|
308
|
+
row_name == name && row_owner == owner.to_s && kind != :instance
|
|
248
309
|
end
|
|
249
310
|
end
|
|
250
311
|
end
|
|
251
312
|
|
|
313
|
+
# The key of a name's slot in the visibility table on `kind`'s side (`ScopeIndexer#visibility_slot`).
|
|
314
|
+
def visibility_slot(method_name, kind)
|
|
315
|
+
kind == :singleton ? [:singleton, method_name.to_sym] : method_name.to_sym
|
|
316
|
+
end
|
|
317
|
+
|
|
252
318
|
def first_hit(chain, scope, from)
|
|
253
319
|
index = from - 1
|
|
254
320
|
found = nil
|
|
@@ -265,9 +331,10 @@ module Rigor
|
|
|
265
331
|
end
|
|
266
332
|
|
|
267
333
|
private_class_method :override_answer, :retro_position, :candidates, :past_fold_copies, :decided,
|
|
268
|
-
:external_may_answer?, :implicit_object_answers?, :external_lacks?,
|
|
334
|
+
:external_may_answer?, :implicit_object_answers?, :project_tail_defines?, :external_lacks?,
|
|
269
335
|
:outcomes, :collapse, :possible?,
|
|
270
|
-
:default_answer, :definer_answer, :visibility_answer, :
|
|
336
|
+
:default_answer, :definer_answer, :visibility_answer, :side_visibility,
|
|
337
|
+
:self_extended_copy_visibility, :module_function_row?, :visibility_slot, :first_hit
|
|
271
338
|
end
|
|
272
339
|
end
|
|
273
340
|
end
|
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../type"
|
|
6
6
|
require_relative "../reflection"
|
|
7
7
|
require_relative "../ast"
|
|
8
|
+
require_relative "clause_widening"
|
|
8
9
|
require_relative "../source/constant_path"
|
|
9
10
|
require_relative "../source/node_children"
|
|
10
11
|
require_relative "../source/node_walker"
|
|
@@ -41,6 +42,7 @@ require_relative "receiver_alias"
|
|
|
41
42
|
require_relative "repeated_or_writes"
|
|
42
43
|
require_relative "singleton_object_constant"
|
|
43
44
|
require_relative "refined_dispatch"
|
|
45
|
+
require_relative "singleton_external_ahead"
|
|
44
46
|
require_relative "stored_block_call"
|
|
45
47
|
require_relative "struct_fold_safety"
|
|
46
48
|
require_relative "unknown_store_widening"
|
|
@@ -1208,17 +1210,22 @@ module Rigor
|
|
|
1208
1210
|
Type::Combinator.constant_of(nil)
|
|
1209
1211
|
end
|
|
1210
1212
|
|
|
1211
|
-
rescue_types = rescue_chain_types(rescue_clause)
|
|
1213
|
+
rescue_types = rescue_chain_types(rescue_clause, node.statements)
|
|
1212
1214
|
Type::Combinator.union(primary_type, *rescue_types)
|
|
1213
1215
|
end
|
|
1214
1216
|
|
|
1215
1217
|
# Issue #1360 — the evaluator binds `$!` in a rescue clause it enters; a clause typed here, such as one in a
|
|
1216
1218
|
# `begin` passed as an argument, reads `$!`, `$@` and `$?` unbound rather than an enclosing clause's.
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
+
#
|
|
1220
|
+
# Issue #1839 — it also reads each name the `body` writes as `Dynamic[top]` ({ClauseWidening}), as the evaluator's
|
|
1221
|
+
# rescue clause does: `[begin; conn = make; work; rescue; conn; end]` holds the connection, not the entry's `nil`.
|
|
1222
|
+
def rescue_chain_types(rescue_node, body)
|
|
1223
|
+
arm_scope = rescue_arm_scope
|
|
1224
|
+
writes = ClauseWidening.writes([body])
|
|
1219
1225
|
types = []
|
|
1220
1226
|
current = rescue_node
|
|
1221
1227
|
while current
|
|
1228
|
+
arm_typer = typer_under(ClauseWidening.widen(arm_scope, writes, current.statements) || arm_scope)
|
|
1222
1229
|
types << arm_typer.send(:statements_or_nil, current.statements)
|
|
1223
1230
|
current = current.subsequent
|
|
1224
1231
|
end
|
|
@@ -1279,11 +1286,23 @@ module Rigor
|
|
|
1279
1286
|
def type_of_range(node)
|
|
1280
1287
|
left_static, left = static_range_endpoint(node.left)
|
|
1281
1288
|
right_static, right = static_range_endpoint(node.right)
|
|
1282
|
-
|
|
1289
|
+
if left_static && right_static
|
|
1290
|
+
range = static_range_value(left, right, node.exclude_end?)
|
|
1291
|
+
return Type::Combinator.constant_of(range) if range
|
|
1292
|
+
end
|
|
1283
1293
|
|
|
1284
1294
|
nominal_range_for_endpoints(node.left, node.right)
|
|
1285
1295
|
end
|
|
1286
1296
|
|
|
1297
|
+
# The `Range` the static endpoints denote, or nil when Ruby would refuse to build it (`1.."a"`,
|
|
1298
|
+
# `"a"..1` raise `ArgumentError: bad value for range`). The literal is a runtime error,
|
|
1299
|
+
# not a constant, so the fold declines and the caller types it as a plain Range.
|
|
1300
|
+
def static_range_value(left, right, exclude_end)
|
|
1301
|
+
Range.new(left, right, exclude_end)
|
|
1302
|
+
rescue ArgumentError
|
|
1303
|
+
nil
|
|
1304
|
+
end
|
|
1305
|
+
|
|
1287
1306
|
# Derives `Nominal[Range, [T]]` from the endpoint expression types when at least one endpoint is
|
|
1288
1307
|
# statically typeable. The element parameter is the union of the endpoint types (lifted from
|
|
1289
1308
|
# `Constant<v>` to `Nominal<v.class>` so the carrier matches what `Range#each` would yield). Falls back
|
|
@@ -2583,6 +2602,10 @@ module Rigor
|
|
|
2583
2602
|
|
|
2584
2603
|
def_node, owner = resolve_singleton_def_with_owner(receiver.class_name, method_name)
|
|
2585
2604
|
return nil if def_node.nil?
|
|
2605
|
+
# Issue #1936 — a contested slot holds a `def` a later definer may have replaced
|
|
2606
|
+
# (`singleton_class.alias_method :x, :y` after `def self.x`), or one that may not have run: its body is not
|
|
2607
|
+
# proven to be the method Ruby calls, so the call falls through to `Dynamic`.
|
|
2608
|
+
return nil if scope.discovery.contested?(:discovered_singleton_def_nodes, [owner, method_name.to_sym])
|
|
2586
2609
|
|
|
2587
2610
|
result = infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
|
|
2588
2611
|
return result if result.nil?
|
|
@@ -2597,6 +2620,10 @@ module Rigor
|
|
|
2597
2620
|
# `discovered_def_nodes` / `discovered_superclasses` / `discovered_includes`, and this walk reads a
|
|
2598
2621
|
# fourth table those three do not imply. Keying on what the answer actually depends on is the #682
|
|
2599
2622
|
# lesson — a memo that outlives its inputs serves one scope's answer to another.
|
|
2623
|
+
#
|
|
2624
|
+
# It is the TYPING read only, so it also withholds (`[nil, nil]`, the call types `Dynamic`) where an external
|
|
2625
|
+
# ancestor may answer ahead of the definer found (#1572: `extend M; extend Enumerable` makes `C.to_a`
|
|
2626
|
+
# `Enumerable#to_a`, which the reader passes over). The existence reads keep the reader's own answer.
|
|
2600
2627
|
def resolve_singleton_def_with_owner(class_name, method_name)
|
|
2601
2628
|
by_singleton = (class_graph_buckets[:singleton_def] ||= {}.compare_by_identity)
|
|
2602
2629
|
cache = (by_singleton[scope.discovered_singleton_def_nodes] ||= {})
|
|
@@ -2604,7 +2631,10 @@ module Rigor
|
|
|
2604
2631
|
key = method_name.to_sym
|
|
2605
2632
|
return table[key] if table.key?(key)
|
|
2606
2633
|
|
|
2607
|
-
|
|
2634
|
+
found = scope.singleton_def_through_ancestors(class_name, method_name)
|
|
2635
|
+
owner = found.last
|
|
2636
|
+
found = [nil, nil] if owner && SingletonExternalAhead.withhold?(scope, class_name, method_name, owner)
|
|
2637
|
+
table[key] = found
|
|
2608
2638
|
end
|
|
2609
2639
|
|
|
2610
2640
|
# The project-side singleton-method band: a `Foo.bar` call resolved against a `class << …` / `def self.…`
|