use std::path::Path;
use serde::{Deserialize, Serialize};
use tree_sitter::Language;
#[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",
}
}
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,
})
}
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
}
}
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(),
}
}
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"),
}
}
}
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() {
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);
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);
}
}