pub struct ExtEntry {
pub ext: &'static str,
pub display: &'static str,
pub linter: &'static str,
}
macro_rules! e {
($ext:literal, $lang:literal) => {
ExtEntry {
ext: $ext,
display: $lang,
linter: $lang,
}
};
($ext:literal, $display:literal, $linter:literal) => {
ExtEntry {
ext: $ext,
display: $display,
linter: $linter,
}
};
}
pub static EXT_MAP: &[ExtEntry] = &[
e!(".rs", "rust"),
e!(".tsx", "tsx", "typescript"),
e!(".mts", "typescript"),
e!(".cts", "typescript"),
e!(".ts", "typescript"),
e!(".jsx", "jsx", "javascript"),
e!(".mjs", "javascript"),
e!(".cjs", "javascript"),
e!(".js", "javascript"),
e!(".pyw", "python"),
e!(".pyi", "python"),
e!(".py", "python"),
e!(".java", "java"),
e!(".go", "go"),
e!(".kts", "kotlin"),
e!(".kt", "kotlin"),
e!(".swift", "swift"),
e!(".gemspec", "ruby"),
e!(".rake", "ruby"),
e!(".ru", "ruby"),
e!(".rb", "ruby"),
e!(".phtml", "php"),
e!(".php", "php"),
e!(".cpp", "cpp"),
e!(".cc", "cpp"),
e!(".cxx", "cpp"),
e!(".hpp", "cpp"),
e!(".hh", "cpp"),
e!(".hxx", "cpp"),
e!(".c", "c", "cpp"),
e!(".h", "c", "cpp"),
e!(".cs", "csharp"),
e!(".scala", "scala"),
];
pub fn lang_for_extension(path: &str) -> &'static str {
let lower = path.to_ascii_lowercase();
EXT_MAP
.iter()
.find(|e| lower.ends_with(e.ext))
.map(|e| e.display)
.unwrap_or("unknown")
}
pub fn lang_for_linter(path: &str) -> Option<&'static str> {
let lower = path.to_ascii_lowercase();
EXT_MAP
.iter()
.find(|e| lower.ends_with(e.ext))
.map(|e| e.linter)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lang_for_extension_rust() {
assert_eq!(lang_for_extension("src/main.rs"), "rust");
assert_eq!(lang_for_extension("LIB.RS"), "rust"); }
#[test]
fn lang_for_extension_typescript_variants() {
assert_eq!(lang_for_extension("app.ts"), "typescript");
assert_eq!(lang_for_extension("src/index.d.ts"), "typescript");
assert_eq!(lang_for_extension("mod.mts"), "typescript");
assert_eq!(lang_for_extension("mod.cts"), "typescript");
}
#[test]
fn lang_for_extension_tsx_is_distinct() {
assert_eq!(lang_for_extension("App.tsx"), "tsx");
assert_eq!(lang_for_extension("App.TSX"), "tsx");
}
#[test]
fn lang_for_extension_javascript_variants() {
assert_eq!(lang_for_extension("index.js"), "javascript");
assert_eq!(lang_for_extension("mod.mjs"), "javascript");
assert_eq!(lang_for_extension("mod.cjs"), "javascript");
}
#[test]
fn lang_for_extension_jsx_is_distinct() {
assert_eq!(lang_for_extension("App.jsx"), "jsx");
}
#[test]
fn lang_for_extension_python_variants() {
assert_eq!(lang_for_extension("script.py"), "python");
assert_eq!(lang_for_extension("stubs.pyi"), "python");
assert_eq!(lang_for_extension("gui.pyw"), "python");
}
#[test]
fn lang_for_extension_jvm_languages() {
assert_eq!(lang_for_extension("Main.java"), "java");
assert_eq!(lang_for_extension("Main.kt"), "kotlin");
assert_eq!(lang_for_extension("build.kts"), "kotlin");
assert_eq!(lang_for_extension("Main.scala"), "scala");
}
#[test]
fn lang_for_extension_systems_languages() {
assert_eq!(lang_for_extension("main.go"), "go");
assert_eq!(lang_for_extension("foo.swift"), "swift");
assert_eq!(lang_for_extension("lib.cpp"), "cpp");
assert_eq!(lang_for_extension("lib.cc"), "cpp");
assert_eq!(lang_for_extension("lib.cxx"), "cpp");
assert_eq!(lang_for_extension("include.hpp"), "cpp");
assert_eq!(lang_for_extension("include.hh"), "cpp");
assert_eq!(lang_for_extension("include.hxx"), "cpp");
assert_eq!(lang_for_extension("main.c"), "c");
assert_eq!(lang_for_extension("header.h"), "c");
assert_eq!(lang_for_extension("app.cs"), "csharp");
}
#[test]
fn lang_for_extension_ruby_variants() {
assert_eq!(lang_for_extension("app.rb"), "ruby");
assert_eq!(lang_for_extension("task.rake"), "ruby");
assert_eq!(lang_for_extension("gem.gemspec"), "ruby");
assert_eq!(lang_for_extension("config.ru"), "ruby");
}
#[test]
fn lang_for_extension_php_variants() {
assert_eq!(lang_for_extension("index.php"), "php");
assert_eq!(lang_for_extension("template.phtml"), "php");
}
#[test]
fn lang_for_extension_unknown_fallback() {
assert_eq!(lang_for_extension("Makefile"), "unknown");
assert_eq!(lang_for_extension("data.csv"), "unknown");
assert_eq!(lang_for_extension("README.md"), "unknown");
}
#[test]
fn lang_for_linter_tsx_routes_to_typescript() {
assert_eq!(lang_for_linter("App.tsx"), Some("typescript"));
}
#[test]
fn lang_for_linter_jsx_routes_to_javascript() {
assert_eq!(lang_for_linter("App.jsx"), Some("javascript"));
}
#[test]
fn lang_for_linter_c_routes_to_cpp() {
assert_eq!(lang_for_linter("main.c"), Some("cpp"));
assert_eq!(lang_for_linter("header.h"), Some("cpp"));
}
#[test]
fn lang_for_linter_unknown_is_none() {
assert_eq!(lang_for_linter("Makefile"), None);
assert_eq!(lang_for_linter("data.json"), None);
}
#[test]
fn ext_map_no_duplicate_entries() {
let mut seen = std::collections::HashSet::new();
for entry in EXT_MAP {
assert!(
seen.insert(entry.ext),
"duplicate extension in EXT_MAP: {:?}",
entry.ext
);
}
}
#[test]
fn ext_map_suffix_ordering_invariant() {
for (a_idx, a_entry) in EXT_MAP.iter().enumerate() {
for (b_idx, b_entry) in EXT_MAP.iter().enumerate() {
if a_idx == b_idx {
continue;
}
if a_entry.ext.ends_with(b_entry.ext) && a_entry.ext.len() > b_entry.ext.len() {
assert!(
a_idx < b_idx,
"EXT_MAP suffix-ordering violation: {:?} (index {}) ends with {:?} \
(index {}) but appears AFTER it — the more-specific suffix must \
precede the less-specific one so `ends_with` matching routes \
correctly (e.g. `.kts` must precede `.ts`)",
a_entry.ext,
a_idx,
b_entry.ext,
b_idx,
);
}
}
}
}
#[test]
fn ext_map_covers_all_adapter_extensions() {
use crate::lang::{
CAnalyzer, CSharpAnalyzer, CppAnalyzer, GoAnalyzer, JavaAnalyzer, JavaScriptAnalyzer,
KotlinAnalyzer, LanguageAnalyzer, PhpAnalyzer, PythonAnalyzer, RubyAnalyzer,
RustAnalyzer, ScalaAnalyzer, SwiftAnalyzer, TypeScriptAnalyzer,
};
let ext_set: std::collections::HashSet<&str> = EXT_MAP.iter().map(|e| e.ext).collect();
let adapters: Vec<Box<dyn LanguageAnalyzer>> = vec![
Box::new(RustAnalyzer::new()),
Box::new(TypeScriptAnalyzer::new()),
Box::new(JavaScriptAnalyzer::new()),
Box::new(PythonAnalyzer::new()),
Box::new(JavaAnalyzer::new()),
Box::new(GoAnalyzer::new()),
Box::new(CppAnalyzer::new()),
Box::new(CAnalyzer::new()),
Box::new(KotlinAnalyzer::new()),
Box::new(SwiftAnalyzer::new()),
Box::new(RubyAnalyzer::new()),
Box::new(PhpAnalyzer::new()),
Box::new(CSharpAnalyzer::new()),
Box::new(ScalaAnalyzer::new()),
];
for adapter in &adapters {
for ext in adapter.supported_extensions() {
assert!(
ext_set.contains(ext),
"adapter '{}' has extension {:?} not present in EXT_MAP — add it",
adapter.language(),
ext,
);
}
}
}
}