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
@@ -8,6 +8,7 @@ require_relative "parameters"
8
8
  require_relative "../inference/def_return_typer"
9
9
  require_relative "../inference/origin_lookup"
10
10
  require_relative "../sig_gen/type_elaborator"
11
+ require_relative "../source/alias_names"
11
12
 
12
13
  module Rigor
13
14
  module Lens
@@ -22,7 +23,10 @@ module Rigor
22
23
  # walk cannot place (under an `if`, a `begin`, a block) leaves that default unknown.
23
24
  # A `detached` body stands for code that is not the class body (a block, a method body, a superclass
24
25
  # expression, an omitted class): its toggles and named calls stay local and change no site.
25
- Body = Struct.new(:current, :uncertain, :detached)
26
+ # `kind` is :module, :class or :singleton. `module_function` is :off, :on (a bare `module_function` written
27
+ # directly in a module body covers the later direct defs) or :unknown (the reading Ruby gives and the one
28
+ # `check`'s tables give could differ, so the later defs are omissions).
29
+ Body = Struct.new(:current, :uncertain, :detached, :kind, :module_function)
26
30
 
27
31
  ATTRIBUTE_CALLS = { attr_reader: "reader", attr_writer: "writer", attr_accessor: "accessor" }.freeze
28
32
  OMITTED_CALLS = {
@@ -30,6 +34,8 @@ module Rigor
30
34
  refine: "refine"
31
35
  }.freeze
32
36
  VISIBILITY_CALLS = %i[private protected public].freeze
37
+ # Instance methods Ruby makes private whatever the visibility they are defined or aliased with.
38
+ FORCED_PRIVATE = %w[initialize initialize_copy initialize_clone initialize_dup respond_to_missing?].freeze
33
39
  CLASS_VISIBILITY_CALLS = { private_class_method: "private", public_class_method: "public" }.freeze
34
40
  BLOCK_NODES = [Prism::BlockNode, Prism::LambdaNode].freeze
35
41
  CONSTANT_WRITES = [
@@ -66,10 +72,11 @@ module Rigor
66
72
  when Prism::SingletonClassNode then walk_singleton_class(node, nesting, singleton, in_block)
67
73
  when Prism::DefNode then read_def(node, nesting, singleton, in_block)
68
74
  when Prism::CallNode then walk_call(node, nesting, singleton, in_block)
69
- when Prism::AliasMethodNode
70
- omit("alias", node.new_name.slice.delete_prefix(":"), node, owner: nesting.last, singleton: singleton)
75
+ when Prism::AliasMethodNode then read_alias(node, nesting, singleton, in_block)
71
76
  when Prism::MultiWriteNode then walk_multi_write(node, nesting, singleton, in_block)
72
- when *CONSTANT_WRITES then walk_constant(node, nesting)
77
+ when Prism::ForNode then walk_assigning(node, node.index, nesting, singleton, in_block)
78
+ when Prism::RescueNode then walk_assigning(node, node.reference, nesting, singleton, in_block)
79
+ when *CONSTANT_WRITES then walk_constant(node, nesting, in_block)
73
80
  else walk_children(node, nesting, singleton, in_block)
74
81
  end
75
82
  end
@@ -85,7 +92,7 @@ module Rigor
85
92
  end
86
93
 
87
94
  def detached
88
- @bodies.push(Body.new("public", false, true))
95
+ @bodies.push(Body.new("public", false, true, :detached, :off))
89
96
  yield
90
97
  ensure
91
98
  @bodies.pop
@@ -97,16 +104,16 @@ module Rigor
97
104
  if node.is_a?(Prism::ClassNode) && node.superclass
98
105
  detached { walk(node.superclass, nesting, singleton: false, in_block: true) }
99
106
  end
100
- if in_block || name.nil?
101
- omit(kind, name || node.constant_path.slice, node)
107
+ if in_block || name.nil? || constant_scope_unknown?
108
+ omit(kind, constant_scope_unknown? ? node.constant_path.slice : (name || node.constant_path.slice), node)
102
109
  detached { walk(node.body, nesting, singleton: false, in_block: true) } if node.body
103
110
  return
104
111
  end
105
112
 
106
113
  add_site(node, { "kind" => kind, "name" => name, "owner" => nil, "scope" => nil, "attribute" => nil,
107
- "members" => [], "visibility" => nil, "params" => nil, "block" => nil,
108
- "body_return" => nil })
109
- walk_body(node.body, nesting + [name], singleton: false)
114
+ "members" => [], "module_function" => false, "visibility" => nil, "params" => nil,
115
+ "block" => nil, "body_return" => nil })
116
+ walk_body(node.body, nesting + [name], singleton: false, kind: kind.to_sym)
110
117
  end
