1#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8
9use std::path::Path;
13use std::sync::Arc;
14use tree_sitter::Language;
15
16#[allow(unused_imports)]
21use crate::macros::{
22 get_language, mk_action, mk_code, mk_emacs_mode, mk_extensions, mk_lang, mk_langs,
23};
24use crate::preproc::PreprocResults;
25use crate::*;
26
27mk_langs!(
28 (
58 "javascript",
59 Javascript,
60 "The `JavaScript` language (upstream `tree-sitter-javascript` \
61 grammar; the default for `.js` / `.mjs` / `.cjs` / `.jsx`)",
62 "javascript",
63 JavascriptCode,
64 JavascriptParser,
65 tree_sitter_javascript,
66 [js, mjs, cjs, jsx],
67 ["js", "js2"],
68 "0.25.0"
69 ),
70 (
71 "mozjs",
72 Mozjs,
73 "The Mozilla/SpiderMonkey `JavaScript` dialect (vendored \
74 `tree-sitter-mozjs` fork; opt-in, owns the `.jsm` module \
75 extension)",
76 "mozjs",
77 MozjsCode,
78 MozjsParser,
79 tree_sitter_mozjs,
80 [jsm],
81 [],
82 "2.4.0"
83 ),
84 (
85 "java",
86 Java,
87 "The `Java` language",
88 "java",
89 JavaCode,
90 JavaParser,
91 tree_sitter_java,
92 [java],
93 ["java"],
94 "0.23.5"
95 ),
96 (
97 "go",
98 Go,
99 "The `Go` language",
100 "go",
101 GoCode,
102 GoParser,
103 tree_sitter_go,
104 [go],
105 ["go"],
106 "0.25.0"
107 ),
108 (
109 "kotlin",
110 Kotlin,
111 "The `Kotlin` language",
112 "kotlin",
113 KotlinCode,
114 KotlinParser,
115 tree_sitter_kotlin_ng,
116 [kt, kts],
117 ["kotlin"],
118 "1.1.0"
119 ),
120 (
121 "lua",
122 Lua,
123 "The `Lua` language",
124 "lua",
125 LuaCode,
126 LuaParser,
127 tree_sitter_lua,
128 [lua],
129 ["lua"],
130 "0.5.0"
131 ),
132 (
133 "rust",
134 Rust,
135 "The `Rust` language",
136 "rust",
137 RustCode,
138 RustParser,
139 tree_sitter_rust,
140 [rs],
141 ["rust"],
142 "0.24.2"
143 ),
144 (
145 "tcl",
146 Tcl,
147 "The `Tcl` language",
148 "tcl",
149 TclCode,
150 TclParser,
151 tree_sitter_tcl,
152 [tcl, tk, tm],
153 ["tcl"],
154 "2.4.0"
155 ),
156 (
157 "irules",
158 Irules,
159 "The `Irules` language",
160 "irules",
161 IrulesCode,
162 IrulesParser,
163 tree_sitter_irules,
164 [irule, irules],
165 ["irules"],
166 "0.2.0"
167 ),
168 (
169 "c",
170 C,
171 "The `C` language (upstream `tree-sitter-c` grammar). Owns `.c` \
172 and the `c` emacs mode since #721. C++ headers stay on \
173 `LANG::Cpp` (`.h` is asymmetric: a C++ header through the C \
174 grammar ERROR-cascades on `class` / `template`, while a C \
175 header through the C++ grammar only trips on C++-keyword \
176 identifiers).",
177 "c",
178 CCode,
179 CParser,
180 tree_sitter_c,
181 [c],
182 ["c"],
183 "0.24.2"
184 ),
185 (
186 "cpp",
187 Cpp,
188 "The `C/C++` language (upstream `tree-sitter-cpp` grammar; the \
189 default for `.cpp` / `.cc` / `.h` and the rest of the C-family \
190 extensions). C moved to `LANG::C` (`.c`) in #721; the \
191 Mozilla/Gecko dialect moved to opt-in `LANG::Mozcpp` in #720. \
192 Objective-C (`.m`) moved to `LANG::Objc` in #724; `.mm` \
193 Objective-C++ stays here because the C++ grammar handles the \
194 C++ half (the ObjC glue still ERROR-cascades).",
195 "cpp",
196 CppCode,
197 CppParser,
198 tree_sitter_cpp,
199 [cpp, cxx, cc, hxx, hpp, h, hh, inc, mm],
200 ["c++", "objc++", "objective-c++"],
201 "0.23.4"
202 ),
203 (
204 "mozcpp",
205 Mozcpp,
206 "The Mozilla/Gecko `C++` dialect (vendored `tree-sitter-mozcpp` \
207 fork: upstream `tree-sitter-cpp` plus the `MOZ_*` / `QM_TRY_*` / \
208 alone-macro overlay; opt-in, owns no file extensions — select it \
209 explicitly with `--language mozcpp`, a manifest, or the API).",
210 "mozcpp",
211 MozcppCode,
212 MozcppParser,
213 tree_sitter_mozcpp,
214 [],
215 [],
216 "2.4.0"
217 ),
218 (
219 "objc",
220 Objc,
221 "The `Objective-C` language (upstream `tree-sitter-objc` \
222 grammar). Owns `.m` and the `objc` / `objective-c` emacs \
223 modes since #724. Objective-C++ (`.mm`) stays on `LANG::Cpp`: \
224 the ObjC grammar parses C but not the C++ half of a `.mm` \
225 file, and C++ is the larger surface, so `Cpp` degrades more \
226 gracefully there (the same trade-off #721 used for `.h`).",
227 "objc",
228 ObjcCode,
229 ObjcParser,
230 tree_sitter_objc,
231 [m],
232 ["objc", "objective-c"],
233 "3.0.2"
234 ),
235 (
236 "csharp",
237 Csharp,
238 "The `C#` language",
239 "csharp",
240 CsharpCode,
241 CsharpParser,
242 tree_sitter_c_sharp,
243 [cs, csx, cake],
244 ["csharp"],
245 "0.23.5"
246 ),
247 (
248 "elixir",
249 Elixir,
250 "The `Elixir` language",
251 "elixir",
252 ElixirCode,
253 ElixirParser,
254 tree_sitter_elixir,
255 [ex, exs],
256 ["elixir"],
257 "0.3.5"
258 ),
259 (
260 "python",
261 Python,
262 "The `Python` language",
263 "python",
264 PythonCode,
265 PythonParser,
266 tree_sitter_python,
267 [py],
268 ["python"],
269 "0.25.0"
270 ),
271 (
272 "typescript",
273 Tsx,
274 "The `Tsx` language incorporates the `JSX` syntax inside `TypeScript`",
275 "tsx",
276 TsxCode,
277 TsxParser,
278 tree_sitter_tsx,
279 [tsx],
280 [],
281 "0.23.2"
282 ),
283 (
284 "typescript",
285 Typescript,
286 "The `TypeScript` language",
287 "typescript",
288 TypescriptCode,
289 TypescriptParser,
290 tree_sitter_typescript,
291 [ts, jsw, jsmw],
292 ["typescript"],
293 "0.23.2"
294 ),
295 (
296 "bash",
297 Bash,
298 "The `Bash` language",
299 "bash",
300 BashCode,
301 BashParser,
302 tree_sitter_bash,
303 [sh, bash],
304 ["sh"],
305 "0.25.1"
306 ),
307 (
308 "c-family-helpers",
309 Ccomment,
310 "The `Ccomment` language is a variant of the `C` language focused on comments",
311 "ccomment",
312 CcommentCode,
313 CcommentParser,
314 tree_sitter_ccomment,
315 [],
316 [],
317 "2.4.0"
318 ),
319 (
320 "c-family-helpers",
321 Preproc,
322 "The `PreProc` language is a variant of the `C/C++` language focused on macros",
323 "preproc",
324 PreprocCode,
325 PreprocParser,
326 tree_sitter_preproc,
327 [],
328 [],
329 "2.4.0"
330 ),
331 (
332 "perl",
333 Perl,
334 "The `Perl` language",
335 "perl",
336 PerlCode,
337 PerlParser,
338 tree_sitter_perl,
339 [pl, pm, t],
340 ["perl", "cperl"],
341 "1.1.2"
342 ),
343 (
344 "php",
345 Php,
346 "The `Php` language",
347 "php",
348 PhpCode,
349 PhpParser,
350 tree_sitter_php,
351 [php, phtml, php3, php4, php5, php7, phps],
352 ["php"],
353 "0.25.1"
354 ),
355 (
356 "ruby",
357 Ruby,
358 "The `Ruby` language",
359 "ruby",
360 RubyCode,
361 RubyParser,
362 tree_sitter_ruby,
363 [rb, rake, gemspec],
364 ["ruby"],
365 "0.23.1"
366 ),
367 (
368 "groovy",
369 Groovy,
370 "The `Groovy` language",
371 "groovy",
372 GroovyCode,
373 GroovyParser,
374 dekobon_tree_sitter_groovy,
375 [groovy, gradle, gvy, gy, gsh],
376 ["groovy"],
377 "0.3.0"
378 )
379);
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use crate::MetricsError;
385
386 #[cfg(any(
393 feature = "bash",
394 feature = "c",
395 feature = "c-family-helpers",
396 feature = "cpp",
397 feature = "csharp",
398 feature = "elixir",
399 feature = "go",
400 feature = "groovy",
401 feature = "irules",
402 feature = "java",
403 feature = "javascript",
404 feature = "kotlin",
405 feature = "lua",
406 feature = "mozcpp",
407 feature = "mozjs",
408 feature = "objc",
409 feature = "perl",
410 feature = "php",
411 feature = "python",
412 feature = "ruby",
413 feature = "rust",
414 feature = "tcl",
415 feature = "typescript",
416 ))]
417 #[test]
418 fn grammar_version_matches_cargo_toml_pin() {
419 let manifest = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../Cargo.toml"));
422
423 let dep_key = |lang: LANG| -> &'static str {
428 match lang {
429 LANG::Javascript => "tree-sitter-javascript",
430 LANG::Mozjs => "tree-sitter-mozjs",
431 LANG::Java => "tree-sitter-java",
432 LANG::Go => "tree-sitter-go",
433 LANG::Kotlin => "tree-sitter-kotlin-ng",
434 LANG::Lua => "tree-sitter-lua",
435 LANG::Rust => "tree-sitter-rust",
436 LANG::Tcl => "tree-sitter-tcl",
437 LANG::Irules => "tree-sitter-irules",
438 LANG::C => "tree-sitter-c",
439 LANG::Cpp => "tree-sitter-cpp",
440 LANG::Mozcpp => "tree-sitter-mozcpp",
441 LANG::Objc => "tree-sitter-objc",
442 LANG::Csharp => "tree-sitter-c-sharp",
443 LANG::Elixir => "tree-sitter-elixir",
444 LANG::Python => "tree-sitter-python",
445 LANG::Tsx | LANG::Typescript => "tree-sitter-typescript",
446 LANG::Bash => "tree-sitter-bash",
447 LANG::Ccomment => "tree-sitter-ccomment",
448 LANG::Preproc => "tree-sitter-preproc",
449 LANG::Perl => "tree-sitter-perl",
450 LANG::Php => "tree-sitter-php",
451 LANG::Ruby => "tree-sitter-ruby",
452 LANG::Groovy => "dekobon-tree-sitter-groovy",
453 }
454 };
455
456 let pinned = |key: &str| -> String {
463 let prefix = format!("{key} = ");
464 let line = manifest
465 .lines()
466 .map(str::trim)
467 .find(|l| l.starts_with(&prefix))
468 .unwrap_or_else(|| panic!("no `{key}` pin in workspace Cargo.toml"));
469 let Some(quote_eq) = line.find("\"=") else {
470 panic!("`{key}` pin is not an exact `=X.Y.Z` version: {line}")
471 };
472 let after_eq = &line[quote_eq + 2..];
473 let end = after_eq
474 .find('"')
475 .expect("closing quote on the version pin");
476 after_eq[..end].to_owned()
477 };
478
479 for lang in LANG::into_enum_iter() {
480 assert_eq!(
481 lang.grammar_version(),
482 pinned(dep_key(lang)),
483 "grammar_version() for {lang:?} drifted from its Cargo.toml pin",
484 );
485 }
486 }
487
488 #[cfg(feature = "all-languages")]
499 #[test]
500 fn every_lang_variant_is_enabled_under_all_languages() {
501 for lang in LANG::into_enum_iter() {
502 assert!(
503 lang.is_enabled(),
504 "{} should be enabled under the default `all-languages` feature set",
505 lang.name(),
506 );
507 }
508 }
509
510 #[cfg(not(feature = "javascript"))]
519 #[test]
520 fn disabled_language_dispatch_returns_language_disabled() {
521 assert!(!LANG::Javascript.is_enabled());
522 match LANG::Javascript.tree_sitter_language() {
523 Err(MetricsError::LanguageDisabled(LANG::Javascript)) => {}
524 other => panic!(
525 "expected Err(LanguageDisabled(Javascript)) for disabled `javascript` feature, got {other:?}",
526 ),
527 }
528 }
529
530 #[test]
536 fn is_enabled_matches_get_tree_sitter_language() {
537 for lang in LANG::into_enum_iter() {
538 let lookup = lang.tree_sitter_language();
539 assert_eq!(
540 lang.is_enabled(),
541 lookup.is_ok(),
542 "{} disagrees: is_enabled={}, tree_sitter_language={:?}",
543 lang.name(),
544 lang.is_enabled(),
545 lookup.map(|_| "Ok"),
546 );
547 }
548 }
549
550 #[test]
554 fn display_matches_name_for_every_variant() {
555 for lang in LANG::into_enum_iter() {
556 assert_eq!(lang.to_string(), lang.name());
557 }
558 }
559
560 #[test]
571 fn display_fromstr_round_trips_exactly_for_every_variant() {
572 use std::str::FromStr;
573 for lang in LANG::into_enum_iter() {
574 assert_eq!(
575 LANG::from_str(&lang.to_string()),
576 Ok(lang),
577 "{} did not round-trip exactly through Display/FromStr",
578 lang.name(),
579 );
580 }
581 }
582
583 #[test]
589 fn no_variant_slug_contains_punctuation() {
590 for lang in LANG::into_enum_iter() {
591 let name = lang.name();
592 assert!(
593 !name.contains('/') && !name.contains('#'),
594 "{name} contains punctuation that breaks FromStr lookup",
595 );
596 }
597 }
598
599 #[test]
605 fn javascript_pair_has_distinct_names() {
606 use std::str::FromStr;
607 assert_eq!(LANG::Javascript.name(), "javascript");
608 assert_eq!(LANG::Mozjs.name(), "mozjs");
609 assert_eq!(LANG::from_str("javascript"), Ok(LANG::Javascript));
610 assert_eq!(LANG::from_str("mozjs"), Ok(LANG::Mozjs));
611 }
612
613 #[test]
618 fn typescript_pair_has_distinct_slugs() {
619 use std::str::FromStr;
620 assert_eq!(LANG::Tsx.name(), "tsx");
621 assert_eq!(LANG::Typescript.name(), "typescript");
622 assert_eq!(LANG::from_str("tsx"), Ok(LANG::Tsx));
623 assert_eq!(LANG::from_str("typescript"), Ok(LANG::Typescript));
624 }
625
626 #[test]
631 fn javascript_extension_dispatch_defaults_to_upstream() {
632 assert_eq!(get_from_ext("js"), Some(LANG::Javascript));
633 assert_eq!(get_from_ext("mjs"), Some(LANG::Javascript));
634 assert_eq!(get_from_ext("cjs"), Some(LANG::Javascript));
635 assert_eq!(get_from_ext("jsx"), Some(LANG::Javascript));
636 assert_eq!(get_from_ext("jsm"), Some(LANG::Mozjs));
637 }
638
639 #[test]
643 fn javascript_emacs_mode_dispatch_defaults_to_upstream() {
644 assert_eq!(get_from_emacs_mode("js"), Some(LANG::Javascript));
645 assert_eq!(get_from_emacs_mode("js2"), Some(LANG::Javascript));
646 }
647
648 #[test]
653 fn cpp_and_csharp_slugs_round_trip() {
654 use std::str::FromStr;
655 assert_eq!(LANG::Cpp.to_string(), "cpp");
656 assert_eq!(LANG::from_str("cpp"), Ok(LANG::Cpp));
657 assert_eq!(LANG::Csharp.to_string(), "csharp");
658 assert_eq!(LANG::from_str("csharp"), Ok(LANG::Csharp));
659 assert!(LANG::from_str("c/c++").is_err());
661 assert!(LANG::from_str("c#").is_err());
662 }
663
664 #[test]
669 fn cpp_pair_has_distinct_names() {
670 use std::str::FromStr;
671 assert_eq!(LANG::Cpp.name(), "cpp");
672 assert_eq!(LANG::Mozcpp.name(), "mozcpp");
673 assert_eq!(LANG::from_str("cpp"), Ok(LANG::Cpp));
674 assert_eq!(LANG::from_str("mozcpp"), Ok(LANG::Mozcpp));
675 }
676
677 #[cfg(any(feature = "cpp", feature = "mozcpp"))]
684 #[test]
685 fn cpp_extension_dispatch_defaults_to_upstream() {
686 assert_eq!(get_from_ext("cpp"), Some(LANG::Cpp));
687 assert_eq!(get_from_ext("cc"), Some(LANG::Cpp));
688 assert_eq!(get_from_ext("hpp"), Some(LANG::Cpp));
689 assert_eq!(get_from_ext("h"), Some(LANG::Cpp));
691 assert_eq!(
693 LANG::Mozcpp.extensions(),
694 [] as [&str; 0],
695 "Mozcpp must own no file extension"
696 );
697 }
698
699 #[test]
706 fn c_language_dispatch_owns_dot_c_and_h_stays_cpp() {
707 use std::str::FromStr;
708 assert_eq!(LANG::C.name(), "c");
709 assert_eq!(LANG::from_str("c"), Ok(LANG::C));
710 assert_eq!(get_from_ext("c"), Some(LANG::C));
711 assert_eq!(get_from_emacs_mode("c"), Some(LANG::C));
712 assert_eq!(get_from_ext("h"), Some(LANG::Cpp));
714 assert_eq!(get_from_ext("cpp"), Some(LANG::Cpp));
715 assert_eq!(get_from_ext("m"), Some(LANG::Objc));
719 assert_eq!(get_from_ext("mm"), Some(LANG::Cpp));
720 assert_eq!(LANG::Objc.name(), "objc");
721 assert_eq!(LANG::from_str("objc"), Ok(LANG::Objc));
722 assert_eq!(get_from_emacs_mode("objc"), Some(LANG::Objc));
723 assert_eq!(get_from_emacs_mode("objective-c"), Some(LANG::Objc));
724 assert_eq!(get_from_emacs_mode("objc++"), Some(LANG::Cpp));
726 assert_eq!(get_from_emacs_mode("objective-c++"), Some(LANG::Cpp));
727 }
728
729 #[test]
732 fn fromstr_rejects_unknown_and_miscased() {
733 use std::str::FromStr;
734 assert!(LANG::from_str("Rust").is_err());
735 assert!(LANG::from_str("klingon").is_err());
736 assert!(LANG::from_str("").is_err());
737 let err = LANG::from_str("klingon").unwrap_err();
740 assert!(err.to_string().contains("klingon"));
741 assert_eq!(err.input(), "klingon");
742 }
743
744 #[cfg(any(feature = "cpp", feature = "python", feature = "rust"))]
747 #[test]
748 fn lang_is_usable_as_hash_key() {
749 use std::collections::{HashMap, HashSet};
750 let mut set = HashSet::new();
751 for lang in LANG::into_enum_iter() {
752 assert!(set.insert(lang), "{} inserted twice", lang.name());
753 }
754 assert_eq!(set.len(), LANG::into_enum_iter().count());
755
756 let mut map = HashMap::new();
757 map.insert(LANG::Rust, "rs");
758 map.insert(LANG::Python, "py");
759 assert_eq!(map.get(&LANG::Rust), Some(&"rs"));
760 assert_eq!(map.get(&LANG::Cpp), None);
761 }
762
763 #[cfg(feature = "rust")]
768 #[test]
769 fn language_disabled_display_includes_language_name() {
770 let err = MetricsError::LanguageDisabled(LANG::Rust);
771 let rendered = err.to_string();
772 assert!(
773 rendered.contains("rust"),
774 "expected LanguageDisabled display to mention `rust`, got {rendered:?}",
775 );
776 }
777}