frs/cmds/rsl/rules/
relative_path.rs1use 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}