Skip to main content

frs/cmds/rsl/rules/
qualified_item.rs

1//! Qualified-item rule for `frs rsl`.
2
3use std::path::Path;
4
5use proc_macro2::Span;
6use syn::Expr;
7use syn::spanned::Spanned;
8use syn::visit::Visit;
9
10use super::common::Location;
11use super::common::ModuleIdx;
12use super::common::associated_receiver_parts;
13use super::common::has_name_clash_parts;
14use super::common::is_import_style_path;
15use super::common::module_idx;
16use super::common::path_parts;
17use crate::cmds::rsl::engine::FileContext;
18use crate::cmds::rsl::rules::TypedRule;
19use crate::cmds::rsl::rules::TypedRuleViolation;
20
21pub(super) const QUALIFIED_ALLOWED_PATHS: &[&str] = &[
22    "anyhow::Result",
23    "rootcause::Result",
24    "std::fmt::Result",
25    "std::io::Result",
26];
27
28#[derive(Debug)]
29#[cfg_attr(test, derive(Eq, PartialEq))]
30pub struct QualifiedItemViolation {
31    pub location: Location,
32    pub details: QualifiedItemDetails,
33}
34
35impl QualifiedItemViolation {
36    pub(super) fn new(path: &Path, span: Span, actual_path: String, expected_import: String) -> Self {
37        Self {
38            location: Location::from_span(path, span),
39            details: QualifiedItemDetails {
40                actual_path,
41                expected_import,
42            },
43        }
44    }
45}
46
47#[derive(Debug)]
48#[cfg_attr(test, derive(Eq, PartialEq))]
49pub struct QualifiedItemDetails {
50    pub actual_path: String,
51    pub expected_import: String,
52}
53
54pub struct QualifiedItemRule {
55    allowed_paths: &'static [&'static str],
56}
57
58impl QualifiedItemRule {
59    pub(super) const fn new(allowed_paths: &'static [&'static str]) -> Self {
60        Self { allowed_paths }
61    }
62}
63
64impl TypedRule for QualifiedItemRule {
65    type Violation = QualifiedItemViolation;
66
67    fn code() -> &'static str {
68        "qualified_item"
69    }
70
71    fn check(&self, ctx: &FileContext<'_>) -> Vec<Self::Violation> {
72        let idx = module_idx(ctx.file);
73        let mut violations = Vec::new();
74
75        for scope in &idx.scopes {
76            let mut visitor = QualifiedItemVisitor {
77                idx: &idx,
78                current_module: &scope.path,
79                source_path: ctx.path,
80                allowed_paths: self.allowed_paths,
81                violations: &mut violations,
82                skip_call_path: false,
83            };
84            for item in scope.items {
85                visitor.visit_item(item);
86            }
87        }
88
89        violations
90    }
91}
92
93struct QualifiedItemVisitor<'idx, 'ast, 'output> {
94    idx: &'idx ModuleIdx<'ast>,
95    current_module: &'idx [String],
96    source_path: &'idx Path,
97    allowed_paths: &'static [&'static str],
98    violations: &'output mut Vec<QualifiedItemViolation>,
99    skip_call_path: bool,
100}
101
102impl<'ast> Visit<'ast> for QualifiedItemVisitor<'_, '_, '_> {
103    fn visit_expr_call(&mut self, expression: &'ast syn::ExprCall) {
104        if let Expr::Path(path) = expression.func.as_ref()
105            && let Some(parts) = associated_receiver_parts(&path.path)
106        {
107            check_non_fn_path(self, &parts, path.path.span());
108        }
109
110        let previous = self.skip_call_path;
111        self.skip_call_path = matches!(expression.func.as_ref(), Expr::Path(_));
112        syn::visit::visit_expr_call(self, expression);
113        self.skip_call_path = previous;
114    }
115
116    fn visit_path(&mut self, path: &'ast syn::Path) {
117        if self.skip_call_path {
118            self.skip_call_path = false;
119        } else if let Some(parts) = path_parts(path) {
120            check_non_fn_path(self, &parts, path.span());
121        }
122
123        syn::visit::visit_path(self, path);
124    }
125
126    fn visit_item_mod(&mut self, _module: &'ast syn::ItemMod) {}
127
128    fn visit_item_use(&mut self, _item_use: &'ast syn::ItemUse) {}
129
130    fn visit_attribute(&mut self, _attribute: &'ast syn::Attribute) {}
131
132    fn visit_macro(&mut self, _mac: &'ast syn::Macro) {}
133}
134
135fn check_non_fn_path(visitor: &mut QualifiedItemVisitor<'_, '_, '_>, parts: &[String], span: Span) {
136    if parts.len() <= 1 || !is_import_style_path(parts) {
137        return;
138    }
139
140    let actual_path = parts.join("::");
141    if visitor.allowed_paths.iter().any(|allowed| *allowed == actual_path)
142        || has_name_clash_parts(visitor.idx, visitor.current_module, parts)
143    {
144        return;
145    }
146
147    visitor.violations.push(QualifiedItemViolation::new(
148        visitor.source_path,
149        span,
150        actual_path.clone(),
151        format!("use {actual_path};"),
152    ));
153}
154
155impl TypedRuleViolation for QualifiedItemViolation {
156    type Rule = QualifiedItemRule;
157}
158
159#[cfg(test)]
160mod tests {
161    use std::path::PathBuf;
162
163    use test_that::prelude::*;
164
165    use super::*;
166    use crate::cmds::rsl::rules::TypedRule;
167    use crate::cmds::rsl::rules::common::Location;
168
169    fn qualified_item_rule() -> QualifiedItemRule {
170        QualifiedItemRule::new(&[])
171    }
172
173    #[test]
174    fn test_qualified_item_check_when_non_fn_path_is_fully_qualified_reports_import() {
175        let syntax = syn::parse_file(
176            r"
177            mod values {
178                pub const VALUE: usize = 1;
179            }
180            fn read() -> usize {
181                crate::values::VALUE
182            }
183            ",
184        )
185        .unwrap();
186
187        let result = qualified_item_rule().check(&crate::cmds::rsl::rules::test_ctx(&syntax));
188
189        assert_that!(
190            result,
191            eq(vec![QualifiedItemViolation {
192                location: Location::new(PathBuf::from("test.rs"), 6, 17),
193                details: QualifiedItemDetails {
194                    actual_path: "crate::values::VALUE".to_owned(),
195                    expected_import: "use crate::values::VALUE;".to_owned(),
196                },
197            }])
198        );
199    }
200
201    #[test]
202    fn test_qualified_item_check_when_path_is_allowed_returns_no_violations() {
203        let syntax = syn::parse_file(
204            r"
205            fn inspect(_: std::fmt::Result) -> rootcause::Result<()> {
206                panic!()
207            }
208            ",
209        )
210        .unwrap();
211
212        let result = QualifiedItemRule::new(QUALIFIED_ALLOWED_PATHS).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
213
214        assert_that!(result, is_empty());
215    }
216
217    #[test]
218    fn test_qualified_item_check_when_path_is_not_allowed_reports_import() {
219        let syntax = syn::parse_file(
220            r"
221            fn inspect(_: std::fmt::Formatter<'_>) {}
222            ",
223        )
224        .unwrap();
225
226        let result = QualifiedItemRule::new(QUALIFIED_ALLOWED_PATHS).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
227
228        assert_that!(
229            result,
230            eq(vec![QualifiedItemViolation {
231                location: Location::new(PathBuf::from("test.rs"), 2, 27),
232                details: QualifiedItemDetails {
233                    actual_path: "std::fmt::Formatter".to_owned(),
234                    expected_import: "use std::fmt::Formatter;".to_owned(),
235                },
236            }])
237        );
238    }
239
240    #[test]
241    fn test_qualified_item_check_when_non_fn_name_clashes_allows_qualified_path() {
242        let syntax = syn::parse_file(
243            r"
244            mod values {
245                pub const VALUE: usize = 1;
246            }
247            const VALUE: usize = 2;
248            fn read() -> usize {
249                crate::values::VALUE
250            }
251            ",
252        )
253        .unwrap();
254
255        let result = qualified_item_rule().check(&crate::cmds::rsl::rules::test_ctx(&syntax));
256
257        assert_that!(result, is_empty());
258    }
259
260    #[test]
261    fn test_qualified_item_check_when_struct_names_clash_allows_one_qualified_path() {
262        let syntax = syn::parse_file(
263            r"
264            mod first {
265                pub struct Thing;
266            }
267            mod second {
268                pub struct Thing;
269            }
270            use crate::first::Thing;
271            fn read() -> crate::second::Thing {
272                panic!()
273            }
274            ",
275        )
276        .unwrap();
277
278        let result = qualified_item_rule().check(&crate::cmds::rsl::rules::test_ctx(&syntax));
279
280        assert_that!(result, is_empty());
281    }
282
283    #[test]
284    fn test_qualified_item_check_when_external_paths_are_qualified_reports_paths() {
285        let syntax = syn::parse_file(
286            r"
287            mod external;
288            use external::Thing;
289            struct Data;
290            impl Data {
291                fn run() {}
292                fn call(&self) {
293                    self.run();
294                    Self::run();
295                }
296            }
297            fn read() -> external::Thing {
298                external::Thing::new()
299            }
300            ",
301        )
302        .unwrap();
303
304        let result = qualified_item_rule().check(&crate::cmds::rsl::rules::test_ctx(&syntax));
305
306        assert_that!(
307            result,
308            eq(vec![
309                QualifiedItemViolation {
310                    location: Location::new(PathBuf::from("test.rs"), 12, 26),
311                    details: QualifiedItemDetails {
312                        actual_path: "external::Thing".to_owned(),
313                        expected_import: "use external::Thing;".to_owned(),
314                    },
315                },
316                QualifiedItemViolation {
317                    location: Location::new(PathBuf::from("test.rs"), 13, 17),
318                    details: QualifiedItemDetails {
319                        actual_path: "external::Thing".to_owned(),
320                        expected_import: "use external::Thing;".to_owned(),
321                    },
322                },
323            ])
324        );
325    }
326
327    #[test]
328    fn test_qualified_item_check_when_unknown_external_type_is_qualified_reports_import() {
329        let syntax = syn::parse_file(
330            r"
331            fn inspect(_: syn::ExprCall) {}
332            ",
333        )
334        .unwrap();
335
336        let result = qualified_item_rule().check(&crate::cmds::rsl::rules::test_ctx(&syntax));
337
338        assert_that!(
339            result,
340            eq(vec![QualifiedItemViolation {
341                location: Location::new(PathBuf::from("test.rs"), 2, 27),
342                details: QualifiedItemDetails {
343                    actual_path: "syn::ExprCall".to_owned(),
344                    expected_import: "use syn::ExprCall;".to_owned(),
345                },
346            }])
347        );
348    }
349
350    #[test]
351    fn test_qualified_item_check_when_enum_variant_is_qualified_ignores_path() {
352        let syntax = syn::parse_file(
353            r"
354            enum Kind {
355                First,
356                Second,
357            }
358            fn select(kind: Kind) -> Kind {
359                match kind {
360                    Kind::First => Kind::Second,
361                    Kind::Second => Kind::First,
362                }
363            }
364            ",
365        )
366        .unwrap();
367
368        let result = qualified_item_rule().check(&crate::cmds::rsl::rules::test_ctx(&syntax));
369
370        assert_that!(result, is_empty());
371    }
372
373    #[test]
374    fn test_qualified_item_check_when_associated_fn_is_referenced_ignores_path() {
375        let syntax = syn::parse_file(
376            r"
377            fn converter() -> fn(&String) -> &str {
378                String::as_str
379            }
380            ",
381        )
382        .unwrap();
383
384        let result = qualified_item_rule().check(&crate::cmds::rsl::rules::test_ctx(&syntax));
385
386        assert_that!(result, is_empty());
387    }
388}