code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
use std::collections::BTreeSet;
use std::sync::LazyLock;

use regex::Regex;
use toml::Value;

use crate::findings::Finding;
use crate::source_analysis_context::SourceAnalysisContext;

use super::{CargoManifest, finding, manifest_directory};

pub(super) fn analyze(
    context: &SourceAnalysisContext<'_>,
    manifests: &[CargoManifest<'_>],
) -> Vec<Finding> {
    let libraries = manifests
        .iter()
        .filter(|manifest| is_library_manifest(context, manifest))
        .collect::<Vec<_>>();
    if libraries.is_empty() {
        return Vec::new();
    }
    let mut findings = Vec::new();
    for (path, _) in context
        .sources()
        .iter()
        .filter(|(path, _)| path.ends_with(".rs"))
    {
        let Some(owner) = owning_manifest(path, manifests) else {
            continue;
        };
        if !libraries.iter().any(|library| library.path == owner.path) {
            continue;
        }
        let masked = context.masked_lines(path).join("\n");
        append_public_anyhow_findings(path, &masked, &mut findings);
    }
    findings
}

fn is_library_manifest(context: &SourceAnalysisContext<'_>, manifest: &CargoManifest<'_>) -> bool {
    let Some(package) = manifest.value.get("package").and_then(Value::as_table) else {
        return false;
    };
    if !has_publication_intent(package) || package_has_binary_target(context, manifest) {
        return false;
    }
    if manifest
        .value
        .get("lib")
        .and_then(Value::as_table)
        .is_some()
    {
        return true;
    }
    context
        .sources()
        .contains_key(&default_source_path(manifest.path, "lib.rs"))
}

fn has_publication_intent(package: &toml::Table) -> bool {
    if package.get("publish").and_then(Value::as_bool) == Some(false)
        || package
            .get("publish")
            .and_then(Value::as_array)
            .is_some_and(Vec::is_empty)
        || package.get("version").and_then(Value::as_str) == Some("0.0.0")
    {
        return false;
    }
    let Some(description) = package.get("description").and_then(Value::as_str) else {
        return false;
    };
    if INTERNAL_CRATE_DESCRIPTION.is_match(description) {
        return false;
    }
    [
        "categories",
        "documentation",
        "homepage",
        "keywords",
        "license",
        "readme",
        "repository",
    ]
    .iter()
    .any(|field| package.contains_key(*field))
}

fn package_has_binary_target(
    context: &SourceAnalysisContext<'_>,
    manifest: &CargoManifest<'_>,
) -> bool {
    manifest
        .value
        .get("bin")
        .and_then(Value::as_array)
        .is_some_and(|targets| !targets.is_empty())
        || context
            .sources()
            .contains_key(&default_source_path(manifest.path, "main.rs"))
}

fn default_source_path(manifest_path: &str, name: &str) -> String {
    let directory = manifest_directory(manifest_path);
    if directory.is_empty() {
        format!("src/{name}")
    } else {
        format!("{directory}/src/{name}")
    }
}

fn owning_manifest<'a>(
    source_path: &str,
    manifests: &'a [CargoManifest<'a>],
) -> Option<&'a CargoManifest<'a>> {
    manifests
        .iter()
        .filter(|manifest| manifest.value.get("package").is_some())
        .filter(|manifest| source_belongs_to_manifest(source_path, manifest.path))
        .max_by_key(|manifest| manifest_directory(manifest.path).len())
}

fn source_belongs_to_manifest(source_path: &str, manifest_path: &str) -> bool {
    let directory = manifest_directory(manifest_path);
    directory.is_empty()
        || source_path
            .strip_prefix(directory)
            .is_some_and(|relative| relative.starts_with('/'))
}

fn append_public_anyhow_findings(path: &str, source: &str, findings: &mut Vec<Finding>) {
    let aliases = ANYHOW_ALIAS
        .captures_iter(source)
        .filter_map(|capture| capture.get(1).map(|name| name.as_str().to_owned()))
        .collect::<BTreeSet<_>>();
    for function in PUBLIC_FUNCTION.find_iter(source) {
        let Some(open) = source[function.end()..]
            .find('(')
            .map(|offset| function.end() + offset)
        else {
            continue;
        };
        let Some(close) = matching_parenthesis(source, open) else {
            continue;
        };
        let header_end = source[close + 1..]
            .find(['{', ';'])
            .map_or(source.len(), |offset| close + 1 + offset);
        let return_type = &source[close + 1..header_end];
        if !ANYHOW_RETURN.is_match(return_type)
            && !RETURN_TYPE.captures(return_type).is_some_and(|capture| {
                capture
                    .get(1)
                    .is_some_and(|name| aliases.contains(name.as_str()))
            })
        {
            continue;
        }
        let line = source[..function.start()]
            .bytes()
            .filter(|byte| *byte == b'\n')
            .count()
            + 1;
        findings.push(finding(
            "rust-public-anyhow-result",
            path,
            line,
            "public library function exposes an opaque anyhow error".to_owned(),
        ));
    }
}

fn matching_parenthesis(source: &str, open: usize) -> Option<usize> {
    let mut depth = 0usize;
    for (offset, byte) in source.as_bytes()[open..].iter().enumerate() {
        match byte {
            b'(' => depth += 1,
            b')' => {
                depth = depth.saturating_sub(1);
                if depth == 0 {
                    return Some(open + offset);
                }
            }
            _ => {}
        }
    }
    None
}

