rdar 0.6.7

radar - the repository cartographer for AI agents: compiles a repo into tiny committed MAP.md routers, with measured token benchmarks
Documentation
//! Language detection and the tier-1 grammar registry.

use std::path::Path;

use serde::{Deserialize, Serialize};
use tree_sitter::Language;

/// Tier-1 languages with a statically linked tree-sitter grammar.
///
/// `Tsx` shares TypeScript's queries but parses with the TSX grammar.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Lang {
    Rust,
    Python,
    JavaScript,
    TypeScript,
    Tsx,
    Go,
    Java,
    C,
    Cpp,
    CSharp,
    Php,
    Ruby,
    Bash,
    Apex,
    Kotlin,
    Lua,
    Html,
}

impl Lang {
    pub const ALL: [Lang; 17] = [
        Lang::Rust,
        Lang::Python,
        Lang::JavaScript,
        Lang::TypeScript,
        Lang::Tsx,
        Lang::Go,
        Lang::Java,
        Lang::C,
        Lang::Cpp,
        Lang::CSharp,
        Lang::Php,
        Lang::Ruby,
        Lang::Bash,
        Lang::Apex,
        Lang::Kotlin,
        Lang::Lua,
        Lang::Html,
    ];

    pub fn name(self) -> &'static str {
        match self {
            Lang::Rust => "rust",
            Lang::Python => "python",
            Lang::JavaScript => "javascript",
            Lang::TypeScript => "typescript",
            Lang::Tsx => "tsx",
            Lang::Go => "go",
            Lang::Java => "java",
            Lang::C => "c",
            Lang::Cpp => "cpp",
            Lang::CSharp => "csharp",
            Lang::Php => "php",
            Lang::Ruby => "ruby",
            Lang::Bash => "bash",
            Lang::Apex => "apex",
            Lang::Kotlin => "kotlin",
            Lang::Lua => "lua",
            Lang::Html => "html",
        }
    }

    /// Detect a language from a file path's extension.
    ///
    /// `salesforce` disambiguates `.cls`, which Salesforce Apex shares with
    /// other ecosystems (LaTeX, VB). In a Salesforce project it is Apex;
    /// elsewhere it stays unsupported rather than being parsed with the wrong
    /// grammar. Pass [`is_salesforce_project`] for the scanned tree. The
    /// `.trigger`/`.apex` extensions are unambiguously Salesforce and stay Apex
    /// regardless.
    pub fn from_path(path: &Path, salesforce: bool) -> Option<Lang> {
        let ext = path.extension()?.to_str()?;
        Some(match ext {
            "rs" => Lang::Rust,
            "py" | "pyi" => Lang::Python,
            "js" | "mjs" | "cjs" | "jsx" => Lang::JavaScript,
            "ts" | "mts" | "cts" => Lang::TypeScript,
            "tsx" => Lang::Tsx,
            "go" => Lang::Go,
            "java" => Lang::Java,
            "c" | "h" => Lang::C,
            "cc" | "cpp" | "cxx" | "hpp" | "hh" | "hxx" => Lang::Cpp,
            "cs" => Lang::CSharp,
            "php" => Lang::Php,
            "rb" | "rake" => Lang::Ruby,
            "sh" | "bash" => Lang::Bash,
            "cls" if salesforce => Lang::Apex,
            "cls" => return None,
            "trigger" | "apex" => Lang::Apex,
            "kt" | "kts" => Lang::Kotlin,
            "lua" => Lang::Lua,
            "html" | "htm" => Lang::Html,
            _ => return None,
        })
    }

    /// Detect a language from a `#!` shebang line (extensionless scripts).
    pub fn from_shebang(first_bytes: &[u8]) -> Option<Lang> {
        if !first_bytes.starts_with(b"#!") {
            return None;
        }
        let line = first_bytes.split(|&b| b == b'\n').next()?;
        let line = std::str::from_utf8(line).ok()?;
        if line.contains("python") {
            Some(Lang::Python)
        } else if line.contains("bash") || line.ends_with("/sh") || line.contains("env sh") {
            Some(Lang::Bash)
        } else if line.contains("ruby") {
            Some(Lang::Ruby)
        } else if line.contains("node") {
            Some(Lang::JavaScript)
        } else if line.contains("kotlin") {
            Some(Lang::Kotlin)
        } else if line.contains("lua") {
            Some(Lang::Lua)
        } else {
            None
        }
    }

    /// The tree-sitter grammar for this language.
    pub fn language(self) -> Language {
        match self {
            Lang::Rust => tree_sitter_rust::LANGUAGE.into(),
            Lang::Python => tree_sitter_python::LANGUAGE.into(),
            Lang::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
            Lang::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
            Lang::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
            Lang::Go => tree_sitter_go::LANGUAGE.into(),
            Lang::Java => tree_sitter_java::LANGUAGE.into(),
            Lang::C => tree_sitter_c::LANGUAGE.into(),
            Lang::Cpp => tree_sitter_cpp::LANGUAGE.into(),
            Lang::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
            Lang::Php => tree_sitter_php::LANGUAGE_PHP.into(),
            Lang::Ruby => tree_sitter_ruby::LANGUAGE.into(),
            Lang::Bash => tree_sitter_bash::LANGUAGE.into(),
            Lang::Apex => tree_sitter_sfapex::apex::LANGUAGE.into(),
            Lang::Kotlin => tree_sitter_kotlin_ng::LANGUAGE.into(),
            Lang::Lua => tree_sitter_lua::LANGUAGE.into(),
            Lang::Html => tree_sitter_html::LANGUAGE.into(),
        }
    }

    /// The def/ref extraction query source for this language.
    pub fn query_source(self) -> &'static str {
        match self {
            Lang::Rust => include_str!("queries/rust.scm"),
            Lang::Python => include_str!("queries/python.scm"),
            Lang::JavaScript => include_str!("queries/javascript.scm"),
            Lang::TypeScript => include_str!("queries/typescript.scm"),
            Lang::Tsx => include_str!("queries/tsx.scm"),
            Lang::Go => include_str!("queries/go.scm"),
            Lang::Java => include_str!("queries/java.scm"),
            Lang::C => include_str!("queries/c.scm"),
            Lang::Cpp => include_str!("queries/cpp.scm"),
            Lang::CSharp => include_str!("queries/c_sharp.scm"),
            Lang::Php => include_str!("queries/php.scm"),
            Lang::Ruby => include_str!("queries/ruby.scm"),
            Lang::Bash => include_str!("queries/bash.scm"),
            Lang::Apex => include_str!("queries/apex.scm"),
            Lang::Kotlin => include_str!("queries/kotlin.scm"),
            Lang::Lua => include_str!("queries/lua.scm"),
            Lang::Html => include_str!("queries/html.scm"),
        }
    }
}

