code-buster 0.11.0

Repository-aware static analysis for AI-assisted development
Documentation
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};

use crate::findings::{Finding, Severity};

pub fn analyze(root: &Path, sources: &BTreeMap<String, String>) -> Vec<Finding> {
    let mut findings = re_export_findings(sources);
    let privacy = PackagePrivacy::from_auxiliary(&package_manifests(root, sources));
    if privacy.has_private_packages() {
        findings.extend(dead_export_findings(sources, &privacy));
    }
    findings
}

fn package_manifests(root: &Path, sources: &BTreeMap<String, String>) -> BTreeMap<String, String> {
    let mut candidates = BTreeSet::<PathBuf>::new();
    for source_path in sources.keys() {
        let mut directory = Path::new(source_path).parent();
        while let Some(relative) = directory {
            for manifest in ["Cargo.toml", "package.json"] {
                candidates.insert(relative.join(manifest));
            }
            if relative.as_os_str().is_empty() {
                break;
            }
            directory = relative.parent();
        }
    }
    candidates
        .into_iter()
        .filter_map(|relative| {
            fs::read_to_string(root.join(&relative))
                .ok()
                .map(|source| (relative.to_string_lossy().replace('\\', "/"), source))
        })
        .collect()
}

#[derive(Default)]
struct PackagePrivacy {
    rust: Vec<(String, bool)>,
    javascript: Vec<(String, bool)>,
}

impl PackagePrivacy {
    fn from_auxiliary(auxiliary: &BTreeMap<String, String>) -> Self {
        let mut privacy = Self::default();
        for (path, source) in auxiliary {
            if let Some(root) = manifest_root(path, "Cargo.toml") {
                privacy.rust.push((root, cargo_package_is_private(source)));
            } else if let Some(root) = manifest_root(path, "package.json") {
                privacy
                    .javascript
                    .push((root, node_package_is_private(source)));
            }
        }
        privacy.rust.sort_by_key(|entry| Reverse(entry.0.len()));
        privacy
            .javascript
            .sort_by_key(|entry| Reverse(entry.0.len()));
        privacy
    }

    fn has_private_packages(&self) -> bool {
        self.rust
            .iter()
            .chain(&self.javascript)
            .any(|(_, private)| *private)
    }

    fn source_is_private(&self, path: &str) -> bool {
        let packages = if path.ends_with(".rs") {
            &self.rust
        } else {
            &self.javascript
        };
        packages
            .iter()
            .find(|(root, _)| path_is_below(root, path))
            .is_some_and(|(_, private)| *private)
    }
}

fn manifest_root(path: &str, manifest: &str) -> Option<String> {
    path.strip_suffix(manifest)
        .map(|root| root.trim_end_matches('/').to_owned())
}

fn path_is_below(root: &str, path: &str) -> bool {
    root.is_empty()
        || path
            .strip_prefix(root)
            .is_some_and(|relative| relative.starts_with('/'))
}

fn cargo_package_is_private(source: &str) -> bool {
    let mut in_package = false;
    for line in source.lines() {
        let line = line.split('#').next().unwrap_or_default().trim();
        if line.starts_with('[') {
            in_package = line == "[package]";
        } else if in_package
            && line
                .split_once('=')
                .is_some_and(|(key, value)| key.trim() == "publish" && value.trim() == "false")
        {
            return true;
        }
    }
    false
}

#[derive(serde::Deserialize)]
struct NodePackagePrivacy {
    #[serde(default)]
    private: bool,
}

fn node_package_is_private(source: &str) -> bool {
    serde_json::from_str::<NodePackagePrivacy>(source).is_ok_and(|package| package.private)
}

fn re_export_findings(sources: &BTreeMap<String, String>) -> Vec<Finding> {
    let mut findings = Vec::new();
    for (path, source) in sources {
        for (index, line) in source.split('\n').enumerate() {
            if is_re_export(path, line.trim())
                && let Some(finding) = finding(
                    "re-export",
                    path,
                    index + 1,
                    "module API is forwarded through a re-export facade",
                    "medium",
                )
            {
                findings.push(finding);
            }
        }
    }
    findings
}

fn is_re_export(path: &str, line: &str) -> bool {
    path.ends_with(".rs") && line.starts_with("pub use ")
        || path.ends_with(".dart") && line.starts_with("export ")
        || matches!(
            path.rsplit_once('.').map(|(_, extension)| extension),
            Some("js" | "jsx" | "mjs" | "cjs" | "ts" | "tsx" | "mts" | "cts")
        ) && (line.starts_with("export *")
            || line.starts_with("export {") && line.contains(" from "))
}

fn dead_export_findings(
    sources: &BTreeMap<String, String>,
    privacy: &PackagePrivacy,
) -> Vec<Finding> {
    let declarations = sources
        .iter()
        .filter(|(path, _)| privacy.source_is_private(path))
        .flat_map(|(path, source)| {
            source
                .split('\n')
                .enumerate()
                .filter_map(move |(index, line)| {
                    exported_declaration_name(path, line.trim())
                        .map(|name| (path.as_str(), index + 1, name))
                })
        })
        .collect::<Vec<_>>();

    declarations
        .into_iter()
        .filter(|(_, _, name)| !matches!(*name, "main" | "run"))
        .filter(|(_, _, name)| identifier_occurrences(sources, name) == 1)
        .filter_map(|(path, line, name)| {
            finding(
                "dead-export",
                path,
                line,
                &format!("`{name}` is exported but has no repository-local references"),
                "low",
            )
        })
        .collect()
}

