Skip to main content

aptu_coder_core/
lang.rs

1// SPDX-FileCopyrightText: 2026 aptu-coder contributors
2// SPDX-License-Identifier: Apache-2.0
3//! Language detection by file extension.
4//!
5//! Maps file extensions to supported language identifiers.
6//!
7//! All languages are unconditionally compiled in.  The historical `lang-*` Cargo feature
8//! gates have been removed; every registered extension is always available at runtime.
9//! This simplifies the build matrix and guarantees that callers never get a feature-gate
10//! error for a language that "should" be supported.
11
12const EXTENSION_MAP: &[(&str, &str)] = &[
13    ("c", "c"),
14    ("cc", "cpp"),
15    ("cjs", "javascript"),
16    ("cpp", "cpp"),
17    ("cxx", "cpp"),
18    ("f", "fortran"),
19    ("f03", "fortran"),
20    ("f08", "fortran"),
21    ("f77", "fortran"),
22    ("f90", "fortran"),
23    ("f95", "fortran"),
24    ("for", "fortran"),
25    ("ftn", "fortran"),
26    ("h", "cpp"),
27    ("cs", "csharp"),
28    ("hpp", "cpp"),
29    ("hxx", "cpp"),
30    ("js", "javascript"),
31    ("mjs", "javascript"),
32    ("go", "go"),
33    ("java", "java"),
34    ("kt", "kotlin"),
35    ("kts", "kotlin"),
36    ("py", "python"),
37    ("rs", "rust"),
38    ("ts", "typescript"),
39    ("tsx", "tsx"),
40    ("html", "html"),
41    ("htm", "html"),
42    ("md", "markdown"),
43    ("mdx", "markdown"),
44    ("astro", "astro"),
45    ("css", "css"),
46    ("yaml", "yaml"),
47    ("yml", "yaml"),
48    ("json", "json"),
49    ("toml", "toml"),
50];
51
52/// Returns the language identifier for the given file extension, or `None` if unsupported.
53///
54/// The lookup is case-insensitive. Supported extensions include `rs`, `py`, `go`, `java`,
55/// `ts`, `tsx`, `js`, `mjs`, `cjs`, `c`, `cc`, `cpp`, `cxx`, `h`, `hpp`, `hxx`, `cs`,
56/// Fortran variants `f`, `f77`, `f90`, `f95`, `f03`, `f08`, `for`, `ftn`,
57/// HTML variants `html`, `htm`, and Markdown variants `md`, `mdx`.
58#[must_use]
59pub fn language_for_extension(ext: &str) -> Option<&'static str> {
60    EXTENSION_MAP
61        .iter()
62        .find(|(e, _)| e.eq_ignore_ascii_case(ext))
63        .map(|(_, lang)| *lang)
64}
65
66/// Returns all file extensions supported by the compiled feature set.
67///
68/// Each entry corresponds to one row in `EXTENSION_MAP`. The list is used to
69/// build human-readable error messages without duplicating the extension list.
70#[must_use]
71pub fn supported_extensions() -> Vec<&'static str> {
72    EXTENSION_MAP.iter().map(|(ext, _)| *ext).collect()
73}
74
75/// Returns a static slice of all supported language names.
76///
77/// All languages are unconditionally compiled in; the historical `lang-*` feature
78/// gates have been removed.
79#[must_use]
80pub fn supported_languages() -> &'static [&'static str] {
81    &[
82        "rust",
83        "go",
84        "java",
85        "kotlin",
86        "python",
87        "typescript",
88        "tsx",
89        "javascript",
90        "fortran",
91        "c",
92        "cpp",
93        "csharp",
94        "html",
95        "markdown",
96    ]
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_language_for_extension_happy_path() {
105        assert_eq!(language_for_extension("rs"), Some("rust"));
106        assert_eq!(language_for_extension("py"), Some("python"));
107        assert_eq!(language_for_extension("go"), Some("go"));
108        assert_eq!(language_for_extension("java"), Some("java"));
109        assert_eq!(language_for_extension("ts"), Some("typescript"));
110        assert_eq!(language_for_extension("tsx"), Some("tsx"));
111        assert_eq!(language_for_extension("f90"), Some("fortran"));
112        assert_eq!(language_for_extension("for"), Some("fortran"));
113        assert_eq!(language_for_extension("ftn"), Some("fortran"));
114        assert_eq!(language_for_extension("c"), Some("c"));
115        assert_eq!(language_for_extension("cpp"), Some("cpp"));
116        assert_eq!(language_for_extension("h"), Some("cpp"));
117        assert_eq!(language_for_extension("hpp"), Some("cpp"));
118        assert_eq!(language_for_extension("cc"), Some("cpp"));
119        assert_eq!(language_for_extension("kt"), Some("kotlin"));
120        assert_eq!(language_for_extension("kts"), Some("kotlin"));
121    }
122
123    /// Asserts every extension in `EXTENSION_MAP` appears as an alternation in
124    /// `SUPPORTED_FILE_EXT_PATTERN`, preventing drift when a new language is added.
125    /// The check is a substring match: the pattern has the form `...(ext1|ext2|...)...`
126    /// so each extension must appear as `ext|` or `ext)`.
127    #[test]
128    fn test_supported_file_ext_pattern_covers_all_extension_map_entries() {
129        #[cfg(feature = "schemars")]
130        for (ext, _lang) in EXTENSION_MAP {
131            let in_alternation = crate::schema_helpers::SUPPORTED_FILE_EXT_PATTERN
132                .contains(&format!("{ext}|"))
133                || crate::schema_helpers::SUPPORTED_FILE_EXT_PATTERN.contains(&format!("{ext})"));
134            assert!(
135                in_alternation,
136                "SUPPORTED_FILE_EXT_PATTERN is missing extension '{ext}' from EXTENSION_MAP; \
137                 add it to schema_helpers.rs"
138            );
139        }
140    }
141
142    #[test]
143    fn test_language_for_extension_edge_case() {
144        assert_eq!(language_for_extension("unknown"), None);
145        assert_eq!(language_for_extension(""), None);
146        assert_eq!(language_for_extension("RS"), Some("rust"));
147        // Uppercase Fortran extensions resolved via eq_ignore_ascii_case
148        assert_eq!(language_for_extension("F90"), Some("fortran"));
149        assert_eq!(language_for_extension("FOR"), Some("fortran"));
150    }
151}