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