fn exported_declaration_name<'a>(path: &str, line: &'a str) -> Option<&'a str> {
    let declaration = if path.ends_with(".rs") {
        line.strip_prefix("pub ")?
    } else if matches!(
        path.rsplit_once('.').map(|(_, extension)| extension),
        Some("js" | "jsx" | "mjs" | "cjs" | "ts" | "tsx" | "mts" | "cts")
    ) {
        let declaration = line.strip_prefix("export ")?;
        declaration.strip_prefix("default ").unwrap_or(declaration)
    } else {
        return None;
    };
    let declaration = declaration
        .strip_prefix("async ")
        .or_else(|| declaration.strip_prefix("unsafe "))
        .unwrap_or(declaration);
    for prefix in [
        "fn ",
        "struct ",
        "enum ",
        "trait ",
        "const ",
        "static ",
        "type ",
        "function ",
        "class ",
        "interface ",
        "let ",
        "var ",
    ] {
        if let Some(rest) = declaration.strip_prefix(prefix) {
            let name_end = rest
                .bytes()
                .position(|byte| !(byte.is_ascii_alphanumeric() || byte == b'_'))
                .unwrap_or(rest.len());
            return (name_end > 0).then(|| &rest[..name_end]);
        }
    }
    None
}

fn identifier_occurrences(sources: &BTreeMap<String, String>, name: &str) -> usize {
    sources
        .values()
        .map(|source| {
            source
                .match_indices(name)
                .filter(|(index, _)| {
                    let bytes = source.as_bytes();
                    let before = index
                        .checked_sub(1)
                        .and_then(|index| bytes.get(index))
                        .copied();
                    let after = bytes.get(index + name.len()).copied();
                    before.is_none_or(identifier_boundary) && after.is_none_or(identifier_boundary)
                })
                .count()
        })
        .sum()
}

fn identifier_boundary(byte: u8) -> bool {
    !byte.is_ascii_alphanumeric() && byte != b'_'
}

fn finding(
    code: &str,
    path: &str,
    line: usize,
    message: &str,
    confidence: &str,
) -> Option<Finding> {
    let metadata = crate::rule_catalog::metadata(code)?;
    Some(Finding {
        code: code.to_owned(),
        severity: Severity::parse(metadata.default_severity)?,
        path: path.to_owned(),
        line,
        end_line: line,
        message: message.to_owned(),
        confidence: confidence.to_owned(),
        why: metadata.why.to_owned(),
        suggestion: metadata.suggestion.to_owned(),
        related_files: Vec::new(),
        snippet: String::new(),
        code_flow: Vec::new(),
    })
}

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

    use super::{PackagePrivacy, dead_export_findings, re_export_findings};

    #[test]
    fn reports_re_exports_for_supported_module_syntaxes() {
        let sources = BTreeMap::from([
            (
                "lib.rs".to_owned(),
                "pub use crate::api::Client;".to_owned(),
            ),
            (
                "index.ts".to_owned(),
                "export { Button } from './button';".to_owned(),
            ),
            ("models.dart".to_owned(), "export 'user.dart';".to_owned()),
        ]);

        let findings = re_export_findings(&sources);

        assert_eq!(findings.len(), 3);
        assert!(findings.iter().all(|finding| finding.code == "re-export"));
    }

    #[test]
    fn reports_only_exports_without_repository_local_references() {
        let sources = BTreeMap::from([
            (
                "lib.rs".to_owned(),
                "pub fn orphaned() {}\npub fn used() {}\nfn caller() { used(); }".to_owned(),
            ),
            (
                "index.ts".to_owned(),
                "export class Lonely {}\nexport function called() {}\ncalled();".to_owned(),
            ),
        ]);
        let privacy = PackagePrivacy::from_auxiliary(&BTreeMap::from([
            (
                "Cargo.toml".to_owned(),
                "[package]\nname = \"private\"\npublish = false".to_owned(),
            ),
            ("package.json".to_owned(), r#"{"private": true}"#.to_owned()),
        ]));

        let findings = dead_export_findings(&sources, &privacy);

        assert_eq!(
            findings
                .iter()
                .map(|finding| (finding.path.as_str(), finding.line))
                .collect::<Vec<_>>(),
            vec![("index.ts", 1), ("lib.rs", 1)]
        );
    }

    #[test]
    fn scopes_private_exports_to_the_nearest_package_manifest() {
        let sources = BTreeMap::from([
            (
                "src/public.rs".to_owned(),
                "pub fn public_root() {}".to_owned(),
            ),
            (
                "crates/tool/src/lib.rs".to_owned(),
                "pub fn private_rust() {}".to_owned(),
            ),
            (
                "web/public.ts".to_owned(),
                "export function publicNode() {}".to_owned(),
            ),
            (
                "web/packages/tool/index.ts".to_owned(),
                "export function privateNode() {}".to_owned(),
            ),
        ]);
        let privacy = PackagePrivacy::from_auxiliary(&BTreeMap::from([
            (
                "Cargo.toml".to_owned(),
                "[package]\nname = \"public\"".to_owned(),
            ),
            (
                "crates/tool/Cargo.toml".to_owned(),
                "[package]\nname = \"tool\"\npublish = false".to_owned(),
            ),
            ("web/package.json".to_owned(), "{}".to_owned()),
            (
                "web/packages/tool/package.json".to_owned(),
                r#"{"private": true}"#.to_owned(),
            ),
        ]));

        let findings = dead_export_findings(&sources, &privacy);

        assert_eq!(
            findings
                .iter()
                .map(|finding| finding.path.as_str())
                .collect::<Vec<_>>(),
            ["crates/tool/src/lib.rs", "web/packages/tool/index.ts"]
        );
    }
}