Skip to main content

rscheck_cli/rules/layer_direction/
mod.rs

1use crate::analysis::Workspace;
2use crate::config::{LayerDirectionConfig, LayerRuleSet};
3use crate::emit::Emitter;
4use crate::report::{Finding, Severity};
5use crate::rules::use_tree_path::flatten as flatten_use_tree_path;
6use crate::rules::{Rule, RuleBackend, RuleContext, RuleFamily, RuleInfo};
7use crate::span::Span;
8use globset::{Glob, GlobSetBuilder};
9use quote::ToTokens;
10use std::path::Path;
11use syn::spanned::Spanned;
12use syn::visit::Visit;
13
14pub struct LayerDirectionRule;
15
16impl LayerDirectionRule {
17    pub fn static_info() -> RuleInfo {
18        RuleInfo {
19            id: "architecture.layer_direction",
20            family: RuleFamily::Architecture,
21            backend: RuleBackend::Syntax,
22            summary: "Checks layer dependencies against configured direction rules.",
23            default_level: LayerDirectionConfig::default().level,
24            schema: "level, layers = [{ name, include, may_depend_on }]",
25            config_example: "[rules.\"architecture.layer_direction\"]\nlevel = \"deny\"\nlayers = [{ name = \"api\", include = [\"src/api/**\"], may_depend_on = [\"domain\"] }]",
26            fixable: false,
27        }
28    }
29}
30
31impl Rule for LayerDirectionRule {
32    fn info(&self) -> RuleInfo {
33        Self::static_info()
34    }
35
36    fn run(&self, ws: &Workspace, ctx: &RuleContext<'_>, out: &mut dyn Emitter) {
37        for file in &ws.files {
38            let cfg = match ctx
39                .policy
40                .decode_rule::<LayerDirectionConfig>(Self::static_info().id, Some(&file.path))
41            {
42                Ok(cfg) => cfg,
43                Err(_) => continue,
44            };
45            if !cfg.level.enabled() || cfg.layers.is_empty() {
46                continue;
47            }
48            let Some(current) = match_layer(&cfg.layers, &file.path) else {
49                continue;
50            };
51            let Some(ast) = &file.ast else { continue };
52            let mut visitor = LayerVisitor {
53                file: &file.path,
54                current,
55                layers: &cfg.layers,
56                severity: cfg.level.to_severity(),
57                out,
58            };
59            visitor.visit_file(ast);
60        }
61    }
62}
63
64struct LayerVisitor<'a> {
65    file: &'a Path,
66    current: &'a LayerRuleSet,
67    layers: &'a [LayerRuleSet],
68    severity: Severity,
69    out: &'a mut dyn Emitter,
70}
71
72impl LayerVisitor<'_> {
73    fn check_path(&mut self, span: proc_macro2::Span, path: &syn::Path) {
74        let text = path.to_token_stream().to_string().replace(' ', "");
75        let Some(target) = self
76            .layers
77            .iter()
78            .find(|layer| text.starts_with(&format!("crate::{}", layer.name)))
79        else {
80            return;
81        };
82        if target.name == self.current.name
83            || self
84                .current
85                .may_depend_on
86                .iter()
87                .any(|name| name == &target.name)
88        {
89            return;
90        }
91        self.out.emit(Finding {
92            rule_id: LayerDirectionRule::static_info().id.to_string(),
93            family: Some(LayerDirectionRule::static_info().family),
94            engine: Some(LayerDirectionRule::static_info().backend),
95            severity: self.severity,
96            message: format!(
97                "layer `{}` cannot depend on `{}` through `{text}`",
98                self.current.name, target.name
99            ),
100            primary: Some(Span::from_pm_span(self.file, span)),
101            secondary: Vec::new(),
102            help: Some("Move the dependency behind an allowed layer boundary.".to_string()),
103            evidence: None,
104            confidence: None,
105            tags: vec!["architecture".to_string(), "layers".to_string()],
106            labels: Vec::new(),
107            notes: Vec::new(),
108            fixes: Vec::new(),
109        });
110    }
111}
112
113impl<'ast> Visit<'ast> for LayerVisitor<'_> {
114    fn visit_item_use(&mut self, node: &'ast syn::ItemUse) {
115        if let Some(path) = use_tree_path(&node.tree) {
116            self.check_path(node.span(), &path);
117        }
118        syn::visit::visit_item_use(self, node);
119    }
120}
121
122fn match_layer<'a>(layers: &'a [LayerRuleSet], path: &Path) -> Option<&'a LayerRuleSet> {
123    let candidate = path.to_string_lossy();
124    layers
125        .iter()
126        .find(|layer| glob_matches(&layer.include, candidate.as_ref()))
127}
128
129fn glob_matches(patterns: &[String], candidate: &str) -> bool {
130    let mut builder = GlobSetBuilder::new();
131    for pattern in patterns {
132        let Ok(glob) = Glob::new(pattern) else {
133            continue;
134        };
135        builder.add(glob);
136    }
137    let Ok(set) = builder.build() else {
138        return false;
139    };
140    set.is_match(candidate)
141}
142
143fn use_tree_path(tree: &syn::UseTree) -> Option<syn::Path> {
144    flatten_use_tree_path(tree)
145}
146
147#[cfg(test)]
148mod tests;