rigortype 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +16 -0
  5. data/docs/manual/02-cli-reference.md +4 -3
  6. data/docs/manual/03-configuration.md +16 -5
  7. data/docs/manual/04-diagnostics.md +44 -2
  8. data/docs/manual/07-plugins.md +1 -1
  9. data/docs/manual/10-mcp-server.md +32 -3
  10. data/docs/manual/18-removing-dead-code.md +11 -1
  11. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  12. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  13. data/exe/rigor +12 -17
  14. data/lib/rigor/analysis/check_rules/rule_ids.rb +57 -0
  15. data/lib/rigor/analysis/check_rules.rb +46 -36
  16. data/lib/rigor/analysis/incremental.rb +12 -0
  17. data/lib/rigor/analysis/incremental_session.rb +121 -21
  18. data/lib/rigor/analysis/reachability/graph.rb +23 -3
  19. data/lib/rigor/analysis/reachability/scan.rb +51 -9
  20. data/lib/rigor/analysis/rule_catalog.rb +6 -1
  21. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  22. data/lib/rigor/analysis/run_cache_probe.rb +5 -3
  23. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
  24. data/lib/rigor/analysis/runner/pool_coordinator.rb +110 -15
  25. data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
  26. data/lib/rigor/analysis/runner.rb +35 -9
  27. data/lib/rigor/analysis/severity_stamp.rb +34 -1
  28. data/lib/rigor/analysis/worker_session.rb +57 -1
  29. data/lib/rigor/cache/descriptor.rb +40 -6
  30. data/lib/rigor/cache/incremental_snapshot.rb +20 -4
  31. data/lib/rigor/cli/mcp_command.rb +1 -1
  32. data/lib/rigor/config_audit.rb +111 -15
  33. data/lib/rigor/configuration/dependencies.rb +19 -11
  34. data/lib/rigor/environment/rbs_loader.rb +46 -10
  35. data/lib/rigor/inference/block_call_timing.rb +5 -0
  36. data/lib/rigor/inference/captured_locals.rb +26 -3
  37. data/lib/rigor/inference/definer_resolution.rb +16 -7
  38. data/lib/rigor/inference/expression_typer.rb +59 -4
  39. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  40. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  41. data/lib/rigor/inference/module_function_state.rb +80 -13
  42. data/lib/rigor/inference/narrowing.rb +184 -68
  43. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  44. data/lib/rigor/inference/repeated_or_writes.rb +414 -1
  45. data/lib/rigor/inference/scope_indexer.rb +439 -70
  46. data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
  47. data/lib/rigor/inference/statement_evaluator.rb +84 -29
  48. data/lib/rigor/lens/anchor.rb +3 -3
  49. data/lib/rigor/lens/contract_reader.rb +15 -0
  50. data/lib/rigor/lens/document.rb +2 -2
  51. data/lib/rigor/lens/file_reader.rb +442 -73
  52. data/lib/rigor/lens/members.rb +65 -15
  53. data/lib/rigor/lens/run.rb +44 -2
  54. data/lib/rigor/lens.rb +2 -1
  55. data/lib/rigor/mcp/server.rb +61 -20
  56. data/lib/rigor/plugin/box_probe.rb +154 -7
  57. data/lib/rigor/project_environment.rb +6 -3
  58. data/lib/rigor/scope.rb +11 -6
  59. data/lib/rigor/sig_gen/layout_index.rb +36 -0
  60. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  61. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  62. data/lib/rigor/sig_gen/writer.rb +47 -34
  63. data/lib/rigor/version.rb +1 -1
  64. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  65. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  66. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
  67. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  68. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  69. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  70. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  71. metadata +4 -1