111
118
 
112
119
  # `class << self` directly in a class or module body opens its singleton class. Any other `class << x`, one
@@ -117,33 +124,79 @@ module Rigor
117
124
  return detached { walk(node.body, nesting, singleton: singleton, in_block: true) } if node.body
118
125
  end
119
126
 
120
- walk_body(node.body, nesting, singleton: true)
127
+ walk_body(node.body, nesting, singleton: true, kind: :singleton)
121
128
  end
122
129
 
123
- # A class body's statements, tracking the running visibility and `module_function`. A bare `module_function`
124
- # makes every later `def` of the body a method on both sides: one site, two members, which revision 1 does
125
- # not model.
126
- def walk_body(body, nesting, singleton:)
130
+ # A class body's statements, tracking the running visibility and `module_function`.
131
+ def walk_body(body, nesting, singleton:, kind:)
127
132
  return if body.nil?
128
133
 
129
- @bodies.push(Body.new("public", false, false))
134
+ @bodies.push(Body.new("public", false, false, kind, :off))
130
135
  statements = body.is_a?(Prism::StatementsNode) ? body.body : [body]
131
- module_function_on = false
132
- statements.each do |child|
133
- if module_function_on && child.is_a?(Prism::DefNode)
134
- omit("module_function", child.name.to_s, child, owner: nesting.last, singleton: singleton)
135
- next
136
- end
137
- module_function_on = true if bare_call?(child, :module_function)
138
- next if toggle_visibility?(child)
139
-
140
- visibility_call(child, nesting, singleton, direct: true) if visibility_call?(child)
141
- walk(child, nesting, singleton: singleton, in_block: false)
142
- end
136
+ statements.each { |child| walk_body_statement(child, nesting, singleton) }
143
137
  ensure
144
138
  @bodies.pop if body
145
139
  end
146
140
 
141
+ def walk_body_statement(child, nesting, singleton)
142
+ return bare_module_function if bare_call?(child, :module_function)
143
+ return end_module_function if toggle_visibility?(child)
144
+ return named_module_function(child, nesting, singleton) if named_module_function?(child)
145
+ return constant_visibility(child, nesting, direct: true) if constant_visibility_call?(child)
146
+
147
+ visibility_call(child, nesting, singleton, direct: true) if visibility_call?(child)
148
+ @direct_statement = child
149
+ walk(child, nesting, singleton: singleton, in_block: false)
150
+ ensure
151
+ @direct_statement = nil
152
+ end
153
+
154
+ # A bare `module_function` directly in a module body turns the mode on for the later direct defs. In a class
155
+ # (where Ruby raises) or after the mode was left unknown, the later defs stay omissions. A second bare call
156
+ # after a toggle also stays unknown, though Ruby turns the mode on again: the safe side.
157
+ def bare_module_function
158
+ body = @bodies.last
159
+ body.module_function = body.kind == :module && body.module_function != :unknown ? :on : :unknown
160
+ end
161
+
162
+ # A bare `private` / `public` / `protected` ends the mode in Ruby, but `check`'s tables keep it on. The two
163
+ # readings differ for the later defs, so they are left as omissions.
164
+ def end_module_function
165
+ body = @bodies.last
166
+ body.module_function = :unknown if body.module_function == :on
167
+ end
168
+
169
+ def named_module_function?(child)
170
+ child.is_a?(Prism::CallNode) && child.receiver.nil? && child.name == :module_function && child.arguments
171
+ end
172
+
173
+ # A named `module_function` (`module_function :a`, `module_function %i[a]`, `module_function def a`) is an
174
+ # omission: `check`'s singleton table reads it differently from Ruby (a def nested in the body, a reopened
175
+ # module, a redefinition after the call, a wrapped def). The instance methods it names turn private in Ruby,
176
+ # so their visibility reads unknown, and the omission makes each named member contested.
177
+ def named_module_function(node, nesting, singleton)
178
+ owner = nesting.last
179
+ node.arguments.arguments.each do |argument|
180
+ # The copy replaces any `def self.x` already there, so the singleton side is contested as well.
181
+ names = argument.is_a?(Prism::DefNode) ? [argument.name.to_s] : argument_names(argument)
182
+ if names.nil?
183
+ [false, true].each do |side|
184
+ unknown_visibility(owner, scope_name(side), default_too: false)
185
+ omit("module_function", nil, argument, owner: owner, singleton: side, any_member: true)
186
+ end
187
+ else
188
+ names.product([false, true]).each do |name, side|
189
+ name_visibility(owner, scope_name(side), name, nil)
190
+ omit("module_function", name, argument.is_a?(Prism::DefNode) ? argument : node, owner: owner,
191
+ singleton: side)
192
+ end
193
+ end
194
+ if argument.is_a?(Prism::DefNode) && argument.body
195
+ detached { walk(argument.body, nesting, singleton: singleton, in_block: true) }
196
+ end
197
+ end
198
+ end
199
+
147
200
  # A bare `private` / `protected` / `public` written directly in the body sets the default for later defs.
