archspec 0.4.0 → 1.0.0.rc1
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 +29 -8
- data/lib/archspec/analyzer.rb +217 -84
- data/lib/archspec/architectures.rb +130 -98
- data/lib/archspec/cli.rb +113 -105
- data/lib/archspec/definition.rb +1 -6
- data/lib/archspec/diagnostic.rb +2 -0
- data/lib/archspec/dsl.rb +50 -43
- data/lib/archspec/error.rb +7 -0
- data/lib/archspec/evaluator.rb +6 -8
- data/lib/archspec/formatters/explanation.rb +123 -0
- data/lib/archspec/formatters/style.rb +39 -0
- data/lib/archspec/formatters/text.rb +92 -8
- data/lib/archspec/model.rb +142 -46
- data/lib/archspec/rules/component_rules.rb +1 -1
- data/lib/archspec/rules/concern_rules.rb +3 -3
- data/lib/archspec/rules/cycle_rule.rb +3 -3
- data/lib/archspec/rules/dependency_rules.rb +11 -19
- data/lib/archspec/rules/naming_rules.rb +237 -0
- data/lib/archspec/rules/privacy_rule.rb +3 -3
- data/lib/archspec/rules/protocol_rules.rb +10 -7
- data/lib/archspec/source_location.rb +8 -2
- data/lib/archspec/todo.rb +17 -2
- data/lib/archspec/value_object.rb +4 -0
- data/lib/archspec/version.rb +1 -1
- data/lib/archspec.rb +10 -15
- metadata +6 -4
- data/lib/archspec/presets.rb +0 -14
- data/lib/archspec/rules/zeitwerk_rule.rb +0 -51
data/lib/archspec/model.rb
CHANGED
|
@@ -7,7 +7,7 @@ require_relative 'value_object'
|
|
|
7
7
|
|
|
8
8
|
module ArchSpec
|
|
9
9
|
ParseError = ValueObject.define(:message, :location)
|
|
10
|
-
MethodDefinition = ValueObject.define(:owner, :name, :scope, :location)
|
|
10
|
+
MethodDefinition = ValueObject.define(:owner, :name, :scope, :location, :visibility)
|
|
11
11
|
|
|
12
12
|
Suppression = ValueObject.define(:rule, :start_line, :end_line, :reason) do
|
|
13
13
|
def matches?(diagnostic)
|
|
@@ -18,26 +18,27 @@ module ArchSpec
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
class SourceFile
|
|
21
|
-
attr_reader :path, :relative_path, :
|
|
21
|
+
attr_reader :path, :relative_path, :parse_errors, :suppressions
|
|
22
22
|
|
|
23
|
-
def initialize(root:, path:,
|
|
23
|
+
def initialize(root:, path:, parse_errors:, suppressions:)
|
|
24
24
|
@path = path
|
|
25
25
|
@relative_path = Pathname(path).relative_path_from(Pathname(root)).to_s
|
|
26
|
-
@expected_constant = expected_constant
|
|
27
26
|
@parse_errors = parse_errors
|
|
28
27
|
@suppressions = suppressions
|
|
29
28
|
end
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
class ConstantNode
|
|
33
|
-
attr_reader :name, :kind, :path, :location, :instance_methods, :class_methods, :method_definitions, :mixins
|
|
32
|
+
attr_reader :name, :kind, :path, :location, :instance_methods, :class_methods, :method_definitions, :mixins,
|
|
33
|
+
:nesting
|
|
34
34
|
attr_accessor :superclass
|
|
35
35
|
|
|
36
|
-
def initialize(name:, kind:, path:, location:)
|
|
36
|
+
def initialize(name:, kind:, path:, location:, nesting: [])
|
|
37
37
|
@name = name
|
|
38
38
|
@kind = kind
|
|
39
39
|
@path = path
|
|
40
40
|
@location = location
|
|
41
|
+
@nesting = Array(nesting).dup.freeze
|
|
41
42
|
@instance_methods = Set.new
|
|
42
43
|
@class_methods = Set.new
|
|
43
44
|
@method_definitions = []
|
|
@@ -56,22 +57,58 @@ module ArchSpec
|
|
|
56
57
|
kind == :module
|
|
57
58
|
end
|
|
58
59
|
|
|
59
|
-
def add_instance_method(name, location:)
|
|
60
|
+
def add_instance_method(name, location:, visibility: :public)
|
|
60
61
|
instance_methods.add(name.to_sym)
|
|
61
|
-
method_definitions << MethodDefinition.new(self.name, name.to_sym, :instance, location)
|
|
62
|
+
method_definitions << MethodDefinition.new(self.name, name.to_sym, :instance, location, visibility)
|
|
62
63
|
end
|
|
63
64
|
|
|
64
|
-
def add_class_method(name, location:)
|
|
65
|
+
def add_class_method(name, location:, visibility: :public)
|
|
65
66
|
class_methods.add(name.to_sym)
|
|
66
|
-
method_definitions << MethodDefinition.new(self.name, name.to_sym, :class, location)
|
|
67
|
+
method_definitions << MethodDefinition.new(self.name, name.to_sym, :class, location, visibility)
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def add_mixin(kind, name)
|
|
70
71
|
mixins.fetch(kind).add(name)
|
|
71
72
|
end
|
|
73
|
+
|
|
74
|
+
# Rewrites the visibility of already-recorded definitions, for the
|
|
75
|
+
# <tt>private :foo, :bar</tt> form that names methods defined earlier.
|
|
76
|
+
def set_visibility(name, scope, visibility)
|
|
77
|
+
name = name.to_sym
|
|
78
|
+
method_definitions.map! do |definition|
|
|
79
|
+
definition.name == name && definition.scope == scope ? definition.with(visibility: visibility) : definition
|
|
80
|
+
end
|
|
81
|
+
end
|
|
72
82
|
end
|
|
73
83
|
|
|
74
|
-
Edge = ValueObject.define(
|
|
84
|
+
Edge = ValueObject.define(
|
|
85
|
+
:type,
|
|
86
|
+
:from_path,
|
|
87
|
+
:from_constant,
|
|
88
|
+
:to,
|
|
89
|
+
:location,
|
|
90
|
+
:confidence,
|
|
91
|
+
:receiver,
|
|
92
|
+
:lexical_nesting
|
|
93
|
+
) do
|
|
94
|
+
VERBS = {
|
|
95
|
+
references_constant: 'references',
|
|
96
|
+
inherits_from: 'inherits from',
|
|
97
|
+
includes: 'includes',
|
|
98
|
+
prepends: 'prepends',
|
|
99
|
+
extends: 'extends',
|
|
100
|
+
calls_named_method: 'calls',
|
|
101
|
+
instantiates_and_invokes: 'instantiates and invokes',
|
|
102
|
+
requires: 'requires',
|
|
103
|
+
requires_relative: 'requires',
|
|
104
|
+
dynamic_feature: 'uses dynamic feature'
|
|
105
|
+
}.freeze
|
|
106
|
+
|
|
107
|
+
# The edge type as prose, for diagnostics and explain output.
|
|
108
|
+
def verb
|
|
109
|
+
VERBS.fetch(type, type.to_s.tr('_', ' '))
|
|
110
|
+
end
|
|
111
|
+
end
|
|
75
112
|
|
|
76
113
|
class Component
|
|
77
114
|
attr_reader :name, :files, :constants, :file_reasons, :constant_reasons
|
|
@@ -80,6 +117,7 @@ module ArchSpec
|
|
|
80
117
|
@name = name.to_sym
|
|
81
118
|
@files = Set.new
|
|
82
119
|
@constants = Set.new
|
|
120
|
+
@constant_occurrences = Set.new
|
|
83
121
|
@file_reasons = Hash.new { |hash, key| hash[key] = Set.new }
|
|
84
122
|
@constant_reasons = Hash.new { |hash, key| hash[key] = Set.new }
|
|
85
123
|
end
|
|
@@ -89,10 +127,17 @@ module ArchSpec
|
|
|
89
127
|
file_reasons[path].add(reason) if reason
|
|
90
128
|
end
|
|
91
129
|
|
|
92
|
-
def add_constant(name, reason: nil)
|
|
130
|
+
def add_constant(name, path:, reason: nil)
|
|
93
131
|
constants.add(name)
|
|
132
|
+
@constant_occurrences.add([name, path])
|
|
94
133
|
constant_reasons[name].add(reason) if reason
|
|
95
134
|
end
|
|
135
|
+
|
|
136
|
+
def includes_constant?(name, path: nil)
|
|
137
|
+
return constants.include?(name) unless path
|
|
138
|
+
|
|
139
|
+
@constant_occurrences.include?([name, path])
|
|
140
|
+
end
|
|
96
141
|
end
|
|
97
142
|
|
|
98
143
|
class Graph
|
|
@@ -117,29 +162,30 @@ module ArchSpec
|
|
|
117
162
|
@components = {}
|
|
118
163
|
end
|
|
119
164
|
|
|
120
|
-
def add_file(path:,
|
|
165
|
+
def add_file(path:, parse_errors:, suppressions: [])
|
|
121
166
|
files[path] = SourceFile.new(
|
|
122
167
|
root: root,
|
|
123
168
|
path: path,
|
|
124
|
-
expected_constant: expected_constant,
|
|
125
169
|
parse_errors: parse_errors,
|
|
126
170
|
suppressions: suppressions
|
|
127
171
|
)
|
|
128
172
|
end
|
|
129
173
|
|
|
130
|
-
def add_constant(name:, kind:, path:, location:)
|
|
174
|
+
def add_constant(name:, kind:, path:, location:, nesting: [])
|
|
131
175
|
normalized = normalize_constant(name)
|
|
132
176
|
existing = @constants_by_name[normalized].find { |constant| constant.path == path && constant.kind == kind }
|
|
133
177
|
return existing if existing
|
|
134
178
|
|
|
135
|
-
constant = ConstantNode.new(name: normalized, kind: kind, path: path, location: location)
|
|
179
|
+
constant = ConstantNode.new(name: normalized, kind: kind, path: path, location: location, nesting: nesting)
|
|
136
180
|
constants << constant
|
|
137
181
|
@constants_by_name[normalized] << constant
|
|
138
182
|
constant
|
|
139
183
|
end
|
|
140
184
|
|
|
141
|
-
def add_edge(type:, from_path:, from_constant:, to:, location:, confidence: :high, receiver: nil
|
|
142
|
-
|
|
185
|
+
def add_edge(type:, from_path:, from_constant:, to:, location:, confidence: :high, receiver: nil,
|
|
186
|
+
lexical_nesting: nil)
|
|
187
|
+
nesting = lexical_nesting&.map { |name| normalize_constant(name) }&.freeze
|
|
188
|
+
edges << Edge.new(type, from_path, from_constant, to.to_s, location, confidence, receiver, nesting)
|
|
143
189
|
end
|
|
144
190
|
|
|
145
191
|
def constants_named(name)
|
|
@@ -154,7 +200,13 @@ module ArchSpec
|
|
|
154
200
|
component = components[name.to_sym]
|
|
155
201
|
return [] unless component
|
|
156
202
|
|
|
157
|
-
|
|
203
|
+
constants_for_component(name).flat_map(&:method_definitions)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Every method definition in the graph, across all constants. Used by
|
|
207
|
+
# project-wide naming rules that are not scoped to one component.
|
|
208
|
+
def method_definitions
|
|
209
|
+
constants.flat_map(&:method_definitions)
|
|
158
210
|
end
|
|
159
211
|
|
|
160
212
|
def assign_components(component_specs)
|
|
@@ -162,18 +214,22 @@ module ArchSpec
|
|
|
162
214
|
|
|
163
215
|
component_specs.each do |spec|
|
|
164
216
|
component = Component.new(spec.name)
|
|
217
|
+
files_matched_by_pattern = Set.new
|
|
165
218
|
|
|
166
219
|
spec.file_patterns.each do |pattern|
|
|
167
|
-
each_matching_file(pattern)
|
|
220
|
+
each_matching_file(pattern) do |path|
|
|
221
|
+
files_matched_by_pattern.add(path)
|
|
222
|
+
component.add_file(path, reason: "matched file pattern #{pattern}")
|
|
223
|
+
end
|
|
168
224
|
end
|
|
169
225
|
|
|
170
226
|
constants.each do |constant|
|
|
171
|
-
matched_file =
|
|
227
|
+
matched_file = files_matched_by_pattern.include?(constant.path)
|
|
172
228
|
matched_constant = spec.matches_constant?(constant.name)
|
|
173
229
|
next unless matched_file || matched_constant
|
|
174
230
|
|
|
175
231
|
component.add_file(constant.path, reason: "defines #{constant.name}") if matched_constant
|
|
176
|
-
component.add_constant(constant.name,
|
|
232
|
+
component.add_constant(constant.name, path: constant.path,
|
|
177
233
|
reason: matched_file ? 'defined in matched file' : 'matched namespace/constant selector')
|
|
178
234
|
end
|
|
179
235
|
|
|
@@ -187,14 +243,31 @@ module ArchSpec
|
|
|
187
243
|
end
|
|
188
244
|
end
|
|
189
245
|
|
|
190
|
-
def component_names_for_constant(name)
|
|
246
|
+
def component_names_for_constant(name, path: nil)
|
|
191
247
|
normalized = normalize_constant(name)
|
|
192
248
|
|
|
193
249
|
components.values.each_with_object(Set.new) do |component, names|
|
|
194
|
-
names.add(component.name) if component.
|
|
250
|
+
names.add(component.name) if component.includes_constant?(normalized, path: path)
|
|
195
251
|
end
|
|
196
252
|
end
|
|
197
253
|
|
|
254
|
+
# The edge's source as prose: its constant when known, otherwise the
|
|
255
|
+
# root-relative path of the file. Diagnostics use this so evidence never
|
|
256
|
+
# embeds an absolute path, which would make todo fingerprints
|
|
257
|
+
# machine-specific.
|
|
258
|
+
def edge_source_name(edge)
|
|
259
|
+
edge.from_constant || files[edge.from_path]&.relative_path || edge.from_path
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Components that own the source of an edge. Constant and namespace
|
|
263
|
+
# selectors stay precise when a file also defines unrelated constants;
|
|
264
|
+
# top-level edges fall back to the file assignment.
|
|
265
|
+
def source_components_for(edge)
|
|
266
|
+
return component_names_for_path(edge.from_path) unless edge.from_constant
|
|
267
|
+
|
|
268
|
+
component_names_for_constant(edge.from_constant, path: edge.from_path)
|
|
269
|
+
end
|
|
270
|
+
|
|
198
271
|
def dependency_edges
|
|
199
272
|
edges.select { |edge| DEPENDENCY_EDGE_TYPES.include?(edge.type) }
|
|
200
273
|
end
|
|
@@ -202,31 +275,30 @@ module ArchSpec
|
|
|
202
275
|
def target_components_for(edge)
|
|
203
276
|
return Set.new unless DEPENDENCY_EDGE_TYPES.include?(edge.type)
|
|
204
277
|
|
|
205
|
-
resolved =
|
|
278
|
+
resolved = resolve_edge_constant(edge)
|
|
206
279
|
constants_named(resolved).each_with_object(Set.new) do |constant, names|
|
|
207
|
-
names.merge(
|
|
208
|
-
names.merge(component_names_for_constant(constant.name))
|
|
280
|
+
names.merge(component_names_for_constant(resolved, path: constant.path))
|
|
209
281
|
end
|
|
210
282
|
end
|
|
211
283
|
|
|
212
|
-
def resolve_constant_reference(name, from_constant)
|
|
284
|
+
def resolve_constant_reference(name, from_constant = nil, lexical_nesting: nil)
|
|
285
|
+
absolute = name.to_s.start_with?('::')
|
|
213
286
|
normalized = normalize_constant(name)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if from_constant
|
|
217
|
-
namespace = normalize_constant(from_constant).split('::')
|
|
218
|
-
namespace.pop
|
|
219
|
-
|
|
220
|
-
until namespace.empty?
|
|
221
|
-
candidates << "#{namespace.join('::')}::#{normalized}"
|
|
222
|
-
namespace.pop
|
|
223
|
-
end
|
|
224
|
-
end
|
|
287
|
+
return normalized if absolute
|
|
225
288
|
|
|
289
|
+
scopes = lexical_nesting || inferred_nesting(from_constant)
|
|
290
|
+
candidates = scopes.map { |scope| "#{normalize_constant(scope)}::#{normalized}" }
|
|
226
291
|
candidates << normalized
|
|
227
292
|
candidates.find { |candidate| constants_named(candidate).any? } || normalized
|
|
228
293
|
end
|
|
229
294
|
|
|
295
|
+
# Resolves an edge with the lexical nesting captured where the reference
|
|
296
|
+
# appeared. This matters for compact class declarations and superclass
|
|
297
|
+
# expressions, where inferring scope from the finished class name is wrong.
|
|
298
|
+
def resolve_edge_constant(edge)
|
|
299
|
+
resolve_constant_reference(edge.to, edge.from_constant, lexical_nesting: edge.lexical_nesting)
|
|
300
|
+
end
|
|
301
|
+
|
|
230
302
|
# Instance methods a constant responds to, walking resolvable superclasses
|
|
231
303
|
# and include/prepend mixins. Returns [methods, unresolved ancestor names];
|
|
232
304
|
# a non-empty second element means the answer is incomplete.
|
|
@@ -246,11 +318,21 @@ module ArchSpec
|
|
|
246
318
|
nodes.each do |node|
|
|
247
319
|
methods.merge(node.instance_methods)
|
|
248
320
|
|
|
249
|
-
|
|
250
|
-
|
|
321
|
+
mixins = node.mixins[:include].to_a + node.mixins[:prepend].to_a
|
|
322
|
+
|
|
323
|
+
mixins.each do |ancestor|
|
|
324
|
+
resolved_name = resolve_constant_reference(
|
|
325
|
+
ancestor,
|
|
326
|
+
node.name,
|
|
327
|
+
lexical_nesting: [node.name] + node.nesting
|
|
328
|
+
)
|
|
329
|
+
ancestor_methods, ancestor_unresolved = effective_instance_methods(resolved_name, visited)
|
|
330
|
+
methods.merge(ancestor_methods)
|
|
331
|
+
unresolved.merge(ancestor_unresolved)
|
|
332
|
+
end
|
|
251
333
|
|
|
252
|
-
|
|
253
|
-
resolved_name = resolve_constant_reference(
|
|
334
|
+
if node.superclass
|
|
335
|
+
resolved_name = resolve_constant_reference(node.superclass, node.name, lexical_nesting: node.nesting)
|
|
254
336
|
ancestor_methods, ancestor_unresolved = effective_instance_methods(resolved_name, visited)
|
|
255
337
|
methods.merge(ancestor_methods)
|
|
256
338
|
unresolved.merge(ancestor_unresolved)
|
|
@@ -265,7 +347,7 @@ module ArchSpec
|
|
|
265
347
|
pairs = Set.new
|
|
266
348
|
|
|
267
349
|
dependency_edges.each do |edge|
|
|
268
|
-
source_components =
|
|
350
|
+
source_components = source_components_for(edge)
|
|
269
351
|
source_components &= allowed_sources unless allowed_sources.empty?
|
|
270
352
|
next if source_components.empty?
|
|
271
353
|
|
|
@@ -287,16 +369,23 @@ module ArchSpec
|
|
|
287
369
|
end
|
|
288
370
|
end
|
|
289
371
|
|
|
290
|
-
def component_assignment_reasons_for_constant(name)
|
|
372
|
+
def component_assignment_reasons_for_constant(name, path: nil)
|
|
291
373
|
normalized = normalize_constant(name)
|
|
292
374
|
|
|
293
375
|
components.values.each_with_object({}) do |component, reasons|
|
|
294
|
-
next unless component.
|
|
376
|
+
next unless component.includes_constant?(normalized, path: path)
|
|
295
377
|
|
|
296
378
|
reasons[component.name] = component.constant_reasons[normalized].to_a.sort
|
|
297
379
|
end
|
|
298
380
|
end
|
|
299
381
|
|
|
382
|
+
def constants_for_component(name)
|
|
383
|
+
component = components[name.to_sym]
|
|
384
|
+
return [] unless component
|
|
385
|
+
|
|
386
|
+
constants.select { |constant| component.includes_constant?(constant.name, path: constant.path) }
|
|
387
|
+
end
|
|
388
|
+
|
|
300
389
|
def suppressed?(diagnostic)
|
|
301
390
|
files[diagnostic.location.path]&.suppressions&.any? { |suppression| suppression.matches?(diagnostic) }
|
|
302
391
|
end
|
|
@@ -314,5 +403,12 @@ module ArchSpec
|
|
|
314
403
|
def normalize_constant(value)
|
|
315
404
|
value.to_s.sub(/\A::/, '')
|
|
316
405
|
end
|
|
406
|
+
|
|
407
|
+
def inferred_nesting(from_constant)
|
|
408
|
+
return [] unless from_constant
|
|
409
|
+
|
|
410
|
+
parts = normalize_constant(from_constant).split('::')
|
|
411
|
+
parts.length.downto(1).map { |length| parts.first(length).join('::') }
|
|
412
|
+
end
|
|
317
413
|
end
|
|
318
414
|
end
|
|
@@ -30,7 +30,7 @@ module ArchSpec
|
|
|
30
30
|
Diagnostic.new(
|
|
31
31
|
rule: id,
|
|
32
32
|
message: because ? "#{source} must stay empty: #{because}" : "#{source} must stay empty",
|
|
33
|
-
location: SourceLocation.
|
|
33
|
+
location: SourceLocation.point(path, 1, 1),
|
|
34
34
|
evidence: "#{relative} belongs to #{source}"
|
|
35
35
|
)
|
|
36
36
|
end
|
|
@@ -34,7 +34,7 @@ module ArchSpec
|
|
|
34
34
|
consumers = includers[edge.from_constant]
|
|
35
35
|
next if consumers.empty?
|
|
36
36
|
|
|
37
|
-
target = graph.
|
|
37
|
+
target = graph.resolve_edge_constant(edge)
|
|
38
38
|
includer = consumers.find { |name| target == name || target.start_with?("#{name}::") }
|
|
39
39
|
next unless includer
|
|
40
40
|
|
|
@@ -42,7 +42,7 @@ module ArchSpec
|
|
|
42
42
|
rule: id,
|
|
43
43
|
message: "#{edge.from_constant} must not reference its includer #{includer}",
|
|
44
44
|
location: edge.location,
|
|
45
|
-
evidence: "#{edge.from_constant} #{edge.
|
|
45
|
+
evidence: "#{edge.from_constant} #{edge.verb} #{target}"
|
|
46
46
|
)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -56,7 +56,7 @@ module ArchSpec
|
|
|
56
56
|
graph.edges.each do |edge|
|
|
57
57
|
next unless MIXIN_TYPES.include?(edge.type) && edge.from_constant
|
|
58
58
|
|
|
59
|
-
mod = graph.
|
|
59
|
+
mod = graph.resolve_edge_constant(edge)
|
|
60
60
|
map[mod].add(edge.from_constant) unless mod == edge.from_constant
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module ArchSpec
|
|
4
4
|
module Rules
|
|
5
|
-
# Backs ArchSpec::DSL::Context#no_cycles
|
|
5
|
+
# Backs ArchSpec::DSL::Context#no_cycles. Flags dependency cycles between
|
|
6
6
|
# components, reporting each cycle once in a canonical order.
|
|
7
7
|
class NoCyclesRule
|
|
8
8
|
attr_reader :components
|
|
@@ -17,7 +17,7 @@ module ArchSpec
|
|
|
17
17
|
|
|
18
18
|
def evaluate(graph)
|
|
19
19
|
cycles(graph).map do |cycle|
|
|
20
|
-
location = first_location_for_cycle(graph, cycle) || SourceLocation.
|
|
20
|
+
location = first_location_for_cycle(graph, cycle) || SourceLocation.point(graph.root, 1, 1)
|
|
21
21
|
Diagnostic.new(
|
|
22
22
|
rule: id,
|
|
23
23
|
message: "component dependency cycle: #{cycle.join(' -> ')}",
|
|
@@ -74,7 +74,7 @@ module ArchSpec
|
|
|
74
74
|
def first_location_for_cycle(graph, cycle)
|
|
75
75
|
source, target = cycle
|
|
76
76
|
graph.dependency_edges.find do |edge|
|
|
77
|
-
graph.
|
|
77
|
+
graph.source_components_for(edge).include?(source) &&
|
|
78
78
|
graph.target_components_for(edge).include?(target)
|
|
79
79
|
end&.location
|
|
80
80
|
end
|
|
@@ -29,19 +29,11 @@ module ArchSpec
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
31
|
def relevant_edges(graph)
|
|
32
|
-
graph.dependency_edges.select { |edge| graph.
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def target_components(graph, edge)
|
|
36
|
-
graph.target_components_for(edge)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def edge_target(edge)
|
|
40
|
-
edge.to
|
|
32
|
+
graph.dependency_edges.select { |edge| graph.source_components_for(edge).include?(source) }
|
|
41
33
|
end
|
|
42
34
|
end
|
|
43
35
|
|
|
44
|
-
# Backs ArchSpec::DSL::ComponentProxy#
|
|
36
|
+
# Backs ArchSpec::DSL::ComponentProxy#can_only_use. Flags references from the
|
|
45
37
|
# source to any component outside its allowlist.
|
|
46
38
|
class AllowDependenciesRule < DependencyRule
|
|
47
39
|
def id
|
|
@@ -50,14 +42,14 @@ module ArchSpec
|
|
|
50
42
|
|
|
51
43
|
def evaluate(graph)
|
|
52
44
|
relevant_edges(graph).flat_map do |edge|
|
|
53
|
-
|
|
45
|
+
graph.target_components_for(edge).filter_map do |target|
|
|
54
46
|
next if target == source || targets.include?(target)
|
|
55
47
|
|
|
56
48
|
Diagnostic.new(
|
|
57
49
|
rule: id,
|
|
58
50
|
message: "#{source} may not depend on #{target}",
|
|
59
51
|
location: edge.location,
|
|
60
|
-
evidence: "#{
|
|
52
|
+
evidence: "#{graph.edge_source_name(edge)} #{edge.verb} #{edge.to}"
|
|
61
53
|
)
|
|
62
54
|
end
|
|
63
55
|
end
|
|
@@ -73,14 +65,14 @@ module ArchSpec
|
|
|
73
65
|
|
|
74
66
|
def evaluate(graph)
|
|
75
67
|
relevant_edges(graph).flat_map do |edge|
|
|
76
|
-
forbidden =
|
|
68
|
+
forbidden = graph.target_components_for(edge) & targets
|
|
77
69
|
|
|
78
70
|
forbidden.map do |target|
|
|
79
71
|
Diagnostic.new(
|
|
80
72
|
rule: id,
|
|
81
73
|
message: "#{source} must not depend on #{target}",
|
|
82
74
|
location: edge.location,
|
|
83
|
-
evidence: "#{
|
|
75
|
+
evidence: "#{graph.edge_source_name(edge)} #{edge.verb} #{edge.to}"
|
|
84
76
|
)
|
|
85
77
|
end
|
|
86
78
|
end
|
|
@@ -115,7 +107,7 @@ module ArchSpec
|
|
|
115
107
|
graph.dependency_edges.flat_map do |edge|
|
|
116
108
|
next [] unless graph.target_components_for(edge).include?(source)
|
|
117
109
|
|
|
118
|
-
offenders = graph.
|
|
110
|
+
offenders = graph.source_components_for(edge).reject do |component|
|
|
119
111
|
component == source || consumers.include?(component)
|
|
120
112
|
end
|
|
121
113
|
|
|
@@ -124,7 +116,7 @@ module ArchSpec
|
|
|
124
116
|
rule: id,
|
|
125
117
|
message: message_for(offender),
|
|
126
118
|
location: edge.location,
|
|
127
|
-
evidence: "#{
|
|
119
|
+
evidence: "#{graph.edge_source_name(edge)} #{edge.verb} #{edge.to}"
|
|
128
120
|
)
|
|
129
121
|
end
|
|
130
122
|
end
|
|
@@ -164,16 +156,16 @@ module ArchSpec
|
|
|
164
156
|
|
|
165
157
|
def evaluate(graph)
|
|
166
158
|
graph.dependency_edges.filter_map do |edge|
|
|
167
|
-
next unless graph.
|
|
159
|
+
next unless graph.source_components_for(edge).include?(source)
|
|
168
160
|
|
|
169
|
-
referenced = graph.
|
|
161
|
+
referenced = graph.resolve_edge_constant(edge)
|
|
170
162
|
next unless constants.any? { |constant| referenced == constant || referenced.start_with?("#{constant}::") }
|
|
171
163
|
|
|
172
164
|
Diagnostic.new(
|
|
173
165
|
rule: id,
|
|
174
166
|
message: "#{source} must not reference #{referenced}",
|
|
175
167
|
location: edge.location,
|
|
176
|
-
evidence: "#{
|
|
168
|
+
evidence: "#{graph.edge_source_name(edge)} #{edge.verb} #{edge.to}"
|
|
177
169
|
)
|
|
178
170
|
end
|
|
179
171
|
end
|