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.
Files changed (149) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +13 -0
  3. data/LICENSE +7 -0
  4. data/README.md +375 -0
  5. data/lib/archunit/common/assertion/empty_test_violation.rb +50 -0
  6. data/lib/archunit/common/assertion/violation.rb +14 -0
  7. data/lib/archunit/common/extraction/edge.rb +46 -0
  8. data/lib/archunit/common/extraction/graph.rb +73 -0
  9. data/lib/archunit/common/extraction/import_kind.rb +21 -0
  10. data/lib/archunit/common/filter.rb +46 -0
  11. data/lib/archunit/common/fluentapi/check_options.rb +48 -0
  12. data/lib/archunit/common/fluentapi/checkable.rb +70 -0
  13. data/lib/archunit/common/logging/check_logger.rb +124 -0
  14. data/lib/archunit/common/logging/logging_options.rb +57 -0
  15. data/lib/archunit/common/pattern.rb +99 -0
  16. data/lib/archunit/common/pattern_matching.rb +76 -0
  17. data/lib/archunit/common/projection/cycles/johnson_cycles.rb +103 -0
  18. data/lib/archunit/common/projection/cycles/tarjan_scc.rb +82 -0
  19. data/lib/archunit/common/projection/edge_projections.rb +50 -0
  20. data/lib/archunit/common/projection/mapped_edge.rb +26 -0
  21. data/lib/archunit/common/projection/project_cycles.rb +92 -0
  22. data/lib/archunit/common/projection/project_edges.rb +69 -0
  23. data/lib/archunit/common/projection/project_to_nodes.rb +59 -0
  24. data/lib/archunit/common/projection/projected_edge.rb +35 -0
  25. data/lib/archunit/common/projection/projected_node.rb +36 -0
  26. data/lib/archunit/common/regex_factory.rb +96 -0
  27. data/lib/archunit/error/technical_error.rb +6 -0
  28. data/lib/archunit/error/user_error.rb +6 -0
  29. data/lib/archunit/extraction/enumerate_source_files.rb +131 -0
  30. data/lib/archunit/extraction/extract_dependencies.rb +61 -0
  31. data/lib/archunit/extraction/extract_graph.rb +89 -0
  32. data/lib/archunit/extraction/extract_imports.rb +197 -0
  33. data/lib/archunit/extraction/locate_project.rb +80 -0
  34. data/lib/archunit/extraction/resolve_import.rb +76 -0
  35. data/lib/archunit/extraction/resolved_import.rb +46 -0
  36. data/lib/archunit/files/assertion/custom_file_condition.rb +89 -0
  37. data/lib/archunit/files/assertion/cycle_free.rb +58 -0
  38. data/lib/archunit/files/assertion/depend_on_external_modules.rb +116 -0
  39. data/lib/archunit/files/assertion/depend_on_files.rb +110 -0
  40. data/lib/archunit/files/assertion/matching_files.rb +87 -0
  41. data/lib/archunit/files/extraction/extract_file_info.rb +84 -0
  42. data/lib/archunit/files/extraction/file_info.rb +46 -0
  43. data/lib/archunit/files/fluentapi/custom_file_condition.rb +73 -0
  44. data/lib/archunit/files/fluentapi/cycle_free_file_condition.rb +55 -0
  45. data/lib/archunit/files/fluentapi/depend_on_external_module_condition.rb +69 -0
  46. data/lib/archunit/files/fluentapi/depend_on_external_module_condition_builder.rb +36 -0
  47. data/lib/archunit/files/fluentapi/depend_on_file_condition.rb +80 -0
  48. data/lib/archunit/files/fluentapi/depend_on_file_condition_builder.rb +49 -0
  49. data/lib/archunit/files/fluentapi/file_condition_builder.rb +71 -0
  50. data/lib/archunit/files/fluentapi/file_rule_support.rb +24 -0
  51. data/lib/archunit/files/fluentapi/files.rb +31 -0
  52. data/lib/archunit/files/fluentapi/match_pattern_file_condition.rb +59 -0
  53. data/lib/archunit/files/fluentapi/match_pattern_file_condition_builder.rb +66 -0
  54. data/lib/archunit/files/fluentapi/negated_match_pattern_file_condition_builder.rb +16 -0
  55. data/lib/archunit/files/fluentapi/positive_match_pattern_file_condition_builder.rb +21 -0
  56. data/lib/archunit/graph/fluentapi/graph.rb +29 -0
  57. data/lib/archunit/graph/fluentapi/project_graph_builder.rb +106 -0
  58. data/lib/archunit/graph/projection/aggregate_edges.rb +48 -0
  59. data/lib/archunit/graph/projection/collapse_node.rb +33 -0
  60. data/lib/archunit/graph/projection/create_snapshot.rb +84 -0
  61. data/lib/archunit/graph/projection/folder_depth_collapse.rb +18 -0
  62. data/lib/archunit/graph/projection/graph_query_options.rb +81 -0
  63. data/lib/archunit/graph/projection/graph_report_edge.rb +50 -0
  64. data/lib/archunit/graph/projection/graph_report_node.rb +22 -0
  65. data/lib/archunit/graph/projection/graph_report_snapshot.rb +43 -0
  66. data/lib/archunit/graph/projection/graph_report_summary.rb +20 -0
  67. data/lib/archunit/graph/projection/node_selection.rb +111 -0
  68. data/lib/archunit/graph/projection/pattern_collapse.rb +24 -0
  69. data/lib/archunit/graph/rendering/csv_renderer.rb +30 -0
  70. data/lib/archunit/graph/rendering/d2_renderer.rb +30 -0
  71. data/lib/archunit/graph/rendering/dot_renderer.rb +51 -0
  72. data/lib/archunit/graph/rendering/escaping.rb +31 -0
  73. data/lib/archunit/graph/rendering/export_report.rb +29 -0
  74. data/lib/archunit/graph/rendering/graph_renderer.rb +54 -0
  75. data/lib/archunit/graph/rendering/html_document.rb +46 -0
  76. data/lib/archunit/graph/rendering/html_renderer.rb +102 -0
  77. data/lib/archunit/graph/rendering/json_renderer.rb +38 -0
  78. data/lib/archunit/graph/rendering/mermaid_renderer.rb +33 -0
  79. data/lib/archunit/graph/rendering/rendering_support.rb +24 -0
  80. data/lib/archunit/layers/assertion/layer_definition.rb +56 -0
  81. data/lib/archunit/layers/assertion/layer_dependencies.rb +91 -0
  82. data/lib/archunit/layers/assertion/layer_dependency_violation.rb +59 -0
  83. data/lib/archunit/layers/fluentapi/layer_definition_builder.rb +46 -0
  84. data/lib/archunit/layers/fluentapi/layer_dependency_rule_builder.rb +44 -0
  85. data/lib/archunit/layers/fluentapi/layered_architecture.rb +133 -0
  86. data/lib/archunit/layers/fluentapi/layered_architecture_values.rb +51 -0
  87. data/lib/archunit/layers/fluentapi/layers.rb +29 -0
  88. data/lib/archunit/metrics/assertion/custom_metric.rb +73 -0
  89. data/lib/archunit/metrics/assertion/metric_predicate.rb +66 -0
  90. data/lib/archunit/metrics/assertion/metric_threshold.rb +99 -0
  91. data/lib/archunit/metrics/assertion/metric_zone.rb +57 -0
  92. data/lib/archunit/metrics/calculation/count.rb +45 -0
  93. data/lib/archunit/metrics/calculation/distance.rb +84 -0
  94. data/lib/archunit/metrics/calculation/lcom.rb +103 -0
  95. data/lib/archunit/metrics/calculation/metric.rb +52 -0
  96. data/lib/archunit/metrics/calculation/numeric_value.rb +25 -0
  97. data/lib/archunit/metrics/extraction/distance_info.rb +60 -0
  98. data/lib/archunit/metrics/extraction/extract_distance_info.rb +50 -0
  99. data/lib/archunit/metrics/extraction/extract_project_info.rb +90 -0
  100. data/lib/archunit/metrics/extraction/metric_info.rb +159 -0
  101. data/lib/archunit/metrics/extraction/source_metrics_visitor.rb +278 -0
  102. data/lib/archunit/metrics/fluentapi/count_metrics_builder.rb +49 -0
  103. data/lib/archunit/metrics/fluentapi/custom_metric_builder.rb +30 -0
  104. data/lib/archunit/metrics/fluentapi/custom_metric_condition.rb +40 -0
  105. data/lib/archunit/metrics/fluentapi/distance_metrics_builder.rb +48 -0
  106. data/lib/archunit/metrics/fluentapi/lcom_metrics_builder.rb +39 -0
  107. data/lib/archunit/metrics/fluentapi/metric_measurement.rb +36 -0
  108. data/lib/archunit/metrics/fluentapi/metric_predicate_condition.rb +42 -0
  109. data/lib/archunit/metrics/fluentapi/metric_report_builder.rb +34 -0
  110. data/lib/archunit/metrics/fluentapi/metric_selection.rb +54 -0
  111. data/lib/archunit/metrics/fluentapi/metric_threshold_condition.rb +47 -0
  112. data/lib/archunit/metrics/fluentapi/metrics.rb +21 -0
  113. data/lib/archunit/metrics/fluentapi/metrics_builder.rb +136 -0
  114. data/lib/archunit/metrics/fluentapi/zone_condition.rb +40 -0
  115. data/lib/archunit/metrics/reporting/metrics_export_options.rb +57 -0
  116. data/lib/archunit/metrics/reporting/metrics_exporter.rb +130 -0
  117. data/lib/archunit/slices/assertion/adhere_to_diagram.rb +57 -0
  118. data/lib/archunit/slices/assertion/contain_dependency.rb +42 -0
  119. data/lib/archunit/slices/assertion/diagram_adherence_options.rb +37 -0
  120. data/lib/archunit/slices/assertion/slice_dependency_violation.rb +70 -0
  121. data/lib/archunit/slices/fluentapi/diagram_slice_condition.rb +65 -0
  122. data/lib/archunit/slices/fluentapi/diagram_source.rb +36 -0
  123. data/lib/archunit/slices/fluentapi/forbidden_slice_dependency_condition.rb +57 -0
  124. data/lib/archunit/slices/fluentapi/negative_slice_condition_builder.rb +27 -0
  125. data/lib/archunit/slices/fluentapi/positive_slice_condition_builder.rb +51 -0
  126. data/lib/archunit/slices/fluentapi/slice_scope_builder.rb +83 -0
  127. data/lib/archunit/slices/fluentapi/slices.rb +29 -0
  128. data/lib/archunit/slices/projection/slice_projection.rb +76 -0
  129. data/lib/archunit/slices/projection/slicing_projections.rb +98 -0
  130. data/lib/archunit/slices/uml/export_diagram.rb +70 -0
  131. data/lib/archunit/slices/uml/parse_diagram.rb +53 -0
  132. data/lib/archunit/slices/uml/plant_uml_dependency.rb +25 -0
  133. data/lib/archunit/slices/uml/plant_uml_diagram.rb +44 -0
  134. data/lib/archunit/testing/assert_passes.rb +24 -0
  135. data/lib/archunit/testing/assertion_failure.rb +22 -0
  136. data/lib/archunit/testing/color_utils.rb +66 -0
  137. data/lib/archunit/testing/layer_violation_formatter.rb +26 -0
  138. data/lib/archunit/testing/metric_violation_formatter.rb +58 -0
  139. data/lib/archunit/testing/minitest_adapter.rb +33 -0
  140. data/lib/archunit/testing/result_factory.rb +93 -0
  141. data/lib/archunit/testing/rspec_adapter.rb +74 -0
  142. data/lib/archunit/testing/slice_violation_formatter.rb +25 -0
  143. data/lib/archunit/testing/test_result.rb +26 -0
  144. data/lib/archunit/testing/test_violation.rb +23 -0
  145. data/lib/archunit/testing/violation_factory.rb +138 -0
  146. data/lib/archunit/testing.rb +39 -0
  147. data/lib/archunit/version.rb +5 -0
  148. data/lib/archunit.rb +110 -0
  149. 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