1use std::path::Path;
10
11fn language_from_filename(path: &Path) -> Option<String> {
14 let name = path.file_name()?.to_str()?.to_ascii_lowercase();
15 let language = if name == "dockerfile" || name.starts_with("dockerfile.")
18 || name == "containerfile" || name.starts_with("containerfile.")
19 {
20 "dockerfile"
21 } else if name == "makefile" || name == "gnumakefile" {
22 "make"
23 } else {
24 return None;
25 };
26 Some(language.to_string())
27}
28
29pub fn language_from_path(path: &Path) -> Option<String> {
32 language_from_filename(path).or_else(|| language_from_extension(path))
33}
34
35pub fn language_from_extension(path: &Path) -> Option<String> {
37 let ext = path.extension()?.to_str()?.to_ascii_lowercase();
38 let language = match ext.as_str() {
39 "md" | "markdown" => "markdown",
40 "py" | "pyi" | "pyw" => "python",
41 "rb" => "ruby",
42 "sh" | "bash" | "zsh" => "sh",
43 "fish" => "fish",
44 "ps1" | "psm1" => "powershell",
45 "js" | "mjs" | "cjs" | "jsx" => "javascript",
46 "ts" | "mts" | "cts" | "tsx" => "typescript",
47 "c" | "h" => "c",
48 "cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => "cpp",
49 "cs" => "cs",
50 "rs" => "rust",
51 "go" => "go",
52 "java" => "java",
53 "kt" | "kts" => "kotlin",
54 "swift" => "swift",
55 "dart" => "dart",
56 "zig" => "zig",
57 "lua" => "lua",
58 "vim" => "vim",
59 "nix" => "nix",
60 "php" => "php",
61 "pl" | "pm" => "perl",
62 "r" => "r",
63 "jl" => "julia",
64 "hs" => "haskell",
65 "ex" | "exs" => "elixir",
66 "erl" | "hrl" => "erlang",
67 "clj" | "cljs" | "cljc" | "edn" => "clojure",
68 "sql" => "sql",
69 "html" | "htm" => "html",
70 "vue" => "vue",
71 "svelte" => "svelte",
72 "erb" | "html.erb" => "erb",
73 "hbs" | "handlebars" => "handlebars",
74 "css" => "css",
75 "scss" => "scss",
76 "json" | "jsonc" => "json",
77 "yaml" | "yml" => "yaml",
78 "toml" => "toml",
79 "ini" | "cfg" | "conf" | "properties" => "ini",
80 "xml" => "xml",
81 "txt" | "text" | "log" => "text",
82 _ => return None,
83 };
84 Some(language.to_string())
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 fn lang(name: &str) -> Option<String> {
92 language_from_extension(Path::new(name))
93 }
94
95 #[test]
96 fn common_extensions() {
97 assert_eq!(lang("README.md").as_deref(), Some("markdown"));
98 assert_eq!(lang("app.py").as_deref(), Some("python"));
99 assert_eq!(lang("run.sh").as_deref(), Some("sh"));
100 assert_eq!(lang("main.rs").as_deref(), Some("rust"));
101 assert_eq!(lang("conf.ini").as_deref(), Some("ini"));
102 assert_eq!(lang("x.toml").as_deref(), Some("toml"));
103 }
104
105 #[test]
106 fn case_and_paths() {
107 assert_eq!(lang("/a/b/NOTES.MD").as_deref(), Some("markdown"));
108 assert_eq!(lang("archive.TAR.GZ"), None);
109 }
110
111 #[test]
112 fn unknown_extensions() {
113 assert_eq!(lang("Makefile"), None);
114 assert_eq!(lang("data.bin"), None);
115 assert_eq!(lang("no_extension"), None);
116 }
117}
118
119#[cfg(test)]
120mod filename_tests {
121 use super::*;
122
123 #[test]
124 fn extensionless_filenames() {
125 assert_eq!(
126 language_from_path(Path::new("docker/Dockerfile")).as_deref(),
127 Some("dockerfile")
128 );
129 assert_eq!(
130 language_from_path(Path::new("deploy/Containerfile.prod")).as_deref(),
131 Some("dockerfile")
132 );
133 assert_eq!(
134 language_from_path(Path::new("Makefile")).as_deref(),
135 Some("make")
136 );
137 assert_eq!(language_from_path(Path::new("src/main.rs")).as_deref(), Some("rust"));
138 }
139
140 #[test]
141 fn case_insensitive_names() {
142 assert_eq!(
143 language_from_path(Path::new("docker/dockerfile")).as_deref(),
144 Some("dockerfile")
145 );
146 }
147}