148
201
  def toggle_visibility?(child)
149
202
  return false unless VISIBILITY_CALLS.any? { |name| bare_call?(child, name) }
@@ -157,20 +210,32 @@ module Rigor
157
210
  if node.receiver.nil? && ATTRIBUTE_CALLS.key?(node.name)
158
211
  read_attribute(node, nesting, singleton, in_block)
159
212
  elsif node.receiver.nil? && OMITTED_CALLS.key?(node.name)
160
- return if bare_call?(node, :module_function)
161
-
162
- omit(OMITTED_CALLS.fetch(node.name), first_argument_name(node), node,
163
- owner: node.name == :refine ? nil : nesting.last, singleton: singleton)
164
- walk_children(node, nesting, singleton, true) unless node.name == :refine
213
+ omitted_call(node, nesting, singleton, in_block)
214
+ elsif constant_visibility_call?(node)
215
+ constant_visibility(node, nesting, direct: false)
165
216
  else
166
- @visited_visibility_calls ||= {}.compare_by_identity
167
- if visibility_call?(node) && !@visited_visibility_calls.key?(node)
168
- visibility_call(node, nesting, singleton, direct: false)
169
- end
217
+ unplaced_visibility_call(node, nesting, singleton)
170
218
  walk_children(node, nesting, singleton, in_block)
171
219
  end
172
220
  end
173
221
 
222
+ # A visibility call the body walk did not already read directly.
223
+ def unplaced_visibility_call(node, nesting, singleton)
224
+ @visited_visibility_calls ||= {}.compare_by_identity
225
+ return unless visibility_call?(node) && !@visited_visibility_calls.key?(node)
226
+
227
+ visibility_call(node, nesting, singleton, direct: false)
228
+ end
229
+
230
+ def omitted_call(node, nesting, singleton, in_block)
231
+ return unplaced_module_function if bare_call?(node, :module_function)
232
+ return read_alias(node, nesting, singleton, in_block) if node.name == :alias_method
233
+
234
+ omit(OMITTED_CALLS.fetch(node.name), first_argument_name(node), node,
235
+ owner: node.name == :refine ? nil : nesting.last, singleton: singleton)
236
+ walk_children(node, nesting, singleton, true) unless node.name == :refine
237
+ end
238
+
174
239
  def visibility_call?(node)
175
240
  node.is_a?(Prism::CallNode) && node.receiver.nil? &&
176
241
  (VISIBILITY_CALLS.include?(node.name) || CLASS_VISIBILITY_CALLS.key?(node.name))
@@ -235,63 +300,354 @@ module Rigor
235
300
  end
236
301
  end
237
302
 
238
- def unknown_visibility(owner, scope)
303
+ # `default_too: false` leaves the body's running default alone: `module_function(*NAMES)` changes only the
304
+ # members it names, while `private *NAMES` is read as making the default unknown too, the safe side.
305
+ def unknown_visibility(owner, scope, default_too: true)
239
306
  @sites.each do |site|
240
- site.data["visibility"] = nil if site.data["owner"] == owner && site.data["scope"] == scope
307
+ data = site.data
308
+ next unless data["owner"] == owner
309
+
310
+ data["visibility"] = nil if data["scope"] == scope
311
+ data["singleton_visibility"] = nil if scope == "singleton" && data["module_function"]
241
312
  end
242
- mark_uncertain
313
+ mark_uncertain if default_too
243
314
  end
244
315
 
245
316
  # `private :x` changes the visibility of the members already defined under that name in this file.
246
317
  def name_visibility(owner, scope, name, visibility)
247
318
  @sites.each do |site|
248
319
  data = site.data
249
- next unless data["owner"] == owner && data["scope"] == scope && data["members"].include?(name)
320
+ next unless data["owner"] == owner && data["members"].include?(name)
250
321
 
251
- data["visibility"] = data["members"] == [name] ? visibility : nil
322
+ if data["scope"] == scope
323
+ data["visibility"] = data["members"] == [name] ? visibility : nil
324
+ elsif scope == "singleton" && data["module_function"]
325
+ # `private_class_method :x` reaches a module function's singleton copy.
326
+ data["singleton_visibility"] = visibility
327
+ end
252
328
  end
253
329
  end
254
330
 
