Skip to main content

frs/cmds/rsl/rules/
unqualified_call.rs

1//! Unqualified fn-call rule for `frs rsl`.
2
3use std::path::Path;
4
5use super::common::CallDetails;
6use super::common::FnCallFinding;
7use super::common::FnCallKind;
8use super::common::Location;
9use super::common::find_fn_calls;
10use crate::cmds::rsl::engine::FileContext;
11use crate::cmds::rsl::rules::TypedRule;
12use crate::cmds::rsl::rules::TypedRuleViolation;
13
14pub struct UnqualifiedCallRule;
15
16impl TypedRule for UnqualifiedCallRule {
17    type Violation = UnqualifiedCallViolation;
18
19    fn code() -> &'static str {
20        "unqualified_call"
21    }
22
23    fn check(&self, ctx: &FileContext<'_>) -> Vec<Self::Violation> {
24        find_fn_calls(ctx.file, FnCallKind::Unqualified)
25            .into_iter()
26            .map(|finding| UnqualifiedCallViolation::new(ctx.path, finding))
27            .collect()
28    }
29}
30
31#[derive(Debug)]
32#[cfg_attr(test, derive(Eq, PartialEq))]
33pub struct UnqualifiedCallViolation {
34    pub location: Location,
35    pub details: CallDetails,
36}
37
38impl UnqualifiedCallViolation {
39    fn new(path: &Path, finding: FnCallFinding) -> Self {
40        Self {
41            location: Location::from_span(path, finding.span),
42            details: CallDetails {
43                actual_path: finding.actual_path,
44                replacement_path: finding.suggestion.expected_path,
45                add_import: finding.suggestion.required_import,
46            },
47        }
48    }
49}
50
51impl TypedRuleViolation for UnqualifiedCallViolation {
52    type Rule = UnqualifiedCallRule;
53}
54
55#[cfg(test)]
56mod tests {
57    use std::path::PathBuf;
58
59    use test_that::prelude::*;
60
61    use super::*;
62    use crate::cmds::rsl::rules::TypedRule;
63    use crate::cmds::rsl::rules::common::Location;
64
65    #[test]
66    fn test_unqualified_call_check_when_same_module_call_is_bare_returns_no_violations() {
67        let syntax = syn::parse_file(
68            r"
69            fn helper() {}
70            fn run() {
71                helper();
72            }
73            ",
74        )
75        .unwrap();
76
77        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
78
79        assert_that!(result, is_empty());
80    }
81
82    #[test]
83    fn test_unqualified_call_check_when_same_module_call_is_bare_with_glob_import_returns_no_violations() {
84        let syntax = syn::parse_file(
85            r"
86            mod tests {
87                use super::*;
88
89                fn helper() {}
90
91                fn run() {
92                    helper();
93                }
94            }
95            ",
96        )
97        .unwrap();
98
99        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
100
101        assert_that!(result, is_empty());
102    }
103
104    #[test]
105    fn test_unqualified_call_check_when_parent_fn_is_bare_with_super_glob_import_returns_no_violations() {
106        let syntax = syn::parse_file(
107            r"
108            fn helper() {}
109
110            mod tests {
111                use super::*;
112                use test_that::prelude::*;
113
114                fn invoke() {
115                    helper();
116                }
117            }
118            ",
119        )
120        .unwrap();
121
122        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
123
124        assert_that!(result, is_empty());
125    }
126
127    #[test]
128    fn test_unqualified_call_check_when_explicit_import_is_mixed_with_glob_import_reports_imported_module() {
129        let syntax = syn::parse_file(
130            r"
131            mod external {
132                pub fn run() {}
133            }
134            mod tests {
135                use super::*;
136                use crate::external::run;
137
138                fn invoke() {
139                    run();
140                }
141            }
142            ",
143        )
144        .unwrap();
145
146        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
147
148        assert_that!(
149            result,
150            eq(vec![UnqualifiedCallViolation {
151                location: Location::new(PathBuf::from("test.rs"), 10, 21),
152                details: CallDetails {
153                    actual_path: "run".to_owned(),
154                    replacement_path: "external::run".to_owned(),
155                    add_import: Some("use crate::external;".to_owned()),
156                },
157            }])
158        );
159    }
160
161    #[test]
162    fn test_unqualified_call_check_when_parent_explicit_import_is_reexported_by_glob_reports_imported_module() {
163        let syntax = syn::parse_file(
164            r"
165            mod external {
166                pub fn run() {}
167            }
168            use crate::external::run;
169
170            mod tests {
171                use super::*;
172
173                fn invoke() {
174                    run();
175                }
176            }
177            ",
178        )
179        .unwrap();
180
181        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
182
183        assert_that!(
184            result,
185            eq(vec![UnqualifiedCallViolation {
186                location: Location::new(PathBuf::from("test.rs"), 11, 21),
187                details: CallDetails {
188                    actual_path: "run".to_owned(),
189                    replacement_path: "external::run".to_owned(),
190                    add_import: Some("use crate::external;".to_owned()),
191                },
192            }])
193        );
194    }
195
196    #[test]
197    fn test_unqualified_call_check_when_glob_imported_call_is_unresolved_returns_no_violations() {
198        let syntax = syn::parse_file(
199            r"
200            mod external {
201                pub fn imported() {}
202            }
203            mod tests {
204                use super::*;
205
206                fn run() {
207                    imported();
208                }
209            }
210            ",
211        )
212        .unwrap();
213
214        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
215
216        assert_that!(result, is_empty());
217    }
218
219    #[test]
220    fn test_unqualified_call_check_when_same_module_call_uses_self_returns_no_violations() {
221        let syntax = syn::parse_file(
222            r"
223            fn helper() {}
224            fn run() {
225                self::helper();
226            }
227            ",
228        )
229        .unwrap();
230
231        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
232
233        assert_that!(result, is_empty());
234    }
235
236    #[test]
237    fn test_unqualified_call_check_when_nested_fn_call_is_local_returns_no_violations() {
238        let syntax = syn::parse_file(
239            r"
240            fn run() {
241                fn helper() {}
242                helper();
243            }
244            ",
245        )
246        .unwrap();
247
248        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
249
250        assert_that!(result, is_empty());
251    }
252
253    #[test]
254    fn test_unqualified_call_check_when_callable_parameter_is_called_returns_no_violations() {
255        let syntax = syn::parse_file(
256            r"
257            fn run(check: impl Fn()) {
258                check();
259            }
260            ",
261        )
262        .unwrap();
263
264        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
265
266        assert_that!(result, is_empty());
267    }
268
269    #[test]
270    fn test_unqualified_call_check_when_closure_binding_is_called_returns_no_violations() {
271        let syntax = syn::parse_file(
272            r"
273            fn run() {
274                let check = || {};
275                check();
276            }
277            ",
278        )
279        .unwrap();
280
281        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
282
283        assert_that!(result, is_empty());
284    }
285
286    #[test]
287    fn test_unqualified_call_check_when_local_binding_shadows_imported_fn_returns_no_violations() {
288        let syntax = syn::parse_file(
289            r"
290            mod external {
291                pub fn check() {}
292            }
293            use external::check;
294            fn run() {
295                let check = || {};
296                check();
297            }
298            ",
299        )
300        .unwrap();
301
302        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
303
304        assert_that!(result, is_empty());
305    }
306
307    #[test]
308    fn test_unqualified_call_check_when_imported_foreign_call_is_bare_reports_call() {
309        let syntax = syn::parse_file(
310            r"
311            mod external;
312            use external::run;
313            fn main() {
314                run();
315            }
316            ",
317        )
318        .unwrap();
319
320        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
321
322        assert_that!(
323            result,
324            eq(vec![UnqualifiedCallViolation {
325                location: Location::new(PathBuf::from("test.rs"), 5, 17),
326                details: CallDetails {
327                    actual_path: "run".to_owned(),
328                    replacement_path: "external::run".to_owned(),
329                    add_import: None,
330                },
331            }])
332        );
333    }
334
335    #[test]
336    fn test_unqualified_call_check_when_imported_external_crate_call_is_bare_omits_import() {
337        let syntax = syn::parse_file(
338            r"
339            use tempfile::tempdir;
340            fn main() {
341                tempdir();
342            }
343            ",
344        )
345        .unwrap();
346
347        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
348
349        assert_that!(
350            result,
351            eq(vec![UnqualifiedCallViolation {
352                location: Location::new(PathBuf::from("test.rs"), 4, 17),
353                details: CallDetails {
354                    actual_path: "tempdir".to_owned(),
355                    replacement_path: "tempfile::tempdir".to_owned(),
356                    add_import: None,
357                },
358            }])
359        );
360    }
361
362    #[test]
363    fn test_unqualified_call_check_when_associated_fn_receiver_is_uppercase_returns_no_violations() {
364        let syntax = syn::parse_file(
365            r#"
366            fn open() {
367                File::open("foo.md");
368            }
369            "#,
370        )
371        .unwrap();
372
373        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
374
375        assert_that!(result, is_empty());
376    }
377
378    #[test]
379    fn test_unqualified_call_check_when_bare_uppercase_constructor_is_called_returns_no_violations() {
380        let syntax = syn::parse_file(
381            r"
382            fn read() {
383                Ok(());
384                Err(());
385                Some(1);
386            }
387            ",
388        )
389        .unwrap();
390
391        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
392
393        assert_that!(result, is_empty());
394    }
395
396    #[test]
397    fn test_unqualified_call_check_when_bare_prelude_fn_is_called_returns_no_violations() {
398        let syntax = syn::parse_file(
399            r"
400            fn read(value: usize) {
401                drop(value);
402            }
403            ",
404        )
405        .unwrap();
406
407        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
408
409        assert_that!(result, is_empty());
410    }
411
412    #[test]
413    fn test_unqualified_call_check_when_unknown_bare_fn_call_returns_no_violations() {
414        let syntax = syn::parse_file(
415            r#"
416            fn read() {
417                read_to_string("foo.md");
418            }
419            "#,
420        )
421        .unwrap();
422
423        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
424
425        assert_that!(result, is_empty());
426    }
427
428    #[test]
429    fn test_unqualified_call_check_when_nested_module_call_is_bare_returns_no_violations() {
430        let syntax = syn::parse_file(
431            r"
432            mod outer {
433                fn helper() {}
434                fn run() {
435                    helper();
436                }
437            }
438            ",
439        )
440        .unwrap();
441
442        let result = UnqualifiedCallRule.check(&crate::cmds::rsl::rules::test_ctx(&syntax));
443
444        assert_that!(result, is_empty());
445    }
446}