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,80 @@
|
|
|
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_files'
|
|
9
|
+
require_relative 'file_rule_support'
|
|
10
|
+
|
|
11
|
+
module ArchUnit
|
|
12
|
+
module Files
|
|
13
|
+
module FluentApi
|
|
14
|
+
# Executable internal file-dependency rule with chainable object selectors.
|
|
15
|
+
class DependOnFileCondition
|
|
16
|
+
include Common::FluentApi::Checkable
|
|
17
|
+
|
|
18
|
+
attr_reader :builder, :project_locator, :subject_filters, :object_filters, :is_negated
|
|
19
|
+
|
|
20
|
+
def initialize(builder, object_filters:)
|
|
21
|
+
unless builder.is_a?(DependOnFileConditionBuilder)
|
|
22
|
+
raise ArgumentError, 'builder must be a DependOnFileConditionBuilder'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@builder = builder
|
|
26
|
+
@project_locator = builder.project_locator
|
|
27
|
+
@subject_filters = builder.subject_filters
|
|
28
|
+
@object_filters = immutable_object_filters(object_filters)
|
|
29
|
+
@is_negated = builder.negated?
|
|
30
|
+
freeze
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def negated?
|
|
34
|
+
is_negated
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_name(pattern, except: nil)
|
|
38
|
+
with_filter(Common::RegexFactory.filename_matcher(pattern, except:))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def in_folder(pattern, except: nil)
|
|
42
|
+
with_filter(Common::RegexFactory.folder_matcher(pattern, except:))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def in_path(pattern, except: nil)
|
|
46
|
+
with_filter(Common::RegexFactory.path_matcher(pattern, except:))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def immutable_object_filters(values)
|
|
52
|
+
filters = Array(values).dup
|
|
53
|
+
unless !filters.empty? && filters.all?(Common::Filter)
|
|
54
|
+
raise ArgumentError, 'object_filters must contain at least one Filter'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
filters.freeze
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def with_filter(filter)
|
|
61
|
+
self.class.new(builder, object_filters: [*object_filters, filter])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def perform_check(options)
|
|
65
|
+
graph = ArchUnit::Extraction.extract_graph(project_locator, options:)
|
|
66
|
+
nodes = FileRuleSupport.selected_nodes(graph, subject_filters)
|
|
67
|
+
empty_test = empty_test_violation(
|
|
68
|
+
nodes, filters: subject_filters, negated: is_negated, options:
|
|
69
|
+
)
|
|
70
|
+
return empty_test if empty_test
|
|
71
|
+
|
|
72
|
+
edges = Common::Projection.project_edges(graph, Common::Projection.per_internal_edge)
|
|
73
|
+
Assertion.gather_file_dependency_violations(
|
|
74
|
+
edges, subject_filters, object_filters, is_negated:
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/regex_factory'
|
|
4
|
+
require_relative 'depend_on_file_condition'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Files
|
|
8
|
+
module FluentApi
|
|
9
|
+
# Immutable object stage selecting internal dependency targets.
|
|
10
|
+
class DependOnFileConditionBuilder
|
|
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 with_name(pattern, except: nil)
|
|
30
|
+
condition(Common::RegexFactory.filename_matcher(pattern, except:))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def in_folder(pattern, except: nil)
|
|
34
|
+
condition(Common::RegexFactory.folder_matcher(pattern, except:))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def in_path(pattern, except: nil)
|
|
38
|
+
condition(Common::RegexFactory.path_matcher(pattern, except:))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def condition(filter)
|
|
44
|
+
DependOnFileCondition.new(self, object_filters: [filter])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/filter'
|
|
4
|
+
require_relative '../../common/regex_factory'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Files
|
|
8
|
+
module FluentApi
|
|
9
|
+
# Immutable scope builder selecting the files to which a rule will apply.
|
|
10
|
+
class FileConditionBuilder
|
|
11
|
+
attr_reader :project_locator, :filters
|
|
12
|
+
|
|
13
|
+
def initialize(project_locator: nil, filters: [])
|
|
14
|
+
@project_locator = immutable_project_locator(project_locator)
|
|
15
|
+
@filters = immutable_filters(filters)
|
|
16
|
+
freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def with_name(pattern, except: nil)
|
|
20
|
+
with_filter(Common::RegexFactory.filename_matcher(pattern, except:))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def in_folder(pattern, except: nil)
|
|
24
|
+
with_filter(Common::RegexFactory.folder_matcher(pattern, except:))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def in_path(pattern, except: nil)
|
|
28
|
+
with_filter(Common::RegexFactory.path_matcher(pattern, except:))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def in_file(file_path, except: nil)
|
|
32
|
+
with_filter(Common::RegexFactory.exact_file_matcher(file_path, except:))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def should
|
|
36
|
+
PositiveMatchPatternFileConditionBuilder.new(self)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def should_not
|
|
40
|
+
NegatedMatchPatternFileConditionBuilder.new(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def with_filter(filter)
|
|
46
|
+
self.class.new(project_locator:, filters: [*filters, filter])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def immutable_project_locator(locator)
|
|
50
|
+
return if locator.nil?
|
|
51
|
+
|
|
52
|
+
locator = locator.to_path if locator.respond_to?(:to_path)
|
|
53
|
+
unless locator.is_a?(String) && !locator.empty?
|
|
54
|
+
raise ArgumentError, 'project_locator must be a non-empty path or nil'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
locator.dup.freeze
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def immutable_filters(values)
|
|
61
|
+
filters = Array(values).dup
|
|
62
|
+
unless filters.all?(Common::Filter)
|
|
63
|
+
raise ArgumentError, 'filters must contain only Filter values'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
filters.freeze
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/pattern_matching'
|
|
4
|
+
require_relative '../../common/projection/project_to_nodes'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Files
|
|
8
|
+
module FluentApi
|
|
9
|
+
# Shared file selection for executable rule terminals.
|
|
10
|
+
module FileRuleSupport
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def selected_nodes(graph, filters)
|
|
14
|
+
nodes = Common::Projection.project_to_nodes(graph)
|
|
15
|
+
return nodes if filters.empty?
|
|
16
|
+
|
|
17
|
+
nodes.select do |node|
|
|
18
|
+
Common::PatternMatching.matches_all_patterns?(node.label, filters)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'file_condition_builder'
|
|
4
|
+
require_relative 'positive_match_pattern_file_condition_builder'
|
|
5
|
+
require_relative 'negated_match_pattern_file_condition_builder'
|
|
6
|
+
|
|
7
|
+
# Public ArchUnitRuby entry points for file rules.
|
|
8
|
+
module ArchUnit
|
|
9
|
+
module Files
|
|
10
|
+
# Sentence-like entry points and builders for file architecture rules.
|
|
11
|
+
module FluentApi
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def project_files(project_locator = nil)
|
|
15
|
+
FileConditionBuilder.new(project_locator:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
alias files project_files
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.project_files(project_locator = nil)
|
|
25
|
+
Files::FluentApi.project_files(project_locator)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
alias files project_files
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/filter'
|
|
4
|
+
require_relative '../../common/fluentapi/checkable'
|
|
5
|
+
require_relative '../../extraction/extract_graph'
|
|
6
|
+
require_relative '../assertion/matching_files'
|
|
7
|
+
require_relative 'file_rule_support'
|
|
8
|
+
|
|
9
|
+
module ArchUnit
|
|
10
|
+
module Files
|
|
11
|
+
module FluentApi
|
|
12
|
+
# Executable file name or location predicate for either mood.
|
|
13
|
+
class MatchPatternFileCondition
|
|
14
|
+
include Common::FluentApi::Checkable
|
|
15
|
+
|
|
16
|
+
attr_reader :project_locator, :filters, :check_filter, :is_negated
|
|
17
|
+
|
|
18
|
+
def initialize(mood, check_filter:)
|
|
19
|
+
validate_mood(mood)
|
|
20
|
+
validate_check_filter(check_filter)
|
|
21
|
+
@project_locator = mood.project_locator
|
|
22
|
+
@filters = mood.filters
|
|
23
|
+
@check_filter = check_filter
|
|
24
|
+
@is_negated = mood.negated?
|
|
25
|
+
freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def negated?
|
|
29
|
+
is_negated
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def validate_mood(value)
|
|
35
|
+
return if value.is_a?(MatchPatternFileConditionBuilder)
|
|
36
|
+
|
|
37
|
+
raise ArgumentError, 'mood must be a file condition builder'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate_check_filter(value)
|
|
41
|
+
return if value.is_a?(Common::Filter)
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, 'check_filter must be a Filter'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def perform_check(options)
|
|
47
|
+
graph = ArchUnit::Extraction.extract_graph(project_locator, options:)
|
|
48
|
+
nodes = FileRuleSupport.selected_nodes(graph, filters)
|
|
49
|
+
empty_test = empty_test_violation(
|
|
50
|
+
nodes, filters:, negated: is_negated, options:
|
|
51
|
+
)
|
|
52
|
+
return empty_test if empty_test
|
|
53
|
+
|
|
54
|
+
Assertion.gather_matching_file_violations(nodes, check_filter, is_negated:)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/regex_factory'
|
|
4
|
+
require_relative 'custom_file_condition'
|
|
5
|
+
require_relative 'depend_on_external_module_condition_builder'
|
|
6
|
+
require_relative 'depend_on_file_condition_builder'
|
|
7
|
+
require_relative 'match_pattern_file_condition'
|
|
8
|
+
|
|
9
|
+
module ArchUnit
|
|
10
|
+
module Files
|
|
11
|
+
module FluentApi
|
|
12
|
+
# Shared immutable state for positive and negated file predicate builders.
|
|
13
|
+
class MatchPatternFileConditionBuilder
|
|
14
|
+
attr_reader :project_locator, :filters
|
|
15
|
+
|
|
16
|
+
def initialize(scope, negated:)
|
|
17
|
+
unless scope.is_a?(FileConditionBuilder)
|
|
18
|
+
raise ArgumentError, 'scope must be a FileConditionBuilder'
|
|
19
|
+
end
|
|
20
|
+
unless [true, false].include?(negated)
|
|
21
|
+
raise ArgumentError, 'negated must be true or false'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@project_locator = scope.project_locator
|
|
25
|
+
@filters = scope.filters
|
|
26
|
+
@negated = negated
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def negated?
|
|
31
|
+
@negated
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def have_name(pattern, except: nil)
|
|
35
|
+
matching(Common::RegexFactory.filename_matcher(pattern, except:))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def be_in_folder(pattern, except: nil)
|
|
39
|
+
matching(Common::RegexFactory.folder_matcher(pattern, except:))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def be_in_path(pattern, except: nil)
|
|
43
|
+
matching(Common::RegexFactory.path_matcher(pattern, except:))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def depend_on_files
|
|
47
|
+
DependOnFileConditionBuilder.new(self)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def depend_on_external_modules
|
|
51
|
+
DependOnExternalModuleConditionBuilder.new(self)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def adhere_to(condition, message)
|
|
55
|
+
CustomFileCondition.new(self, condition:, message:)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def matching(check_filter)
|
|
61
|
+
MatchPatternFileCondition.new(self, check_filter:)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'match_pattern_file_condition_builder'
|
|
4
|
+
|
|
5
|
+
module ArchUnit
|
|
6
|
+
module Files
|
|
7
|
+
module FluentApi
|
|
8
|
+
# The `should_not` mood for file predicates.
|
|
9
|
+
class NegatedMatchPatternFileConditionBuilder < MatchPatternFileConditionBuilder
|
|
10
|
+
def initialize(scope)
|
|
11
|
+
super(scope, negated: true)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'match_pattern_file_condition_builder'
|
|
4
|
+
require_relative 'cycle_free_file_condition'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Files
|
|
8
|
+
module FluentApi
|
|
9
|
+
# The `should` mood for file predicates.
|
|
10
|
+
class PositiveMatchPatternFileConditionBuilder < MatchPatternFileConditionBuilder
|
|
11
|
+
def initialize(scope)
|
|
12
|
+
super(scope, negated: false)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def have_no_cycles
|
|
16
|
+
CycleFreeFileCondition.new(self)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'project_graph_builder'
|
|
4
|
+
|
|
5
|
+
# Public ArchUnitRuby entry points for dependency graph reports.
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module GraphReporting
|
|
8
|
+
# Sentence-like entry points and builders for graph reporting.
|
|
9
|
+
module FluentApi
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def project_graph(project_locator = nil)
|
|
13
|
+
ProjectGraphBuilder.new(project_locator:)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
alias dependency_graph project_graph
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.project_graph(project_locator = nil)
|
|
23
|
+
GraphReporting::FluentApi.project_graph(project_locator)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
alias dependency_graph project_graph
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/fluentapi/check_options'
|
|
4
|
+
require_relative '../../common/regex_factory'
|
|
5
|
+
require_relative '../../extraction/extract_graph'
|
|
6
|
+
require_relative '../projection/create_snapshot'
|
|
7
|
+
require_relative '../rendering/graph_renderer'
|
|
8
|
+
|
|
9
|
+
module ArchUnit
|
|
10
|
+
module GraphReporting
|
|
11
|
+
module FluentApi
|
|
12
|
+
# Immutable query builder for dependency graph snapshots and reports.
|
|
13
|
+
class ProjectGraphBuilder
|
|
14
|
+
attr_reader :project_locator, :options, :check_options
|
|
15
|
+
|
|
16
|
+
def initialize(project_locator: nil, options: nil, check_options: nil)
|
|
17
|
+
@project_locator = immutable_project_locator(project_locator)
|
|
18
|
+
@options = Projection::GraphQueryOptions.resolve(options)
|
|
19
|
+
@check_options = immutable_check_options(check_options)
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def include_external_dependencies
|
|
24
|
+
with_options(options.with(include_external_dependencies: true))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def include_self_dependencies
|
|
28
|
+
with_options(options.with(include_self_dependencies: true))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def focus_on(pattern, depth = 1, except: nil)
|
|
32
|
+
filter = Common::RegexFactory.path_matcher(pattern, except:)
|
|
33
|
+
with_options(options.with(focus: filter, focus_depth: depth))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reachable_from(pattern, except: nil)
|
|
37
|
+
filter = Common::RegexFactory.path_matcher(pattern, except:)
|
|
38
|
+
with_options(options.with(reachable_from: filter))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def dependents_of(pattern, except: nil)
|
|
42
|
+
filter = Common::RegexFactory.path_matcher(pattern, except:)
|
|
43
|
+
with_options(options.with(dependents_of: filter))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def collapse_to_folder_depth(depth)
|
|
47
|
+
with_options(options.with(collapse: Projection::FolderDepthCollapse.new(depth:)))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def collapse_by_pattern(pattern, replacement = '\\1')
|
|
51
|
+
collapse = Projection::PatternCollapse.from(pattern, replacement)
|
|
52
|
+
with_options(options.with(collapse:))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def titled(title)
|
|
56
|
+
with_options(options.with(title:))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def with_check_options(value)
|
|
60
|
+
self.class.new(project_locator:, options:, check_options: value)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def snapshot
|
|
64
|
+
graph = ArchUnit::Extraction.extract_graph(project_locator, options: check_options)
|
|
65
|
+
Projection::SnapshotFactory.create(graph, options)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def summary
|
|
69
|
+
snapshot.summary
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
Rendering::GraphRenderer::RENDERERS.each_key do |format|
|
|
73
|
+
define_method("to_#{format}") do
|
|
74
|
+
Rendering::GraphRenderer.render(snapshot, format)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
define_method("export_as_#{format}") do |output_path|
|
|
78
|
+
Rendering::GraphRenderer.export(snapshot, format, output_path)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def with_options(new_options)
|
|
85
|
+
self.class.new(project_locator:, options: new_options, check_options:)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def immutable_project_locator(locator)
|
|
89
|
+
return if locator.nil?
|
|
90
|
+
|
|
91
|
+
locator = locator.to_path if locator.respond_to?(:to_path)
|
|
92
|
+
return locator.dup.freeze if locator.is_a?(String) && !locator.empty?
|
|
93
|
+
|
|
94
|
+
raise ArgumentError, 'project_locator must be a non-empty path or nil'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def immutable_check_options(value)
|
|
98
|
+
return if value.nil?
|
|
99
|
+
return value if value.is_a?(Common::FluentApi::CheckOptions)
|
|
100
|
+
|
|
101
|
+
raise ArgumentError, 'check_options must be a CheckOptions value or nil'
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'collapse_node'
|
|
4
|
+
require_relative 'graph_report_edge'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module GraphReporting
|
|
8
|
+
module Projection
|
|
9
|
+
# Aggregates selected raw edges after applying one node-collapse strategy.
|
|
10
|
+
module AggregateEdges
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def aggregate(edges, collapse:, include_self_dependencies:)
|
|
14
|
+
groups = Hash.new do |hash, key|
|
|
15
|
+
hash[key] = { count: 0, external: false, import_kinds: [] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
edges.each do |edge|
|
|
19
|
+
source = CollapseNode.collapse(edge.source, collapse)
|
|
20
|
+
target = CollapseNode.collapse(edge.target, collapse)
|
|
21
|
+
next if !include_self_dependencies && source == target
|
|
22
|
+
|
|
23
|
+
accumulate(groups[[source, target]], edge)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
build_edges(groups)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def accumulate(group, edge)
|
|
30
|
+
group[:count] += 1
|
|
31
|
+
group[:external] ||= edge.external
|
|
32
|
+
group[:import_kinds] |= edge.import_kinds
|
|
33
|
+
end
|
|
34
|
+
private_class_method :accumulate
|
|
35
|
+
|
|
36
|
+
def build_edges(groups)
|
|
37
|
+
groups.sort_by { |(source, target), _group| [source, target] }.map do |key, group|
|
|
38
|
+
GraphReportEdge.new(
|
|
39
|
+
source: key.first, target: key.last, count: group[:count],
|
|
40
|
+
external: group[:external], import_kinds: group[:import_kinds]
|
|
41
|
+
)
|
|
42
|
+
end.freeze
|
|
43
|
+
end
|
|
44
|
+
private_class_method :build_edges
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'folder_depth_collapse'
|
|
4
|
+
require_relative 'pattern_collapse'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module GraphReporting
|
|
8
|
+
module Projection
|
|
9
|
+
# Pure node-label collapsing shared by snapshot nodes and edges.
|
|
10
|
+
module CollapseNode
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def collapse(node, strategy)
|
|
14
|
+
return node if strategy.nil?
|
|
15
|
+
return node.gsub(strategy.regexp, strategy.replacement) if strategy.is_a?(PatternCollapse)
|
|
16
|
+
|
|
17
|
+
collapse_to_folder(node, strategy.depth)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def collapse_to_folder(node, depth)
|
|
21
|
+
parts = node.split('/').reject(&:empty?)
|
|
22
|
+
return node if parts.length <= 1
|
|
23
|
+
|
|
24
|
+
folders = parts[0...-1]
|
|
25
|
+
return node if folders.empty?
|
|
26
|
+
|
|
27
|
+
folders.first(depth).join('/')
|
|
28
|
+
end
|
|
29
|
+
private_class_method :collapse_to_folder
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|