archunit 0.0.1
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 +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE +7 -0
- data/README.md +375 -0
- data/lib/archunit/common/assertion/empty_test_violation.rb +50 -0
- data/lib/archunit/common/assertion/violation.rb +14 -0
- data/lib/archunit/common/extraction/edge.rb +46 -0
- data/lib/archunit/common/extraction/graph.rb +73 -0
- data/lib/archunit/common/extraction/import_kind.rb +21 -0
- data/lib/archunit/common/filter.rb +46 -0
- data/lib/archunit/common/fluentapi/check_options.rb +48 -0
- data/lib/archunit/common/fluentapi/checkable.rb +70 -0
- data/lib/archunit/common/logging/check_logger.rb +124 -0
- data/lib/archunit/common/logging/logging_options.rb +57 -0
- data/lib/archunit/common/pattern.rb +99 -0
- data/lib/archunit/common/pattern_matching.rb +76 -0
- data/lib/archunit/common/projection/cycles/johnson_cycles.rb +103 -0
- data/lib/archunit/common/projection/cycles/tarjan_scc.rb +82 -0
- data/lib/archunit/common/projection/edge_projections.rb +50 -0
- data/lib/archunit/common/projection/mapped_edge.rb +26 -0
- data/lib/archunit/common/projection/project_cycles.rb +92 -0
- data/lib/archunit/common/projection/project_edges.rb +69 -0
- data/lib/archunit/common/projection/project_to_nodes.rb +59 -0
- data/lib/archunit/common/projection/projected_edge.rb +35 -0
- data/lib/archunit/common/projection/projected_node.rb +36 -0
- data/lib/archunit/common/regex_factory.rb +96 -0
- data/lib/archunit/error/technical_error.rb +6 -0
- data/lib/archunit/error/user_error.rb +6 -0
- data/lib/archunit/extraction/enumerate_source_files.rb +131 -0
- data/lib/archunit/extraction/extract_dependencies.rb +61 -0
- data/lib/archunit/extraction/extract_graph.rb +89 -0
- data/lib/archunit/extraction/extract_imports.rb +197 -0
- data/lib/archunit/extraction/locate_project.rb +80 -0
- data/lib/archunit/extraction/resolve_import.rb +76 -0
- data/lib/archunit/extraction/resolved_import.rb +46 -0
- data/lib/archunit/files/assertion/custom_file_condition.rb +89 -0
- data/lib/archunit/files/assertion/cycle_free.rb +58 -0
- data/lib/archunit/files/assertion/depend_on_external_modules.rb +116 -0
- data/lib/archunit/files/assertion/depend_on_files.rb +110 -0
- data/lib/archunit/files/assertion/matching_files.rb +87 -0
- data/lib/archunit/files/extraction/extract_file_info.rb +84 -0
- data/lib/archunit/files/extraction/file_info.rb +46 -0
- data/lib/archunit/files/fluentapi/custom_file_condition.rb +73 -0
- data/lib/archunit/files/fluentapi/cycle_free_file_condition.rb +55 -0
- data/lib/archunit/files/fluentapi/depend_on_external_module_condition.rb +69 -0
- data/lib/archunit/files/fluentapi/depend_on_external_module_condition_builder.rb +36 -0
- data/lib/archunit/files/fluentapi/depend_on_file_condition.rb +80 -0
- data/lib/archunit/files/fluentapi/depend_on_file_condition_builder.rb +49 -0
- data/lib/archunit/files/fluentapi/file_condition_builder.rb +71 -0
- data/lib/archunit/files/fluentapi/file_rule_support.rb +24 -0
- data/lib/archunit/files/fluentapi/files.rb +31 -0
- data/lib/archunit/files/fluentapi/match_pattern_file_condition.rb +59 -0
- data/lib/archunit/files/fluentapi/match_pattern_file_condition_builder.rb +66 -0
- data/lib/archunit/files/fluentapi/negated_match_pattern_file_condition_builder.rb +16 -0
- data/lib/archunit/files/fluentapi/positive_match_pattern_file_condition_builder.rb +21 -0
- data/lib/archunit/graph/fluentapi/graph.rb +29 -0
- data/lib/archunit/graph/fluentapi/project_graph_builder.rb +106 -0
- data/lib/archunit/graph/projection/aggregate_edges.rb +48 -0
- data/lib/archunit/graph/projection/collapse_node.rb +33 -0
- data/lib/archunit/graph/projection/create_snapshot.rb +84 -0
- data/lib/archunit/graph/projection/folder_depth_collapse.rb +18 -0
- data/lib/archunit/graph/projection/graph_query_options.rb +81 -0
- data/lib/archunit/graph/projection/graph_report_edge.rb +50 -0
- data/lib/archunit/graph/projection/graph_report_node.rb +22 -0
- data/lib/archunit/graph/projection/graph_report_snapshot.rb +43 -0
- data/lib/archunit/graph/projection/graph_report_summary.rb +20 -0
- data/lib/archunit/graph/projection/node_selection.rb +111 -0
- data/lib/archunit/graph/projection/pattern_collapse.rb +24 -0
- data/lib/archunit/graph/rendering/csv_renderer.rb +30 -0
- data/lib/archunit/graph/rendering/d2_renderer.rb +30 -0
- data/lib/archunit/graph/rendering/dot_renderer.rb +51 -0
- data/lib/archunit/graph/rendering/escaping.rb +31 -0
- data/lib/archunit/graph/rendering/export_report.rb +29 -0
- data/lib/archunit/graph/rendering/graph_renderer.rb +54 -0
- data/lib/archunit/graph/rendering/html_document.rb +46 -0
- data/lib/archunit/graph/rendering/html_renderer.rb +102 -0
- data/lib/archunit/graph/rendering/json_renderer.rb +38 -0
- data/lib/archunit/graph/rendering/mermaid_renderer.rb +33 -0
- data/lib/archunit/graph/rendering/rendering_support.rb +24 -0
- data/lib/archunit/layers/assertion/layer_definition.rb +56 -0
- data/lib/archunit/layers/assertion/layer_dependencies.rb +91 -0
- data/lib/archunit/layers/assertion/layer_dependency_violation.rb +59 -0
- data/lib/archunit/layers/fluentapi/layer_definition_builder.rb +46 -0
- data/lib/archunit/layers/fluentapi/layer_dependency_rule_builder.rb +44 -0
- data/lib/archunit/layers/fluentapi/layered_architecture.rb +133 -0
- data/lib/archunit/layers/fluentapi/layered_architecture_values.rb +51 -0
- data/lib/archunit/layers/fluentapi/layers.rb +29 -0
- data/lib/archunit/metrics/assertion/custom_metric.rb +73 -0
- data/lib/archunit/metrics/assertion/metric_predicate.rb +66 -0
- data/lib/archunit/metrics/assertion/metric_threshold.rb +99 -0
- data/lib/archunit/metrics/assertion/metric_zone.rb +57 -0
- data/lib/archunit/metrics/calculation/count.rb +45 -0
- data/lib/archunit/metrics/calculation/distance.rb +84 -0
- data/lib/archunit/metrics/calculation/lcom.rb +103 -0
- data/lib/archunit/metrics/calculation/metric.rb +52 -0
- data/lib/archunit/metrics/calculation/numeric_value.rb +25 -0
- data/lib/archunit/metrics/extraction/distance_info.rb +60 -0
- data/lib/archunit/metrics/extraction/extract_distance_info.rb +50 -0
- data/lib/archunit/metrics/extraction/extract_project_info.rb +90 -0
- data/lib/archunit/metrics/extraction/metric_info.rb +159 -0
- data/lib/archunit/metrics/extraction/source_metrics_visitor.rb +278 -0
- data/lib/archunit/metrics/fluentapi/count_metrics_builder.rb +49 -0
- data/lib/archunit/metrics/fluentapi/custom_metric_builder.rb +30 -0
- data/lib/archunit/metrics/fluentapi/custom_metric_condition.rb +40 -0
- data/lib/archunit/metrics/fluentapi/distance_metrics_builder.rb +48 -0
- data/lib/archunit/metrics/fluentapi/lcom_metrics_builder.rb +39 -0
- data/lib/archunit/metrics/fluentapi/metric_measurement.rb +36 -0
- data/lib/archunit/metrics/fluentapi/metric_predicate_condition.rb +42 -0
- data/lib/archunit/metrics/fluentapi/metric_report_builder.rb +34 -0
- data/lib/archunit/metrics/fluentapi/metric_selection.rb +54 -0
- data/lib/archunit/metrics/fluentapi/metric_threshold_condition.rb +47 -0
- data/lib/archunit/metrics/fluentapi/metrics.rb +21 -0
- data/lib/archunit/metrics/fluentapi/metrics_builder.rb +136 -0
- data/lib/archunit/metrics/fluentapi/zone_condition.rb +40 -0
- data/lib/archunit/metrics/reporting/metrics_export_options.rb +57 -0
- data/lib/archunit/metrics/reporting/metrics_exporter.rb +130 -0
- data/lib/archunit/slices/assertion/adhere_to_diagram.rb +57 -0
- data/lib/archunit/slices/assertion/contain_dependency.rb +42 -0
- data/lib/archunit/slices/assertion/diagram_adherence_options.rb +37 -0
- data/lib/archunit/slices/assertion/slice_dependency_violation.rb +70 -0
- data/lib/archunit/slices/fluentapi/diagram_slice_condition.rb +65 -0
- data/lib/archunit/slices/fluentapi/diagram_source.rb +36 -0
- data/lib/archunit/slices/fluentapi/forbidden_slice_dependency_condition.rb +57 -0
- data/lib/archunit/slices/fluentapi/negative_slice_condition_builder.rb +27 -0
- data/lib/archunit/slices/fluentapi/positive_slice_condition_builder.rb +51 -0
- data/lib/archunit/slices/fluentapi/slice_scope_builder.rb +83 -0
- data/lib/archunit/slices/fluentapi/slices.rb +29 -0
- data/lib/archunit/slices/projection/slice_projection.rb +76 -0
- data/lib/archunit/slices/projection/slicing_projections.rb +98 -0
- data/lib/archunit/slices/uml/export_diagram.rb +70 -0
- data/lib/archunit/slices/uml/parse_diagram.rb +53 -0
- data/lib/archunit/slices/uml/plant_uml_dependency.rb +25 -0
- data/lib/archunit/slices/uml/plant_uml_diagram.rb +44 -0
- data/lib/archunit/testing/assert_passes.rb +24 -0
- data/lib/archunit/testing/assertion_failure.rb +22 -0
- data/lib/archunit/testing/color_utils.rb +66 -0
- data/lib/archunit/testing/layer_violation_formatter.rb +26 -0
- data/lib/archunit/testing/metric_violation_formatter.rb +58 -0
- data/lib/archunit/testing/minitest_adapter.rb +33 -0
- data/lib/archunit/testing/result_factory.rb +93 -0
- data/lib/archunit/testing/rspec_adapter.rb +74 -0
- data/lib/archunit/testing/slice_violation_formatter.rb +25 -0
- data/lib/archunit/testing/test_result.rb +26 -0
- data/lib/archunit/testing/test_violation.rb +23 -0
- data/lib/archunit/testing/violation_factory.rb +138 -0
- data/lib/archunit/testing.rb +39 -0
- data/lib/archunit/version.rb +5 -0
- data/lib/archunit.rb +110 -0
- metadata +251 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/assertion/violation'
|
|
4
|
+
require_relative '../../common/filter'
|
|
5
|
+
require_relative '../../common/pattern_matching'
|
|
6
|
+
require_relative '../../common/projection/projected_edge'
|
|
7
|
+
|
|
8
|
+
module ArchUnit
|
|
9
|
+
module Files
|
|
10
|
+
# Pure assertions over dependencies from project files to external modules.
|
|
11
|
+
module Assertion
|
|
12
|
+
# Data describing an external module dependency that disagrees with a rule.
|
|
13
|
+
class ExternalModuleDependencyViolation < Common::Assertion::Violation
|
|
14
|
+
attr_reader :dependency, :is_negated
|
|
15
|
+
|
|
16
|
+
def initialize(dependency:, is_negated: false)
|
|
17
|
+
@dependency = projected_edge(dependency)
|
|
18
|
+
@is_negated = boolean_value(is_negated)
|
|
19
|
+
super()
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def negated?
|
|
23
|
+
is_negated
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ==(other)
|
|
27
|
+
other.is_a?(self.class) &&
|
|
28
|
+
dependency == other.dependency &&
|
|
29
|
+
is_negated == other.is_negated
|
|
30
|
+
end
|
|
31
|
+
alias eql? ==
|
|
32
|
+
|
|
33
|
+
def hash
|
|
34
|
+
[self.class, dependency, is_negated].hash
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def projected_edge(value)
|
|
40
|
+
return value if value.is_a?(Common::Projection::ProjectedEdge)
|
|
41
|
+
|
|
42
|
+
raise ArgumentError, 'dependency must be a ProjectedEdge'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def boolean_value(value)
|
|
46
|
+
return value if [true, false].include?(value)
|
|
47
|
+
|
|
48
|
+
raise ArgumentError, 'is_negated must be true or false'
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
def gather_external_module_dependency_violations(
|
|
55
|
+
edges, subject_filters, module_filters, is_negated:
|
|
56
|
+
)
|
|
57
|
+
validate_external_dependency_arguments(
|
|
58
|
+
edges, subject_filters, module_filters, is_negated
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
edges.filter_map do |edge|
|
|
62
|
+
external_dependency_violation(
|
|
63
|
+
edge, subject_filters, module_filters, is_negated
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def external_dependency_violation(edge, subject_filters, module_filters, is_negated)
|
|
69
|
+
return unless Common::PatternMatching.matches_all_patterns?(
|
|
70
|
+
edge.source_label, subject_filters
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
target_matches = Common::PatternMatching.matches_any_pattern?(
|
|
74
|
+
edge.target_label, module_filters
|
|
75
|
+
)
|
|
76
|
+
return unless is_negated ? target_matches : !target_matches
|
|
77
|
+
|
|
78
|
+
ExternalModuleDependencyViolation.new(dependency: edge, is_negated:)
|
|
79
|
+
end
|
|
80
|
+
private_class_method :external_dependency_violation
|
|
81
|
+
|
|
82
|
+
def validate_external_dependency_arguments(
|
|
83
|
+
edges, subject_filters, module_filters, is_negated
|
|
84
|
+
)
|
|
85
|
+
validate_external_projected_edges(edges)
|
|
86
|
+
validate_external_filters(subject_filters, :subject_filters, allow_empty: true)
|
|
87
|
+
validate_external_filters(module_filters, :module_filters, allow_empty: false)
|
|
88
|
+
return if [true, false].include?(is_negated)
|
|
89
|
+
|
|
90
|
+
raise ArgumentError, 'is_negated must be true or false'
|
|
91
|
+
end
|
|
92
|
+
private_class_method :validate_external_dependency_arguments
|
|
93
|
+
|
|
94
|
+
def validate_external_projected_edges(values)
|
|
95
|
+
return if values.is_a?(Array) && values.all?(Common::Projection::ProjectedEdge)
|
|
96
|
+
|
|
97
|
+
raise ArgumentError, 'edges must be an Array of ProjectedEdge values'
|
|
98
|
+
end
|
|
99
|
+
private_class_method :validate_external_projected_edges
|
|
100
|
+
|
|
101
|
+
def validate_external_filters(values, name, allow_empty:)
|
|
102
|
+
valid = values.is_a?(Array) && values.all?(Common::Filter)
|
|
103
|
+
valid &&= !values.empty? unless allow_empty
|
|
104
|
+
return if valid
|
|
105
|
+
|
|
106
|
+
requirement = if allow_empty
|
|
107
|
+
'an Array of Filter values'
|
|
108
|
+
else
|
|
109
|
+
'a non-empty Array of Filter values'
|
|
110
|
+
end
|
|
111
|
+
raise ArgumentError, "#{name} must be #{requirement}"
|
|
112
|
+
end
|
|
113
|
+
private_class_method :validate_external_filters
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/assertion/violation'
|
|
4
|
+
require_relative '../../common/filter'
|
|
5
|
+
require_relative '../../common/pattern_matching'
|
|
6
|
+
require_relative '../../common/projection/projected_edge'
|
|
7
|
+
|
|
8
|
+
module ArchUnit
|
|
9
|
+
module Files
|
|
10
|
+
# Pure assertions over dependencies between project files.
|
|
11
|
+
module Assertion
|
|
12
|
+
# Data describing an internal file dependency that disagrees with a rule.
|
|
13
|
+
class FileDependencyViolation < Common::Assertion::Violation
|
|
14
|
+
attr_reader :dependency, :is_negated
|
|
15
|
+
|
|
16
|
+
def initialize(dependency:, is_negated: false)
|
|
17
|
+
@dependency = projected_edge(dependency)
|
|
18
|
+
@is_negated = boolean_value(is_negated)
|
|
19
|
+
super()
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def negated?
|
|
23
|
+
is_negated
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ==(other)
|
|
27
|
+
other.is_a?(self.class) &&
|
|
28
|
+
dependency == other.dependency &&
|
|
29
|
+
is_negated == other.is_negated
|
|
30
|
+
end
|
|
31
|
+
alias eql? ==
|
|
32
|
+
|
|
33
|
+
def hash
|
|
34
|
+
[self.class, dependency, is_negated].hash
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def projected_edge(value)
|
|
40
|
+
return value if value.is_a?(Common::Projection::ProjectedEdge)
|
|
41
|
+
|
|
42
|
+
raise ArgumentError, 'dependency must be a ProjectedEdge'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def boolean_value(value)
|
|
46
|
+
return value if [true, false].include?(value)
|
|
47
|
+
|
|
48
|
+
raise ArgumentError, 'is_negated must be true or false'
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
module_function
|
|
53
|
+
|
|
54
|
+
def gather_file_dependency_violations(
|
|
55
|
+
edges, subject_filters, object_filters, is_negated:
|
|
56
|
+
)
|
|
57
|
+
validate_dependency_arguments(edges, subject_filters, object_filters, is_negated)
|
|
58
|
+
|
|
59
|
+
edges.filter_map do |edge|
|
|
60
|
+
dependency_violation(edge, subject_filters, object_filters, is_negated)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dependency_violation(edge, subject_filters, object_filters, is_negated)
|
|
65
|
+
return unless Common::PatternMatching.matches_all_patterns?(
|
|
66
|
+
edge.source_label, subject_filters
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
target_matches = Common::PatternMatching.matches_all_patterns?(
|
|
70
|
+
edge.target_label, object_filters
|
|
71
|
+
)
|
|
72
|
+
return unless is_negated ? target_matches : !target_matches
|
|
73
|
+
|
|
74
|
+
FileDependencyViolation.new(dependency: edge, is_negated:)
|
|
75
|
+
end
|
|
76
|
+
private_class_method :dependency_violation
|
|
77
|
+
|
|
78
|
+
def validate_dependency_arguments(edges, subject_filters, object_filters, is_negated)
|
|
79
|
+
validate_projected_edges(edges)
|
|
80
|
+
validate_filters(subject_filters, :subject_filters, allow_empty: true)
|
|
81
|
+
validate_filters(object_filters, :object_filters, allow_empty: false)
|
|
82
|
+
return if [true, false].include?(is_negated)
|
|
83
|
+
|
|
84
|
+
raise ArgumentError, 'is_negated must be true or false'
|
|
85
|
+
end
|
|
86
|
+
private_class_method :validate_dependency_arguments
|
|
87
|
+
|
|
88
|
+
def validate_projected_edges(values)
|
|
89
|
+
return if values.is_a?(Array) && values.all?(Common::Projection::ProjectedEdge)
|
|
90
|
+
|
|
91
|
+
raise ArgumentError, 'edges must be an Array of ProjectedEdge values'
|
|
92
|
+
end
|
|
93
|
+
private_class_method :validate_projected_edges
|
|
94
|
+
|
|
95
|
+
def validate_filters(values, name, allow_empty:)
|
|
96
|
+
valid = values.is_a?(Array) && values.all?(Common::Filter)
|
|
97
|
+
valid &&= !values.empty? unless allow_empty
|
|
98
|
+
return if valid
|
|
99
|
+
|
|
100
|
+
requirement = if allow_empty
|
|
101
|
+
'an Array of Filter values'
|
|
102
|
+
else
|
|
103
|
+
'a non-empty Array of Filter values'
|
|
104
|
+
end
|
|
105
|
+
raise ArgumentError, "#{name} must be #{requirement}"
|
|
106
|
+
end
|
|
107
|
+
private_class_method :validate_filters
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/assertion/violation'
|
|
4
|
+
require_relative '../../common/filter'
|
|
5
|
+
require_relative '../../common/pattern_matching'
|
|
6
|
+
require_relative '../../common/projection/projected_node'
|
|
7
|
+
|
|
8
|
+
module ArchUnit
|
|
9
|
+
module Files
|
|
10
|
+
# Pure file-pattern assertion functions and violation values.
|
|
11
|
+
module Assertion
|
|
12
|
+
# Data describing a selected file that disagrees with a pattern predicate.
|
|
13
|
+
class FilePatternViolation < Common::Assertion::Violation
|
|
14
|
+
attr_reader :check_filter, :projected_node, :is_negated
|
|
15
|
+
|
|
16
|
+
def initialize(check_filter:, projected_node:, is_negated: false)
|
|
17
|
+
@check_filter = filter_value(check_filter)
|
|
18
|
+
@projected_node = node_value(projected_node)
|
|
19
|
+
@is_negated = boolean_value(is_negated)
|
|
20
|
+
super()
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def negated?
|
|
24
|
+
is_negated
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def ==(other)
|
|
28
|
+
other.is_a?(self.class) &&
|
|
29
|
+
check_filter == other.check_filter &&
|
|
30
|
+
projected_node == other.projected_node &&
|
|
31
|
+
is_negated == other.is_negated
|
|
32
|
+
end
|
|
33
|
+
alias eql? ==
|
|
34
|
+
|
|
35
|
+
def hash
|
|
36
|
+
[self.class, check_filter, projected_node, is_negated].hash
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def filter_value(value)
|
|
42
|
+
return value if value.is_a?(Common::Filter)
|
|
43
|
+
|
|
44
|
+
raise ArgumentError, 'check_filter must be a Filter'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def node_value(value)
|
|
48
|
+
return value if value.is_a?(Common::Projection::ProjectedNode)
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, 'projected_node must be a ProjectedNode'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def boolean_value(value)
|
|
54
|
+
return value if [true, false].include?(value)
|
|
55
|
+
|
|
56
|
+
raise ArgumentError, 'is_negated must be true or false'
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
module_function
|
|
61
|
+
|
|
62
|
+
def gather_matching_file_violations(nodes, check_filter, is_negated:)
|
|
63
|
+
validate_matching_arguments(nodes, check_filter, is_negated)
|
|
64
|
+
|
|
65
|
+
nodes.filter_map do |node|
|
|
66
|
+
matches = Common::PatternMatching.matches_pattern?(node.label, check_filter)
|
|
67
|
+
next unless is_negated ? matches : !matches
|
|
68
|
+
|
|
69
|
+
FilePatternViolation.new(check_filter:, projected_node: node, is_negated:)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_matching_arguments(nodes, check_filter, is_negated)
|
|
74
|
+
unless nodes.is_a?(Array) && nodes.all?(Common::Projection::ProjectedNode)
|
|
75
|
+
raise ArgumentError, 'nodes must be an Array of ProjectedNode values'
|
|
76
|
+
end
|
|
77
|
+
unless check_filter.is_a?(Common::Filter)
|
|
78
|
+
raise ArgumentError, 'check_filter must be a Filter'
|
|
79
|
+
end
|
|
80
|
+
return if [true, false].include?(is_negated)
|
|
81
|
+
|
|
82
|
+
raise ArgumentError, 'is_negated must be true or false'
|
|
83
|
+
end
|
|
84
|
+
private_class_method :validate_matching_arguments
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require_relative '../../error/technical_error'
|
|
5
|
+
require_relative 'file_info'
|
|
6
|
+
|
|
7
|
+
module ArchUnit
|
|
8
|
+
module Files
|
|
9
|
+
# Ruby-specific descriptive data extraction for file rules.
|
|
10
|
+
module Extraction
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def extract_file_info(project_root, relative_path)
|
|
14
|
+
root = project_directory(project_root)
|
|
15
|
+
identifier = normalized_identifier(relative_path)
|
|
16
|
+
source_path = source_path_within(root, identifier)
|
|
17
|
+
build_file_info(identifier, read_source(source_path))
|
|
18
|
+
rescue TechnicalError
|
|
19
|
+
raise
|
|
20
|
+
rescue SystemCallError => e
|
|
21
|
+
raise TechnicalError, "could not extract file information: #{e.message}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def read_source(path)
|
|
25
|
+
File.binread(path).force_encoding(Encoding::UTF_8).scrub
|
|
26
|
+
end
|
|
27
|
+
private_class_method :read_source
|
|
28
|
+
|
|
29
|
+
def build_file_info(identifier, content)
|
|
30
|
+
extension = File.extname(identifier)
|
|
31
|
+
FileInfo.new(
|
|
32
|
+
path: identifier,
|
|
33
|
+
name: File.basename(identifier, extension),
|
|
34
|
+
extension:,
|
|
35
|
+
directory: directory_of(identifier),
|
|
36
|
+
content:,
|
|
37
|
+
lines_of_code: content.lines.count { |line| !line.strip.empty? }
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
private_class_method :build_file_info
|
|
41
|
+
|
|
42
|
+
def project_directory(value)
|
|
43
|
+
value = value.to_path if value.respond_to?(:to_path)
|
|
44
|
+
unless value.is_a?(String) && !value.empty?
|
|
45
|
+
raise ArgumentError, 'project_root must be a non-empty path'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
root = Pathname.new(value).expand_path
|
|
49
|
+
raise TechnicalError, 'project_root must be an existing directory' unless root.directory?
|
|
50
|
+
|
|
51
|
+
root.realpath
|
|
52
|
+
end
|
|
53
|
+
private_class_method :project_directory
|
|
54
|
+
|
|
55
|
+
def normalized_identifier(value)
|
|
56
|
+
unless value.is_a?(String) && !value.empty?
|
|
57
|
+
raise ArgumentError, 'relative_path must be a non-empty String'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
value.tr('\\', '/').freeze
|
|
61
|
+
end
|
|
62
|
+
private_class_method :normalized_identifier
|
|
63
|
+
|
|
64
|
+
def source_path_within(root, identifier)
|
|
65
|
+
path = root.join(identifier).cleanpath
|
|
66
|
+
relative = path.relative_path_from(root).to_s.tr('\\', '/')
|
|
67
|
+
if relative == '..' || relative.start_with?('../')
|
|
68
|
+
raise TechnicalError, 'source file must be inside the project root'
|
|
69
|
+
end
|
|
70
|
+
raise TechnicalError, "source file does not exist: #{identifier}" unless path.file?
|
|
71
|
+
|
|
72
|
+
path
|
|
73
|
+
end
|
|
74
|
+
private_class_method :source_path_within
|
|
75
|
+
|
|
76
|
+
def directory_of(identifier)
|
|
77
|
+
return '' unless identifier.include?('/')
|
|
78
|
+
|
|
79
|
+
File.dirname(identifier).tr('\\', '/').freeze
|
|
80
|
+
end
|
|
81
|
+
private_class_method :directory_of
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArchUnit
|
|
4
|
+
module Files
|
|
5
|
+
# Ruby-specific descriptive data extraction for file rules.
|
|
6
|
+
module Extraction
|
|
7
|
+
# Immutable source-file information exposed to custom predicates.
|
|
8
|
+
FileInfo = Data.define(
|
|
9
|
+
:path, :name, :extension, :directory, :content, :lines_of_code
|
|
10
|
+
) do
|
|
11
|
+
# rubocop:disable Metrics/ParameterLists -- FileInfo has six required public fields.
|
|
12
|
+
def initialize(path:, name:, extension:, directory:, content:, lines_of_code:)
|
|
13
|
+
path = immutable_string(path, :path, allow_empty: false, normalize_path: true)
|
|
14
|
+
name = immutable_string(name, :name, allow_empty: false)
|
|
15
|
+
extension = immutable_string(extension, :extension, allow_empty: true)
|
|
16
|
+
directory = immutable_string(
|
|
17
|
+
directory, :directory, allow_empty: true, normalize_path: true
|
|
18
|
+
)
|
|
19
|
+
content = immutable_string(content, :content, allow_empty: true)
|
|
20
|
+
lines_of_code = non_negative_integer(lines_of_code)
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
# rubocop:enable Metrics/ParameterLists
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def immutable_string(value, attribute, allow_empty:, normalize_path: false)
|
|
28
|
+
valid = value.is_a?(String) && (allow_empty || !value.empty?)
|
|
29
|
+
unless valid
|
|
30
|
+
requirement = allow_empty ? 'a String' : 'a non-empty String'
|
|
31
|
+
raise ArgumentError, "#{attribute} must be #{requirement}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
value = value.tr('\\', '/') if normalize_path
|
|
35
|
+
value.dup.freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def non_negative_integer(value)
|
|
39
|
+
return value if value.is_a?(Integer) && value >= 0
|
|
40
|
+
|
|
41
|
+
raise ArgumentError, 'lines_of_code must be a non-negative Integer'
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative '../../extraction/extract_graph'
|
|
5
|
+
require_relative '../../extraction/locate_project'
|
|
6
|
+
require_relative '../assertion/custom_file_condition'
|
|
7
|
+
require_relative '../extraction/extract_file_info'
|
|
8
|
+
require_relative 'file_rule_support'
|
|
9
|
+
|
|
10
|
+
module ArchUnit
|
|
11
|
+
module Files
|
|
12
|
+
module FluentApi
|
|
13
|
+
# Executable custom predicate evaluated against immutable FileInfo values.
|
|
14
|
+
class CustomFileCondition
|
|
15
|
+
include Common::FluentApi::Checkable
|
|
16
|
+
|
|
17
|
+
attr_reader :project_locator, :subject_filters, :condition, :message, :is_negated
|
|
18
|
+
|
|
19
|
+
def initialize(mood, condition:, message:)
|
|
20
|
+
validate_mood(mood)
|
|
21
|
+
@project_locator = mood.project_locator
|
|
22
|
+
@subject_filters = mood.filters
|
|
23
|
+
@condition = callable_value(condition)
|
|
24
|
+
@message = message_value(message)
|
|
25
|
+
@is_negated = mood.negated?
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def negated?
|
|
30
|
+
is_negated
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def validate_mood(value)
|
|
36
|
+
return if value.is_a?(MatchPatternFileConditionBuilder)
|
|
37
|
+
|
|
38
|
+
raise ArgumentError, 'mood must be a file condition builder'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def callable_value(value)
|
|
42
|
+
return value if value.respond_to?(:call)
|
|
43
|
+
|
|
44
|
+
raise ArgumentError, 'condition must be callable'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def message_value(value)
|
|
48
|
+
return value.dup.freeze if value.is_a?(String) && !value.strip.empty?
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, 'message must be a non-empty String'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def perform_check(options)
|
|
54
|
+
graph = ArchUnit::Extraction.extract_graph(project_locator, options:)
|
|
55
|
+
nodes = FileRuleSupport.selected_nodes(graph, subject_filters)
|
|
56
|
+
empty_test = empty_test_violation(
|
|
57
|
+
nodes, filters: subject_filters, negated: is_negated, options:
|
|
58
|
+
)
|
|
59
|
+
return empty_test if empty_test
|
|
60
|
+
|
|
61
|
+
Assertion.gather_custom_file_violations(
|
|
62
|
+
extract_file_infos(nodes), condition, message, is_negated:
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def extract_file_infos(nodes)
|
|
67
|
+
root = ArchUnit::Extraction.locate_project(project_locator)
|
|
68
|
+
nodes.map { |node| Files::Extraction.extract_file_info(root, node.label) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative '../../common/projection/edge_projections'
|
|
5
|
+
require_relative '../../common/projection/project_cycles'
|
|
6
|
+
require_relative '../../common/projection/project_edges'
|
|
7
|
+
require_relative '../../extraction/extract_graph'
|
|
8
|
+
require_relative '../assertion/cycle_free'
|
|
9
|
+
require_relative 'file_rule_support'
|
|
10
|
+
|
|
11
|
+
module ArchUnit
|
|
12
|
+
module Files
|
|
13
|
+
module FluentApi
|
|
14
|
+
# Executable positive rule requiring the selected file graph to be acyclic.
|
|
15
|
+
class CycleFreeFileCondition
|
|
16
|
+
include Common::FluentApi::Checkable
|
|
17
|
+
|
|
18
|
+
attr_reader :project_locator, :filters
|
|
19
|
+
|
|
20
|
+
def initialize(mood)
|
|
21
|
+
unless mood.is_a?(PositiveMatchPatternFileConditionBuilder)
|
|
22
|
+
raise ArgumentError, 'mood must be a positive file condition builder'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@project_locator = mood.project_locator
|
|
26
|
+
@filters = mood.filters
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def perform_check(options)
|
|
33
|
+
graph = ArchUnit::Extraction.extract_graph(project_locator, options:)
|
|
34
|
+
nodes = FileRuleSupport.selected_nodes(graph, filters)
|
|
35
|
+
empty_test = empty_test_violation(
|
|
36
|
+
nodes, filters:, negated: false, options:
|
|
37
|
+
)
|
|
38
|
+
return empty_test if empty_test
|
|
39
|
+
|
|
40
|
+
cycles = cycles_within(graph, nodes)
|
|
41
|
+
Assertion.gather_cycle_violations(cycles)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def cycles_within(graph, nodes)
|
|
45
|
+
selected_labels = nodes.to_h { |node| [node.label, true] }
|
|
46
|
+
edges = Common::Projection.project_edges(graph, Common::Projection.per_internal_edge)
|
|
47
|
+
.select do |edge|
|
|
48
|
+
selected_labels[edge.source_label] && selected_labels[edge.target_label]
|
|
49
|
+
end
|
|
50
|
+
Common::Projection.project_cycles(edges)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative '../../common/projection/edge_projections'
|
|
5
|
+
require_relative '../../common/projection/project_edges'
|
|
6
|
+
require_relative '../../common/regex_factory'
|
|
7
|
+
require_relative '../../extraction/extract_graph'
|
|
8
|
+
require_relative '../assertion/depend_on_external_modules'
|
|
9
|
+
require_relative 'file_rule_support'
|
|
10
|
+
|
|
11
|
+
module ArchUnit
|
|
12
|
+
module Files
|
|
13
|
+
module FluentApi
|
|
14
|
+
# Executable external-module rule with OR-combined module selectors.
|
|
15
|
+
class DependOnExternalModuleCondition
|
|
16
|
+
include Common::FluentApi::Checkable
|
|
17
|
+
|
|
18
|
+
attr_reader :builder, :project_locator, :subject_filters, :module_filters, :is_negated
|
|
19
|
+
|
|
20
|
+
def initialize(builder, module_filters:)
|
|
21
|
+
unless builder.is_a?(DependOnExternalModuleConditionBuilder)
|
|
22
|
+
raise ArgumentError, 'builder must be a DependOnExternalModuleConditionBuilder'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@builder = builder
|
|
26
|
+
@project_locator = builder.project_locator
|
|
27
|
+
@subject_filters = builder.subject_filters
|
|
28
|
+
@module_filters = immutable_module_filters(module_filters)
|
|
29
|
+
@is_negated = builder.negated?
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def negated?
|
|
34
|
+
is_negated
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def matching(module_name, except: nil)
|
|
38
|
+
filter = Common::RegexFactory.path_matcher(module_name, except:)
|
|
39
|
+
self.class.new(builder, module_filters: [*module_filters, filter])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def immutable_module_filters(values)
|
|
45
|
+
filters = Array(values).dup
|
|
46
|
+
unless !filters.empty? && filters.all?(Common::Filter)
|
|
47
|
+
raise ArgumentError, 'module_filters must contain at least one Filter'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
filters.freeze
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def perform_check(options)
|
|
54
|
+
graph = ArchUnit::Extraction.extract_graph(project_locator, options:)
|
|
55
|
+
nodes = FileRuleSupport.selected_nodes(graph, subject_filters)
|
|
56
|
+
empty_test = empty_test_violation(
|
|
57
|
+
nodes, filters: subject_filters, negated: is_negated, options:
|
|
58
|
+
)
|
|
59
|
+
return empty_test if empty_test
|
|
60
|
+
|
|
61
|
+
edges = Common::Projection.project_edges(graph, Common::Projection.per_external_edge)
|
|
62
|
+
Assertion.gather_external_module_dependency_violations(
|
|
63
|
+
edges, subject_filters, module_filters, is_negated:
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/regex_factory'
|
|
4
|
+
require_relative 'depend_on_external_module_condition'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Files
|
|
8
|
+
module FluentApi
|
|
9
|
+
# Immutable object stage selecting external dependency names.
|
|
10
|
+
class DependOnExternalModuleConditionBuilder
|
|
11
|
+
attr_reader :mood, :project_locator, :subject_filters, :is_negated
|
|
12
|
+
|
|
13
|
+
def initialize(mood)
|
|
14
|
+
unless mood.is_a?(MatchPatternFileConditionBuilder)
|
|
15
|
+
raise ArgumentError, 'mood must be a file condition builder'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@mood = mood
|
|
19
|
+
@project_locator = mood.project_locator
|
|
20
|
+
@subject_filters = mood.filters
|
|
21
|
+
@is_negated = mood.negated?
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def negated?
|
|
26
|
+
is_negated
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def matching(module_name, except: nil)
|
|
30
|
+
filter = Common::RegexFactory.path_matcher(module_name, except:)
|
|
31
|
+
DependOnExternalModuleCondition.new(self, module_filters: [filter])
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|