static INTERNAL_CRATE_DESCRIPTION: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?i)\binternal (?:component )?crate\b").unwrap());
static ANYHOW_ALIAS: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r"(?m)^[ \t]*(?:pub(?:\([^)]*\))?[ \t]+)?type[ \t]+([A-Za-z_]\w*)(?:[ \t]*<[^;=]+>)?[ \t]*=[ \t]*(?:anyhow::Result\b|Result[ \t]*<[^;]*anyhow::Error)",
    )
    .unwrap()
});
static PUBLIC_FUNCTION: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r"(?m)^[ \t]*pub[ \t]+(?:(?:async|const|unsafe)[ \t]+|extern(?:[ \t]+[^\s]+)?[ \t]+)*fn[ \t]+[A-Za-z_]\w*",
    )
    .unwrap()
});
static ANYHOW_RETURN: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"(?s)->\s*(?:anyhow::Result\b|anyhow::Error\b|Result\s*<[^>{};]*anyhow::Error)")
        .unwrap()
});
static RETURN_TYPE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?s)->\s*([A-Za-z_]\w*)\b").unwrap());

#[cfg(test)]
mod tests {
    use std::collections::{BTreeMap, BTreeSet};

    use super::*;

    #[test]
    fn reports_direct_and_locally_aliased_anyhow_in_public_library_apis() {
        let findings = analyze_project(
            [(
                "Cargo.toml",
                "[package]\nname = \"lib\"\nversion = \"1.0.0\"\ndescription = \"Published library\"\nlicense = \"MIT\"\nrust-version = \"1.85\"",
            )],
            [(
                "src/lib.rs",
                "pub type AppResult<T> = anyhow::Result<T>;\n\npub async fn direct(\n    value: usize,\n) -> anyhow::Result<usize> { todo!() }\n\npub fn aliased() -> AppResult<()> { todo!() }\n\npub(crate) fn crate_only() -> anyhow::Result<()> { todo!() }\nfn private() -> anyhow::Result<()> { todo!() }\npub fn typed() -> Result<(), DomainError> { todo!() }",
            )],
        );

        assert_eq!(
            findings
                .iter()
                .map(|finding| (finding.code.as_str(), finding.line))
                .collect::<Vec<_>>(),
            vec![
                ("rust-public-anyhow-result", 3),
                ("rust-public-anyhow-result", 7),
            ]
        );
    }

    #[test]
    fn ignores_public_anyhow_functions_owned_only_by_a_binary() {
        let findings = analyze_project(
            [(
                "Cargo.toml",
                "[package]\nname = \"app\"\nversion = \"1.0.0\"\ndescription = \"Application\"\nlicense = \"MIT\"\nrust-version = \"1.85\"",
            )],
            [(
                "src/main.rs",
                "pub fn run() -> anyhow::Result<()> { Ok(()) }",
            )],
        );

        assert!(findings.is_empty());
    }

    #[test]
    fn assigns_nested_sources_to_the_nearest_manifest() {
        let findings = analyze_project(
            [
                (
                    "Cargo.toml",
                    "[package]\nname = \"root\"\nversion = \"1.0.0\"\ndescription = \"Published library\"\nlicense = \"MIT\"\nrust-version = \"1.85\"",
                ),
                (
                    "tool/Cargo.toml",
                    "[package]\nname = \"tool\"\nversion = \"1.0.0\"\ndescription = \"Application\"\nlicense = \"MIT\"\nrust-version = \"1.85\"",
                ),
            ],
            [
                (
                    "src/lib.rs",
                    "pub fn root() -> anyhow::Result<()> { Ok(()) }",
                ),
                (
                    "tool/src/main.rs",
                    "pub fn tool() -> anyhow::Result<()> { Ok(()) }",
                ),
            ],
        );

        assert_eq!(findings.len(), 1);
        assert_eq!(findings[0].path, "src/lib.rs");
    }

    #[test]
    fn ignores_internal_library_packages() {
        let findings = analyze_project(
            [
                (
                    "private/Cargo.toml",
                    "[package]\nname = \"private\"\nversion = \"1.0.0\"\npublish = false\ndescription = \"Private library\"\nlicense = \"MIT\"",
                ),
                (
                    "zero/Cargo.toml",
                    "[package]\nname = \"zero\"\nversion = \"0.0.0\"\ndescription = \"Workspace library\"\nlicense = \"MIT\"",
                ),
                (
                    "internal/Cargo.toml",
                    "[package]\nname = \"internal\"\nversion = \"1.2.3\"\ndescription = \"An internal component crate of the app\"\nlicense = \"MIT\"",
                ),
            ],
            [
                (
                    "private/src/lib.rs",
                    "pub fn run() -> anyhow::Result<()> { Ok(()) }",
                ),
                (
                    "zero/src/lib.rs",
                    "pub fn run() -> anyhow::Result<()> { Ok(()) }",
                ),
                (
                    "internal/src/lib.rs",
                    "pub fn run() -> anyhow::Result<()> { Ok(()) }",
                ),
            ],
        );

        assert!(findings.is_empty());
    }

    fn analyze_project<const A: usize, const S: usize>(
        files: [(&str, &str); A],
        source_files: [(&str, &str); S],
    ) -> Vec<Finding> {
        let auxiliary = files
            .into_iter()
            .map(|(path, source)| (path.to_owned(), source.to_owned()))
            .collect::<BTreeMap<_, _>>();
        let sources = source_files
            .into_iter()
            .map(|(path, source)| (path.to_owned(), source.to_owned()))
            .collect::<BTreeMap<_, _>>();
        let base = BTreeMap::new();
        let changed = BTreeSet::new();
        let context =
            SourceAnalysisContext::with_auxiliary(&sources, &base, &changed, false, &auxiliary);
        let manifests = super::super::parse_manifests(&context);
        analyze(&context, &manifests)
    }
}