rulewright 0.1.1

Executable engineering standards for Rust workspaces
Documentation
#[cfg(test)]
use googletest::prelude::*;

use super::{
    CARGO_WORKSPACE_REL, cargo_document, inherits_workspace, is_cargo_member, key_line,
    nested_table, section_line,
};
use crate::{Example, TomlCtx, Violation, violation};

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example { label: "workspace declares MSRV", code: "[workspace]\nmembers = []\n\n[workspace.package]\nedition = \"2024\"\nrust-version = \"1.85\"\n", pass: true },
    Example { label: "workspace without MSRV", code: "[workspace]\nmembers = []\n\n[workspace.package]\nedition = \"2024\"\n", pass: false },
    Example { label: "member inherits MSRV", code: "[package]\nname = \"foo\"\nrust-version.workspace = true\n", pass: true },
    Example { label: "member omits MSRV inheritance", code: "[package]\nname = \"foo\"\n", pass: false },
    Example { label: "member overrides MSRV", code: "[package]\nname = \"foo\"\nrust-version = \"1.85\"\n", pass: false },
];

crate::toml_rule!(
    toml_cargo_msrv,
    "Require the workspace to declare rust-version and members to inherit it instead of overriding it.",
    "A declared MSRV makes the supported-compiler contract explicit, and per-crate overrides silently fragment it.",
    Low,
);

fn check_toml_cargo_msrv(ctx: &TomlCtx<'_>) -> Vec<Violation> {
    let Some(document) = cargo_document(ctx) else {
        return Vec::new();
    };

    if ctx.file.rel == CARGO_WORKSPACE_REL {
        let has_workspace = document.contains_key("workspace");
        let (section, package) = if has_workspace {
            (
                "workspace.package",
                nested_table(&document, &["workspace", "package"]),
            )
        } else {
            ("package", nested_table(&document, &["package"]))
        };
        let declares_msrv = package
            .and_then(|package| package.get("rust-version"))
            .and_then(toml::Value::as_str)
            .is_some();

        let mut violations = Vec::new();

        if !declares_msrv {
            violations.push(violation(
                ctx.file.rel,
                section_line(ctx.file.lines, section),
                format!("declare literal `rust-version` (MSRV) in [{section}]"),
            ));
        }

        if has_workspace
            && let Some(root_package) = nested_table(&document, &["package"])
            && !root_package
                .get("rust-version")
                .is_some_and(inherits_workspace)
        {
            violations.push(violation(
                ctx.file.rel,
                key_line(ctx.file.lines, "package", "rust-version"),
                "root workspace package must use `rust-version.workspace = true`",
            ));
        }

        return violations;
    }

    if is_cargo_member(ctx.file.rel) && document.contains_key("package") {
        let inherits_msrv = nested_table(&document, &["package"])
            .and_then(|package| package.get("rust-version"))
            .is_some_and(inherits_workspace);

        if !inherits_msrv {
            return vec![violation(
                ctx.file.rel,
                key_line(ctx.file.lines, "package", "rust-version"),
                "member must inherit the workspace MSRV with `rust-version.workspace = true`",
            )];
        }
    }

    Vec::new()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[gtest]
    fn examples() -> Result<()> {
        for example in EXAMPLES {
            let rel = if example.code.contains("[workspace") {
                "Cargo.toml"
            } else {
                "nested/foo/Cargo.toml"
            };
            let violations =
                crate::test_support::check_source_toml_at(rel, example.code, check_toml_cargo_msrv);

            verify_eq!(violations.is_empty(), example.pass)?;
        }

        Ok(())
    }

    #[gtest]
    fn member_without_msrv_is_rejected() -> Result<()> {
        let violations = crate::test_support::check_source_toml_at(
            "nested/foo/Cargo.toml",
            "[package]\nname = \"foo\"\nedition.workspace = true\n",
            check_toml_cargo_msrv,
        );

        verify_eq!(violations.len(), 1)?;
        verify_true!(violations[0].message.contains("must inherit"))?;

        Ok(())
    }

    #[gtest]
    fn root_package_declares_its_own_msrv() -> Result<()> {
        let violations = crate::test_support::check_source_toml_at(
            "Cargo.toml",
            "[package]\nname = \"root\"\nrust-version = \"1.85\"\n",
            check_toml_cargo_msrv,
        );

        verify_true!(violations.is_empty())?;

        Ok(())
    }

    #[gtest]
    fn combined_root_package_inherits_workspace_msrv() -> Result<()> {
        let valid = crate::test_support::check_source_toml_at(
            "Cargo.toml",
            "[workspace]\nmembers = []\n\n[workspace.package]\nrust-version = \"1.85\"\n\n[package]\nname = \"root\"\nrust-version.workspace = true\n",
            check_toml_cargo_msrv,
        );
        let invalid = crate::test_support::check_source_toml_at(
            "Cargo.toml",
            "[workspace]\nmembers = []\n\n[workspace.package]\nrust-version = \"1.85\"\n\n[package]\nname = \"root\"\nrust-version = \"1.85\"\n",
            check_toml_cargo_msrv,
        );

        verify_true!(valid.is_empty())?;

        verify_that!(
            invalid,
            contains(field!(
                Violation.message,
                contains_substring("root workspace package")
            ))
        )
    }
}