1use crate::analysis::Workspace;
2use crate::config::AbsoluteModulePathsConfig;
3use crate::emit::Emitter;
4use crate::fix::{find_use_insertion_offset, line_col_to_byte_offset};
5use crate::report::{
6 Finding, FindingLabel, FindingLabelKind, FindingNote, FindingNoteKind, Fix, FixSafety,
7 Severity, TextEdit,
8};
9use crate::rules::{Rule, RuleBackend, RuleContext, RuleFamily, RuleInfo};
10use crate::span::Span;
11use quote::ToTokens;
12use std::collections::HashSet;
13use std::path::Path;
14use syn::spanned::Spanned;
15use syn::visit::Visit;
16
17pub struct AbsoluteModulePathsRule;
18
19impl AbsoluteModulePathsRule {
20 pub fn static_info() -> RuleInfo {
21 RuleInfo {
22 id: "architecture.qualified_module_paths",
23 family: RuleFamily::Architecture,
24 backend: RuleBackend::Syntax,
25 summary: "Flags direct `std::`, `crate::`, and `::` paths in code.",
26 default_level: AbsoluteModulePathsConfig::default().level,
27 schema: "level, allow_prefixes, roots, allow_crate_root_macros, allow_crate_root_consts, allow_crate_root_fn_calls",
28 config_example: "[rules.\"architecture.qualified_module_paths\"]\nlevel = \"deny\"\nroots = [\"std\", \"core\", \"alloc\", \"crate\"]",
29 fixable: true,
30 }
31 }
32}
33
34impl Rule for AbsoluteModulePathsRule {
35 fn info(&self) -> RuleInfo {
36 Self::static_info()
37 }
38
39 fn run(&self, ws: &Workspace, ctx: &RuleContext<'_>, out: &mut dyn Emitter) {
40 for file in &ws.files {
41 let cfg = match ctx
42 .policy
43 .decode_rule::<AbsoluteModulePathsConfig>(Self::static_info().id, Some(&file.path))
44 {
45 Ok(cfg) => cfg,
46 Err(_) => continue,
47 };
48 let Some(ast) = &file.ast else { continue };
49 let mut v = Visitor {
50 file_path: &file.path,
51 file_text: &file.text,
52 allow_prefixes: &cfg.allow_prefixes,
53 roots: &cfg.roots,
54 allow_crate_root_macros: cfg.allow_crate_root_macros,
55 allow_crate_root_consts: cfg.allow_crate_root_consts,
56 allow_crate_root_fn_calls: cfg.allow_crate_root_fn_calls,
57 severity: cfg.level.to_severity(),
58 out,
59 };
60 v.visit_file(ast);
61 }
62 }
63}
64
65struct Visitor<'a> {
66 file_path: &'a Path,
67 file_text: &'a str,
68 allow_prefixes: &'a [String],
69 roots: &'a [String],
70 allow_crate_root_macros: bool,
71 allow_crate_root_consts: bool,
72 allow_crate_root_fn_calls: bool,
73 severity: Severity,
74 out: &'a mut dyn Emitter,
75}
76
77impl Visitor<'_> {
78 fn allowed(&self, path_str: &str) -> bool {
79 self.allow_prefixes
80 .iter()
81 .any(|p| !p.is_empty() && path_str.starts_with(p))
82 }
83
84 fn emit_str(&mut self, span: proc_macro2::Span, path_str: String) {
85 if self.allowed(&path_str) {
86 return;
87 }
88 let fixes = Vec::new();
89 self.out.emit(Finding {
90 rule_id: AbsoluteModulePathsRule::static_info().id.to_string(),
91 family: Some(AbsoluteModulePathsRule::static_info().family),
92 engine: Some(AbsoluteModulePathsRule::static_info().backend),
93 severity: self.severity,
94 message: format!("qualified module path: {path_str}"),
95 primary: Some(Span::from_pm_span(self.file_path, span)),
96 secondary: Vec::new(),
97 help: Some("Import the item and use the local name.".to_string()),
98 evidence: None,
99 confidence: None,
100 tags: vec!["imports".to_string(), "style".to_string()],
101 labels: vec![FindingLabel {
102 kind: FindingLabelKind::Primary,
103 span: Span::from_pm_span(self.file_path, span),
104 message: Some("qualified path used here".to_string()),
105 }],
106 notes: vec![FindingNote {
107 kind: FindingNoteKind::Help,
108 message: "Import the item and use the local name.".to_string(),
109 }],
110 fixes,
111 });
112 }
113
114 fn emit_path(&mut self, span: proc_macro2::Span, path: &syn::Path) {
115 let path_str = path_to_string(path);
116 if should_flag_path(&path_str, self.roots) {
117 if self.allowed(&path_str) {
118 return;
119 }
120 let fixes = self.build_fixes(span, path);
121 self.out.emit(Finding {
122 rule_id: AbsoluteModulePathsRule::static_info().id.to_string(),
123 family: Some(AbsoluteModulePathsRule::static_info().family),
124 engine: Some(AbsoluteModulePathsRule::static_info().backend),
125 severity: self.severity,
126 message: format!("qualified module path: {path_str}"),
127 primary: Some(Span::from_pm_span(self.file_path, span)),
128 secondary: Vec::new(),
129 help: Some("Import the item and use the local name.".to_string()),
130 evidence: None,
131 confidence: None,
132 tags: vec!["imports".to_string(), "style".to_string()],
133 labels: vec![FindingLabel {
134 kind: FindingLabelKind::Primary,
135 span: Span::from_pm_span(self.file_path, span),
136 message: Some("qualified path used here".to_string()),
137 }],
138 notes: vec![FindingNote {
139 kind: FindingNoteKind::Help,
140 message: "Import the item and use the local name.".to_string(),
141 }],
142 fixes,
143 });
144 }
145 }
146
147 fn build_fixes(&self, span: proc_macro2::Span, path: &syn::Path) -> Vec<Fix> {
148 if path.leading_colon.is_some() {
149 return Vec::new();
150 }
151
152 let (import_path, replacement, imported_name) = match compute_import_and_replacement(path) {
153 Some(v) => v,
154 None => return Vec::new(),
155 };
156
157 let mut safety = FixSafety::Safe;
158 if name_conflicts(self.file_text, imported_name.as_deref()) {
159 safety = FixSafety::Unsafe;
160 }
161
162 let start = span.start();
163 let end = span.end();
164 let byte_start = match line_col_to_byte_offset(
165 self.file_text,
166 start.line as u32,
167 (start.column as u32).saturating_add(1),
168 ) {
169 Ok(v) => v,
170 Err(_) => return Vec::new(),
171 };
172 let byte_end = match line_col_to_byte_offset(
173 self.file_text,
174 end.line as u32,
175 (end.column as u32).saturating_add(1),
176 ) {
177 Ok(v) => v,
178 Err(_) => return Vec::new(),
179 };
180
181 let mut edits = Vec::new();
182 edits.push(TextEdit {
183 file: self.file_path.to_string_lossy().to_string(),
184 byte_start: byte_start as u32,
185 byte_end: byte_end as u32,
186 replacement: replacement.to_string(),
187 });
188
189 if !self.file_text.contains(&format!("use {import_path};")) {
190 let insert_at = find_use_insertion_offset(self.file_text);
191 edits.push(TextEdit {
192 file: self.file_path.to_string_lossy().to_string(),
193 byte_start: insert_at as u32,
194 byte_end: insert_at as u32,
195 replacement: format!("use {import_path};\n"),
196 });
197 }
198
199 vec![Fix {
200 id: format!("{}::import", AbsoluteModulePathsRule::static_info().id),
201 safety,
202 message: format!("Import `{import_path}` and use `{replacement}`."),
203 edits,
204 }]
205 }
206}
207
208impl<'ast> Visit<'ast> for Visitor<'_> {
209 fn visit_item_use(&mut self, node: &'ast syn::ItemUse) {
210 if node.leading_colon.is_some() {
211 if let Some(path_str) = use_tree_path_str(true, &node.tree) {
212 self.emit_str(node.span(), path_str);
213 }
214 }
215 }
216
217 fn visit_type_path(&mut self, node: &'ast syn::TypePath) {
218 self.emit_path(node.span(), &node.path);
219 syn::visit::visit_type_path(self, node);
220 }
221
222 fn visit_expr_path(&mut self, node: &'ast syn::ExprPath) {
223 let path_str = path_to_string(&node.path);
224 if should_flag_path(&path_str, self.roots)
225 && !is_allowed_crate_root_const(&path_str, self.allow_crate_root_consts)
226 {
227 self.emit_path(node.span(), &node.path);
228 }
229 syn::visit::visit_expr_path(self, node);
230 }
231
232 fn visit_pat(&mut self, node: &'ast syn::Pat) {
233 if let syn::Pat::Path(p) = node {
234 let path_str = path_to_string(&p.path);
235 if should_flag_path(&path_str, self.roots)
236 && !is_allowed_crate_root_const(&path_str, self.allow_crate_root_consts)
237 {
238 self.emit_path(p.span(), &p.path);
239 }
240 }
241 syn::visit::visit_pat(self, node);
242 }
243
244 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
245 if self.allow_crate_root_fn_calls {
246 if let syn::Expr::Path(func) = node.func.as_ref() {
247 let path_str = path_to_string(&func.path);
248 if is_allowed_crate_root_call(&path_str) {
249 for arg in &node.args {
250 self.visit_expr(arg);
251 }
252 return;
253 }
254 }
255 }
256 syn::visit::visit_expr_call(self, node);
257 }
258
259 fn visit_macro(&mut self, node: &'ast syn::Macro) {
260 let path_str = path_to_string(&node.path);
261 if should_flag_path(&path_str, self.roots)
262 && !(self.allow_crate_root_macros && is_allowed_crate_root_macro(&path_str))
263 {
264 self.emit_str(node.span(), path_str);
265 }
266 syn::visit::visit_macro(self, node);
267 }
268}
269
270#[cfg(test)]
271mod tests;
272
273fn should_flag_path(path_str: &str, roots: &[String]) -> bool {
274 if path_str.starts_with("::") {
275 return true;
276 }
277
278 let first = path_str.split("::").next().unwrap_or("");
279 if first.is_empty() {
280 return false;
281 }
282 if !roots.iter().any(|r| r == first) {
283 return false;
284 }
285
286 path_str.contains("::")
287}
288
289fn use_tree_path_str(leading_colon: bool, tree: &syn::UseTree) -> Option<String> {
290 fn flatten(tree: &syn::UseTree, out: &mut Vec<String>) {
291 match tree {
292 syn::UseTree::Path(p) => {
293 out.push(p.ident.to_string());
294 flatten(&p.tree, out);
295 }
296 syn::UseTree::Name(n) => out.push(n.ident.to_string()),
297 syn::UseTree::Rename(r) => out.push(r.ident.to_string()),
298 syn::UseTree::Glob(_) => out.push("*".to_string()),
299 syn::UseTree::Group(g) => {
300 if g.items.len() == 1 {
301 flatten(&g.items[0], out);
302 } else {
303 out.push("{...}".to_string());
304 }
305 }
306 }
307 }
308
309 let mut parts = Vec::new();
310 flatten(tree, &mut parts);
311 if parts.is_empty() {
312 return None;
313 }
314
315 let mut s = parts.join("::");
316 if leading_colon {
317 s = format!("::{s}");
318 }
319 Some(s)
320}
321
322fn path_to_string(path: &syn::Path) -> String {
323 path.to_token_stream().to_string().replace(' ', "")
324}
325
326fn is_allowed_crate_root_call(path_str: &str) -> bool {
327 is_two_segment_crate_root(path_str)
328}
329
330fn is_allowed_crate_root_macro(path_str: &str) -> bool {
331 is_two_segment_crate_root(path_str)
332}
333
334fn is_allowed_crate_root_const(path_str: &str, enabled: bool) -> bool {
335 if !enabled {
336 return false;
337 }
338 let Some(ident) = two_segment_crate_root_ident(path_str) else {
339 return false;
340 };
341 is_screaming_snake(&ident)
342}
343
344fn is_two_segment_crate_root(path_str: &str) -> bool {
345 two_segment_crate_root_ident(path_str).is_some()
346}
347
348fn two_segment_crate_root_ident(path_str: &str) -> Option<String> {
349 if path_str.starts_with("::") {
350 return None;
351 }
352 let mut parts = path_str.split("::");
353 let first = parts.next()?;
354 let second = parts.next()?;
355 if parts.next().is_some() {
356 return None;
357 }
358 if first != "crate" {
359 return None;
360 }
361 if second.is_empty() {
362 return None;
363 }
364 Some(second.to_string())
365}
366
367fn is_screaming_snake(ident: &str) -> bool {
368 let mut chars = ident.chars();
369 let Some(first) = chars.next() else {
370 return false;
371 };
372 if !first.is_ascii_uppercase() {
373 return false;
374 }
375 for c in chars {
376 if !(c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_') {
377 return false;
378 }
379 }
380 true
381}
382
383fn compute_import_and_replacement(path: &syn::Path) -> Option<(String, String, Option<String>)> {
384 if path.leading_colon.is_some() {
385 return None;
386 }
387
388 let segs: Vec<&syn::PathSegment> = path.segments.iter().collect();
389 if segs.len() < 2 {
390 return None;
391 }
392
393 let idents: Vec<String> = segs.iter().map(|s| s.ident.to_string()).collect();
394 if idents[0].is_empty() {
395 return None;
396 }
397
398 let last_ident = idents.last()?;
399 let penult_ident = &idents[idents.len() - 2];
400 let last_is_type = last_ident
401 .chars()
402 .next()
403 .is_some_and(|c| c.is_ascii_uppercase());
404 let penult_is_type = penult_ident
405 .chars()
406 .next()
407 .is_some_and(|c| c.is_ascii_uppercase());
408
409 let last_tokens = segs[segs.len() - 1]
410 .to_token_stream()
411 .to_string()
412 .replace(' ', "");
413 let penult_tokens = segs[segs.len() - 2]
414 .to_token_stream()
415 .to_string()
416 .replace(' ', "");
417
418 if segs.len() == 2 {
419 let import_path = idents.join("::");
420 let replacement = last_tokens;
421 return Some((import_path, replacement, Some(last_ident.to_string())));
422 }
423
424 if last_is_type {
425 let import_path = idents.join("::");
426 let replacement = last_tokens;
427 return Some((import_path, replacement, Some(last_ident.to_string())));
428 }
429
430 if penult_is_type {
431 let import_path = idents[..idents.len() - 1].join("::");
432 let replacement = format!("{penult_tokens}::{last_tokens}");
433 return Some((import_path, replacement, Some(penult_ident.to_string())));
434 }
435
436 let import_path = idents.join("::");
438 let replacement = last_tokens;
439 Some((import_path, replacement, Some(last_ident.to_string())))
440}
441
442fn name_conflicts(file_text: &str, name: Option<&str>) -> bool {
443 let Some(name) = name else { return false };
444 let Ok(ast) = syn::parse_file(file_text) else {
445 return false;
446 };
447
448 let mut collector = TakenNameCollector {
449 taken: HashSet::new(),
450 };
451 collector.visit_file(&ast);
452 let taken = collector.taken;
453 taken.contains(name)
454}
455
456struct TakenNameCollector {
457 taken: HashSet<String>,
458}
459
460impl TakenNameCollector {
461 fn insert_ident(&mut self, ident: &syn::Ident) {
462 self.taken.insert(ident.to_string());
463 }
464}
465
466impl<'ast> Visit<'ast> for TakenNameCollector {
467 fn visit_item_const(&mut self, node: &'ast syn::ItemConst) {
468 self.insert_ident(&node.ident);
469 syn::visit::visit_item_const(self, node);
470 }
471
472 fn visit_item_enum(&mut self, node: &'ast syn::ItemEnum) {
473 self.insert_ident(&node.ident);
474 syn::visit::visit_item_enum(self, node);
475 }
476
477 fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
478 self.insert_ident(&node.sig.ident);
479 syn::visit::visit_item_fn(self, node);
480 }
481
482 fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) {
483 self.insert_ident(&node.ident);
484 syn::visit::visit_item_mod(self, node);
485 }
486
487 fn visit_item_static(&mut self, node: &'ast syn::ItemStatic) {
488 self.insert_ident(&node.ident);
489 syn::visit::visit_item_static(self, node);
490 }
491
492 fn visit_item_struct(&mut self, node: &'ast syn::ItemStruct) {
493 self.insert_ident(&node.ident);
494 syn::visit::visit_item_struct(self, node);
495 }
496
497 fn visit_item_trait(&mut self, node: &'ast syn::ItemTrait) {
498 self.insert_ident(&node.ident);
499 syn::visit::visit_item_trait(self, node);
500 }
501
502 fn visit_item_type(&mut self, node: &'ast syn::ItemType) {
503 self.insert_ident(&node.ident);
504 syn::visit::visit_item_type(self, node);
505 }
506
507 fn visit_item_use(&mut self, node: &'ast syn::ItemUse) {
508 collect_use_names(&node.tree, &mut self.taken);
509 syn::visit::visit_item_use(self, node);
510 }
511
512 fn visit_local(&mut self, node: &'ast syn::Local) {
513 collect_pat_names(&node.pat, &mut self.taken);
514 syn::visit::visit_local(self, node);
515 }
516
517 fn visit_pat_ident(&mut self, node: &'ast syn::PatIdent) {
518 self.insert_ident(&node.ident);
519 syn::visit::visit_pat_ident(self, node);
520 }
521
522 fn visit_generic_param(&mut self, node: &'ast syn::GenericParam) {
523 match node {
524 syn::GenericParam::Type(param) => self.insert_ident(¶m.ident),
525 syn::GenericParam::Const(param) => self.insert_ident(¶m.ident),
526 syn::GenericParam::Lifetime(_) => {}
527 }
528 syn::visit::visit_generic_param(self, node);
529 }
530}
531
532fn collect_use_names(tree: &syn::UseTree, out: &mut HashSet<String>) {
533 match tree {
534 syn::UseTree::Path(p) => {
535 collect_use_names(&p.tree, out);
536 }
537 syn::UseTree::Name(n) => {
538 out.insert(n.ident.to_string());
539 }
540 syn::UseTree::Rename(r) => {
541 out.insert(r.rename.to_string());
542 }
543 syn::UseTree::Glob(_) => {}
544 syn::UseTree::Group(g) => {
545 for it in &g.items {
546 collect_use_names(it, out);
547 }
548 }
549 }
550}
551
552fn collect_pat_names(pat: &syn::Pat, out: &mut HashSet<String>) {
553 match pat {
554 syn::Pat::Ident(ident) => {
555 out.insert(ident.ident.to_string());
556 }
557 syn::Pat::Or(or_pat) => {
558 for case in &or_pat.cases {
559 collect_pat_names(case, out);
560 }
561 }
562 syn::Pat::Paren(paren) => collect_pat_names(&paren.pat, out),
563 syn::Pat::Reference(reference) => collect_pat_names(&reference.pat, out),
564 syn::Pat::Slice(slice) => {
565 for elem in &slice.elems {
566 collect_pat_names(elem, out);
567 }
568 }
569 syn::Pat::Struct(struct_pat) => {
570 for field in &struct_pat.fields {
571 collect_pat_names(&field.pat, out);
572 }
573 }
574 syn::Pat::Tuple(tuple) => {
575 for elem in &tuple.elems {
576 collect_pat_names(elem, out);
577 }
578 }
579 syn::Pat::TupleStruct(tuple) => {
580 for elem in &tuple.elems {
581 collect_pat_names(elem, out);
582 }
583 }
584 syn::Pat::Type(typed) => collect_pat_names(&typed.pat, out),
585 _ => {}
586 }
587}