Skip to main content

wowlab_tidy/languages/rust/rules/hygiene/
wildcard_imports.rs

1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{
4    AstNode,
5    ast::{self, HasVisibility},
6};
7
8use crate::{AstCtx, Example, Violation};
9
10#[rustfmt::skip]
11const EXAMPLES: &[Example] = &[
12    Example {
13        label: "wildcard import",
14        code: "use std::collections::*;",
15        pass: false,
16    },
17    Example {
18        label: "explicit import",
19        code: "use std::collections::HashMap;",
20        pass: true,
21    },
22    Example {
23        label: "prelude allowed",
24        code: "use my_crate::prelude::*;",
25        pass: true,
26    },
27    Example {
28        label: "super star allowed",
29        code: "use super::*;",
30        pass: true,
31    },
32    Example {
33        label: "super nested allowed",
34        code: "use super::types::*;",
35        pass: true,
36    },
37    Example {
38        label: "pub use reexport allowed",
39        code: "pub use my_module::*;",
40        pass: true,
41    },
42    Example {
43        label: "pub crate reexport allowed",
44        code: "pub(crate) use my_module::*;",
45        pass: true,
46    },
47    Example {
48        label: "private third party glob",
49        code: "use some_crate::types::*;",
50        pass: false,
51    },
52    Example {
53        label: "enum glob in fn allowed",
54        code: "fn f() { use MyEnum::*; }",
55        pass: true,
56    },
57    Example {
58        label: "enum glob at module level allowed",
59        code: "use ColType::*;",
60        pass: true,
61    },
62    Example {
63        label: "crate internal types glob allowed",
64        code: "use crate::types::*;",
65        pass: true,
66    },
67    Example {
68        label: "deep external glob",
69        code: "use external_crate::deep::module::*;",
70        pass: false,
71    },
72    Example {
73        label: "wildcard in test module",
74        code: "#[cfg(test)]\nmod tests {\n    use std::collections::*;\n}",
75        pass: true,
76    },
77    Example {
78        label: "nested prelude allowed",
79        code: "use some_crate::module::prelude::*;",
80        pass: true,
81    },
82];
83
84crate::ast_rule!(
85    wildcard_imports,
86    "Ban `use foo::*` outside tests and preludes.",
87    "Wildcard imports make it unclear where names come from and cause surprising breakage when upstream adds new items.",
88    Medium,
89);
90
91fn check_wildcard_imports(ctx: &AstCtx<'_>) -> Vec<Violation> {
92    ctx.nodes::<ast::Use>()
93        .filter(|item| !ctx.is_in_test(item) && item.visibility().is_none())
94        .flat_map(|item| {
95            item.use_tree()
96                .into_iter()
97                .flat_map(|tree| tree.syntax().descendants().filter_map(ast::UseTree::cast))
98                .filter_map(|tree| {
99                    let star = tree.star_token()?;
100                    let names = use_path_names(&tree);
101
102                    should_flag_glob(&names).then(|| {
103                        let line =
104                            ctx.line_index.line_col(star.text_range().start()).line as usize + 1;
105
106                        crate::violation(
107                            ctx.file.rel,
108                            line,
109                            "wildcard import (use explicit imports)",
110                        )
111                    })
112                })
113                .collect::<Vec<_>>()
114        })
115        .collect()
116}
117
118fn is_pascal_case(s: &str) -> bool {
119    let mut chars = s.chars();
120    let Some(first) = chars.next() else {
121        return false;
122    };
123
124    first.is_ascii_uppercase() && chars.any(|c| c.is_ascii_lowercase())
125}
126
127fn use_path_names(tree: &ast::UseTree) -> Vec<String> {
128    let mut trees: Vec<ast::UseTree> = tree
129        .syntax()
130        .ancestors()
131        .filter_map(ast::UseTree::cast)
132        .collect();
133
134    trees.reverse();
135
136    trees
137        .into_iter()
138        .filter_map(|tree| tree.path())
139        .flat_map(|path| {
140            path.syntax()
141                .descendants()
142                .filter_map(ast::NameRef::cast)
143                .map(|name| name.text().to_string())
144                .collect::<Vec<_>>()
145        })
146        .collect()
147}
148
149fn should_flag_glob(names: &[String]) -> bool {
150    let parent = names.last().map(String::as_str);
151    let grandparent = names.iter().rev().nth(1).map(String::as_str);
152
153    !parent.is_some_and(|name| name == "prelude" || name == "super")
154        && !grandparent.is_some_and(|name| name == "super" || name == "crate")
155        && !parent.is_some_and(is_pascal_case)
156}
157
158crate::tidy_ast_test!(check_wildcard_imports, {
159    crate::example_tests!(EXAMPLES, check_wildcard_imports);
160
161    #[gtest]
162    fn production_caught_test_allowed() -> Result<()> {
163        let v = run("use std::io::*;\n\
164             #[cfg(test)]\n\
165             mod tests {\n\
166                 use std::collections::*;\n\
167             }");
168        verify_eq!(v.len(), 1)?;
169        verify_eq!(v[0].line, 1)?;
170
171        Ok(())
172    }
173});