carton-tools 0.3.0
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 +7 -0
- data/LICENSE.txt +19 -0
- data/README.md +22 -0
- data/exe/carton +6 -0
- data/lib/carton/tools/bindings.rb +110 -0
- data/lib/carton/tools/checks.rb +360 -0
- data/lib/carton/tools/cli.rb +134 -0
- data/lib/carton/tools/project.rb +186 -0
- data/lib/carton/tools/signatures.rb +220 -0
- data/lib/carton/tools/source_file.rb +649 -0
- data/lib/carton/tools/version.rb +9 -0
- data/lib/carton/tools.rb +4 -0
- metadata +87 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
5
|
+
module Carton
|
|
6
|
+
module Tools
|
|
7
|
+
# What one Ruby file imports, requires, exports, and defines at top level,
|
|
8
|
+
# read with Prism without running it. `carton rbs` and `carton check` build
|
|
9
|
+
# on these facts.
|
|
10
|
+
class SourceFile
|
|
11
|
+
# An `import`, `import_relative`, `require`, or `require_relative` call.
|
|
12
|
+
# `path` is nil when the argument is not a string literal. `lazy` is true
|
|
13
|
+
# inside a method, block, or lambda, which may run after loading.
|
|
14
|
+
# `constant` and `local` name the constant or local variable the call's
|
|
15
|
+
# result is assigned to, and `pattern` is the Reference for a pattern
|
|
16
|
+
# matched directly against it, such as `import('math') => { add: }`.
|
|
17
|
+
Call =
|
|
18
|
+
Data.define(
|
|
19
|
+
:kind,
|
|
20
|
+
:path,
|
|
21
|
+
:file,
|
|
22
|
+
:line,
|
|
23
|
+
:lazy,
|
|
24
|
+
:constant,
|
|
25
|
+
:local,
|
|
26
|
+
:pattern,
|
|
27
|
+
) { def relative? = kind.end_with?('_relative') }
|
|
28
|
+
|
|
29
|
+
# A use of a name that may be bound to an import: `name` as written (a
|
|
30
|
+
# constant path, or a local variable when `local`), the innermost
|
|
31
|
+
# namespace it appears in, and how it is used. `kind` is :call for a
|
|
32
|
+
# method call (`X.add`), :const for a constant path (`X::PI`), :fetch for
|
|
33
|
+
# a lookup that raises when the name is missing (`fetch`, a `=>`
|
|
34
|
+
# pattern), :lookup for one that does not (`[]`, `key?`, an `in`
|
|
35
|
+
# pattern), and :value when the namespace itself is passed on, so its
|
|
36
|
+
# uses cannot be followed. `member` is the name used, or nil for :value.
|
|
37
|
+
# `rescued` lists the exception classes an enclosing `rescue` catches.
|
|
38
|
+
Reference =
|
|
39
|
+
Data.define(:name, :local, :scope, :kind, :member, :line, :rescued)
|
|
40
|
+
|
|
41
|
+
# A hash pattern matched directly against an import: its Reference
|
|
42
|
+
# `kind` (:fetch for `=>`, :lookup for `in`) and the keys it reads, with
|
|
43
|
+
# `'**'` for a rest pattern.
|
|
44
|
+
Pattern = Data.define(:kind, :keys)
|
|
45
|
+
|
|
46
|
+
# An `export` call at top level. `value` names the constant it exports,
|
|
47
|
+
# if it is one. `keys` maps a keyword or Hash literal export's keys to
|
|
48
|
+
# RBS types, or is nil. `symbol` is true when it was given a Symbol,
|
|
49
|
+
# which `export` refuses. `branches` lists the enclosing `if`,
|
|
50
|
+
# `unless`, and `case` branches as [conditional, branch] pairs, so two
|
|
51
|
+
# calls in different branches of the same conditional are known never
|
|
52
|
+
# to both run.
|
|
53
|
+
Export =
|
|
54
|
+
Data.define(:value, :keys, :symbol, :line, :branches) do
|
|
55
|
+
def exclusive?(other)
|
|
56
|
+
branches.any? do |id, branch|
|
|
57
|
+
other.branches.any? do |other_id, other_branch|
|
|
58
|
+
id == other_id && branch != other_branch
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A top-level method and its RBS method type.
|
|
65
|
+
Def = Data.define(:name, :type, :line)
|
|
66
|
+
|
|
67
|
+
IMPORTS = %i[import import_relative].freeze
|
|
68
|
+
REQUIRES = %i[require require_relative].freeze
|
|
69
|
+
|
|
70
|
+
attr_reader :path,
|
|
71
|
+
:imports,
|
|
72
|
+
:requires,
|
|
73
|
+
:exports,
|
|
74
|
+
:methods,
|
|
75
|
+
:constants,
|
|
76
|
+
:definitions,
|
|
77
|
+
:references,
|
|
78
|
+
:private_constants,
|
|
79
|
+
:local_writes,
|
|
80
|
+
:errors
|
|
81
|
+
|
|
82
|
+
def self.parse(path) = new(path, Prism.parse_file(path))
|
|
83
|
+
|
|
84
|
+
def initialize(path, result)
|
|
85
|
+
@path = path
|
|
86
|
+
@imports = []
|
|
87
|
+
@requires = []
|
|
88
|
+
@exports = []
|
|
89
|
+
# Top-level methods by name, and RBS types of top-level constants.
|
|
90
|
+
@methods = {}
|
|
91
|
+
@constants = {}
|
|
92
|
+
# Every class and module defined, by full name: :class or :module.
|
|
93
|
+
@definitions = {}
|
|
94
|
+
@references = []
|
|
95
|
+
# Full names made private with `private_constant`.
|
|
96
|
+
@private_constants = []
|
|
97
|
+
# How many times each local variable is assigned, so a local bound to
|
|
98
|
+
# an import is only followed when nothing reassigns it.
|
|
99
|
+
@local_writes = Hash.new(0)
|
|
100
|
+
@errors =
|
|
101
|
+
result.errors.map do |error|
|
|
102
|
+
[error.location.start_line, error.message]
|
|
103
|
+
end
|
|
104
|
+
@annotations =
|
|
105
|
+
result.comments.to_h do |comment|
|
|
106
|
+
[comment.location.start_line, comment.slice]
|
|
107
|
+
end
|
|
108
|
+
Visitor.new(self).visit(result.value)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Whether a `# carton:dynamic` comment on `line`, or the line above,
|
|
112
|
+
# marks an import as intentionally dynamic.
|
|
113
|
+
def dynamic_marker?(line)
|
|
114
|
+
[line, line - 1].any? do |l|
|
|
115
|
+
@annotations[l]&.include?('carton:dynamic')
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The rbs-inline `#: TYPE` annotation on `line`, if any.
|
|
120
|
+
def annotation(line)
|
|
121
|
+
text = @annotations[line]
|
|
122
|
+
text[2..].strip if text&.start_with?('#:')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The RBS type of a literal value node, or `untyped`.
|
|
126
|
+
def self.literal_type(node)
|
|
127
|
+
case node
|
|
128
|
+
when Prism::StringNode, Prism::InterpolatedStringNode
|
|
129
|
+
'::String'
|
|
130
|
+
when Prism::SymbolNode
|
|
131
|
+
'::Symbol'
|
|
132
|
+
when Prism::IntegerNode
|
|
133
|
+
'::Integer'
|
|
134
|
+
when Prism::FloatNode
|
|
135
|
+
'::Float'
|
|
136
|
+
when Prism::TrueNode, Prism::FalseNode
|
|
137
|
+
'bool'
|
|
138
|
+
when Prism::NilNode
|
|
139
|
+
'nil'
|
|
140
|
+
when Prism::ArrayNode
|
|
141
|
+
'::Array[untyped]'
|
|
142
|
+
when Prism::HashNode
|
|
143
|
+
'::Hash[untyped, untyped]'
|
|
144
|
+
when Prism::RegularExpressionNode
|
|
145
|
+
'::Regexp'
|
|
146
|
+
when Prism::CallNode
|
|
147
|
+
node.name == :freeze ? literal_type(node.receiver) : 'untyped'
|
|
148
|
+
else
|
|
149
|
+
'untyped'
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# An RBS method type with untyped parameters matching a `def`.
|
|
154
|
+
def self.method_type(parameters)
|
|
155
|
+
return '() -> untyped' unless parameters
|
|
156
|
+
|
|
157
|
+
params = parameters.requireds.map { |node| positional(node) }
|
|
158
|
+
parameters.optionals.each { |node| params << "?untyped #{node.name}" }
|
|
159
|
+
if parameters.rest.is_a?(Prism::RestParameterNode)
|
|
160
|
+
params << "*untyped #{parameters.rest.name}".strip
|
|
161
|
+
end
|
|
162
|
+
parameters.posts.each { |node| params << positional(node) }
|
|
163
|
+
parameters.keywords.each do |node|
|
|
164
|
+
optional = node.is_a?(Prism::OptionalKeywordParameterNode)
|
|
165
|
+
params << "#{'?' if optional}#{node.name}: untyped"
|
|
166
|
+
end
|
|
167
|
+
block = parameters.block ? ' ?{ (?) -> untyped }' : ''
|
|
168
|
+
case parameters.keyword_rest
|
|
169
|
+
when Prism::KeywordRestParameterNode
|
|
170
|
+
params << "**untyped #{parameters.keyword_rest.name}".strip
|
|
171
|
+
when Prism::ForwardingParameterNode
|
|
172
|
+
params.push('*untyped', '**untyped')
|
|
173
|
+
block = ' ?{ (?) -> untyped }'
|
|
174
|
+
end
|
|
175
|
+
"(#{params.join(', ')})#{block} -> untyped"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def self.positional(node)
|
|
179
|
+
node.respond_to?(:name) ? "untyped #{node.name}" : 'untyped'
|
|
180
|
+
end
|
|
181
|
+
private_class_method :positional
|
|
182
|
+
|
|
183
|
+
# Walks the tree once, tracking the class and module nesting and whether
|
|
184
|
+
# code runs at load time.
|
|
185
|
+
class Visitor < Prism::Visitor
|
|
186
|
+
# Lookups by name, and whether a missing name raises.
|
|
187
|
+
LOOKUPS = {
|
|
188
|
+
:[] => :lookup,
|
|
189
|
+
:key? => :lookup,
|
|
190
|
+
:has_key? => :lookup,
|
|
191
|
+
:values_at => :lookup,
|
|
192
|
+
:fetch => :fetch,
|
|
193
|
+
:fetch_values => :fetch,
|
|
194
|
+
}.freeze
|
|
195
|
+
# Calls that reach names the code does not spell out.
|
|
196
|
+
ESCAPES = %i[
|
|
197
|
+
keys
|
|
198
|
+
to_h
|
|
199
|
+
deconstruct_keys
|
|
200
|
+
send
|
|
201
|
+
public_send
|
|
202
|
+
__send__
|
|
203
|
+
method
|
|
204
|
+
public_method
|
|
205
|
+
const_get
|
|
206
|
+
constants
|
|
207
|
+
respond_to?
|
|
208
|
+
].freeze
|
|
209
|
+
|
|
210
|
+
def initialize(file)
|
|
211
|
+
super()
|
|
212
|
+
@file = file
|
|
213
|
+
@namespace = []
|
|
214
|
+
@defs = 0
|
|
215
|
+
@blocks = 0
|
|
216
|
+
@constant = nil
|
|
217
|
+
@local = nil
|
|
218
|
+
@pattern = nil
|
|
219
|
+
# Nodes already recorded as a receiver or pattern value, and the
|
|
220
|
+
# constant paths that name a class or module being defined.
|
|
221
|
+
@handled = {}.compare_by_identity
|
|
222
|
+
@defining = {}.compare_by_identity
|
|
223
|
+
@rescued = []
|
|
224
|
+
@branches = []
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Code in a `begin` body may raise into its `rescue` clauses, which
|
|
228
|
+
# record the classes they catch; a bare `rescue` catches StandardError.
|
|
229
|
+
def visit_begin_node(node)
|
|
230
|
+
return super unless node.rescue_clause
|
|
231
|
+
|
|
232
|
+
rescuing(rescued_classes(node.rescue_clause)) do
|
|
233
|
+
visit(node.statements) if node.statements
|
|
234
|
+
end
|
|
235
|
+
visit(node.rescue_clause)
|
|
236
|
+
visit(node.else_clause) if node.else_clause
|
|
237
|
+
visit(node.ensure_clause) if node.ensure_clause
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def visit_rescue_modifier_node(node)
|
|
241
|
+
rescuing(['StandardError']) { visit(node.expression) }
|
|
242
|
+
visit(node.rescue_expression)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def visit_call_node(node)
|
|
246
|
+
record_call(node) if node.receiver.nil?
|
|
247
|
+
record_use(node) if bindable?(node.receiver)
|
|
248
|
+
super
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def visit_constant_path_node(node)
|
|
252
|
+
unless @defining[node]
|
|
253
|
+
if bindable?(node.parent)
|
|
254
|
+
@handled[node.parent] = true
|
|
255
|
+
reference(node.parent, :const, node.name.to_s)
|
|
256
|
+
end
|
|
257
|
+
reference(node, :value, nil) unless @handled[node]
|
|
258
|
+
end
|
|
259
|
+
super
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def visit_constant_read_node(node)
|
|
263
|
+
reference(node, :value, nil) unless @defining[node] || @handled[node]
|
|
264
|
+
super
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def visit_local_variable_read_node(node)
|
|
268
|
+
reference(node, :value, nil) unless @handled[node]
|
|
269
|
+
super
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def visit_local_variable_write_node(node)
|
|
273
|
+
name = node.name.to_s
|
|
274
|
+
@file.local_writes[name] += 1
|
|
275
|
+
binding(:@local, name, node.value) { super }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
%i[
|
|
279
|
+
local_variable_operator_write_node
|
|
280
|
+
local_variable_or_write_node
|
|
281
|
+
local_variable_and_write_node
|
|
282
|
+
local_variable_target_node
|
|
283
|
+
].each do |type|
|
|
284
|
+
define_method(:"visit_#{type}") do |node|
|
|
285
|
+
@file.local_writes[node.name.to_s] += 1
|
|
286
|
+
super(node)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def visit_match_required_node(node)
|
|
291
|
+
match(node.value, node.pattern, :fetch) { super }
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def visit_match_predicate_node(node)
|
|
295
|
+
match(node.value, node.pattern, :lookup) { super }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def visit_def_node(node)
|
|
299
|
+
if top_level? && node.receiver.nil?
|
|
300
|
+
line = node.location.start_line
|
|
301
|
+
type =
|
|
302
|
+
@file.annotation(line - 1) ||
|
|
303
|
+
SourceFile.method_type(node.parameters)
|
|
304
|
+
@file.methods[node.name.to_s] = Def.new(node.name.to_s, type, line)
|
|
305
|
+
end
|
|
306
|
+
nest(:@defs) { super }
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def visit_block_node(node) = nest(:@blocks) { super }
|
|
310
|
+
|
|
311
|
+
def visit_lambda_node(node) = nest(:@blocks) { super }
|
|
312
|
+
|
|
313
|
+
def visit_class_node(node) = define(node, :class) { super }
|
|
314
|
+
|
|
315
|
+
def visit_module_node(node) = define(node, :module) { super }
|
|
316
|
+
|
|
317
|
+
def visit_case_match_node(node)
|
|
318
|
+
node.conditions.each do |condition|
|
|
319
|
+
next unless condition.is_a?(Prism::InNode)
|
|
320
|
+
|
|
321
|
+
match(node.predicate, condition.pattern, :lookup)
|
|
322
|
+
end
|
|
323
|
+
visit_branches(
|
|
324
|
+
node,
|
|
325
|
+
node.predicate,
|
|
326
|
+
node.conditions,
|
|
327
|
+
node.else_clause,
|
|
328
|
+
)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def visit_case_node(node)
|
|
332
|
+
visit_branches(
|
|
333
|
+
node,
|
|
334
|
+
node.predicate,
|
|
335
|
+
node.conditions,
|
|
336
|
+
node.else_clause,
|
|
337
|
+
)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def visit_if_node(node)
|
|
341
|
+
visit_branches(
|
|
342
|
+
node,
|
|
343
|
+
node.predicate,
|
|
344
|
+
[node.statements],
|
|
345
|
+
node.subsequent,
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def visit_unless_node(node)
|
|
350
|
+
visit_branches(
|
|
351
|
+
node,
|
|
352
|
+
node.predicate,
|
|
353
|
+
[node.statements],
|
|
354
|
+
node.else_clause,
|
|
355
|
+
)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def visit_singleton_class_node(node) = nest(:@defs) { super }
|
|
359
|
+
|
|
360
|
+
def visit_constant_write_node(node)
|
|
361
|
+
name = qualify(node.name.to_s)
|
|
362
|
+
if top_level?
|
|
363
|
+
@file.constants[name] = @file.annotation(
|
|
364
|
+
node.location.start_line,
|
|
365
|
+
) || SourceFile.literal_type(node.value)
|
|
366
|
+
end
|
|
367
|
+
binding(:@constant, name, node.value) { super }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def visit_constant_path_write_node(node)
|
|
371
|
+
name = constant_name(node.target)
|
|
372
|
+
@defining[node.target] = true
|
|
373
|
+
binding(:@constant, name && qualify(name), node.value) { super }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
private
|
|
377
|
+
|
|
378
|
+
def record_call(node)
|
|
379
|
+
if IMPORTS.include?(node.name) || REQUIRES.include?(node.name)
|
|
380
|
+
record_load(node)
|
|
381
|
+
elsif node.name == :export && top_level?
|
|
382
|
+
record_export(node)
|
|
383
|
+
elsif node.name == :private_constant && @namespace.any?
|
|
384
|
+
record_private(@namespace.last, node)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def record_load(node)
|
|
389
|
+
args = node.arguments&.arguments || []
|
|
390
|
+
return unless args.size == 1
|
|
391
|
+
|
|
392
|
+
call =
|
|
393
|
+
Call.new(
|
|
394
|
+
kind: node.name.to_s,
|
|
395
|
+
path:
|
|
396
|
+
(args.first.unescaped if args.first.is_a?(Prism::StringNode)),
|
|
397
|
+
file: @file.path,
|
|
398
|
+
line: node.location.start_line,
|
|
399
|
+
lazy: @defs.positive? || @blocks.positive?,
|
|
400
|
+
constant: @constant,
|
|
401
|
+
local: @local,
|
|
402
|
+
pattern: (@pattern.last if @pattern&.first.equal?(node)),
|
|
403
|
+
)
|
|
404
|
+
list = IMPORTS.include?(node.name) ? @file.imports : @file.requires
|
|
405
|
+
list << call
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def record_export(node)
|
|
409
|
+
args = node.arguments&.arguments || []
|
|
410
|
+
arg = args.first if args.size == 1
|
|
411
|
+
@file.exports << Export.new(
|
|
412
|
+
value: constant_name(arg),
|
|
413
|
+
keys: hash_types(arg),
|
|
414
|
+
symbol: arg.is_a?(Prism::SymbolNode) || arg.is_a?(Prism::DefNode),
|
|
415
|
+
line: node.location.start_line,
|
|
416
|
+
branches: @branches.dup,
|
|
417
|
+
)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# `export version: '1.0'` or `export({ version: '1.0' })`: each
|
|
421
|
+
# Symbol key's RBS type, or nil when a key is not a Symbol literal.
|
|
422
|
+
def hash_types(node)
|
|
423
|
+
case node
|
|
424
|
+
when Prism::KeywordHashNode, Prism::HashNode
|
|
425
|
+
node.elements.to_h do |element|
|
|
426
|
+
unless element.is_a?(Prism::AssocNode) &&
|
|
427
|
+
element.key.is_a?(Prism::SymbolNode)
|
|
428
|
+
return nil
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
[element.key.unescaped, SourceFile.literal_type(element.value)]
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Visit a conditional's predicate, then each branch and the else
|
|
437
|
+
# branch under its own [conditional, branch] entry.
|
|
438
|
+
def visit_branches(node, predicate, branches, otherwise)
|
|
439
|
+
visit(predicate) if predicate
|
|
440
|
+
id = node.location.start_offset
|
|
441
|
+
[*branches, otherwise].each_with_index do |branch, index|
|
|
442
|
+
next unless branch
|
|
443
|
+
|
|
444
|
+
@branches.push([id, index])
|
|
445
|
+
begin
|
|
446
|
+
visit(branch)
|
|
447
|
+
ensure
|
|
448
|
+
@branches.pop
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# A method call on a name that may be bound to an import.
|
|
454
|
+
def record_use(node)
|
|
455
|
+
receiver = node.receiver
|
|
456
|
+
@handled[receiver] = true
|
|
457
|
+
if node.name == :private_constant
|
|
458
|
+
record_private(qualify(constant_name(receiver).to_s), node)
|
|
459
|
+
elsif LOOKUPS.key?(node.name)
|
|
460
|
+
record_lookup(receiver, node)
|
|
461
|
+
elsif ESCAPES.include?(node.name)
|
|
462
|
+
reference(receiver, :value, nil)
|
|
463
|
+
else
|
|
464
|
+
reference(receiver, :call, node.name.to_s)
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# `fetch` with a default or a block does not raise, and only
|
|
469
|
+
# `values_at` and `fetch_values` read every argument.
|
|
470
|
+
def record_lookup(receiver, node)
|
|
471
|
+
args = node.arguments&.arguments || []
|
|
472
|
+
args = args.first(1) unless %i[values_at fetch_values].include?(
|
|
473
|
+
node.name,
|
|
474
|
+
)
|
|
475
|
+
keys = args.map { |arg| literal_key(arg) }
|
|
476
|
+
if keys.empty? || keys.include?(nil)
|
|
477
|
+
return reference(receiver, :value, nil)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
kind = LOOKUPS[node.name]
|
|
481
|
+
kind = :lookup if node.name == :fetch &&
|
|
482
|
+
(node.block || node.arguments.arguments.size > 1)
|
|
483
|
+
keys.each { |key| reference(receiver, kind, key) }
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def record_private(namespace, node)
|
|
487
|
+
(node.arguments&.arguments || []).each do |arg|
|
|
488
|
+
next unless arg.is_a?(Prism::SymbolNode)
|
|
489
|
+
|
|
490
|
+
@file.private_constants << [namespace, arg.unescaped].reject(
|
|
491
|
+
&:empty?
|
|
492
|
+
).join('::')
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def match(value, pattern, kind)
|
|
497
|
+
keys = pattern_keys(pattern)
|
|
498
|
+
saved = @pattern
|
|
499
|
+
if keys && bindable?(value)
|
|
500
|
+
@handled[value] = true
|
|
501
|
+
keys.each do |key|
|
|
502
|
+
if key == '**'
|
|
503
|
+
reference(value, :value, nil)
|
|
504
|
+
else
|
|
505
|
+
reference(value, kind, key)
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
elsif keys && import_call?(value)
|
|
509
|
+
@pattern = [value, Pattern.new(kind:, keys:)]
|
|
510
|
+
end
|
|
511
|
+
yield if block_given?
|
|
512
|
+
ensure
|
|
513
|
+
@pattern = saved
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# The keys a hash pattern reads, with `'**'` for a rest pattern, or
|
|
517
|
+
# nil for any other pattern.
|
|
518
|
+
def pattern_keys(pattern)
|
|
519
|
+
return unless pattern.is_a?(Prism::HashPatternNode)
|
|
520
|
+
|
|
521
|
+
keys =
|
|
522
|
+
pattern.elements.map do |element|
|
|
523
|
+
key = element.key if element.is_a?(Prism::AssocNode)
|
|
524
|
+
key.is_a?(Prism::SymbolNode) ? key.unescaped : '**'
|
|
525
|
+
end
|
|
526
|
+
keys << '**' if pattern.rest.is_a?(Prism::AssocSplatNode)
|
|
527
|
+
keys
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def literal_key(node)
|
|
531
|
+
unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
|
|
532
|
+
return
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
node.unescaped
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def reference(node, kind, member)
|
|
539
|
+
local = node.is_a?(Prism::LocalVariableReadNode)
|
|
540
|
+
name = local ? node.name.to_s : constant_name(node)
|
|
541
|
+
return unless name
|
|
542
|
+
|
|
543
|
+
@file.references << Reference.new(
|
|
544
|
+
name:,
|
|
545
|
+
local:,
|
|
546
|
+
scope: @namespace.last,
|
|
547
|
+
kind:,
|
|
548
|
+
member:,
|
|
549
|
+
line: node.location.start_line,
|
|
550
|
+
rescued: @rescued.flatten.uniq,
|
|
551
|
+
)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def rescuing(classes)
|
|
555
|
+
@rescued.push(classes)
|
|
556
|
+
yield
|
|
557
|
+
ensure
|
|
558
|
+
@rescued.pop
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def rescued_classes(clause)
|
|
562
|
+
classes = []
|
|
563
|
+
while clause
|
|
564
|
+
names = clause.exceptions.filter_map { |node| constant_name(node) }
|
|
565
|
+
classes.concat(names.empty? ? ['StandardError'] : names)
|
|
566
|
+
clause = clause.subsequent
|
|
567
|
+
end
|
|
568
|
+
classes
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def bindable?(node)
|
|
572
|
+
node.is_a?(Prism::ConstantReadNode) ||
|
|
573
|
+
node.is_a?(Prism::ConstantPathNode) ||
|
|
574
|
+
node.is_a?(Prism::LocalVariableReadNode)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def import_call?(node)
|
|
578
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? &&
|
|
579
|
+
IMPORTS.include?(node.name)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def define(node, kind, &)
|
|
583
|
+
mark_defining(node.constant_path)
|
|
584
|
+
name = constant_name(node.constant_path)
|
|
585
|
+
return nest(:@defs, 0, &) unless name
|
|
586
|
+
|
|
587
|
+
name = qualify(name)
|
|
588
|
+
@file.definitions[name] ||= kind
|
|
589
|
+
@namespace.push(name)
|
|
590
|
+
begin
|
|
591
|
+
nest(:@defs, 0, &)
|
|
592
|
+
ensure
|
|
593
|
+
@namespace.pop
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# A class or module body runs at load time, so it resets `@defs`;
|
|
598
|
+
# `step` 0 keeps the count while restoring it afterwards.
|
|
599
|
+
def nest(counter, step = 1)
|
|
600
|
+
saved = instance_variable_get(counter)
|
|
601
|
+
instance_variable_set(counter, step.zero? ? 0 : saved + step)
|
|
602
|
+
yield
|
|
603
|
+
ensure
|
|
604
|
+
instance_variable_set(counter, saved)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# The constant path a class or module definition names is not a use.
|
|
608
|
+
def mark_defining(node)
|
|
609
|
+
while node.is_a?(Prism::ConstantPathNode) ||
|
|
610
|
+
node.is_a?(Prism::ConstantReadNode)
|
|
611
|
+
@defining[node] = true
|
|
612
|
+
node = node.respond_to?(:parent) ? node.parent : nil
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# Only a direct `CONST = import ...` or `local = import ...` binds the
|
|
617
|
+
# name to the import.
|
|
618
|
+
def binding(ivar, name, value)
|
|
619
|
+
saved = instance_variable_get(ivar)
|
|
620
|
+
instance_variable_set(ivar, (name if import_call?(value)))
|
|
621
|
+
yield
|
|
622
|
+
ensure
|
|
623
|
+
instance_variable_set(ivar, saved)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# The constant path as written, or nil for a dynamic one like `x::Y`.
|
|
627
|
+
def constant_name(node)
|
|
628
|
+
case node
|
|
629
|
+
when Prism::ConstantReadNode
|
|
630
|
+
node.name.to_s
|
|
631
|
+
when Prism::ConstantPathNode
|
|
632
|
+
node.full_name
|
|
633
|
+
end
|
|
634
|
+
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
|
|
635
|
+
nil
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def qualify(name)
|
|
639
|
+
return name.delete_prefix('::') if name.start_with?('::')
|
|
640
|
+
|
|
641
|
+
[*@namespace.last, name].join('::')
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def top_level? = @namespace.empty? && @defs.zero?
|
|
645
|
+
end
|
|
646
|
+
private_constant :Visitor
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
end
|
data/lib/carton/tools.rb
ADDED