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}