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
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module ArchSpec
|
|
6
|
+
module Rules
|
|
7
|
+
# Backs the +method_names.matching(...)+ DSL. One rule pairs a selector (which
|
|
8
|
+
# methods it judges) with a constraint (what must hold of them). It judges a
|
|
9
|
+
# component's defined, public methods in the requested scope, or every such
|
|
10
|
+
# method in the project when +source+ is nil. Every finding is name-based and
|
|
11
|
+
# exact, so confidence is always +:high+.
|
|
12
|
+
class NamingRule
|
|
13
|
+
VALID_SCOPES = %i[instance class].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :source, :selector, :scope, :except
|
|
16
|
+
|
|
17
|
+
def initialize(source:, selector:, constraint:, scope: :instance, except: [])
|
|
18
|
+
unless VALID_SCOPES.include?(scope)
|
|
19
|
+
raise Error, "method_names scope: must be :instance or :class, got #{scope.inspect}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@source = source&.to_sym
|
|
23
|
+
@selector = selector
|
|
24
|
+
@constraint = constraint
|
|
25
|
+
@scope = scope
|
|
26
|
+
@except = Array(except).flatten.map(&:to_sym).to_set
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def id
|
|
30
|
+
@constraint.id
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def evaluate(graph)
|
|
34
|
+
selected = candidate_methods(graph).filter_map do |definition|
|
|
35
|
+
next if except.include?(definition.name)
|
|
36
|
+
|
|
37
|
+
match = selector.match(definition)
|
|
38
|
+
[definition, match] if match
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@constraint.diagnostics(selected, self, graph)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def candidate_methods(graph)
|
|
47
|
+
definitions = source ? graph.method_definitions_for_component(source) : graph.method_definitions
|
|
48
|
+
definitions.select { |definition| definition.scope == scope && definition.visibility == :public }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The selectors and constraints the naming DSL composes, plus the builder the
|
|
53
|
+
# DSL returns.
|
|
54
|
+
module Naming
|
|
55
|
+
# Selects methods whose name matches a regex. A named capture in the regex
|
|
56
|
+
# (such as <tt>(?<base>.+)</tt>) is exposed to the +requires+ constraint.
|
|
57
|
+
class NameSelector
|
|
58
|
+
attr_reader :regex
|
|
59
|
+
|
|
60
|
+
def initialize(regex)
|
|
61
|
+
raise Error, "method_names matching expects a Regexp, got #{regex.inspect}" unless regex.is_a?(Regexp)
|
|
62
|
+
|
|
63
|
+
@regex = regex
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def match(definition)
|
|
67
|
+
regex.match(definition.name.to_s)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def describe
|
|
71
|
+
"matches #{regex.inspect}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Forbids any selected method from existing. Rule id +naming.forbidden+.
|
|
76
|
+
class Forbidden
|
|
77
|
+
def initialize(because: nil)
|
|
78
|
+
@because = because
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def id
|
|
82
|
+
'naming.forbidden'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def diagnostics(selected, rule, _graph)
|
|
86
|
+
selected.map do |definition, _match|
|
|
87
|
+
Diagnostic.new(
|
|
88
|
+
rule: id,
|
|
89
|
+
message: message_for(definition),
|
|
90
|
+
location: definition.location,
|
|
91
|
+
evidence: "#{definition.owner} defines #{definition.scope} method #{definition.name} (#{rule.selector.describe})"
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def message_for(definition)
|
|
99
|
+
base = "#{definition.owner} must not define ##{definition.name}"
|
|
100
|
+
@because ? "#{base}: #{@because}" : base
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Requires each selected method to have a sibling named by a template, in
|
|
105
|
+
# the same component or another (+on:+), at a given scope. The template
|
|
106
|
+
# interpolates the selector's named captures, as in
|
|
107
|
+
# <tt>requires("without_%{base}")</tt>. Rule id +naming.requires+.
|
|
108
|
+
class Requires
|
|
109
|
+
def initialize(template, on: nil, scope: :instance, because: nil)
|
|
110
|
+
unless NamingRule::VALID_SCOPES.include?(scope)
|
|
111
|
+
raise Error, "requires scope: must be :instance or :class, got #{scope.inspect}"
|
|
112
|
+
end
|
|
113
|
+
raise Error, "requires expects a String template, got #{template.inspect}" unless template.is_a?(String)
|
|
114
|
+
|
|
115
|
+
@template = template
|
|
116
|
+
@on = on
|
|
117
|
+
@target_scope = scope
|
|
118
|
+
@because = because
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def id
|
|
122
|
+
'naming.requires'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def diagnostics(selected, rule, graph)
|
|
126
|
+
target = @on || rule.source
|
|
127
|
+
existing = existing_names(graph, target)
|
|
128
|
+
|
|
129
|
+
selected.filter_map do |definition, match|
|
|
130
|
+
sibling = expand(match)
|
|
131
|
+
next if sibling.nil? || existing.include?(sibling.to_sym)
|
|
132
|
+
|
|
133
|
+
Diagnostic.new(
|
|
134
|
+
rule: id,
|
|
135
|
+
message: message_for(definition, sibling, target, rule),
|
|
136
|
+
location: definition.location,
|
|
137
|
+
evidence: "#{definition.owner} defines ##{definition.name}, expected ##{sibling}"
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def existing_names(graph, target)
|
|
145
|
+
definitions = target ? graph.method_definitions_for_component(target) : graph.method_definitions
|
|
146
|
+
definitions.select { |definition| definition.scope == @target_scope }.map(&:name).to_set
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def expand(match)
|
|
150
|
+
@template % captures(match)
|
|
151
|
+
rescue KeyError, ArgumentError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def captures(match)
|
|
156
|
+
match.is_a?(MatchData) ? match.named_captures.transform_keys(&:to_sym) : {}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def message_for(definition, sibling, target, rule)
|
|
160
|
+
clause =
|
|
161
|
+
if target && target != rule.source
|
|
162
|
+
"#{target} to define ##{sibling}"
|
|
163
|
+
else
|
|
164
|
+
"a matching ##{sibling}"
|
|
165
|
+
end
|
|
166
|
+
base = "#{definition.owner}##{definition.name} requires #{clause}"
|
|
167
|
+
@because ? "#{base}: #{@because}" : base
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Returned by ArchSpec::DSL::ComponentProxy#method_names. Starts a selector.
|
|
172
|
+
class Builder
|
|
173
|
+
def initialize(component, scope: :instance)
|
|
174
|
+
@component = component
|
|
175
|
+
@scope = scope
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def matching(regex)
|
|
179
|
+
Selected.new(@component, NameSelector.new(regex), @scope)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A chosen selector, waiting for a constraint. Each constraint method builds
|
|
184
|
+
# a NamingRule, attaches it, and returns the component proxy so rules chain.
|
|
185
|
+
class Selected
|
|
186
|
+
def initialize(component, selector, scope)
|
|
187
|
+
@component = component
|
|
188
|
+
@selector = selector
|
|
189
|
+
@scope = scope
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def forbidden(except: [], because: nil)
|
|
193
|
+
add(Forbidden.new(because: because), except)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def requires(template, on: nil, scope: :instance, except: [], because: nil)
|
|
197
|
+
validate_template!(template)
|
|
198
|
+
add(Requires.new(template, on: component_name(on), scope: scope, because: because), except)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def add(constraint, except)
|
|
204
|
+
rule = NamingRule.new(
|
|
205
|
+
source: @component.name,
|
|
206
|
+
selector: @selector,
|
|
207
|
+
constraint: constraint,
|
|
208
|
+
scope: @scope,
|
|
209
|
+
except: except
|
|
210
|
+
)
|
|
211
|
+
@component.definition.add_rule(rule)
|
|
212
|
+
@component
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def component_name(target)
|
|
216
|
+
return if target.nil?
|
|
217
|
+
|
|
218
|
+
name = target.respond_to?(:name) ? target.name : target.to_sym
|
|
219
|
+
unless @component.definition.component?(name)
|
|
220
|
+
raise Error, "#{@component.name}.method_names.requires references unknown component: #{name}"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
name
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def validate_template!(template)
|
|
227
|
+
return unless template.is_a?(String)
|
|
228
|
+
|
|
229
|
+
captures = @selector.regex.names.to_h { |name| [name.to_sym, name] }
|
|
230
|
+
template % captures
|
|
231
|
+
rescue KeyError, ArgumentError => e
|
|
232
|
+
raise Error, "invalid requires template #{template.inspect}: #{e.message}"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -36,9 +36,9 @@ module ArchSpec
|
|
|
36
36
|
public_names = public_constant_names(graph)
|
|
37
37
|
|
|
38
38
|
graph.dependency_edges.filter_map do |edge|
|
|
39
|
-
next if
|
|
39
|
+
next if graph.source_components_for(edge).include?(source)
|
|
40
40
|
|
|
41
|
-
resolved = graph.
|
|
41
|
+
resolved = graph.resolve_edge_constant(edge)
|
|
42
42
|
next unless graph.component_names_for_constant(resolved).include?(source)
|
|
43
43
|
next if public?(resolved, public_names)
|
|
44
44
|
|
|
@@ -46,7 +46,7 @@ module ArchSpec
|
|
|
46
46
|
rule: id,
|
|
47
47
|
message: "#{resolved} is private to #{source}",
|
|
48
48
|
location: edge.location,
|
|
49
|
-
evidence: "#{
|
|
49
|
+
evidence: "#{graph.edge_source_name(edge)} #{edge.verb} #{resolved}"
|
|
50
50
|
)
|
|
51
51
|
end
|
|
52
52
|
end
|
|
@@ -35,14 +35,14 @@ module ArchSpec
|
|
|
35
35
|
next unless edge.type == :calls_named_method
|
|
36
36
|
next unless method_names.include?(edge.to.to_sym)
|
|
37
37
|
next if receiver == :none && edge.receiver != :none
|
|
38
|
-
next unless graph.
|
|
38
|
+
next unless graph.source_components_for(edge).include?(source)
|
|
39
39
|
next if own_method_call?(graph, edge)
|
|
40
40
|
|
|
41
41
|
Diagnostic.new(
|
|
42
42
|
rule: id,
|
|
43
43
|
message: "#{source} must not call ##{edge.to}",
|
|
44
44
|
location: edge.location,
|
|
45
|
-
evidence: "#{
|
|
45
|
+
evidence: "#{graph.edge_source_name(edge)} calls #{edge.to}"
|
|
46
46
|
)
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -50,7 +50,7 @@ module ArchSpec
|
|
|
50
50
|
private
|
|
51
51
|
|
|
52
52
|
# A receiverless call to a method the class itself defines (directly,
|
|
53
|
-
# inherited, or via attr_*/delegate) is a call to its own API.
|
|
53
|
+
# inherited, or via attr_*/attribute/delegate) is a call to its own API.
|
|
54
54
|
def own_method_call?(graph, edge)
|
|
55
55
|
return false unless edge.receiver == :none && edge.from_constant
|
|
56
56
|
|
|
@@ -107,7 +107,10 @@ module ArchSpec
|
|
|
107
107
|
|
|
108
108
|
def initialize(source, method_names)
|
|
109
109
|
@source = source.to_sym
|
|
110
|
-
|
|
110
|
+
names = Array(method_names).flatten.compact
|
|
111
|
+
raise Error, 'must_implement_one_of requires at least one method' if names.empty?
|
|
112
|
+
|
|
113
|
+
@method_names = names.map(&:to_sym)
|
|
111
114
|
end
|
|
112
115
|
|
|
113
116
|
def merge_key
|
|
@@ -154,7 +157,7 @@ module ArchSpec
|
|
|
154
157
|
component = graph.components[source]
|
|
155
158
|
return [] unless component
|
|
156
159
|
|
|
157
|
-
|
|
160
|
+
graph.constants_for_component(source).select(&:class?).uniq(&:name)
|
|
158
161
|
end
|
|
159
162
|
|
|
160
163
|
def for(constant, methods, unresolved)
|
|
@@ -222,13 +225,13 @@ module ArchSpec
|
|
|
222
225
|
def evaluate(graph)
|
|
223
226
|
graph.edges.filter_map do |edge|
|
|
224
227
|
next unless edge.type == :instantiates_and_invokes
|
|
225
|
-
next unless graph.
|
|
228
|
+
next unless graph.source_components_for(edge).include?(source)
|
|
226
229
|
|
|
227
230
|
Diagnostic.new(
|
|
228
231
|
rule: id,
|
|
229
232
|
message: "#{source} must not instantiate and immediately invoke #{edge.to}",
|
|
230
233
|
location: edge.location,
|
|
231
|
-
evidence: "#{
|
|
234
|
+
evidence: "#{graph.edge_source_name(edge)} uses #{edge.to}"
|
|
232
235
|
)
|
|
233
236
|
end
|
|
234
237
|
end
|
|
@@ -5,9 +5,15 @@ require 'pathname'
|
|
|
5
5
|
require_relative 'value_object'
|
|
6
6
|
|
|
7
7
|
module ArchSpec
|
|
8
|
-
SourceLocation = ValueObject.define(:path, :line, :column) do
|
|
8
|
+
SourceLocation = ValueObject.define(:path, :line, :column, :end_line, :end_column) do
|
|
9
9
|
def self.from_prism(path, location)
|
|
10
|
-
new(path, location.start_line, location.start_column + 1)
|
|
10
|
+
new(path, location.start_line, location.start_column + 1, location.end_line, location.end_column + 1)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# A zero-width location for diagnostics that point at a file rather than a
|
|
14
|
+
# span of code.
|
|
15
|
+
def self.point(path, line, column)
|
|
16
|
+
new(path, line, column, line, column)
|
|
11
17
|
end
|
|
12
18
|
|
|
13
19
|
def relative_path(root)
|
data/lib/archspec/todo.rb
CHANGED
|
@@ -16,11 +16,24 @@ module ArchSpec
|
|
|
16
16
|
return empty(root: root) unless path && File.exist?(path)
|
|
17
17
|
|
|
18
18
|
document = YAML.safe_load_file(path, permitted_classes: [], aliases: false) || {}
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
unless document.is_a?(Hash) && (document['violations'].nil? || document['violations'].is_a?(Array))
|
|
20
|
+
raise Error, "invalid todo file #{path}: expected a violations list"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ids = Array(document['violations']).map.with_index do |entry, index|
|
|
24
|
+
id = entry.is_a?(Hash) ? entry['id'] : entry
|
|
25
|
+
unless id.is_a?(String) && !id.empty?
|
|
26
|
+
raise Error, "invalid todo file #{path}: violation #{index + 1} has no id"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
id
|
|
21
30
|
end
|
|
22
31
|
|
|
23
32
|
new(ids.to_set, root: root)
|
|
33
|
+
rescue Error
|
|
34
|
+
raise
|
|
35
|
+
rescue Psych::Exception, SystemCallError => e
|
|
36
|
+
raise Error, "could not load todo file #{path}: #{e.message}"
|
|
24
37
|
end
|
|
25
38
|
|
|
26
39
|
def self.write(path, diagnostics, root:)
|
|
@@ -37,6 +50,8 @@ module ArchSpec
|
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
File.write(path, payload.to_yaml)
|
|
53
|
+
rescue SystemCallError => e
|
|
54
|
+
raise Error, "could not write todo file #{path}: #{e.message}"
|
|
40
55
|
end
|
|
41
56
|
|
|
42
57
|
def initialize(ids, root:)
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ArchSpec
|
|
4
|
+
# A frozen value type with positional or keyword construction and #with,
|
|
5
|
+
# mirroring Ruby 3.2's Data.define. Deliberately hand-rolled: the gem
|
|
6
|
+
# supports Ruby 3.1, where Data does not exist. Do not replace this with
|
|
7
|
+
# Data until the required Ruby version reaches 3.2.
|
|
4
8
|
module ValueObject
|
|
5
9
|
def self.define(*members, &block)
|
|
6
10
|
klass = Struct.new(*members) do
|
data/lib/archspec/version.rb
CHANGED
data/lib/archspec.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'archspec/error'
|
|
3
4
|
require_relative 'archspec/version'
|
|
4
5
|
require_relative 'archspec/value_object'
|
|
5
6
|
require_relative 'archspec/source_location'
|
|
@@ -12,16 +13,17 @@ require_relative 'archspec/dsl'
|
|
|
12
13
|
require_relative 'archspec/analyzer'
|
|
13
14
|
require_relative 'archspec/evaluator'
|
|
14
15
|
require_relative 'archspec/architectures'
|
|
15
|
-
require_relative 'archspec/presets'
|
|
16
16
|
require_relative 'archspec/rules/component_rules'
|
|
17
17
|
require_relative 'archspec/rules/concern_rules'
|
|
18
18
|
require_relative 'archspec/rules/dependency_rules'
|
|
19
|
+
require_relative 'archspec/rules/naming_rules'
|
|
19
20
|
require_relative 'archspec/rules/privacy_rule'
|
|
20
21
|
require_relative 'archspec/rules/protocol_rules'
|
|
21
22
|
require_relative 'archspec/rules/cycle_rule'
|
|
22
|
-
require_relative 'archspec/
|
|
23
|
+
require_relative 'archspec/formatters/style'
|
|
23
24
|
require_relative 'archspec/formatters/text'
|
|
24
25
|
require_relative 'archspec/formatters/json'
|
|
26
|
+
require_relative 'archspec/formatters/explanation'
|
|
25
27
|
require_relative 'archspec/cli'
|
|
26
28
|
|
|
27
29
|
# ArchSpec turns your application's architecture into executable checks.
|
|
@@ -43,15 +45,7 @@ require_relative 'archspec/cli'
|
|
|
43
45
|
#
|
|
44
46
|
# You can also build a definition in plain Ruby with ArchSpec.define.
|
|
45
47
|
module ArchSpec
|
|
46
|
-
# Raised for configuration and usage errors, such as an unknown architecture
|
|
47
|
-
# name or a malformed rule option.
|
|
48
|
-
class Error < StandardError; end
|
|
49
|
-
|
|
50
48
|
class << self
|
|
51
|
-
# The definition produced by the most recent ArchSpec.define call, or by
|
|
52
|
-
# evaluating an +Archspec.rb+ file. The CLI reads this after loading config.
|
|
53
|
-
attr_accessor :last_definition
|
|
54
|
-
|
|
55
49
|
# Builds an architecture definition from a block of DSL calls.
|
|
56
50
|
#
|
|
57
51
|
# ArchSpec.define do
|
|
@@ -60,16 +54,17 @@ module ArchSpec
|
|
|
60
54
|
# models.cannot_use :controllers
|
|
61
55
|
# end
|
|
62
56
|
#
|
|
63
|
-
# An +Archspec.rb+ file
|
|
64
|
-
# the DSL, so bare +component+ and +architecture+ calls work
|
|
65
|
-
# +define+ when constructing a definition from Ruby, such as
|
|
57
|
+
# An +Archspec.rb+ file is not written with this wrapper. Its top level is
|
|
58
|
+
# already the DSL, so bare +component+ and +architecture+ calls work
|
|
59
|
+
# directly. Use +define+ when constructing a definition from Ruby, such as
|
|
60
|
+
# in a test.
|
|
66
61
|
#
|
|
67
|
-
# Returns the ArchSpec::Definition
|
|
62
|
+
# Returns the ArchSpec::Definition.
|
|
68
63
|
def define(name = nil, &block)
|
|
69
64
|
definition = Definition.new(name)
|
|
70
65
|
definition.extend(DSL::Context)
|
|
71
66
|
definition.instance_eval(&block) if block
|
|
72
|
-
|
|
67
|
+
definition
|
|
73
68
|
end
|
|
74
69
|
end
|
|
75
70
|
end
|
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.
|
|
4
|
+
version: 1.0.0.rc1
|
|
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-
|
|
11
|
+
date: 2026-08-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: prism
|
|
@@ -93,18 +93,20 @@ files:
|
|
|
93
93
|
- lib/archspec/definition.rb
|
|
94
94
|
- lib/archspec/diagnostic.rb
|
|
95
95
|
- lib/archspec/dsl.rb
|
|
96
|
+
- lib/archspec/error.rb
|
|
96
97
|
- lib/archspec/evaluator.rb
|
|
98
|
+
- lib/archspec/formatters/explanation.rb
|
|
97
99
|
- lib/archspec/formatters/json.rb
|
|
100
|
+
- lib/archspec/formatters/style.rb
|
|
98
101
|
- lib/archspec/formatters/text.rb
|
|
99
102
|
- lib/archspec/model.rb
|
|
100
|
-
- lib/archspec/presets.rb
|
|
101
103
|
- lib/archspec/rules/component_rules.rb
|
|
102
104
|
- lib/archspec/rules/concern_rules.rb
|
|
103
105
|
- lib/archspec/rules/cycle_rule.rb
|
|
104
106
|
- lib/archspec/rules/dependency_rules.rb
|
|
107
|
+
- lib/archspec/rules/naming_rules.rb
|
|
105
108
|
- lib/archspec/rules/privacy_rule.rb
|
|
106
109
|
- lib/archspec/rules/protocol_rules.rb
|
|
107
|
-
- lib/archspec/rules/zeitwerk_rule.rb
|
|
108
110
|
- lib/archspec/source_location.rb
|
|
109
111
|
- lib/archspec/todo.rb
|
|
110
112
|
- lib/archspec/value_object.rb
|
data/lib/archspec/presets.rb
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ArchSpec
|
|
4
|
-
# Backwards-compatible alias for ArchSpec::Architectures. New code should use
|
|
5
|
-
# +architecture+ in the DSL and the ArchSpec::Architectures module.
|
|
6
|
-
module Presets
|
|
7
|
-
module_function
|
|
8
|
-
|
|
9
|
-
# Delegates to ArchSpec::Architectures.apply.
|
|
10
|
-
def apply(name, dsl, **options)
|
|
11
|
-
Architectures.apply(name, dsl, **options)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ArchSpec
|
|
4
|
-
module Rules
|
|
5
|
-
# Backs ArchSpec::DSL::Context#verify_zeitwerk_names!. Flags files that do
|
|
6
|
-
# not define the constant their path implies under Zeitwerk.
|
|
7
|
-
class ZeitwerkNamingRule
|
|
8
|
-
attr_reader :only
|
|
9
|
-
|
|
10
|
-
# only: globs restricting which files are checked. Default is every file
|
|
11
|
-
# ArchSpec can name, but lib autoloading is opt-in, so apps that require
|
|
12
|
-
# lib manually scope this to app/.
|
|
13
|
-
def initialize(only: nil)
|
|
14
|
-
@only = Array(only).flatten.compact.map(&:to_s)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def id
|
|
18
|
-
'zeitwerk.naming'
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def evaluate(graph)
|
|
22
|
-
scoped_paths = scoped_paths(graph)
|
|
23
|
-
|
|
24
|
-
graph.files.values.filter_map do |file|
|
|
25
|
-
next unless file.expected_constant
|
|
26
|
-
next if scoped_paths && !scoped_paths.include?(file.path)
|
|
27
|
-
|
|
28
|
-
defined = graph.constants_for_path(file.path).map(&:name)
|
|
29
|
-
next if defined.include?(file.expected_constant)
|
|
30
|
-
|
|
31
|
-
Diagnostic.new(
|
|
32
|
-
rule: id,
|
|
33
|
-
message: "#{file.relative_path} should define #{file.expected_constant}",
|
|
34
|
-
location: SourceLocation.new(file.path, 1, 1),
|
|
35
|
-
evidence: "defined constants: #{defined.empty? ? '(none)' : defined.join(', ')}"
|
|
36
|
-
)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
|
|
42
|
-
def scoped_paths(graph)
|
|
43
|
-
return nil if only.empty?
|
|
44
|
-
|
|
45
|
-
only.flat_map { |pattern| Dir.glob(File.absolute_path(pattern, graph.root)) }
|
|
46
|
-
.map { |path| File.expand_path(path) }
|
|
47
|
-
.to_set
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|