@@ -0,0 +1,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../source/node_children"
6
+ require_relative "../source/parameter_envelope"
7
+ require_relative "../type"
8
+ require_relative "guard_rebinding"
9
+ require_relative "project_method_ownership"
10
+ require_relative "refined_dispatch/own_method"
11
+
12
+ module Rigor
13
+ module Inference
14
+ # Issue #1596 — the instance variables an implicit-self or `self.` statement call may write, when the analysis
15
+ # can prove that set. The intervening-call reset (`StatementEvaluator#invalidate_ivars_for_intervening_call`)
16
+ # and the class-guard restore ({GuardRebinding}) both assume such a call may write any instance variable and
17
+ # run any code; with a proven set they widen only the instance variables in it, so `return unless
18
+ # @io.is_a?(StringIO); log("dumping"); @io.string` keeps `@io` a `StringIO` when `log` writes no instance
19
+ # variable.
20
+ #
21
+ # {.of} answers a frozen `Set` of names, or nil when it cannot prove one. It proves one only for an instance
22
+ # method's `self` (a project class, not a module, whose instance chain the resolution chain settles) and a call
23
+ # with no block argument (`&blk`, `...`), and only when every method the call can reach is proven:
24
+ #
25
+ # - A method the project defines on the class's chain must resolve to one `def` (not `define_method`, an
26
+ # `attr_*` writer or an alias), found ahead of every external ancestor, with every project subclass resolving
27
+ # the name to the same definer, and no `refine` block defining the name. Its parameters' default values and
28
+ # its body are read. A recursive or mutually recursive call adds nothing the first read did not.
29
+ # - Any other method on `self` must be one {GuardRebinding} counts as running no foreign code (`warn`, `puts`,
30
+ # `format`), and not a code-running name (`send`, `instance_variable_set`, `instance_eval`, …).
31
+ # - A call on another receiver counts only on a literal, a global, a constant naming a core or standard-library
32
+ # class, or an instance variable read as its class-ivar seed, and only when {GuardRebinding} counts its method
33
+ # as running no foreign code there. A local, a parameter or a call result is a receiver the read cannot type.
34
+ #
35
+ # Read code that writes an instance variable adds its name (an `instance_variable_set` call is a code-running
36
+ # name, so it proves nothing). Code that writes a global or a constant, yields, calls `super`, defines a method
37
+ # or opens a singleton class, an operator write on a local, and a `for` loop prove nothing. A literal block at
38
+ # the call site or in a read body is read with the code around it. The accepted gap is {GuardRebinding}'s:
39
+ # an implicit conversion a core method runs (`warn obj` runs `obj.to_s`) is read as the core method alone, and
40
+ # so is a singleton method an `extend` or `def obj.m` gives one object at run time.
41
+ module SelfCallIvarWrites
42
+ # The most `def` bodies one proof reads before it gives up.
43
+ DEF_BUDGET = 32
44
+ # Code the proof does not read past: it rebinds a global or constant, runs code the body does not show, or
45
+ # changes which method a later call dispatches to.
46
+ UNPROVABLE_NODES = Set[
47
+ Prism::GlobalVariableWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableAndWriteNode,
48
+ Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableTargetNode,
49
+ Prism::ConstantWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode,
50
+ Prism::ConstantOperatorWriteNode, Prism::ConstantTargetNode, Prism::ConstantPathWriteNode,
51
+ Prism::ConstantPathOrWriteNode, Prism::ConstantPathAndWriteNode, Prism::ConstantPathOperatorWriteNode,
52
+ Prism::ConstantPathTargetNode,
53
+ Prism::YieldNode, Prism::SuperNode, Prism::ForwardingSuperNode, Prism::ForwardingArgumentsNode,
54
+ Prism::BlockArgumentNode, Prism::DefNode, Prism::SingletonClassNode, Prism::ForNode,
55
+ Prism::LocalVariableOperatorWriteNode, Prism::ClassVariableOperatorWriteNode, Prism::CallOperatorWriteNode,
56
+ Prism::CallOrWriteNode, Prism::CallAndWriteNode, Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode,
57
+ Prism::IndexAndWriteNode
58
+ ].freeze
59
+ # Literal receivers, by the core class their value is an instance of.
60
+ LITERAL_CLASSES = {
61
+ Prism::StringNode => "String", Prism::InterpolatedStringNode => "String", Prism::XStringNode => "String",
62
+ Prism::SymbolNode => "Symbol", Prism::InterpolatedSymbolNode => "Symbol", Prism::IntegerNode => "Integer",
63
+ Prism::FloatNode => "Float", Prism::ArrayNode => "Array", Prism::HashNode => "Hash",
64
+ Prism::RangeNode => "Range", Prism::RegularExpressionNode => "Regexp",
65
+ Prism::InterpolatedRegularExpressionNode => "Regexp", Prism::NilNode => "NilClass",
66
+ Prism::TrueNode => "TrueClass", Prism::FalseNode => "FalseClass"
67
+ }.freeze
68
+ # The methods `Kernel` or `Object` own that a read accepts on `self` when the project does not define them. Any
69
+ # other (`display`, `inspect`, `==`, `hash`, `extend`, `method`, `binding`, `instance_variables`) proves nothing.
70
+ # These may still convert a value they are given, which runs that value's methods: {PROTOCOL_NAMES} answers for
71
+ # a value that is `self`.
72
+ SELF_SAFE_NAMES = Set[
73
+ :warn, :puts, :print, :p, :pp, :format, :sprintf, :printf, :raise, :fail, :loop, :catch, :throw, :rand,
74
+ :sleep, :Integer, :Float, :String, :Array, :Hash, :Rational, :Complex, :block_given?, :frozen?, :nil?,
75
+ :is_a?, :kind_of?, :instance_of?, :object_id, :class, :__method__, :caller, :abort, :exit
76
+ ].freeze
77
+ # Issue #1596 — the methods core code calls on a value without the program spelling them: conversions,
78
+ # comparisons, hashing, iteration, `respond_to?` and its hook, `exception` for `raise`, `divmod` for `sleep`.
79
+ # A value aliasing `self` (`@me = self`, `me = self`, a constant holding the instance) handed to a core
80
+ # method runs `self`'s own definition of one of these, which the read never sees, so a class defining any
81
+ # proves nothing.
82
+ PROTOCOL_NAMES = Set[
83
+ :to_s, :inspect, :hash, :eql?, :==, :===, :<=>, :to_a, :to_ary, :to_str, :to_hash, :to_int, :to_i, :to_f,
84
+ :to_r, :to_c, :to_proc, :to_path, :to_sym, :to_io, :coerce, :exception, :divmod, :each, :respond_to?,
85
+ :respond_to_missing?, :method_missing, :initialize_copy, :marshal_dump, :pretty_print
86
+ ].freeze
87
+ private_constant :UNPROVABLE_NODES, :LITERAL_CLASSES, :SELF_SAFE_NAMES, :PROTOCOL_NAMES
88
+
89
+ # Raised inside a proof to abandon it.
90
+ class Unprovable < StandardError
91
+ end
92
+ private_constant :Unprovable
93
+
94
+ module_function
95
+
96
+ # The names of the instance variables `call_node`, an implicit-self or `self.` call, may write, as a frozen
97
+ # `Set`, or nil when the analysis cannot prove the set. `scope` is the scope the call's operands were typed in.
98
+ def of(call_node, scope)
99
+ return nil unless call_node.is_a?(Prism::CallNode)
100
+ return nil unless call_node.receiver.nil? || call_node.receiver.is_a?(Prism::SelfNode)
101
+
102
+ self_type = scope.self_type
103
+ return nil unless self_type.is_a?(Type::Nominal)
104
+
105
+ proof = Proof.new(self_type, scope)
106
+ proof.no_self_value(call_node.arguments)
107
+ proof.no_protocol_override
108
+ proof.self_call(call_node)
109
+ proof.read(call_node.block)
110
+ proof.written.freeze
111
+ rescue StandardError # an `Unprovable`, or a lookup that raised: either proves nothing
112
+ nil
113
+ end
114
+
115
+ # One proof's state: the instance variables read code writes, and the `def` bodies already read.
116
+ class Proof
117
+ attr_reader :written
118
+
119
+ def initialize(self_type, scope)
120
+ @self_type = self_type
121
+ @class_name = self_type.class_name
122
+ @scope = scope
123
+ @written = Set.new
124
+ @visited = {}.compare_by_identity
125
+ end
126
+
127
+ # Reads the method an implicit-self or `self.` call to `call_node`'s name runs.
128
+ def self_call(call_node)
129
+ name = call_node.name
130
+ unprovable! if GuardRebinding.code_running_name?(name) || refined?(name)
131
+
132
+ if ProjectMethodOwnership.defines?(@class_name, name, :instance, @scope)
133
+ read_def(proven_def(name))
134
+ elsif !SELF_SAFE_NAMES.include?(name) || GuardRebinding.type_method_foreign?(@self_type, name, @scope)
135
+ unprovable!
136
+ end
137
+ end
138
+
139
+ # Issue #1596 — gives up when `self`'s class, a project subclass, or a project ancestor or module on either's
140
+ # chain defines a {PROTOCOL_NAMES} method (a `def`, `define_method`, `attr_*` or alias), rewrites its methods
141
+ # beyond the names it spells, or is refined on one; or when either chain holds an external ancestor, whose
142
+ # own definitions the read cannot see.
143
+ def no_protocol_override
144
+ classes = [@class_name]
145
+ RefinedDispatch::OwnMethod.each_subclass(@scope, @class_name) { |subclass| classes << subclass }
146
+ classes.each do |class_name|
147
+ chain = Scope::ResolutionChain.for(@scope, class_name.to_s, :instance, :methods)
148
+ unprovable! if chain.truncated? || chain.wildcard_mixin? || !settled?(chain)
149
+ chain.search(@scope) do |entry|
150
+ unprovable! if entry.external? || rewritten?(entry.name) || protocol_defined?(entry.name)
151
+ nil
152
+ end
153
+ end
154
+ end
155
+
156
+ # Issue #1596 — `self` handed on as a value (an argument, an interpolation, a splat, a `when` value, a literal
157
+ # element or key) lets the code it reaches run `self`'s methods by protocol (`to_s`, `hash`, `==`, `to_a`),
158
+ # which the read does not follow, so it proves nothing. Reads `node` for one, without reading anything else.
159
+ def no_self_value(node)
160
+ return unless node.is_a?(Prism::Node)
161
+
162
+ unprovable! if node.is_a?(Prism::SelfNode)
163
+ node.rigor_each_child { |child| no_self_value(child) }
164
+ end
165
+
166
+ # Reads `node`, adding each instance variable it writes.
167
+ def read(node)
168
+ return unless node.is_a?(Prism::Node)
169
+
170
+ unprovable! if UNPROVABLE_NODES.include?(node.class) || node.is_a?(Prism::SelfNode)
171
+ written = ivar_write_name(node)
172
+ @written << written if written
173
+ if node.is_a?(Prism::InstanceVariableOperatorWriteNode) && GuardRebinding.type_method_foreign?(
174
+ ivar_seed(node.name), node.binary_operator, @scope
175
+ )
176
+ unprovable!
177
+ end
178
+ read_call(node) if node.is_a?(Prism::CallNode)
179
+ # `self.m` is the one place `self` may stand: a call on it, which {#read_call} read.
180
+ receiver = call_receiver(node)
181
+ node.rigor_each_child { |child| read(child) unless child.is_a?(Prism::SelfNode) && child.equal?(receiver) }
182
+ end
183
+
184
+ private
185
+
186
+ # Whether a `refine` block anywhere in the project defines `name`, on any class: a `using` in effect where the
187
+ # read code sits may route the call to it (`refine Kernel do def warn …`), whatever the receiver.
188
+ def refined?(name)
189
+ @scope.discovered_refinements.any? { |_refined, methods| methods.key?(name) }
190
+ end
191
+
192
+ def call_receiver(node) = node.is_a?(Prism::CallNode) ? node.receiver : nil
193
+
194
+ def ivar_write_name(node)
195
+ case node
196
+ when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
197
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
198
+ node.name
199
+ end
200
+ end
201
+
202
+ def read_call(node)
203
+ receiver = node.receiver
204
+ return self_call(node) if receiver.nil? || receiver.is_a?(Prism::SelfNode)
205
+
206
+ unprovable! if GuardRebinding.code_running_name?(node.name) || refined?(node.name)
207
+ type = receiver_type(receiver)
208
+ unprovable! if type.nil? || GuardRebinding.type_method_foreign?(type, node.name, @scope)
209
+ end
210
+
211
+ def read_def(def_node)
212
+ return if @visited.key?(def_node)
213
+
214
+ @visited[def_node] = true
215
+ unprovable! if @visited.size > DEF_BUDGET
216
+ read(def_node.parameters)
217
+ read(def_node.body)
218
+ end
219
+
220
+ # The `def` the name resolves to on the class and on every project subclass, or unprovable.
221
+ def proven_def(name)
222
+ unprovable! if RefinedDispatch::OwnMethod.module_name?(@scope, @class_name)
223
+
224
+ owner = definer_ahead(@class_name, name, nil)
225
+ unprovable! if owner.nil?
226
+ def_node = @scope.user_def_for(owner, name)
227
+ unprovable! unless def_node.is_a?(Prism::DefNode) && def_node.receiver.nil?
228
+ single_definition(owner, name)
229
+
230
+ # A subclass whose superclass the tables do not know is enumerated too, and its chain ends short of the
231
+ # owner; one that reaches no definer of its own leaves the call to its superclass.
232
+ RefinedDispatch::OwnMethod.each_subclass(@scope, @class_name) do |subclass|
233
+ unprovable! unless [owner, nil].include?(definer_ahead(subclass, name, owner))
234
+ end
235
+ def_node
236
+ end
237
+
238
+ # Issue #1596 — the `def` the table holds must be the owner's only definition of `name`: the owner declared in
239
+ # one file, with one `def` of the name there (a reopening or a `class_eval` body in the same file is a second
240
+ # one), and no `define_method`, `attr_*` or alias of it, nor a rewrite no literal names (`T.define_method`,
241
+ # `T.send(:define_method, …)`, `T.class_eval` with a computed name), nor a definition the walk could not prove
242
+ # runs (a possible or contested one, as `DefinerResolution` reads them). The class edge files every declaring
243
+ # file.
244
+ def single_definition(owner, name)
245
+ Scope::ResolutionChain.record_class(@scope, owner) if Analysis::DependencyRecorder.active?
246
+ unprovable! if possible_definition?(owner, name)
247
+ bucket = @scope.discovered_parameter_envelopes[owner] || {}
248
+ unprovable! if bucket[[:instance, name]] == Source::ParameterEnvelope::OPAQUE
249
+ unprovable! if (@scope.discovered_class_sources[owner]&.size || 0) > 1
250
+ unprovable! if definition_count(owner, name) > 1
251
+ end
252
+
253
+ def possible_definition?(owner, name)
254
+ discovery = @scope.discovery
255
+ discovery.possible_method?(owner, name, :instance) ||
256
+ discovery.contested?(:discovered_def_nodes, [owner, name]) ||
257
+ discovery.contested?(:discovered_parameter_envelopes, [owner, [:instance, name]])
258
+ end
259
+
260
+ def definition_count(owner, name)
261
+ @scope.discovered_deferred_ranges.sum do |_path, rows|
262
+ rows.count { |row| row[2] == name && row[3] == :instance && row[4] == owner }
263
+ end
264
+ end
265
+
266
+ # Whether `chain`'s own order stands with no second world: no order mark and no fork. `ResolutionChain#settle`
267
+ # answers `:chain` for an answer no world can match only then; a fork sends it to the retro world, which the
268
+ # block answers otherwise, or to master's order.
269
+ def settled?(chain)
270
+ chain.settle(@scope, :chain_order) { :other_world } == :chain
271
+ end
272
+
273
+ def protocol_defined?(class_name)
274
+ refined = @scope.discovered_refinements[class_name] || {}
275
+ PROTOCOL_NAMES.any? { |name| defines_here?(class_name, name) || refined.key?(name) }
276
+ end
277
+
278
+ # Whether the project rewrites `class_name`'s method table beyond the names it spells, or refines it.
279
+ def rewritten?(class_name)
280
+ bucket = @scope.discovered_parameter_envelopes[class_name]
281
+ !bucket.nil? && Scope::DiscoveryIndex.rewritten_surface?(bucket)
282
+ end
283
+
284
+ # The first project class or module on `class_name`'s settled instance chain that defines `name`, stopping
285
+ # at `owner`; nil when the chain ends without one. An external ancestor reached first is unprovable: its
286
+ # signature, or a gem without one, may define the name.
287
+ def definer_ahead(class_name, name, owner)
288
+ chain = Scope::ResolutionChain.for(@scope, class_name.to_s, :instance, :methods)
289
+ unprovable! if chain.truncated? || chain.wildcard_mixin? || !settled?(chain)
290
+
291
+ chain.search(@scope) do |entry|
292
+ unprovable! if entry.external? || rewritten?(entry.name)
293
+
294
+ entry.name if entry.name == owner || defines_here?(entry.name, name)
295
+ end
296
+ end
297
+
298
+ # A `def`, `define_method`, `attr_*` or alias of `name` in `class_name`'s own body: the discovery table holds
299
+ # the last three project-wide, and the `def` table the first.
300
+ def defines_here?(class_name, name)
301
+ @scope.discovered_method?(class_name, name, :instance) || !@scope.def_node_slot(class_name, name).nil?
302
+ end
303
+
304
+ def receiver_type(node)
305
+ literal = LITERAL_CLASSES[node.class]
306
+ return Type::Combinator.nominal_of(literal) if literal
307
+
308
+ case node
309
+ when Prism::GlobalVariableReadNode then @scope.type_of(node)
310
+ when Prism::InstanceVariableReadNode then ivar_seed(node.name)
311
+ when Prism::ConstantReadNode, Prism::ConstantPathNode then core_constant_type(node)
312
+ end
313
+ end
314
+
315
+ def ivar_seed(name)
316
+ @scope.class_ivars_for(@class_name)[name] || unprovable!
317
+ end
318
+
319
+ # A constant receiver read in the caller's scope, which the callee's lexical scope may resolve otherwise: only
320
+ # a core or standard-library class object or instance, under a name no project class or constant ends with.
321
+ def core_constant_type(node)
322
+ type = @scope.type_of(node)
323
+ return nil unless type.is_a?(Type::Singleton) || type.is_a?(Type::Nominal)
324
+
325
+ loader = @scope.environment.rbs_loader
326
+ return nil if loader.nil? || !loader.core_or_stdlib_class?(type.class_name)
327
+
328
+ segment = node.name.to_s
329
+ project = [@scope.discovered_classes, @scope.in_source_constants]
330
+ return nil if project.any? { |table| table.each_key.any? { |name| name.to_s.split("::").last == segment } }
331
+
332
+ type
333
+ end
334
+
335
+ def unprovable!
336
+ raise Unprovable
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end
@@ -27,6 +27,7 @@ require_relative "content_join"
27
27
  require_relative "define_method_block_self"
