Skip to main content

frs/cmds/rsl/rules/
relative_path.rs

1//! Relative-path rule for `frs rsl`.
2
3use std::path::Path;
4
5use proc_macro2::Span;
6use syn::Expr;
7use syn::UseTree;
8use syn::spanned::Spanned;
9use syn::visit::Visit;
10
11use super::common::Location;
12use crate::cmds::rsl::ast::is_test_module_declaration;
13use crate::cmds::rsl::engine::FileContext;
14use crate::cmds::rsl::rules::TypedRule;
15use crate::cmds::rsl::rules::TypedRuleViolation;
16
17pub struct RelativePathRule;
18
19impl TypedRule for RelativePathRule {
20    type Violation = RelativePathViolation;
21
22    fn code() -> &'static str {
23        "relative_path"
24    }
25
26    fn check(&self, ctx: &FileContext<'_>) -> Vec<Self::Violation> {
27        let mut violations = Vec::new();
28        let mut visitor = RelativePathVisitor {
29            source_path: ctx.path,
30            violations: &mut violations,
31            in_test_module: false,
32        };
33
34        for item in &ctx.file.items {
35            visitor.visit_item(item);
36        }
37
38        violations
39    }
40}
41
42#[derive(Debug)]
43#[cfg_attr(test, derive(Eq, PartialEq))]
44pub struct RelativePathViolation {
45    pub location: Location,
46}
47
48impl RelativePathViolation {
49    fn new(path: &Path, span: Span) -> Self {
50        Self {
51            location: Location::from_span(path, span),
52        }
53    }
54}
55
56impl TypedRuleViolation for RelativePathViolation {
57    type Rule = RelativePathRule;
58}
59
60struct RelativePathVisitor<'output> {
61    source_path: &'output Path,
62    violations: &'output mut Vec<RelativePathViolation>,
63    in_test_module: bool,
64}
65
66impl<'ast> Visit<'ast> for RelativePathVisitor<'_> {
67    fn visit_item_mod(&mut self, module: &'ast syn::ItemMod) {
68        let previous = self.in_test_module;
69        self.in_test_module |= is_test_module_declaration(module);
70        syn::visit::visit_item_mod(self, module);
71        self.in_test_module = previous;
72    }
73
74    fn visit_item_use(&mut self, item_use: &'ast syn::ItemUse) {
75        if !is_allowed_test_glob(item_use, self.in_test_module) {
76            let mut spans = Vec::new();
77            relative_use_spans(&item_use.tree, &mut spans);
78            self.violations.extend(
79                spans
80                    .into_iter()
81                    .map(|span| RelativePathViolation::new(self.source_path, span)),
82            );
83        }
84
85        syn::visit::visit_item_use(self, item_use);
86    }
87
88    fn visit_expr_call(&mut self, expression: &'ast syn::ExprCall) {
89        if let Expr::Path(path) = expression.func.as_ref()
90            && path_starts_with_super(&path.path)
91        {
92            self.violations
93                .push(RelativePathViolation::new(self.source_path, path.path.span()));
94        }
95
96        syn::visit::visit_expr_call(self, expression);
97    }
98
99    fn visit_macro(&mut self, _mac: &'ast syn::Macro) {}
100}
101
102fn is_allowed_test_glob(item_use: &syn::ItemUse, in_test_module: bool) -> bool {
103    in_test_module
104        && matches!(item_use.vis, syn::Visibility::Inherited)
105        && matches!(
106            &item_use.tree,
107            UseTree::Path(path)
108                if path.ident == "super" && matches!(path.tree.as_ref(), UseTree::Glob(_))
109        )
110}
111
112fn relative_use_spans(tree: &UseTree, spans: &mut Vec<Span>) {
113    match tree {
114        UseTree::Path(path) if path.ident == "super" => spans.push(tree.span()),
115        UseTree::Group(group) => {
116            for tree in &group.items {
117                relative_use_spans(tree, spans);
118            }
119        }
120        UseTree::Name(name) if name.ident == "super" => spans.push(tree.span()),
121        UseTree::Rename(rename) if rename.ident == "super" => spans.push(tree.span()),
122        UseTree::Path(_) | UseTree::Glob(_) | UseTree::Name(_) | UseTree::Rename(_) => {}
123    }
124}
125
126fn path_starts_with_super(path: &syn::Path) -> bool {
127    path.segments.first().is_some_and(|segment| segment.ident == "super")
128}
129
130#[cfg(test)]
131mod tests {
132    use std::path::PathBuf;
133
134    use test_that::prelude::*;
135
136    use super::*;
137    use crate::cmds::rsl::rules::TypedRule;
138    use crate::cmds::rsl::rules::common::Location;
139
140    #[test]
141    fn test_relative_path_check_when_import_is_outside_tests_reports_violation() {
142        let syntax = syn::parse_file(
143            r"
144            mod parent {
145                fn helper() {}
146                mod child {
147                    use super::helper;
148                }
149            }
150            ",
151        )
152        .unwrap();
153
154        let result = RelativePathRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
155
156        assert_that!(
157            result,
158            eq(vec![RelativePathViolation {
159                location: Location::new(PathBuf::from("test.rs"), 5, 25),
160            }])
161        );
162    }
163
164    #[test]
165    fn test_relative_path_check_when_test_module_uses_super_glob_returns_no_violations() {
166        let syntax = syn::parse_file(
167            r"
168            #[cfg(test)]
169            mod tests {
170                use super::*;
171            }
172            ",
173        )
174        .unwrap();
175
176        let result = RelativePathRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
177
178        assert_that!(result, is_empty());
179    }
180
181    #[test]
182    fn test_relative_path_check_when_test_module_uses_explicit_super_import_reports_violation() {
183        let syntax = syn::parse_file(
184            r"
185            #[cfg(test)]
186            mod tests {
187                use super::helper;
188            }
189            ",
190        )
191        .unwrap();
192
193        let result = RelativePathRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
194
195        assert_that!(
196            result,
197            eq(vec![RelativePathViolation {
198                location: Location::new(PathBuf::from("test.rs"), 4, 21),
199            }])
200        );
201    }
202
203    #[test]
204    fn test_relative_path_check_when_call_uses_super_reports_violation() {
205        let syntax = syn::parse_file(
206            r"
207            mod parent {
208                fn helper() {}
209                mod child {
210                    fn run() {
211                        super::helper();
212                    }
213                }
214            }
215            ",
216        )
217        .unwrap();
218
219        let result = RelativePathRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
220
221        assert_that!(
222            result,
223            eq(vec![RelativePathViolation {
224                location: Location::new(PathBuf::from("test.rs"), 6, 25),
225            }])
226        );
227    }
228
229    #[test]
230    fn test_relative_path_check_when_test_call_uses_super_reports_violation() {
231        let syntax = syn::parse_file(
232            r"
233            mod parent {
234                fn helper() {}
235                #[cfg(test)]
236                mod tests {
237                    use super::*;
238                    fn run() {
239                        super::helper();
240                    }
241                }
242            }
243            ",
244        )
245        .unwrap();
246
247        let result = RelativePathRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
248
249        assert_that!(
250            result,
251            eq(vec![RelativePathViolation {
252                location: Location::new(PathBuf::from("test.rs"), 8, 25),
253            }])
254        );
255    }
256
257    #[test]
258    fn test_relative_path_check_when_block_import_uses_super_reports_violation() {
259        let syntax = syn::parse_file(
260            r"
261            mod parent {
262                fn helper() {}
263                mod child {
264                    fn run() {
265                        use super::helper;
266                    }
267                }
268            }
269            ",
270        )
271        .unwrap();
272
273        let result = RelativePathRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
274
275        assert_that!(
276            result,
277            eq(vec![RelativePathViolation {
278                location: Location::new(PathBuf::from("test.rs"), 6, 29),
279            }])
280        );
281    }
282}