repopilot 0.10.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::audits::context::LanguageKind;
use crate::knowledge::bundled_knowledge;
use crate::knowledge::model::LanguageProfile;
use crate::scan::facts::FileFacts;
use std::path::Path;

pub fn detect_language_for_path(path: &Path) -> Option<&'static str> {
    let file_name = path.file_name().and_then(|name| name.to_str())?;
    let extension = path.extension().and_then(|extension| extension.to_str());

    bundled_knowledge().languages.iter().find_map(|language| {
        let file_name_matches = language
            .filenames
            .iter()
            .any(|candidate| candidate.eq_ignore_ascii_case(file_name));
        let extension_matches = extension.is_some_and(|extension| {
            language
                .extensions
                .iter()
                .any(|candidate| candidate.eq_ignore_ascii_case(extension))
        });

        (file_name_matches || extension_matches).then_some(language.name.as_str())
    })
}

pub fn profile_by_id(id: &str) -> Option<&'static LanguageProfile> {
    bundled_knowledge()
        .languages
        .iter()
        .find(|language| language.id == id)
}

pub fn language_id_for_name(name: &str) -> Option<&'static str> {
    let normalized = normalize(name);
    bundled_knowledge()
        .languages
        .iter()
        .find(|language| {
            normalize(&language.name) == normalized
                || language
                    .aliases
                    .iter()
                    .any(|alias| normalize(alias) == normalized)
                || normalize(&language.id) == normalized
        })
        .map(|language| language.id.as_str())
}

pub fn language_kind_for_file(file: &FileFacts) -> LanguageKind {
    if let Some(language) = &file.language
        && let Some(kind) = language_kind_from_name(language)
    {
        return kind;
    }

    detect_language_for_path(&file.path)
        .and_then(language_kind_from_name)
        .unwrap_or(LanguageKind::Unknown)
}

pub fn language_kind_from_name(name: &str) -> Option<LanguageKind> {
    let profile = language_id_for_name(name).and_then(profile_by_id)?;

    Some(language_kind_from_id(&profile.id))
}

pub fn language_kind_from_id(id: &str) -> LanguageKind {
    match id {
        "rust" => LanguageKind::Rust,
        "typescript" | "typescript-react" => LanguageKind::TypeScript,
        "javascript" | "javascript-react" => LanguageKind::JavaScript,
        "csharp" => LanguageKind::CSharp,
        "python" => LanguageKind::Python,
        "go" => LanguageKind::Go,
        "java" => LanguageKind::Java,
        "kotlin" => LanguageKind::Kotlin,
        "swift" => LanguageKind::Swift,
        "c" => LanguageKind::C,
        "cpp" => LanguageKind::Cpp,
        "c-header" => LanguageKind::CHeader,
        "php" => LanguageKind::Php,
        "ruby" => LanguageKind::Ruby,
        "dart" => LanguageKind::Dart,
        "scala" => LanguageKind::Scala,
        "shell" => LanguageKind::Shell,
        "powershell" => LanguageKind::PowerShell,
        "sql" => LanguageKind::Sql,
        "html" => LanguageKind::Html,
        "css" => LanguageKind::Css,
        "scss" => LanguageKind::Scss,
        "elixir" => LanguageKind::Elixir,
        "erlang" => LanguageKind::Erlang,
        "haskell" => LanguageKind::Haskell,
        "ocaml" => LanguageKind::OCaml,
        "fsharp" => LanguageKind::FSharp,
        "r" => LanguageKind::R,
        "julia" => LanguageKind::Julia,
        "lua" => LanguageKind::Lua,
        "perl" => LanguageKind::Perl,
        "zig" => LanguageKind::Zig,
        "solidity" => LanguageKind::Solidity,
        "objective-c" => LanguageKind::ObjectiveC,
        "terraform" => LanguageKind::Terraform,
        "dockerfile" => LanguageKind::Dockerfile,
        "nix" => LanguageKind::Nix,
        "json" => LanguageKind::Json,
        "toml" => LanguageKind::Toml,
        "yaml" => LanguageKind::Yaml,
        "markdown" => LanguageKind::Markdown,
        _ => LanguageKind::Unknown,
    }
}

fn normalize(value: &str) -> String {
    value.trim().to_lowercase()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::knowledge::model::SupportLevel;

    #[test]
    fn detects_language_from_bundled_extensions() {
        assert_eq!(
            detect_language_for_path(Path::new("src/main.rs")),
            Some("Rust")
        );
        assert_eq!(
            detect_language_for_path(Path::new("Dockerfile")),
            Some("Dockerfile")
        );
        assert_eq!(
            detect_language_for_path(Path::new("infra/main.tf")),
            Some("Terraform")
        );
    }

    #[test]
    fn exposes_support_levels() {
        assert_eq!(
            profile_by_id("rust").map(|profile| profile.support),
            Some(SupportLevel::RuleAware)
        );
        assert_eq!(
            profile_by_id("zig").map(|profile| profile.support),
            Some(SupportLevel::DetectOnly)
        );
    }
}