/// True when `root` is the root of a Salesforce DX project, i.e. it contains an
/// `sfdx-project.json`. Used to resolve the ambiguous `.cls` extension in
/// [`Lang::from_path`]: Apex here, unsupported elsewhere. Checked once per scan,
/// not per file.
pub fn is_salesforce_project(root: &Path) -> bool {
    root.join("sfdx-project.json").is_file()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn detects_common_extensions() {
        assert_eq!(
            Lang::from_path(Path::new("a/b/mod.rs"), false),
            Some(Lang::Rust)
        );
        assert_eq!(Lang::from_path(Path::new("x.tsx"), false), Some(Lang::Tsx));
        assert_eq!(Lang::from_path(Path::new("x.hpp"), false), Some(Lang::Cpp));
        assert_eq!(Lang::from_path(Path::new("x.lua"), false), Some(Lang::Lua));
        assert_eq!(
            Lang::from_path(Path::new("index.html"), false),
            Some(Lang::Html)
        );
        assert_eq!(
            Lang::from_path(Path::new("src/Main.kt"), false),
            Some(Lang::Kotlin)
        );
        assert_eq!(
            Lang::from_path(Path::new("build.gradle.kts"), false),
            Some(Lang::Kotlin)
        );
        assert_eq!(Lang::from_path(Path::new("noext"), false), None);
    }

    #[test]
    fn cls_maps_to_apex_only_in_salesforce_projects() {
        // `.cls` is Apex in a Salesforce project and unsupported otherwise.
        assert_eq!(
            Lang::from_path(Path::new("classes/AccountService.cls"), true),
            Some(Lang::Apex)
        );
        assert_eq!(Lang::from_path(Path::new("src/Foo.cls"), false), None);
        // `.trigger`/`.apex` are unambiguously Salesforce - Apex either way.
        assert_eq!(
            Lang::from_path(Path::new("triggers/AccountTrigger.trigger"), false),
            Some(Lang::Apex)
        );
        assert_eq!(
            Lang::from_path(Path::new("anon.apex"), false),
            Some(Lang::Apex)
        );
    }

    #[test]
    fn salesforce_marker_must_be_a_file() {
        let root =
            std::env::temp_dir().join(format!("radar-salesforce-marker-{}", std::process::id()));
        let marker = root.join("sfdx-project.json");
        let _ = std::fs::remove_dir_all(&root);
        std::fs::create_dir_all(&marker).expect("marker directory");
        assert!(!is_salesforce_project(&root));
        std::fs::remove_dir_all(&marker).expect("remove marker directory");
        std::fs::write(&marker, "{}\n").expect("marker file");
        assert!(is_salesforce_project(&root));
        let _ = std::fs::remove_dir_all(&root);
    }

    #[test]
    fn detects_shebangs() {
        assert_eq!(
            Lang::from_shebang(b"#!/usr/bin/env python3\n"),
            Some(Lang::Python)
        );
        assert_eq!(Lang::from_shebang(b"#!/bin/bash\n"), Some(Lang::Bash));
        assert_eq!(
            Lang::from_shebang(b"#!/usr/bin/env kotlin\n"),
            Some(Lang::Kotlin)
        );
        assert_eq!(Lang::from_shebang(b"#!/usr/bin/env lua\n"), Some(Lang::Lua));
        assert_eq!(Lang::from_shebang(b"no shebang"), None);
    }
}