cha_core/plugins/
layer_violation.rs1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3#[derive(Default)]
13pub struct LayerViolationAnalyzer {
14 pub layers: Vec<(String, u32)>,
16}
17
18impl Plugin for LayerViolationAnalyzer {
19 fn name(&self) -> &str {
20 "layer_violation"
21 }
22
23 fn smells(&self) -> Vec<String> {
24 vec!["layer_violation".into()]
25 }
26
27 fn description(&self) -> &str {
28 "Cross-layer dependency violation"
29 }
30
31 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
32 if self.layers.is_empty() {
33 return vec![];
34 }
35
36 let file_path = ctx.file.path.to_string_lossy();
37 let file_layer = self.layer_of(&file_path);
38
39 ctx.model
40 .imports
41 .iter()
42 .filter_map(|imp| self.check_import(ctx, &file_layer, imp))
43 .collect()
44 }
45}
46
47impl LayerViolationAnalyzer {
48 fn check_import(
50 &self,
51 ctx: &AnalysisContext,
52 file_layer: &Option<(String, u32)>,
53 imp: &crate::ImportInfo,
54 ) -> Option<Finding> {
55 let import_layer = self.layer_of(&imp.source);
56 let (_, file_level) = file_layer.as_ref()?;
57 let (imp_name, imp_level) = import_layer.as_ref()?;
58 if file_level >= imp_level {
59 return None;
60 }
61 Some(Finding {
62 smell_name: "layer_violation".into(),
63 category: SmellCategory::Couplers,
64 severity: Severity::Error,
65 location: Location {
66 path: ctx.file.path.clone(),
67 start_line: imp.line,
68 start_col: imp.col,
69 end_line: imp.line,
70 name: None,
71 ..Default::default()
72 },
73 message: format!(
74 "Import `{}` violates layer boundary (importing from layer `{}` into lower layer)",
75 imp.source, imp_name
76 ),
77 suggested_refactorings: vec!["Move Method".into(), "Extract Interface".into()],
78 ..Default::default()
79 })
80 }
81
82 fn layer_of(&self, path: &str) -> Option<(String, u32)> {
83 self.layers
84 .iter()
85 .find(|(prefix, _)| path.contains(prefix.as_str()))
86 .map(|(name, level)| (name.clone(), *level))
87 }
88
89 pub fn from_config_str(s: &str) -> Self {
91 let layers = s
92 .split(',')
93 .filter_map(|part| {
94 let mut parts = part.trim().splitn(2, ':');
95 let name = parts.next()?.trim().to_string();
96 let level = parts.next()?.trim().parse().ok()?;
97 Some((name, level))
98 })
99 .collect();
100 Self { layers }
101 }
102}