1use serde::Serialize;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4pub enum LanguageId {
5 Rust,
6 TypeScript,
7 JavaScript,
8 Python,
9 Go,
10 Java,
11 C,
12 Cpp,
13 Ruby,
14 CSharp,
15 Kotlin,
16 Swift,
17 Php,
18 Bash,
19 Dart,
20 Scala,
21 Elixir,
22 Zig,
23 Gdscript,
24 Lua,
25 Luau,
26 Vue,
27 Svelte,
28 Tscn,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct LanguageCapabilities {
35 pub deps_edges: bool,
36 pub deep_queries: bool,
37 pub import_resolver: bool,
38}
39
40impl LanguageId {
41 pub fn id_str(&self) -> &'static str {
42 match self {
43 LanguageId::Rust => "rust",
44 LanguageId::TypeScript => "typescript",
45 LanguageId::JavaScript => "javascript",
46 LanguageId::Python => "python",
47 LanguageId::Go => "go",
48 LanguageId::Java => "java",
49 LanguageId::C => "c",
50 LanguageId::Cpp => "cpp",
51 LanguageId::Ruby => "ruby",
52 LanguageId::CSharp => "csharp",
53 LanguageId::Kotlin => "kotlin",
54 LanguageId::Swift => "swift",
55 LanguageId::Php => "php",
56 LanguageId::Bash => "bash",
57 LanguageId::Dart => "dart",
58 LanguageId::Scala => "scala",
59 LanguageId::Elixir => "elixir",
60 LanguageId::Zig => "zig",
61 LanguageId::Gdscript => "gdscript",
62 LanguageId::Lua => "lua",
63 LanguageId::Luau => "luau",
64 LanguageId::Vue => "vue",
65 LanguageId::Svelte => "svelte",
66 LanguageId::Tscn => "tscn",
67 }
68 }
69}
70
71pub fn capabilities(lang: LanguageId) -> LanguageCapabilities {
72 match lang {
73 LanguageId::Rust
75 | LanguageId::TypeScript
76 | LanguageId::JavaScript
77 | LanguageId::Python
78 | LanguageId::Go
79 | LanguageId::Java
80 | LanguageId::C
81 | LanguageId::Cpp
82 | LanguageId::Ruby
83 | LanguageId::CSharp
84 | LanguageId::Kotlin
85 | LanguageId::Swift
86 | LanguageId::Php
87 | LanguageId::Bash
88 | LanguageId::Dart
89 | LanguageId::Scala
90 | LanguageId::Elixir
91 | LanguageId::Zig
92 | LanguageId::Gdscript
93 | LanguageId::Lua
94 | LanguageId::Luau => LanguageCapabilities {
95 deps_edges: true,
96 deep_queries: true,
97 import_resolver: true,
98 },
99 LanguageId::Vue | LanguageId::Svelte => LanguageCapabilities {
101 deps_edges: true,
102 deep_queries: false,
103 import_resolver: false,
104 },
105 LanguageId::Tscn => LanguageCapabilities {
108 deps_edges: true,
109 deep_queries: false,
110 import_resolver: true,
111 },
112 }
113}
114
115pub fn language_for_ext(ext: &str) -> Option<LanguageId> {
116 let e = ext.trim().trim_start_matches('.').to_lowercase();
117 match e.as_str() {
118 "rs" => Some(LanguageId::Rust),
119 "ts" | "tsx" => Some(LanguageId::TypeScript),
120 "js" | "jsx" => Some(LanguageId::JavaScript),
121 "py" => Some(LanguageId::Python),
122 "go" => Some(LanguageId::Go),
123 "java" => Some(LanguageId::Java),
124 "c" | "h" => Some(LanguageId::C),
125 "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Some(LanguageId::Cpp),
126 "rb" => Some(LanguageId::Ruby),
127 "cs" => Some(LanguageId::CSharp),
128 "kt" | "kts" => Some(LanguageId::Kotlin),
129 "swift" => Some(LanguageId::Swift),
130 "php" => Some(LanguageId::Php),
131 "sh" | "bash" => Some(LanguageId::Bash),
132 "dart" => Some(LanguageId::Dart),
133 "scala" | "sc" => Some(LanguageId::Scala),
134 "ex" | "exs" => Some(LanguageId::Elixir),
135 "zig" => Some(LanguageId::Zig),
136 "gd" => Some(LanguageId::Gdscript),
137 "lua" => Some(LanguageId::Lua),
138 "luau" => Some(LanguageId::Luau),
139 "vue" => Some(LanguageId::Vue),
140 "svelte" => Some(LanguageId::Svelte),
141 "tscn" => Some(LanguageId::Tscn),
142 _ => None,
143 }
144}
145
146pub fn language_for_path(path: &str) -> Option<LanguageId> {
147 std::path::Path::new(path)
148 .extension()
149 .and_then(|e| e.to_str())
150 .and_then(language_for_ext)
151}
152
153pub fn is_indexable_ext(ext: &str) -> bool {
154 language_for_ext(ext).is_some()
155}
156
157pub const ALL_LANGUAGES: &[LanguageId] = &[
160 LanguageId::Rust,
161 LanguageId::TypeScript,
162 LanguageId::JavaScript,
163 LanguageId::Python,
164 LanguageId::Go,
165 LanguageId::Java,
166 LanguageId::C,
167 LanguageId::Cpp,
168 LanguageId::Ruby,
169 LanguageId::CSharp,
170 LanguageId::Kotlin,
171 LanguageId::Swift,
172 LanguageId::Php,
173 LanguageId::Bash,
174 LanguageId::Dart,
175 LanguageId::Scala,
176 LanguageId::Elixir,
177 LanguageId::Zig,
178 LanguageId::Gdscript,
179 LanguageId::Lua,
180 LanguageId::Luau,
181 LanguageId::Vue,
182 LanguageId::Svelte,
183 LanguageId::Tscn,
184];
185
186pub fn graph_supported_language_names() -> Vec<&'static str> {
188 ALL_LANGUAGES.iter().map(LanguageId::id_str).collect()
189}
190
191pub fn supports_call_graph(lang: LanguageId) -> bool {
196 matches!(
197 lang,
198 LanguageId::TypeScript
199 | LanguageId::JavaScript
200 | LanguageId::Rust
201 | LanguageId::Python
202 | LanguageId::Go
203 | LanguageId::Java
204 | LanguageId::Kotlin
205 | LanguageId::Gdscript
206 | LanguageId::Lua
207 | LanguageId::Luau
208 | LanguageId::CSharp
209 )
210}
211
212pub fn callgraph_supported_language_names() -> Vec<&'static str> {
214 ALL_LANGUAGES
215 .iter()
216 .filter(|l| supports_call_graph(**l))
217 .map(LanguageId::id_str)
218 .collect()
219}
220
221#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
225pub struct LanguageCapabilityRow {
226 pub language: &'static str,
227 pub files: usize,
228 pub symbols: bool,
229 pub imports: bool,
230 pub call_graph: bool,
231 pub symbols_found: Option<usize>,
234 pub imports_found: Option<usize>,
236 pub calls_found: Option<usize>,
239}
240
241pub fn language_capability_matrix<I, S>(file_paths: I) -> Vec<LanguageCapabilityRow>
245where
246 I: IntoIterator<Item = S>,
247 S: AsRef<str>,
248{
249 let mut counts: std::collections::HashMap<LanguageId, usize> = std::collections::HashMap::new();
250 for path in file_paths {
251 if let Some(lang) = language_for_path(path.as_ref()) {
252 *counts.entry(lang).or_default() += 1;
253 }
254 }
255 let mut rows: Vec<LanguageCapabilityRow> = counts
256 .into_iter()
257 .map(|(lang, files)| {
258 let caps = capabilities(lang);
259 LanguageCapabilityRow {
260 language: lang.id_str(),
261 files,
262 symbols: caps.deep_queries,
263 imports: caps.import_resolver,
264 call_graph: supports_call_graph(lang),
265 symbols_found: None,
266 imports_found: None,
267 calls_found: None,
268 }
269 })
270 .collect();
271 rows.sort_by(|a, b| {
272 b.files
273 .cmp(&a.files)
274 .then_with(|| a.language.cmp(b.language))
275 });
276 rows
277}
278
279pub fn language_capability_matrix_realized(
292 file_paths: &[String],
293 symbol_files: &[String],
294 import_from_files: &[String],
295 call_caller_files: Option<&[String]>,
296) -> Vec<LanguageCapabilityRow> {
297 use std::collections::HashMap;
298
299 fn tally(paths: &[String], acc: &mut HashMap<LanguageId, usize>) {
300 for p in paths {
301 if let Some(lang) = language_for_path(p) {
302 *acc.entry(lang).or_default() += 1;
303 }
304 }
305 }
306
307 let mut files: HashMap<LanguageId, usize> = HashMap::new();
308 let mut symbols: HashMap<LanguageId, usize> = HashMap::new();
309 let mut imports: HashMap<LanguageId, usize> = HashMap::new();
310 let mut calls: HashMap<LanguageId, usize> = HashMap::new();
311 tally(file_paths, &mut files);
312 tally(symbol_files, &mut symbols);
313 tally(import_from_files, &mut imports);
314 if let Some(callers) = call_caller_files {
315 tally(callers, &mut calls);
316 }
317
318 let mut rows: Vec<LanguageCapabilityRow> = files
319 .into_iter()
320 .map(|(lang, file_count)| {
321 let caps = capabilities(lang);
322 LanguageCapabilityRow {
323 language: lang.id_str(),
324 files: file_count,
325 symbols: caps.deep_queries,
326 imports: caps.import_resolver,
327 call_graph: supports_call_graph(lang),
328 symbols_found: Some(symbols.get(&lang).copied().unwrap_or(0)),
329 imports_found: Some(imports.get(&lang).copied().unwrap_or(0)),
330 calls_found: call_caller_files.map(|_| calls.get(&lang).copied().unwrap_or(0)),
331 }
332 })
333 .collect();
334 rows.sort_by(|a, b| {
335 b.files
336 .cmp(&a.files)
337 .then_with(|| a.language.cmp(b.language))
338 });
339 rows
340}
341
342fn unsupported_source_language_name(ext: &str) -> Option<&'static str> {
348 match ext.trim().trim_start_matches('.').to_lowercase().as_str() {
349 "r" => Some("R"),
350 "jl" => Some("Julia"),
351 "nim" => Some("Nim"),
352 "cr" => Some("Crystal"),
353 "clj" | "cljs" | "cljc" => Some("Clojure"),
354 "erl" | "hrl" => Some("Erlang"),
355 "hs" => Some("Haskell"),
356 "ml" | "mli" => Some("OCaml"),
357 "fs" | "fsx" => Some("F#"),
358 "pl" | "pm" => Some("Perl"),
359 "groovy" | "gradle" => Some("Groovy"),
360 "tf" => Some("Terraform"),
361 "sol" => Some("Solidity"),
362 "f90" | "f95" | "f03" => Some("Fortran"),
363 "pas" => Some("Pascal"),
364 "d" => Some("D"),
365 "sql" => Some("SQL"),
366 "tcl" => Some("Tcl"),
367 "raku" | "rakumod" => Some("Raku"),
368 _ => None,
369 }
370}
371
372pub fn scan_unsupported_source_languages(root: &str, max_entries: usize) -> Vec<(String, usize)> {
378 let mut counts: std::collections::HashMap<&'static str, usize> =
379 std::collections::HashMap::new();
380 let walker = ignore::WalkBuilder::new(root)
381 .hidden(true)
382 .git_ignore(true)
383 .git_global(true)
384 .git_exclude(true)
385 .require_git(false)
386 .max_depth(Some(20))
387 .filter_entry(crate::core::walk_filter::keep_entry)
388 .build();
389 for entry in walker.flatten().take(max_entries) {
390 if !entry.file_type().is_some_and(|ft| ft.is_file()) {
391 continue;
392 }
393 let ext = entry
394 .path()
395 .extension()
396 .and_then(|e| e.to_str())
397 .unwrap_or("");
398 if let Some(name) = unsupported_source_language_name(ext) {
399 *counts.entry(name).or_default() += 1;
400 }
401 }
402 let mut ranked: Vec<(String, usize)> = counts
403 .into_iter()
404 .map(|(k, c)| (k.to_string(), c))
405 .collect();
406 ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
407 ranked.truncate(5);
408 ranked
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn ext_mapping_basic() {
417 assert_eq!(language_for_ext("rs"), Some(LanguageId::Rust));
418 assert_eq!(language_for_ext(".tsx"), Some(LanguageId::TypeScript));
419 assert_eq!(language_for_ext("JS"), Some(LanguageId::JavaScript));
420 assert_eq!(language_for_ext("hxx"), Some(LanguageId::Cpp));
421 assert_eq!(language_for_ext("exs"), Some(LanguageId::Elixir));
422 assert_eq!(language_for_ext("unknown"), None);
423 }
424
425 #[test]
426 fn indexable_ext_true_for_known() {
427 assert!(is_indexable_ext("rs"));
428 assert!(is_indexable_ext("vue"));
429 assert!(!is_indexable_ext("md"));
430 }
431
432 #[test]
433 fn caps_are_deterministic() {
434 let c1 = capabilities(LanguageId::Rust);
435 let c2 = capabilities(LanguageId::Rust);
436 assert_eq!(c1, c2);
437 assert!(c1.deps_edges);
438 }
439
440 #[test]
441 fn all_languages_match_ext_table() {
442 for lang in ALL_LANGUAGES {
445 let names = graph_supported_language_names();
446 assert!(names.contains(&lang.id_str()));
447 }
448 assert!(graph_supported_language_names().contains(&"rust"));
449 assert_eq!(ALL_LANGUAGES.len(), graph_supported_language_names().len());
450 }
451
452 #[test]
453 fn callgraph_support_is_consistent() {
454 assert!(supports_call_graph(LanguageId::CSharp));
456 let names = callgraph_supported_language_names();
457 assert!(names.contains(&"csharp"));
458 assert!(names.contains(&"rust"));
459 assert!(names.contains(&"typescript"));
460 for name in &names {
463 assert!(graph_supported_language_names().contains(name));
464 }
465 assert!(names.len() <= ALL_LANGUAGES.len());
466 assert!(!supports_call_graph(LanguageId::Ruby));
468 }
469
470 #[test]
471 fn capability_matrix_reports_per_language_support() {
472 let paths = ["a.rs", "b.rs", "c.rb", "d.cs", "readme.md"];
473 let matrix = language_capability_matrix(paths);
474
475 assert_eq!(matrix.len(), 3);
477
478 let rust = matrix.iter().find(|r| r.language == "rust").unwrap();
479 assert_eq!(rust.files, 2);
480 assert!(rust.symbols && rust.imports && rust.call_graph);
481
482 let ruby = matrix.iter().find(|r| r.language == "ruby").unwrap();
484 assert!(ruby.symbols && ruby.imports && !ruby.call_graph);
485
486 let csharp = matrix.iter().find(|r| r.language == "csharp").unwrap();
488 assert!(csharp.symbols && csharp.imports && csharp.call_graph);
489
490 assert_eq!(matrix[0].language, "rust");
492 }
493
494 #[test]
495 fn realized_matrix_counts_actual_symbols_imports_calls() {
496 let files = vec!["a.rs".to_string(), "b.rs".to_string(), "c.rb".to_string()];
497 let symbol_files = vec!["a.rs".to_string(), "a.rs".to_string(), "c.rb".to_string()];
498 let import_from = vec!["a.rs".to_string()]; let callers = vec!["b.rs".to_string()]; let m = language_capability_matrix_realized(
502 &files,
503 &symbol_files,
504 &import_from,
505 Some(&callers),
506 );
507
508 let rust = m.iter().find(|r| r.language == "rust").unwrap();
509 assert_eq!(rust.files, 2);
510 assert_eq!(rust.symbols_found, Some(2));
511 assert_eq!(rust.imports_found, Some(1));
512 assert_eq!(rust.calls_found, Some(1));
513
514 let ruby = m.iter().find(|r| r.language == "ruby").unwrap();
515 assert_eq!(ruby.files, 1);
516 assert_eq!(ruby.symbols_found, Some(1));
517 assert_eq!(ruby.imports_found, Some(0)); assert_eq!(ruby.calls_found, Some(0));
519
520 let m2 = language_capability_matrix_realized(&files, &symbol_files, &import_from, None);
522 let rust2 = m2.iter().find(|r| r.language == "rust").unwrap();
523 assert_eq!(rust2.calls_found, None);
524 }
525
526 #[test]
527 fn lua_luau_are_first_class_indexed() {
528 assert_eq!(language_for_ext("lua"), Some(LanguageId::Lua));
530 assert_eq!(language_for_ext(".luau"), Some(LanguageId::Luau));
531 assert!(is_indexable_ext("lua"));
532 assert!(is_indexable_ext("luau"));
533 assert!(unsupported_source_language_name("lua").is_none());
534 assert!(unsupported_source_language_name("luau").is_none());
535 assert!(supports_call_graph(LanguageId::Lua));
537 assert!(supports_call_graph(LanguageId::Luau));
538 let names = graph_supported_language_names();
539 assert!(names.contains(&"lua"));
540 assert!(names.contains(&"luau"));
541 }
542
543 #[test]
544 fn unsupported_source_languages_named_but_not_indexed() {
545 assert_eq!(unsupported_source_language_name("r"), Some("R"));
547 assert_eq!(unsupported_source_language_name(".jl"), Some("Julia"));
548 assert!(!is_indexable_ext("r"));
549 assert!(!is_indexable_ext("jl"));
550 assert_eq!(unsupported_source_language_name("rs"), None);
552 assert_eq!(unsupported_source_language_name("lua"), None);
553 assert_eq!(unsupported_source_language_name("md"), None);
554 assert_eq!(unsupported_source_language_name("json"), None);
555 }
556
557 #[test]
558 fn scan_reports_unsupported_project() {
559 let dir = tempfile::tempdir().unwrap();
560 std::fs::write(dir.path().join("analysis.r"), "x <- 1").unwrap();
561 std::fs::write(dir.path().join("model.jl"), "x = 1").unwrap();
562 std::fs::write(dir.path().join("README.md"), "# docs").unwrap();
563 let found = scan_unsupported_source_languages(&dir.path().to_string_lossy(), 1000);
564 let names: Vec<&str> = found.iter().map(|(n, _)| n.as_str()).collect();
565 assert!(names.contains(&"R"));
566 assert!(names.contains(&"Julia"));
567 }
568}