331
+ # `a, B = …`: each constant target is an omission. The targets are walked too, since an index or call target
332
+ # (`a[expr], b = …`) or a constant path's parent can hold a declaration; the value is walked last.
255
333
  def walk_multi_write(node, nesting, singleton, in_block)
256
- (node.lefts + [node.rest&.expression].compact + node.rights).each do |target|
257
- next unless target.is_a?(Prism::ConstantTargetNode) || target.is_a?(Prism::ConstantPathTargetNode)
334
+ omit_constant_targets(node, nesting)
335
+ targets_of(node).each { |target| walk(target, nesting, singleton: singleton, in_block: in_block) }
336
+ walk(node.value, nesting, singleton: singleton, in_block: in_block)
337
+ end
258
338
 
259
- omit("constant", qualify(nesting, target) || target.slice, target)
339
+ # A `for A in …` index or a `rescue => E` reference assigns the constants it names, like a multiple
340
+ # assignment does.
341
+ def walk_assigning(node, target, nesting, singleton, in_block)
342
+ omit_constant_targets(target, nesting) if target
343
+ walk_children(node, nesting, singleton, in_block)
344
+ end
345
+
346
+ def omit_constant_targets(target, nesting)
347
+ constant_targets(target).each do |constant|
348
+ omit("constant", qualify(nesting, constant) || constant.slice, constant)
260
349
  end
261
- walk(node.value, nesting, singleton: singleton, in_block: in_block)
262
350
  end
263
351
 
264
- def walk_constant(node, nesting)
352
+ # The constants a target assigns: itself when it is one, and those inside a nested `(a, B)` group or a
353
+ # `*Rest` splat. A group's rest slot is a `SplatNode` (`a, *B = …`, or a bare `*`) or an `ImplicitRestNode`
354
+ # (the trailing comma of `a, = …`), which names nothing.
355
+ def constant_targets(target)
356
+ case target
357
+ when Prism::ConstantTargetNode, Prism::ConstantPathTargetNode then [target]
358
+ when Prism::MultiWriteNode, Prism::MultiTargetNode then targets_of(target).flat_map { |t| constant_targets(t) }
359
+ when Prism::SplatNode then target.expression ? constant_targets(target.expression) : []
360
+ else []
361
+ end
362
+ end
363
+
364
+ def targets_of(node)
365
+ node.lefts + [node.rest].compact + node.rights
366
+ end
367
+
368
+ def walk_constant(node, nesting, in_block)
265
369
  target = node.respond_to?(:target) ? node.target : node
266
370
  name = node.respond_to?(:target) ? qualify(nesting, node.target) : qualify_name(nesting, node.name.to_s)
267
- kind = case data_or_struct(node.value)
268
- when :data then "data_class"
269
- when :struct then "struct_class"
270
- else "constant"
271
- end
272
- omit(kind, name || target.slice, node)
371
+ builder = data_or_struct(node.value)
372
+ if modelled_write?(node, name, in_block)
373
+ fields = builder && builder_fields(node.value, builder)
374
+ return read_builder_class(node, name, nesting, builder, fields) if fields
375
+ # A path write (`Foo::BAR = 1`, `::BAR = 1`) lands on the path's namespace, which `check` resolves
376
+ # through the nesting and Rigor does not name the way it does: an omission.
377
+ return read_constant(node, name, nesting) if builder.nil? && node.is_a?(Prism::ConstantWriteNode)
378
+ end
379
+
380
+ kind = { data: "data_class", struct: "struct_class" }.fetch(builder, "constant")
381
+ # Under a builder block or `class << self` the nesting does not name where the constant lands: as written.
382
+ written = node.respond_to?(:target) ? target.slice : node.name.to_s
383
+ omit(kind, constant_scope_unknown? ? written : (name || written), node)
273
384
  walk(node.value, nesting, singleton: false, in_block: true)
274
385
  end
275
386
 
