archspec 1.0.1 → 1.1.0.rc2
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 +12 -6
- data/lib/archspec/analyzer.rb +639 -261
- data/lib/archspec/architectures.rb +7 -3
- data/lib/archspec/component_spec.rb +13 -2
- data/lib/archspec/diagnostic.rb +19 -3
- data/lib/archspec/dsl.rb +69 -38
- data/lib/archspec/formatters/explanation.rb +33 -5
- data/lib/archspec/formatters/json.rb +1 -0
- data/lib/archspec/formatters/text.rb +19 -1
- data/lib/archspec/model.rb +266 -65
- data/lib/archspec/rubydex_index.rb +379 -0
- data/lib/archspec/rules/component_rules.rb +3 -4
- data/lib/archspec/rules/concern_rules.rb +7 -1
- data/lib/archspec/rules/cycle_rule.rb +130 -33
- data/lib/archspec/rules/naming_rules.rb +11 -22
- data/lib/archspec/rules/protocol_rules.rb +117 -22
- data/lib/archspec/rules/reasoned.rb +37 -0
- data/lib/archspec/source_location.rb +1 -3
- data/lib/archspec/version.rb +1 -1
- data/lib/archspec.rb +4 -2
- metadata +21 -6
- data/lib/archspec/value_object.rb +0 -46
data/lib/archspec/analyzer.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'prism'
|
|
4
|
+
require 'set'
|
|
4
5
|
|
|
5
6
|
module ArchSpec
|
|
6
7
|
module Analyzer
|
|
@@ -9,18 +10,23 @@ module ArchSpec
|
|
|
9
10
|
def analyze(definition, root:)
|
|
10
11
|
root = File.expand_path(root)
|
|
11
12
|
graph = Graph.new(root)
|
|
13
|
+
paths = ruby_files(definition, root)
|
|
14
|
+
syntax = SyntaxOverlay.new
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
paths.each do |path|
|
|
14
17
|
result = Prism.parse_file(path)
|
|
15
18
|
graph.add_file(
|
|
16
19
|
path: path,
|
|
17
20
|
parse_errors: parse_errors_for(path, result.errors),
|
|
18
21
|
suppressions: suppressions_for(result.comments)
|
|
19
22
|
)
|
|
20
|
-
|
|
21
|
-
SourceVisitor.visit(graph, path, result.value) if result.value
|
|
23
|
+
syntax.scan(path, result.value) if result.value
|
|
22
24
|
end
|
|
23
25
|
|
|
26
|
+
method_names = definition.rules.grep(Rules::CannotCallRule).flat_map(&:method_names)
|
|
27
|
+
index = RubydexIndex.new(paths, syntax: syntax, method_names: method_names)
|
|
28
|
+
index.populate(graph)
|
|
29
|
+
syntax.apply(graph, call_resolutions: index.call_resolutions)
|
|
24
30
|
graph.assign_components(definition.component_specs.values)
|
|
25
31
|
graph
|
|
26
32
|
end
|
|
@@ -81,8 +87,6 @@ module ArchSpec
|
|
|
81
87
|
when 'next-line'
|
|
82
88
|
suppressions << Suppression.new(rule, line + 1, line + 1, reason)
|
|
83
89
|
else
|
|
84
|
-
# The block form covers its own line too, so a trailing
|
|
85
|
-
# `# archspec:disable RULE` works like RuboCop's.
|
|
86
90
|
active[rule] << [line, reason]
|
|
87
91
|
end
|
|
88
92
|
elsif (match = text.match(ENABLE_PATTERN))
|
|
@@ -116,8 +120,28 @@ module ArchSpec
|
|
|
116
120
|
end
|
|
117
121
|
end
|
|
118
122
|
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
# Rubydex supplies the semantic graph. This pass records the few facts
|
|
124
|
+
# whose syntax matters to ArchSpec's rules or that Rubydex does not model.
|
|
125
|
+
class SyntaxOverlay
|
|
126
|
+
Owner = Data.define(:name, :scope)
|
|
127
|
+
ConstantSite = Data.define(:path, :name, :owner, :nesting, :location)
|
|
128
|
+
CallSite = Data.define(:path, :name, :owner, :receiver, :receiver_name, :scope, :nesting, :location)
|
|
129
|
+
CallResolution = Data.define(:receiver, :scope)
|
|
130
|
+
GeneratedMethod = Data.define(:path, :owner, :message, :node, :location, :visibility, :scope)
|
|
131
|
+
MethodOwner = Data.define(:path, :location, :owner)
|
|
132
|
+
DeclarationName = Data.define(:path, :raw, :qualified, :location)
|
|
133
|
+
DeclarationFallback = Data.define(
|
|
134
|
+
:path,
|
|
135
|
+
:name,
|
|
136
|
+
:kind,
|
|
137
|
+
:location,
|
|
138
|
+
:nesting,
|
|
139
|
+
:superclass,
|
|
140
|
+
:superclass_location,
|
|
141
|
+
:dynamic_superclass
|
|
142
|
+
)
|
|
143
|
+
MethodFallback = Data.define(:path, :owner, :name, :scope, :location, :visibility, :signatures)
|
|
144
|
+
MixinFallback = Data.define(:path, :owner, :message, :target, :location, :nesting)
|
|
121
145
|
|
|
122
146
|
DYNAMIC_MESSAGES = %i[
|
|
123
147
|
class_eval
|
|
@@ -131,392 +155,712 @@ module ArchSpec
|
|
|
131
155
|
send
|
|
132
156
|
].freeze
|
|
133
157
|
|
|
134
|
-
|
|
158
|
+
GENERATED_METHOD_MACROS = {
|
|
159
|
+
attr_reader: %i[reader],
|
|
160
|
+
attr_writer: %i[writer],
|
|
161
|
+
attr_accessor: %i[reader writer],
|
|
162
|
+
attribute: %i[reader writer],
|
|
163
|
+
delegate: %i[reader],
|
|
164
|
+
alias_attribute: %i[reader writer],
|
|
165
|
+
enum: %i[reader writer],
|
|
166
|
+
store_accessor: %i[reader writer],
|
|
167
|
+
belongs_to: %i[reader writer],
|
|
168
|
+
has_one: %i[reader writer],
|
|
169
|
+
has_many: %i[reader writer],
|
|
170
|
+
has_and_belongs_to_many: %i[reader writer]
|
|
171
|
+
}.freeze
|
|
172
|
+
|
|
173
|
+
SINGLE_NAME_MACROS = %i[
|
|
174
|
+
attribute
|
|
175
|
+
alias_attribute
|
|
176
|
+
enum
|
|
177
|
+
belongs_to
|
|
178
|
+
has_one
|
|
179
|
+
has_many
|
|
180
|
+
has_and_belongs_to_many
|
|
181
|
+
].freeze
|
|
182
|
+
|
|
183
|
+
SINGULAR_ASSOCIATION_MACROS = %i[belongs_to has_one].freeze
|
|
184
|
+
SINGULAR_ASSOCIATION_HELPERS = ['build_%s', 'create_%s', 'create_%s!', 'reload_%s'].freeze
|
|
185
|
+
|
|
186
|
+
BUILDER_KINDS = {
|
|
187
|
+
%w[Class new] => :class,
|
|
188
|
+
%w[Struct new] => :class,
|
|
189
|
+
%w[Data define] => :class,
|
|
190
|
+
%w[Module new] => :module
|
|
191
|
+
}.freeze
|
|
192
|
+
|
|
193
|
+
MIXIN_EDGE_TYPES = {
|
|
135
194
|
include: :includes,
|
|
136
195
|
prepend: :prepends,
|
|
137
196
|
extend: :extends
|
|
138
197
|
}.freeze
|
|
139
198
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
199
|
+
VISIBILITY_MODIFIERS = {
|
|
200
|
+
private: [:private, nil],
|
|
201
|
+
protected: [:protected, nil],
|
|
202
|
+
public: [:public, nil],
|
|
203
|
+
private_class_method: [:private, :class],
|
|
204
|
+
public_class_method: [:public, :class]
|
|
144
205
|
}.freeze
|
|
145
206
|
|
|
146
|
-
|
|
147
|
-
|
|
207
|
+
attr_reader :constant_sites, :call_sites
|
|
208
|
+
|
|
209
|
+
def initialize
|
|
210
|
+
@constant_sites = []
|
|
211
|
+
@constant_sites_by_path = Hash.new { |hash, key| hash[key] = [] }
|
|
212
|
+
@call_sites = []
|
|
213
|
+
@generated_methods = []
|
|
214
|
+
@declaration_fallbacks = []
|
|
215
|
+
@method_fallbacks = []
|
|
216
|
+
@mixin_fallbacks = []
|
|
217
|
+
@extra_edges = []
|
|
218
|
+
@namespace_flags = Hash.new { |hash, key| hash[key] = [] }
|
|
219
|
+
@declaration_kinds = {}
|
|
220
|
+
@declaration_names = []
|
|
221
|
+
@superclass_overrides = {}
|
|
222
|
+
@superclass_fallbacks = {}
|
|
223
|
+
@method_owner_overrides = []
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def scan(path, node)
|
|
227
|
+
visit(File.expand_path(path), node)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def apply(graph, call_resolutions: {})
|
|
231
|
+
@declaration_fallbacks.each do |fallback|
|
|
232
|
+
constant = graph.constants_named(fallback.name).find { |candidate| candidate.path == fallback.path }
|
|
233
|
+
unless constant
|
|
234
|
+
constant = graph.add_constant(
|
|
235
|
+
name: fallback.name,
|
|
236
|
+
kind: fallback.kind,
|
|
237
|
+
path: fallback.path,
|
|
238
|
+
location: fallback.location,
|
|
239
|
+
nesting: fallback.nesting
|
|
240
|
+
)
|
|
241
|
+
constant.superclass = fallback.superclass if constant.class?
|
|
242
|
+
end
|
|
243
|
+
add_fallback_ancestry(graph, fallback)
|
|
244
|
+
end
|
|
245
|
+
apply_method_fallbacks(graph)
|
|
246
|
+
apply_mixin_fallbacks(graph)
|
|
247
|
+
@namespace_flags.each do |(name, path), flags|
|
|
248
|
+
graph.set_namespace_only(name, path, flags.all?)
|
|
249
|
+
end
|
|
250
|
+
apply_generated_methods(graph)
|
|
251
|
+
@extra_edges.each { |edge| graph.add_edge(**edge) }
|
|
252
|
+
call_sites.each do |site|
|
|
253
|
+
resolution = call_resolutions[site]
|
|
254
|
+
resolved_method = graph.resolve_method_alias(resolution.receiver, site.name, resolution.scope) if resolution
|
|
255
|
+
graph.add_edge(
|
|
256
|
+
type: :calls_named_method,
|
|
257
|
+
from_path: site.path,
|
|
258
|
+
from_constant: site.owner,
|
|
259
|
+
to: site.name,
|
|
260
|
+
location: site.location,
|
|
261
|
+
receiver: site.receiver,
|
|
262
|
+
resolved_receiver: resolution&.receiver,
|
|
263
|
+
receiver_scope: resolution&.scope,
|
|
264
|
+
resolved_method: resolved_method
|
|
265
|
+
)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def declaration_kind(name, path)
|
|
270
|
+
@declaration_kinds[[normalize(name), path]]
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def declaration_name(name, path = nil, location = nil)
|
|
274
|
+
normalized = normalize(name)
|
|
275
|
+
return normalized unless path
|
|
276
|
+
|
|
277
|
+
candidates = @declaration_names.select do |candidate|
|
|
278
|
+
candidate.path == path &&
|
|
279
|
+
(normalized == candidate.raw || normalized.start_with?("#{candidate.raw}::"))
|
|
280
|
+
end
|
|
281
|
+
candidates.select! { |candidate| contains?(candidate.location, location) } if location
|
|
282
|
+
mapping = candidates.max_by { |candidate| candidate.raw.length }
|
|
283
|
+
mapping ? "#{mapping.qualified}#{normalized.delete_prefix(mapping.raw)}" : normalized
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def superclass_override(name, path)
|
|
287
|
+
@superclass_overrides[[normalize(name), path]]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def superclass_fallback(name, path)
|
|
291
|
+
@superclass_fallbacks[[normalize(name), path]]
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def constant_name_at(path, location)
|
|
295
|
+
sites = @constant_sites_by_path[path].select { |site| contains?(site.location, location) }
|
|
296
|
+
sites.max_by { |site| site.name.length }&.name
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def method_owner_override(path, location)
|
|
300
|
+
override = @method_owner_overrides.reverse_each.find do |candidate|
|
|
301
|
+
candidate.path == path && contains?(candidate.location, location)
|
|
302
|
+
end
|
|
303
|
+
override&.owner
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def method_owner(name, scope)
|
|
307
|
+
Owner.new(name, scope) if name
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
private
|
|
311
|
+
|
|
312
|
+
def visit(path, node, current_constant: nil, namespace: [], nesting: [], visibility: :public,
|
|
313
|
+
default_scope: :instance, forced_method_owner: nil)
|
|
148
314
|
return unless node
|
|
149
315
|
|
|
150
316
|
case node
|
|
151
317
|
when Prism::ProgramNode, Prism::StatementsNode
|
|
152
|
-
visit_children(
|
|
153
|
-
|
|
318
|
+
visit_children(path, node, current_constant: current_constant, namespace: namespace, nesting: nesting,
|
|
319
|
+
visibility: visibility, default_scope: default_scope,
|
|
320
|
+
forced_method_owner: forced_method_owner)
|
|
154
321
|
when Prism::ClassNode
|
|
155
|
-
visit_class(
|
|
322
|
+
visit_class(path, node, current_constant: current_constant, namespace: namespace, nesting: nesting)
|
|
156
323
|
when Prism::ModuleNode
|
|
157
|
-
visit_module(
|
|
324
|
+
visit_module(path, node, current_constant: current_constant, namespace: namespace, nesting: nesting)
|
|
158
325
|
when Prism::SingletonClassNode
|
|
159
|
-
visit_singleton_class(
|
|
160
|
-
|
|
161
|
-
|
|
326
|
+
visit_singleton_class(path, node, current_constant: current_constant, namespace: namespace,
|
|
327
|
+
nesting: nesting, visibility: visibility,
|
|
328
|
+
forced_method_owner: forced_method_owner)
|
|
162
329
|
when Prism::DefNode
|
|
163
|
-
visit_def(
|
|
164
|
-
|
|
330
|
+
visit_def(path, node, current_constant: current_constant, namespace: namespace, nesting: nesting,
|
|
331
|
+
visibility: visibility, default_scope: default_scope,
|
|
332
|
+
forced_method_owner: forced_method_owner)
|
|
165
333
|
when Prism::CallNode
|
|
166
|
-
visit_call(
|
|
167
|
-
|
|
334
|
+
visit_call(path, node, current_constant: current_constant, namespace: namespace, nesting: nesting,
|
|
335
|
+
visibility: visibility, default_scope: default_scope,
|
|
336
|
+
forced_method_owner: forced_method_owner)
|
|
337
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode
|
|
338
|
+
visit_constant_assignment(path, node, current_constant: current_constant, namespace: namespace,
|
|
339
|
+
nesting: nesting)
|
|
168
340
|
when Prism::ConstantPathNode, Prism::ConstantReadNode
|
|
169
|
-
|
|
341
|
+
record_constant_site(path, node, current_constant, nesting)
|
|
170
342
|
else
|
|
171
|
-
visit_children(
|
|
172
|
-
|
|
343
|
+
visit_children(path, node, current_constant: current_constant, namespace: namespace, nesting: nesting,
|
|
344
|
+
visibility: visibility, default_scope: default_scope,
|
|
345
|
+
forced_method_owner: forced_method_owner)
|
|
173
346
|
end
|
|
174
347
|
end
|
|
175
348
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def visit_class(graph, path, node, current_constant:, namespace:, nesting:)
|
|
349
|
+
def visit_class(path, node, current_constant:, namespace:, nesting:)
|
|
179
350
|
name = qualified_constant_name(node.constant_path, namespace)
|
|
180
|
-
|
|
181
|
-
|
|
351
|
+
unless name
|
|
352
|
+
return visit_children(path, node, current_constant: current_constant, namespace: namespace,
|
|
353
|
+
nesting: nesting)
|
|
354
|
+
end
|
|
182
355
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
path
|
|
187
|
-
|
|
188
|
-
|
|
356
|
+
location = SourceLocation.from_prism(path, node.location)
|
|
357
|
+
record_namespace(
|
|
358
|
+
name,
|
|
359
|
+
path,
|
|
360
|
+
node.superclass.nil? && namespace_only_body?(node.body),
|
|
361
|
+
declared_as: constant_reference_name(node.constant_path),
|
|
362
|
+
location: location
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
@declaration_fallbacks << DeclarationFallback.new(
|
|
366
|
+
path,
|
|
367
|
+
name,
|
|
368
|
+
:class,
|
|
369
|
+
location,
|
|
370
|
+
nesting,
|
|
371
|
+
class_superclass(node),
|
|
372
|
+
node.superclass && SourceLocation.from_prism(path, node.superclass.location),
|
|
373
|
+
node.superclass && !constant_node?(node.superclass)
|
|
189
374
|
)
|
|
190
375
|
|
|
191
376
|
if node.superclass
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if superclass
|
|
195
|
-
constant.superclass = superclass
|
|
196
|
-
graph.add_edge(
|
|
197
|
-
type: :inherits_from,
|
|
198
|
-
from_path: path,
|
|
199
|
-
from_constant: constant.name,
|
|
200
|
-
to: superclass,
|
|
201
|
-
location: SourceLocation.from_prism(path, node.superclass.location),
|
|
202
|
-
lexical_nesting: nesting
|
|
203
|
-
)
|
|
377
|
+
if constant_node?(node.superclass)
|
|
378
|
+
@superclass_fallbacks[[normalize(name), path]] = constant_reference_name(node.superclass)
|
|
204
379
|
else
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
# references, and ancestry stays marked unresolved.
|
|
208
|
-
constant.superclass = node.superclass.slice
|
|
209
|
-
visit(graph, path, node.superclass, current_constant: constant.name, namespace: namespace,
|
|
210
|
-
nesting: nesting)
|
|
380
|
+
@superclass_overrides[[normalize(name), path]] = node.superclass.slice
|
|
381
|
+
visit(path, node.superclass, current_constant: name, namespace: namespace, nesting: nesting)
|
|
211
382
|
end
|
|
212
383
|
end
|
|
213
384
|
|
|
214
|
-
visit_constant_body(
|
|
215
|
-
nesting: [constant.name] + nesting)
|
|
385
|
+
visit_constant_body(path, name, node.body, name.split('::'), nesting: [name] + nesting)
|
|
216
386
|
end
|
|
217
387
|
|
|
218
|
-
def visit_module(
|
|
388
|
+
def visit_module(path, node, current_constant:, namespace:, nesting:)
|
|
219
389
|
name = qualified_constant_name(node.constant_path, namespace)
|
|
220
|
-
|
|
221
|
-
|
|
390
|
+
unless name
|
|
391
|
+
return visit_children(path, node, current_constant: current_constant, namespace: namespace,
|
|
392
|
+
nesting: nesting)
|
|
393
|
+
end
|
|
222
394
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
path
|
|
227
|
-
|
|
228
|
-
|
|
395
|
+
location = SourceLocation.from_prism(path, node.location)
|
|
396
|
+
record_namespace(
|
|
397
|
+
name,
|
|
398
|
+
path,
|
|
399
|
+
namespace_only_body?(node.body),
|
|
400
|
+
declared_as: constant_reference_name(node.constant_path),
|
|
401
|
+
location: location
|
|
229
402
|
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
403
|
+
@declaration_fallbacks << DeclarationFallback.new(
|
|
404
|
+
path,
|
|
405
|
+
name,
|
|
406
|
+
:module,
|
|
407
|
+
location,
|
|
408
|
+
nesting,
|
|
409
|
+
nil,
|
|
410
|
+
nil,
|
|
411
|
+
false
|
|
412
|
+
)
|
|
413
|
+
visit_constant_body(path, name, node.body, name.split('::'), nesting: [name] + nesting)
|
|
233
414
|
end
|
|
234
415
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def visit_singleton_class(graph, path, node, current_constant:, namespace:, nesting:, visibility:, default_scope:)
|
|
241
|
-
target = singleton_target(node.expression, current_constant)
|
|
242
|
-
constant = target && graph.constants_named(target).find { |candidate| candidate.path == path }
|
|
416
|
+
def visit_constant_assignment(path, node, current_constant:, namespace:, nesting:)
|
|
417
|
+
name = assigned_constant_name(node, namespace)
|
|
418
|
+
unless name
|
|
419
|
+
return visit(path, node.value, current_constant: current_constant, namespace: namespace, nesting: nesting)
|
|
420
|
+
end
|
|
243
421
|
|
|
244
|
-
if
|
|
245
|
-
|
|
246
|
-
else
|
|
247
|
-
visit_children(graph, path, node, current_constant: nil, namespace: namespace,
|
|
248
|
-
nesting: nesting, visibility: visibility, default_scope: :class)
|
|
422
|
+
if node.is_a?(Prism::ConstantPathWriteNode) && node.target.parent
|
|
423
|
+
visit(path, node.target.parent, current_constant: name, namespace: namespace, nesting: nesting)
|
|
249
424
|
end
|
|
250
|
-
end
|
|
251
425
|
|
|
252
|
-
|
|
253
|
-
|
|
426
|
+
kind = builder_kind(node.value)
|
|
427
|
+
superclass = builder_superclass(node.value) if kind == :class
|
|
428
|
+
superclass_argument = node.value.arguments&.arguments&.first if kind == :class
|
|
429
|
+
static_superclass = superclass_argument && constant_node?(superclass_argument)
|
|
430
|
+
location = SourceLocation.from_prism(path, node.location)
|
|
431
|
+
@declaration_fallbacks << DeclarationFallback.new(
|
|
432
|
+
path,
|
|
433
|
+
name,
|
|
434
|
+
kind || :constant,
|
|
435
|
+
location,
|
|
436
|
+
nesting,
|
|
437
|
+
superclass,
|
|
438
|
+
static_superclass && SourceLocation.from_prism(path, superclass_argument.location),
|
|
439
|
+
!!(superclass && !static_superclass)
|
|
440
|
+
)
|
|
441
|
+
unless kind
|
|
442
|
+
return visit(path, node.value, current_constant: name, namespace: namespace, nesting: nesting)
|
|
443
|
+
end
|
|
254
444
|
|
|
255
|
-
|
|
445
|
+
@declaration_kinds[[normalize(name), path]] = kind
|
|
446
|
+
record_namespace(name, path, false, declared_as: name, location: location)
|
|
447
|
+
if kind == :class
|
|
448
|
+
if static_superclass
|
|
449
|
+
@superclass_fallbacks[[normalize(name), path]] = superclass
|
|
450
|
+
else
|
|
451
|
+
record_builder_superclass(name, path, node.value)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
block = node.value.block
|
|
456
|
+
return unless block.is_a?(Prism::BlockNode) && block.body
|
|
457
|
+
|
|
458
|
+
visit_constant_body(
|
|
459
|
+
path,
|
|
460
|
+
name,
|
|
461
|
+
block.body,
|
|
462
|
+
name.split('::'),
|
|
463
|
+
nesting: [name] + nesting,
|
|
464
|
+
forced_method_owner: name
|
|
465
|
+
)
|
|
256
466
|
end
|
|
257
467
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
468
|
+
def visit_singleton_class(path, node, current_constant:, namespace:, nesting:, visibility:,
|
|
469
|
+
forced_method_owner:)
|
|
470
|
+
target = singleton_target(node.expression, current_constant)
|
|
471
|
+
visit_constant_body(
|
|
472
|
+
path,
|
|
473
|
+
target,
|
|
474
|
+
node.body,
|
|
475
|
+
namespace,
|
|
476
|
+
nesting: nesting,
|
|
477
|
+
default_scope: :class,
|
|
478
|
+
forced_method_owner: forced_method_owner || target
|
|
479
|
+
)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def visit_constant_body(path, name, body, namespace, nesting:, default_scope: :instance,
|
|
483
|
+
forced_method_owner: nil)
|
|
265
484
|
return unless body
|
|
266
485
|
|
|
267
486
|
unless body.is_a?(Prism::StatementsNode)
|
|
268
|
-
return visit(
|
|
269
|
-
|
|
487
|
+
return visit(path, body, current_constant: name, namespace: namespace, nesting: nesting,
|
|
488
|
+
default_scope: default_scope, forced_method_owner: forced_method_owner)
|
|
270
489
|
end
|
|
271
490
|
|
|
272
491
|
visibility = :public
|
|
273
492
|
body.body.each do |statement|
|
|
274
493
|
if (modifier = visibility_modifier(statement))
|
|
275
|
-
visibility = apply_visibility_modifier(
|
|
276
|
-
|
|
494
|
+
visibility = apply_visibility_modifier(
|
|
495
|
+
path,
|
|
496
|
+
name,
|
|
497
|
+
statement,
|
|
498
|
+
namespace,
|
|
499
|
+
nesting,
|
|
500
|
+
visibility,
|
|
501
|
+
modifier,
|
|
502
|
+
default_scope,
|
|
503
|
+
forced_method_owner
|
|
504
|
+
)
|
|
277
505
|
else
|
|
278
|
-
visit(
|
|
279
|
-
|
|
506
|
+
visit(path, statement, current_constant: name, namespace: namespace, nesting: nesting,
|
|
507
|
+
visibility: visibility, default_scope: default_scope,
|
|
508
|
+
forced_method_owner: forced_method_owner)
|
|
280
509
|
end
|
|
281
510
|
end
|
|
282
511
|
end
|
|
283
512
|
|
|
284
|
-
def visit_def(
|
|
285
|
-
|
|
286
|
-
owner =
|
|
287
|
-
if owner && (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
513
|
+
def visit_def(path, node, current_constant:, namespace:, nesting:, visibility:, default_scope:,
|
|
514
|
+
forced_method_owner:)
|
|
515
|
+
owner = forced_method_owner || current_constant
|
|
516
|
+
if owner && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
517
|
+
scope = (node.receiver || default_scope == :class) ? :class : :instance
|
|
518
|
+
@method_fallbacks << MethodFallback.new(
|
|
519
|
+
path,
|
|
520
|
+
owner,
|
|
521
|
+
node.name,
|
|
522
|
+
scope,
|
|
523
|
+
SourceLocation.from_prism(path, node.location),
|
|
524
|
+
visibility,
|
|
525
|
+
prism_signatures(node)
|
|
526
|
+
)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
if forced_method_owner && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
530
|
+
scope = (node.receiver || default_scope == :class) ? :class : :instance
|
|
531
|
+
owner = Owner.new(forced_method_owner, scope)
|
|
532
|
+
@method_owner_overrides << MethodOwner.new(
|
|
533
|
+
path,
|
|
534
|
+
SourceLocation.from_prism(path, node.location),
|
|
535
|
+
owner
|
|
536
|
+
)
|
|
296
537
|
end
|
|
297
538
|
|
|
298
539
|
if node.name == :method_missing
|
|
299
|
-
|
|
540
|
+
@extra_edges << {
|
|
300
541
|
type: :dynamic_feature,
|
|
301
542
|
from_path: path,
|
|
302
|
-
from_constant: current_constant,
|
|
543
|
+
from_constant: forced_method_owner || current_constant,
|
|
303
544
|
to: 'method_missing',
|
|
304
545
|
location: SourceLocation.from_prism(path, node.location),
|
|
305
546
|
confidence: :unknown_due_to_dynamic_feature
|
|
306
|
-
|
|
547
|
+
}
|
|
307
548
|
end
|
|
308
549
|
|
|
309
|
-
visit_children(
|
|
550
|
+
visit_children(path, node, current_constant: forced_method_owner || current_constant,
|
|
551
|
+
namespace: namespace, nesting: nesting, default_scope: scope,
|
|
552
|
+
forced_method_owner: forced_method_owner)
|
|
310
553
|
end
|
|
311
554
|
|
|
312
|
-
def
|
|
313
|
-
|
|
314
|
-
return current_constant if node.receiver.is_a?(Prism::SelfNode)
|
|
315
|
-
return unless constant_node?(node.receiver)
|
|
316
|
-
|
|
317
|
-
name = constant_reference_name(node.receiver)
|
|
318
|
-
graph.resolve_constant_reference(name, current_constant, lexical_nesting: nesting) if name
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def visit_call(graph, path, node, current_constant:, namespace:, nesting:, visibility: :public,
|
|
322
|
-
default_scope: :instance)
|
|
555
|
+
def visit_call(path, node, current_constant:, namespace:, nesting:, visibility:, default_scope:,
|
|
556
|
+
forced_method_owner:)
|
|
323
557
|
unless node.message
|
|
324
|
-
return visit_children(
|
|
325
|
-
|
|
326
|
-
|
|
558
|
+
return visit_children(path, node, current_constant: current_constant, namespace: namespace,
|
|
559
|
+
nesting: nesting, visibility: visibility,
|
|
560
|
+
default_scope: default_scope, forced_method_owner: forced_method_owner)
|
|
327
561
|
end
|
|
328
562
|
|
|
329
563
|
message = node.message.to_sym
|
|
330
564
|
location = SourceLocation.from_prism(path, node.location)
|
|
565
|
+
owner = forced_method_owner || current_constant
|
|
566
|
+
@call_sites << CallSite.new(
|
|
567
|
+
path,
|
|
568
|
+
message,
|
|
569
|
+
owner,
|
|
570
|
+
receiver_kind(node),
|
|
571
|
+
constant_reference_name(node.receiver),
|
|
572
|
+
default_scope,
|
|
573
|
+
nesting,
|
|
574
|
+
location
|
|
575
|
+
)
|
|
331
576
|
|
|
332
577
|
if (one_shot = instantiates_and_invokes(node))
|
|
333
|
-
|
|
578
|
+
@extra_edges << {
|
|
334
579
|
type: :instantiates_and_invokes,
|
|
335
580
|
from_path: path,
|
|
336
|
-
from_constant:
|
|
581
|
+
from_constant: owner,
|
|
337
582
|
to: one_shot,
|
|
338
583
|
location: location
|
|
339
|
-
|
|
584
|
+
}
|
|
340
585
|
end
|
|
341
586
|
|
|
342
587
|
if (required = literal_require_argument(node))
|
|
343
|
-
|
|
588
|
+
@extra_edges << {
|
|
344
589
|
type: message == :require_relative ? :requires_relative : :requires,
|
|
345
590
|
from_path: path,
|
|
346
|
-
from_constant:
|
|
591
|
+
from_constant: owner,
|
|
347
592
|
to: required,
|
|
348
593
|
location: location
|
|
349
|
-
|
|
594
|
+
}
|
|
350
595
|
end
|
|
351
596
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (edge_type = MIXIN_MESSAGES[message])
|
|
355
|
-
constant_arguments(node).each do |constant_name|
|
|
356
|
-
if current_constant && (constant = graph.constants_named(current_constant).find do |candidate|
|
|
357
|
-
candidate.path == path
|
|
358
|
-
end)
|
|
359
|
-
constant.add_mixin(message, constant_name)
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
graph.add_edge(
|
|
363
|
-
type: edge_type,
|
|
364
|
-
from_path: path,
|
|
365
|
-
from_constant: current_constant,
|
|
366
|
-
to: constant_name,
|
|
367
|
-
location: location,
|
|
368
|
-
lexical_nesting: nesting
|
|
369
|
-
)
|
|
370
|
-
end
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
graph.add_edge(
|
|
374
|
-
type: :calls_named_method,
|
|
375
|
-
from_path: path,
|
|
376
|
-
from_constant: current_constant,
|
|
377
|
-
to: message,
|
|
378
|
-
location: location,
|
|
379
|
-
receiver: receiver_kind(node)
|
|
380
|
-
)
|
|
597
|
+
record_generated_method(path, node, message, owner, location, visibility, default_scope)
|
|
598
|
+
record_mixin_fallback(path, node, message, owner, location, nesting)
|
|
381
599
|
|
|
382
600
|
if DYNAMIC_MESSAGES.include?(message)
|
|
383
|
-
|
|
601
|
+
@extra_edges << {
|
|
384
602
|
type: :dynamic_feature,
|
|
385
603
|
from_path: path,
|
|
386
|
-
from_constant:
|
|
604
|
+
from_constant: owner,
|
|
387
605
|
to: message,
|
|
388
606
|
location: location,
|
|
389
607
|
confidence: :unknown_due_to_dynamic_feature
|
|
390
|
-
|
|
608
|
+
}
|
|
391
609
|
end
|
|
392
610
|
|
|
393
|
-
visit_children(
|
|
394
|
-
|
|
611
|
+
visit_children(path, node, current_constant: owner, namespace: namespace, nesting: nesting,
|
|
612
|
+
visibility: visibility, default_scope: default_scope,
|
|
613
|
+
forced_method_owner: forced_method_owner)
|
|
395
614
|
end
|
|
396
615
|
|
|
397
|
-
def
|
|
616
|
+
def record_constant_site(path, node, current_constant, nesting)
|
|
398
617
|
name = constant_reference_name(node)
|
|
399
618
|
return unless name
|
|
400
619
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
lexical_nesting: nesting
|
|
620
|
+
site = ConstantSite.new(
|
|
621
|
+
path,
|
|
622
|
+
name,
|
|
623
|
+
current_constant,
|
|
624
|
+
nesting,
|
|
625
|
+
SourceLocation.from_prism(path, node.location)
|
|
408
626
|
)
|
|
627
|
+
@constant_sites << site
|
|
628
|
+
@constant_sites_by_path[path] << site
|
|
409
629
|
end
|
|
410
630
|
|
|
411
|
-
def visit_children(
|
|
412
|
-
default_scope: :instance)
|
|
631
|
+
def visit_children(path, node, current_constant:, namespace:, nesting:, visibility: :public,
|
|
632
|
+
default_scope: :instance, forced_method_owner: nil)
|
|
413
633
|
node.child_nodes.compact.each do |child|
|
|
414
|
-
visit(
|
|
415
|
-
|
|
634
|
+
visit(path, child, current_constant: current_constant, namespace: namespace, nesting: nesting,
|
|
635
|
+
visibility: visibility, default_scope: default_scope,
|
|
636
|
+
forced_method_owner: forced_method_owner)
|
|
416
637
|
end
|
|
417
638
|
end
|
|
418
639
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
640
|
+
def record_namespace(name, path, namespace_only, declared_as:, location:)
|
|
641
|
+
@namespace_flags[[normalize(name), path]] << namespace_only
|
|
642
|
+
raw = normalize(declared_as)
|
|
643
|
+
return unless raw.include?('::') && raw != normalize(name)
|
|
644
|
+
|
|
645
|
+
@declaration_names << DeclarationName.new(path, raw, normalize(name), location)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def namespace_only_body?(body)
|
|
649
|
+
body.is_a?(Prism::StatementsNode) &&
|
|
650
|
+
!body.body.empty? &&
|
|
651
|
+
body.body.all? { |child| child.is_a?(Prism::ClassNode) || child.is_a?(Prism::ModuleNode) }
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def assigned_constant_name(node, namespace)
|
|
655
|
+
return qualified_constant_name(node.target, namespace) if node.is_a?(Prism::ConstantPathWriteNode)
|
|
656
|
+
|
|
657
|
+
namespace.empty? ? node.name.to_s : "#{namespace.join('::')}::#{node.name}"
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def builder_kind(value)
|
|
661
|
+
return unless value.is_a?(Prism::CallNode) && constant_node?(value.receiver)
|
|
662
|
+
|
|
663
|
+
receiver = constant_reference_name(value.receiver)&.sub(/\A::/, '')
|
|
664
|
+
BUILDER_KINDS[[receiver, value.message]]
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def class_superclass(node)
|
|
668
|
+
return unless node.superclass
|
|
669
|
+
return constant_reference_name(node.superclass) if constant_node?(node.superclass)
|
|
670
|
+
|
|
671
|
+
node.superclass.slice
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def record_builder_superclass(name, path, value)
|
|
675
|
+
argument = value.arguments&.arguments&.first
|
|
676
|
+
return if argument && constant_node?(argument)
|
|
677
|
+
|
|
678
|
+
receiver = constant_reference_name(value.receiver)&.sub(/\A::/, '')
|
|
679
|
+
return if receiver == 'Class'
|
|
680
|
+
|
|
681
|
+
@superclass_overrides[[normalize(name), path]] = "#{constant_reference_name(value.receiver)}.#{value.message}"
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def builder_superclass(value)
|
|
685
|
+
argument = value.arguments&.arguments&.first
|
|
686
|
+
return constant_reference_name(argument) if argument && constant_node?(argument)
|
|
687
|
+
|
|
688
|
+
receiver = constant_reference_name(value.receiver)&.sub(/\A::/, '')
|
|
689
|
+
"#{constant_reference_name(value.receiver)}.#{value.message}" unless receiver == 'Class'
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def singleton_target(expression, current_constant)
|
|
693
|
+
return current_constant if expression.is_a?(Prism::SelfNode)
|
|
694
|
+
|
|
695
|
+
constant_reference_name(expression) if constant_node?(expression)
|
|
696
|
+
end
|
|
430
697
|
|
|
431
|
-
# The [visibility, forced_scope] a bare visibility call sets, or nil when
|
|
432
|
-
# the node is not one. Only receiverless calls count, so +obj.private+ is
|
|
433
|
-
# ignored.
|
|
434
698
|
def visibility_modifier(node)
|
|
435
699
|
return unless node.is_a?(Prism::CallNode) && node.receiver.nil?
|
|
436
700
|
|
|
437
701
|
VISIBILITY_MODIFIERS[node.message&.to_sym]
|
|
438
702
|
end
|
|
439
703
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
# inline (+private def foo+) and symbol-list (+private :foo+) forms mark
|
|
443
|
-
# only the methods they name and leave the default unchanged.
|
|
444
|
-
def apply_visibility_modifier(graph, path, constant, node, namespace, current, spec, default_scope, nesting)
|
|
704
|
+
def apply_visibility_modifier(path, owner, node, namespace, nesting, current, spec, default_scope,
|
|
705
|
+
forced_method_owner)
|
|
445
706
|
visibility, forced_scope = spec
|
|
446
|
-
scope = forced_scope || default_scope
|
|
447
707
|
arguments = node.arguments&.arguments || []
|
|
448
708
|
definitions = arguments.select { |argument| argument.is_a?(Prism::DefNode) }
|
|
449
709
|
names = arguments.select { |argument| argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode) }
|
|
450
710
|
|
|
451
711
|
if definitions.any?
|
|
452
|
-
visit(
|
|
453
|
-
|
|
712
|
+
visit(path, node, current_constant: owner, namespace: namespace, nesting: nesting,
|
|
713
|
+
visibility: visibility, default_scope: default_scope,
|
|
714
|
+
forced_method_owner: forced_method_owner)
|
|
454
715
|
current
|
|
455
716
|
elsif names.any?
|
|
456
|
-
names
|
|
457
|
-
visit(
|
|
458
|
-
|
|
717
|
+
set_fallback_visibility(path, owner, names, forced_scope || default_scope, visibility)
|
|
718
|
+
visit(path, node, current_constant: owner, namespace: namespace, nesting: nesting,
|
|
719
|
+
visibility: current, default_scope: default_scope,
|
|
720
|
+
forced_method_owner: forced_method_owner)
|
|
459
721
|
current
|
|
460
722
|
elsif arguments.empty? && forced_scope.nil?
|
|
461
|
-
visit(
|
|
462
|
-
|
|
723
|
+
visit(path, node, current_constant: owner, namespace: namespace, nesting: nesting,
|
|
724
|
+
visibility: visibility, default_scope: default_scope,
|
|
725
|
+
forced_method_owner: forced_method_owner)
|
|
463
726
|
visibility
|
|
464
727
|
else
|
|
465
|
-
visit(
|
|
466
|
-
|
|
467
|
-
|
|
728
|
+
visit(path, node, current_constant: owner, namespace: namespace, nesting: nesting,
|
|
729
|
+
visibility: forced_scope.nil? ? visibility : current,
|
|
730
|
+
default_scope: default_scope, forced_method_owner: forced_method_owner)
|
|
468
731
|
current
|
|
469
732
|
end
|
|
470
733
|
end
|
|
471
734
|
|
|
472
|
-
def
|
|
473
|
-
return unless
|
|
474
|
-
return if node.receiver
|
|
735
|
+
def record_generated_method(path, node, message, owner, location, visibility, scope)
|
|
736
|
+
return unless owner && node.receiver.nil? && GENERATED_METHOD_MACROS.key?(message)
|
|
475
737
|
|
|
476
|
-
|
|
477
|
-
|
|
738
|
+
@generated_methods << GeneratedMethod.new(path, owner, message, node, location, visibility, scope)
|
|
739
|
+
end
|
|
478
740
|
|
|
479
|
-
|
|
741
|
+
def record_mixin_fallback(path, node, message, owner, location, nesting)
|
|
742
|
+
return unless MIXIN_EDGE_TYPES.key?(message)
|
|
743
|
+
|
|
744
|
+
constant_arguments(node).each do |target|
|
|
745
|
+
@mixin_fallbacks << MixinFallback.new(path, owner, message, target, location, nesting)
|
|
746
|
+
end
|
|
480
747
|
end
|
|
481
748
|
|
|
482
|
-
def
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
749
|
+
def apply_generated_methods(graph)
|
|
750
|
+
@generated_methods.each do |generated|
|
|
751
|
+
constant = graph.constants_named(generated.owner).find { |candidate| candidate.path == generated.path }
|
|
752
|
+
next unless constant
|
|
753
|
+
next if generated.message == :delegate && truthy_keyword_argument?(generated.node, :prefix)
|
|
754
|
+
|
|
755
|
+
generated_method_names(constant, generated.node, generated.message).each do |name|
|
|
756
|
+
accessors_for(generated.message, name).each do |accessor|
|
|
757
|
+
scope = generated.scope == :class ? :class : :instance
|
|
758
|
+
next if constant.method_definitions.any? do |definition|
|
|
759
|
+
definition.name == accessor && definition.scope == scope
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
adder = scope == :class ? :add_class_method : :add_instance_method
|
|
763
|
+
constant.public_send(
|
|
764
|
+
adder,
|
|
765
|
+
accessor,
|
|
766
|
+
location: generated.location,
|
|
767
|
+
visibility: generated.visibility
|
|
768
|
+
)
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
end
|
|
486
772
|
end
|
|
487
773
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
774
|
+
def apply_method_fallbacks(graph)
|
|
775
|
+
@method_fallbacks.each do |fallback|
|
|
776
|
+
constant = graph.constants_named(fallback.owner).find { |candidate| candidate.path == fallback.path }
|
|
777
|
+
next unless constant
|
|
778
|
+
next if constant.method_definitions.any? do |definition|
|
|
779
|
+
definition.name == fallback.name && definition.scope == fallback.scope
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
adder = fallback.scope == :class ? :add_class_method : :add_instance_method
|
|
783
|
+
constant.public_send(
|
|
784
|
+
adder,
|
|
785
|
+
fallback.name,
|
|
786
|
+
location: fallback.location,
|
|
787
|
+
visibility: fallback.visibility,
|
|
788
|
+
signatures: fallback.signatures
|
|
789
|
+
)
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def add_fallback_ancestry(graph, fallback)
|
|
794
|
+
return unless fallback.kind == :class && fallback.superclass && !fallback.dynamic_superclass
|
|
795
|
+
return if graph.edges.any? do |edge|
|
|
796
|
+
edge.type == :inherits_from && edge.from_path == fallback.path && edge.from_constant == fallback.name
|
|
797
|
+
end
|
|
493
798
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
799
|
+
graph.add_edge(
|
|
800
|
+
type: :inherits_from,
|
|
801
|
+
from_path: fallback.path,
|
|
802
|
+
from_constant: fallback.name,
|
|
803
|
+
to: fallback.superclass,
|
|
804
|
+
location: fallback.superclass_location || fallback.location,
|
|
805
|
+
lexical_nesting: fallback.nesting
|
|
806
|
+
)
|
|
807
|
+
end
|
|
497
808
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
809
|
+
def apply_mixin_fallbacks(graph)
|
|
810
|
+
@mixin_fallbacks.each do |fallback|
|
|
811
|
+
type = MIXIN_EDGE_TYPES.fetch(fallback.message)
|
|
812
|
+
next if graph.edges.any? do |edge|
|
|
813
|
+
edge.type == type && edge.from_path == fallback.path && edge.from_constant == fallback.owner &&
|
|
814
|
+
edge.to == fallback.target
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
constant = graph.constants_named(fallback.owner).find { |candidate| candidate.path == fallback.path }
|
|
818
|
+
constant&.add_mixin(fallback.message, fallback.target)
|
|
819
|
+
graph.add_edge(
|
|
820
|
+
type: type,
|
|
821
|
+
from_path: fallback.path,
|
|
822
|
+
from_constant: fallback.owner,
|
|
823
|
+
to: fallback.target,
|
|
824
|
+
location: fallback.location,
|
|
825
|
+
lexical_nesting: fallback.nesting
|
|
826
|
+
)
|
|
504
827
|
end
|
|
505
828
|
end
|
|
506
829
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
830
|
+
def set_fallback_visibility(path, owner, names, scope, visibility)
|
|
831
|
+
method_names = names.map { |name| name.unescaped.to_sym }
|
|
832
|
+
@method_fallbacks.map! do |fallback|
|
|
833
|
+
if fallback.path == path && fallback.owner == owner && fallback.scope == scope &&
|
|
834
|
+
method_names.include?(fallback.name)
|
|
835
|
+
fallback.with(visibility: visibility)
|
|
836
|
+
else
|
|
837
|
+
fallback
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
def accessors_for(message, name)
|
|
843
|
+
kinds = GENERATED_METHOD_MACROS.fetch(message)
|
|
844
|
+
accessors = []
|
|
845
|
+
accessors << name if kinds.include?(:reader)
|
|
846
|
+
accessors << :"#{name}=" if kinds.include?(:writer)
|
|
847
|
+
return accessors unless SINGULAR_ASSOCIATION_MACROS.include?(message)
|
|
848
|
+
|
|
849
|
+
accessors + SINGULAR_ASSOCIATION_HELPERS.map { |template| format(template, name).to_sym }
|
|
850
|
+
end
|
|
851
|
+
|
|
510
852
|
def generated_method_names(constant, node, message)
|
|
511
853
|
names = symbol_arguments(node)
|
|
512
|
-
return names unless message
|
|
513
|
-
|
|
514
|
-
superclass = constant.superclass.to_s.sub(/\A::/, '')
|
|
515
|
-
return names if superclass == 'ActiveSupport::CurrentAttributes'
|
|
854
|
+
return names unless SINGLE_NAME_MACROS.include?(message)
|
|
855
|
+
return names if message == :attribute && current_attributes?(constant)
|
|
516
856
|
|
|
517
857
|
names.first(1)
|
|
518
858
|
end
|
|
519
859
|
|
|
860
|
+
def current_attributes?(constant)
|
|
861
|
+
constant.superclass.to_s.sub(/\A::/, '') == 'ActiveSupport::CurrentAttributes'
|
|
862
|
+
end
|
|
863
|
+
|
|
520
864
|
def symbol_arguments(node)
|
|
521
865
|
node.arguments&.arguments&.filter_map do |argument|
|
|
522
866
|
argument.unescaped.to_sym if argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
@@ -534,6 +878,40 @@ module ArchSpec
|
|
|
534
878
|
end
|
|
535
879
|
end
|
|
536
880
|
|
|
881
|
+
def literal_require_argument(node)
|
|
882
|
+
return unless %i[require require_relative].include?(node.message.to_sym)
|
|
883
|
+
return if node.receiver
|
|
884
|
+
|
|
885
|
+
first_argument = node.arguments&.arguments&.first
|
|
886
|
+
first_argument.unescaped if first_argument.is_a?(Prism::StringNode)
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def constant_arguments(node)
|
|
890
|
+
node.arguments&.arguments&.filter_map do |argument|
|
|
891
|
+
constant_reference_name(argument) if constant_node?(argument)
|
|
892
|
+
end || []
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
def prism_signatures(node)
|
|
896
|
+
parameters = node.parameters
|
|
897
|
+
return [] unless parameters
|
|
898
|
+
|
|
899
|
+
keywords = parameters.keywords
|
|
900
|
+
forward = parameters.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
901
|
+
[
|
|
902
|
+
MethodSignature.new(
|
|
903
|
+
parameters.requireds.length + parameters.posts.length,
|
|
904
|
+
parameters.optionals.length,
|
|
905
|
+
!parameters.rest.nil? || forward,
|
|
906
|
+
keywords.grep(Prism::RequiredKeywordParameterNode).map(&:name),
|
|
907
|
+
keywords.grep(Prism::OptionalKeywordParameterNode).map(&:name),
|
|
908
|
+
!parameters.keyword_rest.nil? && !forward,
|
|
909
|
+
!parameters.block.nil? || forward,
|
|
910
|
+
forward
|
|
911
|
+
)
|
|
912
|
+
]
|
|
913
|
+
end
|
|
914
|
+
|
|
537
915
|
def receiver_kind(node)
|
|
538
916
|
receiver = node.receiver
|
|
539
917
|
return :none if receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
@@ -545,33 +923,20 @@ module ArchSpec
|
|
|
545
923
|
def instantiates_and_invokes(node)
|
|
546
924
|
receiver = node.receiver
|
|
547
925
|
return unless receiver.is_a?(Prism::CallNode) && receiver.message&.to_sym == :new
|
|
926
|
+
return unless constant_node?(receiver.receiver)
|
|
548
927
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
name = constant_reference_name(receiver_node)
|
|
553
|
-
return unless name
|
|
554
|
-
|
|
555
|
-
"#{name}##{node.message}"
|
|
928
|
+
name = constant_reference_name(receiver.receiver)
|
|
929
|
+
"#{name}##{node.message}" if name
|
|
556
930
|
end
|
|
557
931
|
|
|
558
|
-
# class Users::RolesController inside module Admin defines
|
|
559
|
-
# Admin::Users::RolesController, so compact paths join the namespace too.
|
|
560
932
|
def qualified_constant_name(node, namespace)
|
|
561
933
|
raw = constant_reference_name(node)
|
|
562
934
|
return unless raw
|
|
563
935
|
|
|
564
936
|
absolute = node.is_a?(Prism::ConstantPathNode) && node.parent.nil?
|
|
565
|
-
|
|
566
|
-
if absolute || namespace.empty?
|
|
567
|
-
raw
|
|
568
|
-
else
|
|
569
|
-
"#{namespace.join('::')}::#{raw}"
|
|
570
|
-
end
|
|
937
|
+
absolute || namespace.empty? ? raw : "#{namespace.join('::')}::#{raw}"
|
|
571
938
|
end
|
|
572
939
|
|
|
573
|
-
# nil when the path has dynamic parts (self.class::FOO): there is no
|
|
574
|
-
# static name to check against.
|
|
575
940
|
def constant_reference_name(node)
|
|
576
941
|
return unless constant_node?(node)
|
|
577
942
|
|
|
@@ -584,6 +949,19 @@ module ArchSpec
|
|
|
584
949
|
def constant_node?(node)
|
|
585
950
|
node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
586
951
|
end
|
|
952
|
+
|
|
953
|
+
def contains?(outer, inner)
|
|
954
|
+
position(outer.line, outer.column) <= position(inner.line, inner.column) &&
|
|
955
|
+
position(outer.end_line, outer.end_column) >= position(inner.end_line, inner.end_column)
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
def position(line, column)
|
|
959
|
+
(line << 32) + column
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
def normalize(name)
|
|
963
|
+
name.to_s.sub(/\A::/, '')
|
|
964
|
+
end
|
|
587
965
|
end
|
|
588
966
|
end
|
|
589
967
|
end
|