rigortype 0.4.2 → 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 (128) 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 +28 -5
  5. data/docs/manual/02-cli-reference.md +23 -4
  6. data/docs/manual/03-configuration.md +21 -8
  7. data/docs/manual/04-diagnostics.md +44 -2
  8. data/docs/manual/05-inspecting-types.md +50 -0
  9. data/docs/manual/07-plugins.md +1 -1
  10. data/docs/manual/10-mcp-server.md +32 -3
  11. data/docs/manual/12-caching.md +18 -1
  12. data/docs/manual/14-rails-quickstart.md +2 -2
  13. data/docs/manual/18-removing-dead-code.md +26 -9
  14. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  15. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  16. data/exe/rigor +12 -17
  17. data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
  18. data/lib/rigor/analysis/check_rules.rb +68 -57
  19. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  20. data/lib/rigor/analysis/incremental.rb +12 -0
  21. data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
  22. data/lib/rigor/analysis/incremental_session.rb +207 -40
  23. data/lib/rigor/analysis/path_expansion.rb +62 -6
  24. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  25. data/lib/rigor/analysis/reachability/graph.rb +139 -15
  26. data/lib/rigor/analysis/reachability/scan.rb +374 -173
  27. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  28. data/lib/rigor/analysis/rule_catalog.rb +6 -1
  29. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  30. data/lib/rigor/analysis/run_cache_probe.rb +6 -4
  31. data/lib/rigor/analysis/run_stats.rb +28 -6
  32. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
  33. data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
  34. data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
  35. data/lib/rigor/analysis/runner.rb +62 -18
  36. data/lib/rigor/analysis/severity_stamp.rb +34 -1
  37. data/lib/rigor/analysis/worker_session.rb +58 -2
  38. data/lib/rigor/cache/descriptor.rb +40 -6
  39. data/lib/rigor/cache/engine_source.rb +21 -7
  40. data/lib/rigor/cache/incremental_snapshot.rb +56 -12
  41. data/lib/rigor/cli/check_command.rb +138 -39
  42. data/lib/rigor/cli/docs_command.rb +5 -4
  43. data/lib/rigor/cli/lens_command.rb +144 -0
  44. data/lib/rigor/cli/lsp_command.rb +5 -2
  45. data/lib/rigor/cli/mcp_command.rb +1 -1
  46. data/lib/rigor/cli/skill_command.rb +3 -3
  47. data/lib/rigor/cli/skill_describe.rb +1 -1
  48. data/lib/rigor/cli/type_of_command.rb +2 -1
  49. data/lib/rigor/cli/unused_command.rb +157 -24
  50. data/lib/rigor/cli.rb +30 -4
  51. data/lib/rigor/config_audit.rb +111 -15
  52. data/lib/rigor/configuration/dependencies.rb +19 -11
  53. data/lib/rigor/configuration.rb +15 -0
  54. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  55. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  56. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  57. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  58. data/lib/rigor/environment/rbs_loader.rb +79 -10
  59. data/lib/rigor/environment/required_features.rb +4 -2
  60. data/lib/rigor/environment.rb +12 -5
  61. data/lib/rigor/inference/block_call_timing.rb +5 -0
  62. data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
  63. data/lib/rigor/inference/captured_locals.rb +69 -6
  64. data/lib/rigor/inference/definer_resolution.rb +16 -7
  65. data/lib/rigor/inference/expression_typer.rb +117 -29
  66. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  67. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
  68. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  69. data/lib/rigor/inference/match_rebinding.rb +1 -1
  70. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  71. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  72. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  73. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  74. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  75. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
  76. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  77. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  78. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  79. data/lib/rigor/inference/method_dispatcher.rb +52 -3
  80. data/lib/rigor/inference/module_function_state.rb +80 -13
  81. data/lib/rigor/inference/narrowing.rb +184 -68
  82. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  83. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  84. data/lib/rigor/inference/project_patched_scanner.rb +38 -24
  85. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  86. data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
  87. data/lib/rigor/inference/refined_dispatch.rb +62 -14
  88. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  89. data/lib/rigor/inference/repeated_or_writes.rb +416 -1
  90. data/lib/rigor/inference/scope_indexer.rb +565 -61
  91. data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
  92. data/lib/rigor/inference/statement_evaluator.rb +307 -60
  93. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  94. data/lib/rigor/language_server/project_context.rb +56 -5
  95. data/lib/rigor/lens/anchor.rb +115 -0
  96. data/lib/rigor/lens/contract_reader.rb +133 -0
  97. data/lib/rigor/lens/document.rb +113 -0
  98. data/lib/rigor/lens/file_reader.rb +802 -0
  99. data/lib/rigor/lens/members.rb +203 -0
  100. data/lib/rigor/lens/parameters.rb +91 -0
  101. data/lib/rigor/lens/plugins.rb +40 -0
  102. data/lib/rigor/lens/run.rb +322 -0
  103. data/lib/rigor/lens/slot.rb +79 -0
  104. data/lib/rigor/lens.rb +41 -0
  105. data/lib/rigor/mcp/server.rb +61 -20
  106. data/lib/rigor/plugin/box_probe.rb +154 -7
  107. data/lib/rigor/project_environment.rb +23 -1
  108. data/lib/rigor/protection/mutation_cache.rb +2 -1
  109. data/lib/rigor/scope.rb +33 -17
  110. data/lib/rigor/sig_gen/generator.rb +45 -5
  111. data/lib/rigor/sig_gen/layout_index.rb +36 -0
  112. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  113. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  114. data/lib/rigor/sig_gen/writer.rb +47 -34
  115. data/lib/rigor/version.rb +1 -1
  116. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  117. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  118. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  119. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  120. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
  121. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  122. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  123. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  124. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  125. data/sig/rigor/scope.rbs +3 -0
  126. data/sig/rigor.rbs +6 -3
  127. data/skills/rigor-project-init/references/02-configure.md +2 -2
  128. metadata +21 -1
