Skip to main content

big_code_analysis_ast/
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
9//! The [`LANG`] enum, extension and emacs-mode lookup, the per-language
10//! `*Code` tags and `*Parser` aliases, and [`AnyParser`].
11
12use std::path::Path;
13use std::sync::Arc;
14use tree_sitter::Language;
15
16// `get_language` is referenced from feature-gated arms inside the
17// `mk_lang!` expansion; an `--no-default-features` build with no
18// language features compiles every arm out, leaving the import
19// nominally unused. The macro itself carries the same allow.
20#[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    // 1) Cargo feature name that enables this variant's grammar
29    // 2) Name for enum
30    // 3) Language description
31    // 4) Display name
32    // 5) Empty struct name to implement
33    // 6) Parser name
34    // 7) tree-sitter function to call to get a Language
35    // 8) file extensions
36    // 9) emacs modes
37    // 10) pinned grammar crate version (mirrors the `=X.Y.Z` pin in the
38    //     workspace `Cargo.toml`; a drift test below asserts they agree)
39    //
40    // Per #252, each variant carries a Cargo feature that gates the
41    // grammar crate references in `mk_lang!` / `mk_action!`. The enum
42    // surface (variants, file-extension lookup, emacs-mode lookup,
43    // per-language `*Code` / `*Parser` tags) is always compiled in;
44    // disabling a feature only strips the grammar crate from the dep
45    // graph and turns every dispatcher into
46    // `Err(MetricsError::LanguageDisabled(_))`.
47    //
48    // `Ccomment` and `Preproc` ride the internal `c-family-helpers`
49    // feature because they are helpers for the C-family pipeline; that
50    // feature pulls the `tree-sitter-ccomment` / `tree-sitter-preproc`
51    // crates and is enabled by every C-family language feature (`cpp`,
52    // `c`, and `mozcpp`), so the helpers are compiled in whenever any of
53    // them is (#721). `Tsx` rides `typescript` because
54    // both variants resolve to the `tree-sitter-typescript` crate
55    // (TSX vs TypeScript is a per-grammar `LANGUAGE_*` constant
56    // inside that one crate, see `get_language!` in `src/macros/mod.rs`).
57    (
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    /// Drift guard: every [`LANG::grammar_version`] literal must match the
387    /// `=X.Y.Z` pin in the workspace `Cargo.toml`. Bumping a grammar pin
388    /// without updating the macro literal (or vice versa) fails here, so the
389    /// runtime accessor and the actual dependency graph cannot silently
390    /// disagree (#727). Feature-independent: the version literals and the
391    /// manifest pins both exist regardless of the enabled language set.
392    #[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        // CARGO_MANIFEST_DIR is this crate's dir, one level below the
420        // workspace root whose manifest carries `[workspace.dependencies]`.
421        let manifest = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../Cargo.toml"));
422
423        // LANG variant -> its `[workspace.dependencies]` key. Tsx and
424        // Typescript share the one `tree-sitter-typescript` crate; the
425        // C-family helpers and dialect forks each have their own vendored
426        // `bca-tree-sitter-*` crate (aliased back to `tree-sitter-*`).
427        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        // Pull the pinned `=X.Y.Z` for `key` out of the manifest. Handles
457        // both the bare-string form (`key = "=1.2.3"`) and the table form
458        // (`key = { package = ..., version = "=1.1.0" }`): the `"=` token
459        // appears only at the exact-version pin in either shape. The
460        // trailing space in the prefix keeps `tree-sitter-c` from matching
461        // `tree-sitter-c-sharp` / `tree-sitter-cpp`.
462        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    // The test suite normally runs under the workspace default
489    // feature set (`all-languages` is on, see `Cargo.toml`), so
490    // every variant must report itself as enabled. A regression in
491    // the cfg-gating of `is_enabled` would flip individual arms to
492    // `false` even when the matching grammar crate is in the dep
493    // graph; this test would catch that without needing a separate
494    // `--no-default-features` build matrix entry. Gated on
495    // `feature = "all-languages"` so the CI minimal-langs matrix
496    // entry (`--no-default-features --features rust,typescript`)
497    // still compiles cleanly without a runtime failure.
498    #[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    // Smoke test for the `LanguageDisabled` contract on a build
511    // without the `javascript` feature: every dispatch entry point
512    // (here, `tree_sitter_language`) must hand back
513    // `Err(LanguageDisabled(LANG::Javascript))`. Gated on
514    // `not(feature = "javascript")` so it only runs in a feature-
515    // subset build where the language is actually disabled — the
516    // `all-languages` default would have `is_enabled` return true
517    // and `tree_sitter_language` succeed.
518    #[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    // `is_enabled` and `tree_sitter_language` must agree: a
531    // variant that reports itself enabled must hand back a usable
532    // `Language`, never `Err(LanguageDisabled)`. The pairing exists
533    // so callers that branch on `is_enabled` (rather than match on
534    // the error) can rely on the language lookup succeeding.
535    #[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    // `Display` must agree with `name` for every variant — the
551    // impl delegates to it, so this pins that contract against future
552    // refactors that might diverge the two.
553    #[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    // `Display` -> `FromStr` round-trip over EVERY variant. Since #540
561    // every variant has a distinct canonical slug, so the round-trip is
562    // exact: `from_str(to_string(l)) == Ok(l)` for all `l`. This single
563    // iterating assertion proves both round-trip fidelity AND injectivity
564    // (a slug collision would make `from_str` resolve to the
565    // first-declared sibling, failing the `Ok(l)` equality for the
566    // later one). Test-via-revert: reintroducing the old "c/c++" display
567    // for `Cpp` keeps the round-trip passing for `Cpp` (it still parses
568    // back to `Cpp`), but reverting `Tsx` to "typescript" makes this
569    // test fail on whichever of `Tsx`/`Typescript` is declared second.
570    #[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    // No variant's canonical slug may contain `/` or `#` — the
584    // punctuation that made the old "c/c++" / "c#" display forms
585    // unusable as lookup tokens and leaked through the web `/metrics`
586    // `language` field (#540). Guards against reintroducing a
587    // human-pretty display form.
588    #[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    // The JavaScript pair has distinct display names since #507, so
600    // `Display` is injective for it and the round-trip is exact —
601    // "javascript" -> `Javascript` (upstream grammar, the default) and
602    // "mozjs" -> `Mozjs` (the opt-in Mozilla fork). Pin both so a future
603    // reorder or display-string change is deliberate and test-visible.
604    #[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    // The TypeScript pair has distinct slugs since #540 ("tsx" for
614    // `Tsx`, "typescript" for `Typescript`), even though both ride the
615    // upstream `tree-sitter-typescript` crate. Both round-trip to their
616    // own variant — no aliasing collapse.
617    #[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    // Extension dispatch after the #507 default-grammar swap: the
627    // standard JS extensions resolve to the upstream `Javascript`
628    // grammar (including the newly-supported `.cjs`), while the Mozilla
629    // fork owns only `.jsm`.
630    #[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    // The `js` / `js2` emacs modes moved to the upstream `Javascript`
640    // default alongside the extensions; pin them so a future `mk_langs!`
641    // reorder cannot silently reroute emacs-mode dispatch to the fork.
642    #[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    // The `Cpp` and `Csharp` variants now expose punctuation-free
649    // canonical slugs ("cpp", "csharp") since #540; `FromStr` round-trips
650    // them and the former human-pretty "c/c++" / "c#" forms are no longer
651    // accepted (the slug is the single canonical token everywhere).
652    #[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        // The dropped pretty forms no longer parse.
660        assert!(LANG::from_str("c/c++").is_err());
661        assert!(LANG::from_str("c#").is_err());
662    }
663
664    // Since #720 the C++ pair is split like the JS pair: upstream
665    // `Cpp` ("cpp", the default for the C-family extensions) and the
666    // opt-in Mozilla fork `Mozcpp` ("mozcpp"). Distinct slugs, both
667    // round-trip — no aliasing collapse (the #540 injectivity contract).
668    #[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    // Extension dispatch after the #720 default-grammar swap: the
678    // C-family extensions resolve to the upstream `Cpp` grammar, while
679    // the Mozilla fork `Mozcpp` owns *no* extension — it is reachable
680    // only by explicit `--language mozcpp` / manifest / API selection.
681    // Pin this so a future `mk_langs!` reorder cannot silently hand a
682    // C-family extension to the fork (the failure mode #720 guards).
683    #[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        // `.h` stays on `Cpp` (decision-log #1: asymmetric failure modes).
690        assert_eq!(get_from_ext("h"), Some(LANG::Cpp));
691        // The fork claims zero extensions.
692        assert_eq!(
693            LANG::Mozcpp.extensions(),
694            [] as [&str; 0],
695            "Mozcpp must own no file extension"
696        );
697    }
698
699    // The dedicated C language (#721) owns `.c` and the `c` emacs mode,
700    // both moved out of `Cpp`. `.h` stays on `Cpp` (decision-log #1:
701    // asymmetric failure modes — a C++ header through the C grammar
702    // ERROR-cascades, while a C header through C++ only trips on
703    // C++-keyword identifiers). Pin the reroute so a `mk_langs!` reorder
704    // cannot silently hand `.c` back to the C++ grammar.
705    #[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        // `.h` and the C++ extensions remain on `Cpp`.
713        assert_eq!(get_from_ext("h"), Some(LANG::Cpp));
714        assert_eq!(get_from_ext("cpp"), Some(LANG::Cpp));
715        // `.m` (Objective-C) now owns `LANG::Objc` (#724); `.mm`
716        // Objective-C++ stays on `Cpp` by design (the ObjC grammar
717        // can't parse the C++ half of a `.mm` file).
718        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        // The ObjC++ emacs modes remain mapped to `Cpp`.
725        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    // Unknown / mis-cased input is rejected; matching is case-sensitive,
730    // mirroring `Metric`'s `FromStr`.
731    #[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        // The error carries the offending input verbatim, recoverable
738        // both via `Display` and the additive `input()` accessor (#536).
739        let err = LANG::from_str("klingon").unwrap_err();
740        assert!(err.to_string().contains("klingon"));
741        assert_eq!(err.input(), "klingon");
742    }
743
744    // `Hash` (+ `Eq`) lets `LANG` key a `HashMap` / populate a
745    // `HashSet` — the headline use case from issue #508.
746    #[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    // The error variant carries the originating `LANG` so callers
764    // can distinguish "X is disabled" from "Y is disabled" in a
765    // mixed batch. Verifies the `Display` impl mentions the
766    // language name as documented in `src/error.rs`.
767    #[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}