1use 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}