rigortype 0.4.5 → 0.4.6
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/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugins_command.rb +5 -0
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +4 -4
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -1
|
@@ -5,6 +5,7 @@ require "prism"
|
|
|
5
5
|
require_relative "../type"
|
|
6
6
|
require_relative "../source/node_children"
|
|
7
7
|
require_relative "return_barrier"
|
|
8
|
+
require_relative "optimistic_origin"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
10
11
|
module Inference
|
|
@@ -21,10 +22,16 @@ module Rigor
|
|
|
21
22
|
#
|
|
22
23
|
# Extracted from `Rigor::SigGen::Generator#infer_return_type` so `LineTypeCollector` (`rigor annotate`'s
|
|
23
24
|
# def-line annotator) and the sig-generator share one source of truth.
|
|
25
|
+
#
|
|
26
|
+
# `keep_lookup_nil: true` is the reading a signature needs (#2197). The engine reads a lookup that can miss
|
|
27
|
+
# (`Hash#[]`, `Array#first`, the {OptimisticOrigin} family) as nil-free on purpose, so `if h[k]` stays
|
|
28
|
+
# silent, but a `-> V` written into `sig/` is a contract that every caller's `nil` check is dead code. Each
|
|
29
|
+
# returned expression whose value is such a lookup therefore contributes `nil` beside its type. Only the
|
|
30
|
+
# value positions a lookup's `nil` reaches the return through are followed ({#optimistic_value?}).
|
|
24
31
|
module DefReturnTyper
|
|
25
32
|
module_function
|
|
26
33
|
|
|
27
|
-
def call(def_node, scope_index)
|
|
34
|
+
def call(def_node, scope_index, keep_lookup_nil: false)
|
|
28
35
|
body = def_node.body
|
|
29
36
|
return nil if body.nil?
|
|
30
37
|
|
|
@@ -37,11 +44,152 @@ module Rigor
|
|
|
37
44
|
last_type = safe_type_of(inner_scope, last)
|
|
38
45
|
return nil if last_type.nil?
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
lookup_context = keep_lookup_nil ? LookupContext.new(scope_index, truthy_guards(body)) : nil
|
|
48
|
+
last_type = with_lookup_nil(last_type, last, inner_scope, lookup_context) if lookup_context
|
|
49
|
+
union_with_explicit_returns(body, last_type, scope_index, lookup_context: lookup_context)
|
|
41
50
|
rescue StandardError
|
|
42
51
|
nil
|
|
43
52
|
end
|
|
44
53
|
|
|
54
|
+
# What {#optimistic_value?} reads beside a node's scope: the engine's per-node scopes, and for each lookup
|
|
55
|
+
# under a guard the source of the guarding predicates ({#truthy_guards}).
|
|
56
|
+
LookupContext = Data.define(:scope_index, :guards)
|
|
57
|
+
|
|
58
|
+
def with_lookup_nil(type, node, scope, context = nil)
|
|
59
|
+
return type unless optimistic_value?(node, scope, context)
|
|
60
|
+
|
|
61
|
+
Type::Combinator.union(type, Type::Combinator.constant_of(nil))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Whether `node`'s value can be the `nil` an optimistic lookup's miss returns: the lookup itself (a call
|
|
65
|
+
# the dispatcher marked, or `recv&.m` over one), a local or ivar bound to one, or a branch or operand that
|
|
66
|
+
# hands its value on. `a && b` hands on either operand (a falsy `a`, a missed lookup's `nil` included, is
|
|
67
|
+
# the result); `a || b` hands on `b` only, since `a` is returned only when truthy, so `h[k] || 0` stays
|
|
68
|
+
# nil-free. `x ||= v` and `x &&= v` hand on `v` and, for `&&=`, the binding. A call ON the lookup
|
|
69
|
+
# (`h[k].to_s`) is a different value.
|
|
70
|
+
#
|
|
71
|
+
# Each child is read under the scope the engine recorded for it (`scope_index`), so a branch a guard narrows
|
|
72
|
+
# (`v ? v : "d"`, `return "z" unless v`) reads the binding the narrowing left, not the enclosing one.
|
|
73
|
+
def optimistic_value?(node, scope, context = nil)
|
|
74
|
+
scope = context&.scope_index&.[](node) || scope
|
|
75
|
+
case node
|
|
76
|
+
when Prism::CallNode then optimistic_call?(node, scope, context)
|
|
77
|
+
when Prism::LocalVariableReadNode then !scope.optimistic_local(node.name).nil?
|
|
78
|
+
when Prism::InstanceVariableReadNode then !scope.optimistic_ivar(node.name).nil?
|
|
79
|
+
when Prism::AndNode then [node.left, node.right].any? { optimistic_value?(it, scope, context) }
|
|
80
|
+
when Prism::OrNode then optimistic_value?(node.right, scope, context)
|
|
81
|
+
else optimistic_write?(node, scope, context) || optimistic_branch?(node, scope, context)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def optimistic_call?(node, scope, context)
|
|
86
|
+
if scope.optimistic_origins[node]
|
|
87
|
+
return false if guarded_lookup?(node, context) || non_empty_end?(node, scope)
|
|
88
|
+
|
|
89
|
+
return true
|
|
90
|
+
end
|
|
91
|
+
node.safe_navigation? && optimistic_value?(node.receiver, scope, context)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# `return h[k] if h[k]`, `h[k] ? h[k] : d`: the same lookup, read again under a predicate that just found it
|
|
95
|
+
# truthy. Only a lookup whose receiver and arguments are plain reads or literals counts, so the second read
|
|
96
|
+
# sees what the first did.
|
|
97
|
+
GUARDABLE_LOOKUPS = %i[[] first last dig].freeze
|
|
98
|
+
private_constant :GUARDABLE_LOOKUPS
|
|
99
|
+
|
|
100
|
+
def guarded_lookup?(node, context)
|
|
101
|
+
return false if context.nil? || !GUARDABLE_LOOKUPS.include?(node.name)
|
|
102
|
+
return false unless [node.receiver, *node.arguments&.arguments].all? { stable_operand?(it) }
|
|
103
|
+
|
|
104
|
+
context.guards.fetch(node, EMPTY_GUARDS).include?(node.slice)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
EMPTY_GUARDS = [].freeze
|
|
108
|
+
private_constant :EMPTY_GUARDS
|
|
109
|
+
|
|
110
|
+
def stable_operand?(node)
|
|
111
|
+
case node
|
|
112
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::SymbolNode, Prism::StringNode,
|
|
113
|
+
Prism::IntegerNode, Prism::SelfNode, Prism::ConstantReadNode then true
|
|
114
|
+
else false
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# `a.first` / `a.last` on a receiver the engine has narrowed to a non-empty array (`a.empty? ? d : a.first`)
|
|
119
|
+
# cannot miss.
|
|
120
|
+
def non_empty_end?(node, scope)
|
|
121
|
+
return false unless %i[first last].include?(node.name) && node.arguments.nil? && node.receiver
|
|
122
|
+
|
|
123
|
+
receiver = scope.type_of(node.receiver)
|
|
124
|
+
receiver.is_a?(Type::Difference) && receiver.removed == Type::Combinator.tuple_of
|
|
125
|
+
rescue StandardError
|
|
126
|
+
false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def optimistic_write?(node, scope, context)
|
|
130
|
+
case node
|
|
131
|
+
when Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode,
|
|
132
|
+
Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode
|
|
133
|
+
optimistic_value?(node.value, scope, context)
|
|
134
|
+
when Prism::LocalVariableAndWriteNode
|
|
135
|
+
!scope.optimistic_local(node.name).nil? || optimistic_value?(node.value, scope, context)
|
|
136
|
+
when Prism::InstanceVariableAndWriteNode
|
|
137
|
+
!scope.optimistic_ivar(node.name).nil? || optimistic_value?(node.value, scope, context)
|
|
138
|
+
else false
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The value-carrying children of a statement list or a conditional: any of them can be the value returned.
|
|
143
|
+
def optimistic_branch?(node, scope, context)
|
|
144
|
+
value_branches(node).any? { |branch| !branch.nil? && optimistic_value?(branch, scope, context) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The lookup a truthy arm (the body of an `if` or ternary, the `else` of an `unless`) reads FIRST, mapped to
|
|
148
|
+
# the source of the predicate that just tested it, for {#guarded_lookup?}. Only the first expression, or the
|
|
149
|
+
# value of a first `return`, counts: anything run between the test and the re-read (`h.delete(k)`, a
|
|
150
|
+
# reassignment, a block, `yield`, another call that mutates the receiver) can make the re-read miss.
|
|
151
|
+
def truthy_guards(body)
|
|
152
|
+
guards = {}.compare_by_identity
|
|
153
|
+
collect_truthy_guards(body, guards)
|
|
154
|
+
guards
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def collect_truthy_guards(node, guards)
|
|
158
|
+
return unless node.is_a?(Prism::Node)
|
|
159
|
+
|
|
160
|
+
predicate, arm = truthy_arm(node)
|
|
161
|
+
first = first_value(arm)
|
|
162
|
+
guards[first] = [predicate.slice] if first
|
|
163
|
+
node.rigor_each_child { collect_truthy_guards(it, guards) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def first_value(arm)
|
|
167
|
+
return nil unless arm.is_a?(Prism::StatementsNode)
|
|
168
|
+
|
|
169
|
+
first = arm.body.first
|
|
170
|
+
args = first.is_a?(Prism::ReturnNode) ? first.arguments&.arguments : nil
|
|
171
|
+
args&.size == 1 ? args.first : first
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def truthy_arm(node)
|
|
175
|
+
case node
|
|
176
|
+
when Prism::IfNode then [node.predicate, node.statements]
|
|
177
|
+
when Prism::UnlessNode then [node.predicate, node.else_clause&.statements]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def value_branches(node)
|
|
182
|
+
case node
|
|
183
|
+
when Prism::ParenthesesNode then [node.body && body_last_expression(node.body)]
|
|
184
|
+
when Prism::StatementsNode, Prism::BeginNode then [body_last_expression(node)]
|
|
185
|
+
when Prism::IfNode then [node.statements, node.subsequent]
|
|
186
|
+
when Prism::UnlessNode then [node.statements, node.else_clause]
|
|
187
|
+
when Prism::ElseNode, Prism::WhenNode, Prism::InNode then [node.statements]
|
|
188
|
+
when Prism::CaseNode, Prism::CaseMatchNode then [*node.conditions, node.else_clause]
|
|
189
|
+
else []
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
45
193
|
def body_last_expression(body)
|
|
46
194
|
case body
|
|
47
195
|
when Prism::StatementsNode then body.body.last
|
|
@@ -50,27 +198,27 @@ module Rigor
|
|
|
50
198
|
end
|
|
51
199
|
end
|
|
52
200
|
|
|
53
|
-
def union_with_explicit_returns(body, last_type, scope_index)
|
|
201
|
+
def union_with_explicit_returns(body, last_type, scope_index, lookup_context: nil)
|
|
54
202
|
return_types = []
|
|
55
|
-
collect_return_types(body, scope_index, return_types)
|
|
203
|
+
collect_return_types(body, scope_index, return_types, lookup_context: lookup_context)
|
|
56
204
|
return last_type if return_types.empty?
|
|
57
205
|
|
|
58
206
|
Type::Combinator.union(last_type, *return_types)
|
|
59
207
|
end
|
|
60
208
|
|
|
61
|
-
def collect_return_types(node, scope_index, out)
|
|
209
|
+
def collect_return_types(node, scope_index, out, lookup_context: nil)
|
|
62
210
|
return unless node.is_a?(Prism::Node)
|
|
63
211
|
return if ReturnBarrier.node?(node)
|
|
64
212
|
|
|
65
|
-
type_return_node(node, scope_index, out) if node.is_a?(Prism::ReturnNode)
|
|
213
|
+
type_return_node(node, scope_index, out, lookup_context: lookup_context) if node.is_a?(Prism::ReturnNode)
|
|
66
214
|
# A barrier call's receiver and arguments still run in the method; only its block is cut off.
|
|
67
215
|
barrier_block = node.block if node.is_a?(Prism::CallNode) && ReturnBarrier.block_call?(node)
|
|
68
216
|
node.rigor_each_child do |c|
|
|
69
|
-
collect_return_types(c, scope_index, out) unless c.equal?(barrier_block)
|
|
217
|
+
collect_return_types(c, scope_index, out, lookup_context: lookup_context) unless c.equal?(barrier_block)
|
|
70
218
|
end
|
|
71
219
|
end
|
|
72
220
|
|
|
73
|
-
def type_return_node(return_node, scope_index, out)
|
|
221
|
+
def type_return_node(return_node, scope_index, out, lookup_context: nil)
|
|
74
222
|
args = return_node.arguments&.arguments || []
|
|
75
223
|
if args.empty?
|
|
76
224
|
out << Type::Combinator.constant_of(nil)
|
|
@@ -84,7 +232,9 @@ module Rigor
|
|
|
84
232
|
return unless args.size == 1
|
|
85
233
|
|
|
86
234
|
type = safe_type_of(scope, args.first)
|
|
87
|
-
|
|
235
|
+
return if type.nil?
|
|
236
|
+
|
|
237
|
+
out << (lookup_context ? with_lookup_nil(type, args.first, scope, lookup_context) : type)
|
|
88
238
|
end
|
|
89
239
|
|
|
90
240
|
def safe_type_of(scope, node)
|
|
@@ -221,11 +221,15 @@ module Rigor
|
|
|
221
221
|
# alone where a later `attr_*` replaced the `def`: the attr writes the name's visibility slot, so the reader's
|
|
222
222
|
# visibility is certain. A `define_method` or alias that replaced it contests the visibility slot as well, which
|
|
223
223
|
# this question does read.
|
|
224
|
+
#
|
|
225
|
+
# Issue #2134 — the contest is a read of the slot, so a question that reads it files `visibility:<owner>#<name>`:
|
|
226
|
+
# an answer block may answer from RBS without reading the slot itself (`nearest_ancestor_method_def`).
|
|
224
227
|
def possible?(scope, hit, method_name, reads_visibility)
|
|
225
228
|
discovery = scope.discovery
|
|
226
229
|
owner = hit.owner
|
|
227
230
|
kind = hit.side == :singleton ? :singleton : :instance
|
|
228
231
|
nodes = kind == :singleton ? :discovered_singleton_def_nodes : :discovered_def_nodes
|
|
232
|
+
Analysis::DependencyRecorder.read_visibility(owner, method_name) if reads_visibility
|
|
229
233
|
discovery.possible_method?(owner, method_name, kind) ||
|
|
230
234
|
(!(reads_visibility == :visibility && kind == :instance) &&
|
|
231
235
|
discovery.contested?(nodes, [owner, method_name.to_sym])) ||
|
|
@@ -277,7 +281,7 @@ module Rigor
|
|
|
277
281
|
|
|
278
282
|
def side_visibility(scope, owner, method_name, kind)
|
|
279
283
|
if kind == :singleton
|
|
280
|
-
scope.
|
|
284
|
+
scope.discovered_method_visibility(owner, method_name, singleton: true) ||
|
|
281
285
|
self_extended_copy_visibility(scope, owner, method_name)
|
|
282
286
|
else
|
|
283
287
|
scope.discovered_method_visibility(owner, method_name)
|
|
@@ -301,11 +305,21 @@ module Rigor
|
|
|
301
305
|
|
|
302
306
|
# Whether any file's deferred ranges carry a `module_function` row (`:both` or `:singleton`) for the name on
|
|
303
307
|
# `owner` (`ModuleFunctionState`).
|
|
308
|
+
#
|
|
309
|
+
# Issue #2161 — the answer reads every file's rows, so it records `visibility:<owner>#<name>` whichever way it
|
|
310
|
+
# answers: `ScopeIndexer.visibility_entries` carries each file's rows of this shape, so a file that starts or
|
|
311
|
+
# stops writing one fires the key. A swap from `private :x` to `module_function :x` in a reopening leaves the
|
|
312
|
+
# file's visibility table and declared classes as they were, and fires nothing else. The caller's own
|
|
313
|
+
# visibility reads record the same key today; this one keeps the reader's edge where it reads.
|
|
314
|
+
#
|
|
315
|
+
# Issue #2209 — a row named {ModuleFunctionState::ANY_ROW_NAME} (a `module_function` spelling whose names the
|
|
316
|
+
# prescan cannot read) stands for every name of its owner, so it counts too, and its key is read as well
|
|
317
|
+
# ({ModuleFunctionState.row_names}).
|
|
304
318
|
def module_function_row?(scope, owner, method_name)
|
|
305
|
-
|
|
319
|
+
names = ModuleFunctionState.row_names(method_name).each { Analysis::DependencyRecorder.read_visibility(owner, it) }
|
|
306
320
|
scope.discovered_deferred_ranges.each_value.any? do |rows|
|
|
307
321
|
rows.any? do |(_start, _finish, row_name, kind, row_owner)|
|
|
308
|
-
row_name
|
|
322
|
+
names.include?(row_name) && row_owner == owner.to_s && kind != :instance
|
|
309
323
|
end
|
|
310
324
|
end
|
|
311
325
|
end
|
|
@@ -53,8 +53,8 @@ module Rigor
|
|
|
53
53
|
# made a class guard on `$!` itself narrow it; #1447 narrows this decline to the guards that still do not.
|
|
54
54
|
#
|
|
55
55
|
# `$@` calls the exception's `backtrace`, which is an `Array[String]` for a raised exception. It is left unbound
|
|
56
|
-
# in a program that defines a method named `backtrace` anywhere, which may return anything,
|
|
57
|
-
#
|
|
56
|
+
# in a program that defines a method named `backtrace` anywhere, which may return anything, in a clause whose
|
|
57
|
+
# body calls `set_backtrace`, which may set it to nil, and in a clause whose body never reads it.
|
|
58
58
|
#
|
|
59
59
|
# `$?` is unbound because the exception may have been raised while a subprocess waited: a backtick, `%x` or
|
|
60
60
|
# `system` sets `$?` to nil before it runs the child, so a `Timeout::Error`, an `Interrupt` or a `Thread#raise`
|
|
@@ -65,6 +65,10 @@ module Rigor
|
|
|
65
65
|
return entered if guarded?(body, reference)
|
|
66
66
|
|
|
67
67
|
entered = entered.with_global(ERROR_INFO, rescued_type(exception_type, scope))
|
|
68
|
+
# Issue #1967 — a clause that never reads `$@` leaves it unbound without asking about `backtrace`: the answer
|
|
69
|
+
# could not reach a diagnostic, and asking records a `defines:backtrace` edge that re-checked every `rescue`
|
|
70
|
+
# in the program whenever such a def came or went.
|
|
71
|
+
return entered unless backtrace_read?(body)
|
|
68
72
|
return entered if calls_set_backtrace?(body) || BlockCallTiming.project_defines_anywhere?(:backtrace, scope)
|
|
69
73
|
|
|
70
74
|
entered.with_global(BACKTRACE, backtrace_type)
|
|
@@ -114,7 +118,17 @@ module Rigor
|
|
|
114
118
|
node.rigor_each_child { |child| found ||= calls_set_backtrace?(child) }
|
|
115
119
|
found
|
|
116
120
|
end
|
|
117
|
-
|
|
121
|
+
|
|
122
|
+
# True when `node` reads `$@` anywhere in it, a block included. An empty clause (nil) reads nothing.
|
|
123
|
+
def backtrace_read?(node)
|
|
124
|
+
return false unless node.is_a?(Prism::Node)
|
|
125
|
+
return node.name == BACKTRACE if node.is_a?(Prism::GlobalVariableReadNode)
|
|
126
|
+
|
|
127
|
+
found = false
|
|
128
|
+
node.rigor_each_child { |child| found ||= backtrace_read?(child) }
|
|
129
|
+
found
|
|
130
|
+
end
|
|
131
|
+
private_class_method :guard?, :error_info_read?, :calls_set_backtrace?, :backtrace_read?
|
|
118
132
|
|
|
119
133
|
# True when `node` reads `$!`, `$@` or `$?` anywhere in it: the gate on typing a rescue modifier's fallback
|
|
120
134
|
# under {.modifier_entry}, which most fallbacks (`rescue nil`) never need.
|
|
@@ -12,6 +12,7 @@ require_relative "../source/node_walker"
|
|
|
12
12
|
require_relative "../analysis/self_call_resolution_recorder"
|
|
13
13
|
require_relative "block_call_timing"
|
|
14
14
|
require_relative "block_repetition"
|
|
15
|
+
require_relative "project_yielder"
|
|
15
16
|
require_relative "block_parameter_binder"
|
|
16
17
|
require_relative "method_parameter_binder"
|
|
17
18
|
require_relative "body_fixpoint"
|
|
@@ -1051,7 +1052,7 @@ module Rigor
|
|
|
1051
1052
|
# optimistic-carrier decline) and one terminating-arm rule, and cannot drift apart again. Only the
|
|
1052
1053
|
# value is read; the post-scope belongs to whichever walk owns the statement.
|
|
1053
1054
|
def type_of_conditional(node)
|
|
1054
|
-
scope.evaluate(node, tracer: tracer).first
|
|
1055
|
+
LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
|
|
1055
1056
|
end
|
|
1056
1057
|
|
|
1057
1058
|
def type_of_else(node)
|
|
@@ -1272,11 +1273,22 @@ module Rigor
|
|
|
1272
1273
|
Type::Combinator.bot
|
|
1273
1274
|
end
|
|
1274
1275
|
|
|
1275
|
-
# `while`
|
|
1276
|
-
#
|
|
1277
|
-
#
|
|
1278
|
-
|
|
1279
|
-
|
|
1276
|
+
# A `while` / `until` / `for` loop in value position. Like {#type_of_conditional}, it delegates to the
|
|
1277
|
+
# statement evaluator, which owns the loop's value: `nil` or a `while` / `until` loop's `break` values
|
|
1278
|
+
# (issue #2031), and a `for` loop's collection or its `break` values (issue #1216).
|
|
1279
|
+
#
|
|
1280
|
+
# A `while` / `until` loop no `break` leaves is `nil` without evaluating its body, as
|
|
1281
|
+
# {StatementEvaluator#loop_value} answers it: a loop that is a call's receiver or argument is typed again by
|
|
1282
|
+
# every narrowing and rule that types that operand, and evaluating it each time re-ran the passes of every loop
|
|
1283
|
+
# nested in it.
|
|
1284
|
+
def type_of_loop(node)
|
|
1285
|
+
return Type::Combinator.constant_of(nil) if break_free_condition_loop?(node)
|
|
1286
|
+
|
|
1287
|
+
LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
|
|
1288
|
+
end
|
|
1289
|
+
|
|
1290
|
+
def break_free_condition_loop?(node)
|
|
1291
|
+
!node.is_a?(Prism::ForNode) && !block_level_jump?(node.statements, Prism::BreakNode)
|
|
1280
1292
|
end
|
|
1281
1293
|
|
|
1282
1294
|
def type_of_lambda(_node)
|
|
@@ -3318,21 +3330,34 @@ module Rigor
|
|
|
3318
3330
|
cache = class_graph_buckets[:yields]
|
|
3319
3331
|
return cache[def_node] if cache.key?(def_node)
|
|
3320
3332
|
|
|
3321
|
-
cache[def_node] = yield_reachable?(def_node.body)
|
|
3333
|
+
cache[def_node] = yield_reachable?(def_node.body, def_node.parameters&.block&.name)
|
|
3322
3334
|
end
|
|
3323
3335
|
|
|
3324
|
-
def yield_reachable?(node)
|
|
3336
|
+
def yield_reachable?(node, block_name)
|
|
3325
3337
|
return false if node.nil?
|
|
3326
|
-
return true if node.is_a?(Prism::YieldNode)
|
|
3338
|
+
return true if node.is_a?(Prism::YieldNode) || forwards_block_argument?(node, block_name)
|
|
3327
3339
|
|
|
3328
3340
|
node.rigor_each_child do |child|
|
|
3329
3341
|
next if YIELD_BOUNDARY_NODES.include?(child.class)
|
|
3330
3342
|
|
|
3331
|
-
return true if yield_reachable?(child)
|
|
3343
|
+
return true if yield_reachable?(child, block_name)
|
|
3332
3344
|
end
|
|
3333
3345
|
false
|
|
3334
3346
|
end
|
|
3335
3347
|
|
|
3348
|
+
# Issue #1157 — a call that hands on its method's block (`&`, or `&blk` naming the block parameter) reaches the
|
|
3349
|
+
# caller's block as a `yield` does. Whether `blk` is still the method's own block (not rebound or shadowed) is
|
|
3350
|
+
# decided where it is typed ({#forwarded_block_return_type}); this only keeps the call-site dimension for a def
|
|
3351
|
+
# that might.
|
|
3352
|
+
def forwards_block_argument?(node, block_name)
|
|
3353
|
+
block = node.respond_to?(:block) ? node.block : nil
|
|
3354
|
+
return false unless block.is_a?(Prism::BlockArgumentNode)
|
|
3355
|
+
|
|
3356
|
+
expression = block.expression
|
|
3357
|
+
expression.nil? || (!block_name.nil? && expression.is_a?(Prism::LocalVariableReadNode) &&
|
|
3358
|
+
expression.name == block_name)
|
|
3359
|
+
end
|
|
3360
|
+
|
|
3336
3361
|
# The candidate-frame memo path: consult the current run generation's bucket, and on a miss compute
|
|
3337
3362
|
# and store a FINAL result. Reached only when `memo_candidate?` held (see `infer_user_method_return`).
|
|
3338
3363
|
# The store gate (ADR-84 WD3): a result is stored when no transient-machinery event during the
|
|
@@ -4328,9 +4353,13 @@ module Rigor
|
|
|
4328
4353
|
|
|
4329
4354
|
# Whether the call may run its block more than once, so a later run reads a captured binding an earlier
|
|
4330
4355
|
# run rebound — the premise of laying the #587 (b) binding ({#captured_block_bindings}) under the block.
|
|
4331
|
-
# {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412).
|
|
4356
|
+
# {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412). Issue #2301 — a
|
|
4357
|
+
# project callee that hands the block on ({ProjectYielder.hands_block_on?}) also counts: the block's value is what
|
|
4358
|
+
# the forward's target `yield`s, and that target may run it many times, so a captured local the block rebinds must
|
|
4359
|
+
# not read its entry binding.
|
|
4332
4360
|
def block_may_repeat?(call_node, receiver_type)
|
|
4333
|
-
BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope)
|
|
4361
|
+
BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope) ||
|
|
4362
|
+
ProjectYielder.hands_block_on?(call_node, receiver_type, scope)
|
|
4334
4363
|
end
|
|
4335
4364
|
|
|
4336
4365
|
EMPTY_BREAK_ARMS = [].freeze
|
|
@@ -4426,10 +4455,22 @@ module Rigor
|
|
|
4426
4455
|
)
|
|
4427
4456
|
type_block_body(block_arg, entry, captured: captured)
|
|
4428
4457
|
when Prism::BlockArgumentNode
|
|
4429
|
-
symbol_block_return_type(block_arg, expected)
|
|
4458
|
+
forwarded_block_return_type(block_arg) || symbol_block_return_type(block_arg, expected)
|
|
4430
4459
|
end
|
|
4431
4460
|
end
|
|
4432
4461
|
|
|
4462
|
+
# Issue #1157 — `&` / `&blk` hands the callee the block this method was itself called with, so the callee's
|
|
4463
|
+
# `yield` is worth what the CALLER's block returns: the value {.current_yield_value_type} already holds for
|
|
4464
|
+
# this frame (the one a `yield` written here reads). Without it a name resolver that passes its block on
|
|
4465
|
+
# (`def walk(x, &) = first_hit(x, &)`) read as `Dynamic[top]` through the forward and was skipped by
|
|
4466
|
+
# `sig-gen`. Nil when the argument is not a forwarded block or the call site passed no block.
|
|
4467
|
+
def forwarded_block_return_type(block_arg)
|
|
4468
|
+
frame = scope.match_frame
|
|
4469
|
+
return nil unless frame&.forwards_block?(block_arg)
|
|
4470
|
+
|
|
4471
|
+
ExpressionTyper.current_yield_value_type
|
|
4472
|
+
end
|
|
4473
|
+
|
|
4433
4474
|
# The scope a block body is typed under: the surrounding scope plus the parameter bindings the receiving
|
|
4434
4475
|
# method's signature implies, with the #587 (b) `captured` binding ({#repeating_captured_bindings}) laid
|
|
4435
4476
|
# between the two, so the body reads what a captured local, instance variable, class variable or global
|
|
@@ -4524,6 +4565,14 @@ module Rigor
|
|
|
4524
4565
|
# A failure in the re-answer falls back to the plain tail-only answer, for the reason
|
|
4525
4566
|
# {#threaded_block_body_type} gives: a raise reaching `block_return_type_for` would report "no block".
|
|
4526
4567
|
def tail_only_block_body_type(body, block_scope, captured)
|
|
4568
|
+
StatementEvaluator.provisional_returns { tail_only_block_body_type_unrecorded(body, block_scope, captured) }
|
|
4569
|
+
end
|
|
4570
|
+
|
|
4571
|
+
# Issue #2299 — the tail alone, typed from `block_scope`, is not the walk the method's returns are collected
|
|
4572
|
+
# from: a `return` in it reads a name the skipped prefix binds as unbound. Its records are provisional
|
|
4573
|
+
# ({StatementEvaluator.provisional_returns}): the statement walk's record of the same `return` replaces one,
|
|
4574
|
+
# and one stands only for a block the walk never enters (a block in expression position).
|
|
4575
|
+
def tail_only_block_body_type_unrecorded(body, block_scope, captured)
|
|
4527
4576
|
return block_scope.type_of(body) unless block_body_threading_suppressed? && body.is_a?(Prism::StatementsNode)
|
|
4528
4577
|
|
|
4529
4578
|
answered =
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../source/constant_path"
|
|
6
6
|
require_relative "../source/node_children"
|
|
7
|
+
require_relative "ivar_writer_census"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Inference
|
|
@@ -26,6 +27,8 @@ module Rigor
|
|
|
26
27
|
# kept apart from `[:defines, …]`.
|
|
27
28
|
# - `[:main_mixin, name]` — a module a top-level `include`, `prepend` or `extend` mixes into `Object` (or
|
|
28
29
|
# `main`), as written. A module RBS does not know may carry any method, so it reaches every class.
|
|
30
|
+
# - the instance-variable writer entries of {IvarWriterCensus} (issue #1154): `[:ivar_set, name]`,
|
|
31
|
+
# `[:ivar_set_class, class_name]` and `[:own_write, class_name, name, path]`.
|
|
29
32
|
#
|
|
30
33
|
# The census is a `Set` of those frozen entries, merged by union across files, the project pre-pass and the
|
|
31
34
|
# `pre_eval:` files. It is recorded inside descents that already visit every node ({Collector#visit}), never
|
|
@@ -74,12 +77,40 @@ module Rigor
|
|
|
74
77
|
census.include?(DEFINES_ANY) || names.any? { |name| census.include?([:defines, name]) }
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
# #1154 — the {IvarWriterCensus} entries of `census`, for the incremental diff and its readers.
|
|
81
|
+
IVAR_KINDS = %i[ivar_set ivar_set_class own_write].to_set.freeze
|
|
82
|
+
|
|
83
|
+
def ivar_set_entries(census) = census.select { |entry| IVAR_KINDS.include?(entry.first) }.to_set
|
|
84
|
+
|
|
85
|
+
# The negative-dependency key an {IvarWriterCensus} entry fires, the one `UnseenIvarWriters` records.
|
|
86
|
+
def ivar_key(entry)
|
|
87
|
+
case entry.first
|
|
88
|
+
when :ivar_set then "ivar_set:#{entry[1]}"
|
|
89
|
+
when :ivar_set_class then "ivar_set_class:#{entry[1]}"
|
|
90
|
+
else "own_write:#{entry[1]}##{entry[2]}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
77
94
|
# Whether some refinement may add `write`.
|
|
78
95
|
def refines_write?(census) = census.include?(REFINES_ANY) || census.include?(%i[refines write])
|
|
79
96
|
|
|
80
97
|
# The module names the top-level mixins spell.
|
|
81
98
|
def main_mixins(census) = census.filter_map { |entry| entry[1] if entry[0] == :main_mixin }
|
|
82
99
|
|
|
100
|
+
# Issue #2158 — the `global_census:` dependency key a census entry moves, without the kind prefix: `alias:$x`,
|
|
101
|
+
# `defines:write`, `refines:write`, `defines_any`, `refines_any`, and `main_mixin` for every top-level mixin: its
|
|
102
|
+
# reader asks about each, and one shared key is a deliberate precision trade-off (a mixin moving anywhere
|
|
103
|
+
# re-checks every write that read them). The rules that read the census record these keys
|
|
104
|
+
# (`CheckRules#record_global_census`), and `Analysis::Incremental.changed_global_census_keys` fires them for
|
|
105
|
+
# every entry that appeared in or vanished from a file's census.
|
|
106
|
+
def dependency_key(entry)
|
|
107
|
+
case entry
|
|
108
|
+
when DEFINES_ANY then "defines_any"
|
|
109
|
+
when REFINES_ANY then "refines_any"
|
|
110
|
+
else entry[0] == :main_mixin ? "main_mixin" : "#{entry[0]}:#{entry[1]}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
83
114
|
# A standalone walk, for a file no existing descent visits (a `pre_eval:` entry).
|
|
84
115
|
def scan(root)
|
|
85
116
|
collector = Collector.new(InEffectRefinements.new(root))
|
|
@@ -107,12 +138,16 @@ module Rigor
|
|
|
107
138
|
def initialize(in_effect)
|
|
108
139
|
@census = Set.new
|
|
109
140
|
@refine_ranges = []
|
|
141
|
+
@ivar_writers = IvarWriterCensus.new(@census)
|
|
110
142
|
@in_effect = in_effect
|
|
111
143
|
end
|
|
112
144
|
|
|
113
145
|
# A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
|
|
114
146
|
# feeds: the census then counts it as a definition of every name, which only declines a report.
|
|
115
|
-
|
|
147
|
+
# `owner` is the qualified name of the class whose body the node sits in, for {IvarWriterCensus} (nil where
|
|
148
|
+
# the host walk does not track it, or no class owns an instance there).
|
|
149
|
+
def visit(node, top_level:, owner: nil)
|
|
150
|
+
@ivar_writers.visit(node, owner)
|
|
116
151
|
case node
|
|
117
152
|
when Prism::AliasGlobalVariableNode then visit_global_alias(node)
|
|
118
153
|
when Prism::DefNode then record(node, node.name)
|