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/model.rb
CHANGED
|
@@ -3,13 +3,53 @@
|
|
|
3
3
|
require 'pathname'
|
|
4
4
|
require 'set'
|
|
5
5
|
|
|
6
|
-
require_relative 'value_object'
|
|
7
|
-
|
|
8
6
|
module ArchSpec
|
|
9
|
-
ParseError =
|
|
10
|
-
|
|
7
|
+
ParseError = Data.define(:message, :location)
|
|
8
|
+
AnalysisDiagnostic = Data.define(:rule, :message, :location)
|
|
9
|
+
MethodSignature = Data.define(
|
|
10
|
+
:required,
|
|
11
|
+
:optional,
|
|
12
|
+
:rest,
|
|
13
|
+
:keywords,
|
|
14
|
+
:optional_keywords,
|
|
15
|
+
:keyword_rest,
|
|
16
|
+
:block,
|
|
17
|
+
:forward
|
|
18
|
+
) do
|
|
19
|
+
def accepts_arity?(arity)
|
|
20
|
+
return true if forward
|
|
21
|
+
|
|
22
|
+
maximum = rest ? Float::INFINITY : required + optional
|
|
23
|
+
arity >= required && arity <= maximum
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def accepts_keywords?(names)
|
|
27
|
+
return true if forward
|
|
11
28
|
|
|
12
|
-
|
|
29
|
+
names = names.to_set
|
|
30
|
+
return false unless keywords.all? { |name| names.include?(name) }
|
|
31
|
+
return true if keyword_rest
|
|
32
|
+
|
|
33
|
+
names.all? { |name| keywords.include?(name) || optional_keywords.include?(name) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def describe
|
|
37
|
+
positional =
|
|
38
|
+
if rest
|
|
39
|
+
"#{required}+ positional"
|
|
40
|
+
elsif optional.positive?
|
|
41
|
+
"#{required}..#{required + optional} positional"
|
|
42
|
+
else
|
|
43
|
+
"#{required} positional"
|
|
44
|
+
end
|
|
45
|
+
named = (keywords.map { |name| "#{name}:" } + optional_keywords.map { |name| "#{name}:?" }).join(', ')
|
|
46
|
+
[positional, ("keywords #{named}" unless named.empty?), ('**keywords' if keyword_rest), ('&block' if block),
|
|
47
|
+
('forwarding' if forward)].compact.join(', ')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
MethodDefinition = Data.define(:owner, :name, :scope, :location, :visibility, :signatures, :alias_target)
|
|
51
|
+
|
|
52
|
+
Suppression = Data.define(:rule, :start_line, :end_line, :reason) do
|
|
13
53
|
def matches?(diagnostic)
|
|
14
54
|
(rule.nil? || rule == diagnostic.rule) &&
|
|
15
55
|
diagnostic.location.line >= start_line &&
|
|
@@ -31,13 +71,14 @@ module ArchSpec
|
|
|
31
71
|
class ConstantNode
|
|
32
72
|
attr_reader :name, :kind, :path, :location, :instance_methods, :class_methods, :method_definitions, :mixins,
|
|
33
73
|
:nesting
|
|
34
|
-
attr_accessor :superclass
|
|
74
|
+
attr_accessor :superclass, :namespace_only
|
|
35
75
|
|
|
36
|
-
def initialize(name:, kind:, path:, location:, nesting: [])
|
|
76
|
+
def initialize(name:, kind:, path:, location:, nesting: [], namespace_only: false)
|
|
37
77
|
@name = name
|
|
38
78
|
@kind = kind
|
|
39
79
|
@path = path
|
|
40
80
|
@location = location
|
|
81
|
+
@namespace_only = namespace_only
|
|
41
82
|
@nesting = Array(nesting).dup.freeze
|
|
42
83
|
@instance_methods = Set.new
|
|
43
84
|
@class_methods = Set.new
|
|
@@ -57,14 +98,18 @@ module ArchSpec
|
|
|
57
98
|
kind == :module
|
|
58
99
|
end
|
|
59
100
|
|
|
60
|
-
def add_instance_method(name, location:, visibility: :public)
|
|
101
|
+
def add_instance_method(name, location:, visibility: :public, signatures: [], alias_target: nil)
|
|
61
102
|
instance_methods.add(name.to_sym)
|
|
62
|
-
method_definitions << MethodDefinition.new(
|
|
103
|
+
method_definitions << MethodDefinition.new(
|
|
104
|
+
self.name, name.to_sym, :instance, location, visibility, signatures, alias_target
|
|
105
|
+
)
|
|
63
106
|
end
|
|
64
107
|
|
|
65
|
-
def add_class_method(name, location:, visibility: :public)
|
|
108
|
+
def add_class_method(name, location:, visibility: :public, signatures: [], alias_target: nil)
|
|
66
109
|
class_methods.add(name.to_sym)
|
|
67
|
-
method_definitions << MethodDefinition.new(
|
|
110
|
+
method_definitions << MethodDefinition.new(
|
|
111
|
+
self.name, name.to_sym, :class, location, visibility, signatures, alias_target
|
|
112
|
+
)
|
|
68
113
|
end
|
|
69
114
|
|
|
70
115
|
def add_mixin(kind, name)
|
|
@@ -81,7 +126,7 @@ module ArchSpec
|
|
|
81
126
|
end
|
|
82
127
|
end
|
|
83
128
|
|
|
84
|
-
Edge =
|
|
129
|
+
Edge = Data.define(
|
|
85
130
|
:type,
|
|
86
131
|
:from_path,
|
|
87
132
|
:from_constant,
|
|
@@ -89,7 +134,11 @@ module ArchSpec
|
|
|
89
134
|
:location,
|
|
90
135
|
:confidence,
|
|
91
136
|
:receiver,
|
|
92
|
-
:lexical_nesting
|
|
137
|
+
:lexical_nesting,
|
|
138
|
+
:resolved_to,
|
|
139
|
+
:resolved_receiver,
|
|
140
|
+
:receiver_scope,
|
|
141
|
+
:resolved_method
|
|
93
142
|
) do
|
|
94
143
|
VERBS = {
|
|
95
144
|
references_constant: 'references',
|
|
@@ -151,7 +200,7 @@ module ArchSpec
|
|
|
151
200
|
|
|
152
201
|
RESOLVED_ROOTS = %w[Object BasicObject].freeze
|
|
153
202
|
|
|
154
|
-
attr_reader :root, :files, :constants, :edges, :components
|
|
203
|
+
attr_reader :root, :files, :constants, :edges, :components, :analysis_diagnostics
|
|
155
204
|
|
|
156
205
|
def initialize(root)
|
|
157
206
|
@root = File.expand_path(root)
|
|
@@ -160,6 +209,9 @@ module ArchSpec
|
|
|
160
209
|
@constants_by_name = Hash.new { |hash, key| hash[key] = [] }
|
|
161
210
|
@edges = []
|
|
162
211
|
@components = {}
|
|
212
|
+
@analysis_diagnostics = []
|
|
213
|
+
@effective_definition_cache = {}
|
|
214
|
+
@effective_method_cache = {}
|
|
163
215
|
end
|
|
164
216
|
|
|
165
217
|
def add_file(path:, parse_errors:, suppressions: [])
|
|
@@ -171,21 +223,50 @@ module ArchSpec
|
|
|
171
223
|
)
|
|
172
224
|
end
|
|
173
225
|
|
|
174
|
-
def add_constant(name:, kind:, path:, location:, nesting: [])
|
|
226
|
+
def add_constant(name:, kind:, path:, location:, nesting: [], namespace_only: false)
|
|
175
227
|
normalized = normalize_constant(name)
|
|
176
228
|
existing = @constants_by_name[normalized].find { |constant| constant.path == path && constant.kind == kind }
|
|
177
|
-
|
|
229
|
+
if existing
|
|
230
|
+
existing.namespace_only &&= namespace_only
|
|
231
|
+
return existing
|
|
232
|
+
end
|
|
178
233
|
|
|
179
|
-
constant = ConstantNode.new(name: normalized, kind: kind, path: path, location: location, nesting: nesting
|
|
234
|
+
constant = ConstantNode.new(name: normalized, kind: kind, path: path, location: location, nesting: nesting,
|
|
235
|
+
namespace_only: namespace_only)
|
|
180
236
|
constants << constant
|
|
181
237
|
@constants_by_name[normalized] << constant
|
|
182
238
|
constant
|
|
183
239
|
end
|
|
184
240
|
|
|
185
241
|
def add_edge(type:, from_path:, from_constant:, to:, location:, confidence: :high, receiver: nil,
|
|
186
|
-
lexical_nesting: nil
|
|
242
|
+
lexical_nesting: nil, resolved_to: nil, resolved_receiver: nil, receiver_scope: nil,
|
|
243
|
+
resolved_method: nil)
|
|
187
244
|
nesting = lexical_nesting&.map { |name| normalize_constant(name) }&.freeze
|
|
188
|
-
edges << Edge.new(
|
|
245
|
+
edges << Edge.new(
|
|
246
|
+
type,
|
|
247
|
+
from_path,
|
|
248
|
+
from_constant,
|
|
249
|
+
to.to_s,
|
|
250
|
+
location,
|
|
251
|
+
confidence,
|
|
252
|
+
receiver,
|
|
253
|
+
nesting,
|
|
254
|
+
resolved_to,
|
|
255
|
+
resolved_receiver,
|
|
256
|
+
receiver_scope,
|
|
257
|
+
resolved_method
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def add_analysis_diagnostic(rule:, message:, location:)
|
|
262
|
+
analysis_diagnostics << AnalysisDiagnostic.new(rule, message, location)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def set_namespace_only(name, path, value)
|
|
266
|
+
normalized = normalize_constant(name)
|
|
267
|
+
@constants_by_name[normalized].each do |constant|
|
|
268
|
+
constant.namespace_only = value if constant.path == path && (constant.class? || constant.module?)
|
|
269
|
+
end
|
|
189
270
|
end
|
|
190
271
|
|
|
191
272
|
def constants_named(name)
|
|
@@ -215,22 +296,36 @@ module ArchSpec
|
|
|
215
296
|
component_specs.each do |spec|
|
|
216
297
|
component = Component.new(spec.name)
|
|
217
298
|
files_matched_by_pattern = Set.new
|
|
299
|
+
excluded_files = spec.exclude_patterns.each_with_object(Set.new) do |pattern, matches|
|
|
300
|
+
each_matching_file(pattern) { |path| matches.add(path) }
|
|
301
|
+
end
|
|
218
302
|
|
|
219
303
|
spec.file_patterns.each do |pattern|
|
|
220
304
|
each_matching_file(pattern) do |path|
|
|
305
|
+
next if excluded_files.include?(path)
|
|
306
|
+
|
|
221
307
|
files_matched_by_pattern.add(path)
|
|
222
308
|
component.add_file(path, reason: "matched file pattern #{pattern}")
|
|
223
309
|
end
|
|
224
310
|
end
|
|
225
311
|
|
|
226
312
|
constants.each do |constant|
|
|
227
|
-
matched_file = files_matched_by_pattern.include?(constant.path)
|
|
313
|
+
matched_file = files_matched_by_pattern.include?(constant.path) && !defers_to_real_definition?(constant)
|
|
228
314
|
matched_constant = spec.matches_constant?(constant.name)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
315
|
+
matched_ancestor = spec.matching_ancestor(constant.name, self)
|
|
316
|
+
next unless matched_file || matched_constant || matched_ancestor
|
|
317
|
+
|
|
318
|
+
component.add_file(constant.path, reason: "defines #{constant.name}") if matched_constant || matched_ancestor
|
|
319
|
+
reason =
|
|
320
|
+
if matched_file
|
|
321
|
+
'defined in matched file'
|
|
322
|
+
elsif matched_ancestor
|
|
323
|
+
"descends from #{matched_ancestor}"
|
|
324
|
+
else
|
|
325
|
+
'matched namespace/constant selector'
|
|
326
|
+
end
|
|
232
327
|
component.add_constant(constant.name, path: constant.path,
|
|
233
|
-
reason:
|
|
328
|
+
reason: reason)
|
|
234
329
|
end
|
|
235
330
|
|
|
236
331
|
@components[component.name] = component
|
|
@@ -296,69 +391,137 @@ module ArchSpec
|
|
|
296
391
|
# appeared. This matters for compact class declarations and superclass
|
|
297
392
|
# expressions, where inferring scope from the finished class name is wrong.
|
|
298
393
|
def resolve_edge_constant(edge)
|
|
394
|
+
return normalize_constant(edge.resolved_to) if edge.resolved_to
|
|
395
|
+
|
|
299
396
|
resolve_constant_reference(edge.to, edge.from_constant, lexical_nesting: edge.lexical_nesting)
|
|
300
397
|
end
|
|
301
398
|
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
#
|
|
305
|
-
|
|
399
|
+
# Every resolvable ancestor in Ruby lookup order, plus names whose
|
|
400
|
+
# declarations are outside the analyzed source. The scope controls whether
|
|
401
|
+
# mixins come from include/prepend or extend; superclass traversal applies
|
|
402
|
+
# to both sides.
|
|
403
|
+
def ancestor_names(name, scope: :instance, visited: Set.new)
|
|
306
404
|
normalized = normalize_constant(name)
|
|
307
|
-
return [Set.new, Set.new] if visited.include?(normalized)
|
|
308
|
-
|
|
309
|
-
visited.add(normalized)
|
|
310
|
-
return [Set.new, Set.new] if RESOLVED_ROOTS.include?(normalized)
|
|
405
|
+
return [Set.new, Set.new] if visited.include?(normalized) || RESOLVED_ROOTS.include?(normalized)
|
|
311
406
|
|
|
407
|
+
visited = visited.dup.add(normalized)
|
|
312
408
|
nodes = constants_named(normalized)
|
|
313
409
|
return [Set.new, Set[normalized]] if nodes.empty?
|
|
314
410
|
|
|
315
|
-
|
|
411
|
+
ancestors = Set.new
|
|
316
412
|
unresolved = Set.new
|
|
317
|
-
|
|
318
413
|
nodes.each do |node|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
mixins = node.mixins[:include].to_a + node.mixins[:prepend].to_a
|
|
322
|
-
|
|
414
|
+
mixins = scope == :class ? node.mixins[:extend] : node.mixins[:prepend] | node.mixins[:include]
|
|
323
415
|
mixins.each do |ancestor|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
)
|
|
329
|
-
ancestor_methods, ancestor_unresolved = effective_instance_methods(resolved_name, visited)
|
|
330
|
-
methods.merge(ancestor_methods)
|
|
331
|
-
unresolved.merge(ancestor_unresolved)
|
|
416
|
+
resolved = resolve_constant_reference(ancestor, node.name, lexical_nesting: [node.name] + node.nesting)
|
|
417
|
+
ancestors.add(resolved)
|
|
418
|
+
nested, missing = ancestor_names(resolved, visited: visited)
|
|
419
|
+
ancestors.merge(nested)
|
|
420
|
+
unresolved.merge(missing)
|
|
332
421
|
end
|
|
333
422
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
423
|
+
next unless node.superclass
|
|
424
|
+
|
|
425
|
+
resolved = resolve_constant_reference(node.superclass, node.name, lexical_nesting: node.nesting)
|
|
426
|
+
ancestors.add(resolved)
|
|
427
|
+
nested, missing = ancestor_names(resolved, scope: scope, visited: visited)
|
|
428
|
+
ancestors.merge(nested)
|
|
429
|
+
unresolved.merge(missing)
|
|
340
430
|
end
|
|
341
431
|
|
|
342
|
-
[
|
|
432
|
+
[ancestors, unresolved]
|
|
343
433
|
end
|
|
344
434
|
|
|
345
|
-
def
|
|
346
|
-
|
|
347
|
-
|
|
435
|
+
def effective_instance_methods(name)
|
|
436
|
+
effective_methods(name, :instance)
|
|
437
|
+
end
|
|
348
438
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
next if source_components.empty?
|
|
439
|
+
def effective_class_methods(name)
|
|
440
|
+
effective_methods(name, :class)
|
|
441
|
+
end
|
|
353
442
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
443
|
+
# Method definitions visible on a constant, including resolvable ancestry.
|
|
444
|
+
# Extended modules contribute their instance methods to the class side.
|
|
445
|
+
def effective_method_definitions(name, scope, visited = Set.new)
|
|
446
|
+
normalized = normalize_constant(name)
|
|
447
|
+
key = [normalized, scope]
|
|
448
|
+
cached = @effective_definition_cache[key]
|
|
449
|
+
return cached if visited.empty? && cached
|
|
450
|
+
|
|
451
|
+
root = visited.empty?
|
|
452
|
+
return [[], Set.new] if visited.include?(key)
|
|
453
|
+
|
|
454
|
+
visited = visited.dup.add(key)
|
|
455
|
+
return [[], Set.new] if RESOLVED_ROOTS.include?(normalized)
|
|
456
|
+
|
|
457
|
+
nodes = constants_named(normalized)
|
|
458
|
+
return [[], Set[normalized]] if nodes.empty?
|
|
459
|
+
|
|
460
|
+
definitions = []
|
|
461
|
+
visible_names = Set.new
|
|
462
|
+
unresolved = Set.new
|
|
463
|
+
|
|
464
|
+
if scope == :instance
|
|
465
|
+
append_mixin_definitions(nodes, :prepend, definitions, visible_names, unresolved, visited)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
own = nodes.flat_map { |node| node.method_definitions.select { |definition| definition.scope == scope } }
|
|
469
|
+
append_visible_definitions(definitions, visible_names, own)
|
|
470
|
+
|
|
471
|
+
mixin_kind = scope == :class ? :extend : :include
|
|
472
|
+
append_mixin_definitions(nodes, mixin_kind, definitions, visible_names, unresolved, visited)
|
|
473
|
+
append_superclass_definitions(nodes, scope, definitions, visible_names, unresolved, visited)
|
|
474
|
+
|
|
475
|
+
result = [definitions, unresolved]
|
|
476
|
+
@effective_definition_cache[key] = result if root
|
|
477
|
+
result
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def resolve_method_alias(owner, name, scope, visited = Set.new)
|
|
481
|
+
name = name.to_sym
|
|
482
|
+
return if visited.include?(name)
|
|
483
|
+
|
|
484
|
+
definitions, = effective_method_definitions(owner, scope)
|
|
485
|
+
targets = definitions.filter_map do |definition|
|
|
486
|
+
definition.alias_target if definition.name == name
|
|
487
|
+
end.uniq
|
|
488
|
+
return unless targets.one?
|
|
489
|
+
|
|
490
|
+
target = targets.first
|
|
491
|
+
resolve_method_alias(owner, target, scope, visited.dup.add(name)) || target
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def effective_methods(name, scope)
|
|
495
|
+
key = [normalize_constant(name), scope]
|
|
496
|
+
@effective_method_cache[key] ||= begin
|
|
497
|
+
definitions, unresolved = effective_method_definitions(name, scope)
|
|
498
|
+
[definitions.map(&:name).to_set, unresolved]
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def incoming_dependency_edges(name)
|
|
503
|
+
normalized = normalize_constant(name)
|
|
504
|
+
dependency_edges.select { |edge| resolve_edge_constant(edge) == normalized }
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def analysis_census(path: nil)
|
|
508
|
+
scoped_edges = path ? edges.select { |edge| edge.from_path == path } : edges
|
|
509
|
+
scoped_diagnostics =
|
|
510
|
+
if path
|
|
511
|
+
analysis_diagnostics.select { |diagnostic| diagnostic.location.path == path }
|
|
512
|
+
else
|
|
513
|
+
analysis_diagnostics
|
|
358
514
|
end
|
|
515
|
+
unresolved = scoped_edges.select { |edge| DEPENDENCY_EDGE_TYPES.include?(edge.type) }.count do |edge|
|
|
516
|
+
constants_named(resolve_edge_constant(edge)).empty?
|
|
359
517
|
end
|
|
360
518
|
|
|
361
|
-
|
|
519
|
+
{
|
|
520
|
+
unresolved_constants: unresolved,
|
|
521
|
+
dynamic_features: scoped_edges.count { |edge| edge.type == :dynamic_feature },
|
|
522
|
+
unknown_receivers: scoped_edges.count { |edge| edge.type == :calls_named_method && edge.receiver == :other },
|
|
523
|
+
rubydex_diagnostics: scoped_diagnostics.group_by(&:rule).transform_values(&:size).sort.to_h
|
|
524
|
+
}
|
|
362
525
|
end
|
|
363
526
|
|
|
364
527
|
def component_assignment_reasons_for_path(path)
|
|
@@ -392,6 +555,44 @@ module ArchSpec
|
|
|
392
555
|
|
|
393
556
|
private
|
|
394
557
|
|
|
558
|
+
def append_mixin_definitions(nodes, kind, definitions, visible_names, unresolved, visited)
|
|
559
|
+
mixins = nodes.flat_map do |node|
|
|
560
|
+
node.mixins[kind].map { |ancestor| [node, ancestor] }
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
mixins.reverse_each do |node, ancestor|
|
|
564
|
+
resolved_name = resolve_constant_reference(
|
|
565
|
+
ancestor,
|
|
566
|
+
node.name,
|
|
567
|
+
lexical_nesting: [node.name] + node.nesting
|
|
568
|
+
)
|
|
569
|
+
inherited, missing = effective_method_definitions(resolved_name, :instance, visited)
|
|
570
|
+
append_visible_definitions(definitions, visible_names, inherited)
|
|
571
|
+
unresolved.merge(missing)
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def append_superclass_definitions(nodes, scope, definitions, visible_names, unresolved, visited)
|
|
576
|
+
nodes.filter_map(&:superclass).uniq.each do |superclass|
|
|
577
|
+
node = nodes.find { |candidate| candidate.superclass == superclass }
|
|
578
|
+
resolved_name = resolve_constant_reference(superclass, node.name, lexical_nesting: node.nesting)
|
|
579
|
+
inherited, missing = effective_method_definitions(resolved_name, scope, visited)
|
|
580
|
+
append_visible_definitions(definitions, visible_names, inherited)
|
|
581
|
+
unresolved.merge(missing)
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def append_visible_definitions(definitions, visible_names, candidates)
|
|
586
|
+
names = candidates.map(&:name).to_set - visible_names
|
|
587
|
+
definitions.concat(candidates.select { |definition| names.include?(definition.name) })
|
|
588
|
+
visible_names.merge(names)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def defers_to_real_definition?(constant)
|
|
592
|
+
constant.namespace_only &&
|
|
593
|
+
@constants_by_name[constant.name].any? { |other| !other.namespace_only }
|
|
594
|
+
end
|
|
595
|
+
|
|
395
596
|
def each_matching_file(pattern)
|
|
396
597
|
glob = File.absolute_path(pattern, root)
|
|
397
598
|
Dir.glob(glob).sort.each do |path|
|