@@ -0,0 +1,802 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "anchor"
6
+ require_relative "slot"
7
+ require_relative "parameters"
8
+ require_relative "../inference/def_return_typer"
9
+ require_relative "../inference/origin_lookup"
10
+ require_relative "../sig_gen/type_elaborator"
11
+ require_relative "../source/alias_names"
12
+
13
+ module Rigor
14
+ module Lens
15
+ # Reads one analysed file into sites and omissions (docs/internal-spec/lens.md § Files, § Sites). Runs inside
16
+ # the check run, on the seeded scope index the check rules read, so a body return is the type `check` gives
17
+ # that body. Every declaration the walk meets is either a site or an omission; none is dropped.
18
+ class FileReader # rubocop:disable Metrics/ClassLength
19
+ # A site plus what the member pass needs from it: the declaration node and the contract comments above it.
20
+ Site = Data.define(:data, :node, :contract_comments)
21
+ Result = Data.define(:sites, :omissions, :digest)
22
+ # The running visibility of one class body: Ruby's default for its later `def`s, and whether a toggle the
23
+ # walk cannot place (under an `if`, a `begin`, a block) leaves that default unknown.
24
+ # A `detached` body stands for code that is not the class body (a block, a method body, a superclass
25
+ # expression, an omitted class): its toggles and named calls stay local and change no site.
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)
30
+
31
+ ATTRIBUTE_CALLS = { attr_reader: "reader", attr_writer: "writer", attr_accessor: "accessor" }.freeze
32
+ OMITTED_CALLS = {
33
+ alias_method: "alias", define_method: "define_method", module_function: "module_function",
34
+ refine: "refine"
35
+ }.freeze
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
39
+ CLASS_VISIBILITY_CALLS = { private_class_method: "private", public_class_method: "public" }.freeze
40
+ BLOCK_NODES = [Prism::BlockNode, Prism::LambdaNode].freeze
41
+ CONSTANT_WRITES = [
42
+ Prism::ConstantWriteNode, Prism::ConstantPathWriteNode, Prism::ConstantOrWriteNode,
43
+ Prism::ConstantPathOrWriteNode, Prism::ConstantAndWriteNode, Prism::ConstantPathAndWriteNode,
44
+ Prism::ConstantOperatorWriteNode, Prism::ConstantPathOperatorWriteNode
45
+ ].freeze
46
+
47
+ # @param path — the file, relative to the project root
48
+ # @param spell — callable `(type, owner) -> String` spelling a type as sig-gen writes it
49
+ def initialize(path:, parse_result:, index:, spell:)
50
+ @path = path
51
+ @root = parse_result.value
52
+ @source = parse_result.source.source
53
+ @index = index
54
+ @spell = spell
55
+ @anchor = Anchor.new(@source, parse_result.comments)
56
+ @bodies = []
57
+ @node_visibility = {}.compare_by_identity
58
+ @sites = []
59
+ @omissions = []
60
+ end
61
+
62
+ def read
63
+ walk(@root, [], singleton: false, in_block: false)
64
+ Result.new(sites: @sites, omissions: @omissions, digest: Lens.digest(@source.b))
65
+ end
66
+
67
+ private
68
+
69
+ def walk(node, nesting, singleton:, in_block:)
70
+ case node
71
+ when Prism::ClassNode, Prism::ModuleNode then walk_namespace(node, nesting, in_block)
72
+ when Prism::SingletonClassNode then walk_singleton_class(node, nesting, singleton, in_block)
73
+ when Prism::DefNode then read_def(node, nesting, singleton, in_block)
74
+ when Prism::CallNode then walk_call(node, nesting, singleton, in_block)
75
+ when Prism::AliasMethodNode then read_alias(node, nesting, singleton, in_block)
76
+ when Prism::MultiWriteNode then walk_multi_write(node, nesting, singleton, in_block)
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)
80
+ else walk_children(node, nesting, singleton, in_block)
81
+ end
82
+ end
83
+
84
+ def walk_children(node, nesting, singleton, in_block)
85
+ node.compact_child_nodes.each do |child|
86
+ if BLOCK_NODES.include?(child.class)
87
+ detached { walk(child, nesting, singleton: singleton, in_block: true) }
88
+ else
89
+ walk(child, nesting, singleton: singleton, in_block: in_block)
90
+ end
91
+ end
92
+ end
93
+
94
+ def detached
95
+ @bodies.push(Body.new("public", false, true, :detached, :off))
96
+ yield
97
+ ensure
98
+ @bodies.pop
99
+ end
100
+
101
+ def walk_namespace(node, nesting, in_block)
102
+ kind = node.is_a?(Prism::ClassNode) ? "class" : "module"
103
+ name = qualify(nesting, node.constant_path)
104
+ if node.is_a?(Prism::ClassNode) && node.superclass
105
+ detached { walk(node.superclass, nesting, singleton: false, in_block: true) }
106
+ end
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)
109
+ detached { walk(node.body, nesting, singleton: false, in_block: true) } if node.body
110
+ return
111
+ end
112
+
113
+ add_site(node, { "kind" => kind, "name" => name, "owner" => nil, "scope" => nil, "attribute" => nil,
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)
117
+ end
118
+
119
+ # `class << self` directly in a class or module body opens its singleton class. Any other `class << x`, one
120
+ # nested in another, or one at the top level (which opens `main`'s) is not modelled.
121
+ def walk_singleton_class(node, nesting, singleton, in_block)
122
+ if in_block || singleton || nesting.empty? || !node.expression.is_a?(Prism::SelfNode)
123
+ omit("singleton_class", nil, node)
124
+ return detached { walk(node.body, nesting, singleton: singleton, in_block: true) } if node.body
125
+ end
126
+
127
+ walk_body(node.body, nesting, singleton: true, kind: :singleton)
128
+ end
129
+
130
+ # A class body's statements, tracking the running visibility and `module_function`.
131
+ def walk_body(body, nesting, singleton:, kind:)
132
+ return if body.nil?
133
+
134
+ @bodies.push(Body.new("public", false, false, kind, :off))
135
+ statements = body.is_a?(Prism::StatementsNode) ? body.body : [body]
136
+ statements.each { |child| walk_body_statement(child, nesting, singleton) }
137
+ ensure
138
+ @bodies.pop if body
139
+ end
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
+
200
+ # A bare `private` / `protected` / `public` written directly in the body sets the default for later defs.
201
+ def toggle_visibility?(child)
202
+ return false unless VISIBILITY_CALLS.any? { |name| bare_call?(child, name) }
203
+
204
+ @bodies.last.current = child.name.to_s
205
+ @bodies.last.uncertain = false
206
+ true
207
+ end
208
+
209
+ def walk_call(node, nesting, singleton, in_block)
210
+ if node.receiver.nil? && ATTRIBUTE_CALLS.key?(node.name)
211
+ read_attribute(node, nesting, singleton, in_block)
212
+ elsif node.receiver.nil? && OMITTED_CALLS.key?(node.name)
213
+ omitted_call(node, nesting, singleton, in_block)
214
+ elsif constant_visibility_call?(node)
215
+ constant_visibility(node, nesting, direct: false)
216
+ else
217
+ unplaced_visibility_call(node, nesting, singleton)
218
+ walk_children(node, nesting, singleton, in_block)
219
+ end
220
+ end
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
+
239
+ def visibility_call?(node)
240
+ node.is_a?(Prism::CallNode) && node.receiver.nil? &&
241
+ (VISIBILITY_CALLS.include?(node.name) || CLASS_VISIBILITY_CALLS.key?(node.name))
242
+ end
243
+
244
+ # `private :x`, `private %i[x]`, `private def x`, `private attr_reader :x`, `private_class_method :x`, and a
245
+ # bare toggle the walk cannot place in the body's statement list. A named call changes members only when it
246
+ # is written directly in the body (`direct`); one under an `if`, a block or a method body runs at a time
247
+ # Rigor does not know, so the members it names read unknown.
248
+ def visibility_call(node, nesting, singleton, direct:)
249
+ @visited_visibility_calls ||= {}.compare_by_identity
250
+ @visited_visibility_calls[node] = true
251
+ direct &&= !@bodies.last&.detached
252
+ if VISIBILITY_CALLS.include?(node.name)
253
+ return mark_uncertain if node.arguments.nil?
254
+
255
+ apply_visibility(node, node.name.to_s, nesting.last, singleton, direct)
256
+ else
257
+ apply_visibility(node, CLASS_VISIBILITY_CALLS.fetch(node.name), nesting.last, true, direct)
258
+ end
259
+ end
260
+
261
+ def mark_uncertain
262
+ @bodies.last&.uncertain = true
263
+ end
264
+
265
+ def apply_visibility(node, visibility, owner, singleton, direct)
266
+ scope = singleton ? "singleton" : "instance"
267
+ node.arguments.arguments.each do |argument|
268
+ names = argument_names(argument)
269
+ if wrapped_declaration?(argument, singleton)
270
+ @node_visibility[argument] = visibility
271
+ elsif argument.is_a?(Prism::DefNode)
272
+ # `private def self.x`: Ruby looks `x` up among instance methods and rejects it, so the result is unknown.
273
+ @node_visibility[argument] = nil
274
+ elsif names.nil?
275
+ # `private *NAMES`: which members change is unknown.
276
+ unknown_visibility(owner, scope)
277
+ else
278
+ names.each { |name| name_visibility(owner, scope, name, direct ? visibility : nil) }
279
+ end
280
+ end
281
+ end
282
+
283
+ # A `def` or `attr_*` the visibility call wraps. `private def self.x` is not one: `private` looks the
284
+ # name up among instance methods.
285
+ def wrapped_declaration?(argument, singleton)
286
+ case argument
287
+ when Prism::DefNode then argument.receiver.nil? || singleton
288
+ when Prism::CallNode then argument.receiver.nil? && ATTRIBUTE_CALLS.key?(argument.name)
289
+ else false
290
+ end
291
+ end
292
+
293
+ # The names a visibility call's argument lists, or nil when it is not a literal list of them.
294
+ def argument_names(argument)
295
+ case argument
296
+ when Prism::SymbolNode, Prism::StringNode then [argument.unescaped]
297
+ when Prism::ArrayNode
298
+ names = argument.elements.map { |element| argument_names(element)&.first }
299
+ names.include?(nil) ? nil : names
300
+ end
301
+ end
302
+
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)
306
+ @sites.each do |site|
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"]
312
+ end
313
+ mark_uncertain if default_too
314
+ end
315
+
316
+ # `private :x` changes the visibility of the members already defined under that name in this file.
317
+ def name_visibility(owner, scope, name, visibility)
318
+ @sites.each do |site|
319
+ data = site.data
320
+ next unless data["owner"] == owner && data["members"].include?(name)
321
+
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
328
+ end
329
+ end
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.
333
+ def walk_multi_write(node, nesting, singleton, in_block)
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
338
+
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)
349
+ end
350
+ end
351
+
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)
369
+ target = node.respond_to?(:target) ? node.target : node
370
+ name = node.respond_to?(:target) ? qualify(nesting, node.target) : qualify_name(nesting, node.name.to_s)
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)
384
+ walk(node.value, nesting, singleton: false, in_block: true)
385
+ end
386
+
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)
511
+ scope = def_scope(node, nesting, singleton)
512
+ module_function ||= module_function_state(node, singleton)
513
+ if in_block || scope.nil?
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)
517
+ else
518
+ add_site(node, method_site(node, nesting, scope, module_function))
519
+ end
520
+ # A declaration inside a method body runs only when the method does: not modelled.
521
+ detached { walk(node.body, nesting, singleton: singleton, in_block: true) } if node.body
522
+ end
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
+
651
+ def read_attribute(node, nesting, singleton, in_block)
652
+ arguments = node.arguments&.arguments || []
653
+ names = arguments.map { |arg| arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::StringNode) ? arg.unescaped : nil }
654
+ if in_block || nesting.empty? || names.empty? || names.include?(nil)
655
+ return omit("attribute", names.compact.first, node)
656
+ end
657
+
658
+ owner = nesting.last
659
+ scope = singleton ? :singleton : :instance
660
+ attribute = ATTRIBUTE_CALLS.fetch(node.name)
661
+ names.each_with_index do |name, ordinal|
662
+ members = attribute_members(name, attribute)
663
+ add_site(node, { "kind" => "attribute", "name" => name, "owner" => owner, "scope" => scope.to_s,
664
+ "attribute" => attribute, "members" => members,
665
+ "module_function" => false,
666
+ "visibility" => attribute_visibility(node, nesting, scope, members),
667
+ "params" => nil, "block" => nil, "body_return" => nil }, ordinal: ordinal)
668
+ end
669
+ end
670
+
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)
681
+ location = node.location
682
+ anchor = @anchor.for_span(location.start_offset, location.end_offset, location.start_line, location.end_line,
683
+ leading: contracts == true, trailing: contracts != false)
684
+ key = "#{@path}:#{location.start_offset}-#{location.end_offset}:#{ordinal}"
685
+ tail = { "ordinal" => ordinal, "span" => Lens.span(location),
686
+ "contract_span" => Lens.contract_span(anchor.contract_comments), "anchor" => anchor.token,
687
+ "contract" => nil }
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)
690
+ end
691
+
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)
696
+ @omissions << { "kind" => kind, "owner" => member ? owner : nil,
697
+ "scope" => member ? scope_name(singleton) : nil, "name" => name,
698
+ "span" => Lens.span(node.location), "reason" => "unsupported_kind" }
699
+ nil
700
+ end
701
+
702
+ def scope_name(singleton)
703
+ singleton ? "singleton" : "instance"
704
+ end
705
+
706
+ def attribute_members(name, attribute)
707
+ case attribute
708
+ when "reader" then [name]
709
+ when "writer" then ["#{name}="]
710
+ else [name, "#{name}="]
711
+ end
712
+ end
713
+
714
+ # The member's visibility as Ruby gives it: a `private def` / `private attr_reader` wrapper, else the running
715
+ # default of the body (unknown after a toggle the walk could not place). A class body's toggles do not reach
716
+ # a `def self.x`. A later `private :x` revises it ({#name_visibility}). A top-level `def` is private.
717
+ def visibility(node, nesting, scope)
718
+ return "private" if nesting.empty?
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)
721
+ return "public" if scope == :singleton && node.is_a?(Prism::DefNode) && node.receiver
722
+
723
+ body = @bodies.last
724
+ return nil if body.nil? || body.uncertain
725
+
726
+ body.current
727
+ end
728
+
729
+ # `def x` in a class body is an instance method, in `class << self` a singleton one; `def self.x` is a
730
+ # singleton method of a class body. A `def self.x` inside `class << self`, a top-level `def self.x`, and any
731
+ # other receiver define methods revision 1 does not model.
732
+ def def_scope(node, nesting, singleton)
733
+ return (singleton ? :singleton : :instance) if node.receiver.nil?
734
+ return nil unless node.receiver.is_a?(Prism::SelfNode)
735
+ return nil if singleton || nesting.empty?
736
+
737
+ :singleton
738
+ end
739
+
740
+ # The body's return under the seeded index, with its parameters as declared (§ Sites).
741
+ def body_return(node, owner, scope)
742
+ return Slot.typed("void", "inferred") if scope == :instance && node.name == :initialize
743
+ return Slot.typed("nil", "inferred") if node.body.nil?
744
+
745
+ type = Inference::DefReturnTyper.call(node, @index)
746
+ return Slot.unavailable("not_computed") if type.nil?
747
+ return Slot.dynamic("inferred", return_cause(node)) if type.is_a?(Type::Dynamic)
748
+
749
+ spelled = @spell.call(type, owner)
750
+ spelled == "untyped" ? Slot.dynamic("inferred", return_cause(node)) : Slot.typed(spelled, "inferred")
751
+ rescue StandardError
752
+ Slot.unavailable("not_computed")
753
+ end
754
+
755
+ def return_cause(node)
756
+ last = Inference::DefReturnTyper.body_last_expression(node.body)
757
+ scope = last && (@index[last] || @index[node.body])
758
+ scope && Inference::OriginLookup.origin_for(scope, last)
759
+ end
760
+
761
+ # The qualified name a constant path declares under `nesting`, or nil when it is not a plain constant path
762
+ # (`self::Foo`, `expr::Foo`).
763
+ def qualify(nesting, path)
764
+ case path
765
+ when Prism::ConstantReadNode, Prism::ConstantTargetNode then qualify_name(nesting, path.name.to_s)
766
+ when Prism::ConstantPathNode, Prism::ConstantPathTargetNode
767
+ return path.name.to_s if path.parent.nil?
768
+
769
+ parent = qualify(nesting, path.parent)
770
+ parent && "#{parent}::#{path.name}"
771
+ end
772
+ end
773
+
774
+ def qualify_name(nesting, name)
775
+ nesting.empty? ? name : "#{nesting.last}::#{name}"
776
+ end
777
+
778
+ def bare_call?(node, name)
779
+ node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == name && node.arguments.nil? &&
780
+ node.block.nil?
781
+ end
782
+
783
+ def first_argument_name(node)
784
+ argument = node.arguments&.arguments&.first
785
+ case argument
786
+ when Prism::SymbolNode, Prism::StringNode then argument.unescaped
787
+ when Prism::DefNode then argument.name.to_s
788
+ end
789
+ end
790
+
791
+ def data_or_struct(value)
792
+ return nil unless value.is_a?(Prism::CallNode) && value.receiver
793
+
794
+ receiver = value.receiver.slice.delete_prefix("::")
795
+ return :data if receiver == "Data" && value.name == :define
796
+ return :struct if receiver == "Struct" && value.name == :new
797
+
798
+ nil
799
+ end
800
+ end
801
+ end
802
+ end