276
- def read_def(node, nesting, singleton, in_block)
387
+ # `X = value` written directly in a namespace body (or at the top level): a `constant` site (capability
388
+ # `constant`). Its `value` slot is filled from the run's tables ({Run#read_constant_value}). A declaration
389
+ # inside the value (a lambda body, a `Class.new` block) is an omission, as anywhere in a block.
390
+ def read_constant(node, name, nesting)
391
+ owner = nesting.last || "Object"
392
+ add_site(node, { "kind" => "constant", "name" => name, "owner" => owner, "scope" => nil,
393
+ "attribute" => nil, "members" => [], "module_function" => false,
394
+ "visibility" => reassigned_visibility(name, owner), "params" => nil, "block" => nil,
395
+ "body_return" => nil, "value" => Slot.unavailable("not_computed") })
396
+ walk(node.value, nesting, singleton: false, in_block: true)
397
+ end
398
+
399
+ # Reassigning a constant keeps its visibility in Ruby: a `private_constant` above still holds.
400
+ def reassigned_visibility(name, owner)
401
+ earlier = @sites.rfind do |site|
402
+ site.data["kind"] == "constant" && site.data["owner"] == owner && site.data["name"] == name
403
+ end
404
+ earlier ? earlier.data["visibility"] : "public"
405
+ end
406
+
407
+ # `Foo = Data.define(:a, :b) do … end` and `Foo = Struct.new(:a, keyword_init: true) do … end`: a class site
408
+ # with `builder` set, an attribute site per field (a reader for `Data`, an accessor for `Struct`), and the block
409
+ # read as the class body, which Ruby `class_eval`s on the new class. `check`'s discovery keys the block's defs
410
+ # and the fields under the same constant name.
411
+ def read_builder_class(node, name, nesting, builder, fields)
412
+ add_site(node, { "kind" => "class", "name" => name, "owner" => nil, "scope" => nil, "attribute" => nil,
413
+ "members" => [], "module_function" => false, "builder" => builder.to_s,
414
+ "visibility" => nil, "params" => nil, "block" => nil, "body_return" => nil })
415
+ attribute = builder == :data ? "reader" : "accessor"
416
+ start_line = node.location.start_line
417
+ fields.each do |field, field_node|
418
+ # rbs-inline reads a `#:` trailing a field on its own line of a multi-line call as that field's type.
419
+ trailing = field_node.location.start_line != start_line
420
+ add_site(field_node, { "kind" => "attribute", "name" => field, "owner" => name, "scope" => "instance",
421
+ "attribute" => attribute, "members" => attribute_members(field, attribute),
422
+ "module_function" => false, "visibility" => "public", "params" => nil,
423
+ "block" => nil, "body_return" => nil }, contracts: trailing ? :trailing : false)
424
+ end
425
+ block = node.value.block
426
+ walk_body(block.body, nesting + [name], singleton: false, kind: :builder) if block&.body
427
+ end
428
+
429
+ # Whether a constant, class or module written here lands somewhere the nesting does not name: a builder
430
+ # block, which Ruby `class_eval`s (defs go to the new class, but constants stay with the lexical scope), or a
431
+ # `class << self` body (constants go to the singleton class). Such a declaration is an omission.
432
+ def constant_scope_unknown?
433
+ @bodies.any? { |body| %i[builder singleton].include?(body.kind) }
434
+ end
435
+
436
+ def constant_visibility_call?(node)
437
+ node.is_a?(Prism::CallNode) && node.receiver.nil? && %i[private_constant public_constant].include?(node.name) &&
438
+ node.arguments
439
+ end
440
+
441
+ # `private_constant :X` written directly in a namespace body makes the constants it names, already assigned
442
+ # above it in this body's namespace, private. Written anywhere else (an `if`, a block, a method) it runs at a
443
+ # time Rigor does not know, so those constants read unknown; a non-literal argument leaves every constant of
444
+ # the namespace unknown. Under a builder block or `class << self` it targets the new class or the singleton
445
+ # class, which hold none of the constants listed here.
446
+ def constant_visibility(node, nesting, direct:)
447
+ return if constant_scope_unknown?
448
+
449
+ direct &&= !@bodies.last&.detached
450
+ visibility = node.name == :private_constant ? "private" : "public"
451
+ node.arguments.arguments.each do |argument|
452
+ names = argument_names(argument)
453
+ named_constants(nesting, names).each { |site| site.data["visibility"] = names && direct ? visibility : nil }
454
+ end
455
+ end
456
+
457
+ # The namespace's constant sites so far that `names` names, or all of them when `names` is nil.
458
+ def named_constants(nesting, names)
459
+ owner = nesting.last || "Object"
460
+ qualified = names&.map { |name| qualify_name(nesting, name) }
461
+ @sites.select do |site|
462
+ site.data["kind"] == "constant" && site.data["owner"] == owner &&
463
+ (qualified.nil? || qualified.include?(site.data["name"]))
464
+ end
465
+ end
466
+
467
+ # A plain constant assignment whose name Rigor can qualify, outside any block or a body whose constants land
468
+ # somewhere the nesting does not name.
469
+ def modelled_write?(node, name, in_block)
470
+ name && !in_block && plain_write?(node) && !constant_scope_unknown?
471
+ end
472
+
473
+ def plain_write?(node)
474
+ node.is_a?(Prism::ConstantWriteNode) || node.is_a?(Prism::ConstantPathWriteNode)
475
+ end
476
+
477
+ # `[[name, node], …]` for a builder call whose fields are all symbols, or nil. `Struct.new` may end with
478
+ # `keyword_init:` alone. Not modelled: a string field (`check`'s discovery reads only symbols, and
479
+ # `Struct.new("Name", …)` defines `Struct::Name`), a `&block` argument, and a duplicate field or a writer-like
480
+ # name (`a=`), which Ruby rejects at load time.
481
+ def builder_fields(call, builder)
482
+ return nil if call.block.is_a?(Prism::BlockArgumentNode)
483
+
484
+ arguments = call.arguments&.arguments || []
485
+ arguments = arguments[0...-1] if builder == :struct && keyword_init_only?(arguments.last)
486
+ return nil unless arguments.all?(Prism::SymbolNode)
487
+
488
+ names = arguments.map(&:unescaped)
489
+ return nil if names.uniq.size != names.size || names.any? { |name| name.end_with?("=") }
490
+
491
+ names.zip(arguments)
492
+ end
493
+
494
+ def keyword_init_only?(argument)
495
+ return false unless argument.is_a?(Prism::KeywordHashNode)
496
+
497
+ argument.elements.all? do |element|
498
+ element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode) &&
499
+ element.key.unescaped == "keyword_init"
500
+ end
501
+ end
502
+
503
+ # A bare `module_function` the walk cannot place in the body's statement list (under an `if`, say) leaves the
504
+ # mode unknown for the body's later defs.
505
+ def unplaced_module_function
506
+ body = @bodies.last
507
+ body.module_function = :unknown if body && !body.detached
508
+ end
509
+
510
+ def read_def(node, nesting, singleton, in_block, module_function: false)
277
511
  scope = def_scope(node, nesting, singleton)
