rigortype 0.4.3 → 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/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- 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 +60 -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 +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- 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 +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- 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/mcp_command.rb +1 -1
- 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 +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- 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 +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- 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 +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- 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 +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- 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 +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -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 +56 -8
- 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 +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- 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 +50 -35
- 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-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- 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-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-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- 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 +16 -1
|
@@ -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?
|
|
@@ -165,7 +171,12 @@ module Rigor
|
|
|
165
171
|
# nothing for names this rare, and the block-return pass is no independent check here: it types a
|
|
166
172
|
# self-call to an overridden `raise` as Kernel's `bot` too, so a missed override is a wrong `bot`.
|
|
167
173
|
# {LastLine.reads_line?} declines on it the same way for `gets` and `readline`.
|
|
174
|
+
#
|
|
175
|
+
# Issue #1459 — the answer reads every project file's tables whole, so it records a `defines:<name>` edge
|
|
176
|
+
# whichever way it answers: an edit that makes a file start or stop defining the name re-checks the reader
|
|
177
|
+
# (`Incremental.changed_defined_names`). A `pre_eval:` patch moves the snapshot's own fingerprint.
|
|
168
178
|
def project_defines_anywhere?(method_name, scope)
|
|
179
|
+
Analysis::DependencyRecorder.read_missing(:defines, method_name) if Analysis::DependencyRecorder.active?
|
|
169
180
|
return true if scope.top_level_def_for(method_name)
|
|
170
181
|
# `discovered_methods` withholds a plain cross-file `def` (the ADR-17 monkey-patch contract); the
|
|
171
182
|
# def-node tables still carry it, on both sides.
|
|
@@ -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,9 +6,11 @@ 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"
|
|
13
|
+
require_relative "reflective_ivar_write"
|
|
12
14
|
require_relative "unknown_store_widening"
|
|
13
15
|
|
|
14
16
|
module Rigor
|
|
@@ -108,16 +110,22 @@ module Rigor
|
|
|
108
110
|
# `base_scope`), except that one still on its class-wide binding does not: ADR-58's declaration seed is
|
|
109
111
|
# the union of every write in the class, this body's included, so nothing the body stores can move it,
|
|
110
112
|
# and rebinding it would only drop the declaration mark that keeps its nil from being diagnostic fuel.
|
|
111
|
-
#
|
|
112
|
-
# `
|
|
113
|
-
#
|
|
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.
|
|
114
122
|
# @param non_locals — the per-element fold's superset of `ivars`: the class variables and globals the body
|
|
115
123
|
# rebinds count too, on the same terms, and so does an attribute setter called on `self` (`self.w = v`)
|
|
116
124
|
# as a rebind of the instance variable it is named after, `@w`: the `attr_writer` / `attr_accessor`
|
|
117
125
|
# convention stores there, and no write node shows it. A hand-written setter storing elsewhere is not
|
|
118
126
|
# seen.
|
|
119
127
|
# @return the captured names the body writes, each once, in first-write order.
|
|
120
|
-
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)
|
|
121
129
|
body = block_node.body
|
|
122
130
|
return NO_NAMES if body.nil?
|
|
123
131
|
|
|
@@ -125,17 +133,51 @@ module Rigor
|
|
|
125
133
|
names = nil
|
|
126
134
|
outliving = non_locals || ivars
|
|
127
135
|
Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
elsif outliving
|
|
132
|
-
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)
|
|
133
139
|
end
|
|
134
|
-
|
|
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
|
|
135
145
|
end
|
|
136
146
|
names ? names.uniq : NO_NAMES
|
|
137
147
|
end
|
|
138
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
|
+
|
|
174
|
+
# Issue #1889 — every ivar `base_scope` binds: an `instance_variable_set` on `self` whose target no literal
|
|
175
|
+
# names ({ReflectiveIvarWrite.unknown_target?}) may write any of them. One on its ADR-58 declaration seed
|
|
176
|
+
# counts too, since the census behind the seed sees no reflective write; {.bind} keeps its mark.
|
|
177
|
+
def reflective_unknown_ivars(base_scope)
|
|
178
|
+
base_scope.ivars.keys
|
|
179
|
+
end
|
|
180
|
+
|
|
139
181
|
def rebindable_outliving_write(node, base_scope, non_locals)
|
|
140
182
|
name = outliving_write_name(node, non_locals)
|
|
141
183
|
name if name && rebindable_non_local?(base_scope, name)
|
|
@@ -151,8 +193,12 @@ module Rigor
|
|
|
151
193
|
yield.include?(name) ? nil : name
|
|
152
194
|
end
|
|
153
195
|
|
|
154
|
-
# The non-local `node` rebinds under `non_locals:` — or, without it, the instance variable only.
|
|
196
|
+
# The non-local `node` rebinds under `non_locals:` — or, without it, the instance variable only. A literal-name
|
|
197
|
+
# `instance_variable_set` on `self` ({ReflectiveIvarWrite.literal_name}) is a write of that ivar under either.
|
|
155
198
|
def outliving_write_name(node, non_locals)
|
|
199
|
+
reflective = ReflectiveIvarWrite.literal_name(node)
|
|
200
|
+
return reflective if reflective
|
|
201
|
+
|
|
156
202
|
name = non_local_write_name(node)
|
|
157
203
|
non_locals || (name && ivar_name?(name) && NON_LOCAL_WRITE_NODES.include?(node.class)) ? name : nil
|
|
158
204
|
end
|
|
@@ -201,13 +247,21 @@ module Rigor
|
|
|
201
247
|
# it the miss answer the mark records (issue #1302).
|
|
202
248
|
def bind(scope, name, type, optimistic: nil)
|
|
203
249
|
case variable_kind(name)
|
|
204
|
-
when :ivar then
|
|
250
|
+
when :ivar then bind_ivar(scope, name, type, optimistic)
|
|
205
251
|
when :cvar then scope.with_cvar(name, type)
|
|
206
252
|
when :global then scope.with_global(name, type)
|
|
207
253
|
else OptimisticOrigin.carry_local_mark(scope, scope.with_local(name, type), name, optimistic)
|
|
208
254
|
end
|
|
209
255
|
end
|
|
210
256
|
|
|
257
|
+
# An ivar on its ADR-58 declaration seed reaches {.bind} through {.reflective_unknown_ivars}, and through
|
|
258
|
+
# `ExpressionTyper#prefix_answered_scope`; rebinding it re-stamps the declaration mark, as
|
|
259
|
+
# `ReflectiveIvarWrite.apply` does, so a nil in the seed fuels no nil-receiver diagnostic.
|
|
260
|
+
def bind_ivar(scope, name, type, optimistic)
|
|
261
|
+
bound = OptimisticOrigin.carry_ivar_mark(scope, scope.with_ivar(name, type), name, optimistic)
|
|
262
|
+
scope.declaration_sourced?(:ivar, name) ? bound.seed_declaration_sourced_ivar(name, type) : bound
|
|
263
|
+
end
|
|
264
|
+
|
|
211
265
|
# The optimistic nil-freeness mark `scope` holds for a name from {.writes}, or nil.
|
|
212
266
|
def optimistic_mark(scope, name)
|
|
213
267
|
case variable_kind(name)
|
|
@@ -290,7 +344,7 @@ module Rigor
|
|
|
290
344
|
receiver = node.receiver
|
|
291
345
|
return true if receiver && MutationWidening::SHAPE_MUTATORS.include?(node.name)
|
|
292
346
|
|
|
293
|
-
callee_call?(node)
|
|
347
|
+
ReflectiveIvarWrite.self_write?(node) || callee_call?(node)
|
|
294
348
|
end
|
|
295
349
|
|
|
296
350
|
# Issue #1412 — {.content_mutations} for a `while` / `until` / `for` body: the locals bound in `base_scope`
|
|
@@ -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
|
|
@@ -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) ? question : nil
|
|
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
|
|
@@ -153,23 +159,39 @@ module Rigor
|
|
|
153
159
|
end
|
|
154
160
|
|
|
155
161
|
# The implicit tail after a chain's last project superclass: `Object` (with `Kernel` and `BasicObject`) on the
|
|
156
|
-
# 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.
|
|
157
167
|
def implicit_object_answers?(scope, chain, method_name)
|
|
158
|
-
!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
|
|
159
188
|
end
|
|
160
189
|
|
|
161
190
|
# True only for an ancestor RBS knows whose declaration lacks the name, on the side the entry stands for: an
|
|
162
191
|
# external superclass on a singleton chain (`:singleton`, its whole tail) is asked for a singleton method, an
|
|
163
192
|
# extended module (`:instance`) and every instance-side entry for an instance method.
|
|
164
193
|
def external_lacks?(scope, candidates, side, method_name)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
known = candidates.find { |candidate| Rigor::Reflection.rbs_class_known?(candidate, scope: scope) }
|
|
168
|
-
return false if known.nil?
|
|
169
|
-
|
|
170
|
-
Rigor::Reflection.singleton_method_definition(known, method_name, scope: scope).nil?
|
|
171
|
-
rescue StandardError
|
|
172
|
-
false
|
|
194
|
+
Scope::ResolutionChain::Relevance.external_lacks_on?(scope, candidates, side, method_name)
|
|
173
195
|
end
|
|
174
196
|
|
|
175
197
|
# What a candidate set says, for {Scope::ResolutionChain#settle} to compare across worlds.
|
|
@@ -188,15 +210,27 @@ module Rigor
|
|
|
188
210
|
end
|
|
189
211
|
|
|
190
212
|
# 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
|
-
|
|
213
|
+
# contested slot of one of the single-valued members. A contested visibility slot counts only for a question
|
|
214
|
+
# that reads the visibility (`reads_visibility`): a slot is contested there for a visibility call the walk
|
|
215
|
+
# cannot place (an uncertain toggle, `private(*names)`, `private def self.x`), which leaves the definer and its
|
|
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.
|
|
224
|
+
def possible?(scope, hit, method_name, reads_visibility)
|
|
193
225
|
discovery = scope.discovery
|
|
194
226
|
owner = hit.owner
|
|
195
227
|
kind = hit.side == :singleton ? :singleton : :instance
|
|
196
228
|
nodes = kind == :singleton ? :discovered_singleton_def_nodes : :discovered_def_nodes
|
|
197
229
|
discovery.possible_method?(owner, method_name, kind) ||
|
|
198
|
-
|
|
199
|
-
|
|
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)])) ||
|
|
200
234
|
discovery.contested?(:discovered_parameter_envelopes, [owner, [kind, method_name.to_sym]])
|
|
201
235
|
end
|
|
202
236
|
|
|
@@ -226,20 +260,61 @@ module Rigor
|
|
|
226
260
|
|
|
227
261
|
# The first project entry that records the name (a discovered method, an instance `def` the def table
|
|
228
262
|
# holds, or a visibility change such as `private :foo` recorded for it), with its visibility (`:public` where
|
|
229
|
-
# 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.
|
|
230
265
|
def visibility_answer(scope, method_name)
|
|
231
266
|
lambda do |chain, from|
|
|
232
267
|
first_hit(chain, scope, from) do |entry|
|
|
233
268
|
kind = entry.side == :singleton ? :singleton : :instance
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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))
|
|
237
272
|
|
|
238
|
-
|
|
273
|
+
recorded || :public
|
|
239
274
|
end
|
|
240
275
|
end
|
|
241
276
|
end
|
|
242
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
|
|
286
|
+
|
|
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
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
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
|
+
|
|
243
318
|
def first_hit(chain, scope, from)
|
|
244
319
|
index = from - 1
|
|
245
320
|
found = nil
|
|
@@ -256,9 +331,10 @@ module Rigor
|
|
|
256
331
|
end
|
|
257
332
|
|
|
258
333
|
private_class_method :override_answer, :retro_position, :candidates, :past_fold_copies, :decided,
|
|
259
|
-
:external_may_answer?, :implicit_object_answers?, :external_lacks?,
|
|
334
|
+
:external_may_answer?, :implicit_object_answers?, :project_tail_defines?, :external_lacks?,
|
|
260
335
|
:outcomes, :collapse, :possible?,
|
|
261
|
-
: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
|
|
262
338
|
end
|
|
263
339
|
end
|
|
264
340
|
end
|