semtree-core 0.5.0

Core types for semtree: Language, Span, Node, Chunk
Documentation
use serde::{Deserialize, Serialize};
use std::fmt;

// `Ord` follows declaration order, which is what stable, grouped output (stats,
// help text) sorts by when counts tie.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Language {
    Rust,
    Python,
    JavaScript,
    TypeScript,
    Tsx,
    Go,
    Java,
    C,
    Cpp,
    CSharp,
    Ruby,
    Php,
    Kotlin,
    Scala,
    Swift,
    OCaml,
    Solidity,
    Lua,
    Zig,
    Elisp,
    Unknown,
}

impl Language {
    pub fn from_extension(ext: &str) -> Self {
        match ext {
            "rs" => Self::Rust,
            "py" => Self::Python,
            "js" | "mjs" | "cjs" | "jsx" => Self::JavaScript,
            "ts" => Self::TypeScript,
            "tsx" => Self::Tsx,
            "go" => Self::Go,
            "java" => Self::Java,
            "c" | "h" => Self::C,
            "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => Self::Cpp,
            "cs" => Self::CSharp,
            "rb" => Self::Ruby,
            "php" => Self::Php,
            "kt" | "kts" => Self::Kotlin,
            "scala" | "sc" => Self::Scala,
            "swift" => Self::Swift,
            "ml" | "mli" => Self::OCaml,
            "sol" => Self::Solidity,
            "lua" => Self::Lua,
            "zig" => Self::Zig,
            "el" => Self::Elisp,
            _ => Self::Unknown,
        }
    }

    pub fn from_path(path: &std::path::Path) -> Self {
        path.extension()
            .and_then(|e| e.to_str())
            .map(Self::from_extension)
            .unwrap_or(Self::Unknown)
    }

    /// Parse a user-facing language name, as accepted by a `--lang` filter.
    ///
    /// Case-insensitive, and takes the common short forms (`rs`, `py`, `ts`)
    /// alongside the canonical name [`Display`](std::fmt::Display) prints.
    /// Returns `None` for anything unrecognized so a caller can report a typo
    /// rather than silently filtering everything out.
    pub fn from_name(name: &str) -> Option<Self> {
        Some(match name.to_lowercase().as_str() {
            "rust" | "rs" => Self::Rust,
            "python" | "py" => Self::Python,
            "javascript" | "js" => Self::JavaScript,
            "typescript" | "ts" => Self::TypeScript,
            "tsx" => Self::Tsx,
            "go" | "golang" => Self::Go,
            "java" => Self::Java,
            "c" => Self::C,
            "cpp" | "c++" => Self::Cpp,
            "csharp" | "c#" | "cs" => Self::CSharp,
            "ruby" | "rb" => Self::Ruby,
            "php" => Self::Php,
            "kotlin" | "kt" => Self::Kotlin,
            "scala" => Self::Scala,
            "swift" => Self::Swift,
            "ocaml" | "ml" => Self::OCaml,
            "solidity" | "sol" => Self::Solidity,
            "lua" => Self::Lua,
            "zig" => Self::Zig,
            "elisp" | "emacs-lisp" | "el" => Self::Elisp,
            _ => return None,
        })
    }

    /// Every language semtree can parse, in a stable order. `Unknown` is not a
    /// language, so it is absent; use it to build help text and error messages.
    pub const ALL: &'static [Self] = &[
        Self::Rust,
        Self::Python,
        Self::JavaScript,
        Self::TypeScript,
        Self::Tsx,
        Self::Go,
        Self::Java,
        Self::C,
        Self::Cpp,
        Self::CSharp,
        Self::Ruby,
        Self::Php,
        Self::Kotlin,
        Self::Scala,
        Self::Swift,
        Self::OCaml,
        Self::Solidity,
        Self::Lua,
        Self::Zig,
        Self::Elisp,
    ];
}

impl fmt::Display for Language {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::Rust => "rust",
            Self::Python => "python",
            Self::JavaScript => "javascript",
            Self::TypeScript => "typescript",
            Self::Tsx => "tsx",
            Self::Go => "go",
            Self::Java => "java",
            Self::C => "c",
            Self::Cpp => "cpp",
            Self::CSharp => "csharp",
            Self::Ruby => "ruby",
            Self::Php => "php",
            Self::Kotlin => "kotlin",
            Self::Scala => "scala",
            Self::Swift => "swift",
            Self::OCaml => "ocaml",
            Self::Solidity => "solidity",
            Self::Lua => "lua",
            Self::Zig => "zig",
            Self::Elisp => "elisp",
            Self::Unknown => "unknown",
        };
        write!(f, "{s}")
    }
}

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

    #[test]
    fn every_language_parses_back_from_its_own_name() {
        // Guards the filter path: adding a language to the parser without
        // teaching `from_name` about it would make `--lang <that language>`
        // silently unusable.
        for lang in Language::ALL {
            assert_eq!(
                Language::from_name(&lang.to_string()),
                Some(*lang),
                "{lang} does not round-trip through from_name"
            );
        }
    }

    #[test]
    fn names_are_case_insensitive_and_accept_short_forms() {
        assert_eq!(Language::from_name("Rust"), Some(Language::Rust));
        assert_eq!(Language::from_name("PY"), Some(Language::Python));
        assert_eq!(Language::from_name("c++"), Some(Language::Cpp));
        assert_eq!(Language::from_name("emacs-lisp"), Some(Language::Elisp));
    }

    #[test]
    fn unknown_is_not_a_filterable_language() {
        assert_eq!(Language::from_name("nope"), None);
        assert_eq!(Language::from_name("unknown"), None);
        assert!(!Language::ALL.contains(&Language::Unknown));
    }
}