512
+ module_function ||= module_function_state(node, singleton)
278
513
  if in_block || scope.nil?
279
514
  omit("method", node.name.to_s, node)
515
+ elsif module_function.nil?
516
+ omit("module_function", node.name.to_s, node, owner: nesting.last, singleton: singleton)
280
517
  else
281
- owner = nesting.last || "Object"
282
- data = { "kind" => "method", "name" => node.name.to_s, "owner" => owner, "scope" => scope.to_s,
283
- "attribute" => nil, "members" => [node.name.to_s],
284
- "visibility" => visibility(node, nesting, scope) }
285
- params, block, = Parameters.read(node.parameters, yields: Parameters.yields?(node.body))
286
- shape_params, shape_block = Parameters.shape(params, block)
287
- data.merge!("params" => shape_params, "block" => shape_block,
288
- "body_return" => body_return(node, owner, scope))
289
- add_site(node, data)
518
+ add_site(node, method_site(node, nesting, scope, module_function))
290
519
  end
291
520
  # A declaration inside a method body runs only when the method does: not modelled.
292
521
  detached { walk(node.body, nesting, singleton: singleton, in_block: true) } if node.body
293
522
  end
294
523
 
524
+ def method_site(node, nesting, scope, module_function)
525
+ owner = nesting.last || "Object"
526
+ params, block, = Parameters.read(node.parameters, yields: Parameters.yields?(node.body))
527
+ shape_params, shape_block = Parameters.shape(params, block)
528
+ returns = body_return(node, owner, scope)
529
+ data = { "kind" => "method", "name" => node.name.to_s, "owner" => owner, "scope" => scope.to_s,
530
+ "attribute" => nil, "members" => [node.name.to_s], "module_function" => module_function,
531
+ "visibility" => module_function ? "private" : visibility(node, nesting, scope),
532
+ "params" => shape_params, "block" => shape_block, "body_return" => returns }
533
+ return data unless module_function
534
+
535
+ # Internal to the member pass: the singleton copy is public until `private_class_method` says otherwise,
536
+ # and its body runs with the module as `self`. The instance-scope typing read `self` as an instance of the
537
+ # owner, so a return that may depend on `self` is not the copy's.
538
+ copy = self_dependent?(node.body, returns, owner) ? Slot.unavailable("not_computed") : returns
539
+ data.merge("singleton_visibility" => "public", "singleton_body_return" => copy)
540
+ end
541
+
542
+ # Whether the instance-scope return may differ from the singleton copy's: the body reads `self`, instance or
543
+ # class state, or `super` (nested defs excluded, blocks included, which over-declines safely), or the return
544
+ # names the owner's type, which is where a `self` that flowed through `itself`, `tap` or another module
545
+ # function shows up.
546
+ def self_dependent?(body, returns, owner)
547
+ instance_state?(body) || names_owner?(returns, owner)
548
+ end
549
+
550
+ def instance_state?(node)
551
+ return false if node.nil?
552
+ if node.is_a?(Prism::SelfNode) || node.is_a?(Prism::SuperNode) || node.is_a?(Prism::ForwardingSuperNode)
553
+ return true
554
+ end
555
+ return true if node.type.to_s.start_with?("instance_variable_", "class_variable_")
556
+ return false if node.is_a?(Prism::DefNode)
557
+
558
+ node.compact_child_nodes.any? { |child| instance_state?(child) }
559
+ end
560
+
561
+ # Whether a spelled type mentions the owner, by its qualified name or its last segment. A heuristic, not a proof:
562
+ # a self-dependent body whose instance-scope type folds to something that names no owner slips through.
563
+ def names_owner?(slot, owner)
564
+ type = slot["type"]
565
+ return false unless type
566
+
567
+ [owner, owner.split("::").last].uniq.any? do |name|
568
+ type.match?(/(?<![\w:])(?:::)?#{Regexp.escape(name)}(?!\w)/)
569
+ end
570
+ end
571
+
572
+ # Whether a receiverless instance `def` is a module function: true when the body's mode is on and the def is
573
+ # one of the body's direct statements, nil when the mode is on or unknown but Rigor cannot say (a def nested
574
+ # in the body, or after the mode became unknown), false otherwise.
575
+ def module_function_state(node, singleton)
576
+ body = @bodies.last
577
+ return false if body.nil? || body.detached || singleton || node.receiver
578
+ return false if body.module_function == :off
579
+
580
+ body.module_function == :on && node.equal?(@direct_statement) ? true : nil
581
+ end
582
+
583
+ # `alias new old` or `alias_method :new, :old` (literal names) written as a statement of a class, module or
584
+ # builder body: a `method` site named `new` with `alias_of` set (capability `alias`). Ruby copies the method
585
+ # `old` names at that moment, visibility included, so the site takes the visibility of the last `old` site of
586
+ # the owner above it, and its parameters and body return when that is the only `old` site above it and a
587
+ # `def`. Whether that is the `def` `check` reads (the final one) is the member pass's question. Anywhere else
588
+ # (a block, a method, `class << self`, the top level, wrapped in a visibility call or nested under an `if`),
589
+ # with a computed name, or while `module_function` is on, it is an omission.
590
+ def read_alias(node, nesting, singleton, in_block)
591
+ names = alias_names(node)
592
+ if names && modelled_alias?(node, nesting, singleton, in_block)
593
+ return add_site(node, alias_site(names.map(&:to_s), nesting.last))
594
+ end
595
+
596
+ omit("alias", names&.first&.to_s || alias_written_name(node), node, owner: nesting.last, singleton: singleton)
597
+ walk_children(node, nesting, singleton, true) if node.is_a?(Prism::CallNode)
598
+ end
599
+
600
+ def alias_names(node)
601
+ if node.is_a?(Prism::AliasMethodNode)
602
+ Source::AliasNames.keyword_names(node)
603
+ else
604
+ node.block.nil? ? Source::AliasNames.alias_method_call_names(node) : nil
605
+ end
606
+ end
607
+
608
+ def modelled_alias?(node, nesting, singleton, in_block)
609
+ body = @bodies.last
610
+ !in_block && !singleton && !nesting.empty? && body && !body.detached &&
611
+ body.module_function == :off && node.equal?(@direct_statement)
612
+ end
613
+
614
+ def alias_written_name(node)
615
+ node.is_a?(Prism::AliasMethodNode) ? node.new_name.slice.delete_prefix(":") : first_argument_name(node)
616
+ end
617
+
618
+ def alias_site(names, owner)
619
+ new_name, old_name = names
620
+ last, copy = alias_original(owner, old_name)
621
+ visibility = FORCED_PRIVATE.include?(new_name) ? "private" : last&.data&.fetch("visibility")
622
+ # `initialize` reads `void` by its name, which neither side of an alias across it keeps.
623
+ returns = copy && ![new_name, old_name].include?("initialize") ? copy.data["body_return"] : nil
624
+ { "kind" => "method", "name" => new_name, "owner" => owner, "scope" => "instance", "attribute" => nil,
625
+ "members" => [new_name], "module_function" => false, "visibility" => visibility,
626
+ "params" => copy&.data&.fetch("params"), "block" => copy&.data&.fetch("block"),
627
+ "body_return" => returns || Slot.unavailable("not_computed"),
628
+ "alias_of" => old_name, "aliased_node" => copy && aliased_node(copy) }
629
+ end
630
+
631
+ # `[last, copy]`: the last site of the owner above the alias defining `old` (nil when an omission names
632
+ # `old` too), and that site when it is the file's only one and a `def`.
633
+ def alias_original(owner, old_name)
634
+ olds = @sites.select do |site|
635
+ site.data["owner"] == owner && site.data["scope"] == "instance" && site.data["members"].include?(old_name)
636
+ end
637
+ unplaced = @omissions.any? do |omission|
638
+ [omission["owner"], omission["scope"]] == [owner, "instance"] && [old_name, nil].include?(omission["name"])
639
+ end
640
+ return [nil, nil] if unplaced
641
+
642
+ last = olds.last
643
+ [last, olds.size == 1 && last.data["kind"] == "method" ? last : nil]
644
+ end
645
+
646
+ # The `def` a method site reads: its own, or for an alias the one it copied.
647
+ def aliased_node(site)
648
+ site.data["alias_of"] ? site.data["aliased_node"] : site.node
649
+ end
650
+
295
651
  def read_attribute(node, nesting, singleton, in_block)
296
652
  arguments = node.arguments&.arguments || []
297
653
  names = arguments.map { |arg| arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode) ? arg.unescaped : nil }
