rulewright 0.1.2

Executable engineering standards for Rust workspaces
Documentation
use ra_ap_syntax::{
    AstNode,
    ast::{self, HasAttrs, HasDocComments, HasName, HasVisibility, VisibilityKind},
};

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

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "undocumented pub fn",
        code: "pub fn foo() {}",
        pass: false,
    },
    Example {
        label: "documented pub fn",
        code: "/// Does something.\npub fn foo() {}",
        pass: true,
    },
    Example {
        label: "private fn",
        code: "fn foo() {}",
        pass: true,
    },
    Example {
        label: "pub(crate) fn",
        code: "pub(crate) fn foo() {}",
        pass: true,
    },
    Example {
        label: "doc hidden pub fn",
        code: "#[doc(hidden)]\npub fn foo() {}",
        pass: true,
    },
    Example {
        label: "undocumented pub struct",
        code: "pub struct S;",
        pass: false,
    },
    Example {
        label: "undocumented pub enum",
        code: "pub enum E { A }",
        pass: false,
    },
    Example {
        label: "undocumented pub trait",
        code: "pub trait T {}",
        pass: false,
    },
    Example {
        label: "undocumented pub type alias",
        code: "pub type A = u8;",
        pass: false,
    },
    Example {
        label: "undocumented pub const",
        code: "pub const C: u8 = 1;",
        pass: false,
    },
    Example {
        label: "undocumented pub static",
        code: "pub static S: u8 = 1;",
        pass: false,
    },
    Example {
        label: "documented pub struct",
        code: "/// A struct.\npub struct S;",
        pass: true,
    },
    Example {
        label: "pub(crate) struct not fully public",
        code: "pub(crate) struct S;",
        pass: true,
    },
    Example {
        label: "doc hidden pub struct",
        code: "#[doc(hidden)]\npub struct S;",
        pass: true,
    },
    Example {
        label: "doc name-value attr pub struct",
        code: "#[doc = \"x\"]\npub struct S;",
        pass: true,
    },
];

crate::ast_rule!(
    pub_api_docs,
    "Require doc comments on public items.",
    "Undocumented public items force users to read source code. Doc comments generate searchable API documentation for publishable packages; packages that explicitly set `publish = false` are treated as internal workspace implementation and skipped.",
);

fn check_pub_api_docs(ctx: &AstCtx<'_>) -> Vec<Violation> {
    if ctx.file.is_explicitly_non_publishable() {
        return Vec::new();
    }

    let functions = ctx
        .nodes::<ast::Fn>()
        .filter_map(|item| missing_docs(ctx, &item, "function"));
    let structs = ctx
        .nodes::<ast::Struct>()
        .filter_map(|item| missing_docs(ctx, &item, "struct"));
    let enums = ctx
        .nodes::<ast::Enum>()
        .filter_map(|item| missing_docs(ctx, &item, "enum"));
    let traits = ctx
        .nodes::<ast::Trait>()
        .filter_map(|item| missing_docs(ctx, &item, "trait"));
    let aliases = ctx
        .nodes::<ast::TypeAlias>()
        .filter_map(|item| missing_docs(ctx, &item, "type alias"));
    let constants = ctx
        .nodes::<ast::Const>()
        .filter_map(|item| missing_docs(ctx, &item, "const"));
    let statics = ctx
        .nodes::<ast::Static>()
        .filter_map(|item| missing_docs(ctx, &item, "static"));

    functions
        .chain(structs)
        .chain(enums)
        .chain(traits)
        .chain(aliases)
        .chain(constants)
        .chain(statics)
        .collect()
}

fn missing_docs<T>(ctx: &AstCtx<'_>, item: &T, kind: &str) -> Option<Violation>
where
    T: AstNode + HasAttrs + HasDocComments + HasName + HasVisibility,
{
    if ctx.is_in_test(item)
        || !item
            .visibility()
            .is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
        || item.doc_comments().next().is_some()
        || item
            .attrs()
            .any(|attr| attr.simple_name().as_deref() == Some("doc"))
    {
        return None;
    }

    let name = item.name()?;

    Some(ctx.violation(
        &name,
        format!("public {kind} `{name}` is missing documentation"),
    ))
}

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

    #[test]
    fn package_publishability_controls_downstream_api_docs() {
        let source = "pub fn undocumented() {}";

        assert_eq!(
            crate::test_support::check_source_ast_at("fixture.rs", source, check_pub_api_docs,)
                .len(),
            1
        );
        assert!(
            crate::test_support::check_source_ast_publishability(
                source,
                false,
                check_pub_api_docs,
            )
            .is_empty()
        );
        assert_eq!(
            crate::test_support::check_source_ast_publishability(source, true, check_pub_api_docs,)
                .len(),
            1
        );
    }
});