28
28
  require_relative "macro_block_self_type"
29
29
  require_relative "guard_rebinding"
30
+ require_relative "self_call_ivar_writes"
30
31
  require_relative "match_rebinding"
31
32
  require_relative "element_read_widening"
32
33
  require_relative "hash_lookup_mutation"
@@ -123,9 +124,9 @@ module Rigor
123
124
  Prism::BeginNode => :eval_begin_node,
124
125
  Prism::RescueNode => :eval_rescue,
125
126
  Prism::EnsureNode => :eval_ensure,
126
- Prism::WhileNode => :eval_loop,
127
- Prism::UntilNode => :eval_loop,
128
- Prism::ForNode => :eval_for,
127
+ Prism::WhileNode => :eval_repeating_loop,
128
+ Prism::UntilNode => :eval_repeating_loop,
129
+ Prism::ForNode => :eval_repeating_for,
129
130
  Prism::AndNode => :eval_and_or,
130
131
  Prism::OrNode => :eval_and_or,
131
132
  Prism::ParenthesesNode => :eval_parentheses,
@@ -525,10 +526,13 @@ module Rigor
525
526
  [result_type, current]
526
527
  end
527
528
 
529
+ # The top level is a straight-line body: an index `||=` there whose slot an earlier store may have filled is
530
+ # marked ({RepeatedOrWrites.straight_line_sites}).
528
531
  def eval_program(node)
