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 #[serde(skip_serializing_if = "Option::is_none")]
242 pub semantic_server: Option<crate::core::semantic::coverage::ServerStatus>,
243 #[serde(skip_serializing_if = "Option::is_none")]
246 pub calls_verified: Option<usize>,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub calls_with_evidence: Option<usize>,
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub semantic_negotiated: Option<crate::core::semantic::coverage::NegotiatedBackend>,
252}
253
254pub fn language_capability_matrix<I, S>(file_paths: I) -> Vec<LanguageCapabilityRow>
258where
259 I: IntoIterator<Item = S>,
260 S: AsRef<str>,
261{
262 let mut counts: std::collections::HashMap<LanguageId, usize> = std::collections::HashMap::new();
263 for path in file_paths {
264 if let Some(lang) = language_for_path(path.as_ref()) {
265 *counts.entry(lang).or_default() += 1;
266 }
267 }
268 let mut rows: Vec<LanguageCapabilityRow> = counts
269 .into_iter()
270 .map(|(lang, files)| {
271 let caps = capabilities(lang);
272 LanguageCapabilityRow {
273 language: lang.id_str(),
274 files,
275 symbols: caps.deep_queries,
276 imports: caps.import_resolver,
277 call_graph: supports_call_graph(lang),
278 symbols_found: None,
279 imports_found: None,
280 calls_found: None,
281 semantic_server: None,
282 semantic_negotiated: None,
283 calls_verified: None,
284 calls_with_evidence: None,
285 }
286 })
287 .collect();
288 rows.sort_by(|a, b| {
289 b.files
290 .cmp(&a.files)
291 .then_with(|| a.language.cmp(b.language))
292 });
293 rows
294}
295
296pub fn language_capability_matrix_realized(
309 file_paths: &[String],
310 symbol_files: &[String],
311 import_from_files: &[String],
312 call_caller_files: Option<&[String]>,
313) -> Vec<LanguageCapabilityRow> {
314 use std::collections::HashMap;
315
316 fn tally(paths: &[String], acc: &mut HashMap<LanguageId, usize>) {
317 for p in paths {
318 if let Some(lang) = language_for_path(p) {
319 *acc.entry(lang).or_default() += 1;
320 }
321 }
322 }
323
324 let mut files: HashMap<LanguageId, usize> = HashMap::new();
325 let mut symbols: HashMap<LanguageId, usize> = HashMap::new();
326 let mut imports: HashMap<LanguageId, usize> = HashMap::new();
327 let mut calls: HashMap<LanguageId, usize> = HashMap::new();
328 tally(file_paths, &mut files);
329 tally(symbol_files, &mut symbols);
330 tally(import_from_files, &mut imports);
331 if let Some(callers) = call_caller_files {
332 tally(callers, &mut calls);
333 }
334
335 let mut rows: Vec<LanguageCapabilityRow> = files
336 .into_iter()
337 .map(|(lang, file_count)| {
338 let caps = capabilities(lang);
339 LanguageCapabilityRow {
340 language: lang.id_str(),
341 files: file_count,
342 symbols: caps.deep_queries,
343 imports: caps.import_resolver,
344 call_graph: supports_call_graph(lang),
345 symbols_found: Some(symbols.get(&lang).copied().unwrap_or(0)),
346 imports_found: Some(imports.get(&lang).copied().unwrap_or(0)),
347 calls_found: call_caller_files.map(|_| calls.get(&lang).copied().unwrap_or(0)),
348 semantic_server: None,
349 semantic_negotiated: None,
350 calls_verified: None,
351 calls_with_evidence: None,
352 }
353 })
354 .collect();
355 rows.sort_by(|a, b| {
356 b.files
357 .cmp(&a.files)
358 .then_with(|| a.language.cmp(b.language))
359 });
360 rows
361}
362
363fn unsupported_source_language_name(ext: &str) -> Option<&'static str> {
369 match ext.trim().trim_start_matches('.').to_lowercase().as_str() {
370 "r" => Some("R"),
371 "jl" => Some("Julia"),
372 "nim" => Some("Nim"),
373 "cr" => Some("Crystal"),
374 "clj" | "cljs" | "cljc" => Some("Clojure"),
375 "erl" | "hrl" => Some("Erlang"),
376 "hs" => Some("Haskell"),
377 "ml" | "mli" => Some("OCaml"),
378 "fs" | "fsx" => Some("F#"),
379 "pl" | "pm" => Some("Perl"),
380 "groovy" | "gradle" => Some("Groovy"),
381 "tf" => Some("Terraform"),
382 "sol" => Some("Solidity"),
383 "f90" | "f95" | "f03" => Some("Fortran"),
384 "pas" => Some("Pascal"),
385 "d" => Some("D"),
386 "sql" => Some("SQL"),
387 "tcl" => Some("Tcl"),
388 "raku" | "rakumod" => Some("Raku"),
389 _ => None,
390 }
391}
392
393pub fn scan_unsupported_source_languages(root: &str, max_entries: usize) -> Vec<(String, usize)> {
399 let mut counts: std::collections::HashMap<&'static str, usize> =
400 std::collections::HashMap::new();
401 let walker = ignore::WalkBuilder::new(root)
402 .hidden(true)
403 .git_ignore(true)
404 .git_global(true)
405 .git_exclude(true)
406 .require_git(false)
407 .max_depth(Some(20))
408 .filter_entry(crate::core::walk_filter::keep_entry)
409 .build();
410 for entry in walker.flatten().take(max_entries) {
411 if !entry.file_type().is_some_and(|ft| ft.is_file()) {
412 continue;
413 }
414 let ext = entry
415 .path()
416 .extension()
417 .and_then(|e| e.to_str())
418 .unwrap_or("");
419 if let Some(name) = unsupported_source_language_name(ext) {
420 *counts.entry(name).or_default() += 1;
421 }
422 }
423 let mut ranked: Vec<(String, usize)> = counts
424 .into_iter()
425 .map(|(k, c)| (k.to_string(), c))
426 .collect();
427 ranked.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
428 ranked.truncate(5);
429 ranked
430}
431
432#[cfg(test)]
433pub mod tests {
434 use super::*;
435
436 #[test]
437 fn ext_mapping_basic() {
438 assert_eq!(language_for_ext("rs"), Some(LanguageId::Rust));
439 assert_eq!(language_for_ext(".tsx"), Some(LanguageId::TypeScript));
440 assert_eq!(language_for_ext("JS"), Some(LanguageId::JavaScript));
441 assert_eq!(language_for_ext("hxx"), Some(LanguageId::Cpp));
442 assert_eq!(language_for_ext("exs"), Some(LanguageId::Elixir));
443 assert_eq!(language_for_ext("unknown"), None);
444 }
445
446 #[test]
447 fn indexable_ext_true_for_known() {
448 assert!(is_indexable_ext("rs"));
449 assert!(is_indexable_ext("vue"));
450 assert!(!is_indexable_ext("md"));
451 }
452
453 #[test]
454 fn caps_are_deterministic() {
455 let c1 = capabilities(LanguageId::Rust);
456 let c2 = capabilities(LanguageId::Rust);
457 assert_eq!(c1, c2);
458 assert!(c1.deps_edges);
459 }
460
461 #[test]
462 fn all_languages_match_ext_table() {
463 for lang in ALL_LANGUAGES {
466 let names = graph_supported_language_names();
467 assert!(names.contains(&lang.id_str()));
468 }
469 assert!(graph_supported_language_names().contains(&"rust"));
470 assert_eq!(ALL_LANGUAGES.len(), graph_supported_language_names().len());
471 }
472
473 #[test]
474 fn callgraph_support_is_consistent() {
475 assert!(supports_call_graph(LanguageId::CSharp));
477 let names = callgraph_supported_language_names();
478 assert!(names.contains(&"csharp"));
479 assert!(names.contains(&"rust"));
480 assert!(names.contains(&"typescript"));
481 for name in &names {
484 assert!(graph_supported_language_names().contains(name));
485 }
486 assert!(names.len() <= ALL_LANGUAGES.len());
487 assert!(!supports_call_graph(LanguageId::Ruby));
489 }
490
491 #[test]
492 fn capability_matrix_reports_per_language_support() {
493 let paths = ["a.rs", "b.rs", "c.rb", "d.cs", "readme.md"];
494 let matrix = language_capability_matrix(paths);
495
496 assert_eq!(matrix.len(), 3);
498
499 let rust = matrix.iter().find(|r| r.language == "rust").unwrap();
500 assert_eq!(rust.files, 2);
501 assert!(rust.symbols && rust.imports && rust.call_graph);
502
503 let ruby = matrix.iter().find(|r| r.language == "ruby").unwrap();
505 assert!(ruby.symbols && ruby.imports && !ruby.call_graph);
506
507 let csharp = matrix.iter().find(|r| r.language == "csharp").unwrap();
509 assert!(csharp.symbols && csharp.imports && csharp.call_graph);
510
511 assert_eq!(matrix[0].language, "rust");
513 }
514
515 #[test]
516 fn realized_matrix_counts_actual_symbols_imports_calls() {
517 let files = vec!["a.rs".to_string(), "b.rs".to_string(), "c.rb".to_string()];
518 let symbol_files = vec!["a.rs".to_string(), "a.rs".to_string(), "c.rb".to_string()];
519 let import_from = vec!["a.rs".to_string()]; let callers = vec!["b.rs".to_string()]; let m = language_capability_matrix_realized(
523 &files,
524 &symbol_files,
525 &import_from,
526 Some(&callers),
527 );
528
529 let rust = m.iter().find(|r| r.language == "rust").unwrap();
530 assert_eq!(rust.files, 2);
531 assert_eq!(rust.symbols_found, Some(2));
532 assert_eq!(rust.imports_found, Some(1));
533 assert_eq!(rust.calls_found, Some(1));
534
535 let ruby = m.iter().find(|r| r.language == "ruby").unwrap();
536 assert_eq!(ruby.files, 1);
537 assert_eq!(ruby.symbols_found, Some(1));
538 assert_eq!(ruby.imports_found, Some(0)); assert_eq!(ruby.calls_found, Some(0));
540
541 let m2 = language_capability_matrix_realized(&files, &symbol_files, &import_from, None);
543 let rust2 = m2.iter().find(|r| r.language == "rust").unwrap();
544 assert_eq!(rust2.calls_found, None);
545 }
546
547 #[test]
548 fn lua_luau_are_first_class_indexed() {
549 assert_eq!(language_for_ext("lua"), Some(LanguageId::Lua));
551 assert_eq!(language_for_ext(".luau"), Some(LanguageId::Luau));
552 assert!(is_indexable_ext("lua"));
553 assert!(is_indexable_ext("luau"));
554 assert!(unsupported_source_language_name("lua").is_none());
555 assert!(unsupported_source_language_name("luau").is_none());
556 assert!(supports_call_graph(LanguageId::Lua));
558 assert!(supports_call_graph(LanguageId::Luau));
559 let names = graph_supported_language_names();
560 assert!(names.contains(&"lua"));
561 assert!(names.contains(&"luau"));
562 }
563
564 #[test]
565 fn unsupported_source_languages_named_but_not_indexed() {
566 assert_eq!(unsupported_source_language_name("r"), Some("R"));
568 assert_eq!(unsupported_source_language_name(".jl"), Some("Julia"));
569 assert!(!is_indexable_ext("r"));
570 assert!(!is_indexable_ext("jl"));
571 assert_eq!(unsupported_source_language_name("rs"), None);
573 assert_eq!(unsupported_source_language_name("lua"), None);
574 assert_eq!(unsupported_source_language_name("md"), None);
575 assert_eq!(unsupported_source_language_name("json"), None);
576 }
577
578 #[test]
579 fn scan_reports_unsupported_project() {
580 let dir = tempfile::tempdir().unwrap();
581 std::fs::write(dir.path().join("analysis.r"), "x <- 1").unwrap();
582 std::fs::write(dir.path().join("model.jl"), "x = 1").unwrap();
583 std::fs::write(dir.path().join("README.md"), "# docs").unwrap();
584 let found = scan_unsupported_source_languages(&dir.path().to_string_lossy(), 1000);
585 let names: Vec<&str> = found.iter().map(|(n, _)| n.as_str()).collect();
586 assert!(names.contains(&"R"));
587 assert!(names.contains(&"Julia"));
588 }
589}