archspec 0.1.0.pre1 → 0.1.0.pre2
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 +10 -0
- data/exe/archspec +3 -2
- data/lib/archspec/analyzer.rb +32 -22
- data/lib/archspec/architectures.rb +18 -16
- data/lib/archspec/baseline.rb +11 -10
- data/lib/archspec/cli.rb +40 -37
- data/lib/archspec/component_spec.rb +3 -1
- data/lib/archspec/definition.rb +12 -10
- data/lib/archspec/diagnostic.rb +3 -1
- data/lib/archspec/dsl.rb +9 -2
- data/lib/archspec/evaluator.rb +7 -2
- data/lib/archspec/formatters/json.rb +4 -2
- data/lib/archspec/formatters/text.rb +4 -2
- data/lib/archspec/model.rb +8 -6
- data/lib/archspec/presets.rb +21 -0
- data/lib/archspec/rules/component_rules.rb +38 -0
- data/lib/archspec/rules/cycle_rule.rb +9 -7
- data/lib/archspec/rules/dependency_rules.rb +5 -5
- data/lib/archspec/rules/protocol_rules.rb +10 -8
- data/lib/archspec/rules/zeitwerk_rule.rb +4 -2
- data/lib/archspec/source_location.rb +3 -1
- data/lib/archspec/version.rb +3 -1
- data/lib/archspec.rb +23 -20
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3056236344cd4f0ba0b95312353e866c6186313878d0be016f96191e04bd011
|
|
4
|
+
data.tar.gz: f8fbeea8100183e9258206e227700fcf1a8a6027e53bd57970f495be29addbc7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20640ab2eb6f87cbd911f5270c3b2b6af99ea7841c3ead3c6c5aee51601bafcf4c5e835867a749edf1e59a89100a345693eda407a69f850b7b7ceed2d556adcb
|
|
7
|
+
data.tar.gz: 2369c0d26ba6c88fb13d0e7e837bbb8fadffc4391a5ccc7239b59f895683d22c79079a9f6d4f1034aff6882b5c1ab9bf21d68c2f42d0d95c140950f657dd8cb3
|
data/README.md
CHANGED
|
@@ -39,6 +39,15 @@ ArchSpec.define "Application architecture" do
|
|
|
39
39
|
end
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
Go vanilla, 37signals style — rich models, no service objects:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
ArchSpec.define "Application architecture" do
|
|
46
|
+
root "."
|
|
47
|
+
preset :vanilla_rails
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
42
51
|
Add layers when the app has a clear direction of dependencies:
|
|
43
52
|
|
|
44
53
|
```ruby
|
|
@@ -124,6 +133,7 @@ end
|
|
|
124
133
|
- **Protocols:** required methods such as `resolve`, `perform`, or project-specific interfaces
|
|
125
134
|
- **Objects:** rules against one-shot `Something.new(...).whatever` command objects
|
|
126
135
|
- **Zeitwerk names:** conventional file names defining the expected constants
|
|
136
|
+
- **Empty components:** directories that must stay empty, like `app/services` in vanilla Rails
|
|
127
137
|
- **Suppressions:** narrow local exceptions with a reason
|
|
128
138
|
|
|
129
139
|
## Installation
|
data/exe/archspec
CHANGED
data/lib/archspec/analyzer.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prism'
|
|
4
|
+
require 'pathname'
|
|
4
5
|
|
|
5
6
|
module ArchSpec
|
|
6
7
|
module Analyzer
|
|
@@ -34,7 +35,7 @@ module ArchSpec
|
|
|
34
35
|
definition.analysis_patterns.flat_map do |pattern|
|
|
35
36
|
Dir.glob(File.absolute_path(pattern, root))
|
|
36
37
|
end.select do |path|
|
|
37
|
-
File.file?(path) && path.end_with?(
|
|
38
|
+
File.file?(path) && path.end_with?('.rb')
|
|
38
39
|
end.map do |path|
|
|
39
40
|
File.expand_path(path)
|
|
40
41
|
end.uniq.reject do |path|
|
|
@@ -58,6 +59,8 @@ module ArchSpec
|
|
|
58
59
|
Regexp.last_match(1)
|
|
59
60
|
when %r{\Alib/(.+)\.rb\z}
|
|
60
61
|
Regexp.last_match(1)
|
|
62
|
+
when %r{\A(?:packs|engines)/[^/]+/app/[^/]+/concerns/(.+)\.rb\z}
|
|
63
|
+
Regexp.last_match(1)
|
|
61
64
|
when %r{\A(?:packs|engines)/[^/]+/app/[^/]+/(.+)\.rb\z}
|
|
62
65
|
Regexp.last_match(1)
|
|
63
66
|
else
|
|
@@ -68,9 +71,9 @@ module ArchSpec
|
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
def camelize_path(path)
|
|
71
|
-
path.split(
|
|
72
|
-
part.split(
|
|
73
|
-
end.join(
|
|
74
|
+
path.split('/').map do |part|
|
|
75
|
+
part.split('_').map { |word| word[0] ? word[0].upcase + word[1..] : word }.join
|
|
76
|
+
end.join('::')
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
def suppressions_for(comments)
|
|
@@ -94,7 +97,7 @@ module ArchSpec
|
|
|
94
97
|
active = Hash.new { |hash, key| hash[key] = [] }
|
|
95
98
|
|
|
96
99
|
sorted_comments(comments).each do |comment|
|
|
97
|
-
text = comment.slice.sub(/\A#\s?/,
|
|
100
|
+
text = comment.slice.sub(/\A#\s?/, '').strip
|
|
98
101
|
line = comment.location.start_line
|
|
99
102
|
|
|
100
103
|
if (match = text.match(DISABLE_PATTERN))
|
|
@@ -102,9 +105,9 @@ module ArchSpec
|
|
|
102
105
|
rule = normalize_rule(rule)
|
|
103
106
|
|
|
104
107
|
case mode
|
|
105
|
-
when
|
|
108
|
+
when 'line'
|
|
106
109
|
suppressions << Suppression.new(rule, line, line, reason)
|
|
107
|
-
when
|
|
110
|
+
when 'next-line'
|
|
108
111
|
suppressions << Suppression.new(rule, line + 1, line + 1, reason)
|
|
109
112
|
else
|
|
110
113
|
active[rule] << [line + 1, reason]
|
|
@@ -134,7 +137,7 @@ module ArchSpec
|
|
|
134
137
|
end
|
|
135
138
|
|
|
136
139
|
def normalize_rule(rule)
|
|
137
|
-
return nil if rule.nil? || rule ==
|
|
140
|
+
return nil if rule.nil? || rule == '*'
|
|
138
141
|
|
|
139
142
|
rule.downcase
|
|
140
143
|
end
|
|
@@ -205,7 +208,7 @@ module ArchSpec
|
|
|
205
208
|
)
|
|
206
209
|
end
|
|
207
210
|
|
|
208
|
-
visit(graph, path, node.body, current_constant: constant.name, namespace: constant.name.split(
|
|
211
|
+
visit(graph, path, node.body, current_constant: constant.name, namespace: constant.name.split('::'))
|
|
209
212
|
end
|
|
210
213
|
|
|
211
214
|
def visit_module(graph, path, node, current_constant:, namespace:)
|
|
@@ -217,11 +220,13 @@ module ArchSpec
|
|
|
217
220
|
location: SourceLocation.from_prism(path, node.location)
|
|
218
221
|
)
|
|
219
222
|
|
|
220
|
-
visit(graph, path, node.body, current_constant: constant.name, namespace: constant.name.split(
|
|
223
|
+
visit(graph, path, node.body, current_constant: constant.name, namespace: constant.name.split('::'))
|
|
221
224
|
end
|
|
222
225
|
|
|
223
226
|
def visit_def(graph, path, node, current_constant:, namespace:)
|
|
224
|
-
if current_constant && (constant = graph.constants_named(current_constant).find
|
|
227
|
+
if current_constant && (constant = graph.constants_named(current_constant).find do |candidate|
|
|
228
|
+
candidate.path == path
|
|
229
|
+
end)
|
|
225
230
|
if node.receiver
|
|
226
231
|
constant.add_class_method(node.name, location: SourceLocation.from_prism(path, node.location))
|
|
227
232
|
else
|
|
@@ -234,7 +239,7 @@ module ArchSpec
|
|
|
234
239
|
type: :dynamic_feature,
|
|
235
240
|
from_path: path,
|
|
236
241
|
from_constant: current_constant,
|
|
237
|
-
to:
|
|
242
|
+
to: 'method_missing',
|
|
238
243
|
location: SourceLocation.from_prism(path, node.location),
|
|
239
244
|
confidence: :unknown_due_to_dynamic_feature
|
|
240
245
|
)
|
|
@@ -244,7 +249,10 @@ module ArchSpec
|
|
|
244
249
|
end
|
|
245
250
|
|
|
246
251
|
def visit_call(graph, path, node, current_constant:, namespace:)
|
|
247
|
-
|
|
252
|
+
unless node.message
|
|
253
|
+
return visit_children(graph, path, node, current_constant: current_constant,
|
|
254
|
+
namespace: namespace)
|
|
255
|
+
end
|
|
248
256
|
|
|
249
257
|
message = node.message.to_sym
|
|
250
258
|
location = SourceLocation.from_prism(path, node.location)
|
|
@@ -271,7 +279,9 @@ module ArchSpec
|
|
|
271
279
|
|
|
272
280
|
if (edge_type = MIXIN_MESSAGES[message])
|
|
273
281
|
constant_arguments(node).each do |constant_name|
|
|
274
|
-
if current_constant && (constant = graph.constants_named(current_constant).find
|
|
282
|
+
if current_constant && (constant = graph.constants_named(current_constant).find do |candidate|
|
|
283
|
+
candidate.path == path
|
|
284
|
+
end)
|
|
275
285
|
constant.add_mixin(message, constant_name)
|
|
276
286
|
end
|
|
277
287
|
|
|
@@ -342,7 +352,7 @@ module ArchSpec
|
|
|
342
352
|
def instantiates_and_invokes(node)
|
|
343
353
|
receiver = node.receiver
|
|
344
354
|
return unless receiver.is_a?(Prism::CallNode)
|
|
345
|
-
return unless receiver.message ==
|
|
355
|
+
return unless receiver.message == 'new'
|
|
346
356
|
|
|
347
357
|
"#{new_receiver_name(receiver)}##{node.message}"
|
|
348
358
|
end
|
|
@@ -350,22 +360,22 @@ module ArchSpec
|
|
|
350
360
|
def new_receiver_name(node)
|
|
351
361
|
return constant_reference_name(node.receiver) if constant_node?(node.receiver)
|
|
352
362
|
|
|
353
|
-
node.receiver&.slice ||
|
|
363
|
+
node.receiver&.slice || '(unknown)'
|
|
354
364
|
end
|
|
355
365
|
|
|
356
366
|
def qualified_constant_name(node, namespace)
|
|
357
367
|
raw = constant_reference_name(node)
|
|
358
368
|
absolute = node.respond_to?(:full_name_parts) && node.full_name_parts.first == :""
|
|
359
369
|
|
|
360
|
-
if absolute || raw.include?(
|
|
370
|
+
if absolute || raw.include?('::') || namespace.empty?
|
|
361
371
|
raw
|
|
362
372
|
else
|
|
363
|
-
"#{namespace.join(
|
|
373
|
+
"#{namespace.join('::')}::#{raw}"
|
|
364
374
|
end
|
|
365
375
|
end
|
|
366
376
|
|
|
367
377
|
def constant_reference_name(node)
|
|
368
|
-
node.full_name.to_s.sub(/\A::/,
|
|
378
|
+
node.full_name.to_s.sub(/\A::/, '')
|
|
369
379
|
end
|
|
370
380
|
|
|
371
381
|
def constant_node?(node)
|
|
@@ -1,26 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ArchSpec
|
|
2
4
|
module Architectures
|
|
3
5
|
extend self
|
|
4
6
|
|
|
5
7
|
DEFAULT_LAYERED = {
|
|
6
|
-
interface:
|
|
8
|
+
interface: 'app/controllers/**/*.rb',
|
|
7
9
|
application: %w[app/services/**/*.rb app/jobs/**/*.rb app/mailers/**/*.rb],
|
|
8
|
-
domain:
|
|
10
|
+
domain: 'app/models/**/*.rb'
|
|
9
11
|
}.freeze
|
|
10
12
|
|
|
11
13
|
DEFAULT_RAILS_MVC = {
|
|
12
|
-
controllers:
|
|
13
|
-
models:
|
|
14
|
-
helpers:
|
|
15
|
-
mailers:
|
|
16
|
-
jobs:
|
|
17
|
-
services:
|
|
14
|
+
controllers: 'app/controllers/**/*.rb',
|
|
15
|
+
models: 'app/models/**/*.rb',
|
|
16
|
+
helpers: 'app/helpers/**/*.rb',
|
|
17
|
+
mailers: 'app/mailers/**/*.rb',
|
|
18
|
+
jobs: 'app/jobs/**/*.rb',
|
|
19
|
+
services: 'app/services/**/*.rb'
|
|
18
20
|
}.freeze
|
|
19
21
|
|
|
20
22
|
DEFAULT_HEXAGONAL = {
|
|
21
23
|
application: %w[app/services/**/*.rb app/use_cases/**/*.rb],
|
|
22
|
-
domain:
|
|
23
|
-
ports:
|
|
24
|
+
domain: 'app/domain/**/*.rb',
|
|
25
|
+
ports: 'app/ports/**/*.rb',
|
|
24
26
|
adapters: %w[app/adapters/**/*.rb app/integrations/**/*.rb app/infrastructure/**/*.rb]
|
|
25
27
|
}.freeze
|
|
26
28
|
|
|
@@ -32,15 +34,15 @@ module ArchSpec
|
|
|
32
34
|
}.freeze
|
|
33
35
|
|
|
34
36
|
DEFAULT_CQRS = {
|
|
35
|
-
commands:
|
|
36
|
-
queries:
|
|
37
|
-
read_models:
|
|
37
|
+
commands: 'app/commands/**/*.rb',
|
|
38
|
+
queries: 'app/queries/**/*.rb',
|
|
39
|
+
read_models: 'app/read_models/**/*.rb'
|
|
38
40
|
}.freeze
|
|
39
41
|
|
|
40
42
|
DEFAULT_EVENT_DRIVEN = {
|
|
41
|
-
events:
|
|
42
|
-
publishers:
|
|
43
|
-
subscribers:
|
|
43
|
+
events: 'app/events/**/*.rb',
|
|
44
|
+
publishers: 'app/publishers/**/*.rb',
|
|
45
|
+
subscribers: 'app/subscribers/**/*.rb'
|
|
44
46
|
}.freeze
|
|
45
47
|
|
|
46
48
|
CONTROLLER_METHODS = %i[render redirect_to params session cookies flash].freeze
|
data/lib/archspec/baseline.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
3
4
|
|
|
4
5
|
module ArchSpec
|
|
5
6
|
class Baseline
|
|
@@ -11,8 +12,8 @@ module ArchSpec
|
|
|
11
12
|
return empty(root: root) unless path && File.exist?(path)
|
|
12
13
|
|
|
13
14
|
document = YAML.safe_load_file(path, permitted_classes: [], aliases: false) || {}
|
|
14
|
-
ids = Array(document[
|
|
15
|
-
entry.is_a?(Hash) ? entry[
|
|
15
|
+
ids = Array(document['violations']).filter_map do |entry|
|
|
16
|
+
entry.is_a?(Hash) ? entry['id'] : entry
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
new(ids.to_set, root: root)
|
|
@@ -20,13 +21,13 @@ module ArchSpec
|
|
|
20
21
|
|
|
21
22
|
def self.write(path, diagnostics, root:)
|
|
22
23
|
payload = {
|
|
23
|
-
|
|
24
|
+
'violations' => diagnostics.map do |diagnostic|
|
|
24
25
|
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
'id' => diagnostic.fingerprint(root: root),
|
|
27
|
+
'rule' => diagnostic.rule,
|
|
28
|
+
'path' => diagnostic.location.relative_path(root),
|
|
29
|
+
'line' => diagnostic.location.line,
|
|
30
|
+
'message' => diagnostic.message
|
|
30
31
|
}
|
|
31
32
|
end
|
|
32
33
|
}
|
data/lib/archspec/cli.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
2
4
|
|
|
3
5
|
module ArchSpec
|
|
4
6
|
module CLI
|
|
5
7
|
extend self
|
|
6
8
|
|
|
7
|
-
CONFIG_FILE =
|
|
9
|
+
CONFIG_FILE = 'Archspec.rb'
|
|
8
10
|
TEMPLATE = <<~RUBY
|
|
9
11
|
ArchSpec.define "Application architecture" do
|
|
10
12
|
root "."
|
|
@@ -14,16 +16,16 @@ module ArchSpec
|
|
|
14
16
|
|
|
15
17
|
def run(argv, output: $stdout, error: $stderr)
|
|
16
18
|
argv = argv.dup
|
|
17
|
-
command = argv.shift ||
|
|
19
|
+
command = argv.shift || 'check'
|
|
18
20
|
|
|
19
21
|
case command
|
|
20
|
-
when
|
|
22
|
+
when 'init'
|
|
21
23
|
init(argv, output)
|
|
22
|
-
when
|
|
24
|
+
when 'check'
|
|
23
25
|
check(argv, output)
|
|
24
|
-
when
|
|
26
|
+
when 'explain'
|
|
25
27
|
explain(argv, output)
|
|
26
|
-
when
|
|
28
|
+
when 'version', '--version', '-v'
|
|
27
29
|
output.puts ArchSpec::VERSION
|
|
28
30
|
0
|
|
29
31
|
else
|
|
@@ -31,20 +33,18 @@ module ArchSpec
|
|
|
31
33
|
error.puts usage
|
|
32
34
|
64
|
|
33
35
|
end
|
|
34
|
-
rescue Error =>
|
|
35
|
-
error.puts
|
|
36
|
+
rescue Error => e
|
|
37
|
+
error.puts e.message
|
|
36
38
|
1
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
private
|
|
40
42
|
|
|
41
43
|
def init(argv, output)
|
|
42
|
-
force = argv.delete(
|
|
44
|
+
force = argv.delete('--force')
|
|
43
45
|
path = argv.shift || CONFIG_FILE
|
|
44
46
|
|
|
45
|
-
if File.exist?(path) && !force
|
|
46
|
-
raise Error, "#{path} already exists. Use --force to overwrite it."
|
|
47
|
-
end
|
|
47
|
+
raise Error, "#{path} already exists. Use --force to overwrite it." if File.exist?(path) && !force
|
|
48
48
|
|
|
49
49
|
File.write(path, TEMPLATE)
|
|
50
50
|
output.puts "Created #{path}"
|
|
@@ -54,14 +54,14 @@ module ArchSpec
|
|
|
54
54
|
def check(argv, output)
|
|
55
55
|
options = {
|
|
56
56
|
config: CONFIG_FILE,
|
|
57
|
-
format:
|
|
57
|
+
format: 'text',
|
|
58
58
|
update_baseline: false
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
parser = OptionParser.new do |
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
parser = OptionParser.new do |opts|
|
|
62
|
+
opts.on('--config PATH') { |value| options[:config] = value }
|
|
63
|
+
opts.on('--format FORMAT') { |value| options[:format] = value }
|
|
64
|
+
opts.on('--update-baseline') { options[:update_baseline] = true }
|
|
65
65
|
end
|
|
66
66
|
parser.parse!(argv)
|
|
67
67
|
|
|
@@ -72,7 +72,10 @@ module ArchSpec
|
|
|
72
72
|
diagnostics = Evaluator.evaluate(definition, graph, baseline: baseline)
|
|
73
73
|
|
|
74
74
|
if options[:update_baseline]
|
|
75
|
-
|
|
75
|
+
unless baseline_path
|
|
76
|
+
raise Error,
|
|
77
|
+
"No baseline configured. Add `baseline \".archspec_todo.yml\"` to #{options[:config]}."
|
|
78
|
+
end
|
|
76
79
|
|
|
77
80
|
Baseline.write(baseline_path, diagnostics, root: root)
|
|
78
81
|
output.puts "Updated #{Pathname(baseline_path).relative_path_from(Pathname(root))} with #{diagnostics.size} violations."
|
|
@@ -85,13 +88,13 @@ module ArchSpec
|
|
|
85
88
|
|
|
86
89
|
def explain(argv, output)
|
|
87
90
|
options = { config: CONFIG_FILE }
|
|
88
|
-
parser = OptionParser.new do |
|
|
89
|
-
|
|
91
|
+
parser = OptionParser.new do |opts|
|
|
92
|
+
opts.on('--config PATH') { |value| options[:config] = value }
|
|
90
93
|
end
|
|
91
94
|
parser.parse!(argv)
|
|
92
95
|
|
|
93
96
|
subject = argv.shift
|
|
94
|
-
raise Error,
|
|
97
|
+
raise Error, 'Usage: archspec explain PATH_OR_CONSTANT' unless subject
|
|
95
98
|
|
|
96
99
|
definition, root = load_definition(options[:config])
|
|
97
100
|
graph = Analyzer.analyze(definition, root: root)
|
|
@@ -119,9 +122,9 @@ module ArchSpec
|
|
|
119
122
|
|
|
120
123
|
def formatter_for(name)
|
|
121
124
|
case name
|
|
122
|
-
when
|
|
125
|
+
when 'text'
|
|
123
126
|
Formatters::Text
|
|
124
|
-
when
|
|
127
|
+
when 'json'
|
|
125
128
|
Formatters::JSON
|
|
126
129
|
else
|
|
127
130
|
raise Error, "Unknown format: #{name.inspect}"
|
|
@@ -134,12 +137,12 @@ module ArchSpec
|
|
|
134
137
|
if graph.files.key?(path)
|
|
135
138
|
file = graph.files.fetch(path)
|
|
136
139
|
output.puts file.relative_path
|
|
137
|
-
output.puts " expected constant: #{file.expected_constant ||
|
|
138
|
-
output.puts " defined constants: #{graph.constants_for_path(path).map(&:name).join(
|
|
140
|
+
output.puts " expected constant: #{file.expected_constant || '(none)'}"
|
|
141
|
+
output.puts " defined constants: #{graph.constants_for_path(path).map(&:name).join(', ')}"
|
|
139
142
|
output_parse_errors(output, file)
|
|
140
143
|
output_component_reasons(output, graph.component_assignment_reasons_for_path(path))
|
|
141
144
|
output_suppressions(output, file)
|
|
142
|
-
output.puts
|
|
145
|
+
output.puts ' outgoing facts:'
|
|
143
146
|
|
|
144
147
|
graph.edges.select { |edge| edge.from_path == path }.each do |edge|
|
|
145
148
|
output.puts " #{edge.type} #{edge.to} at #{edge.location.line}:#{edge.location.column}"
|
|
@@ -153,29 +156,29 @@ module ArchSpec
|
|
|
153
156
|
output.puts " kind: #{constant.kind}"
|
|
154
157
|
output.puts " file: #{constant.location.relative_path(graph.root)}:#{constant.location.line}"
|
|
155
158
|
output_component_reasons(output, graph.component_assignment_reasons_for_constant(constant.name))
|
|
156
|
-
output.puts " superclass: #{constant.superclass ||
|
|
157
|
-
output.puts " instance methods: #{constant.instance_methods.to_a.sort.join(
|
|
158
|
-
output.puts " class methods: #{constant.class_methods.to_a.sort.join(
|
|
159
|
+
output.puts " superclass: #{constant.superclass || '(none)'}"
|
|
160
|
+
output.puts " instance methods: #{constant.instance_methods.to_a.sort.join(', ')}"
|
|
161
|
+
output.puts " class methods: #{constant.class_methods.to_a.sort.join(', ')}"
|
|
159
162
|
end
|
|
160
163
|
end
|
|
161
164
|
end
|
|
162
165
|
|
|
163
166
|
def output_component_reasons(output, assignments)
|
|
164
167
|
if assignments.empty?
|
|
165
|
-
output.puts
|
|
168
|
+
output.puts ' components: (none)'
|
|
166
169
|
return
|
|
167
170
|
end
|
|
168
171
|
|
|
169
|
-
output.puts
|
|
172
|
+
output.puts ' components:'
|
|
170
173
|
assignments.sort_by { |name, _reasons| name.to_s }.each do |name, reasons|
|
|
171
|
-
output.puts " #{name}: #{reasons.empty? ?
|
|
174
|
+
output.puts " #{name}: #{reasons.empty? ? '(no recorded reason)' : reasons.join('; ')}"
|
|
172
175
|
end
|
|
173
176
|
end
|
|
174
177
|
|
|
175
178
|
def output_suppressions(output, file)
|
|
176
179
|
return if file.suppressions.empty?
|
|
177
180
|
|
|
178
|
-
output.puts
|
|
181
|
+
output.puts ' suppressions:'
|
|
179
182
|
file.suppressions.each do |suppression|
|
|
180
183
|
line_range =
|
|
181
184
|
if suppression.end_line == Float::INFINITY
|
|
@@ -185,8 +188,8 @@ module ArchSpec
|
|
|
185
188
|
else
|
|
186
189
|
"#{suppression.start_line}-#{suppression.end_line}"
|
|
187
190
|
end
|
|
188
|
-
rule = suppression.rule ||
|
|
189
|
-
reason = suppression.reason ? " -- #{suppression.reason}" :
|
|
191
|
+
rule = suppression.rule || '*'
|
|
192
|
+
reason = suppression.reason ? " -- #{suppression.reason}" : ''
|
|
190
193
|
output.puts " #{rule} on line #{line_range}#{reason}"
|
|
191
194
|
end
|
|
192
195
|
end
|
|
@@ -194,7 +197,7 @@ module ArchSpec
|
|
|
194
197
|
def output_parse_errors(output, file)
|
|
195
198
|
return if file.parse_errors.empty?
|
|
196
199
|
|
|
197
|
-
output.puts
|
|
200
|
+
output.puts ' parse errors:'
|
|
198
201
|
file.parse_errors.each do |parse_error|
|
|
199
202
|
output.puts " #{parse_error.location.line}:#{parse_error.location.column} #{parse_error.message}"
|
|
200
203
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ArchSpec
|
|
2
4
|
class ComponentSpec
|
|
3
5
|
attr_reader :name, :file_patterns, :namespaces, :constants
|
|
@@ -28,7 +30,7 @@ module ArchSpec
|
|
|
28
30
|
private
|
|
29
31
|
|
|
30
32
|
def normalize_constant(value)
|
|
31
|
-
value.to_s.sub(/\A::/,
|
|
33
|
+
value.to_s.sub(/\A::/, '')
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
end
|
data/lib/archspec/definition.rb
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ArchSpec
|
|
2
4
|
class Definition
|
|
3
5
|
DEFAULT_SOURCE_PATTERNS = [
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
'app/**/*.rb',
|
|
7
|
+
'lib/**/*.rb',
|
|
8
|
+
'packs/*/app/**/*.rb',
|
|
9
|
+
'engines/*/app/**/*.rb'
|
|
8
10
|
].freeze
|
|
9
11
|
|
|
10
12
|
DEFAULT_IGNORE_PATTERNS = [
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
'.git/**/*',
|
|
14
|
+
'.bundle/**/*',
|
|
15
|
+
'node_modules/**/*',
|
|
16
|
+
'tmp/**/*',
|
|
17
|
+
'vendor/**/*'
|
|
16
18
|
].freeze
|
|
17
19
|
|
|
18
20
|
attr_accessor :name, :root_path, :baseline_path
|
|
@@ -20,7 +22,7 @@ module ArchSpec
|
|
|
20
22
|
|
|
21
23
|
def initialize(name)
|
|
22
24
|
@name = name
|
|
23
|
-
@root_path =
|
|
25
|
+
@root_path = '.'
|
|
24
26
|
@baseline_path = nil
|
|
25
27
|
@source_patterns = []
|
|
26
28
|
@ignore_patterns = DEFAULT_IGNORE_PATTERNS.dup
|
data/lib/archspec/diagnostic.rb
CHANGED
data/lib/archspec/dsl.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ArchSpec
|
|
2
4
|
module DSL
|
|
3
5
|
module Context
|
|
@@ -15,7 +17,7 @@ module ArchSpec
|
|
|
15
17
|
add_ignore_patterns(patterns)
|
|
16
18
|
end
|
|
17
19
|
|
|
18
|
-
def baseline(path =
|
|
20
|
+
def baseline(path = '.archspec_todo.yml')
|
|
19
21
|
self.baseline_path = path.to_s
|
|
20
22
|
end
|
|
21
23
|
|
|
@@ -101,6 +103,11 @@ module ArchSpec
|
|
|
101
103
|
self
|
|
102
104
|
end
|
|
103
105
|
|
|
106
|
+
def must_be_empty(because: nil)
|
|
107
|
+
add_rule(Rules::MustBeEmptyRule.new(name, because: because))
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
104
111
|
def must_implement(*methods)
|
|
105
112
|
methods.each do |method_name|
|
|
106
113
|
add_rule(Rules::MustImplementRule.new(name, method_name))
|
|
@@ -121,7 +128,7 @@ module ArchSpec
|
|
|
121
128
|
candidate.respond_to?(:merge_key) && candidate.merge_key == rule.merge_key
|
|
122
129
|
end
|
|
123
130
|
|
|
124
|
-
return existing.merge!(rule) if existing
|
|
131
|
+
return existing.merge!(rule) if existing.respond_to?(:merge!)
|
|
125
132
|
return existing if existing
|
|
126
133
|
end
|
|
127
134
|
|
data/lib/archspec/evaluator.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ArchSpec
|
|
2
4
|
module Evaluator
|
|
3
5
|
extend self
|
|
@@ -6,7 +8,10 @@ module ArchSpec
|
|
|
6
8
|
(parser_diagnostics(graph) + definition.rules.flat_map { |rule| rule.evaluate(graph) })
|
|
7
9
|
.reject { |diagnostic| graph.suppressed?(diagnostic) }
|
|
8
10
|
.reject { |diagnostic| baseline.include?(diagnostic) }
|
|
9
|
-
.sort_by
|
|
11
|
+
.sort_by do |diagnostic|
|
|
12
|
+
[diagnostic.location.path, diagnostic.location.line, diagnostic.rule,
|
|
13
|
+
diagnostic.message]
|
|
14
|
+
end
|
|
10
15
|
end
|
|
11
16
|
|
|
12
17
|
private
|
|
@@ -15,7 +20,7 @@ module ArchSpec
|
|
|
15
20
|
graph.files.values.flat_map do |file|
|
|
16
21
|
file.parse_errors.map do |parse_error|
|
|
17
22
|
Diagnostic.new(
|
|
18
|
-
rule:
|
|
23
|
+
rule: 'parser.syntax',
|
|
19
24
|
message: parse_error.message,
|
|
20
25
|
location: parse_error.location,
|
|
21
26
|
evidence: file.relative_path,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ArchSpec
|
|
2
4
|
module Formatters
|
|
3
5
|
module Text
|
|
4
|
-
|
|
6
|
+
module_function
|
|
5
7
|
|
|
6
8
|
def print(output = $stdout, graph:, diagnostics:)
|
|
7
9
|
if diagnostics.empty?
|
|
@@ -9,7 +11,7 @@ module ArchSpec
|
|
|
9
11
|
return
|
|
10
12
|
end
|
|
11
13
|
|
|
12
|
-
output.puts "#{diagnostics.size} architecture #{diagnostics.size == 1 ?
|
|
14
|
+
output.puts "#{diagnostics.size} architecture #{diagnostics.size == 1 ? 'violation' : 'violations'}"
|
|
13
15
|
output.puts
|
|
14
16
|
|
|
15
17
|
diagnostics.each do |diagnostic|
|
data/lib/archspec/model.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
3
4
|
|
|
4
5
|
module ArchSpec
|
|
5
6
|
ParseError = Data.define(:message, :location)
|
|
@@ -167,7 +168,8 @@ module ArchSpec
|
|
|
167
168
|
next unless matched_file || matched_constant
|
|
168
169
|
|
|
169
170
|
component.add_file(constant.path, reason: "defines #{constant.name}") if matched_constant
|
|
170
|
-
component.add_constant(constant.name,
|
|
171
|
+
component.add_constant(constant.name,
|
|
172
|
+
reason: matched_file ? 'defined in matched file' : 'matched namespace/constant selector')
|
|
171
173
|
end
|
|
172
174
|
|
|
173
175
|
@components[component.name] = component
|
|
@@ -207,11 +209,11 @@ module ArchSpec
|
|
|
207
209
|
candidates = []
|
|
208
210
|
|
|
209
211
|
if from_constant
|
|
210
|
-
namespace = normalize_constant(from_constant).split(
|
|
212
|
+
namespace = normalize_constant(from_constant).split('::')
|
|
211
213
|
namespace.pop
|
|
212
214
|
|
|
213
215
|
until namespace.empty?
|
|
214
|
-
candidates << "#{namespace.join(
|
|
216
|
+
candidates << "#{namespace.join('::')}::#{normalized}"
|
|
215
217
|
namespace.pop
|
|
216
218
|
end
|
|
217
219
|
end
|
|
@@ -272,7 +274,7 @@ module ArchSpec
|
|
|
272
274
|
end
|
|
273
275
|
|
|
274
276
|
def normalize_constant(value)
|
|
275
|
-
value.to_s.sub(/\A::/,
|
|
277
|
+
value.to_s.sub(/\A::/, '')
|
|
276
278
|
end
|
|
277
279
|
end
|
|
278
280
|
end
|
data/lib/archspec/presets.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ArchSpec
|
|
2
4
|
module Presets
|
|
3
5
|
module_function
|
|
@@ -8,6 +10,8 @@ module ArchSpec
|
|
|
8
10
|
rails_way(dsl, **options)
|
|
9
11
|
when :rails_strict
|
|
10
12
|
rails_strict(dsl, **options)
|
|
13
|
+
when :vanilla_rails
|
|
14
|
+
vanilla_rails(dsl, **options)
|
|
11
15
|
when :rails_layered
|
|
12
16
|
rails_layered(dsl, **options)
|
|
13
17
|
when :rails_hexagonal
|
|
@@ -33,6 +37,23 @@ module ArchSpec
|
|
|
33
37
|
dsl.no_cycles!(among: %i[controllers models helpers mailers jobs services])
|
|
34
38
|
end
|
|
35
39
|
|
|
40
|
+
VANILLA_RAILS_EMPTY = {
|
|
41
|
+
services: ['app/services/**/*.rb', 'behavior belongs on models, not service objects'],
|
|
42
|
+
forms: ['app/forms/**/*.rb', 'use strong parameters and model validations'],
|
|
43
|
+
policies: ['app/policies/**/*.rb', 'authorization is predicate methods on models'],
|
|
44
|
+
decorators: ['app/decorators/**/*.rb', 'use helpers and partials'],
|
|
45
|
+
presenters: ['app/presenters/**/*.rb', 'presentation objects are POROs in app/models'],
|
|
46
|
+
view_components: ['app/components/**/*.rb', 'use helpers and ERB partials']
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
def vanilla_rails(dsl, **options)
|
|
50
|
+
rails_way(dsl, **options)
|
|
51
|
+
|
|
52
|
+
VANILLA_RAILS_EMPTY.each do |name, (pattern, reason)|
|
|
53
|
+
dsl.component(name, in: pattern).must_be_empty(because: reason)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
36
57
|
def rails_layered(dsl, **options)
|
|
37
58
|
Architectures.apply(:layered, dsl, **options)
|
|
38
59
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArchSpec
|
|
4
|
+
module Rules
|
|
5
|
+
class MustBeEmptyRule
|
|
6
|
+
attr_reader :source, :because
|
|
7
|
+
|
|
8
|
+
def initialize(source, because: nil)
|
|
9
|
+
@source = source.to_sym
|
|
10
|
+
@because = because
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def merge_key
|
|
14
|
+
[self.class, source]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def id
|
|
18
|
+
'components.empty'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def evaluate(graph)
|
|
22
|
+
component = graph.components[source]
|
|
23
|
+
return [] unless component
|
|
24
|
+
|
|
25
|
+
component.files.sort.map do |path|
|
|
26
|
+
relative = graph.files[path]&.relative_path || path
|
|
27
|
+
|
|
28
|
+
Diagnostic.new(
|
|
29
|
+
rule: id,
|
|
30
|
+
message: because ? "#{source} must stay empty: #{because}" : "#{source} must stay empty",
|
|
31
|
+
location: SourceLocation.new(path, 1, 1),
|
|
32
|
+
evidence: "#{relative} belongs to #{source}"
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ArchSpec
|
|
2
4
|
module Rules
|
|
3
5
|
class NoCyclesRule
|
|
@@ -8,7 +10,7 @@ module ArchSpec
|
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def id
|
|
11
|
-
|
|
13
|
+
'dependencies.no_cycles'
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def evaluate(graph)
|
|
@@ -16,9 +18,9 @@ module ArchSpec
|
|
|
16
18
|
location = first_location_for_cycle(graph, cycle) || SourceLocation.new(graph.root, 1, 1)
|
|
17
19
|
Diagnostic.new(
|
|
18
20
|
rule: id,
|
|
19
|
-
message: "component dependency cycle: #{cycle.join(
|
|
21
|
+
message: "component dependency cycle: #{cycle.join(' -> ')}",
|
|
20
22
|
location: location,
|
|
21
|
-
evidence: cycle.join(
|
|
23
|
+
evidence: cycle.join(' -> ')
|
|
22
24
|
)
|
|
23
25
|
end
|
|
24
26
|
end
|
|
@@ -26,17 +28,17 @@ module ArchSpec
|
|
|
26
28
|
private
|
|
27
29
|
|
|
28
30
|
def cycles(graph)
|
|
29
|
-
adjacency =
|
|
31
|
+
adjacency = {}
|
|
30
32
|
graph.component_dependency_pairs(only: components).each do |source, target|
|
|
31
33
|
next if source == target
|
|
32
34
|
|
|
33
|
-
adjacency[source].add(target)
|
|
35
|
+
(adjacency[source] ||= Set.new).add(target)
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
found = Set.new
|
|
37
39
|
result = []
|
|
38
40
|
|
|
39
|
-
adjacency.
|
|
41
|
+
adjacency.each_key do |start|
|
|
40
42
|
walk(adjacency, start, start, [], found, result)
|
|
41
43
|
end
|
|
42
44
|
|
|
@@ -46,7 +48,7 @@ module ArchSpec
|
|
|
46
48
|
def walk(adjacency, start, node, path, found, result)
|
|
47
49
|
next_path = path + [node]
|
|
48
50
|
|
|
49
|
-
adjacency[
|
|
51
|
+
adjacency.fetch(node, []).each do |target|
|
|
50
52
|
if target == start
|
|
51
53
|
cycle = canonical_cycle(next_path + [start])
|
|
52
54
|
key = cycle.join("\0")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ArchSpec
|
|
4
4
|
module Rules
|
|
@@ -36,7 +36,7 @@ module ArchSpec
|
|
|
36
36
|
|
|
37
37
|
class AllowDependenciesRule < DependencyRule
|
|
38
38
|
def id
|
|
39
|
-
|
|
39
|
+
'dependencies.allow'
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def evaluate(graph)
|
|
@@ -57,7 +57,7 @@ module ArchSpec
|
|
|
57
57
|
|
|
58
58
|
class ForbidDependenciesRule < DependencyRule
|
|
59
59
|
def id
|
|
60
|
-
|
|
60
|
+
'dependencies.forbid'
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def evaluate(graph)
|
|
@@ -81,7 +81,7 @@ module ArchSpec
|
|
|
81
81
|
|
|
82
82
|
def initialize(source, constants)
|
|
83
83
|
@source = source.to_sym
|
|
84
|
-
@constants = Array(constants).flatten.map { |constant| constant.to_s.sub(/\A::/,
|
|
84
|
+
@constants = Array(constants).flatten.map { |constant| constant.to_s.sub(/\A::/, '') }
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def merge_key
|
|
@@ -94,7 +94,7 @@ module ArchSpec
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def id
|
|
97
|
-
|
|
97
|
+
'constants.forbid'
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def evaluate(graph)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ArchSpec
|
|
2
4
|
module Rules
|
|
3
5
|
class CannotCallRule
|
|
@@ -18,7 +20,7 @@ module ArchSpec
|
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def id
|
|
21
|
-
|
|
23
|
+
'methods.forbid'
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def evaluate(graph)
|
|
@@ -50,7 +52,7 @@ module ArchSpec
|
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def id
|
|
53
|
-
|
|
55
|
+
'protocol.must_implement'
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
def evaluate(graph)
|
|
@@ -61,7 +63,7 @@ module ArchSpec
|
|
|
61
63
|
rule: id,
|
|
62
64
|
message: "#{constant.name} must implement ##{method_name}",
|
|
63
65
|
location: constant.location,
|
|
64
|
-
evidence: "#{constant.name} methods: #{constant.instance_methods.to_a.sort.join(
|
|
66
|
+
evidence: "#{constant.name} methods: #{constant.instance_methods.to_a.sort.join(', ')}"
|
|
65
67
|
)
|
|
66
68
|
end
|
|
67
69
|
end
|
|
@@ -93,7 +95,7 @@ module ArchSpec
|
|
|
93
95
|
end
|
|
94
96
|
|
|
95
97
|
def id
|
|
96
|
-
|
|
98
|
+
'protocol.must_implement_one_of'
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
def evaluate(graph)
|
|
@@ -102,9 +104,9 @@ module ArchSpec
|
|
|
102
104
|
|
|
103
105
|
Diagnostic.new(
|
|
104
106
|
rule: id,
|
|
105
|
-
message: "#{constant.name} must implement one of #{method_names.map { |name| "##{name}" }.join(
|
|
107
|
+
message: "#{constant.name} must implement one of #{method_names.map { |name| "##{name}" }.join(', ')}",
|
|
106
108
|
location: constant.location,
|
|
107
|
-
evidence: "#{constant.name} methods: #{constant.instance_methods.to_a.sort.join(
|
|
109
|
+
evidence: "#{constant.name} methods: #{constant.instance_methods.to_a.sort.join(', ')}"
|
|
108
110
|
)
|
|
109
111
|
end
|
|
110
112
|
end
|
|
@@ -136,7 +138,7 @@ module ArchSpec
|
|
|
136
138
|
end
|
|
137
139
|
|
|
138
140
|
def id
|
|
139
|
-
|
|
141
|
+
'methods.define_forbid'
|
|
140
142
|
end
|
|
141
143
|
|
|
142
144
|
def evaluate(graph)
|
|
@@ -165,7 +167,7 @@ module ArchSpec
|
|
|
165
167
|
end
|
|
166
168
|
|
|
167
169
|
def id
|
|
168
|
-
|
|
170
|
+
'objects.instantiate_and_invoke_forbid'
|
|
169
171
|
end
|
|
170
172
|
|
|
171
173
|
def evaluate(graph)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module ArchSpec
|
|
2
4
|
module Rules
|
|
3
5
|
class ZeitwerkNamingRule
|
|
4
6
|
def id
|
|
5
|
-
|
|
7
|
+
'zeitwerk.naming'
|
|
6
8
|
end
|
|
7
9
|
|
|
8
10
|
def evaluate(graph)
|
|
@@ -16,7 +18,7 @@ module ArchSpec
|
|
|
16
18
|
rule: id,
|
|
17
19
|
message: "#{file.relative_path} should define #{file.expected_constant}",
|
|
18
20
|
location: SourceLocation.new(file.path, 1, 1),
|
|
19
|
-
evidence: "defined constants: #{defined.empty? ?
|
|
21
|
+
evidence: "defined constants: #{defined.empty? ? '(none)' : defined.join(', ')}"
|
|
20
22
|
)
|
|
21
23
|
end
|
|
22
24
|
end
|
data/lib/archspec/version.rb
CHANGED
data/lib/archspec.rb
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
require_relative
|
|
10
|
-
require_relative
|
|
11
|
-
require_relative
|
|
12
|
-
require_relative
|
|
13
|
-
require_relative
|
|
14
|
-
require_relative
|
|
15
|
-
require_relative
|
|
16
|
-
require_relative
|
|
17
|
-
require_relative
|
|
18
|
-
require_relative
|
|
19
|
-
require_relative
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'archspec/version'
|
|
4
|
+
require_relative 'archspec/source_location'
|
|
5
|
+
require_relative 'archspec/diagnostic'
|
|
6
|
+
require_relative 'archspec/component_spec'
|
|
7
|
+
require_relative 'archspec/model'
|
|
8
|
+
require_relative 'archspec/definition'
|
|
9
|
+
require_relative 'archspec/baseline'
|
|
10
|
+
require_relative 'archspec/dsl'
|
|
11
|
+
require_relative 'archspec/analyzer'
|
|
12
|
+
require_relative 'archspec/evaluator'
|
|
13
|
+
require_relative 'archspec/architectures'
|
|
14
|
+
require_relative 'archspec/presets'
|
|
15
|
+
require_relative 'archspec/rules/component_rules'
|
|
16
|
+
require_relative 'archspec/rules/dependency_rules'
|
|
17
|
+
require_relative 'archspec/rules/protocol_rules'
|
|
18
|
+
require_relative 'archspec/rules/cycle_rule'
|
|
19
|
+
require_relative 'archspec/rules/zeitwerk_rule'
|
|
20
|
+
require_relative 'archspec/formatters/text'
|
|
21
|
+
require_relative 'archspec/formatters/json'
|
|
22
|
+
require_relative 'archspec/cli'
|
|
20
23
|
|
|
21
24
|
module ArchSpec
|
|
22
25
|
class Error < StandardError; end
|
|
@@ -24,7 +27,7 @@ module ArchSpec
|
|
|
24
27
|
class << self
|
|
25
28
|
attr_accessor :last_definition
|
|
26
29
|
|
|
27
|
-
def define(name =
|
|
30
|
+
def define(name = 'Architecture', &block)
|
|
28
31
|
definition = Definition.new(name)
|
|
29
32
|
definition.extend(DSL::Context)
|
|
30
33
|
definition.instance_eval(&block) if block
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: archspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.
|
|
4
|
+
version: 0.1.0.pre2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carmine Paolino
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prism
|
|
@@ -77,6 +77,7 @@ files:
|
|
|
77
77
|
- lib/archspec/formatters/text.rb
|
|
78
78
|
- lib/archspec/model.rb
|
|
79
79
|
- lib/archspec/presets.rb
|
|
80
|
+
- lib/archspec/rules/component_rules.rb
|
|
80
81
|
- lib/archspec/rules/cycle_rule.rb
|
|
81
82
|
- lib/archspec/rules/dependency_rules.rb
|
|
82
83
|
- lib/archspec/rules/protocol_rules.rb
|