archunitpython 1.0.0__py3-none-any.whl
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.
- archunitpython/__init__.py +45 -0
- archunitpython/common/__init__.py +18 -0
- archunitpython/common/assertion/__init__.py +3 -0
- archunitpython/common/assertion/violation.py +21 -0
- archunitpython/common/error/__init__.py +3 -0
- archunitpython/common/error/errors.py +13 -0
- archunitpython/common/extraction/__init__.py +13 -0
- archunitpython/common/extraction/extract_graph.py +345 -0
- archunitpython/common/extraction/graph.py +39 -0
- archunitpython/common/fluentapi/__init__.py +3 -0
- archunitpython/common/fluentapi/checkable.py +28 -0
- archunitpython/common/logging/__init__.py +3 -0
- archunitpython/common/logging/types.py +18 -0
- archunitpython/common/pattern_matching.py +80 -0
- archunitpython/common/projection/__init__.py +30 -0
- archunitpython/common/projection/cycles/__init__.py +4 -0
- archunitpython/common/projection/cycles/cycle_utils.py +49 -0
- archunitpython/common/projection/cycles/cycles.py +26 -0
- archunitpython/common/projection/cycles/johnsons_apsp.py +110 -0
- archunitpython/common/projection/cycles/model.py +22 -0
- archunitpython/common/projection/cycles/tarjan_scc.py +86 -0
- archunitpython/common/projection/edge_projections.py +36 -0
- archunitpython/common/projection/project_cycles.py +85 -0
- archunitpython/common/projection/project_edges.py +43 -0
- archunitpython/common/projection/project_nodes.py +49 -0
- archunitpython/common/projection/types.py +40 -0
- archunitpython/common/regex_factory.py +76 -0
- archunitpython/common/types.py +29 -0
- archunitpython/common/util/__init__.py +3 -0
- archunitpython/common/util/declaration_detector.py +115 -0
- archunitpython/common/util/logger.py +100 -0
- archunitpython/files/__init__.py +3 -0
- archunitpython/files/assertion/__init__.py +28 -0
- archunitpython/files/assertion/custom_file_logic.py +107 -0
- archunitpython/files/assertion/cycle_free.py +29 -0
- archunitpython/files/assertion/depend_on_files.py +67 -0
- archunitpython/files/assertion/matching_files.py +64 -0
- archunitpython/files/fluentapi/__init__.py +3 -0
- archunitpython/files/fluentapi/files.py +403 -0
- archunitpython/metrics/__init__.py +3 -0
- archunitpython/metrics/assertion/__init__.py +0 -0
- archunitpython/metrics/assertion/metric_thresholds.py +51 -0
- archunitpython/metrics/calculation/__init__.py +0 -0
- archunitpython/metrics/calculation/count.py +148 -0
- archunitpython/metrics/calculation/distance.py +110 -0
- archunitpython/metrics/calculation/lcom.py +177 -0
- archunitpython/metrics/common/__init__.py +19 -0
- archunitpython/metrics/common/types.py +67 -0
- archunitpython/metrics/extraction/__init__.py +0 -0
- archunitpython/metrics/extraction/extract_class_info.py +246 -0
- archunitpython/metrics/fluentapi/__init__.py +3 -0
- archunitpython/metrics/fluentapi/export_utils.py +89 -0
- archunitpython/metrics/fluentapi/metrics.py +589 -0
- archunitpython/metrics/projection/__init__.py +0 -0
- archunitpython/py.typed +0 -0
- archunitpython/slices/__init__.py +3 -0
- archunitpython/slices/assertion/__init__.py +13 -0
- archunitpython/slices/assertion/admissible_edges.py +108 -0
- archunitpython/slices/fluentapi/__init__.py +3 -0
- archunitpython/slices/fluentapi/slices.py +220 -0
- archunitpython/slices/projection/__init__.py +8 -0
- archunitpython/slices/projection/slicing_projections.py +128 -0
- archunitpython/slices/uml/__init__.py +4 -0
- archunitpython/slices/uml/export_diagram.py +31 -0
- archunitpython/slices/uml/generate_rules.py +71 -0
- archunitpython/testing/__init__.py +3 -0
- archunitpython/testing/assertion.py +47 -0
- archunitpython/testing/common/__init__.py +4 -0
- archunitpython/testing/common/color_utils.py +57 -0
- archunitpython/testing/common/violation_factory.py +97 -0
- archunitpython/testing/pytest_plugin/__init__.py +0 -0
- archunitpython-1.0.0.dist-info/METADATA +660 -0
- archunitpython-1.0.0.dist-info/RECORD +75 -0
- archunitpython-1.0.0.dist-info/WHEEL +4 -0
- archunitpython-1.0.0.dist-info/licenses/LICENSE +7 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Utility functions for detecting Python declarations in AST."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DeclarationCounts:
|
|
11
|
+
"""Counts of different declaration types in a Python file."""
|
|
12
|
+
|
|
13
|
+
total: int = 0
|
|
14
|
+
protocols: int = 0
|
|
15
|
+
abstract_classes: int = 0
|
|
16
|
+
abstract_methods: int = 0
|
|
17
|
+
concrete_classes: int = 0
|
|
18
|
+
functions: int = 0
|
|
19
|
+
variables: int = 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_abstract_class(node: ast.ClassDef) -> bool:
|
|
23
|
+
"""Check if a class inherits from ABC or uses ABCMeta."""
|
|
24
|
+
for base in node.bases:
|
|
25
|
+
name = _get_name(base)
|
|
26
|
+
if name in ("ABC", "ABCMeta"):
|
|
27
|
+
return True
|
|
28
|
+
for keyword in node.keywords:
|
|
29
|
+
if keyword.arg == "metaclass":
|
|
30
|
+
name = _get_name(keyword.value)
|
|
31
|
+
if name == "ABCMeta":
|
|
32
|
+
return True
|
|
33
|
+
# Also detect by @abstractmethod presence
|
|
34
|
+
for item in node.body:
|
|
35
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
36
|
+
for dec in item.decorator_list:
|
|
37
|
+
if _get_name(dec) == "abstractmethod":
|
|
38
|
+
return True
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def is_protocol_class(node: ast.ClassDef) -> bool:
|
|
43
|
+
"""Check if a class inherits from typing.Protocol."""
|
|
44
|
+
for base in node.bases:
|
|
45
|
+
name = _get_name(base)
|
|
46
|
+
if name == "Protocol":
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_abstract_method(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
52
|
+
"""Check if a method has the @abstractmethod decorator."""
|
|
53
|
+
for dec in node.decorator_list:
|
|
54
|
+
if _get_name(dec) == "abstractmethod":
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def count_declarations(source: str) -> DeclarationCounts:
|
|
60
|
+
"""Count all declaration types in a Python source string.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
source: Python source code as a string.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
DeclarationCounts with totals for each declaration type.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
tree = ast.parse(source)
|
|
70
|
+
except SyntaxError:
|
|
71
|
+
return DeclarationCounts()
|
|
72
|
+
|
|
73
|
+
counts = DeclarationCounts()
|
|
74
|
+
|
|
75
|
+
for node in ast.walk(tree):
|
|
76
|
+
if isinstance(node, ast.ClassDef):
|
|
77
|
+
counts.total += 1
|
|
78
|
+
if is_protocol_class(node):
|
|
79
|
+
counts.protocols += 1
|
|
80
|
+
elif is_abstract_class(node):
|
|
81
|
+
counts.abstract_classes += 1
|
|
82
|
+
else:
|
|
83
|
+
counts.concrete_classes += 1
|
|
84
|
+
|
|
85
|
+
# Count abstract methods
|
|
86
|
+
for item in node.body:
|
|
87
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
88
|
+
if is_abstract_method(item):
|
|
89
|
+
counts.abstract_methods += 1
|
|
90
|
+
|
|
91
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
92
|
+
# Only count module-level functions
|
|
93
|
+
if _is_module_level(node, tree):
|
|
94
|
+
counts.total += 1
|
|
95
|
+
counts.functions += 1
|
|
96
|
+
|
|
97
|
+
elif isinstance(node, ast.Assign) and _is_module_level(node, tree):
|
|
98
|
+
counts.total += 1
|
|
99
|
+
counts.variables += 1
|
|
100
|
+
|
|
101
|
+
return counts
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_module_level(node: ast.AST, tree: ast.Module) -> bool:
|
|
105
|
+
"""Check if a node is directly in the module body."""
|
|
106
|
+
return node in tree.body
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _get_name(node: ast.expr) -> str:
|
|
110
|
+
"""Extract a name from an AST node."""
|
|
111
|
+
if isinstance(node, ast.Name):
|
|
112
|
+
return node.id
|
|
113
|
+
if isinstance(node, ast.Attribute):
|
|
114
|
+
return node.attr
|
|
115
|
+
return ""
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Structured logging for architecture checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from archunitpython.common.logging.types import LoggingOptions
|
|
11
|
+
|
|
12
|
+
_LOG_LEVELS = {
|
|
13
|
+
"debug": logging.DEBUG,
|
|
14
|
+
"info": logging.INFO,
|
|
15
|
+
"warn": logging.WARNING,
|
|
16
|
+
"error": logging.ERROR,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CheckLogger:
|
|
21
|
+
"""Logger specialized for architecture check operations."""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._logger = logging.getLogger("archunitpython")
|
|
25
|
+
self._file_handler: logging.FileHandler | None = None
|
|
26
|
+
self._logger.setLevel(logging.DEBUG)
|
|
27
|
+
|
|
28
|
+
def _should_log(self, options: LoggingOptions | None) -> bool:
|
|
29
|
+
if options is None:
|
|
30
|
+
return False
|
|
31
|
+
return options.enabled
|
|
32
|
+
|
|
33
|
+
def _ensure_file_handler(self, options: LoggingOptions) -> None:
|
|
34
|
+
if not options.log_file:
|
|
35
|
+
return
|
|
36
|
+
if self._file_handler is not None:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
log_dir = Path(os.getcwd()) / "logs"
|
|
40
|
+
log_dir.mkdir(exist_ok=True)
|
|
41
|
+
|
|
42
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
43
|
+
log_path = log_dir / f"archunit-{timestamp}.log"
|
|
44
|
+
|
|
45
|
+
mode = "a" if options.append_to_log_file else "w"
|
|
46
|
+
self._file_handler = logging.FileHandler(str(log_path), mode=mode)
|
|
47
|
+
self._file_handler.setFormatter(
|
|
48
|
+
logging.Formatter("[%(levelname)s] %(message)s")
|
|
49
|
+
)
|
|
50
|
+
self._logger.addHandler(self._file_handler)
|
|
51
|
+
|
|
52
|
+
def _log(self, level: str, options: LoggingOptions | None, message: str) -> None:
|
|
53
|
+
if not self._should_log(options):
|
|
54
|
+
return
|
|
55
|
+
assert options is not None
|
|
56
|
+
log_level = _LOG_LEVELS.get(options.level, logging.INFO)
|
|
57
|
+
msg_level = _LOG_LEVELS.get(level, logging.INFO)
|
|
58
|
+
if msg_level < log_level:
|
|
59
|
+
return
|
|
60
|
+
self._ensure_file_handler(options)
|
|
61
|
+
self._logger.log(msg_level, message)
|
|
62
|
+
|
|
63
|
+
def debug(self, options: LoggingOptions | None, message: str) -> None:
|
|
64
|
+
self._log("debug", options, message)
|
|
65
|
+
|
|
66
|
+
def info(self, options: LoggingOptions | None, message: str) -> None:
|
|
67
|
+
self._log("info", options, message)
|
|
68
|
+
|
|
69
|
+
def warn(self, options: LoggingOptions | None, message: str) -> None:
|
|
70
|
+
self._log("warn", options, message)
|
|
71
|
+
|
|
72
|
+
def error(self, options: LoggingOptions | None, message: str) -> None:
|
|
73
|
+
self._log("error", options, message)
|
|
74
|
+
|
|
75
|
+
def start_check(self, rule_name: str, options: LoggingOptions | None) -> None:
|
|
76
|
+
self.info(options, f"Starting check: {rule_name}")
|
|
77
|
+
|
|
78
|
+
def end_check(
|
|
79
|
+
self, rule_name: str, violation_count: int, options: LoggingOptions | None
|
|
80
|
+
) -> None:
|
|
81
|
+
self.info(
|
|
82
|
+
options,
|
|
83
|
+
f"Finished check: {rule_name} - {violation_count} violation(s)",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def log_violation(self, violation: object, options: LoggingOptions | None) -> None:
|
|
87
|
+
self.warn(options, f"Violation: {violation}")
|
|
88
|
+
|
|
89
|
+
def log_progress(self, message: str, options: LoggingOptions | None) -> None:
|
|
90
|
+
self.info(options, message)
|
|
91
|
+
|
|
92
|
+
def close(self) -> None:
|
|
93
|
+
"""Remove file handler if present."""
|
|
94
|
+
if self._file_handler is not None:
|
|
95
|
+
self._logger.removeHandler(self._file_handler)
|
|
96
|
+
self._file_handler.close()
|
|
97
|
+
self._file_handler = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
shared_logger = CheckLogger()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from archunitpython.files.assertion.custom_file_logic import (
|
|
2
|
+
CustomFileCondition,
|
|
3
|
+
CustomFileViolation,
|
|
4
|
+
FileInfo,
|
|
5
|
+
gather_custom_file_violations,
|
|
6
|
+
)
|
|
7
|
+
from archunitpython.files.assertion.cycle_free import ViolatingCycle, gather_cycle_violations
|
|
8
|
+
from archunitpython.files.assertion.depend_on_files import (
|
|
9
|
+
ViolatingFileDependency,
|
|
10
|
+
gather_depend_on_file_violations,
|
|
11
|
+
)
|
|
12
|
+
from archunitpython.files.assertion.matching_files import (
|
|
13
|
+
ViolatingNode,
|
|
14
|
+
gather_regex_matching_violations,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CustomFileCondition",
|
|
19
|
+
"CustomFileViolation",
|
|
20
|
+
"FileInfo",
|
|
21
|
+
"ViolatingCycle",
|
|
22
|
+
"ViolatingFileDependency",
|
|
23
|
+
"ViolatingNode",
|
|
24
|
+
"gather_custom_file_violations",
|
|
25
|
+
"gather_cycle_violations",
|
|
26
|
+
"gather_depend_on_file_violations",
|
|
27
|
+
"gather_regex_matching_violations",
|
|
28
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Custom file condition logic and FileInfo type."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
from archunitpython.common.assertion.violation import Violation
|
|
10
|
+
from archunitpython.common.pattern_matching import matches_pattern
|
|
11
|
+
from archunitpython.common.projection.types import ProjectedNode
|
|
12
|
+
from archunitpython.common.types import Filter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class FileInfo:
|
|
17
|
+
"""Information about a file, provided to custom conditions."""
|
|
18
|
+
|
|
19
|
+
path: str
|
|
20
|
+
name: str # filename without extension
|
|
21
|
+
extension: str
|
|
22
|
+
directory: str
|
|
23
|
+
content: str
|
|
24
|
+
lines_of_code: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
CustomFileCondition = Callable[[FileInfo], bool]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CustomFileViolation(Violation):
|
|
32
|
+
"""A file that violates a custom condition."""
|
|
33
|
+
|
|
34
|
+
message: str
|
|
35
|
+
file_info: FileInfo
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_file_info(file_path: str) -> FileInfo:
|
|
39
|
+
"""Build a FileInfo from a file path."""
|
|
40
|
+
basename = os.path.basename(file_path)
|
|
41
|
+
name, ext = os.path.splitext(basename)
|
|
42
|
+
directory = os.path.dirname(file_path)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
46
|
+
content = f.read()
|
|
47
|
+
except (OSError, IOError):
|
|
48
|
+
content = ""
|
|
49
|
+
|
|
50
|
+
lines = [line for line in content.splitlines() if line.strip()]
|
|
51
|
+
|
|
52
|
+
return FileInfo(
|
|
53
|
+
path=file_path,
|
|
54
|
+
name=name,
|
|
55
|
+
extension=ext,
|
|
56
|
+
directory=directory,
|
|
57
|
+
content=content,
|
|
58
|
+
lines_of_code=len(lines),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def gather_custom_file_violations(
|
|
63
|
+
nodes: list[ProjectedNode],
|
|
64
|
+
condition: CustomFileCondition,
|
|
65
|
+
message: str,
|
|
66
|
+
is_negated: bool,
|
|
67
|
+
pre_filters: list[Filter],
|
|
68
|
+
) -> list[Violation]:
|
|
69
|
+
"""Evaluate a custom condition on files matching pre_filters.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
nodes: All projected nodes.
|
|
73
|
+
condition: Custom function that returns True if the file passes.
|
|
74
|
+
message: Message to include in violation.
|
|
75
|
+
is_negated: If False (should adhere), violation when condition returns False.
|
|
76
|
+
If True (shouldNot adhere), violation when condition returns True.
|
|
77
|
+
pre_filters: Filters to apply before checking the condition.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List of CustomFileViolation violations.
|
|
81
|
+
"""
|
|
82
|
+
violations: list[Violation] = []
|
|
83
|
+
|
|
84
|
+
for node in nodes:
|
|
85
|
+
# Check if node matches all pre-filters
|
|
86
|
+
if pre_filters and not all(
|
|
87
|
+
matches_pattern(node.label, f) for f in pre_filters
|
|
88
|
+
):
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
file_info = _build_file_info(node.label)
|
|
92
|
+
result = condition(file_info)
|
|
93
|
+
|
|
94
|
+
if is_negated:
|
|
95
|
+
# shouldNot: violation if condition IS True
|
|
96
|
+
if result:
|
|
97
|
+
violations.append(
|
|
98
|
+
CustomFileViolation(message=message, file_info=file_info)
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
# should: violation if condition is NOT True
|
|
102
|
+
if not result:
|
|
103
|
+
violations.append(
|
|
104
|
+
CustomFileViolation(message=message, file_info=file_info)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return violations
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Violation gathering for cycle-free rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from archunitpython.common.assertion.violation import Violation
|
|
8
|
+
from archunitpython.common.projection.types import ProjectedEdge
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ViolatingCycle(Violation):
|
|
13
|
+
"""A circular dependency detected between files."""
|
|
14
|
+
|
|
15
|
+
cycle: list[ProjectedEdge]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def gather_cycle_violations(
|
|
19
|
+
cycles: list[list[ProjectedEdge]],
|
|
20
|
+
) -> list[Violation]:
|
|
21
|
+
"""Convert detected cycles into violations.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
cycles: List of cycles, each being a list of edges forming the cycle.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of ViolatingCycle violations.
|
|
28
|
+
"""
|
|
29
|
+
return [ViolatingCycle(cycle=cycle) for cycle in cycles]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Violation gathering for file dependency rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from archunitpython.common.assertion.violation import Violation
|
|
8
|
+
from archunitpython.common.pattern_matching import matches_pattern
|
|
9
|
+
from archunitpython.common.projection.types import ProjectedEdge
|
|
10
|
+
from archunitpython.common.types import Filter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ViolatingFileDependency(Violation):
|
|
15
|
+
"""A file dependency that violates a rule."""
|
|
16
|
+
|
|
17
|
+
dependency: ProjectedEdge
|
|
18
|
+
is_negated: bool = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def gather_depend_on_file_violations(
|
|
22
|
+
edges: list[ProjectedEdge],
|
|
23
|
+
subject_filters: list[Filter],
|
|
24
|
+
object_filters: list[Filter],
|
|
25
|
+
is_negated: bool,
|
|
26
|
+
) -> list[Violation]:
|
|
27
|
+
"""Check if files have/don't have certain dependencies.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
edges: Projected dependency edges.
|
|
31
|
+
subject_filters: Patterns for the source files (subject of the rule).
|
|
32
|
+
object_filters: Patterns for the target files (dependency targets).
|
|
33
|
+
is_negated: If False (should), files matching subject MUST depend on
|
|
34
|
+
files matching object.
|
|
35
|
+
If True (shouldNot), files matching subject must NOT
|
|
36
|
+
depend on files matching object.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List of violations.
|
|
40
|
+
"""
|
|
41
|
+
violations: list[Violation] = []
|
|
42
|
+
|
|
43
|
+
for edge in edges:
|
|
44
|
+
source_matches = all(
|
|
45
|
+
matches_pattern(edge.source_label, f) for f in subject_filters
|
|
46
|
+
)
|
|
47
|
+
if not source_matches:
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
target_matches = all(
|
|
51
|
+
matches_pattern(edge.target_label, f) for f in object_filters
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if is_negated:
|
|
55
|
+
# shouldNot: violation if dependency EXISTS
|
|
56
|
+
if target_matches:
|
|
57
|
+
violations.append(
|
|
58
|
+
ViolatingFileDependency(dependency=edge, is_negated=True)
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
# should: violation if dependency does NOT match
|
|
62
|
+
if not target_matches:
|
|
63
|
+
violations.append(
|
|
64
|
+
ViolatingFileDependency(dependency=edge, is_negated=False)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return violations
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Violation gathering for file pattern matching rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from archunitpython.common.assertion.violation import Violation
|
|
8
|
+
from archunitpython.common.pattern_matching import matches_pattern
|
|
9
|
+
from archunitpython.common.projection.types import ProjectedNode
|
|
10
|
+
from archunitpython.common.types import Filter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ViolatingNode(Violation):
|
|
15
|
+
"""A file that violates a pattern matching rule."""
|
|
16
|
+
|
|
17
|
+
check_pattern: Filter
|
|
18
|
+
projected_node: ProjectedNode
|
|
19
|
+
is_negated: bool = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def gather_regex_matching_violations(
|
|
23
|
+
nodes: list[ProjectedNode],
|
|
24
|
+
check_filters: list[Filter],
|
|
25
|
+
is_negated: bool,
|
|
26
|
+
) -> list[Violation]:
|
|
27
|
+
"""Check if files match/don't match given patterns.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
nodes: Files to check.
|
|
31
|
+
check_filters: Patterns to match against.
|
|
32
|
+
is_negated: If False (should), files MUST match all patterns.
|
|
33
|
+
If True (shouldNot), files must NOT match any pattern.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of violations.
|
|
37
|
+
"""
|
|
38
|
+
violations: list[Violation] = []
|
|
39
|
+
|
|
40
|
+
for node in nodes:
|
|
41
|
+
for filter_ in check_filters:
|
|
42
|
+
matched = matches_pattern(node.label, filter_)
|
|
43
|
+
if is_negated:
|
|
44
|
+
# shouldNot: violation if file DOES match
|
|
45
|
+
if matched:
|
|
46
|
+
violations.append(
|
|
47
|
+
ViolatingNode(
|
|
48
|
+
check_pattern=filter_,
|
|
49
|
+
projected_node=node,
|
|
50
|
+
is_negated=True,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
# should: violation if file does NOT match
|
|
55
|
+
if not matched:
|
|
56
|
+
violations.append(
|
|
57
|
+
ViolatingNode(
|
|
58
|
+
check_pattern=filter_,
|
|
59
|
+
projected_node=node,
|
|
60
|
+
is_negated=False,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return violations
|