Skip to main content

rscheck_cli/rules/banned_dependencies/
mod.rs

1use crate::analysis::Workspace;
2use crate::config::BannedDependenciesConfig;
3use crate::emit::Emitter;
4use crate::path_pattern::matches_path_prefix;
5use crate::report::{Finding, Severity};
6use crate::rules::use_tree_path::flatten as flatten_use_tree_path;
7use crate::rules::{Rule, RuleBackend, RuleContext, RuleFamily, RuleInfo};
8use crate::span::Span;
9use quote::ToTokens;
10use std::path::Path;
11use syn::spanned::Spanned;
12use syn::visit::Visit;
13
14pub struct BannedDependenciesRule;
15
16impl BannedDependenciesRule {
17    pub fn static_info() -> RuleInfo {
18        RuleInfo {
19            id: "architecture.banned_dependencies",
20            family: RuleFamily::Architecture,
21            backend: RuleBackend::Syntax,
22            summary: "Blocks configured module, crate, type, or function path prefixes.",
23            default_level: BannedDependenciesConfig::default().level,
24            schema: "level, banned_prefixes",
25            config_example: "[rules.\"architecture.banned_dependencies\"]\nlevel = \"deny\"\nbanned_prefixes = [\"std::sync::Mutex\", \"crate::legacy\"]",
26            fixable: false,
27        }
28    }
29}
30
31impl Rule for BannedDependenciesRule {
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::<BannedDependenciesConfig>(Self::static_info().id, Some(&file.path))
41            {
42                Ok(cfg) => cfg,
43                Err(_) => continue,
44            };
45            if !cfg.level.enabled() || cfg.banned_prefixes.is_empty() {
46                continue;
47            }
48            let Some(ast) = &file.ast else { continue };
49            let mut visitor = DependencyVisitor {
50                file: &file.path,
51                banned_prefixes: &cfg.banned_prefixes,
52                severity: cfg.level.to_severity(),
53                out,
54            };
55            visitor.visit_file(ast);
56        }
57    }
58}
59
60struct DependencyVisitor<'a> {
61    file: &'a Path,
62    banned_prefixes: &'a [String],
63    severity: Severity,
64    out: &'a mut dyn Emitter,
65}
66
67impl DependencyVisitor<'_> {
68    fn check_path(&mut self, span: proc_macro2::Span, path: &syn::Path) {
69        let text = path.to_token_stream().to_string().replace(' ', "");
70        if let Some(prefix) = self
71            .banned_prefixes
72            .iter()
73            .find(|prefix| matches_path_prefix(&text, prefix))
74        {
75            self.out.emit(Finding {
76                rule_id: BannedDependenciesRule::static_info().id.to_string(),
77                family: Some(BannedDependenciesRule::static_info().family),
78                engine: Some(BannedDependenciesRule::static_info().backend),
79                severity: self.severity,
80                message: format!("banned dependency path: {text}"),
81                primary: Some(Span::from_pm_span(self.file, span)),
82                secondary: Vec::new(),
83                help: Some(format!("Remove or replace dependency on `{prefix}`.")),
84                evidence: None,
85                confidence: None,
86                tags: vec!["dependencies".to_string()],
87                labels: Vec::new(),
88                notes: Vec::new(),
89                fixes: Vec::new(),
90            });
91        }
92    }
93}
94
95impl<'ast> Visit<'ast> for DependencyVisitor<'_> {
96    fn visit_item_use(&mut self, node: &'ast syn::ItemUse) {
97        if let Some(path) = use_tree_path(&node.tree) {
98            self.check_path(node.span(), &path);
99        }
100        syn::visit::visit_item_use(self, node);
101    }
102
103    fn visit_type_path(&mut self, node: &'ast syn::TypePath) {
104        self.check_path(node.span(), &node.path);
105        syn::visit::visit_type_path(self, node);
106    }
107
108    fn visit_expr_path(&mut self, node: &'ast syn::ExprPath) {
109        self.check_path(node.span(), &node.path);
110        syn::visit::visit_expr_path(self, node);
111    }
112}
113
114fn use_tree_path(tree: &syn::UseTree) -> Option<syn::Path> {
115    flatten_use_tree_path(tree)
116}
117
118#[cfg(test)]
119mod tests;