rulewright 0.2.0

Executable engineering standards for Rust workspaces
Documentation
//! Taplo-backed TOML parsing and rule execution.

pub(crate) mod rules;

use crate::{FileCtx, Rule, RuleCheck, RuleFix, Violation, languages::Analysis};

#[derive(Debug)]
pub struct TomlCtx<'a> {
    pub file: &'a FileCtx<'a>,
    pub parse: &'a taplo::parser::Parse,
    pub dom: &'a taplo::dom::Node,
}

impl TomlCtx<'_> {
    #[must_use]
    pub fn line_of_offset(&self, offset: usize) -> usize {
        self.file
            .contents
            .get(..offset.min(self.file.contents.len()))
            .unwrap_or(self.file.contents)
            .bytes()
            .filter(|byte| *byte == b'\n')
            .count()
            + 1
    }
}

pub(crate) fn analyze(file: &FileCtx<'_>, rules: &[&Rule], fix_mode: bool) -> Analysis {
    let parse = taplo::parser::parse(file.contents);
    let dom = parse.clone().into_dom();
    let ctx = TomlCtx {
        file,
        parse: &parse,
        dom: &dom,
    };
    let mut analysis = Analysis::default();

    if rules
        .iter()
        .any(|rule| matches!(rule.check, RuleCheck::Workspace(_)))
        && let Some(manifest) = workspace_manifest(&ctx)
    {
        analysis.workspace_manifests.push(manifest);
    }

    for rule in rules {
        let RuleCheck::Toml(check) = rule.check else {
            continue;
        };

        if !file.config.applies_to(rule.info.name, file.rel) {
            continue;
        }

        let mut violations: Vec<Violation> = check(&ctx)
            .into_iter()
            .map(|violation| violation.with_rule(rule.info.name))
            .collect();

        if let Some(RuleFix::Toml(fix)) = rule.fix {
            for violation in &mut violations {
                let Some(edit) = fix(&ctx, violation) else {
                    continue;
                };

                violation.mark_fixable();

                if fix_mode {
                    analysis.fixes.push((file.rel.to_owned(), edit));
                }
            }
        }

        analysis.violations.extend(violations);
    }

    analysis
}

pub(crate) fn workspace_manifest(ctx: &TomlCtx<'_>) -> Option<crate::WorkspaceManifest> {
    if ctx.file.path.file_name()?.to_str()? != "Cargo.toml" || !ctx.parse.errors.is_empty() {
        return None;
    }

    let document = toml::from_str::<toml::Table>(ctx.file.contents).ok()?;
    let package_name = document
        .get("package")
        .and_then(|p| p.get("name"))
        .and_then(toml::Value::as_str)
        .map(str::to_owned);
    let mut dependencies = Vec::new();

    extract_dependency_sections(ctx, &document, &ctx.dom.clone(), &mut dependencies);
    let mut workspace_dependencies = Vec::new();

    if let Some(workspace) = document.get("workspace").and_then(toml::Value::as_table) {
        extract_dependency_sections(
            ctx,
            workspace,
            &ctx.dom.get("workspace"),
            &mut workspace_dependencies,
        );
    }

    if let Some(targets) = document.get("target").and_then(toml::Value::as_table) {
        for (name, target) in targets {
            if let Some(target) = target.as_table() {
                extract_dependency_sections(
                    ctx,
                    target,
                    &ctx.dom.get("target").get(name.as_str()),
                    &mut dependencies,
                );
            }
        }
    }

    let mut patch_tables = std::collections::BTreeMap::new();

    if let Some(patches) = document.get("patch").and_then(toml::Value::as_table) {
        for (name, patch) in patches {
            if let Some(table) = patch.as_table() {
                patch_tables.insert(
                    name.clone(),
                    extract_dependencies(
                        ctx,
                        table,
                        &ctx.dom.get("patch").get(name.as_str()),
                        crate::DependencySection::Normal,
                    ),
                );
            }
        }
    }

    Some(crate::WorkspaceManifest {
        rel: ctx.file.rel.to_owned(),
        package_name,
        patch_tables,
        workspace_dependencies,
        dependencies,
    })
}

fn extract_dependency_sections(
    ctx: &TomlCtx<'_>,
    document: &toml::Table,
    dom: &taplo::dom::Node,
    out: &mut Vec<crate::DependencyRecord>,
) {
    for (name, section) in [
        ("dependencies", crate::DependencySection::Normal),
        ("dev-dependencies", crate::DependencySection::Dev),
        ("build-dependencies", crate::DependencySection::Build),
    ] {
        if let Some(table) = document.get(name).and_then(toml::Value::as_table) {
            out.extend(extract_dependencies(ctx, table, &dom.get(name), section));
        }
    }
}

fn extract_dependencies(
    ctx: &TomlCtx<'_>,
    table: &toml::Table,
    dom: &taplo::dom::Node,
    section: crate::DependencySection,
) -> Vec<crate::DependencyRecord> {
    table
        .iter()
        .map(|(name, value)| {
            let source = if value.get("workspace").and_then(toml::Value::as_bool) == Some(true) {
                crate::DependencySource::Workspace
            } else if let Some(path) = value.get("path").and_then(toml::Value::as_str) {
                crate::DependencySource::Path(path.to_owned())
            } else if value.get("git").is_some() {
                crate::DependencySource::Git
            } else {
                crate::DependencySource::Registry
            };
            let line = dom
                .get(name.as_str())
                .text_ranges(false)
                .next()
                .map_or(1, |range| {
                    ctx.line_of_offset(u32::from(range.start()) as usize)
                });

            crate::DependencyRecord {
                name: name.clone(),
                root: name.replace('-', "_"),
                line,
                section,
                source,
                package: value
                    .get("package")
                    .and_then(toml::Value::as_str)
                    .map(str::to_owned),
            }
        })
        .collect()
}