@@ -303,28 +659,40 @@ module Rigor
303
659
  scope = singleton ? :singleton : :instance
304
660
  attribute = ATTRIBUTE_CALLS.fetch(node.name)
305
661
  names.each_with_index do |name, ordinal|
662
+ members = attribute_members(name, attribute)
306
663
  add_site(node, { "kind" => "attribute", "name" => name, "owner" => owner, "scope" => scope.to_s,
307
- "attribute" => attribute, "members" => attribute_members(name, attribute),
308
- "visibility" => visibility(node, nesting, scope),
664
+ "attribute" => attribute, "members" => members,
665
+ "module_function" => false,
666
+ "visibility" => attribute_visibility(node, nesting, scope, members),
309
667
  "params" => nil, "block" => nil, "body_return" => nil }, ordinal: ordinal)
310
668
  end
311
669
  end
312
670
 
313
- def add_site(node, data, ordinal: 0)
671
+ # `attr_reader :initialize` is forced private while its writer is not: not modelled.
672
+ def attribute_visibility(node, nesting, scope, members)
673
+ return nil if scope == :instance && members.intersect?(FORCED_PRIVATE)
674
+
675
+ visibility(node, nesting, scope)
676
+ end
677
+
678
+ # A builder's field reads no leading contract comments, which belong to the constant: `contracts: :trailing`
679
+ # reads only a `#:` trailing it (rbs-inline's per-field form on its own line), and `false` reads none.
680
+ def add_site(node, data, ordinal: 0, contracts: true)
314
681
  location = node.location
