use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LanguageId {
Rust,
TypeScript,
JavaScript,
Python,
Go,
Java,
C,
Cpp,
Ruby,
CSharp,
Kotlin,
Swift,
Php,
Bash,
Dart,
Scala,
Elixir,
Zig,
Gdscript,
Vue,
Svelte,
Tscn,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LanguageCapabilities {
pub deps_edges: bool,
pub deep_queries: bool,
pub import_resolver: bool,
}
impl LanguageId {
pub fn id_str(&self) -> &'static str {
match self {
LanguageId::Rust => "rust",
LanguageId::TypeScript => "typescript",
LanguageId::JavaScript => "javascript",
LanguageId::Python => "python",
LanguageId::Go => "go",
LanguageId::Java => "java",
LanguageId::C => "c",
LanguageId::Cpp => "cpp",
LanguageId::Ruby => "ruby",
LanguageId::CSharp => "csharp",
LanguageId::Kotlin => "kotlin",
LanguageId::Swift => "swift",
LanguageId::Php => "php",
LanguageId::Bash => "bash",
LanguageId::Dart => "dart",
LanguageId::Scala => "scala",
LanguageId::Elixir => "elixir",
LanguageId::Zig => "zig",
LanguageId::Gdscript => "gdscript",
LanguageId::Vue => "vue",
LanguageId::Svelte => "svelte",
LanguageId::Tscn => "tscn",
}
}
}
pub fn capabilities(lang: LanguageId) -> LanguageCapabilities {
match lang {
LanguageId::Rust
| LanguageId::TypeScript
| LanguageId::JavaScript
| LanguageId::Python
| LanguageId::Go
| LanguageId::Java
| LanguageId::C
| LanguageId::Cpp
| LanguageId::Ruby
| LanguageId::CSharp
| LanguageId::Kotlin
| LanguageId::Swift
| LanguageId::Php
| LanguageId::Bash
| LanguageId::Dart
| LanguageId::Scala
| LanguageId::Elixir
| LanguageId::Zig
| LanguageId::Gdscript => LanguageCapabilities {
deps_edges: true,
deep_queries: true,
import_resolver: true,
},
LanguageId::Vue | LanguageId::Svelte => LanguageCapabilities {
deps_edges: true,
deep_queries: false,
import_resolver: false,
},
LanguageId::Tscn => LanguageCapabilities {
deps_edges: true,
deep_queries: false,
import_resolver: true,
},
}
}
pub fn language_for_ext(ext: &str) -> Option<LanguageId> {
let e = ext.trim().trim_start_matches('.').to_lowercase();
match e.as_str() {
"rs" => Some(LanguageId::Rust),
"ts" | "tsx" => Some(LanguageId::TypeScript),
"js" | "jsx" => Some(LanguageId::JavaScript),
"py" => Some(LanguageId::Python),
"go" => Some(LanguageId::Go),
"java" => Some(LanguageId::Java),
"c" | "h" => Some(LanguageId::C),
"cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Some(LanguageId::Cpp),
"rb" => Some(LanguageId::Ruby),
"cs" => Some(LanguageId::CSharp),
"kt" | "kts" => Some(LanguageId::Kotlin),
"swift" => Some(LanguageId::Swift),
"php" => Some(LanguageId::Php),
"sh" | "bash" => Some(LanguageId::Bash),
"dart" => Some(LanguageId::Dart),
"scala" | "sc" => Some(LanguageId::Scala),
"ex" | "exs" => Some(LanguageId::Elixir),
"zig" => Some(LanguageId::Zig),
"gd" => Some(LanguageId::Gdscript),
"vue" => Some(LanguageId::Vue),
"svelte" => Some(LanguageId::Svelte),
"tscn" => Some(LanguageId::Tscn),
_ => None,
}
}
pub fn language_for_path(path: &str) -> Option<LanguageId> {
std::path::Path::new(path)
.extension()
.and_then(|e| e.to_str())
.and_then(language_for_ext)
}
pub fn is_indexable_ext(ext: &str) -> bool {
language_for_ext(ext).is_some()
}
pub const ALL_LANGUAGES: &[LanguageId] = &[
LanguageId::Rust,
LanguageId::TypeScript,
LanguageId::JavaScript,
LanguageId::Python,
LanguageId::Go,
LanguageId::Java,
LanguageId::C,
LanguageId::Cpp,
LanguageId::Ruby,
LanguageId::CSharp,
LanguageId::Kotlin,
LanguageId::Swift,
LanguageId::Php,
LanguageId::Bash,
LanguageId::Dart,
LanguageId::Scala,
LanguageId::Elixir,
LanguageId::Zig,
LanguageId::Gdscript,
LanguageId::Vue,
LanguageId::Svelte,
LanguageId::Tscn,
];
pub fn graph_supported_language_names() -> Vec<&'static str> {
ALL_LANGUAGES.iter().map(LanguageId::id_str).collect()
}
pub fn supports_call_graph(lang: LanguageId) -> bool {
matches!(
lang,
LanguageId::TypeScript
| LanguageId::JavaScript
| LanguageId::Rust
| LanguageId::Python
| LanguageId::Go
| LanguageId::Java
| LanguageId::Kotlin
| LanguageId::Gdscript
| LanguageId::CSharp
)
}
pub fn callgraph_supported_language_names() -> Vec<&'static str> {
ALL_LANGUAGES
.iter()
.filter(|l| supports_call_graph(**l))
.map(LanguageId::id_str)
.collect()
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct LanguageCapabilityRow {
pub language: &'static str,
pub files: usize,
pub symbols: bool,
pub imports: bool,
pub call_graph: bool,
pub symbols_found: Option<usize>,
pub imports_found: Option<usize>,
pub calls_found: Option<usize>,
}
pub fn language_capability_matrix<I, S>(file_paths: I) -> Vec<LanguageCapabilityRow>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut counts: std::collections::HashMap<LanguageId, usize> = std::collections::HashMap::new();
for path in file_paths {
if let Some(lang) = language_for_path(path.as_ref()) {
*counts.entry(lang).or_default() += 1;
}
}
let mut rows: Vec<LanguageCapabilityRow> = counts
.into_iter()
.map(|(lang, files)| {
let caps = capabilities(lang);
LanguageCapabilityRow {
language: lang.id_str(),
files,
symbols: caps.deep_queries,
imports: caps.import_resolver,
call_graph: supports_call_graph(lang),
symbols_found: None,
imports_found: None,
calls_found: None,
}
})
.collect();
rows.sort_by(|a, b| {
b.files
.cmp(&a.files)
.then_with(|| a.language.cmp(b.language))
});
rows
}
pub fn language_capability_matrix_realized(
file_paths: &[String],
symbol_files: &[String],
import_from_files: &[String],
call_caller_files: Option<&[String]>,
) -> Vec<LanguageCapabilityRow> {
use std::collections::HashMap;
fn tally(paths: &[String], acc: &mut HashMap<LanguageId, usize>) {
for p in paths {
if let Some(lang) = language_for_path(p) {
*acc.entry(lang).or_default() += 1;
}
}
}
let mut files: HashMap<LanguageId, usize> = HashMap::new();
let mut symbols: HashMap<LanguageId, usize> = HashMap::new();
let mut imports: HashMap<LanguageId, usize> = HashMap::new();
let mut calls: HashMap<LanguageId, usize> = HashMap::new();
tally(file_paths, &mut files);
tally(symbol_files, &mut symbols);
tally(import_from_files, &mut imports);
if let Some(callers) = call_caller_files {
tally(callers, &mut calls);
}
let mut rows: Vec<LanguageCapabilityRow> = files
.into_iter()
.map(|(lang, file_count)| {
let caps = capabilities(lang);
LanguageCapabilityRow {
language: lang.id_str(),
files: file_count,
symbols: caps.deep_queries,
imports: caps.import_resolver,
call_graph: supports_call_graph(lang),
symbols_found: Some(symbols.get(&lang).copied().unwrap_or(0)),
imports_found: Some(imports.get(&lang).copied().unwrap_or(0)),
calls_found: call_caller_files.map(|_| calls.get(&lang).copied().unwrap_or(0)),
}
})
.collect();
rows.sort_by(|a, b| {
b.files
.cmp(&a.files)
.then_with(|| a.language.cmp(b.language))
});
rows
}
fn unsupported_source_language_name(ext: &str) -> Option<&'static str> {
match ext.trim().trim_start_matches('.').to_lowercase().as_str() {
"lua" => Some("Lua"),
"luau" => Some("Luau"),
"r" => Some("R"),
"jl" => Some("Julia"),
"nim" => Some("Nim"),
"cr" => Some("Crystal"),
"clj" | "cljs" | "cljc" => Some("Clojure"),
"erl" | "hrl" => Some("Erlang"),
"hs" => Some("Haskell"),
"ml" | "mli" => Some("OCaml"),
"fs" | "fsx" => Some("F#"),
"pl" | "pm" => Some("Perl"),
"groovy" | "gradle" => Some("Groovy"),
"tf" => Some("Terraform"),
"sol" => Some("Solidity"),
"f90" | "f95" | "f03" => Some("Fortran"),
"pas" => Some("Pascal"),
"d" => Some("D"),
"sql" => Some("SQL"),
"tcl" => Some("Tcl"),
"raku" | "rakumod" => Some("Raku"),
_ => None,
}
}
pub fn scan_unsupported_source_languages(root: &str, max_entries: usize) -> Vec<(String, usize)> {
let mut counts: std::collections::HashMap<&'static str, usize> =
std::collections::HashMap::new();
let walker = ignore::WalkBuilder::new(root)
.hidden(true)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.require_git(false)
.max_depth(Some(20))
.filter_entry(crate::core::walk_filter::keep_entry)
.build();
for entry in walker.flatten().take(max_entries) {
if !entry.file_type().is_some_and(|ft| ft.is_file()) {
continue;
}
let ext = entry
.path()
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
if let Some(name) = unsupported_source_language_name(ext) {
*counts.entry(name).or_default() += 1;
}
}
let mut ranked: Vec<(String, usize)> = counts
.into_iter()
.map(|(k, c)| (k.to_string(), c))
.collect();
ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
ranked.truncate(5);
ranked
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ext_mapping_basic() {
assert_eq!(language_for_ext("rs"), Some(LanguageId::Rust));
assert_eq!(language_for_ext(".tsx"), Some(LanguageId::TypeScript));
assert_eq!(language_for_ext("JS"), Some(LanguageId::JavaScript));
assert_eq!(language_for_ext("hxx"), Some(LanguageId::Cpp));
assert_eq!(language_for_ext("exs"), Some(LanguageId::Elixir));
assert_eq!(language_for_ext("unknown"), None);
}
#[test]
fn indexable_ext_true_for_known() {
assert!(is_indexable_ext("rs"));
assert!(is_indexable_ext("vue"));
assert!(!is_indexable_ext("md"));
}
#[test]
fn caps_are_deterministic() {
let c1 = capabilities(LanguageId::Rust);
let c2 = capabilities(LanguageId::Rust);
assert_eq!(c1, c2);
assert!(c1.deps_edges);
}
#[test]
fn all_languages_match_ext_table() {
for lang in ALL_LANGUAGES {
let names = graph_supported_language_names();
assert!(names.contains(&lang.id_str()));
}
assert!(graph_supported_language_names().contains(&"rust"));
assert_eq!(ALL_LANGUAGES.len(), graph_supported_language_names().len());
}
#[test]
fn callgraph_support_is_consistent() {
assert!(supports_call_graph(LanguageId::CSharp));
let names = callgraph_supported_language_names();
assert!(names.contains(&"csharp"));
assert!(names.contains(&"rust"));
assert!(names.contains(&"typescript"));
for name in &names {
assert!(graph_supported_language_names().contains(name));
}
assert!(names.len() <= ALL_LANGUAGES.len());
assert!(!supports_call_graph(LanguageId::Ruby));
}
#[test]
fn capability_matrix_reports_per_language_support() {
let paths = ["a.rs", "b.rs", "c.rb", "d.cs", "readme.md"];
let matrix = language_capability_matrix(paths);
assert_eq!(matrix.len(), 3);
let rust = matrix.iter().find(|r| r.language == "rust").unwrap();
assert_eq!(rust.files, 2);
assert!(rust.symbols && rust.imports && rust.call_graph);
let ruby = matrix.iter().find(|r| r.language == "ruby").unwrap();
assert!(ruby.symbols && ruby.imports && !ruby.call_graph);
let csharp = matrix.iter().find(|r| r.language == "csharp").unwrap();
assert!(csharp.symbols && csharp.imports && csharp.call_graph);
assert_eq!(matrix[0].language, "rust");
}
#[test]
fn realized_matrix_counts_actual_symbols_imports_calls() {
let files = vec!["a.rs".to_string(), "b.rs".to_string(), "c.rb".to_string()];
let symbol_files = vec!["a.rs".to_string(), "a.rs".to_string(), "c.rb".to_string()];
let import_from = vec!["a.rs".to_string()]; let callers = vec!["b.rs".to_string()];
let m = language_capability_matrix_realized(
&files,
&symbol_files,
&import_from,
Some(&callers),
);
let rust = m.iter().find(|r| r.language == "rust").unwrap();
assert_eq!(rust.files, 2);
assert_eq!(rust.symbols_found, Some(2));
assert_eq!(rust.imports_found, Some(1));
assert_eq!(rust.calls_found, Some(1));
let ruby = m.iter().find(|r| r.language == "ruby").unwrap();
assert_eq!(ruby.files, 1);
assert_eq!(ruby.symbols_found, Some(1));
assert_eq!(ruby.imports_found, Some(0)); assert_eq!(ruby.calls_found, Some(0));
let m2 = language_capability_matrix_realized(&files, &symbol_files, &import_from, None);
let rust2 = m2.iter().find(|r| r.language == "rust").unwrap();
assert_eq!(rust2.calls_found, None);
}
#[test]
fn unsupported_source_languages_named_but_not_indexed() {
assert_eq!(unsupported_source_language_name("lua"), Some("Lua"));
assert_eq!(unsupported_source_language_name(".luau"), Some("Luau"));
assert!(!is_indexable_ext("lua"));
assert!(!is_indexable_ext("luau"));
assert_eq!(unsupported_source_language_name("rs"), None);
assert_eq!(unsupported_source_language_name("md"), None);
assert_eq!(unsupported_source_language_name("json"), None);
}
#[test]
fn scan_reports_lua_project() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("init.lua"), "local x = 1").unwrap();
std::fs::write(dir.path().join("mod.luau"), "return {}").unwrap();
std::fs::write(dir.path().join("README.md"), "# docs").unwrap();
let found = scan_unsupported_source_languages(&dir.path().to_string_lossy(), 1000);
let names: Vec<&str> = found.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"Lua"));
assert!(names.contains(&"Luau"));
}
}