529
532
  return [Type::Combinator.constant_of(nil), scope] if node.statements.nil?
530
533
 
531
- sub_eval(node.statements, scope)
534
+ marks = RepeatedOrWrites.straight_line_sites(node.statements, scope)
535
+ sub_eval(node.statements, scope.with_repeated_or_writes(marks))
532
536
  end
533
537
 
534
538
  # `name = rvalue` evaluates the rvalue under the entry scope (so earlier assignments in a chained `a = b = expr`
@@ -1380,46 +1384,46 @@ module Rigor
1380
1384
  end
1381
1385
 
1382
1386
  # Returns `[body_scope, updated_falsey_scope]` for a single branch. `WhenNode` branches narrow through
1383
- # `Narrowing.case_when_scopes`. `InNode` branches narrow soundly only for a bare class pattern (`in C` / `in C =>
1384
- # x`, pure `is_a?`); every other pattern keeps the conservative "body = entry + bindings, falsey unchanged" shape.
1385
- # `subject_type` is the predicate's type, which an `in` branch's pattern decomposes to type the names it binds.
1387
+ # `Narrowing.case_when_scopes`, or through {#subjectless_when_scopes} in a `case` without a subject. `InNode`
1388
+ # branches narrow soundly only for a bare class pattern (`in C` / `in C => x`, pure `is_a?`); every other pattern
1389
+ # keeps the conservative "body = entry + bindings, falsey unchanged" shape. `subject_type` is the predicate's
1390
+ # type, which an `in` branch's pattern decomposes to type the names it binds.
1386
1391
  def branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
