big_code_analysis/langs.rs
1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8
9use std::path::Path;
10use std::sync::Arc;
11use tree_sitter::Language;
12
13// `get_language` is referenced from feature-gated arms inside the
14// `mk_lang!` expansion; an `--no-default-features` build with no
15// language features compiles every arm out, leaving the import
16// nominally unused. The macro itself carries the same allow.
17#[allow(unused_imports)]
18use crate::macros::{
19 get_language, mk_action, mk_code, mk_emacs_mode, mk_extensions, mk_lang, mk_langs,
20};
21use crate::preproc::PreprocResults;
22use crate::*;
23
24mk_langs!(
25 // 1) Cargo feature name that enables this variant's grammar
26 // 2) Name for enum
27 // 3) Language description
28 // 4) Display name
29 // 5) Empty struct name to implement
30 // 6) Parser name
31 // 7) tree-sitter function to call to get a Language
32 // 8) file extensions
33 // 9) emacs modes
34 // 10) pinned grammar crate version (mirrors the `=X.Y.Z` pin in the
35 // workspace `Cargo.toml`; a drift test below asserts they agree)
36 //
37 // Per #252, each variant carries a Cargo feature that gates the
38 // grammar crate references in `mk_lang!` / `mk_action!`. The enum
39 // surface (variants, file-extension lookup, emacs-mode lookup,
40 // per-language `*Code` / `*Parser` tags) is always compiled in;
41 // disabling a feature only strips the grammar crate from the dep
42 // graph and turns every dispatcher into
43 // `Err(MetricsError::LanguageDisabled(_))`.
44 //
45 // `Ccomment` and `Preproc` ride the internal `c-family-helpers`
46 // feature because they are helpers for the C-family pipeline; that
47 // feature pulls the `tree-sitter-ccomment` / `tree-sitter-preproc`
48 // crates and is enabled by every C-family language feature (`cpp`,
49 // `c`, and `mozcpp`), so the helpers are compiled in whenever any of
50 // them is (#721). `Tsx` rides `typescript` because
51 // both variants resolve to the `tree-sitter-typescript` crate
52 // (TSX vs TypeScript is a per-grammar `LANGUAGE_*` constant
53 // inside that one crate, see `get_language!` in `src/macros/mod.rs`).
54 (
55 "javascript",
56 Javascript,
57 "The `JavaScript` language (upstream `tree-sitter-javascript` \
58 grammar; the default for `.js` / `.mjs` / `.cjs` / `.jsx`)",
59 "javascript",
60 JavascriptCode,
61 JavascriptParser,
62 tree_sitter_javascript,
63 [js, mjs, cjs, jsx],
64 ["js", "js2"],
65 "0.25.0"
66 ),
67 (
68 "mozjs",
69 Mozjs,
70 "The Mozilla/SpiderMonkey `JavaScript` dialect (vendored \
71 `tree-sitter-mozjs` fork; opt-in, owns the `.jsm` module \
72 extension)",
73 "mozjs",
74 MozjsCode,
75 MozjsParser,
76 tree_sitter_mozjs,
77 [jsm],
78 [],
79 "2.0.0"
80 ),
81 (
82 "java",
83 Java,
84 "The `Java` language",
85 "java",
86 JavaCode,
87 JavaParser,
88 tree_sitter_java,
89 [java],
90 ["java"],
91 "0.23.5"
92 ),
93 (
94 "go",
95 Go,
96 "The `Go` language",
97 "go",
98 GoCode,
99 GoParser,
100 tree_sitter_go,
101 [go],
102 ["go"],
103 "0.25.0"
104 ),
105 (
106 "kotlin",
107 Kotlin,
108 "The `Kotlin` language",
109 "kotlin",
110 KotlinCode,
111 KotlinParser,
112 tree_sitter_kotlin_ng,
113 [kt, kts],
114 ["kotlin"],
115 "1.1.0"
116 ),
117 (
118 "lua",
119 Lua,
120 "The `Lua` language",
121 "lua",
122 LuaCode,
123 LuaParser,
124 tree_sitter_lua,
125 [lua],
126 ["lua"],
127 "0.5.0"
128 ),
129 (
130 "rust",
131 Rust,
132 "The `Rust` language",
133 "rust",
134 RustCode,
135 RustParser,
136 tree_sitter_rust,
137 [rs],
138 ["rust"],
139 "0.24.2"
140 ),
141 (
142 "tcl",
143 Tcl,
144 "The `Tcl` language",
145 "tcl",
146 TclCode,
147 TclParser,
148 tree_sitter_tcl,
149 [tcl, tk, tm],
150 ["tcl"],
151 "2.0.0"
152 ),
153 (
154 "irules",
155 Irules,
156 "The `Irules` language",
157 "irules",
158 IrulesCode,
159 IrulesParser,
160 tree_sitter_irules,
161 [irule, irules],
162 ["irules"],
163 "0.1.1"
164 ),
165 (
166 "c",
167 C,
168 "The `C` language (upstream `tree-sitter-c` grammar). Owns `.c` \
169 and the `c` emacs mode since #721. C++ headers stay on \
170 `LANG::Cpp` (`.h` is asymmetric: a C++ header through the C \
171 grammar ERROR-cascades on `class` / `template`, while a C \
172 header through the C++ grammar only trips on C++-keyword \
173 identifiers).",
174 "c",
175 CCode,
176 CParser,
177 tree_sitter_c,
178 [c],
179 ["c"],
180 "0.24.2"
181 ),
182 (
183 "cpp",
184 Cpp,
185 "The `C/C++` language (upstream `tree-sitter-cpp` grammar; the \
186 default for `.cpp` / `.cc` / `.h` and the rest of the C-family \
187 extensions). C moved to `LANG::C` (`.c`) in #721; the \
188 Mozilla/Gecko dialect moved to opt-in `LANG::Mozcpp` in #720. \
189 Objective-C (`.m`) moved to `LANG::Objc` in #724; `.mm` \
190 Objective-C++ stays here because the C++ grammar handles the \
191 C++ half (the ObjC glue still ERROR-cascades).",
192 "cpp",
193 CppCode,
194 CppParser,
195 tree_sitter_cpp,
196 [cpp, cxx, cc, hxx, hpp, h, hh, inc, mm],
197 ["c++", "objc++", "objective-c++"],
198 "0.23.4"
199 ),
200 (
201 "mozcpp",
202 Mozcpp,
203 "The Mozilla/Gecko `C++` dialect (vendored `tree-sitter-mozcpp` \
204 fork: upstream `tree-sitter-cpp` plus the `MOZ_*` / `QM_TRY_*` / \
205 alone-macro overlay; opt-in, owns no file extensions — select it \
206 explicitly with `--language mozcpp`, a manifest, or the API).",
207 "mozcpp",
208 MozcppCode,
209 MozcppParser,
210 tree_sitter_mozcpp,
211 [],
212 [],
213 "2.0.0"
214 ),
215 (
216 "objc",
217 Objc,
218 "The `Objective-C` language (upstream `tree-sitter-objc` \
219 grammar). Owns `.m` and the `objc` / `objective-c` emacs \
220 modes since #724. Objective-C++ (`.mm`) stays on `LANG::Cpp`: \
221 the ObjC grammar parses C but not the C++ half of a `.mm` \
222 file, and C++ is the larger surface, so `Cpp` degrades more \
223 gracefully there (the same trade-off #721 used for `.h`).",
224 "objc",
225 ObjcCode,
226 ObjcParser,
227 tree_sitter_objc,
228 [m],
229 ["objc", "objective-c"],
230 "3.0.2"
231 ),
232 (
233 "csharp",
234 Csharp,
235 "The `C#` language",
236 "csharp",
237 CsharpCode,
238 CsharpParser,
239 tree_sitter_c_sharp,
240 [cs, csx, cake],
241 ["csharp"],
242 "0.23.5"
243 ),
244 (
245 "elixir",
246 Elixir,
247 "The `Elixir` language",
248 "elixir",
249 ElixirCode,
250 ElixirParser,
251 tree_sitter_elixir,
252 [ex, exs],
253 ["elixir"],
254 "0.3.5"
255 ),
256 (
257 "python",
258 Python,
259 "The `Python` language",
260 "python",
261 PythonCode,
262 PythonParser,
263 tree_sitter_python,
264 [py],
265 ["python"],
266 "0.25.0"
267 ),
268 (
269 "typescript",
270 Tsx,
271 "The `Tsx` language incorporates the `JSX` syntax inside `TypeScript`",
272 "tsx",
273 TsxCode,
274 TsxParser,
275 tree_sitter_tsx,
276 [tsx],
277 [],
278 "0.23.2"
279 ),
280 (
281 "typescript",
282 Typescript,
283 "The `TypeScript` language",
284 "typescript",
285 TypescriptCode,
286 TypescriptParser,
287 tree_sitter_typescript,
288 [ts, jsw, jsmw],
289 ["typescript"],
290 "0.23.2"
291 ),
292 (
293 "bash",
294 Bash,
295 "The `Bash` language",
296 "bash",
297 BashCode,
298 BashParser,
299 tree_sitter_bash,
300 [sh, bash],
301 ["sh"],
302 "0.25.1"
303 ),
304 (
305 "c-family-helpers",
306 Ccomment,
307 "The `Ccomment` language is a variant of the `C` language focused on comments",
308 "ccomment",
309 CcommentCode,
310 CcommentParser,
311 tree_sitter_ccomment,
312 [],
313 [],
314 "2.0.0"
315 ),
316 (
317 "c-family-helpers",
318 Preproc,
319 "The `PreProc` language is a variant of the `C/C++` language focused on macros",
320 "preproc",
321 PreprocCode,
322 PreprocParser,
323 tree_sitter_preproc,
324 [],
325 [],
326 "2.0.0"
327 ),
328 (
329 "perl",
330 Perl,
331 "The `Perl` language",
332 "perl",
333 PerlCode,
334 PerlParser,
335 tree_sitter_perl,
336 [pl, pm, t],
337 ["perl", "cperl"],
338 "1.1.2"
339 ),
340 (
341 "php",
342 Php,
343 "The `Php` language",
344 "php",
345 PhpCode,
346 PhpParser,
347 tree_sitter_php,
348 [php, phtml, php3, php4, php5, php7, phps],
349 ["php"],
350 "0.24.2"
351 ),
352 (
353 "ruby",
354 Ruby,
355 "The `Ruby` language",
356 "ruby",
357 RubyCode,
358 RubyParser,
359 tree_sitter_ruby,
360 [rb, rake, gemspec],
361 ["ruby"],
362 "0.23.1"
363 ),
364 (
365 "groovy",
366 Groovy,
367 "The `Groovy` language",
368 "groovy",
369 GroovyCode,
370 GroovyParser,
371 dekobon_tree_sitter_groovy,
372 [groovy, gradle, gvy, gy, gsh],
373 ["groovy"],
374 "0.2.2"
375 )
376);
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::MetricsError;
382
383 /// Drift guard: every [`LANG::grammar_version`] literal must match the
384 /// `=X.Y.Z` pin in the workspace `Cargo.toml`. Bumping a grammar pin
385 /// without updating the macro literal (or vice versa) fails here, so the
386 /// runtime accessor and the actual dependency graph cannot silently
387 /// disagree (#727). Feature-independent: the version literals and the
388 /// manifest pins both exist regardless of the enabled language set.
389 #[test]
390 fn grammar_version_matches_cargo_toml_pin() {
391 // CARGO_MANIFEST_DIR is the root crate's dir, which is the workspace
392 // root, so this is the manifest that carries `[workspace.dependencies]`.
393 let manifest = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"));
394
395 // LANG variant -> its `[workspace.dependencies]` key. Tsx and
396 // Typescript share the one `tree-sitter-typescript` crate; the
397 // C-family helpers and dialect forks each have their own vendored
398 // `bca-tree-sitter-*` crate (aliased back to `tree-sitter-*`).
399 let dep_key = |lang: LANG| -> &'static str {
400 match lang {
401 LANG::Javascript => "tree-sitter-javascript",
402 LANG::Mozjs => "tree-sitter-mozjs",
403 LANG::Java => "tree-sitter-java",
404 LANG::Go => "tree-sitter-go",
405 LANG::Kotlin => "tree-sitter-kotlin-ng",
406 LANG::Lua => "tree-sitter-lua",
407 LANG::Rust => "tree-sitter-rust",
408 LANG::Tcl => "tree-sitter-tcl",
409 LANG::Irules => "tree-sitter-irules",
410 LANG::C => "tree-sitter-c",
411 LANG::Cpp => "tree-sitter-cpp",
412 LANG::Mozcpp => "tree-sitter-mozcpp",
413 LANG::Objc => "tree-sitter-objc",
414 LANG::Csharp => "tree-sitter-c-sharp",
415 LANG::Elixir => "tree-sitter-elixir",
416 LANG::Python => "tree-sitter-python",
417 LANG::Tsx | LANG::Typescript => "tree-sitter-typescript",
418 LANG::Bash => "tree-sitter-bash",
419 LANG::Ccomment => "tree-sitter-ccomment",
420 LANG::Preproc => "tree-sitter-preproc",
421 LANG::Perl => "tree-sitter-perl",
422 LANG::Php => "tree-sitter-php",
423 LANG::Ruby => "tree-sitter-ruby",
424 LANG::Groovy => "dekobon-tree-sitter-groovy",
425 }
426 };
427
428 // Pull the pinned `=X.Y.Z` for `key` out of the manifest. Handles
429 // both the bare-string form (`key = "=1.2.3"`) and the table form
430 // (`key = { package = ..., version = "=1.1.0" }`): the `"=` token
431 // appears only at the exact-version pin in either shape. The
432 // trailing space in the prefix keeps `tree-sitter-c` from matching
433 // `tree-sitter-c-sharp` / `tree-sitter-cpp`.
434 let pinned = |key: &str| -> String {
435 let prefix = format!("{key} = ");
436 let line = manifest
437 .lines()
438 .map(str::trim)
439 .find(|l| l.starts_with(&prefix))
440 .unwrap_or_else(|| panic!("no `{key}` pin in workspace Cargo.toml"));
441 let Some(quote_eq) = line.find("\"=") else {
442 panic!("`{key}` pin is not an exact `=X.Y.Z` version: {line}")
443 };
444 let after_eq = &line[quote_eq + 2..];
445 let end = after_eq
446 .find('"')
447 .expect("closing quote on the version pin");
448 after_eq[..end].to_owned()
449 };
450
451 for lang in LANG::into_enum_iter() {
452 assert_eq!(
453 lang.grammar_version(),
454 pinned(dep_key(lang)),
455 "grammar_version() for {lang:?} drifted from its Cargo.toml pin",
456 );
457 }
458 }
459
460 // The test suite normally runs under the workspace default
461 // feature set (`all-languages` is on, see `Cargo.toml`), so
462 // every variant must report itself as enabled. A regression in
463 // the cfg-gating of `is_enabled` would flip individual arms to
464 // `false` even when the matching grammar crate is in the dep
465 // graph; this test would catch that without needing a separate
466 // `--no-default-features` build matrix entry. Gated on
467 // `feature = "all-languages"` so the CI minimal-langs matrix
468 // entry (`--no-default-features --features rust,typescript`)
469 // still compiles cleanly without a runtime failure.
470 #[cfg(feature = "all-languages")]
471 #[test]
472 fn every_lang_variant_is_enabled_under_all_languages() {
473 for lang in LANG::into_enum_iter() {
474 assert!(
475 lang.is_enabled(),
476 "{} should be enabled under the default `all-languages` feature set",
477 lang.name(),
478 );
479 }
480 }
481
482 // Smoke test for the `LanguageDisabled` contract on a build
483 // without the `javascript` feature: every dispatch entry point
484 // (here, `tree_sitter_language`) must hand back
485 // `Err(LanguageDisabled(LANG::Javascript))`. Gated on
486 // `not(feature = "javascript")` so it only runs in a feature-
487 // subset build where the language is actually disabled — the
488 // `all-languages` default would have `is_enabled` return true
489 // and `tree_sitter_language` succeed.
490 #[cfg(not(feature = "javascript"))]
491 #[test]
492 fn disabled_language_dispatch_returns_language_disabled() {
493 assert!(!LANG::Javascript.is_enabled());
494 match LANG::Javascript.tree_sitter_language() {
495 Err(MetricsError::LanguageDisabled(LANG::Javascript)) => {}
496 other => panic!(
497 "expected Err(LanguageDisabled(Javascript)) for disabled `javascript` feature, got {other:?}",
498 ),
499 }
500 }
501
502 // `is_enabled` and `tree_sitter_language` must agree: a
503 // variant that reports itself enabled must hand back a usable
504 // `Language`, never `Err(LanguageDisabled)`. The pairing exists
505 // so callers that branch on `is_enabled` (rather than match on
506 // the error) can rely on the language lookup succeeding.
507 #[test]
508 fn is_enabled_matches_get_tree_sitter_language() {
509 for lang in LANG::into_enum_iter() {
510 let lookup = lang.tree_sitter_language();
511 assert_eq!(
512 lang.is_enabled(),
513 lookup.is_ok(),
514 "{} disagrees: is_enabled={}, tree_sitter_language={:?}",
515 lang.name(),
516 lang.is_enabled(),
517 lookup.map(|_| "Ok"),
518 );
519 }
520 }
521
522 // Regression guard for issue #262: the `MetricsError::EmptyRoot`
523 // variant is documented as "Reserved — not produced today".
524 // `metrics_with_options` pushes a synthetic top-level Unit
525 // `FuncSpace` before walking, so every parse — including empty,
526 // whitespace-only, and comment-only input — currently returns
527 // `Ok(FuncSpace { kind: Unit, .. })`. If the walker is ever
528 // changed to legitimately drain its state stack (e.g. by
529 // dropping the synthetic root), this test will start failing
530 // and the variant docs must be revisited.
531 #[test]
532 fn empty_and_comment_only_input_never_returns_empty_root() {
533 use crate::{MetricsOptions, Source, SpaceKind, analyze};
534
535 // Pair every enabled language with sources that would, by
536 // the old (false) variant doc, surface `EmptyRoot`. The
537 // comment syntaxes cover line and block forms across the
538 // supported language families.
539 let inputs: &[&[u8]] = &[b"", b" \n\t\n", b"// just a comment\n", b"/* block */\n"];
540
541 for lang in LANG::into_enum_iter() {
542 if !lang.is_enabled() {
543 continue;
544 }
545 for src in inputs {
546 let space = analyze(Source::new(lang, src), MetricsOptions::default())
547 .unwrap_or_else(|err| {
548 panic!(
549 "{} on input {:?} unexpectedly returned {err:?}; \
550 EmptyRoot is documented as not produced today",
551 lang.name(),
552 String::from_utf8_lossy(src),
553 )
554 });
555 assert_eq!(
556 space.kind,
557 SpaceKind::Unit,
558 "{} on input {:?} produced a non-Unit top-level FuncSpace",
559 lang.name(),
560 String::from_utf8_lossy(src),
561 );
562 }
563 }
564 }
565
566 // `Display` must agree with `name` for every variant — the
567 // impl delegates to it, so this pins that contract against future
568 // refactors that might diverge the two.
569 #[test]
570 fn display_matches_name_for_every_variant() {
571 for lang in LANG::into_enum_iter() {
572 assert_eq!(lang.to_string(), lang.name());
573 }
574 }
575
576 // `Display` -> `FromStr` round-trip over EVERY variant. Since #540
577 // every variant has a distinct canonical slug, so the round-trip is
578 // exact: `from_str(to_string(l)) == Ok(l)` for all `l`. This single
579 // iterating assertion proves both round-trip fidelity AND injectivity
580 // (a slug collision would make `from_str` resolve to the
581 // first-declared sibling, failing the `Ok(l)` equality for the
582 // later one). Test-via-revert: reintroducing the old "c/c++" display
583 // for `Cpp` keeps the round-trip passing for `Cpp` (it still parses
584 // back to `Cpp`), but reverting `Tsx` to "typescript" makes this
585 // test fail on whichever of `Tsx`/`Typescript` is declared second.
586 #[test]
587 fn display_fromstr_round_trips_exactly_for_every_variant() {
588 use std::str::FromStr;
589 for lang in LANG::into_enum_iter() {
590 assert_eq!(
591 LANG::from_str(&lang.to_string()),
592 Ok(lang),
593 "{} did not round-trip exactly through Display/FromStr",
594 lang.name(),
595 );
596 }
597 }
598
599 // No variant's canonical slug may contain `/` or `#` — the
600 // punctuation that made the old "c/c++" / "c#" display forms
601 // unusable as lookup tokens and leaked through the web `/metrics`
602 // `language` field (#540). Guards against reintroducing a
603 // human-pretty display form.
604 #[test]
605 fn no_variant_slug_contains_punctuation() {
606 for lang in LANG::into_enum_iter() {
607 let name = lang.name();
608 assert!(
609 !name.contains('/') && !name.contains('#'),
610 "{name} contains punctuation that breaks FromStr lookup",
611 );
612 }
613 }
614
615 // The JavaScript pair has distinct display names since #507, so
616 // `Display` is injective for it and the round-trip is exact —
617 // "javascript" -> `Javascript` (upstream grammar, the default) and
618 // "mozjs" -> `Mozjs` (the opt-in Mozilla fork). Pin both so a future
619 // reorder or display-string change is deliberate and test-visible.
620 #[test]
621 fn javascript_pair_has_distinct_names() {
622 use std::str::FromStr;
623 assert_eq!(LANG::Javascript.name(), "javascript");
624 assert_eq!(LANG::Mozjs.name(), "mozjs");
625 assert_eq!(LANG::from_str("javascript"), Ok(LANG::Javascript));
626 assert_eq!(LANG::from_str("mozjs"), Ok(LANG::Mozjs));
627 }
628
629 // The TypeScript pair has distinct slugs since #540 ("tsx" for
630 // `Tsx`, "typescript" for `Typescript`), even though both ride the
631 // upstream `tree-sitter-typescript` crate. Both round-trip to their
632 // own variant — no aliasing collapse.
633 #[test]
634 fn typescript_pair_has_distinct_slugs() {
635 use std::str::FromStr;
636 assert_eq!(LANG::Tsx.name(), "tsx");
637 assert_eq!(LANG::Typescript.name(), "typescript");
638 assert_eq!(LANG::from_str("tsx"), Ok(LANG::Tsx));
639 assert_eq!(LANG::from_str("typescript"), Ok(LANG::Typescript));
640 }
641
642 // Extension dispatch after the #507 default-grammar swap: the
643 // standard JS extensions resolve to the upstream `Javascript`
644 // grammar (including the newly-supported `.cjs`), while the Mozilla
645 // fork owns only `.jsm`.
646 #[test]
647 fn javascript_extension_dispatch_defaults_to_upstream() {
648 assert_eq!(get_from_ext("js"), Some(LANG::Javascript));
649 assert_eq!(get_from_ext("mjs"), Some(LANG::Javascript));
650 assert_eq!(get_from_ext("cjs"), Some(LANG::Javascript));
651 assert_eq!(get_from_ext("jsx"), Some(LANG::Javascript));
652 assert_eq!(get_from_ext("jsm"), Some(LANG::Mozjs));
653 }
654
655 // The `js` / `js2` emacs modes moved to the upstream `Javascript`
656 // default alongside the extensions; pin them so a future `mk_langs!`
657 // reorder cannot silently reroute emacs-mode dispatch to the fork.
658 #[test]
659 fn javascript_emacs_mode_dispatch_defaults_to_upstream() {
660 assert_eq!(get_from_emacs_mode("js"), Some(LANG::Javascript));
661 assert_eq!(get_from_emacs_mode("js2"), Some(LANG::Javascript));
662 }
663
664 // The `Cpp` and `Csharp` variants now expose punctuation-free
665 // canonical slugs ("cpp", "csharp") since #540; `FromStr` round-trips
666 // them and the former human-pretty "c/c++" / "c#" forms are no longer
667 // accepted (the slug is the single canonical token everywhere).
668 #[test]
669 fn cpp_and_csharp_slugs_round_trip() {
670 use std::str::FromStr;
671 assert_eq!(LANG::Cpp.to_string(), "cpp");
672 assert_eq!(LANG::from_str("cpp"), Ok(LANG::Cpp));
673 assert_eq!(LANG::Csharp.to_string(), "csharp");
674 assert_eq!(LANG::from_str("csharp"), Ok(LANG::Csharp));
675 // The dropped pretty forms no longer parse.
676 assert!(LANG::from_str("c/c++").is_err());
677 assert!(LANG::from_str("c#").is_err());
678 }
679
680 // Since #720 the C++ pair is split like the JS pair: upstream
681 // `Cpp` ("cpp", the default for the C-family extensions) and the
682 // opt-in Mozilla fork `Mozcpp` ("mozcpp"). Distinct slugs, both
683 // round-trip — no aliasing collapse (the #540 injectivity contract).
684 #[test]
685 fn cpp_pair_has_distinct_names() {
686 use std::str::FromStr;
687 assert_eq!(LANG::Cpp.name(), "cpp");
688 assert_eq!(LANG::Mozcpp.name(), "mozcpp");
689 assert_eq!(LANG::from_str("cpp"), Ok(LANG::Cpp));
690 assert_eq!(LANG::from_str("mozcpp"), Ok(LANG::Mozcpp));
691 }
692
693 // Extension dispatch after the #720 default-grammar swap: the
694 // C-family extensions resolve to the upstream `Cpp` grammar, while
695 // the Mozilla fork `Mozcpp` owns *no* extension — it is reachable
696 // only by explicit `--language mozcpp` / manifest / API selection.
697 // Pin this so a future `mk_langs!` reorder cannot silently hand a
698 // C-family extension to the fork (the failure mode #720 guards).
699 #[test]
700 fn cpp_extension_dispatch_defaults_to_upstream() {
701 assert_eq!(get_from_ext("cpp"), Some(LANG::Cpp));
702 assert_eq!(get_from_ext("cc"), Some(LANG::Cpp));
703 assert_eq!(get_from_ext("hpp"), Some(LANG::Cpp));
704 // `.h` stays on `Cpp` (decision-log #1: asymmetric failure modes).
705 assert_eq!(get_from_ext("h"), Some(LANG::Cpp));
706 // The fork claims zero extensions.
707 assert!(LANG::Mozcpp.extensions().is_empty());
708 assert!(
709 !LANG::into_enum_iter().any(|l| l == LANG::Mozcpp && !l.extensions().is_empty()),
710 "Mozcpp must own no file extension"
711 );
712 }
713
714 // The dedicated C language (#721) owns `.c` and the `c` emacs mode,
715 // both moved out of `Cpp`. `.h` stays on `Cpp` (decision-log #1:
716 // asymmetric failure modes — a C++ header through the C grammar
717 // ERROR-cascades, while a C header through C++ only trips on
718 // C++-keyword identifiers). Pin the reroute so a `mk_langs!` reorder
719 // cannot silently hand `.c` back to the C++ grammar.
720 #[test]
721 fn c_language_dispatch_owns_dot_c_and_h_stays_cpp() {
722 use std::str::FromStr;
723 assert_eq!(LANG::C.name(), "c");
724 assert_eq!(LANG::from_str("c"), Ok(LANG::C));
725 assert_eq!(get_from_ext("c"), Some(LANG::C));
726 assert_eq!(get_from_emacs_mode("c"), Some(LANG::C));
727 // `.h` and the C++ extensions remain on `Cpp`.
728 assert_eq!(get_from_ext("h"), Some(LANG::Cpp));
729 assert_eq!(get_from_ext("cpp"), Some(LANG::Cpp));
730 // `.m` (Objective-C) now owns `LANG::Objc` (#724); `.mm`
731 // Objective-C++ stays on `Cpp` by design (the ObjC grammar
732 // can't parse the C++ half of a `.mm` file).
733 assert_eq!(get_from_ext("m"), Some(LANG::Objc));
734 assert_eq!(get_from_ext("mm"), Some(LANG::Cpp));
735 assert_eq!(LANG::Objc.name(), "objc");
736 assert_eq!(LANG::from_str("objc"), Ok(LANG::Objc));
737 assert_eq!(get_from_emacs_mode("objc"), Some(LANG::Objc));
738 assert_eq!(get_from_emacs_mode("objective-c"), Some(LANG::Objc));
739 // The ObjC++ emacs modes remain mapped to `Cpp`.
740 assert_eq!(get_from_emacs_mode("objc++"), Some(LANG::Cpp));
741 assert_eq!(get_from_emacs_mode("objective-c++"), Some(LANG::Cpp));
742 }
743
744 // Unknown / mis-cased input is rejected; matching is case-sensitive,
745 // mirroring `Metric`'s `FromStr`.
746 #[test]
747 fn fromstr_rejects_unknown_and_miscased() {
748 use std::str::FromStr;
749 assert!(LANG::from_str("Rust").is_err());
750 assert!(LANG::from_str("klingon").is_err());
751 assert!(LANG::from_str("").is_err());
752 // The error carries the offending input verbatim, recoverable
753 // both via `Display` and the additive `input()` accessor (#536).
754 let err = LANG::from_str("klingon").unwrap_err();
755 assert!(err.to_string().contains("klingon"));
756 assert_eq!(err.input(), "klingon");
757 }
758
759 // `Hash` (+ `Eq`) lets `LANG` key a `HashMap` / populate a
760 // `HashSet` — the headline use case from issue #508.
761 #[test]
762 fn lang_is_usable_as_hash_key() {
763 use std::collections::{HashMap, HashSet};
764 let mut set = HashSet::new();
765 for lang in LANG::into_enum_iter() {
766 assert!(set.insert(lang), "{} inserted twice", lang.name());
767 }
768 assert_eq!(set.len(), LANG::into_enum_iter().count());
769
770 let mut map = HashMap::new();
771 map.insert(LANG::Rust, "rs");
772 map.insert(LANG::Python, "py");
773 assert_eq!(map.get(&LANG::Rust), Some(&"rs"));
774 assert_eq!(map.get(&LANG::Cpp), None);
775 }
776
777 // The error variant carries the originating `LANG` so callers
778 // can distinguish "X is disabled" from "Y is disabled" in a
779 // mixed batch. Verifies the `Display` impl mentions the
780 // language name as documented in `src/error.rs`.
781 #[test]
782 fn language_disabled_display_includes_language_name() {
783 let err = MetricsError::LanguageDisabled(LANG::Rust);
784 let rendered = err.to_string();
785 assert!(
786 rendered.contains("rust"),
787 "expected LanguageDisabled display to mention `rust`, got {rendered:?}",
788 );
789 }
790
791 // Drift guard for the crate-level `## Supported Languages` rustdoc
792 // list in `src/lib.rs` (#769): every LANG variant's canonical slug
793 // — the single source of truth from `name()` — must appear in that
794 // list as a backtick-delimited token. Without this guard, adding a
795 // language (or renaming a slug) silently desyncs the docs.rs
796 // landing page, which is exactly how Objective-C went missing for a
797 // full release after #724 shipped it.
798 //
799 // The slug set is derived from `LANG::into_enum_iter()`, which is
800 // compiled unconditionally (the enum surface is feature-independent;
801 // only the grammar crates are gated), so this test is robust under
802 // `--no-default-features` and any per-language feature subset — no
803 // `all-languages` gate needed. Distinct slugs are deduplicated, so
804 // shared-slug families do not require one bullet per variant.
805 #[test]
806 fn supported_languages_rustdoc_lists_every_slug() {
807 // Bound the search to the `## Supported Languages` section so an
808 // incidental backtick match elsewhere in the module docs (e.g.
809 // a slug named in the metrics section) cannot mask a real
810 // omission from the list itself.
811 const SECTION_HEADER: &str = "## Supported Languages";
812 const NEXT_HEADER: &str = "## Supported Metrics";
813
814 let lib_rs = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib.rs"))
815 .expect("src/lib.rs is readable from CARGO_MANIFEST_DIR");
816
817 let section_start = lib_rs
818 .find(SECTION_HEADER)
819 .expect("rustdoc must contain a `## Supported Languages` section");
820 let section_end = lib_rs[section_start..]
821 .find(NEXT_HEADER)
822 .map(|offset| section_start + offset)
823 .expect("`## Supported Languages` must be followed by `## Supported Metrics`");
824 let section = &lib_rs[section_start..section_end];
825
826 for lang in LANG::into_enum_iter() {
827 let slug_token = format!("`{}`", lang.name());
828 assert!(
829 section.contains(&slug_token),
830 "LANG::{lang:?} slug {slug_token} is missing from the \
831 `## Supported Languages` rustdoc list in src/lib.rs — \
832 add an entry there (see #769)",
833 );
834 }
835 }
836}