rulewright 0.2.0

Executable engineering standards for Rust workspaces
Documentation
use crate::{Example, Violation, WorkspaceCtx, matches_ignore, violation};

const RULE: &str = "toml_cargo_layers";
const EXAMPLES: &[Example] = &[Example {
    label: "unrestricted dependency",
    code: "[dependencies]\nserde = \"1\"",
    pass: true,
}];

crate::language_workspace_rule!(
    toml_cargo_layers,
    "Reject dependency edges configured as manifest-glob !> package-glob,package-glob.",
    "Explicit dependency boundaries prevent accidental coupling between workspace layers. Configure the boundaries appropriate to your project; no package names are reserved.",
    High,
    default = false,
    full_manifest_graph = true,
    params {
        rules: [String] = []
    },
);

pub(crate) fn validate_edges(edges: &[String]) -> Result<(), String> {
    for edge in edges {
        let Some((from, targets)) = edge.split_once("!>") else {
            return Err(format!(
                "invalid layer edge `{edge}`: expected `manifest-glob !> package-glob,...`"
            ));
        };

        for pattern in std::iter::once(from).chain(targets.split(',')) {
            let pattern = pattern.trim();

            if pattern.is_empty() || pattern.contains("!>") {
                return Err(format!("invalid empty or nested layer pattern in `{edge}`"));
            }

            crate::glob::validate(pattern)?;
        }
    }

    Ok(())
}

fn check_toml_cargo_layers(ctx: &WorkspaceCtx<'_>) -> Vec<Violation> {
    let mut violations = Vec::new();

    for manifest in ctx.manifests {
        if !ctx.config.applies_to(RULE, &manifest.rel) {
            continue;
        }

        let edges = ctx
            .config
            .get_str_array(RULE, &TOML_CARGO_LAYERS_PARAMS[0], &manifest.rel);

        for edge in edges {
            let Some((from, targets)) = edge.split_once("!>") else {
                continue;
            };

            if !matches_ignore(&manifest.rel, &[from.trim()]) {
                continue;
            }

            let targets = targets.split(',').map(str::trim).collect::<Vec<_>>();

            for dependency in &manifest.dependencies {
                let inherited = (dependency.source == crate::DependencySource::Workspace)
                    .then(|| {
                        ctx.manifests
                            .iter()
                            .flat_map(|manifest| &manifest.workspace_dependencies)
                            .find(|candidate| candidate.name == dependency.name)
                    })
                    .flatten();
                let package = dependency
                    .package
                    .as_deref()
                    .or_else(|| inherited.and_then(|dep| dep.package.as_deref()))
                    .unwrap_or(&dependency.name);
                let target_manifest = ctx
                    .manifests
                    .iter()
                    .find(|target| target.package_name.as_deref() == Some(package));

                if matches_ignore(&dependency.name, &targets)
                    || matches_ignore(package, &targets)
                    || target_manifest.is_some_and(|target| matches_ignore(&target.rel, &targets))
                {
                    violations.push(violation(
                        &manifest.rel,
                        dependency.line,
                        format!(
                            "dependency `{}` (package `{package}`) violates layer edge `{edge}`",
                            dependency.name
                        ),
                    ));
                }
            }
        }
    }

    violations
}