1387
1392
  if branch.is_a?(Prism::InNode)
1388
1393
  in_branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
1394
+ elsif subject.nil?
1395
+ subjectless_when_scopes(branch.conditions, falsey_scope)
1389
1396
  else
1390
1397
  when_conditions = branch.respond_to?(:conditions) ? branch.conditions : []
1391
1398
  Narrowing.case_when_scopes(subject, when_conditions, falsey_scope)
1392
1399
  end
1393
1400
  end
1394
1401
 
1402
+ # Issue #1495 — a `case` without a subject tests each `when` condition for truthiness, in order, and stops at the
1403
+ # first truthy one, so a clause is the `||` of its conditions in an `if` / `elsif` chain. Each condition is
1404
+ # evaluated under the falsey edges of the conditions before it, in this clause and every earlier one; the body
1405
+ # joins the conditions' truthy edges, and the next clause starts from the last falsey edge. A condition
1406
+ # `Narrowing.predicate_scopes` does not recognise narrows nothing: a splat (`when *conds`), and a regexp literal,
1407
+ # which is tested for its truthiness and matches nothing, so it binds no match global (#1486).
1408
+ def subjectless_when_scopes(conditions, entry)
1409
+ falsey = entry
1410
+ truthy_edges = conditions.map do |condition|
1411
+ _type, _post, truthy, falsey = eval_with_edges(condition, falsey)
1412
+ truthy
1413
+ end
1414
+ [reduce_scopes_with_nil_injection(truthy_edges), falsey]
1415
+ end
1416
+
1395
1417
  # ADR-47 WD3a — a bare class pattern matches on `C === subject`, i.e. exactly `subject.is_a?(C)` with no
