selene_lib/lints/
restricted_module_paths.rs

1use super::*;
2use crate::ast_util::name_paths::{name_path, name_path_from_prefix_suffix, take_while_keep_going};
3use std::collections::HashMap;
4use std::convert::Infallible;
5
6use full_moon::{
7    ast::{self, Ast},
8    visitors::Visitor,
9};
10use serde::Deserialize;
11
12#[derive(Clone, Default, Deserialize)]
13#[serde(default)]
14pub struct RestrictedModulePathsConfig {
15    pub restricted_paths: HashMap<String, String>,
16}
17
18pub struct RestrictedModulePathsLint {
19    config: RestrictedModulePathsConfig,
20}
21
22impl Lint for RestrictedModulePathsLint {
23    type Config = RestrictedModulePathsConfig;
24    type Error = Infallible;
25
26    const SEVERITY: Severity = Severity::Error;
27    const LINT_TYPE: LintType = LintType::Correctness;
28
29    fn new(config: Self::Config) -> Result<Self, Self::Error> {
30        Ok(RestrictedModulePathsLint { config })
31    }
32
33    fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec<Diagnostic> {
34        if self.config.restricted_paths.is_empty() {
35            return Vec::new();
36        }
37
38        let mut visitor = RestrictedModulePathsVisitor {
39            restricted_paths: &self.config.restricted_paths,
40            violations: Vec::new(),
41        };
42
43        visitor.visit_ast(ast);
44
45        visitor.violations
46    }
47}
48
49struct RestrictedModulePathsVisitor<'a> {
50    restricted_paths: &'a HashMap<String, String>,
51    violations: Vec<Diagnostic>,
52}
53
54impl<'a> Visitor for RestrictedModulePathsVisitor<'a> {
55    fn visit_expression(&mut self, expression: &ast::Expression) {
56        self.check_expression(expression);
57    }
58
59    fn visit_function_call(&mut self, call: &ast::FunctionCall) {
60        // Handle function call statements (standalone function calls)
61        let mut keep_going = true;
62        let suffixes: Vec<&ast::Suffix> = call
63            .suffixes()
64            .take_while(|suffix| take_while_keep_going(suffix, &mut keep_going))
65            .collect();
66
67        if let Some(path) = name_path_from_prefix_suffix(call.prefix(), suffixes.iter().copied()) {
68            let full_path = path.join(".");
69
70            // Calculate range from prefix start to last suffix end
71            let start_pos = call.prefix().start_position().unwrap();
72            let end_pos = if let Some(last_suffix) = suffixes.last() {
73                last_suffix.end_position().unwrap()
74            } else {
75                call.prefix().end_position().unwrap()
76            };
77
78            self.check_path_restriction(&full_path, (start_pos.bytes(), end_pos.bytes()));
79        }
80    }
81}
82
83impl<'a> RestrictedModulePathsVisitor<'a> {
84    fn check_expression(&mut self, expression: &ast::Expression) {
85        // Only handle variable expressions here, function calls are handled by visit_function_call
86        if let ast::Expression::Var(_) = expression {
87            if let Some(path) = name_path(expression) {
88                let full_path = path.join(".");
89                let range = expression.range().unwrap();
90                self.check_path_restriction(&full_path, (range.0.bytes(), range.1.bytes()));
91            }
92        }
93    }
94
95    fn check_path_restriction(&mut self, full_path: &str, range: (usize, usize)) {
96        // Check if this path is restricted
97        if let Some(message) = self.restricted_paths.get(full_path) {
98            self.violations.push(Diagnostic::new_complete(
99                "restricted_module_paths",
100                format!("Module path `{full_path}` is restricted"),
101                Label::new((range.0 as u32, range.1 as u32)),
102                vec![message.clone()],
103                Vec::new(),
104            ));
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::{super::test_util::test_lint, *};
112    use std::collections::HashMap;
113
114    #[test]
115    fn test_restricted_module_paths() {
116        let mut restricted_paths = HashMap::new();
117        restricted_paths.insert(
118            "OldLibrary.Utils.deprecatedFunction".to_string(),
119            "OldLibrary.Utils.deprecatedFunction has been deprecated. Use NewLibrary.Utils.modernFunction instead.".to_string(),
120        );
121
122        test_lint(
123            RestrictedModulePathsLint::new(RestrictedModulePathsConfig { restricted_paths })
124                .unwrap(),
125            "restricted_module_paths",
126            "restricted_module_paths",
127        );
128    }
129}