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 end_line: imp.line,
65 name: None,
66 },
67 message: format!(
68 "Import `{}` violates layer boundary (importing from layer `{}` into lower layer)",
69 imp.source, imp_name
70 ),
71 suggested_refactorings: vec!["Move Method".into(), "Extract Interface".into()],
72 ..Default::default()
73 })
74 }
75
76 fn layer_of(&self, path: &str) -> Option<(String, u32)> {
77 self.layers
78 .iter()
79 .find(|(prefix, _)| path.contains(prefix.as_str()))
80 .map(|(name, level)| (name.clone(), *level))
81 }
82
83 pub fn from_config_str(s: &str) -> Self {
85 let layers = s
86 .split(',')
87 .filter_map(|part| {
88 let mut parts = part.trim().splitn(2, ':');
89 let name = parts.next()?.trim().to_string();
90 let level = parts.next()?.trim().parse().ok()?;
91 Some((name, level))
92 })
93 .collect();
94 Self { layers }
95 }
96}