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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +16 -0
- data/docs/manual/02-cli-reference.md +4 -3
- data/docs/manual/03-configuration.md +16 -5
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/18-removing-dead-code.md +11 -1
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +57 -0
- data/lib/rigor/analysis/check_rules.rb +46 -36
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_session.rb +121 -21
- data/lib/rigor/analysis/reachability/graph.rb +23 -3
- data/lib/rigor/analysis/reachability/scan.rb +51 -9
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +110 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +35 -9
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +57 -1
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/incremental_snapshot.rb +20 -4
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/environment/rbs_loader.rb +46 -10
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/captured_locals.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +59 -4
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/scope_indexer.rb +439 -70
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +84 -29
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +15 -0
- data/lib/rigor/lens/document.rb +2 -2
- data/lib/rigor/lens/file_reader.rb +442 -73
- data/lib/rigor/lens/members.rb +65 -15
- data/lib/rigor/lens/run.rb +44 -2
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/scope.rb +11 -6
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- 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
|
-
|
|
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
|
|
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" => [], "
|
|
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`.
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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["
|
|
320
|
+
next unless data["owner"] == owner && data["members"].include?(name)
|
|
250
321
|
|
|
251
|
-
|
|
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
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" =>
|
|
308
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|