Skip to main content

fluidattacks_blends/
language.rs

1//! Path-based language detection and the tree-sitter grammar bindings for the
2//! domain `Language` enum.
3
4use std::path::Path;
5
6use tree_sitter::Language as TreeSitterLanguage;
7
8pub use blends_domain::Language;
9
10/// Shell-side operations on the domain [`Language`] enum: path detection and
11/// the tree-sitter grammar bindings domain cannot depend on (it is `no_std`).
12pub trait LanguageExt {
13    /// The language a file extension (without its leading dot) implies, if any.
14    #[must_use]
15    fn from_path(path: &Path) -> Option<Language>;
16    /// This language's tree-sitter grammar, or `None` when none is wired up yet.
17    #[must_use]
18    fn tree_sitter(self) -> Option<TreeSitterLanguage>;
19    /// This language's tree-sitter `node-types.json`, or `None` when none is wired up yet.
20    #[cfg(test)]
21    fn node_types(self) -> Option<&'static str>;
22}
23
24impl LanguageExt for Language {
25    fn from_path(path: &Path) -> Option<Language> {
26        path.extension()
27            .and_then(|extension| extension.to_str())
28            .map(str::to_ascii_lowercase)
29            .as_deref()
30            .and_then(Self::from_extension)
31    }
32
33    fn tree_sitter(self) -> Option<TreeSitterLanguage> {
34        Some(match self {
35            Self::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
36            Self::Elixir => tree_sitter_elixir::LANGUAGE.into(),
37            Self::Go => tree_sitter_go::LANGUAGE.into(),
38            Self::Hcl => tree_sitter_hcl::LANGUAGE.into(),
39            Self::Java => tree_sitter_java::LANGUAGE.into(),
40            Self::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
41            Self::Json => tree_sitter_json::LANGUAGE.into(),
42            Self::Kotlin => tree_sitter_kotlin_ng::LANGUAGE.into(),
43            Self::Php => tree_sitter_php::LANGUAGE_PHP.into(),
44            Self::Ruby => tree_sitter_ruby::LANGUAGE.into(),
45            Self::Rust => tree_sitter_rust::LANGUAGE.into(),
46            Self::Scala => tree_sitter_scala::LANGUAGE.into(),
47            Self::Swift => tree_sitter_swift::LANGUAGE.into(),
48            Self::TypeScript => tree_sitter_typescript::LANGUAGE_TSX.into(),
49            Self::Python => tree_sitter_python::LANGUAGE.into(),
50            Self::Yaml => tree_sitter_yaml::LANGUAGE.into(),
51            _ => return None,
52        })
53    }
54
55    #[cfg(test)]
56    fn node_types(self) -> Option<&'static str> {
57        Some(match self {
58            Self::CSharp => tree_sitter_c_sharp::NODE_TYPES,
59            Self::Elixir => tree_sitter_elixir::NODE_TYPES,
60            Self::Go => tree_sitter_go::NODE_TYPES,
61            Self::Hcl => tree_sitter_hcl::NODE_TYPES,
62            Self::Java => tree_sitter_java::NODE_TYPES,
63            Self::JavaScript => tree_sitter_javascript::NODE_TYPES,
64            Self::Json => tree_sitter_json::NODE_TYPES,
65            Self::Kotlin => tree_sitter_kotlin_ng::NODE_TYPES,
66            Self::Php => tree_sitter_php::PHP_NODE_TYPES,
67            Self::Ruby => tree_sitter_ruby::NODE_TYPES,
68            Self::Rust => tree_sitter_rust::NODE_TYPES,
69            Self::Scala => tree_sitter_scala::NODE_TYPES,
70            Self::Swift => tree_sitter_swift::NODE_TYPES,
71            Self::TypeScript => tree_sitter_typescript::TSX_NODE_TYPES,
72            Self::Python => tree_sitter_python::NODE_TYPES,
73            Self::Yaml => tree_sitter_yaml::NODE_TYPES,
74            _ => return None,
75        })
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::{Language, LanguageExt};
82    use std::path::Path;
83    use test_case::test_case;
84
85    #[test_case("cs" => Some(Language::CSharp))]
86    #[test_case("ex" => Some(Language::Elixir))]
87    #[test_case("exs" => Some(Language::Elixir))]
88    #[test_case("go" => Some(Language::Go))]
89    #[test_case("hcl" => Some(Language::Hcl))]
90    #[test_case("tf" => Some(Language::Hcl))]
91    #[test_case("java" => Some(Language::Java))]
92    #[test_case("js" => Some(Language::JavaScript))]
93    #[test_case("jsx" => Some(Language::JavaScript))]
94    #[test_case("json" => Some(Language::Json))]
95    #[test_case("kt" => Some(Language::Kotlin))]
96    #[test_case("ktm" => Some(Language::Kotlin))]
97    #[test_case("kts" => Some(Language::Kotlin))]
98    #[test_case("php" => Some(Language::Php))]
99    #[test_case("py" => Some(Language::Python))]
100    #[test_case("rb" => Some(Language::Ruby))]
101    #[test_case("rs" => Some(Language::Rust))]
102    #[test_case("scala" => Some(Language::Scala))]
103    #[test_case("swift" => Some(Language::Swift))]
104    #[test_case("ts" => Some(Language::TypeScript))]
105    #[test_case("tsx" => Some(Language::TypeScript))]
106    #[test_case("yaml" => Some(Language::Yaml))]
107    #[test_case("yml" => Some(Language::Yaml))]
108    #[test_case("unknown" => None)]
109    fn detects_by_extension(extension: &str) -> Option<Language> {
110        Language::from_extension(extension)
111    }
112
113    #[test_case("services/user/Api.java" => Some(Language::Java))]
114    #[test_case("integration/TestSuite.JAVA" => Some(Language::Java) ; "uppercase extension")]
115    #[test_case("infra/main.tf" => Some(Language::Hcl))]
116    #[test_case("config/vars.hcl" => Some(Language::Hcl))]
117    #[test_case("lib/app/router.ex" => Some(Language::Elixir))]
118    #[test_case("cmd/main.go" => Some(Language::Go))]
119    #[test_case("test/app/router_test.exs" => Some(Language::Elixir))]
120    #[test_case("Controllers/HouseController.cs" => Some(Language::CSharp))]
121    #[test_case("Models/User.CS" => Some(Language::CSharp) ; "uppercase cs extension")]
122    #[test_case("src/app/index.js" => Some(Language::JavaScript))]
123    #[test_case("src/app/index.JS" => Some(Language::JavaScript) ; "uppercase js extension")]
124    #[test_case("src/app/component.jsx" => Some(Language::JavaScript))]
125    #[test_case("src/app/app.ts" => Some(Language::TypeScript))]
126    #[test_case("src/app/component.tsx" => Some(Language::TypeScript))]
127    #[test_case("src/views/Home.swift" => Some(Language::Swift))]
128    #[test_case("pkg/module.py" => Some(Language::Python))]
129    #[test_case("scripts/build.PY" => Some(Language::Python) ; "uppercase py extension")]
130    #[test_case("config/settings.json" => Some(Language::Json))]
131    #[test_case("app/models/user.rb" => Some(Language::Ruby))]
132    #[test_case("src/main/scala/App.scala" => Some(Language::Scala))]
133    #[test_case("app/Main.kt" => Some(Language::Kotlin))]
134    #[test_case("build.gradle.kts" => Some(Language::Kotlin))]
135    #[test_case("deploy/values.yaml" => Some(Language::Yaml))]
136    #[test_case("ci/pipeline.yml" => Some(Language::Yaml))]
137    #[test_case("deploy/values.YAML" => Some(Language::Yaml) ; "uppercase yaml extension")]
138    #[test_case("src/main.rs" => Some(Language::Rust))]
139    #[test_case("src/unknown/randomfile.unknown" => None)]
140    #[test_case("folder/with/no_extension" => None)]
141    fn detects_language_by_path(path: &str) -> Option<Language> {
142        Language::from_path(Path::new(path))
143    }
144}