Skip to main content

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}