gruff-rs 0.4.0

Rust static analyzer and quality linter for CI: dead-code, complexity, security, secrets, and architecture rules with deterministic SARIF/JSON output and baseline support.
use super::*;

pub(crate) fn read_manifest_summary(
    project_root: &Path,
    diagnostics: &mut Vec<RunDiagnostic>,
) -> Option<ManifestSummary> {
    let raw = read_manifest_raw(project_root, diagnostics)?;
    let value = parse_manifest_value(&raw, diagnostics)?;
    Some(ManifestSummary {
        file_path: "Cargo.toml".to_string(),
        package_line: manifest_package_line(&raw),
        package_name: manifest_package_field(&value, "name"),
        package_description: manifest_package_field(&value, "description"),
        package_license: manifest_package_field(&value, "license"),
        dependencies: manifest_dependencies(&value, &raw),
    })
}

pub(crate) fn read_manifest_raw(
    project_root: &Path,
    diagnostics: &mut Vec<RunDiagnostic>,
) -> Option<String> {
    let path = project_root.join("Cargo.toml");
    if !path.exists() {
        return None;
    }
    Some(match fs::read_to_string(&path) {
        Ok(raw) => raw,
        Err(error) => {
            diagnostics.push(RunDiagnostic {
                diagnostic_type: "manifest-read-error".to_string(),
                message: format!("Unable to read Cargo.toml: {error}"),
                file_path: Some("Cargo.toml".to_string()),
                line: Some(1),
            });
            return None;
        }
    })
}

pub(crate) fn parse_manifest_value(
    raw: &str,
    diagnostics: &mut Vec<RunDiagnostic>,
) -> Option<toml::Value> {
    Some(match raw.parse::<toml::Value>() {
        Ok(value) => value,
        Err(_) => {
            diagnostics.push(RunDiagnostic {
                diagnostic_type: "manifest-parse-error".to_string(),
                message:
                    "Invalid Cargo.toml; fix TOML syntax before project rules use manifest data."
                        .to_string(),
                file_path: Some("Cargo.toml".to_string()),
                line: Some(1),
            });
            return None;
        }
    })
}

pub(crate) fn manifest_package_field(value: &toml::Value, field: &str) -> Option<String> {
    value
        .get("package")
        .and_then(toml::Value::as_table)
        .and_then(|package| package.get(field))
        .and_then(toml::Value::as_str)
        .map(str::to_string)
}

pub(crate) fn manifest_dependencies(value: &toml::Value, raw: &str) -> Vec<DependencySummary> {
    let dependency_lines = manifest_dependency_lines(raw);
    let mut dependencies = Vec::new();
    for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
        collect_manifest_dependencies(value, section, &dependency_lines, &mut dependencies);
    }
    collect_target_manifest_dependencies(value, &dependency_lines, &mut dependencies);
    dependencies.sort_by(|left, right| {
        (left.section.as_str(), left.name.as_str())
            .cmp(&(right.section.as_str(), right.name.as_str()))
    });
    dependencies
}

pub(crate) fn collect_target_manifest_dependencies(
    value: &toml::Value,
    dependency_lines: &HashMap<(String, String), usize>,
    dependencies: &mut Vec<DependencySummary>,
) {
    let Some(targets) = value.get("target").and_then(toml::Value::as_table) else {
        return;
    };
    for (target, target_value) in targets {
        let Some(target_table) = target_value.as_table() else {
            continue;
        };
        for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
            let Some(table) = target_table.get(section).and_then(toml::Value::as_table) else {
                continue;
            };
            let section_name = format!("target.{target}.{section}");
            collect_manifest_dependency_table(table, &section_name, dependency_lines, dependencies);
        }
    }
}

pub(crate) fn collect_manifest_dependencies(
    value: &toml::Value,
    section: &str,
    dependency_lines: &HashMap<(String, String), usize>,
    dependencies: &mut Vec<DependencySummary>,
) {
    let Some(table) = value.get(section).and_then(toml::Value::as_table) else {
        return;
    };
    collect_manifest_dependency_table(table, section, dependency_lines, dependencies);
}

pub(crate) fn collect_manifest_dependency_table(
    table: &toml::value::Table,
    section: &str,
    dependency_lines: &HashMap<(String, String), usize>,
    dependencies: &mut Vec<DependencySummary>,
) {
    for (name, dependency) in table {
        dependencies.push(build_dependency_summary(
            name,
            section,
            dependency,
            dependency_lines,
        ));
    }
}

fn build_dependency_summary(
    name: &str,
    section: &str,
    dependency: &toml::Value,
    dependency_lines: &HashMap<(String, String), usize>,
) -> DependencySummary {
    let (requirement, path, git, rev) = dependency_source_fields(dependency);
    DependencySummary {
        name: name.to_string(),
        section: section.to_string(),
        line: dependency_lines
            .get(&(section.to_string(), name.to_string()))
            .copied()
            .unwrap_or(1),
        requirement,
        path,
        git,
        rev,
    }
}

fn dependency_source_fields(
    dependency: &toml::Value,
) -> (
    Option<String>,
    Option<String>,
    Option<String>,
    Option<String>,
) {
    if let Some(requirement) = dependency.as_str() {
        return (Some(requirement.to_string()), None, None, None);
    }
    let Some(table) = dependency.as_table() else {
        return (None, None, None, None);
    };
    (
        table_str_field(table, "version"),
        table_str_field(table, "path"),
        table_str_field(table, "git"),
        table_str_field(table, "rev"),
    )
}

fn table_str_field(table: &toml::value::Table, key: &str) -> Option<String> {
    table
        .get(key)
        .and_then(toml::Value::as_str)
        .map(str::to_string)
}

pub(crate) fn manifest_package_line(raw: &str) -> usize {
    raw.lines()
        .enumerate()
        .find_map(|(index, line)| (line.trim() == "[package]").then_some(index + 1))
        .unwrap_or(1)
}

pub(crate) fn manifest_dependency_lines(raw: &str) -> HashMap<(String, String), usize> {
    let mut lines = HashMap::new();
    let mut current_section: Option<String> = None;

    for (index, line) in raw.lines().enumerate() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') && trimmed.ends_with(']') {
            let section = normalize_manifest_section(trimmed.trim_matches(&['[', ']'][..]));
            current_section = is_dependency_section(&section).then_some(section);
            continue;
        }

        let Some(section) = &current_section else {
            continue;
        };
        let Some((name, _)) = trimmed.split_once('=') else {
            continue;
        };
        let name = name.trim().trim_matches('"').trim_matches('\'');
        if !name.is_empty() && !name.starts_with('#') {
            lines.insert((section.clone(), name.to_string()), index + 1);
        }
    }

    lines
}

fn normalize_manifest_section(section: &str) -> String {
    section.replace(['"', '\''], "")
}

fn is_dependency_section(section: &str) -> bool {
    matches!(
        section,
        "dependencies" | "dev-dependencies" | "build-dependencies"
    ) || section.ends_with(".dependencies")
        || section.ends_with(".dev-dependencies")
        || section.ends_with(".build-dependencies")
}