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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +26 -9
- 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 +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- 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 +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- 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-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -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
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- 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
|