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