rulewright 0.1.1

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

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "pub mod with pub use reexport",
        code: "pub mod db {\n    pub struct Connection;\n}\npub use db::Connection;",
        pass: false,
    },
    Example {
        label: "pub crate mod with pub use",
        code: "pub(crate) mod db {\n    pub struct Connection;\n}\npub use db::Connection;",
        pass: true,
    },
    Example {
        label: "pub mod with private use",
        code: "pub mod db {\n    pub struct Connection;\n}\nuse db::Connection;",
        pass: true,
    },
    Example {
        label: "self-prefixed reexport",
        code: "pub mod db;\npub use self::db::Connection;",
        pass: false,
    },
    Example {
        label: "grouped reexport",
        code: "pub mod db;\nmod other;\npub use {db::Connection, other::Helper};",
        pass: false,
    },
    Example {
        label: "renamed reexport",
        code: "pub mod db;\npub use db::Connection as Conn;",
        pass: false,
    },
    Example {
        label: "module reexported under new name",
        code: "pub mod db;\npub use db as database;",
        pass: false,
    },
    Example {
        label: "doc hidden module",
        code: "#[doc(hidden)]\npub mod internals;\npub use internals::Helper;",
        pass: true,
    },
    Example {
        label: "underscore module",
        code: "pub mod _private;\npub use _private::Helper;",
        pass: true,
    },
    Example {
        label: "reexport from private sibling",
        code: "mod db {\n    pub struct Connection;\n}\npub use db::Connection;",
        pass: true,
    },
    Example {
        label: "nested module siblings",
        code: "pub mod outer {\n    pub mod inner {\n        pub struct A;\n    }\n    pub use inner::A;\n}",
        pass: false,
    },
    Example {
        label: "reexport in test module",
        code: "#[cfg(test)]\nmod tests {\n    pub mod db {\n        pub struct Connection;\n    }\n    pub use db::Connection;\n}",
        pass: true,
    },
];

crate::ast_rule!(
    single_item_path,
    "Flag `pub use` re-exports that duplicate paths already public through a sibling `pub mod`.",
    "Items reachable through two public paths clutter the API and confuse navigation; make the module non-pub or drop the re-export.",
    Medium,
);

fn check_single_item_path(ctx: &AstCtx<'_>) -> Vec<Violation> {
    let mut out = Vec::new();

    check_siblings(ctx, ctx.root.syntax(), &mut out);

    for module in ctx
        .nodes::<ast::Module>()
        .filter(|module| !ctx.is_in_test(module))
    {
        if let Some(list) = module.item_list() {
            check_siblings(ctx, list.syntax(), &mut out);
        }
    }

    out
}

fn check_siblings(ctx: &AstCtx<'_>, scope: &SyntaxNode, out: &mut Vec<Violation>) {
    let items: Vec<_> = scope.children().filter_map(ast::Item::cast).collect();
    let mods = public_mod_names(&items);

    if mods.is_empty() {
        return;
    }

    for item in &items {
        let ast::Item::Use(use_item) = item else {
            continue;
        };

        if !is_pub(use_item.visibility()) || ctx.is_in_test(use_item) {
            continue;
        }

        if let Some(tree) = use_item.use_tree() {
            check_use_tree(ctx, &tree, &mods, out);
        }
    }
}

// #rw(fn: rust_alloc_in_loop) sibling module names are owned map keys.
fn public_mod_names(items: &[ast::Item]) -> HashSet<String> {
    let mut names = HashSet::default();

    for item in items {
        let ast::Item::Module(module) = item else {
            continue;
        };

        if !is_pub(module.visibility()) {
            continue;
        }

        let Some(name) = module.name().map(|name| name.text().to_string()) else {
            continue;
        };

        if !name.starts_with('_') && !is_doc_hidden(module) {
            names.insert(name);
        }
    }

    names
}

fn is_doc_hidden(module: &ast::Module) -> bool {
    module.attrs().any(|attr| {
        attr.simple_name().as_deref() == Some("doc")
            && attr.syntax().text().to_string().contains("hidden")
    })
}

fn check_use_tree(
    ctx: &AstCtx<'_>,
    tree: &ast::UseTree,
    mods: &HashSet<String>,
    out: &mut Vec<Violation>,
) {
    let mut stack = vec![tree.clone()];

    while let Some(tree) = stack.pop() {
        if let Some(list) = tree.use_tree_list() {
            stack.extend(list.use_trees());
            continue;
        }

        if let Some(path) = tree.path() {
            let mut current = Some(path);
            let mut origin = None;

            while let Some(path) = current {
                if let Some(name) = path.segment().and_then(|segment| segment.name_ref())
                    && name.text() != "self"
                {
                    origin = Some(name);
                }

                current = path.qualifier();
            }

            if let Some(name_ref) = origin {
                let name = name_ref.text();

                if mods.contains(name.as_str()) {
                    out.push(dual_path_violation(ctx, &tree, name.as_str()));
                }
            }
        }
    }
}

fn dual_path_violation(ctx: &AstCtx<'_>, tree: &ast::UseTree, ident: &str) -> Violation {
    ctx.violation(
        tree,
        format!(
            "`pub use` re-exports through `pub mod {ident}` — items become public via two paths; make the module non-pub or drop the re-export"
        ),
    )
}

fn is_pub(visibility: Option<ast::Visibility>) -> bool {
    visibility.is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
}

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

    #[gtest]
    fn separate_scopes_do_not_pair() -> Result<()> {
        let v = run("pub mod a {\n\
                 mod db {\n\
                     pub struct Connection;\n\
                 }\n\
                 pub use db::Connection;\n\
             }\n\
             pub mod db;");
        verify_true!(v.is_empty())?;

        Ok(())
    }
});