1use anyhow::{Context, Result};
2use regex::Regex;
3
4use crate::config::CustomRuleConfig;
5use crate::graph::DependencyGraph;
6use crate::types::{Violation, ViolationKind};
7
8pub struct CompiledCustomRule {
10 pub name: String,
11 pub from_regex: Regex,
12 pub to_regex: Regex,
13 pub severity: crate::types::Severity,
14 pub message: Option<String>,
15}
16
17pub fn compile_rules(configs: &[CustomRuleConfig]) -> Result<Vec<CompiledCustomRule>> {
19 configs
20 .iter()
21 .map(|cfg| {
22 let from_regex = Regex::new(&cfg.from_pattern)
23 .with_context(|| format!("invalid from_pattern in rule '{}'", cfg.name))?;
24 let to_regex = Regex::new(&cfg.to_pattern)
25 .with_context(|| format!("invalid to_pattern in rule '{}'", cfg.name))?;
26 Ok(CompiledCustomRule {
27 name: cfg.name.clone(),
28 from_regex,
29 to_regex,
30 severity: cfg.severity,
31 message: cfg.message.clone(),
32 })
33 })
34 .collect()
35}
36
37pub fn evaluate_custom_rules(
39 graph: &DependencyGraph,
40 rules: &[CompiledCustomRule],
41) -> Vec<Violation> {
42 let mut violations = Vec::new();
43
44 for (src, tgt, edge) in graph.edges_with_nodes() {
45 let from_path = &src.id.0;
46 let to_path = edge.import_path.as_deref().unwrap_or(&tgt.id.0);
47
48 for rule in rules {
49 if rule.from_regex.is_match(from_path) && rule.to_regex.is_match(to_path) {
50 let message = rule.message.clone().unwrap_or_else(|| {
51 format!(
52 "Custom rule '{}' violated: {} -> {}",
53 rule.name, from_path, to_path
54 )
55 });
56
57 violations.push(Violation {
58 kind: ViolationKind::CustomRule {
59 rule_name: rule.name.clone(),
60 },
61 severity: rule.severity,
62 location: edge.location.clone(),
63 message,
64 suggestion: Some(format!(
65 "This dependency is forbidden by custom rule '{}'.",
66 rule.name
67 )),
68 });
69 }
70 }
71 }
72
73 violations
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79 use crate::config::CustomRuleConfig;
80 use crate::graph::DependencyGraph;
81 use crate::types::*;
82 use std::path::PathBuf;
83
84 fn make_component(id: &str, name: &str, layer: Option<ArchLayer>) -> Component {
85 Component {
86 id: ComponentId(id.to_string()),
87 name: name.to_string(),
88 kind: ComponentKind::Entity(EntityInfo {
89 name: name.to_string(),
90 fields: vec![],
91 methods: vec![],
92 is_active_record: false,
93 }),
94 layer,
95 location: SourceLocation {
96 file: PathBuf::from("test.go"),
97 line: 1,
98 column: 1,
99 },
100 is_cross_cutting: false,
101 architecture_mode: ArchitectureMode::Ddd,
102 }
103 }
104
105 fn make_dep(from: &str, to: &str, import: &str) -> Dependency {
106 Dependency {
107 from: ComponentId(from.to_string()),
108 to: ComponentId(to.to_string()),
109 kind: DependencyKind::Import,
110 location: SourceLocation {
111 file: PathBuf::from("test.go"),
112 line: 10,
113 column: 1,
114 },
115 import_path: Some(import.to_string()),
116 }
117 }
118
119 #[test]
120 fn test_compile_and_evaluate_custom_rules() {
121 let configs = vec![CustomRuleConfig {
122 name: "no-internal-to-external".to_string(),
123 from_pattern: ".*/internal/.*".to_string(),
124 to_pattern: ".*/external/.*".to_string(),
125 action: "deny".to_string(),
126 severity: Severity::Error,
127 message: Some("Internal must not import external".to_string()),
128 }];
129
130 let rules = compile_rules(&configs).unwrap();
131 assert_eq!(rules.len(), 1);
132
133 let mut graph = DependencyGraph::new();
134 let c1 = make_component(
135 "app/internal/service",
136 "Service",
137 Some(ArchLayer::Application),
138 );
139 let c2 = make_component(
140 "app/external/client",
141 "Client",
142 Some(ArchLayer::Infrastructure),
143 );
144 graph.add_component(&c1);
145 graph.add_component(&c2);
146 graph.add_dependency(&make_dep(
147 "app/internal/service",
148 "app/external/client",
149 "app/external/client",
150 ));
151
152 let violations = evaluate_custom_rules(&graph, &rules);
153 assert_eq!(violations.len(), 1);
154 assert!(matches!(
155 violations[0].kind,
156 ViolationKind::CustomRule { .. }
157 ));
158 }
159
160 #[test]
161 fn test_no_match_no_violation() {
162 let configs = vec![CustomRuleConfig {
163 name: "no-internal-to-external".to_string(),
164 from_pattern: ".*/internal/.*".to_string(),
165 to_pattern: ".*/external/.*".to_string(),
166 action: "deny".to_string(),
167 severity: Severity::Error,
168 message: None,
169 }];
170
171 let rules = compile_rules(&configs).unwrap();
172
173 let mut graph = DependencyGraph::new();
174 let c1 = make_component("app/domain/user", "User", Some(ArchLayer::Domain));
175 let c2 = make_component("app/domain/order", "Order", Some(ArchLayer::Domain));
176 graph.add_component(&c1);
177 graph.add_component(&c2);
178 graph.add_dependency(&make_dep(
179 "app/domain/user",
180 "app/domain/order",
181 "app/domain/order",
182 ));
183
184 let violations = evaluate_custom_rules(&graph, &rules);
185 assert!(violations.is_empty());
186 }
187}