Skip to main content

parallax_project/
lang.rs

1//! Source / target language identity for Transmute.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::Path;
6
7/// Source language frontend.
8#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum SourceLanguage {
11    /// Python.
12    Python,
13    /// TypeScript.
14    TypeScript,
15    /// JavaScript.
16    JavaScript,
17    /// Rust.
18    Rust,
19    /// Go.
20    Go,
21    /// Other / unknown.
22    Other(String),
23}
24
25impl SourceLanguage {
26    /// CLI name.
27    pub fn as_str(&self) -> &str {
28        match self {
29            Self::Python => "python",
30            Self::TypeScript => "typescript",
31            Self::JavaScript => "javascript",
32            Self::Rust => "rust",
33            Self::Go => "go",
34            Self::Other(s) => s.as_str(),
35        }
36    }
37
38    /// Parse CLI / config name.
39    pub fn parse(s: &str) -> Option<Self> {
40        match s.to_ascii_lowercase().as_str() {
41            "python" | "py" => Some(Self::Python),
42            "typescript" | "ts" => Some(Self::TypeScript),
43            "javascript" | "js" | "node" => Some(Self::JavaScript),
44            "rust" | "rs" => Some(Self::Rust),
45            "go" | "golang" => Some(Self::Go),
46            // Long-tail languages use Other with a canonical connector id.
47            other if !other.is_empty() => Some(Self::Other(other.to_string())),
48            _ => None,
49        }
50    }
51}
52
53impl std::fmt::Display for SourceLanguage {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(f, "{}", self.as_str())
56    }
57}
58
59/// Target language backend.
60#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum TargetLanguage {
63    /// Rust.
64    Rust,
65    /// Go.
66    Go,
67    /// Python.
68    Python,
69    /// TypeScript.
70    TypeScript,
71    /// JavaScript.
72    JavaScript,
73    /// Other.
74    Other(String),
75}
76
77impl TargetLanguage {
78    /// CLI name.
79    pub fn as_str(&self) -> &str {
80        match self {
81            Self::Rust => "rust",
82            Self::Go => "go",
83            Self::Python => "python",
84            Self::TypeScript => "typescript",
85            Self::JavaScript => "javascript",
86            Self::Other(s) => s.as_str(),
87        }
88    }
89
90    /// Parse.
91    pub fn parse(s: &str) -> Option<Self> {
92        match s.to_ascii_lowercase().as_str() {
93            "rust" | "rs" => Some(Self::Rust),
94            "go" | "golang" => Some(Self::Go),
95            "python" | "py" => Some(Self::Python),
96            "typescript" | "ts" => Some(Self::TypeScript),
97            "javascript" | "js" => Some(Self::JavaScript),
98            other if !other.is_empty() => Some(Self::Other(other.to_string())),
99            _ => None,
100        }
101    }
102}
103
104impl std::fmt::Display for TargetLanguage {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        write!(f, "{}", self.as_str())
107    }
108}
109
110/// Detect language mix from a list of relative file paths.
111/// Returns percentages totaling ~100 based on counted source files.
112pub fn detect_languages(
113    paths: &[impl AsRef<Path>],
114) -> (Option<SourceLanguage>, HashMap<String, f64>) {
115    let mut counts: HashMap<String, usize> = HashMap::new();
116    for p in paths {
117        let ext = p
118            .as_ref()
119            .extension()
120            .and_then(|e| e.to_str())
121            .unwrap_or("")
122            .to_ascii_lowercase();
123        let lang = match ext.as_str() {
124            "ts" | "tsx" => "typescript",
125            "js" | "jsx" | "mjs" | "cjs" => "javascript",
126            "py" | "pyi" => "python",
127            "rs" => "rust",
128            "go" => "go",
129            "java" => "java",
130            "kt" | "kts" => "kotlin",
131            "scala" | "sc" => "scala",
132            "cs" | "csx" => "csharp",
133            "fs" | "fsx" => "fsharp",
134            "rb" => "ruby",
135            "php" => "php",
136            "pl" | "pm" => "perl",
137            "lua" => "lua",
138            "r" => "r",
139            "jl" => "julia",
140            "swift" => "swift",
141            "dart" => "dart",
142            "ex" | "exs" => "elixir",
143            "erl" | "hrl" => "erlang",
144            "hs" | "lhs" => "haskell",
145            "ml" | "mli" => "ocaml",
146            "clj" | "cljs" | "cljc" => "clojure",
147            "c" | "h" => "c",
148            "cpp" | "cc" | "cxx" | "hpp" | "hh" => "cpp",
149            "zig" => "zig",
150            "nim" => "nim",
151            "cr" => "crystal",
152            "sol" => "solidity",
153            "sql" => "sql",
154            "sh" | "bash" | "zsh" => "shell",
155            "ps1" | "psm1" => "powershell",
156            "html" | "htm" => "html",
157            "css" => "css",
158            _ => continue,
159        };
160        *counts.entry(lang.to_string()).or_default() += 1;
161    }
162    let total: usize = counts.values().sum();
163    let mut mix = HashMap::new();
164    if total > 0 {
165        for (k, v) in &counts {
166            mix.insert(k.clone(), (*v as f64) * 100.0 / total as f64);
167        }
168    }
169    // Prefer application languages over markup when scoring primary.
170    let priority = [
171        "typescript",
172        "javascript",
173        "python",
174        "rust",
175        "go",
176        "java",
177        "kotlin",
178        "csharp",
179        "ruby",
180        "php",
181        "swift",
182        "dart",
183        "c",
184        "cpp",
185        "scala",
186        "elixir",
187        "haskell",
188    ];
189    let primary = priority
190        .iter()
191        .filter_map(|k| counts.get(*k).map(|c| (*k, *c)))
192        .max_by_key(|(_, c)| *c)
193        .and_then(|(k, _)| SourceLanguage::parse(k));
194    (primary, mix)
195}