Skip to main content

_diffctx/
languages.rs

1use once_cell::sync::Lazy;
2use rustc_hash::FxHashMap;
3use std::path::Path;
4
5pub static EXTENSION_TO_LANGUAGE: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::new(|| {
6    let entries: &[(&str, &str)] = &[
7        (".py", "python"),
8        (".pyw", "python"),
9        (".pyi", "python"),
10        (".js", "javascript"),
11        (".mjs", "javascript"),
12        (".cjs", "javascript"),
13        (".jsx", "jsx"),
14        (".ts", "typescript"),
15        (".tsx", "tsx"),
16        (".mts", "typescript"),
17        (".cts", "typescript"),
18        (".json", "json"),
19        (".yaml", "yaml"),
20        (".yml", "yaml"),
21        (".toml", "toml"),
22        (".md", "markdown"),
23        (".markdown", "markdown"),
24        (".mdx", "markdown"),
25        (".html", "html"),
26        (".htm", "html"),
27        (".css", "css"),
28        (".scss", "scss"),
29        (".less", "less"),
30        (".xml", "xml"),
31        (".svg", "xml"),
32        (".sh", "bash"),
33        (".bash", "bash"),
34        (".zsh", "zsh"),
35        (".fish", "fish"),
36        (".ksh", "bash"),
37        (".ps1", "powershell"),
38        (".psm1", "powershell"),
39        (".psd1", "powershell"),
40        (".bat", "batch"),
41        (".cmd", "batch"),
42        (".c", "c"),
43        (".h", "c"),
44        (".cpp", "cpp"),
45        (".cc", "cpp"),
46        (".cxx", "cpp"),
47        (".hpp", "cpp"),
48        (".hh", "cpp"),
49        (".hxx", "cpp"),
50        (".cs", "csharp"),
51        (".fs", "fsharp"),
52        (".fsi", "fsharp"),
53        (".fsx", "fsharp"),
54        (".java", "java"),
55        (".kt", "kotlin"),
56        (".kts", "kotlin"),
57        (".scala", "scala"),
58        (".sc", "scala"),
59        (".go", "go"),
60        (".rs", "rust"),
61        (".rb", "ruby"),
62        (".rake", "ruby"),
63        (".gemspec", "ruby"),
64        (".php", "php"),
65        (".swift", "swift"),
66        (".m", "objectivec"),
67        (".mm", "objectivec"),
68        (".r", "r"),
69        (".lua", "lua"),
70        (".pl", "perl"),
71        (".pm", "perl"),
72        (".ex", "elixir"),
73        (".exs", "elixir"),
74        (".erl", "erlang"),
75        (".hrl", "erlang"),
76        (".hs", "haskell"),
77        (".lhs", "haskell"),
78        (".ml", "ocaml"),
79        (".mli", "ocaml"),
80        (".clj", "clojure"),
81        (".cljs", "clojure"),
82        (".cljc", "clojure"),
83        (".sql", "sql"),
84        (".graphql", "graphql"),
85        (".gql", "graphql"),
86        (".proto", "protobuf"),
87        (".dockerfile", "dockerfile"),
88        (".tf", "terraform"),
89        (".hcl", "hcl"),
90        (".vim", "vim"),
91        (".el", "elisp"),
92        (".lisp", "lisp"),
93        (".scm", "scheme"),
94        (".rkt", "racket"),
95        (".zig", "zig"),
96        (".nim", "nim"),
97        (".v", "v"),
98        (".sv", "systemverilog"),
99        (".vhd", "vhdl"),
100        (".vhdl", "vhdl"),
101        (".d", "d"),
102        (".dart", "dart"),
103        (".groovy", "groovy"),
104        (".gradle", "groovy"),
105        (".jl", "julia"),
106        (".ini", "ini"),
107        (".cfg", "ini"),
108        (".conf", "ini"),
109        (".properties", "properties"),
110        (".env", "dotenv"),
111        (".ada", "ada"),
112        (".pas", "pascal"),
113        (".f90", "fortran"),
114        (".f95", "fortran"),
115        (".cob", "cobol"),
116        (".asm", "asm"),
117        (".s", "asm"),
118        (".c++", "cpp"),
119        (".h++", "cpp"),
120        (".ipp", "cpp"),
121        (".tpp", "cpp"),
122        (".phtml", "php"),
123        (".php3", "php"),
124        (".php4", "php"),
125        (".php5", "php"),
126        (".php7", "php"),
127        (".phps", "php"),
128        (".adoc", "asciidoc"),
129        (".editorconfig", "editorconfig"),
130        (".tex", "latex"),
131        (".latex", "latex"),
132        (".rst", "rst"),
133        (".txt", "text"),
134        (".log", "text"),
135        (".diff", "diff"),
136        (".patch", "diff"),
137        (".vue", "vue"),
138        (".svelte", "svelte"),
139        (".sty", "latex"),
140        (".cls", "latex"),
141        (".bst", "latex"),
142        (".dtx", "latex"),
143        (".bib", "bibtex"),
144        (".nix", "nix"),
145        (".prisma", "prisma"),
146        (".bzl", "bazel"),
147        (".j2", "jinja"),
148        (".jinja", "jinja"),
149        (".jinja2", "jinja"),
150        (".cmake", "cmake"),
151        (".mk", "make"),
152    ];
153
154    let mut map = FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
155    for &(ext, lang) in entries {
156        map.insert(ext, lang);
157    }
158    map
159});
160
161pub static FILENAME_TO_LANGUAGE: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::new(|| {
162    let entries: &[(&str, &str)] = &[
163        ("makefile", "makefile"),
164        ("gnumakefile", "makefile"),
165        ("dockerfile", "dockerfile"),
166        ("containerfile", "dockerfile"),
167        ("vagrantfile", "ruby"),
168        ("gemfile", "ruby"),
169        ("rakefile", "ruby"),
170        ("guardfile", "ruby"),
171        ("brewfile", "ruby"),
172        ("podfile", "ruby"),
173        ("cmakelists.txt", "cmake"),
174        ("justfile", "just"),
175        (".bashrc", "bash"),
176        (".bash_profile", "bash"),
177        (".bash_aliases", "bash"),
178        (".zshrc", "zsh"),
179        (".zshenv", "zsh"),
180        (".zprofile", "zsh"),
181        (".profile", "bash"),
182        (".gitconfig", "gitconfig"),
183        (".gitattributes", "gitattributes"),
184        (".gitignore", "gitignore"),
185        (".dockerignore", "gitignore"),
186        (".diffctxignore", "gitignore"),
187        (".npmrc", "ini"),
188        (".yarnrc", "yaml"),
189        (".prettierrc", "json"),
190        (".eslintrc", "json"),
191        ("package.json", "json"),
192        ("tsconfig.json", "json"),
193        ("composer.json", "json"),
194        ("cargo.toml", "toml"),
195        ("pyproject.toml", "toml"),
196        ("go.mod", "gomod"),
197        ("go.sum", "gosum"),
198        ("requirements.txt", "text"),
199        ("pipfile", "toml"),
200        ("procfile", "text"),
201        ("jenkinsfile", "groovy"),
202        ("build", "bazel"),
203        ("build.bazel", "bazel"),
204        ("workspace", "bazel"),
205        ("workspace.bazel", "bazel"),
206        ("flake.lock", "json"),
207    ];
208
209    let mut map = FxHashMap::with_capacity_and_hasher(entries.len(), Default::default());
210    for &(name, lang) in entries {
211        map.insert(name, lang);
212    }
213    map
214});
215
216pub fn get_language_for_file(path: &str) -> Option<&'static str> {
217    let p = Path::new(path);
218
219    if let Some(name) = p.file_name() {
220        let name_lower = name.to_string_lossy().to_lowercase();
221        if let Some(&lang) = FILENAME_TO_LANGUAGE.get(name_lower.as_str()) {
222            return Some(lang);
223        }
224    }
225
226    if let Some(ext) = p.extension() {
227        let ext_lower = format!(".{}", ext.to_string_lossy().to_lowercase());
228        if let Some(&lang) = EXTENSION_TO_LANGUAGE.get(ext_lower.as_str()) {
229            return Some(lang);
230        }
231    }
232
233    if let Some(name) = p.file_name() {
234        let name_lower = name.to_string_lossy().to_lowercase();
235        if name_lower.starts_with("dockerfile") {
236            return Some("dockerfile");
237        }
238    }
239
240    None
241}
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn known_extension_resolves_to_its_language() {
248        assert_eq!(get_language_for_file("foo.py"), Some("python"));
249        assert_eq!(get_language_for_file("src/lib.rs"), Some("rust"));
250    }
251
252    #[test]
253    fn extension_matching_is_case_insensitive() {
254        assert_eq!(get_language_for_file("FOO.PY"), Some("python"));
255        assert_eq!(get_language_for_file("Main.RS"), Some("rust"));
256    }
257
258    #[test]
259    fn filename_map_prefix_rule_catches_dockerfile_variants() {
260        assert_eq!(get_language_for_file("Dockerfile"), Some("dockerfile"));
261        assert_eq!(get_language_for_file("Dockerfile.prod"), Some("dockerfile"));
262        assert_eq!(get_language_for_file("dockerfile.dev"), Some("dockerfile"));
263    }
264
265    #[test]
266    fn exact_filename_map_hit_takes_priority_over_extension() {
267        assert_eq!(get_language_for_file("CMakeLists.txt"), Some("cmake"));
268        assert_eq!(get_language_for_file("Makefile"), Some("makefile"));
269    }
270
271    #[test]
272    fn unknown_extension_resolves_to_none() {
273        assert_eq!(get_language_for_file("foo.xyz123notreal"), None);
274        assert_eq!(get_language_for_file("no_extension_at_all"), None);
275    }
276
277    #[test]
278    fn mdx_matches_markdown_extensions_consistently() {
279        assert_eq!(get_language_for_file("component.mdx"), Some("markdown"));
280    }
281
282    #[test]
283    fn cmake_and_make_extensions_are_discoverable() {
284        assert_eq!(get_language_for_file("toolchain.cmake"), Some("cmake"));
285        assert_eq!(get_language_for_file("rules.mk"), Some("make"));
286    }
287
288    #[test]
289    fn extension_table_row_count_is_pinned() {
290        assert_eq!(
291            EXTENSION_TO_LANGUAGE.len(),
292            145,
293            "a row was added or removed from EXTENSION_TO_LANGUAGE; update this count \
294             deliberately and re-check get_language_for_file coverage"
295        );
296    }
297
298    #[test]
299    fn filename_table_row_count_is_pinned() {
300        assert_eq!(
301            FILENAME_TO_LANGUAGE.len(),
302            44,
303            "a row was added or removed from FILENAME_TO_LANGUAGE; update this count \
304             deliberately and re-check get_language_for_file coverage"
305        );
306    }
307}