1396
1418
  # deconstruction, so it narrows like `when C`: the body sees the subject narrowed to `C` and the next clause's
1397
1419
  # falsey scope has `C` removed. Other patterns can fail to match even when a class test would pass (deconstruction
1398
1420
  # arity, hash keys, ...), so removing anything from the falsey scope would be unsound — they keep the conservative
1399
1421
  # shape.
1400
1422
  def in_branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
1401
- class_node = bare_class_pattern_node(branch.pattern)
1402
- unless class_node
1403
- bound = apply_in_pattern_bindings(subject_type, subject, branch.pattern, falsey_scope)
1404
- return [bound, falsey_scope]
1405
- end
1406
-
1407
- truthy_scope, narrowed_falsey = Narrowing.case_when_scopes(subject, [class_node], falsey_scope)
1423
+ truthy_scope, narrowed_falsey = Narrowing.case_clause_scopes(subject, branch, falsey_scope)
1408
1424
  [apply_in_pattern_bindings(subject_type, subject, branch.pattern, truthy_scope), narrowed_falsey]
1409
1425
  end
1410
1426
 
1411
- # The class-constant node of a `in C` / `in C => x` pattern (the only `in` shapes whose match is pure `is_a?`), or
1412
- # nil for any pattern that deconstructs, binds, or matches a value.
1413
- def bare_class_pattern_node(pattern)
1414
- case pattern
1415
- when Prism::ConstantReadNode, Prism::ConstantPathNode
1416
- pattern
1417
- when Prism::CapturePatternNode
1418
- value = pattern.value
1419
- value if value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
1420
- end
1421
- end
1422
-
1423
1427
  def eval_case_else(else_clause, falsey_scope)
1424
1428
  return sub_eval(else_clause, falsey_scope) if else_clause
1425
1429
 
@@ -2065,6 +2069,12 @@ module Rigor
2065
2069
  after.global(:$_) == Type::Combinator.untyped && !LastLine.may_set?(node.statements, scope)
2066
2070
  end
2067
2071
 
2072
+ # Issue #1889 — {#eval_loop} / {#eval_for} with a literal-name `instance_variable_set` in the body joining its
2073
+ # value into the ivar's binding rather than replacing it ({ReflectiveIvarWrite.in_repeating_body}).
2074
+ def eval_repeating_loop(node) = ReflectiveIvarWrite.in_repeating_body { eval_loop(node) }
2075
+
2076
+ def eval_repeating_for(node) = ReflectiveIvarWrite.in_repeating_body { eval_for(node) }
2077
+
2068
2078
  # `while pred; body; end` / `until pred; body; end`. The body might run zero or more times, so half-bound names
