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
|
@@ -296,9 +296,10 @@ module ArchSpec
|
|
|
296
296
|
Rules::NamingRule.new(
|
|
297
297
|
source: nil,
|
|
298
298
|
selector: Rules::Naming::NameSelector.new(regex),
|
|
299
|
-
constraint: Rules::Naming::Forbidden.new
|
|
299
|
+
constraint: Rules::Naming::Forbidden.new,
|
|
300
300
|
scope: scope
|
|
301
|
-
)
|
|
301
|
+
),
|
|
302
|
+
because: reason
|
|
302
303
|
)
|
|
303
304
|
end
|
|
304
305
|
|
|
@@ -314,11 +315,14 @@ module ArchSpec
|
|
|
314
315
|
|
|
315
316
|
def define_component(dsl, name, selector)
|
|
316
317
|
if selector.is_a?(Hash)
|
|
318
|
+
selector = selector.to_h.transform_keys(&:to_sym)
|
|
317
319
|
dsl.component(
|
|
318
320
|
name,
|
|
319
321
|
in: selector[:in] || selector[:files],
|
|
322
|
+
except: selector[:except],
|
|
320
323
|
namespace: selector[:namespace],
|
|
321
|
-
constants: selector[:constants]
|
|
324
|
+
constants: selector[:constants],
|
|
325
|
+
descendants_of: selector[:descendants_of]
|
|
322
326
|
)
|
|
323
327
|
else
|
|
324
328
|
dsl.component(name, in: selector)
|
|
@@ -5,19 +5,23 @@ module ArchSpec
|
|
|
5
5
|
# explicit constant name. Created by ArchSpec::DSL::Context#component. The
|
|
6
6
|
# analyzer uses it to assign files and constants to the component.
|
|
7
7
|
class ComponentSpec
|
|
8
|
-
attr_reader :name, :file_patterns, :namespaces, :constants
|
|
8
|
+
attr_reader :name, :file_patterns, :exclude_patterns, :namespaces, :constants, :ancestor_names
|
|
9
9
|
|
|
10
|
-
def initialize(name, files: [], namespace: nil, constants: nil)
|
|
10
|
+
def initialize(name, files: [], except: [], namespace: nil, constants: nil, descendants_of: nil)
|
|
11
11
|
@name = name.to_sym
|
|
12
12
|
@file_patterns = Array(files).compact.map(&:to_s)
|
|
13
|
+
@exclude_patterns = Array(except).compact.map(&:to_s)
|
|
13
14
|
@namespaces = Array(namespace).compact.map { |value| normalize_constant(value) }
|
|
14
15
|
@constants = Array(constants).compact.map { |value| normalize_constant(value) }
|
|
16
|
+
@ancestor_names = Array(descendants_of).compact.map { |value| normalize_constant(value) }
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def merge!(other)
|
|
18
20
|
@file_patterns |= other.file_patterns
|
|
21
|
+
@exclude_patterns |= other.exclude_patterns
|
|
19
22
|
@namespaces |= other.namespaces
|
|
20
23
|
@constants |= other.constants
|
|
24
|
+
@ancestor_names |= other.ancestor_names
|
|
21
25
|
self
|
|
22
26
|
end
|
|
23
27
|
|
|
@@ -30,6 +34,13 @@ module ArchSpec
|
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
36
|
|
|
37
|
+
def matching_ancestor(name, graph)
|
|
38
|
+
return if ancestor_names.empty?
|
|
39
|
+
|
|
40
|
+
ancestors = graph.ancestor_names(name).first
|
|
41
|
+
ancestor_names.find { |ancestor| ancestors.include?(ancestor) }
|
|
42
|
+
end
|
|
43
|
+
|
|
33
44
|
private
|
|
34
45
|
|
|
35
46
|
def normalize_constant(value)
|
data/lib/archspec/diagnostic.rb
CHANGED
|
@@ -8,14 +8,28 @@ module ArchSpec
|
|
|
8
8
|
# these. Its #fingerprint is the stable id used to match todo entries and
|
|
9
9
|
# suppress specific findings.
|
|
10
10
|
class Diagnostic
|
|
11
|
-
attr_reader :rule, :message, :location, :evidence, :confidence
|
|
11
|
+
attr_reader :rule, :message, :location, :evidence, :confidence, :reason
|
|
12
12
|
|
|
13
|
-
def initialize(rule:, message:, location:, evidence:, confidence: :high)
|
|
13
|
+
def initialize(rule:, message:, location:, evidence:, confidence: :high, reason: nil)
|
|
14
14
|
@rule = rule
|
|
15
15
|
@message = message
|
|
16
16
|
@location = location
|
|
17
17
|
@evidence = evidence
|
|
18
18
|
@confidence = confidence
|
|
19
|
+
@reason = reason
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def with_reason(reason)
|
|
23
|
+
return self unless reason
|
|
24
|
+
|
|
25
|
+
self.class.new(
|
|
26
|
+
rule: rule,
|
|
27
|
+
message: message,
|
|
28
|
+
location: location,
|
|
29
|
+
evidence: evidence,
|
|
30
|
+
confidence: confidence,
|
|
31
|
+
reason: reason
|
|
32
|
+
)
|
|
19
33
|
end
|
|
20
34
|
|
|
21
35
|
# Line numbers stay out of the fingerprint so todo entries survive edits
|
|
@@ -29,7 +43,7 @@ module ArchSpec
|
|
|
29
43
|
end
|
|
30
44
|
|
|
31
45
|
def to_h(root:)
|
|
32
|
-
{
|
|
46
|
+
hash = {
|
|
33
47
|
id: fingerprint(root: root),
|
|
34
48
|
rule: rule,
|
|
35
49
|
message: message,
|
|
@@ -41,6 +55,8 @@ module ArchSpec
|
|
|
41
55
|
evidence: evidence,
|
|
42
56
|
confidence: confidence.to_s
|
|
43
57
|
}
|
|
58
|
+
hash[:reason] = reason if reason
|
|
59
|
+
hash
|
|
44
60
|
end
|
|
45
61
|
end
|
|
46
62
|
end
|
data/lib/archspec/dsl.rb
CHANGED
|
@@ -89,17 +89,26 @@ module ArchSpec
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
# Declares a component: a named set of files, matched by glob, namespace,
|
|
92
|
-
# or
|
|
92
|
+
# explicit constant, or semantic ancestry.
|
|
93
93
|
#
|
|
94
94
|
# component :services, in: "app/services/**/*.rb"
|
|
95
|
+
# component :workflows, in: "app/models/**/*.rb", except: "app/models/legacy/**/*.rb"
|
|
95
96
|
# component :billing, namespace: "Billing"
|
|
96
97
|
# component :legacy, constants: %w[OldReport OldExport]
|
|
98
|
+
# component :records, descendants_of: "ApplicationRecord"
|
|
97
99
|
#
|
|
98
100
|
# Returns an ArchSpec::DSL::ComponentProxy for attaching rules. The
|
|
99
101
|
# component is also available by name later in the file.
|
|
100
|
-
def component(name, in: nil, namespace: nil, constants: nil)
|
|
102
|
+
def component(name, in: nil, except: nil, namespace: nil, constants: nil, descendants_of: nil)
|
|
101
103
|
add_component(
|
|
102
|
-
ComponentSpec.new(
|
|
104
|
+
ComponentSpec.new(
|
|
105
|
+
name,
|
|
106
|
+
files: binding.local_variable_get(:in),
|
|
107
|
+
except: except,
|
|
108
|
+
namespace: namespace,
|
|
109
|
+
constants: constants,
|
|
110
|
+
descendants_of: descendants_of
|
|
111
|
+
)
|
|
103
112
|
)
|
|
104
113
|
ComponentProxy.new(self, name)
|
|
105
114
|
end
|
|
@@ -129,16 +138,16 @@ module ArchSpec
|
|
|
129
138
|
# no_cycles among: %i[billing catalog shared]
|
|
130
139
|
#
|
|
131
140
|
# Rule id: +dependencies.no_cycles+.
|
|
132
|
-
def no_cycles(among: nil)
|
|
141
|
+
def no_cycles(among: nil, because: nil)
|
|
133
142
|
DSL.assert_known_components!(self, among, for_rule: 'no_cycles') if among
|
|
134
|
-
add_rule(Rules::NoCyclesRule.new(among: among))
|
|
143
|
+
add_rule(Rules.with_reason(Rules::NoCyclesRule.new(among: among), because))
|
|
135
144
|
end
|
|
136
145
|
|
|
137
146
|
# Adds a custom rule object. A rule responds to +id+ and
|
|
138
147
|
# <tt>evaluate(graph)</tt>, returning ArchSpec::Diagnostic objects. Use
|
|
139
148
|
# this to extend ArchSpec with project-specific checks.
|
|
140
|
-
def rule(rule)
|
|
141
|
-
add_rule(rule)
|
|
149
|
+
def rule(rule, because: nil)
|
|
150
|
+
add_rule(Rules.with_reason(rule, because))
|
|
142
151
|
end
|
|
143
152
|
|
|
144
153
|
def method_missing(name, ...)
|
|
@@ -172,9 +181,9 @@ module ArchSpec
|
|
|
172
181
|
# controllers.can_only_use :models, :services
|
|
173
182
|
#
|
|
174
183
|
# Rule id: +dependencies.allow+.
|
|
175
|
-
def can_only_use(*targets)
|
|
184
|
+
def can_only_use(*targets, because: nil)
|
|
176
185
|
DSL.assert_known_components!(definition, targets, for_rule: "#{name}.can_only_use")
|
|
177
|
-
add_rule(Rules::AllowDependenciesRule.new(name, targets))
|
|
186
|
+
add_rule(Rules::AllowDependenciesRule.new(name, targets), because: because)
|
|
178
187
|
self
|
|
179
188
|
end
|
|
180
189
|
|
|
@@ -184,9 +193,9 @@ module ArchSpec
|
|
|
184
193
|
# models.cannot_use :controllers, :helpers
|
|
185
194
|
#
|
|
186
195
|
# Rule id: +dependencies.forbid+.
|
|
187
|
-
def cannot_use(*targets)
|
|
196
|
+
def cannot_use(*targets, because: nil)
|
|
188
197
|
DSL.assert_known_components!(definition, targets, for_rule: "#{name}.cannot_use")
|
|
189
|
-
add_rule(Rules::ForbidDependenciesRule.new(name, targets))
|
|
198
|
+
add_rule(Rules::ForbidDependenciesRule.new(name, targets), because: because)
|
|
190
199
|
self
|
|
191
200
|
end
|
|
192
201
|
|
|
@@ -197,26 +206,27 @@ module ArchSpec
|
|
|
197
206
|
# shared_kernel.can_only_be_used_by :billing, :catalog
|
|
198
207
|
#
|
|
199
208
|
# Rule id: +dependencies.consumers+.
|
|
200
|
-
def can_only_be_used_by(*consumers)
|
|
209
|
+
def can_only_be_used_by(*consumers, because: nil)
|
|
201
210
|
DSL.assert_known_components!(definition, consumers, for_rule: "#{name}.can_only_be_used_by")
|
|
202
|
-
add_rule(Rules::AllowedConsumersRule.new(name, consumers))
|
|
211
|
+
add_rule(Rules::AllowedConsumersRule.new(name, consumers), because: because)
|
|
203
212
|
self
|
|
204
213
|
end
|
|
205
214
|
|
|
206
215
|
# Forbids calling the named methods. By default any receiver matches, so
|
|
207
216
|
# this catches +record.update+ and +cache.update+ alike. Pass
|
|
208
|
-
# <tt>receiver: :none</tt> to match only bare, implicit-+self+ calls,
|
|
209
|
-
#
|
|
217
|
+
# <tt>receiver: :none</tt> to match only bare, implicit-+self+ calls, or a
|
|
218
|
+
# constant name to match that semantic class receiver and its descendants.
|
|
210
219
|
#
|
|
211
220
|
# queries.cannot_call :save, :update, :destroy
|
|
212
221
|
# services.cannot_call :render, :params, receiver: :none
|
|
222
|
+
# models.cannot_call :find_by_sql, receiver: "ActiveRecord::Base"
|
|
213
223
|
#
|
|
214
224
|
# A bare call to a method the component defines, inherits, or generates
|
|
215
225
|
# with +attr_*+, Rails +attribute+, or +delegate+ is treated as its own API
|
|
216
|
-
# and not flagged.
|
|
226
|
+
# and not flagged. Resolved method aliases are matched to their target.
|
|
217
227
|
# Rule id: +methods.forbid+.
|
|
218
|
-
def cannot_call(*methods, receiver: :any)
|
|
219
|
-
add_rule(Rules::CannotCallRule.new(name, methods, receiver: receiver))
|
|
228
|
+
def cannot_call(*methods, receiver: :any, because: nil)
|
|
229
|
+
add_rule(Rules::CannotCallRule.new(name, methods, receiver: receiver), because: because)
|
|
220
230
|
self
|
|
221
231
|
end
|
|
222
232
|
|
|
@@ -227,8 +237,8 @@ module ArchSpec
|
|
|
227
237
|
# models.cannot_define :call
|
|
228
238
|
#
|
|
229
239
|
# Rule id: +methods.define_forbid+.
|
|
230
|
-
def cannot_define(*methods)
|
|
231
|
-
add_rule(Rules::CannotDefineMethodRule.new(name, methods))
|
|
240
|
+
def cannot_define(*methods, because: nil)
|
|
241
|
+
add_rule(Rules::CannotDefineMethodRule.new(name, methods), because: because)
|
|
232
242
|
self
|
|
233
243
|
end
|
|
234
244
|
|
|
@@ -237,8 +247,8 @@ module ArchSpec
|
|
|
237
247
|
# toward plain methods over anonymous command objects.
|
|
238
248
|
#
|
|
239
249
|
# Rule id: +objects.instantiate_and_invoke_forbid+.
|
|
240
|
-
def cannot_instantiate_and_invoke
|
|
241
|
-
add_rule(Rules::CannotInstantiateAndInvokeRule.new(name))
|
|
250
|
+
def cannot_instantiate_and_invoke(because: nil)
|
|
251
|
+
add_rule(Rules::CannotInstantiateAndInvokeRule.new(name), because: because)
|
|
242
252
|
self
|
|
243
253
|
end
|
|
244
254
|
|
|
@@ -248,8 +258,8 @@ module ArchSpec
|
|
|
248
258
|
# models.cannot_reference_constants "ActionController", "ActionView"
|
|
249
259
|
#
|
|
250
260
|
# Rule id: +constants.forbid+.
|
|
251
|
-
def cannot_reference_constants(*constants)
|
|
252
|
-
add_rule(Rules::CannotReferenceConstantsRule.new(name, constants))
|
|
261
|
+
def cannot_reference_constants(*constants, because: nil)
|
|
262
|
+
add_rule(Rules::CannotReferenceConstantsRule.new(name, constants), because: because)
|
|
253
263
|
self
|
|
254
264
|
end
|
|
255
265
|
|
|
@@ -263,8 +273,11 @@ module ArchSpec
|
|
|
263
273
|
# +constants+ matches exact names, +namespace+ matches a name and its
|
|
264
274
|
# children. Code inside the component may still reach its own internals.
|
|
265
275
|
# Rule id: +dependencies.privacy+.
|
|
266
|
-
def public_api(*patterns, constants: nil, namespace: nil)
|
|
267
|
-
add_rule(
|
|
276
|
+
def public_api(*patterns, constants: nil, namespace: nil, because: nil)
|
|
277
|
+
add_rule(
|
|
278
|
+
Rules::PublicApiRule.new(name, files: patterns, constants: constants, namespaces: namespace),
|
|
279
|
+
because: because
|
|
280
|
+
)
|
|
268
281
|
self
|
|
269
282
|
end
|
|
270
283
|
|
|
@@ -276,8 +289,8 @@ module ArchSpec
|
|
|
276
289
|
# model_concerns.cannot_reference_includers
|
|
277
290
|
#
|
|
278
291
|
# Rule id: +concerns.independence+.
|
|
279
|
-
def cannot_reference_includers
|
|
280
|
-
add_rule(Rules::ConcernIndependenceRule.new(name))
|
|
292
|
+
def cannot_reference_includers(because: nil)
|
|
293
|
+
add_rule(Rules::ConcernIndependenceRule.new(name), because: because)
|
|
281
294
|
self
|
|
282
295
|
end
|
|
283
296
|
|
|
@@ -290,34 +303,48 @@ module ArchSpec
|
|
|
290
303
|
#
|
|
291
304
|
# Rule id: +components.empty+.
|
|
292
305
|
def must_be_empty(because: nil)
|
|
293
|
-
add_rule(Rules::MustBeEmptyRule.new(name, because: because)
|
|
306
|
+
add_rule(Rules::MustBeEmptyRule.new(name), because: because)
|
|
294
307
|
self
|
|
295
308
|
end
|
|
296
309
|
|
|
297
310
|
# Requires every class in the component to implement all the named
|
|
298
|
-
#
|
|
299
|
-
#
|
|
311
|
+
# methods. Instance methods are checked by default; pass
|
|
312
|
+
# <tt>scope: :class</tt> for the class side. Methods inherited from
|
|
313
|
+
# resolvable superclasses or mixins count. Optional +arity:+ and
|
|
314
|
+
# +keywords:+ constraints check whether each method accepts that call.
|
|
300
315
|
#
|
|
301
316
|
# commands.must_implement :perform
|
|
317
|
+
# commands.must_implement :call, arity: 1, keywords: :actor
|
|
318
|
+
# jobs.must_implement :perform_later, scope: :class
|
|
302
319
|
#
|
|
303
320
|
# Rule id: +protocol.must_implement+.
|
|
304
|
-
def must_implement(*methods)
|
|
321
|
+
def must_implement(*methods, scope: :instance, arity: nil, keywords: nil, because: nil)
|
|
305
322
|
raise Error, 'must_implement requires at least one method' if methods.flatten.compact.empty?
|
|
306
323
|
|
|
307
324
|
methods.each do |method_name|
|
|
308
|
-
add_rule(
|
|
325
|
+
add_rule(
|
|
326
|
+
Rules::MustImplementRule.new(
|
|
327
|
+
name,
|
|
328
|
+
method_name,
|
|
329
|
+
scope: scope,
|
|
330
|
+
arity: arity,
|
|
331
|
+
keywords: keywords
|
|
332
|
+
),
|
|
333
|
+
because: because
|
|
334
|
+
)
|
|
309
335
|
end
|
|
310
336
|
self
|
|
311
337
|
end
|
|
312
338
|
|
|
313
339
|
# Requires every class in the component to implement at least one of the
|
|
314
|
-
# named
|
|
340
|
+
# named methods. Useful when a protocol allows either name. Pass
|
|
341
|
+
# <tt>scope: :class</tt> to check the class side.
|
|
315
342
|
#
|
|
316
343
|
# commands.must_implement_one_of :perform, :call
|
|
317
344
|
#
|
|
318
345
|
# Rule id: +protocol.must_implement_one_of+.
|
|
319
|
-
def must_implement_one_of(*methods)
|
|
320
|
-
add_rule(Rules::MustImplementOneOfRule.new(name, methods))
|
|
346
|
+
def must_implement_one_of(*methods, scope: :instance, because: nil)
|
|
347
|
+
add_rule(Rules::MustImplementOneOfRule.new(name, methods, scope: scope), because: because)
|
|
321
348
|
self
|
|
322
349
|
end
|
|
323
350
|
|
|
@@ -338,13 +365,17 @@ module ArchSpec
|
|
|
338
365
|
|
|
339
366
|
private
|
|
340
367
|
|
|
341
|
-
def add_rule(rule)
|
|
368
|
+
def add_rule(rule, because: nil)
|
|
369
|
+
rule = Rules.with_reason(rule, because)
|
|
342
370
|
if rule.respond_to?(:merge_key)
|
|
343
371
|
existing = definition.rules.find do |candidate|
|
|
344
372
|
candidate.respond_to?(:merge_key) && candidate.merge_key == rule.merge_key
|
|
345
373
|
end
|
|
346
374
|
|
|
347
|
-
|
|
375
|
+
if existing
|
|
376
|
+
Rules.with_reason(existing, rule.archspec_because) if rule.respond_to?(:archspec_because)
|
|
377
|
+
return existing.merge!(rule) if existing.respond_to?(:merge!)
|
|
378
|
+
end
|
|
348
379
|
return existing if existing
|
|
349
380
|
end
|
|
350
381
|
|
|
@@ -28,7 +28,11 @@ module ArchSpec
|
|
|
28
28
|
print_parse_errors(output, style, file)
|
|
29
29
|
print_component_reasons(output, style, graph.component_assignment_reasons_for_path(path))
|
|
30
30
|
print_suppressions(output, style, file)
|
|
31
|
-
print_facts(output, style, graph.edges.select { |edge| edge.from_path == path })
|
|
31
|
+
print_facts(output, style, graph.edges.select { |edge| edge.from_path == path }, label: 'outgoing facts')
|
|
32
|
+
names = graph.constants_for_path(path).map(&:name).to_set
|
|
33
|
+
incoming = graph.dependency_edges.select { |edge| names.include?(graph.resolve_edge_constant(edge)) }
|
|
34
|
+
print_facts(output, style, incoming, label: 'incoming dependencies')
|
|
35
|
+
print_census(output, style, graph.analysis_census(path: path))
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
def explain_constant(output, style, graph, subject)
|
|
@@ -40,14 +44,29 @@ module ArchSpec
|
|
|
40
44
|
output.puts style.bold(constant.name)
|
|
41
45
|
output.puts
|
|
42
46
|
output.puts " #{style.note('kind:')} #{constant.kind}"
|
|
43
|
-
|
|
47
|
+
relative_location = "#{constant.location.relative_path(graph.root)}:#{constant.location.line}"
|
|
48
|
+
output.puts " #{style.note('file:')} #{relative_location}"
|
|
44
49
|
print_component_reasons(
|
|
45
50
|
output, style,
|
|
46
51
|
graph.component_assignment_reasons_for_constant(constant.name, path: constant.path)
|
|
47
52
|
)
|
|
48
53
|
output.puts " #{style.note('superclass:')} #{constant.superclass || '(none)'}"
|
|
54
|
+
ancestors, unresolved = graph.ancestor_names(constant.name)
|
|
55
|
+
resolved_ancestors = ancestors.empty? ? '(none)' : ancestors.to_a.join(', ')
|
|
56
|
+
output.puts " #{style.note('resolved ancestors:')} #{resolved_ancestors}"
|
|
57
|
+
unless unresolved.empty?
|
|
58
|
+
output.puts " #{style.note('unresolved ancestors:')} #{unresolved.to_a.sort.join(', ')}"
|
|
59
|
+
end
|
|
49
60
|
output.puts " #{style.note('instance methods:')} #{constant.instance_methods.to_a.sort.join(', ')}"
|
|
50
61
|
output.puts " #{style.note('class methods:')} #{constant.class_methods.to_a.sort.join(', ')}"
|
|
62
|
+
print_facts(
|
|
63
|
+
output,
|
|
64
|
+
style,
|
|
65
|
+
graph.edges.select { |edge| edge.from_constant == constant.name && edge.from_path == constant.path },
|
|
66
|
+
label: 'outgoing facts'
|
|
67
|
+
)
|
|
68
|
+
print_facts(output, style, graph.incoming_dependency_edges(constant.name), label: 'incoming dependencies')
|
|
69
|
+
print_census(output, style, graph.analysis_census(path: constant.path))
|
|
51
70
|
end
|
|
52
71
|
end
|
|
53
72
|
|
|
@@ -85,13 +104,13 @@ module ArchSpec
|
|
|
85
104
|
end
|
|
86
105
|
end
|
|
87
106
|
|
|
88
|
-
def print_facts(output, style, facts)
|
|
107
|
+
def print_facts(output, style, facts, label:)
|
|
89
108
|
if facts.empty?
|
|
90
|
-
output.puts " #{style.note(
|
|
109
|
+
output.puts " #{style.note("#{label}:")} (none)"
|
|
91
110
|
return
|
|
92
111
|
end
|
|
93
112
|
|
|
94
|
-
output.puts " #{style.note(
|
|
113
|
+
output.puts " #{style.note("#{label}:")}"
|
|
95
114
|
locations = facts.map { |edge| "#{edge.location.line}:#{edge.location.column}" }
|
|
96
115
|
in_gutters(locations) do |gutter, index|
|
|
97
116
|
edge = facts[index]
|
|
@@ -99,6 +118,15 @@ module ArchSpec
|
|
|
99
118
|
end
|
|
100
119
|
end
|
|
101
120
|
|
|
121
|
+
def print_census(output, style, census)
|
|
122
|
+
clauses = []
|
|
123
|
+
clauses << "#{census[:unresolved_constants]} unresolved constants" if census[:unresolved_constants].positive?
|
|
124
|
+
clauses << "#{census[:dynamic_features]} dynamic features" if census[:dynamic_features].positive?
|
|
125
|
+
clauses << "#{census[:unknown_receivers]} unknown receivers" if census[:unknown_receivers].positive?
|
|
126
|
+
census[:rubydex_diagnostics].each { |rule, count| clauses << "#{count} RubyDEX #{rule}" }
|
|
127
|
+
output.puts " #{style.note('analysis gaps:')} #{clauses.empty? ? '(none)' : clauses.join(', ')}"
|
|
128
|
+
end
|
|
129
|
+
|
|
102
130
|
# Yields each label right-justified to the widest one, with the frame
|
|
103
131
|
# gutter bar appended, so columns line up like the check output.
|
|
104
132
|
def in_gutters(labels)
|
|
@@ -28,6 +28,7 @@ module ArchSpec
|
|
|
28
28
|
if diagnostics.empty?
|
|
29
29
|
output.puts "ArchSpec passed: #{graph.files.size} files, #{graph.constants.size} constants, " \
|
|
30
30
|
"#{graph.edges.size} facts checked."
|
|
31
|
+
print_census(output, graph)
|
|
31
32
|
return
|
|
32
33
|
end
|
|
33
34
|
|
|
@@ -40,13 +41,15 @@ module ArchSpec
|
|
|
40
41
|
|
|
41
42
|
label = diagnostics.size == 1 ? 'architecture violation' : 'architecture violations'
|
|
42
43
|
output.puts style.bold("#{diagnostics.size} #{label} found.")
|
|
44
|
+
print_census(output, graph)
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
def print_diagnostic(output, style, graph, diagnostic, sources)
|
|
46
48
|
location = diagnostic.location
|
|
47
49
|
relative = location.relative_path(graph.root)
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
header = "#{style.severity('[error]')} #{style.bold(diagnostic.message)} #{style.faint("[#{diagnostic.rule}]")}"
|
|
52
|
+
output.puts header
|
|
50
53
|
output.puts
|
|
51
54
|
output.puts "#{relative}:#{location.line}:#{location.column}"
|
|
52
55
|
print_frame(output, style, location, sources[location.path])
|
|
@@ -55,6 +58,7 @@ module ArchSpec
|
|
|
55
58
|
output.puts
|
|
56
59
|
output.puts " #{style.note('note:')} #{note}"
|
|
57
60
|
end
|
|
61
|
+
output.puts " #{style.note('reason:')} #{diagnostic.reason}" if diagnostic.reason
|
|
58
62
|
output.puts
|
|
59
63
|
end
|
|
60
64
|
|
|
@@ -107,6 +111,20 @@ module ArchSpec
|
|
|
107
111
|
rescue SystemCallError
|
|
108
112
|
[]
|
|
109
113
|
end
|
|
114
|
+
|
|
115
|
+
def print_census(output, graph)
|
|
116
|
+
census = graph.analysis_census
|
|
117
|
+
clauses = []
|
|
118
|
+
if census[:unresolved_constants].positive?
|
|
119
|
+
clauses << "#{census[:unresolved_constants]} unresolved constant references"
|
|
120
|
+
end
|
|
121
|
+
clauses << "#{census[:dynamic_features]} dynamic features" if census[:dynamic_features].positive?
|
|
122
|
+
clauses << "#{census[:unknown_receivers]} calls with unknown receivers" if census[:unknown_receivers].positive?
|
|
123
|
+
census[:rubydex_diagnostics].each do |rule, count|
|
|
124
|
+
clauses << "#{count} RubyDEX #{rule} #{count == 1 ? 'diagnostic' : 'diagnostics'}"
|
|
125
|
+
end
|
|
126
|
+
output.puts "Analysis gaps: #{clauses.join(', ')}." unless clauses.empty?
|
|
127
|
+
end
|
|
110
128
|
end
|
|
111
129
|
end
|
|
112
130
|
end
|