1use std::collections::HashMap;
8use std::sync::LazyLock;
9
10pub struct LinguistResolver {
12 alias_map: &'static HashMap<&'static str, &'static str>,
14}
15
16impl LinguistResolver {
17 pub fn new() -> Self {
19 Self {
20 alias_map: &LANGUAGE_ALIASES,
21 }
22 }
23
24 pub fn resolve(&self, language: &str) -> String {
29 let lower = language.to_lowercase();
30 self.alias_map
31 .get(lower.as_str())
32 .map(|&s| s.to_string())
33 .unwrap_or(lower)
34 }
35
36 pub fn is_known(&self, language: &str) -> bool {
38 let lower = language.to_lowercase();
39 self.alias_map.contains_key(lower.as_str())
40 }
41}
42
43impl Default for LinguistResolver {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49static LANGUAGE_ALIASES: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
59 let mut m = HashMap::new();
60
61 m.insert("python", "python");
63 m.insert("py", "python");
64 m.insert("python3", "python");
65 m.insert("py3", "python");
66 m.insert("pyw", "python");
67
68 m.insert("javascript", "javascript");
70 m.insert("js", "javascript");
71 m.insert("node", "javascript");
72 m.insert("nodejs", "javascript");
73 m.insert("mjs", "javascript");
74 m.insert("cjs", "javascript");
75
76 m.insert("typescript", "typescript");
78 m.insert("ts", "typescript");
79 m.insert("mts", "typescript");
80 m.insert("cts", "typescript");
81
82 m.insert("shell", "shell");
84 m.insert("bash", "shell");
85 m.insert("sh", "shell");
86 m.insert("zsh", "shell");
87 m.insert("ksh", "shell");
88 m.insert("fish", "shell");
89 m.insert("shellscript", "shell");
90 m.insert("shell-script", "shell");
91
92 m.insert("rust", "rust");
94 m.insert("rs", "rust");
95
96 m.insert("go", "go");
98 m.insert("golang", "go");
99
100 m.insert("ruby", "ruby");
102 m.insert("rb", "ruby");
103 m.insert("jruby", "ruby");
104
105 m.insert("java", "java");
107
108 m.insert("kotlin", "kotlin");
110 m.insert("kt", "kotlin");
111 m.insert("kts", "kotlin");
112
113 m.insert("scala", "scala");
115
116 m.insert("c", "c");
118 m.insert("h", "c");
119
120 m.insert("c++", "cpp");
122 m.insert("cpp", "cpp");
123 m.insert("cxx", "cpp");
124 m.insert("cc", "cpp");
125 m.insert("hpp", "cpp");
126 m.insert("hxx", "cpp");
127
128 m.insert("c#", "csharp");
130 m.insert("csharp", "csharp");
131 m.insert("cs", "csharp");
132
133 m.insert("f#", "fsharp");
135 m.insert("fsharp", "fsharp");
136 m.insert("fs", "fsharp");
137
138 m.insert("swift", "swift");
140
141 m.insert("objective-c", "objective-c");
143 m.insert("objc", "objective-c");
144 m.insert("obj-c", "objective-c");
145
146 m.insert("php", "php");
148
149 m.insert("perl", "perl");
151 m.insert("pl", "perl");
152
153 m.insert("r", "r");
155
156 m.insert("lua", "lua");
158
159 m.insert("haskell", "haskell");
161 m.insert("hs", "haskell");
162
163 m.insert("elixir", "elixir");
165 m.insert("ex", "elixir");
166 m.insert("exs", "elixir");
167
168 m.insert("erlang", "erlang");
170 m.insert("erl", "erlang");
171
172 m.insert("clojure", "clojure");
174 m.insert("clj", "clojure");
175 m.insert("cljs", "clojure");
176 m.insert("cljc", "clojure");
177
178 m.insert("html", "html");
180 m.insert("htm", "html");
181 m.insert("xhtml", "html");
182
183 m.insert("css", "css");
185
186 m.insert("scss", "scss");
188 m.insert("sass", "sass");
189
190 m.insert("less", "less");
192
193 m.insert("json", "json");
195 m.insert("jsonc", "json");
196 m.insert("json5", "json");
197
198 m.insert("yaml", "yaml");
200 m.insert("yml", "yaml");
201
202 m.insert("toml", "toml");
204
205 m.insert("xml", "xml");
207 m.insert("xsd", "xml");
208 m.insert("xsl", "xml");
209 m.insert("xslt", "xml");
210
211 m.insert("markdown", "markdown");
213 m.insert("md", "markdown");
214 m.insert("mkd", "markdown");
215 m.insert("mdx", "markdown");
216
217 m.insert("sql", "sql");
219 m.insert("mysql", "sql");
220 m.insert("postgresql", "sql");
221 m.insert("postgres", "sql");
222 m.insert("sqlite", "sql");
223 m.insert("plsql", "sql");
224 m.insert("tsql", "sql");
225
226 m.insert("graphql", "graphql");
228 m.insert("gql", "graphql");
229
230 m.insert("protobuf", "protobuf");
232 m.insert("proto", "protobuf");
233
234 m.insert("terraform", "terraform");
236 m.insert("tf", "terraform");
237 m.insert("hcl", "hcl");
238
239 m.insert("dockerfile", "dockerfile");
241 m.insert("docker", "dockerfile");
242
243 m.insert("makefile", "makefile");
245 m.insert("make", "makefile");
246
247 m.insert("nix", "nix");
249
250 m.insert("vim", "vim");
252 m.insert("viml", "vim");
253 m.insert("vimscript", "vim");
254
255 m.insert("zig", "zig");
257
258 m.insert("nim", "nim");
260
261 m.insert("julia", "julia");
263 m.insert("jl", "julia");
264
265 m.insert("ocaml", "ocaml");
267 m.insert("ml", "ocaml");
268
269 m.insert("reason", "reason");
271 m.insert("re", "reason");
272
273 m.insert("dart", "dart");
275
276 m.insert("v", "v");
278 m.insert("vlang", "v");
279
280 m.insert("awk", "awk");
282 m.insert("gawk", "awk");
283
284 m.insert("sed", "sed");
286
287 m.insert("powershell", "powershell");
289 m.insert("pwsh", "powershell");
290 m.insert("ps1", "powershell");
291
292 m.insert("batch", "batch");
294 m.insert("bat", "batch");
295 m.insert("cmd", "batch");
296
297 m.insert("diff", "diff");
299 m.insert("patch", "diff");
300
301 m.insert("ini", "ini");
303 m.insert("cfg", "ini");
304 m.insert("conf", "ini");
305
306 m.insert("applescript", "applescript");
308
309 m.insert("groovy", "groovy");
311
312 m.insert("latex", "latex");
314 m.insert("tex", "latex");
315
316 m.insert("text", "text");
318 m.insert("txt", "text");
319 m.insert("plaintext", "text");
320 m.insert("plain", "text");
321
322 m
323});
324
325#[cfg(test)]
326mod bridge_to_main_linguist_table {
327 use super::LANGUAGE_ALIASES;
328 use std::collections::BTreeMap;
329
330 const DELIBERATE_DIVERGENCES: &[(&str, &str, &str)] = &[
341 ("c#", "csharp", "c#"),
343 ("csharp", "csharp", "c#"),
344 ("c++", "cpp", "c++"),
345 ("cpp", "cpp", "c++"),
346 ("f#", "fsharp", "f#"),
347 ("fsharp", "fsharp", "f#"),
348 ("bat", "batch", "batchfile"),
350 ("batch", "batch", "batchfile"),
351 ("proto", "protobuf", "protocol buffer"),
352 ("protobuf", "protobuf", "protocol buffer"),
353 ("vim", "vim", "vim script"),
354 ("viml", "vim", "vim script"),
355 ("vimscript", "vim", "vim script"),
356 ("json5", "json", "json5"),
358 ("plsql", "sql", "plsql"),
359 ("tsql", "sql", "tsql"),
360 ("mdx", "markdown", "mdx"),
361 ("xsl", "xml", "xslt"),
362 ("xslt", "xml", "xslt"),
363 ("latex", "latex", "tex"),
364 ("tex", "latex", "tex"),
365 ("terraform", "terraform", "hcl"),
367 ];
368
369 fn deliberate() -> BTreeMap<&'static str, (&'static str, &'static str)> {
370 DELIBERATE_DIVERGENCES
371 .iter()
372 .map(|(alias, ours, theirs)| (*alias, (*ours, *theirs)))
373 .collect()
374 }
375
376 #[test]
378 fn a_divergence_from_the_main_linguist_table_is_deliberate() {
379 let deliberate = deliberate();
380 let mut unlisted = Vec::new();
381
382 for (alias, ours) in LANGUAGE_ALIASES.iter() {
383 let Some(theirs) = crate::linguist_data::resolve_canonical(alias) else {
384 continue;
385 };
386 let theirs = theirs.to_lowercase();
387 if *ours == theirs {
388 continue;
389 }
390 match deliberate.get(alias) {
391 Some((listed_ours, listed_theirs)) if *listed_ours == *ours && *listed_theirs == theirs => {}
392 _ => unlisted.push(format!("(\"{alias}\", \"{ours}\", \"{theirs}\")")),
393 }
394 }
395
396 unlisted.sort();
397 assert!(
398 unlisted.is_empty(),
399 "these fence tags resolve to different languages in the two tables; add them to \
400 DELIBERATE_DIVERGENCES with a reason, or make the tables agree:\n{}",
401 unlisted.join("\n")
402 );
403 }
404
405 #[test]
409 fn a_listed_divergence_has_not_gone_stale() {
410 let mut stale = Vec::new();
411
412 for (alias, ours, theirs) in DELIBERATE_DIVERGENCES {
413 let Some(actual_ours) = LANGUAGE_ALIASES.get(alias) else {
414 stale.push(format!("{alias}: this table no longer knows it"));
415 continue;
416 };
417 let Some(actual_theirs) = crate::linguist_data::resolve_canonical(alias) else {
418 stale.push(format!("{alias}: the main table no longer knows it"));
419 continue;
420 };
421 let actual_theirs = actual_theirs.to_lowercase();
422 if *actual_ours != *ours || actual_theirs != *theirs {
423 stale.push(format!(
424 "{alias}: listed as {ours}/{theirs}, actually {actual_ours}/{actual_theirs}"
425 ));
426 }
427 }
428
429 assert!(
430 stale.is_empty(),
431 "DELIBERATE_DIVERGENCES is out of date:\n{}",
432 stale.join("\n")
433 );
434 }
435
436 #[test]
444 fn every_language_this_table_resolves_to_is_one_rumdl_knows() {
445 let mut unknown: Vec<&str> = LANGUAGE_ALIASES
446 .values()
447 .filter(|canonical| crate::linguist_data::resolve_canonical(canonical).is_none())
448 .copied()
449 .collect();
450 unknown.sort_unstable();
451 unknown.dedup();
452 assert!(
453 unknown.is_empty(),
454 "languages known only to code-block-tools: {}",
455 unknown.join(", ")
456 );
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463
464 #[test]
465 fn test_resolve_known_alias() {
466 let resolver = LinguistResolver::new();
467
468 assert_eq!(resolver.resolve("py"), "python");
470 assert_eq!(resolver.resolve("python3"), "python");
471 assert_eq!(resolver.resolve("Python"), "python");
472 assert_eq!(resolver.resolve("PY"), "python");
473
474 assert_eq!(resolver.resolve("bash"), "shell");
476 assert_eq!(resolver.resolve("sh"), "shell");
477 assert_eq!(resolver.resolve("zsh"), "shell");
478
479 assert_eq!(resolver.resolve("js"), "javascript");
481 assert_eq!(resolver.resolve("node"), "javascript");
482
483 assert_eq!(resolver.resolve("rs"), "rust");
485 assert_eq!(resolver.resolve("Rust"), "rust");
486 }
487
488 #[test]
489 fn test_resolve_unknown_language() {
490 let resolver = LinguistResolver::new();
491
492 assert_eq!(resolver.resolve("UnknownLang"), "unknownlang");
494 assert_eq!(resolver.resolve("CUSTOM"), "custom");
495 }
496
497 #[test]
498 fn test_resolve_canonical_name() {
499 let resolver = LinguistResolver::new();
500
501 assert_eq!(resolver.resolve("python"), "python");
503 assert_eq!(resolver.resolve("javascript"), "javascript");
504 assert_eq!(resolver.resolve("rust"), "rust");
505 }
506
507 #[test]
508 fn test_is_known() {
509 let resolver = LinguistResolver::new();
510
511 assert!(resolver.is_known("python"));
512 assert!(resolver.is_known("py"));
513 assert!(resolver.is_known("bash"));
514 assert!(resolver.is_known("JavaScript"));
515
516 assert!(!resolver.is_known("unknownlang"));
517 assert!(!resolver.is_known("customformat"));
518 }
519
520 #[test]
521 fn test_case_insensitivity() {
522 let resolver = LinguistResolver::new();
523
524 assert_eq!(resolver.resolve("PYTHON"), "python");
525 assert_eq!(resolver.resolve("Python"), "python");
526 assert_eq!(resolver.resolve("pYtHoN"), "python");
527 assert_eq!(resolver.resolve("JAVASCRIPT"), "javascript");
528 assert_eq!(resolver.resolve("JavaScript"), "javascript");
529 }
530}