rulewright 0.1.2

Executable engineering standards for Rust workspaces
Documentation
#[cfg(test)]
use googletest::prelude::*;
use ra_ap_syntax::{
    AstNode,
    ast::{self, HasVisibility},
};

use crate::{AstCtx, Example, Violation};

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "wildcard import",
        code: "use std::collections::*;",
        pass: false,
    },
    Example {
        label: "explicit import",
        code: "use std::collections::HashMap;",
        pass: true,
    },
    Example {
        label: "prelude allowed",
        code: "use my_crate::prelude::*;",
        pass: true,
    },
    Example {
        label: "super star allowed",
        code: "use super::*;",
        pass: true,
    },
    Example {
        label: "super nested allowed",
        code: "use super::types::*;",
        pass: true,
    },
    Example {
        label: "pub use reexport allowed",
        code: "pub use my_module::*;",
        pass: true,
    },
    Example {
        label: "pub crate reexport allowed",
        code: "pub(crate) use my_module::*;",
        pass: true,
    },
    Example {
        label: "private third party glob",
        code: "use some_crate::types::*;",
        pass: false,
    },
    Example {
        label: "enum glob in fn allowed",
        code: "fn f() { use MyEnum::*; }",
        pass: true,
    },
    Example {
        label: "enum glob at module level allowed",
        code: "use ColType::*;",
        pass: true,
    },
    Example {
        label: "crate internal types glob allowed",
        code: "use crate::types::*;",
        pass: true,
    },
    Example {
        label: "deep external glob",
        code: "use external_crate::deep::module::*;",
        pass: false,
    },
    Example {
        label: "wildcard in test module",
        code: "#[cfg(test)]\nmod tests {\n    use std::collections::*;\n}",
        pass: true,
    },
    Example {
        label: "nested prelude allowed",
        code: "use some_crate::module::prelude::*;",
        pass: true,
    },
];

crate::ast_rule!(
    wildcard_imports,
    "Ban `use foo::*` outside tests and preludes.",
    "Wildcard imports make it unclear where names come from and cause surprising breakage when upstream adds new items.",
    Medium,
);

fn check_wildcard_imports(ctx: &AstCtx<'_>) -> Vec<Violation> {
    ctx.nodes::<ast::Use>()
        .filter(|item| !ctx.is_in_test(item) && item.visibility().is_none())
        .flat_map(|item| {
            item.use_tree()
                .into_iter()
                .flat_map(|tree| tree.syntax().descendants().filter_map(ast::UseTree::cast))
                .filter_map(|tree| {
                    let star = tree.star_token()?;
                    let names = use_path_names(&tree);

                    should_flag_glob(&names).then(|| {
                        let line =
                            ctx.line_index.line_col(star.text_range().start()).line as usize + 1;

                        crate::violation(
                            ctx.file.rel,
                            line,
                            "wildcard import (use explicit imports)",
                        )
                    })
                })
                .collect::<Vec<_>>()
        })
        .collect()
}

fn is_pascal_case(s: &str) -> bool {
    let mut chars = s.chars();
    let Some(first) = chars.next() else {
        return false;
    };

    first.is_ascii_uppercase() && chars.any(|c| c.is_ascii_lowercase())
}

fn use_path_names(tree: &ast::UseTree) -> Vec<String> {
    let mut trees: Vec<ast::UseTree> = tree
        .syntax()
        .ancestors()
        .filter_map(ast::UseTree::cast)
        .collect();

    trees.reverse();

    trees
        .into_iter()
        .filter_map(|tree| tree.path())
        .flat_map(|path| {
            path.syntax()
                .descendants()
                .filter_map(ast::NameRef::cast)
                .map(|name| name.text().to_string())
                .collect::<Vec<_>>()
        })
        .collect()
}

fn should_flag_glob(names: &[String]) -> bool {
    let parent = names.last().map(String::as_str);
    let grandparent = names.iter().rev().nth(1).map(String::as_str);

    !parent.is_some_and(|name| name == "prelude" || name == "super")
        && !grandparent.is_some_and(|name| name == "super" || name == "crate")
        && !parent.is_some_and(is_pascal_case)
}

crate::rulewright_ast_test!(check_wildcard_imports, {
    crate::example_tests!(EXAMPLES, check_wildcard_imports);

    #[gtest]
    fn production_caught_test_allowed() -> Result<()> {
        let v = run("use std::io::*;\n\
             #[cfg(test)]\n\
             mod tests {\n\
                 use std::collections::*;\n\
             }");
        verify_eq!(v.len(), 1)?;
        verify_eq!(v[0].line, 1)?;

        Ok(())
    }
});