archunit 0.0.1 → 1.0.0
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/API.md +290 -0
- data/CHANGELOG.md +45 -0
- data/README.md +389 -90
- data/assets/logo-rounded.png +0 -0
- data/lib/archunit/common/extraction/dependency_evidence.rb +44 -0
- data/lib/archunit/common/extraction/edge.rb +7 -2
- data/lib/archunit/common/extraction/import_kind.rb +2 -1
- data/lib/archunit/common/extraction/parse_diagnostic.rb +23 -0
- data/lib/archunit/common/fluentapi/check_options.rb +43 -2
- data/lib/archunit/common/fluentapi/checkable.rb +25 -1
- data/lib/archunit/common/logging/check_logger.rb +8 -0
- data/lib/archunit/common/logging/inspection.rb +59 -0
- data/lib/archunit/common/logging/logging_options.rb +5 -0
- data/lib/archunit/common/pattern.rb +18 -8
- data/lib/archunit/common/projection/cycles/johnson_cycles.rb +35 -19
- data/lib/archunit/common/projection/cycles/tarjan_scc.rb +30 -10
- data/lib/archunit/error/source_parse_error.rb +38 -0
- data/lib/archunit/extraction/arch_ignore.rb +91 -0
- data/lib/archunit/extraction/conservative_local_flow.rb +100 -0
- data/lib/archunit/extraction/constant_index.rb +110 -0
- data/lib/archunit/extraction/constant_visitor.rb +129 -0
- data/lib/archunit/extraction/discover_gemspec_load_paths.rb +96 -0
- data/lib/archunit/extraction/enumerate_source_files.rb +23 -8
- data/lib/archunit/extraction/extract_dependencies.rb +82 -14
- data/lib/archunit/extraction/extract_graph.rb +56 -12
- data/lib/archunit/extraction/extract_imports.rb +60 -124
- data/lib/archunit/extraction/extraction_profile.rb +105 -0
- data/lib/archunit/extraction/import_resolver.rb +56 -0
- data/lib/archunit/extraction/import_visitor.rb +104 -0
- data/lib/archunit/extraction/local_string_tracker.rb +80 -0
- data/lib/archunit/extraction/resolve_import.rb +5 -3
- data/lib/archunit/extraction/resolve_load_paths.rb +93 -0
- data/lib/archunit/extraction/resolved_import.rb +5 -2
- data/lib/archunit/extraction/validate_extraction_paths.rb +37 -0
- data/lib/archunit/files/extraction/extract_file_info.rb +14 -5
- data/lib/archunit/files/extraction/file_info.rb +1 -2
- data/lib/archunit/files/fluentapi/depend_on_external_module_condition.rb +2 -0
- data/lib/archunit/files/fluentapi/depend_on_external_module_condition_builder.rb +2 -0
- data/lib/archunit/files/fluentapi/depend_on_file_condition.rb +6 -0
- data/lib/archunit/files/fluentapi/depend_on_file_condition_builder.rb +6 -0
- data/lib/archunit/files/fluentapi/file_condition_builder.rb +12 -0
- data/lib/archunit/files/fluentapi/file_rule_support.rb +8 -1
- data/lib/archunit/files/fluentapi/files.rb +6 -0
- data/lib/archunit/files/fluentapi/match_pattern_file_condition_builder.rb +14 -0
- data/lib/archunit/files/fluentapi/positive_match_pattern_file_condition_builder.rb +2 -0
- data/lib/archunit/graph/fluentapi/graph.rb +5 -0
- data/lib/archunit/graph/fluentapi/project_graph_builder.rb +44 -0
- data/lib/archunit/graph/projection/aggregate_edges.rb +4 -2
- data/lib/archunit/graph/projection/graph_query_options.rb +1 -2
- data/lib/archunit/graph/projection/graph_report_edge.rb +12 -2
- data/lib/archunit/graph/rendering/json_renderer.rb +3 -1
- data/lib/archunit/layers/fluentapi/layer_definition_builder.rb +4 -0
- data/lib/archunit/layers/fluentapi/layer_dependency_rule_builder.rb +4 -0
- data/lib/archunit/layers/fluentapi/layered_architecture.rb +5 -0
- data/lib/archunit/layers/fluentapi/layers.rb +5 -0
- data/lib/archunit/metrics/extraction/extract_distance_info.rb +1 -2
- data/lib/archunit/metrics/extraction/extract_project_info.rb +6 -6
- data/lib/archunit/metrics/extraction/metric_info.rb +2 -4
- data/lib/archunit/metrics/extraction/source_metrics_visitor.rb +2 -4
- data/lib/archunit/metrics/fluentapi/custom_metric_builder.rb +2 -0
- data/lib/archunit/metrics/fluentapi/custom_metric_condition.rb +4 -1
- data/lib/archunit/metrics/fluentapi/logged_metric.rb +23 -0
- data/lib/archunit/metrics/fluentapi/metric_predicate_condition.rb +4 -1
- data/lib/archunit/metrics/fluentapi/metric_report_builder.rb +2 -0
- data/lib/archunit/metrics/fluentapi/metric_selection.rb +18 -2
- data/lib/archunit/metrics/fluentapi/metric_threshold_condition.rb +2 -1
- data/lib/archunit/metrics/fluentapi/metrics.rb +5 -0
- data/lib/archunit/metrics/fluentapi/metrics_builder.rb +15 -0
- data/lib/archunit/slices/fluentapi/diagram_slice_condition.rb +1 -0
- data/lib/archunit/slices/fluentapi/forbidden_slice_dependency_condition.rb +1 -0
- data/lib/archunit/slices/fluentapi/negative_slice_condition_builder.rb +2 -0
- data/lib/archunit/slices/fluentapi/positive_slice_condition_builder.rb +8 -0
- data/lib/archunit/slices/fluentapi/slice_scope_builder.rb +12 -0
- data/lib/archunit/slices/fluentapi/slices.rb +5 -0
- data/lib/archunit/testing/assert_passes.rb +4 -0
- data/lib/archunit/testing/result_factory.rb +16 -4
- data/lib/archunit/testing/rspec_adapter.rb +1 -2
- data/lib/archunit/testing.rb +7 -3
- data/lib/archunit/version.rb +1 -1
- data/lib/archunit.rb +9 -0
- metadata +20 -2
|
@@ -9,6 +9,8 @@ module ArchUnit
|
|
|
9
9
|
module FluentApi
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
+
# Starts an immutable named-layer policy.
|
|
13
|
+
# @return [LayeredArchitecture]
|
|
12
14
|
def project_layers(project_locator = nil)
|
|
13
15
|
LayeredArchitecture.new(project_locator:)
|
|
14
16
|
end
|
|
@@ -19,6 +21,9 @@ module ArchUnit
|
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
# Starts an immutable named-layer policy.
|
|
25
|
+
# @param project_locator [String, Pathname, nil] project directory, Gemfile, or gemspec
|
|
26
|
+
# @return [Layers::FluentApi::LayeredArchitecture]
|
|
22
27
|
def self.project_layers(project_locator = nil)
|
|
23
28
|
Layers::FluentApi.project_layers(project_locator)
|
|
24
29
|
end
|
|
@@ -30,7 +30,7 @@ module ArchUnit
|
|
|
30
30
|
end
|
|
31
31
|
private_class_method :build_distance_infos
|
|
32
32
|
|
|
33
|
-
# rubocop:disable Metrics/AbcSize -- Both directional indexes are built in one graph pass.
|
|
33
|
+
# rubocop:disable-next Metrics/AbcSize -- Both directional indexes are built in one graph pass.
|
|
34
34
|
def coupling_indexes(graph, file_paths)
|
|
35
35
|
outgoing = Hash.new { |hash, key| hash[key] = [] }
|
|
36
36
|
incoming = Hash.new { |hash, key| hash[key] = [] }
|
|
@@ -43,7 +43,6 @@ module ArchUnit
|
|
|
43
43
|
end
|
|
44
44
|
[outgoing, incoming]
|
|
45
45
|
end
|
|
46
|
-
# rubocop:enable Metrics/AbcSize
|
|
47
46
|
private_class_method :coupling_indexes
|
|
48
47
|
end
|
|
49
48
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'pathname'
|
|
4
4
|
require 'prism'
|
|
5
5
|
require_relative '../../error/technical_error'
|
|
6
|
+
require_relative '../../error/source_parse_error'
|
|
6
7
|
require_relative '../../extraction/enumerate_source_files'
|
|
7
8
|
require_relative '../../extraction/locate_project'
|
|
8
9
|
require_relative 'metric_info'
|
|
@@ -26,15 +27,15 @@ module ArchUnit
|
|
|
26
27
|
path = source_path(source_file)
|
|
27
28
|
identifier = (relative_path || path.basename.to_s).to_s.tr('\\', '/')
|
|
28
29
|
source = path.read
|
|
29
|
-
|
|
30
|
+
result = SourceParseError.validate!(Prism.parse(source, filepath: path.to_s), path:)
|
|
31
|
+
build_file_info(identifier, source, result)
|
|
30
32
|
rescue SystemCallError => e
|
|
31
33
|
raise TechnicalError, "could not extract Ruby source metrics: #{e.message}"
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
def build_file_info(identifier, source)
|
|
35
|
-
parse_result = Prism.parse(source)
|
|
36
|
+
def build_file_info(identifier, source, parse_result)
|
|
36
37
|
visitor = SourceMetricsVisitor.new(identifier)
|
|
37
|
-
visitor.visit(parse_result.value)
|
|
38
|
+
visitor.visit(parse_result.value)
|
|
38
39
|
FileInfo.new(
|
|
39
40
|
path: identifier, lines_of_code: lines_of_code(source), **visitor_counts(visitor)
|
|
40
41
|
)
|
|
@@ -54,7 +55,7 @@ module ArchUnit
|
|
|
54
55
|
end
|
|
55
56
|
private_class_method :visitor_counts
|
|
56
57
|
|
|
57
|
-
# rubocop:disable Metrics/MethodLength -- The state tracks Ruby's =begin/=end comments.
|
|
58
|
+
# rubocop:disable-next Metrics/MethodLength -- The state tracks Ruby's =begin/=end comments.
|
|
58
59
|
def lines_of_code(source)
|
|
59
60
|
in_block_comment = false
|
|
60
61
|
source.each_line.count do |line|
|
|
@@ -70,7 +71,6 @@ module ArchUnit
|
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
|
-
# rubocop:enable Metrics/MethodLength
|
|
74
74
|
private_class_method :lines_of_code
|
|
75
75
|
|
|
76
76
|
def source_path(value)
|
|
@@ -70,7 +70,7 @@ module ArchUnit
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Static information about one Ruby class declaration.
|
|
73
|
-
# rubocop:disable Lint/DataDefineOverride -- `methods` is the established metric vocabulary.
|
|
73
|
+
# rubocop:disable-next Lint/DataDefineOverride -- `methods` is the established metric vocabulary.
|
|
74
74
|
ClassInfo = Data.define(:name, :file_path, :methods, :fields) do
|
|
75
75
|
include ValueValidation
|
|
76
76
|
|
|
@@ -86,7 +86,6 @@ module ArchUnit
|
|
|
86
86
|
"#{file_path}:#{name}"
|
|
87
87
|
end
|
|
88
88
|
end
|
|
89
|
-
# rubocop:enable Lint/DataDefineOverride
|
|
90
89
|
|
|
91
90
|
# Static counts and class declarations extracted from one Ruby source file.
|
|
92
91
|
FileInfo = Data.define(
|
|
@@ -102,7 +101,7 @@ module ArchUnit
|
|
|
102
101
|
) do
|
|
103
102
|
include ValueValidation
|
|
104
103
|
|
|
105
|
-
# rubocop:disable Metrics/MethodLength, Metrics/ParameterLists -- Independent count facts.
|
|
104
|
+
# rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists -- Independent count facts.
|
|
106
105
|
def initialize(
|
|
107
106
|
path:,
|
|
108
107
|
lines_of_code:,
|
|
@@ -131,7 +130,6 @@ module ArchUnit
|
|
|
131
130
|
|
|
132
131
|
super
|
|
133
132
|
end
|
|
134
|
-
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
|
|
135
133
|
|
|
136
134
|
def identifier
|
|
137
135
|
path
|
|
@@ -7,7 +7,7 @@ module ArchUnit
|
|
|
7
7
|
module Metrics
|
|
8
8
|
module Extraction
|
|
9
9
|
# Collects Ruby-specific count and class-cohesion facts from a Prism tree.
|
|
10
|
-
# rubocop:disable Metrics/ClassLength -- One visitor owns the state of a single AST walk.
|
|
10
|
+
# rubocop:disable-next Metrics/ClassLength -- One visitor owns the state of a single AST walk.
|
|
11
11
|
class SourceMetricsVisitor < Prism::Visitor
|
|
12
12
|
IMPORT_METHODS = %i[autoload load require require_relative].freeze
|
|
13
13
|
ATTRIBUTE_METHODS = %i[attr_accessor attr_reader attr_writer].freeze
|
|
@@ -82,7 +82,7 @@ module ArchUnit
|
|
|
82
82
|
@singleton_class_depth -= 1
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
# rubocop:disable Metrics/MethodLength -- The ensure keeps nested visitor state balanced.
|
|
85
|
+
# rubocop:disable-next Metrics/MethodLength -- The ensure keeps nested visitor state balanced.
|
|
86
86
|
def visit_def_node(node)
|
|
87
87
|
active_class = active_class_accumulator
|
|
88
88
|
unless active_class
|
|
@@ -97,7 +97,6 @@ module ArchUnit
|
|
|
97
97
|
ensure
|
|
98
98
|
@method_stack.pop if active_class
|
|
99
99
|
end
|
|
100
|
-
# rubocop:enable Metrics/MethodLength
|
|
101
100
|
|
|
102
101
|
INSTANCE_VARIABLE_NODES.each do |visitor_method|
|
|
103
102
|
define_method(visitor_method) do |node|
|
|
@@ -271,7 +270,6 @@ module ArchUnit
|
|
|
271
270
|
end
|
|
272
271
|
end
|
|
273
272
|
end
|
|
274
|
-
# rubocop:enable Metrics/ClassLength
|
|
275
273
|
private_constant :SourceMetricsVisitor
|
|
276
274
|
end
|
|
277
275
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative 'logged_metric'
|
|
4
5
|
require_relative '../assertion/custom_metric'
|
|
5
6
|
|
|
6
7
|
module ArchUnit
|
|
@@ -32,7 +33,9 @@ module ArchUnit
|
|
|
32
33
|
)
|
|
33
34
|
return empty_test if empty_test
|
|
34
35
|
|
|
35
|
-
Assertion.gather_custom_metric_violations(
|
|
36
|
+
Assertion.gather_custom_metric_violations(
|
|
37
|
+
classes, LoggedMetric.wrap(selection.metric), predicate
|
|
38
|
+
)
|
|
36
39
|
end
|
|
37
40
|
end
|
|
38
41
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/logging/inspection'
|
|
4
|
+
|
|
5
|
+
module ArchUnit
|
|
6
|
+
module Metrics
|
|
7
|
+
module FluentApi
|
|
8
|
+
# Observes the existing calculation once, including values that pass the rule.
|
|
9
|
+
module LoggedMetric
|
|
10
|
+
def self.wrap(metric)
|
|
11
|
+
logger = Common::Logging::Inspection.logger
|
|
12
|
+
return metric unless logger&.debug?
|
|
13
|
+
|
|
14
|
+
metric.with(calculation: lambda do |subject|
|
|
15
|
+
value = metric.calculate(subject)
|
|
16
|
+
logger.log_metric(name: metric.name, value:, subject: subject.identifier)
|
|
17
|
+
value
|
|
18
|
+
end)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative 'logged_metric'
|
|
4
5
|
require_relative '../assertion/metric_predicate'
|
|
5
6
|
|
|
6
7
|
module ArchUnit
|
|
@@ -34,7 +35,9 @@ module ArchUnit
|
|
|
34
35
|
)
|
|
35
36
|
return empty_test if empty_test
|
|
36
37
|
|
|
37
|
-
Assertion.gather_metric_predicate_violations(
|
|
38
|
+
Assertion.gather_metric_predicate_violations(
|
|
39
|
+
subjects, LoggedMetric.wrap(selection.metric), predicate
|
|
40
|
+
)
|
|
38
41
|
end
|
|
39
42
|
end
|
|
40
43
|
end
|
|
@@ -8,6 +8,8 @@ module ArchUnit
|
|
|
8
8
|
module FluentApi
|
|
9
9
|
# Shared HTML export terminal for one family of scoped metrics.
|
|
10
10
|
module MetricReportBuilder
|
|
11
|
+
# Writes an offline HTML report for all metrics in this family.
|
|
12
|
+
# @return [nil]
|
|
11
13
|
def export_as_html(output_path, options = nil)
|
|
12
14
|
options = Reporting::MetricsExportOptions.resolve(options).with(output_path:)
|
|
13
15
|
Reporting::MetricsExporter.export_as_html(metric_report_data, options)
|
|
@@ -9,6 +9,16 @@ module ArchUnit
|
|
|
9
9
|
module Metrics
|
|
10
10
|
module FluentApi
|
|
11
11
|
# Lazy selection of one metric over a metrics scope.
|
|
12
|
+
# @!method should_be_below(threshold)
|
|
13
|
+
# @return [MetricThresholdCondition]
|
|
14
|
+
# @!method should_be_above(threshold)
|
|
15
|
+
# @return [MetricThresholdCondition]
|
|
16
|
+
# @!method should_be(threshold)
|
|
17
|
+
# @return [MetricThresholdCondition]
|
|
18
|
+
# @!method should_be_below_or_equal(threshold)
|
|
19
|
+
# @return [MetricThresholdCondition]
|
|
20
|
+
# @!method should_be_above_or_equal(threshold)
|
|
21
|
+
# @return [MetricThresholdCondition]
|
|
12
22
|
class MetricSelection
|
|
13
23
|
THRESHOLD_METHODS = {
|
|
14
24
|
should_be_below: :below,
|
|
@@ -29,8 +39,12 @@ module ArchUnit
|
|
|
29
39
|
freeze
|
|
30
40
|
end
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
# Evaluates this metric for every selected subject.
|
|
43
|
+
# @param options [CheckOptions, nil] dependency-analysis options for coupling metrics
|
|
44
|
+
# @return [Array<MetricMeasurement>]
|
|
45
|
+
def measure(options = nil)
|
|
46
|
+
options = Common::FluentApi::CheckOptions.resolve(options)
|
|
47
|
+
scope.__send__(:subjects_for, metric.subject_type, options:).map do |subject|
|
|
34
48
|
MetricMeasurement.new(
|
|
35
49
|
subject:,
|
|
36
50
|
metric_name: metric.name,
|
|
@@ -45,6 +59,8 @@ module ArchUnit
|
|
|
45
59
|
end
|
|
46
60
|
end
|
|
47
61
|
|
|
62
|
+
# Creates a rule from a custom predicate over value and subject.
|
|
63
|
+
# @return [MetricPredicateCondition]
|
|
48
64
|
def should_satisfy(predicate)
|
|
49
65
|
MetricPredicateCondition.new(selection: self, predicate:)
|
|
50
66
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative 'logged_metric'
|
|
4
5
|
require_relative '../assertion/metric_threshold'
|
|
5
6
|
|
|
6
7
|
module ArchUnit
|
|
@@ -38,7 +39,7 @@ module ArchUnit
|
|
|
38
39
|
return empty_test if empty_test
|
|
39
40
|
|
|
40
41
|
Assertion.gather_metric_threshold_violations(
|
|
41
|
-
subjects, selection.metric, comparison, threshold
|
|
42
|
+
subjects, LoggedMetric.wrap(selection.metric), comparison, threshold
|
|
42
43
|
)
|
|
43
44
|
end
|
|
44
45
|
end
|
|
@@ -9,12 +9,17 @@ module ArchUnit
|
|
|
9
9
|
module FluentApi
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
+
# Starts an immutable source-metrics scope.
|
|
13
|
+
# @return [MetricsBuilder]
|
|
12
14
|
def metrics(project_locator = nil)
|
|
13
15
|
MetricsBuilder.new(project_locator:)
|
|
14
16
|
end
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
# Starts an immutable source-metrics scope.
|
|
21
|
+
# @param project_locator [String, Pathname, nil] project directory, Gemfile, or gemspec
|
|
22
|
+
# @return [Metrics::FluentApi::MetricsBuilder]
|
|
18
23
|
def self.metrics(project_locator = nil)
|
|
19
24
|
Metrics::FluentApi.metrics(project_locator)
|
|
20
25
|
end
|
|
@@ -23,41 +23,56 @@ module ArchUnit
|
|
|
23
23
|
freeze
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Selects metric source files by basename.
|
|
27
|
+
# @return [MetricsBuilder]
|
|
26
28
|
def with_name(pattern, except: nil)
|
|
27
29
|
with_filter(Common::RegexFactory.filename_matcher(pattern, except:))
|
|
28
30
|
end
|
|
29
31
|
|
|
32
|
+
# Selects metric source files by containing folder.
|
|
33
|
+
# @return [MetricsBuilder]
|
|
30
34
|
def in_folder(pattern, except: nil)
|
|
31
35
|
with_filter(Common::RegexFactory.folder_matcher(pattern, except:))
|
|
32
36
|
end
|
|
33
37
|
|
|
38
|
+
# Selects metric source files by complete path.
|
|
39
|
+
# @return [MetricsBuilder]
|
|
34
40
|
def in_path(pattern, except: nil)
|
|
35
41
|
with_filter(Common::RegexFactory.path_matcher(pattern, except:))
|
|
36
42
|
end
|
|
37
43
|
|
|
44
|
+
# Selects classes within the selected source files.
|
|
45
|
+
# @return [MetricsBuilder]
|
|
38
46
|
def for_classes_matching(pattern, except: nil)
|
|
39
47
|
with_filter(Common::RegexFactory.classname_matcher(pattern, except:))
|
|
40
48
|
end
|
|
41
49
|
|
|
50
|
+
# @return [CountMetricsBuilder] count metric vocabulary
|
|
42
51
|
def count
|
|
43
52
|
CountMetricsBuilder.new(self)
|
|
44
53
|
end
|
|
45
54
|
|
|
55
|
+
# @return [LCOMMetricsBuilder] cohesion metric vocabulary
|
|
46
56
|
def lcom
|
|
47
57
|
LCOMMetricsBuilder.new(self)
|
|
48
58
|
end
|
|
49
59
|
|
|
60
|
+
# @return [DistanceMetricsBuilder] dependency-distance metric vocabulary
|
|
50
61
|
def distance
|
|
51
62
|
DistanceMetricsBuilder.new(self)
|
|
52
63
|
end
|
|
53
64
|
|
|
65
|
+
# Defines a custom numeric calculation over ClassInfo.
|
|
66
|
+
# @return [CustomMetricBuilder]
|
|
54
67
|
def custom_metric(name, description, calculation)
|
|
55
68
|
CustomMetricBuilder.new(scope: self, name:, description:, calculation:)
|
|
56
69
|
end
|
|
57
70
|
|
|
71
|
+
# @return [MetricProjectInfo] extracted files and classes in this scope
|
|
58
72
|
def analyze
|
|
59
73
|
project = Extraction.extract_project_info(project_locator)
|
|
60
74
|
selected_files = project.files.filter_map { |file| selected_file_info(file) }
|
|
75
|
+
Common::Logging::Inspection.selection('metric file', selected_files, &:path)
|
|
61
76
|
Extraction::ProjectInfo.new(project_root: project.project_root, files: selected_files)
|
|
62
77
|
end
|
|
63
78
|
|
|
@@ -39,6 +39,7 @@ module ArchUnit
|
|
|
39
39
|
|
|
40
40
|
diagram = Uml::PlantUmlParser.parse(diagram_source.read)
|
|
41
41
|
edges = Common::Projection.project_edges(graph, projection)
|
|
42
|
+
Common::Logging::Inspection.projection(edges)
|
|
42
43
|
Assertion.gather_diagram_adherence_violations(edges, diagram, options)
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -41,6 +41,7 @@ module ArchUnit
|
|
|
41
41
|
return empty_test if empty_test
|
|
42
42
|
|
|
43
43
|
edges = Common::Projection.project_edges(graph, projection)
|
|
44
|
+
Common::Logging::Inspection.projection(edges)
|
|
44
45
|
Assertion.gather_forbidden_slice_dependency_violations(
|
|
45
46
|
edges, source_slice, target_slice
|
|
46
47
|
)
|
|
@@ -18,6 +18,8 @@ module ArchUnit
|
|
|
18
18
|
freeze
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Forbids a directed dependency between two named slices.
|
|
22
|
+
# @return [ForbiddenSliceDependencyCondition]
|
|
21
23
|
def contain_dependency(source_slice, target_slice)
|
|
22
24
|
ForbiddenSliceDependencyCondition.new(scope, source_slice:, target_slice:)
|
|
23
25
|
end
|
|
@@ -24,18 +24,26 @@ module ArchUnit
|
|
|
24
24
|
freeze
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Ignores slices that are absent from the diagram and have no dependency edge.
|
|
28
|
+
# @return [PositiveSliceConditionBuilder]
|
|
27
29
|
def ignoring_orphan_slices
|
|
28
30
|
copy(options.with(ignore_orphan_slices: true))
|
|
29
31
|
end
|
|
30
32
|
|
|
33
|
+
# Ignores dependencies to external modules during diagram comparison.
|
|
34
|
+
# @return [PositiveSliceConditionBuilder]
|
|
31
35
|
def ignoring_external_slices
|
|
32
36
|
copy(options.with(ignore_external_slices: true))
|
|
33
37
|
end
|
|
34
38
|
|
|
39
|
+
# Requires the slice graph to match inline PlantUML.
|
|
40
|
+
# @return [DiagramSliceCondition]
|
|
35
41
|
def adhere_to_diagram(text)
|
|
36
42
|
DiagramSliceCondition.new(scope, DiagramSource.inline(text), options:)
|
|
37
43
|
end
|
|
38
44
|
|
|
45
|
+
# Requires the slice graph to match a PlantUML file.
|
|
46
|
+
# @return [DiagramSliceCondition]
|
|
39
47
|
def adhere_to_diagram_in_file(path)
|
|
40
48
|
DiagramSliceCondition.new(scope, DiagramSource.file(path), options:)
|
|
41
49
|
end
|
|
@@ -22,28 +22,40 @@ module ArchUnit
|
|
|
22
22
|
freeze
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Captures slice names with the `(**)` segment in a glob pattern.
|
|
26
|
+
# @return [SliceScopeBuilder]
|
|
25
27
|
def defined_by(pattern, except: nil)
|
|
26
28
|
copy(projection: Projection.slice_by_pattern(pattern, except:))
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
# Captures slice names with the first group in a regular expression.
|
|
32
|
+
# @return [SliceScopeBuilder]
|
|
29
33
|
def defined_by_regex(regexp, except: nil)
|
|
30
34
|
copy(projection: Projection.slice_by_regex(regexp, except:))
|
|
31
35
|
end
|
|
32
36
|
|
|
37
|
+
# Enters the negated slice-rule mood.
|
|
38
|
+
# @return [NegativeSliceConditionBuilder]
|
|
33
39
|
def should_not
|
|
34
40
|
NegativeSliceConditionBuilder.new(self)
|
|
35
41
|
end
|
|
36
42
|
|
|
43
|
+
# Enters the positive slice-rule mood.
|
|
44
|
+
# @return [PositiveSliceConditionBuilder]
|
|
37
45
|
def should
|
|
38
46
|
PositiveSliceConditionBuilder.new(self)
|
|
39
47
|
end
|
|
40
48
|
|
|
49
|
+
# Renders the real slice graph as PlantUML.
|
|
50
|
+
# @return [String]
|
|
41
51
|
def to_plantuml(options = nil)
|
|
42
52
|
graph = extract_graph(options)
|
|
43
53
|
edges = Common::Projection.project_edges(graph, projection)
|
|
44
54
|
Uml::PlantUmlRenderer.render(edges, components: projection.slice_labels(graph))
|
|
45
55
|
end
|
|
46
56
|
|
|
57
|
+
# Writes the real slice graph as PlantUML.
|
|
58
|
+
# @return [nil]
|
|
47
59
|
def export_as_plantuml(output_path, options = nil)
|
|
48
60
|
graph = extract_graph(options)
|
|
49
61
|
edges = Common::Projection.project_edges(graph, projection)
|
|
@@ -9,6 +9,8 @@ module ArchUnit
|
|
|
9
9
|
module FluentApi
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
+
# Starts an immutable slice-rule sentence.
|
|
13
|
+
# @return [SliceScopeBuilder]
|
|
12
14
|
def project_slices(project_locator = nil)
|
|
13
15
|
SliceScopeBuilder.new(project_locator:)
|
|
14
16
|
end
|
|
@@ -19,6 +21,9 @@ module ArchUnit
|
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
|
|
24
|
+
# Starts an immutable slice-rule sentence.
|
|
25
|
+
# @param project_locator [String, Pathname, nil] project directory, Gemfile, or gemspec
|
|
26
|
+
# @return [Slices::FluentApi::SliceScopeBuilder]
|
|
22
27
|
def self.project_slices(project_locator = nil)
|
|
23
28
|
Slices::FluentApi.project_slices(project_locator)
|
|
24
29
|
end
|
|
@@ -10,6 +10,8 @@ module ArchUnit
|
|
|
10
10
|
module Testing
|
|
11
11
|
module_function
|
|
12
12
|
|
|
13
|
+
# Raises AssertionFailure when a rule returns violations.
|
|
14
|
+
# @return [nil]
|
|
13
15
|
def assert_passes(rule, options = nil)
|
|
14
16
|
result = result_for(rule, options)
|
|
15
17
|
return if result.passed?
|
|
@@ -18,6 +20,8 @@ module ArchUnit
|
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
22
|
|
|
23
|
+
# Raises AssertionFailure when a rule returns violations.
|
|
24
|
+
# @return [nil]
|
|
21
25
|
def self.assert_passes(rule, options = nil)
|
|
22
26
|
Testing.assert_passes(rule, options)
|
|
23
27
|
end
|
|
@@ -11,17 +11,29 @@ module ArchUnit
|
|
|
11
11
|
# Shapes a violation collection into one framework-neutral test result.
|
|
12
12
|
class ResultFactory
|
|
13
13
|
class << self
|
|
14
|
-
def from_violations(violations, color: nil, expected_to_pass: true)
|
|
14
|
+
def from_violations(violations, color: nil, expected_to_pass: true, rationale: nil)
|
|
15
15
|
validate_violations(violations)
|
|
16
16
|
validate_expected_to_pass(expected_to_pass)
|
|
17
17
|
color = resolve_color(color)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
result = if violations.empty?
|
|
19
|
+
empty_result(color, expected_to_pass)
|
|
20
|
+
else
|
|
21
|
+
failing_result(violations, color, expected_to_pass)
|
|
22
|
+
end
|
|
23
|
+
explain_result(result, rationale)
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
private
|
|
24
27
|
|
|
28
|
+
def explain_result(result, rationale)
|
|
29
|
+
unless rationale.nil? || (rationale.is_a?(String) && !rationale.strip.empty?)
|
|
30
|
+
raise ArgumentError, 'rationale must be a non-empty String or nil'
|
|
31
|
+
end
|
|
32
|
+
return result unless rationale && result.failed?
|
|
33
|
+
|
|
34
|
+
TestResult.new(passed: false, message: "#{result.message}\n\nBecause: #{rationale.strip}")
|
|
35
|
+
end
|
|
36
|
+
|
|
25
37
|
def passing_result(color)
|
|
26
38
|
message = ColorUtils.green('No architecture violations found.', enabled: color)
|
|
27
39
|
TestResult.new(passed: true, message:)
|
|
@@ -13,11 +13,10 @@ module ArchUnit
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
# RSpec's matcher protocol fixes this method name.
|
|
16
|
-
# rubocop:disable Naming/PredicatePrefix
|
|
16
|
+
# rubocop:disable-next Naming/PredicatePrefix
|
|
17
17
|
def does_not_match?(rule)
|
|
18
18
|
matches_expectation?(rule, expected_to_pass: false)
|
|
19
19
|
end
|
|
20
|
-
# rubocop:enable Naming/PredicatePrefix
|
|
21
20
|
|
|
22
21
|
def failure_message
|
|
23
22
|
result.message
|
data/lib/archunit/testing.rb
CHANGED
|
@@ -14,22 +14,26 @@ module ArchUnit
|
|
|
14
14
|
module Testing
|
|
15
15
|
module_function
|
|
16
16
|
|
|
17
|
+
# Formats structured violations for a terminal or test failure.
|
|
18
|
+
# @return [String]
|
|
17
19
|
def format_violations(violations, color: nil)
|
|
18
20
|
ResultFactory.from_violations(violations, color:).message
|
|
19
21
|
end
|
|
20
22
|
|
|
23
|
+
# Converts a rule check into a framework-neutral TestResult.
|
|
24
|
+
# @return [TestResult]
|
|
21
25
|
def result_for(rule, options = nil, expected_to_pass: true)
|
|
22
26
|
unless rule.is_a?(Common::FluentApi::Checkable)
|
|
23
27
|
raise ArgumentError, 'rule must implement Checkable'
|
|
24
28
|
end
|
|
25
29
|
|
|
26
30
|
violations = rule.check(options)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
ResultFactory.from_violations(violations, expected_to_pass: false)
|
|
31
|
+
ResultFactory.from_violations(violations, expected_to_pass:, rationale: rule.rationale)
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
# Formats structured violations for a terminal or test failure.
|
|
36
|
+
# @return [String]
|
|
33
37
|
def self.format_violations(violations, color: nil)
|
|
34
38
|
Testing.format_violations(violations, color:)
|
|
35
39
|
end
|
data/lib/archunit/version.rb
CHANGED
data/lib/archunit.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'archunit/version'
|
|
4
4
|
require_relative 'archunit/error/technical_error'
|
|
5
|
+
require_relative 'archunit/error/source_parse_error'
|
|
5
6
|
require_relative 'archunit/error/user_error'
|
|
6
7
|
require_relative 'archunit/common/pattern'
|
|
7
8
|
require_relative 'archunit/common/filter'
|
|
@@ -25,8 +26,11 @@ require_relative 'archunit/common/projection/project_to_nodes'
|
|
|
25
26
|
require_relative 'archunit/common/projection/project_cycles'
|
|
26
27
|
require_relative 'archunit/extraction/locate_project'
|
|
27
28
|
require_relative 'archunit/extraction/enumerate_source_files'
|
|
29
|
+
require_relative 'archunit/extraction/resolve_load_paths'
|
|
30
|
+
require_relative 'archunit/extraction/extraction_profile'
|
|
28
31
|
require_relative 'archunit/extraction/resolved_import'
|
|
29
32
|
require_relative 'archunit/extraction/resolve_import'
|
|
33
|
+
require_relative 'archunit/extraction/import_resolver'
|
|
30
34
|
require_relative 'archunit/extraction/extract_imports'
|
|
31
35
|
require_relative 'archunit/extraction/extract_dependencies'
|
|
32
36
|
require_relative 'archunit/extraction/extract_graph'
|
|
@@ -50,6 +54,8 @@ module ArchUnit
|
|
|
50
54
|
Checkable = Common::FluentApi::Checkable
|
|
51
55
|
ImportKind = Common::Extraction::ImportKind
|
|
52
56
|
Edge = Common::Extraction::Edge
|
|
57
|
+
DependencyEvidence = Common::Extraction::DependencyEvidence
|
|
58
|
+
ParseDiagnostic = Common::Extraction::ParseDiagnostic
|
|
53
59
|
Graph = Common::Extraction::Graph
|
|
54
60
|
MappedEdge = Common::Projection::MappedEdge
|
|
55
61
|
ProjectedEdge = Common::Projection::ProjectedEdge
|
|
@@ -104,6 +110,9 @@ module ArchUnit
|
|
|
104
110
|
ResultFactory = Testing::ResultFactory
|
|
105
111
|
AssertionFailure = Testing::AssertionFailure
|
|
106
112
|
|
|
113
|
+
# Clears every cached dependency graph in the current process.
|
|
114
|
+
# Use this after changing analyzed source files between checks.
|
|
115
|
+
# @return [nil]
|
|
107
116
|
def self.clear_graph_cache
|
|
108
117
|
Extraction.clear_graph_cache
|
|
109
118
|
end
|