2069
2079
  # degrade to `T | nil` in the post-loop scope. The loop expression itself types as `Constant[nil]` (Slice 3 phase
2070
2080
  # 1), reflecting the common case where no `break VALUE` is observed.
@@ -2929,6 +2939,7 @@ module Rigor
2929
2939
  # evaluates `def` to the method's name as a Symbol, so the produced type is `Constant[:name]`.
2930
2940
  def eval_def(node)
2931
2941
  body_scope = build_method_entry_scope(node)
2942
+ body_scope = body_scope.with_repeated_or_writes(RepeatedOrWrites.straight_line_sites(node.body, body_scope))
2932
2943
  # Parameter default value expressions (e.g. `self.x` in `def copy(x: self.x)`) execute when the method is
2933
2944
  # *invoked*, not when the `def` is read; their `self` is the instance receiver, not the surrounding class body.
2934
2945
  # Walk the parameters subtree under `body_scope` so the scope-index records the instance `self_type` for every
@@ -3170,6 +3181,9 @@ module Rigor
3170
3181
  # observes the seed's union (not the pre-call narrowed value). Always-safe (only widens; no new facts). See
3171
3182
  # [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md) § "Flow-folding" — G2 intervening-call case.
3172
3183
  post_scope = invalidate_ivars_for_intervening_call(node, post_scope) if statement_call
3184
+ # Issue #1889 — `instance_variable_set` on `self` writes an ivar no write node shows. It runs after the reset
3185
+ # above, which would join a literal-name write back into the class seed.
3186
+ post_scope = ReflectiveIvarWrite.apply(node, post_scope) { type_operand(ReflectiveIvarWrite.value_node(node)) }
3173
3187
  # C1 — regex match-data globals (`$~`, `$1..$N`, `$&`, …) are narrowed to non-nil on a successful-match edge; a
3174
3188
  # later call that itself runs a regex match rebinds them, so the narrowed facts must be dropped. We forget them
3175
3189
  # only when the call may run a match in this frame ({#rebinds_match_globals?}). A call provably match-free on a
@@ -3191,7 +3205,13 @@ module Rigor
3191
3205
  post_scope = post_scope.forget_match_globals if rebinds_match_globals?(node, post_scope)
3192
3206
  post_scope = post_scope.forget_last_line if rebinds_last_line?(node, post_scope)
3193
3207
  if post_scope.guard_narrowed? && GuardRebinding.call_may_rebind?(node, operand_scope)
3194
- post_scope = post_scope.forget_guard_narrowings
3208
+ # Issue #1596 — a self-call proven to write only some instance variables restores only theirs.
3209
+ writes = intervening_call_candidate?(node) ? self_call_ivar_writes(node) : nil
3210
+ post_scope = if writes
3211
+ writes.reduce(post_scope) { |acc, name| acc.forget_guard_narrowing_of(:ivar, name) }
3212
+ else
3213
+ post_scope.forget_guard_narrowings
3214
+ end
3195
3215
  end
3196
3216
  forget_rescued_status(LastStatus.after(node, post_scope, scope), node)
3197
3217
  end
@@ -3316,6 +3336,7 @@ module Rigor
3316
3336
  # Returns a scope with each ivar's narrowed local binding widened back to its class-ivar seed value when the call
3317
3337
  # is one that could plausibly mutate ivars on the enclosing class (implicit-self or explicit `self.foo`).
3318
3338
  # External- receiver calls (`obj.method`) cannot reach the caller's ivars; they pass through unchanged.
3339
+ # Issue #1596 — where the call is proven to write only some ivars ({#self_call_ivar_writes}), only those widen.
3319
3340
  def invalidate_ivars_for_intervening_call(call_node, current_scope)
3320
3341
  return current_scope unless intervening_call_candidate?(call_node)
3321
3342
 
@@ -3330,11 +3351,27 @@ module Rigor
3330
3351
  local_type = current_scope.ivar(ivar_name)
3331
3352
  next if local_type.nil? || local_type == seed_type
3332
3353
 
3333
- widened = widened.with_ivar(ivar_name, Type::Combinator.union(local_type, seed_type))
3354
+ writes = self_call_ivar_writes(call_node)
3355
+ next if writes && !writes.include?(ivar_name)
3356
+
3357
+ joined = Type::Combinator.union(local_type, seed_type)
3358
+ widened = widened.with_ivar(ivar_name, joined)
3359
+ # Issue #1889 — an ivar a reflective write widened keeps its declaration mark, so the reset must too.
3360
+ widened = widened.seed_declaration_sourced_ivar(ivar_name, joined) if
3361
+ current_scope.declaration_sourced?(:ivar, ivar_name)
3334
3362
  end
3335
3363
  widened
3336
3364
  end