315
- anchor = @anchor.for_span(location.start_offset, location.end_offset, location.start_line,
316
- location.end_line)
682
+ anchor = @anchor.for_span(location.start_offset, location.end_offset, location.start_line, location.end_line,
683
+ leading: contracts == true, trailing: contracts != false)
317
684
  key = "#{@path}:#{location.start_offset}-#{location.end_offset}:#{ordinal}"
318
685
  tail = { "ordinal" => ordinal, "span" => Lens.span(location),
319
686
  "contract_span" => Lens.contract_span(anchor.contract_comments), "anchor" => anchor.token,
320
687
  "contract" => nil }
321
- @sites << Site.new(data: { "key" => key }.merge(data).merge(tail), node: node,
322
- contract_comments: anchor.contract_comments)
688
+ head = { "key" => key, "builder" => nil, "value" => nil, "alias_of" => nil }
689
+ @sites << Site.new(data: head.merge(data).merge(tail), node: node, contract_comments: anchor.contract_comments)
323
690
  end
324
691
 
325
- # `owner` / `singleton` name the member the declaration would define, when Rigor can tell.
326
- def omit(kind, name, node, owner: nil, singleton: false)
327
- member = owner && name
692
+ # `owner` / `singleton` name the member the declaration would define, when Rigor can tell. `any_member` keeps
693
+ # them with a null name: the declaration may define any member of that owner and scope.
694
+ def omit(kind, name, node, owner: nil, singleton: false, any_member: false)
695
+ member = owner && (name || any_member)
328
696
  @omissions << { "kind" => kind, "owner" => member ? owner : nil,
329
697
  "scope" => member ? scope_name(singleton) : nil, "name" => name,
330
698
  "span" => Lens.span(node.location), "reason" => "unsupported_kind" }
@@ -349,6 +717,7 @@ module Rigor
349
717
  def visibility(node, nesting, scope)
350
718
  return "private" if nesting.empty?
351
719
  return @node_visibility[node] if @node_visibility.key?(node)
720
+ return "private" if scope == :instance && node.is_a?(Prism::DefNode) && FORCED_PRIVATE.include?(node.name.to_s)
352
721
  return "public" if scope == :singleton && node.is_a?(Prism::DefNode) && node.receiver
353
722
 
354
723
  body = @bodies.last