Skip to main content

rscheck_cli/rules/absolute_module_paths/
mod.rs

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    // Value/function under a module: import the item directly and use the local name.
437    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(&param.ident),
525            syn::GenericParam::Const(param) => self.insert_ident(&param.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}