3337
3365
 
3366
+ # Issue #1596 — the ivars the implicit-self / `self.` call `call_node` may write, or nil when that is not proven
3367
+ # ({SelfCallIvarWrites.of}). Memoised per call node: the reset and the guard restore ask it of the same call.
3368
+ def self_call_ivar_writes(call_node)
3369
+ memo = (@self_call_ivar_writes ||= {}.compare_by_identity)
3370
+ return memo[call_node] if memo.key?(call_node)
3371
+
3372
+ memo[call_node] = SelfCallIvarWrites.of(call_node, operand_scope)
3373
+ end
3374
+
3338
3375
  def intervening_call_candidate?(call_node)
3339
3376
  return false unless call_node.is_a?(Prism::CallNode)
3340
3377
 
@@ -5412,7 +5449,7 @@ module Rigor
5412
5449
  return [] if receiver_type.nil?
5413
5450
 
5414
5451
  arg_types = call_arg_types_for(call_node)
5415
- MethodDispatcher.expected_block_param_types(
5452
+ expected = MethodDispatcher.expected_block_param_types(
5416
5453
  receiver_type: receiver_type,
5417
5454
  method_name: call_node.name,
5418
5455
  arg_types: arg_types,
@@ -5420,10 +5457,28 @@ module Rigor
5420
5457
  scope: scope,
5421
5458
  call_node: call_node
5422
5459
  )
5460
+ with_inject_accumulator(call_node, receiver_type, arg_types, expected)
5423
5461
  rescue StandardError
5424
5462
  []
5425
5463
  end
5426
5464
 
5465
+ # Issue #1889 — a block-form `inject` / `reduce` call's accumulator parameter at the type it holds across
5466
+ # every run ({ExpressionTyper#inject_accumulator_type}) rather than the seed the signature binds it to, once
5467
+ # per call: every write-back pass builds the entry again.
5468
+ def with_inject_accumulator(call_node, receiver_type, arg_types, expected)
5469
+ memo = @inject_accumulator
5470
+ accumulator =
5471
+ if memo && memo[0].equal?(call_node)
5472
+ memo[1]
5473
+ else
5474
+ typed = ExpressionTyper.new(scope: scope, tracer: tracer)
5475
+ .inject_accumulator_type(call_node, receiver_type, arg_types)
5476
+ @inject_accumulator = [call_node, typed]
5477
+ typed
5478
+ end
5479
+ accumulator ? [accumulator, *expected.drop(1)] : expected
5480
+ end
5481
+
5427
5482
  def call_arg_types_for(call_node)
5428
5483
  arguments = call_node.arguments
5429
5484
  return [] if arguments.nil?
@@ -32,9 +32,9 @@ module Rigor
32
32
  end
33
33
 
34
34
  # The anchor of the declaration spanning bytes `start_offset...end_offset` and lines `start_line..end_line`.
35
- def for_span(start_offset, end_offset, start_line, end_line)
36
- leading = leading_contract_comments(start_line)
37
- trailing = trailing_contract_comment(end_line, end_offset)
35
+ def for_span(start_offset, end_offset, start_line, end_line, leading: true, trailing: true)
36
+ leading = leading ? leading_contract_comments(start_line) : []
37
+ trailing = trailing ? trailing_contract_comment(end_line, end_offset) : nil
38
38
  material = +"".b
39
39
  leading.each { |comment| material << line_text_from(comment.location.start_offset) << "\n" }
40
40
  material << @source.byteslice(start_offset, end_offset - start_offset)
@@ -34,6 +34,21 @@ module Rigor
34
34
  unreadable("definition_build_failed", [])
35
35
  end
36
36
 
37
+ # The declared type of the constant `name` (qualified), as a slot with its declaration's provenance, or nil
38
+ # when no RBS or inline declaration names it.
39
+ def read_constant(name, environment)
40
+ declaration = environment&.rbs_loader&.constant_declaration(name)
41
+ return nil if declaration.nil?
42
+
43
+ type, buffer = declaration
44
+ provenance, path = @provenance.classify(buffer)
45
+ return Slot.unavailable("not_computed") if provenance.nil?
46
+
47
+ Slot.from_rbs(type, provenance, path)
48
+ rescue StandardError
49
+ Slot.unavailable("not_computed")
50
+ end
51
+
37
52
  private
38
53
 
39
54
  def contract(defs, annotations)
@@ -10,8 +10,8 @@ module Rigor
10
10
  module Lens
11
11
  # Assembles the `lens/v0` document (docs/internal-spec/lens.md § Document) from a {Run::Outcome}.
12
12
  class Document
13
- SITE_FIELDS = %w[key kind name owner scope attribute members ordinal span contract_span anchor visibility
14
- contract params block body_return].freeze
13
+ SITE_FIELDS = %w[key kind name owner scope attribute members module_function builder ordinal span contract_span
14
+ anchor visibility contract params block body_return value alias_of].freeze
15
15
 
16
16
  def initialize(outcome:, configuration:, root:, config_path:)
17
17
  @outcome = outcome