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,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../calculation/metric'
|
|
4
|
+
require_relative '../extraction/metric_info'
|
|
5
|
+
require_relative 'custom_metric_condition'
|
|
6
|
+
require_relative 'metric_selection'
|
|
7
|
+
|
|
8
|
+
module ArchUnit
|
|
9
|
+
module Metrics
|
|
10
|
+
module FluentApi
|
|
11
|
+
# A user-defined class calculation that can be measured or asserted.
|
|
12
|
+
class CustomMetricBuilder < MetricSelection
|
|
13
|
+
def initialize(scope:, name:, description:, calculation:)
|
|
14
|
+
unless name.is_a?(String) && !name.empty?
|
|
15
|
+
raise ArgumentError, 'custom metric name must be a non-empty String'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
metric = Calculation::Metric.new(
|
|
19
|
+
name:, description:, subject_type: Extraction::ClassInfo, calculation:
|
|
20
|
+
)
|
|
21
|
+
super(scope:, metric:)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def should_satisfy(predicate)
|
|
25
|
+
CustomMetricCondition.new(selection: self, predicate:)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative '../assertion/custom_metric'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Metrics
|
|
8
|
+
module FluentApi
|
|
9
|
+
# Executable custom class-metric predicate.
|
|
10
|
+
class CustomMetricCondition
|
|
11
|
+
include Common::FluentApi::Checkable
|
|
12
|
+
|
|
13
|
+
attr_reader :selection, :predicate
|
|
14
|
+
|
|
15
|
+
def initialize(selection:, predicate:)
|
|
16
|
+
unless selection.is_a?(CustomMetricBuilder)
|
|
17
|
+
raise ArgumentError, 'selection must be a CustomMetricBuilder'
|
|
18
|
+
end
|
|
19
|
+
raise ArgumentError, 'predicate must respond to call' unless predicate.respond_to?(:call)
|
|
20
|
+
|
|
21
|
+
@selection = selection
|
|
22
|
+
@predicate = predicate.dup.freeze
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def perform_check(options)
|
|
29
|
+
classes = selection.scope.__send__(:subjects_for, Extraction::ClassInfo, options:)
|
|
30
|
+
empty_test = empty_test_violation(
|
|
31
|
+
classes, filters: selection.scope.filters, negated: false, options:
|
|
32
|
+
)
|
|
33
|
+
return empty_test if empty_test
|
|
34
|
+
|
|
35
|
+
Assertion.gather_custom_metric_violations(classes, selection.metric, predicate)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../calculation/distance'
|
|
4
|
+
require_relative 'metric_report_builder'
|
|
5
|
+
require_relative 'metric_selection'
|
|
6
|
+
require_relative 'zone_condition'
|
|
7
|
+
|
|
8
|
+
module ArchUnit
|
|
9
|
+
module Metrics
|
|
10
|
+
module FluentApi
|
|
11
|
+
# Fluent distance metric names and architectural zone guards.
|
|
12
|
+
class DistanceMetricsBuilder
|
|
13
|
+
include MetricReportBuilder
|
|
14
|
+
|
|
15
|
+
attr_reader :scope
|
|
16
|
+
|
|
17
|
+
def initialize(scope)
|
|
18
|
+
raise ArgumentError, 'scope must be a MetricsBuilder' unless scope.is_a?(MetricsBuilder)
|
|
19
|
+
|
|
20
|
+
@scope = scope
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Calculation::Distance::CALCULATIONS.each_key do |name|
|
|
25
|
+
define_method(name) do
|
|
26
|
+
MetricSelection.new(scope:, metric: Calculation::Distance.public_send(name))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def not_in_zone_of_pain
|
|
31
|
+
ZoneCondition.new(scope:, zone: :pain)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def not_in_zone_of_uselessness
|
|
35
|
+
ZoneCondition.new(scope:, zone: :uselessness)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def report_metrics
|
|
41
|
+
Calculation::Distance::CALCULATIONS.each_key.map do |name|
|
|
42
|
+
Calculation::Distance.public_send(name)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../calculation/lcom'
|
|
4
|
+
require_relative 'metric_report_builder'
|
|
5
|
+
require_relative 'metric_selection'
|
|
6
|
+
|
|
7
|
+
module ArchUnit
|
|
8
|
+
module Metrics
|
|
9
|
+
module FluentApi
|
|
10
|
+
# Fluent LCOM metric names for one immutable metrics scope.
|
|
11
|
+
class LCOMMetricsBuilder
|
|
12
|
+
include MetricReportBuilder
|
|
13
|
+
|
|
14
|
+
attr_reader :scope
|
|
15
|
+
|
|
16
|
+
def initialize(scope)
|
|
17
|
+
raise ArgumentError, 'scope must be a MetricsBuilder' unless scope.is_a?(MetricsBuilder)
|
|
18
|
+
|
|
19
|
+
@scope = scope
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Calculation::LCOM::CALCULATIONS.each_key do |name|
|
|
24
|
+
define_method(name) do
|
|
25
|
+
MetricSelection.new(scope:, metric: Calculation::LCOM.public_send(name))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def report_metrics
|
|
32
|
+
Calculation::LCOM::CALCULATIONS.each_key.map do |name|
|
|
33
|
+
Calculation::LCOM.public_send(name)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../calculation/numeric_value'
|
|
4
|
+
|
|
5
|
+
module ArchUnit
|
|
6
|
+
module Metrics
|
|
7
|
+
module FluentApi
|
|
8
|
+
# One calculated metric value and the extracted subject it describes.
|
|
9
|
+
MetricMeasurement = Data.define(:subject, :metric_name, :value) do
|
|
10
|
+
def initialize(subject:, metric_name:, value:)
|
|
11
|
+
unless subject.respond_to?(:identifier)
|
|
12
|
+
raise ArgumentError, 'subject must expose an identifier'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
metric_name = immutable_metric_name(metric_name)
|
|
16
|
+
value = Calculation::NumericValue.validate(value)
|
|
17
|
+
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def identifier
|
|
22
|
+
subject.identifier
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def immutable_metric_name(value)
|
|
28
|
+
return value if value.is_a?(Symbol)
|
|
29
|
+
return value.dup.freeze if value.is_a?(String) && !value.empty?
|
|
30
|
+
|
|
31
|
+
raise ArgumentError, 'metric_name must be a Symbol or non-empty String'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative '../assertion/metric_predicate'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Metrics
|
|
8
|
+
module FluentApi
|
|
9
|
+
# Executable arbitrary predicate over one selected built-in metric.
|
|
10
|
+
class MetricPredicateCondition
|
|
11
|
+
include Common::FluentApi::Checkable
|
|
12
|
+
|
|
13
|
+
attr_reader :selection, :predicate
|
|
14
|
+
|
|
15
|
+
def initialize(selection:, predicate:)
|
|
16
|
+
unless selection.is_a?(MetricSelection)
|
|
17
|
+
raise ArgumentError, 'selection must be a MetricSelection'
|
|
18
|
+
end
|
|
19
|
+
raise ArgumentError, 'predicate must respond to call' unless predicate.respond_to?(:call)
|
|
20
|
+
|
|
21
|
+
@selection = selection
|
|
22
|
+
@predicate = predicate.dup.freeze
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def perform_check(options)
|
|
29
|
+
subjects = selection.scope.__send__(
|
|
30
|
+
:subjects_for, selection.metric.subject_type, options:
|
|
31
|
+
)
|
|
32
|
+
empty_test = empty_test_violation(
|
|
33
|
+
subjects, filters: selection.scope.filters, negated: false, options:
|
|
34
|
+
)
|
|
35
|
+
return empty_test if empty_test
|
|
36
|
+
|
|
37
|
+
Assertion.gather_metric_predicate_violations(subjects, selection.metric, predicate)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../reporting/metrics_export_options'
|
|
4
|
+
require_relative '../reporting/metrics_exporter'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Metrics
|
|
8
|
+
module FluentApi
|
|
9
|
+
# Shared HTML export terminal for one family of scoped metrics.
|
|
10
|
+
module MetricReportBuilder
|
|
11
|
+
def export_as_html(output_path, options = nil)
|
|
12
|
+
options = Reporting::MetricsExportOptions.resolve(options).with(output_path:)
|
|
13
|
+
Reporting::MetricsExporter.export_as_html(metric_report_data, options)
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def metric_report_data
|
|
20
|
+
report_metrics.group_by(&:subject_type).each_with_object({}) do |(type, metrics), data|
|
|
21
|
+
subjects = scope.__send__(:subjects_for, type)
|
|
22
|
+
metrics.each { |metric| add_metric_values(data, subjects, metric) }
|
|
23
|
+
end.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add_metric_values(data, subjects, metric)
|
|
27
|
+
subjects.each do |subject|
|
|
28
|
+
data["#{metric.name} [#{subject.identifier}]"] = metric.calculate(subject)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../calculation/metric'
|
|
4
|
+
require_relative 'metric_measurement'
|
|
5
|
+
require_relative 'metric_predicate_condition'
|
|
6
|
+
require_relative 'metric_threshold_condition'
|
|
7
|
+
|
|
8
|
+
module ArchUnit
|
|
9
|
+
module Metrics
|
|
10
|
+
module FluentApi
|
|
11
|
+
# Lazy selection of one metric over a metrics scope.
|
|
12
|
+
class MetricSelection
|
|
13
|
+
THRESHOLD_METHODS = {
|
|
14
|
+
should_be_below: :below,
|
|
15
|
+
should_be_above: :above,
|
|
16
|
+
should_be: :equal,
|
|
17
|
+
should_be_below_or_equal: :below_or_equal,
|
|
18
|
+
should_be_above_or_equal: :above_or_equal
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :scope, :metric
|
|
22
|
+
|
|
23
|
+
def initialize(scope:, metric:)
|
|
24
|
+
raise ArgumentError, 'scope must be a MetricsBuilder' unless scope.is_a?(MetricsBuilder)
|
|
25
|
+
raise ArgumentError, 'metric must be a Metric' unless metric.is_a?(Calculation::Metric)
|
|
26
|
+
|
|
27
|
+
@scope = scope
|
|
28
|
+
@metric = metric
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def measure
|
|
33
|
+
scope.__send__(:subjects_for, metric.subject_type).map do |subject|
|
|
34
|
+
MetricMeasurement.new(
|
|
35
|
+
subject:,
|
|
36
|
+
metric_name: metric.name,
|
|
37
|
+
value: metric.calculate(subject)
|
|
38
|
+
)
|
|
39
|
+
end.freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
THRESHOLD_METHODS.each do |method_name, comparison|
|
|
43
|
+
define_method(method_name) do |threshold|
|
|
44
|
+
MetricThresholdCondition.new(selection: self, comparison:, threshold:)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def should_satisfy(predicate)
|
|
49
|
+
MetricPredicateCondition.new(selection: self, predicate:)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative '../assertion/metric_threshold'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Metrics
|
|
8
|
+
module FluentApi
|
|
9
|
+
# Executable numeric threshold over one selected metric.
|
|
10
|
+
class MetricThresholdCondition
|
|
11
|
+
include Common::FluentApi::Checkable
|
|
12
|
+
|
|
13
|
+
attr_reader :selection, :comparison, :threshold
|
|
14
|
+
|
|
15
|
+
def initialize(selection:, comparison:, threshold:)
|
|
16
|
+
unless selection.is_a?(MetricSelection)
|
|
17
|
+
raise ArgumentError, 'selection must be a MetricSelection'
|
|
18
|
+
end
|
|
19
|
+
unless Assertion::MetricThresholdViolation::COMPARISONS.include?(comparison)
|
|
20
|
+
raise ArgumentError, "unknown metric comparison: #{comparison.inspect}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@selection = selection
|
|
24
|
+
@comparison = comparison
|
|
25
|
+
@threshold = Calculation::NumericValue.validate(threshold, attribute: 'threshold')
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def perform_check(options)
|
|
32
|
+
subjects = selection.scope.__send__(
|
|
33
|
+
:subjects_for, selection.metric.subject_type, options:
|
|
34
|
+
)
|
|
35
|
+
empty_test = empty_test_violation(
|
|
36
|
+
subjects, filters: selection.scope.filters, negated: false, options:
|
|
37
|
+
)
|
|
38
|
+
return empty_test if empty_test
|
|
39
|
+
|
|
40
|
+
Assertion.gather_metric_threshold_violations(
|
|
41
|
+
subjects, selection.metric, comparison, threshold
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'metrics_builder'
|
|
4
|
+
|
|
5
|
+
# Public ArchUnitRuby API.
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Metrics
|
|
8
|
+
# Public entry point for numeric Ruby source metrics.
|
|
9
|
+
module FluentApi
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def metrics(project_locator = nil)
|
|
13
|
+
MetricsBuilder.new(project_locator:)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.metrics(project_locator = nil)
|
|
19
|
+
Metrics::FluentApi.metrics(project_locator)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/filter'
|
|
4
|
+
require_relative '../../common/pattern_matching'
|
|
5
|
+
require_relative '../../common/regex_factory'
|
|
6
|
+
require_relative '../extraction/extract_project_info'
|
|
7
|
+
require_relative '../extraction/extract_distance_info'
|
|
8
|
+
require_relative 'count_metrics_builder'
|
|
9
|
+
require_relative 'custom_metric_builder'
|
|
10
|
+
require_relative 'distance_metrics_builder'
|
|
11
|
+
require_relative 'lcom_metrics_builder'
|
|
12
|
+
|
|
13
|
+
module ArchUnit
|
|
14
|
+
module Metrics
|
|
15
|
+
module FluentApi
|
|
16
|
+
# Immutable scope builder for numeric metrics over Ruby source.
|
|
17
|
+
class MetricsBuilder
|
|
18
|
+
attr_reader :project_locator, :filters
|
|
19
|
+
|
|
20
|
+
def initialize(project_locator: nil, filters: [])
|
|
21
|
+
@project_locator = immutable_project_locator(project_locator)
|
|
22
|
+
@filters = immutable_filters(filters)
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def with_name(pattern, except: nil)
|
|
27
|
+
with_filter(Common::RegexFactory.filename_matcher(pattern, except:))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def in_folder(pattern, except: nil)
|
|
31
|
+
with_filter(Common::RegexFactory.folder_matcher(pattern, except:))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def in_path(pattern, except: nil)
|
|
35
|
+
with_filter(Common::RegexFactory.path_matcher(pattern, except:))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def for_classes_matching(pattern, except: nil)
|
|
39
|
+
with_filter(Common::RegexFactory.classname_matcher(pattern, except:))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def count
|
|
43
|
+
CountMetricsBuilder.new(self)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def lcom
|
|
47
|
+
LCOMMetricsBuilder.new(self)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def distance
|
|
51
|
+
DistanceMetricsBuilder.new(self)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def custom_metric(name, description, calculation)
|
|
55
|
+
CustomMetricBuilder.new(scope: self, name:, description:, calculation:)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def analyze
|
|
59
|
+
project = Extraction.extract_project_info(project_locator)
|
|
60
|
+
selected_files = project.files.filter_map { |file| selected_file_info(file) }
|
|
61
|
+
Extraction::ProjectInfo.new(project_root: project.project_root, files: selected_files)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def with_filter(filter)
|
|
67
|
+
self.class.new(project_locator:, filters: [*filters, filter])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def subjects_for(subject_type, options: nil)
|
|
71
|
+
return distance_subjects(options:) if subject_type == Extraction::DistanceInfo
|
|
72
|
+
|
|
73
|
+
project = analyze
|
|
74
|
+
return project.files if subject_type == Extraction::FileInfo
|
|
75
|
+
return project.classes if subject_type == Extraction::ClassInfo
|
|
76
|
+
|
|
77
|
+
raise ArgumentError, "unsupported metric subject type: #{subject_type}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def distance_subjects(options: nil)
|
|
81
|
+
Extraction.extract_distance_infos(project_locator, options:).select do |distance_info|
|
|
82
|
+
file_selected?(distance_info.file_info)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def file_selected?(file_info)
|
|
87
|
+
!selected_file_info(file_info).nil?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def selected_file_info(file_info)
|
|
91
|
+
path_filters, class_filters = partitioned_filters
|
|
92
|
+
matches_path = Common::PatternMatching.matches_all_patterns?(file_info.path, path_filters)
|
|
93
|
+
return unless matches_path
|
|
94
|
+
return file_info if class_filters.empty?
|
|
95
|
+
|
|
96
|
+
selected_classes = matching_classes(file_info, class_filters)
|
|
97
|
+
return if selected_classes.empty?
|
|
98
|
+
|
|
99
|
+
file_info.with(class_infos: selected_classes)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def matching_classes(file_info, class_filters)
|
|
103
|
+
file_info.class_infos.select do |class_info|
|
|
104
|
+
Common::PatternMatching.matches_all_patterns?(
|
|
105
|
+
file_info.path, class_filters, class_name: class_info.name
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def partitioned_filters
|
|
111
|
+
filters.partition { |filter| filter.target != :classname }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def immutable_project_locator(locator)
|
|
115
|
+
return if locator.nil?
|
|
116
|
+
|
|
117
|
+
locator = locator.to_path if locator.respond_to?(:to_path)
|
|
118
|
+
unless locator.is_a?(String) && !locator.empty?
|
|
119
|
+
raise ArgumentError, 'project_locator must be a non-empty path or nil'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
locator.dup.freeze
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def immutable_filters(values)
|
|
126
|
+
filters = Array(values).dup
|
|
127
|
+
unless filters.all?(Common::Filter)
|
|
128
|
+
raise ArgumentError, 'filters must contain only Filter values'
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
filters.freeze
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../common/fluentapi/checkable'
|
|
4
|
+
require_relative '../assertion/metric_zone'
|
|
5
|
+
|
|
6
|
+
module ArchUnit
|
|
7
|
+
module Metrics
|
|
8
|
+
module FluentApi
|
|
9
|
+
# Executable guard that rejects files in one architectural distance zone.
|
|
10
|
+
class ZoneCondition
|
|
11
|
+
include Common::FluentApi::Checkable
|
|
12
|
+
|
|
13
|
+
attr_reader :scope, :zone
|
|
14
|
+
|
|
15
|
+
def initialize(scope:, zone:)
|
|
16
|
+
raise ArgumentError, 'scope must be a MetricsBuilder' unless scope.is_a?(MetricsBuilder)
|
|
17
|
+
unless Assertion::MetricZoneViolation::ZONES.include?(zone)
|
|
18
|
+
raise ArgumentError, "unknown architectural zone: #{zone.inspect}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@scope = scope
|
|
22
|
+
@zone = zone
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def perform_check(options)
|
|
29
|
+
distance_infos = scope.__send__(:distance_subjects, options:)
|
|
30
|
+
empty_test = empty_test_violation(
|
|
31
|
+
distance_infos, filters: scope.filters, negated: false, options:
|
|
32
|
+
)
|
|
33
|
+
return empty_test if empty_test
|
|
34
|
+
|
|
35
|
+
Assertion.gather_metric_zone_violations(distance_infos, zone)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ArchUnit
|
|
4
|
+
module Metrics
|
|
5
|
+
# Self-contained metrics report rendering and export.
|
|
6
|
+
module Reporting
|
|
7
|
+
# Immutable options for one metrics HTML report.
|
|
8
|
+
MetricsExportOptions = Data.define(
|
|
9
|
+
:output_path, :title, :include_timestamp, :custom_css
|
|
10
|
+
) do
|
|
11
|
+
def self.resolve(value)
|
|
12
|
+
return new if value.nil?
|
|
13
|
+
return value if value.is_a?(self)
|
|
14
|
+
|
|
15
|
+
raise ArgumentError, 'options must be a MetricsExportOptions value or nil'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(
|
|
19
|
+
output_path: nil,
|
|
20
|
+
title: 'ArchUnitRuby Metrics Report',
|
|
21
|
+
include_timestamp: true,
|
|
22
|
+
custom_css: nil
|
|
23
|
+
)
|
|
24
|
+
output_path = optional_path(output_path)
|
|
25
|
+
title = immutable_string(title, :title)
|
|
26
|
+
unless [true, false].include?(include_timestamp)
|
|
27
|
+
raise ArgumentError, 'include_timestamp must be true or false'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
custom_css = optional_string(custom_css, :custom_css)
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def optional_path(value)
|
|
37
|
+
return if value.nil?
|
|
38
|
+
|
|
39
|
+
value = value.to_path if value.respond_to?(:to_path)
|
|
40
|
+
immutable_string(value, :output_path)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def optional_string(value, attribute)
|
|
44
|
+
return if value.nil?
|
|
45
|
+
|
|
46
|
+
immutable_string(value, attribute)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def immutable_string(value, attribute)
|
|
50
|
+
return value.dup.freeze if value.is_a?(String) && !value.empty?
|
|
51
|
+
|
|
52
|
+
raise ArgumentError, "#{attribute} must be a non-empty String"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|