1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[non_exhaustive]
6pub enum LanguageHint<'a> {
7 Id(&'a str),
8 InfoString(&'a str),
9 Path(&'a str),
10 Auto,
11}
12
13impl<'a> From<&'a str> for LanguageHint<'a> {
14 fn from(value: &'a str) -> Self {
15 Self::Id(value)
16 }
17}
18
19impl<'a> LanguageHint<'a> {
20 #[must_use]
22 pub fn as_str(&self) -> &'a str {
23 match self {
24 Self::Id(value) | Self::Path(value) => value,
25 Self::InfoString(value) => value.split_ascii_whitespace().next().unwrap_or_default(),
26 Self::Auto => "",
27 }
28 }
29}
30
31pub fn resolve_language<'a>(
33 hint: impl Into<LanguageHint<'a>>,
34 source: &str,
35) -> Option<&'static str> {
36 let hint = hint.into().as_str();
37 let normalized = hint.trim().replace('\\', "/").to_ascii_lowercase();
38 if normalized.is_empty() {
39 return shebang_id(source);
40 }
41
42 let simple = normalized.rsplit('/').next().unwrap_or(&normalized);
43 canonical_id(&normalized)
44 .or_else(|| canonical_id(simple))
45 .or_else(|| extension_id(simple))
46 .or_else(|| special_file(simple))
47 .or_else(|| arborium::detect_language(&normalized).and_then(canonical_id))
48 .or_else(|| shebang_id(source))
49}
50
51fn canonical_id(hint: &str) -> Option<&'static str> {
52 Some(match hint {
53 "rust" | "rs" => "rust",
54 "javascript" | "js" | "mjs" | "cjs" | "node" | "jsx" => "javascript",
55 "typescript" | "ts" | "mts" | "cts" => "typescript",
56 "tsx" => "tsx",
57 "python" | "py" | "python3" => "python",
58 "bash" | "sh" | "shell" | "zsh" => "bash",
59 "c" | "h" => "c",
60 "csharp" | "c-sharp" | "c#" | "cs" => "c-sharp",
61 "cpp" | "c++" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => "cpp",
62 "go" | "golang" => "go",
63 "java" => "java",
64 "kotlin" | "kt" | "kts" => "kotlin",
65 "ruby" | "rb" => "ruby",
66 "swift" => "swift",
67 "php" => "php",
68 "sql" => "sql",
69 "lua" => "lua",
70 "dockerfile" => "dockerfile",
71 "json" | "jsonc" => "json",
72 "toml" => "toml",
73 "yaml" | "yml" => "yaml",
74 "html" | "htm" => "html",
75 "css" => "css",
76 "markdown" | "md" => "markdown",
77 "zig" => "zig",
78 "nix" => "nix",
79 "haskell" | "hs" => "haskell",
80 "elixir" | "ex" | "exs" => "elixir",
81 "erlang" | "erl" | "hrl" => "erlang",
82 "scala" | "sc" => "scala",
83 "clojure" | "clj" | "cljs" | "cljc" | "edn" => "clojure",
84 "commonlisp" | "common-lisp" | "lisp" | "cl" => "commonlisp",
85 "scheme" | "scm" | "ss" => "scheme",
86 "ocaml" | "ml" | "mli" => "ocaml",
87 "fsharp" | "f#" | "fs" | "fsi" | "fsx" => "fsharp",
88 "dart" => "dart",
89 "powershell" | "pwsh" | "ps1" | "psm1" => "powershell",
90 "fish" => "fish",
91 "make" | "makefile" => "make",
92 "cmake" => "cmake",
93 "ninja" => "ninja",
94 "meson" => "meson",
95 "just" | "justfile" => "just",
96 "hcl" | "terraform" | "tf" | "tfvars" => "hcl",
97 "graphql" | "gql" => "graphql",
98 "protobuf" | "proto" => "proto",
99 "xml" | "xhtml" | "svg" => "xml",
100 "vue" => "vue",
101 "svelte" => "svelte",
102 "scss" => "scss",
103 "asm" | "assembly" => "asm",
104 "x86asm" | "x86-asm" | "nasm" => "x86asm",
105 "objective-c" | "objectivec" | "objc" => "objc",
106 "perl" | "pl" | "pm" => "perl",
107 "r" => "r",
108 "solidity" | "sol" => "solidity",
109 "starlark" | "bzl" | "bazel" => "starlark",
110 "rego" => "rego",
111 "ini" | "cfg" => "ini",
112 "diff" | "patch" => "diff",
113 _ => return None,
114 })
115}
116
117fn extension_id(file: &str) -> Option<&'static str> {
118 let extension = file.rsplit_once('.')?.1;
119 canonical_id(extension).or(match extension {
120 "s" => Some("asm"),
123 "m" | "mm" => Some("objc"),
124 _ => None,
125 })
126}
127
128fn special_file(file: &str) -> Option<&'static str> {
129 match file {
130 "dockerfile" | "containerfile" => Some("dockerfile"),
131 "go.mod" | "go.sum" => Some("go"),
132 "makefile" | "gnumakefile" => Some("make"),
133 "cmakelists.txt" => Some("cmake"),
134 "build.ninja" => Some("ninja"),
135 "meson.build" | "meson_options.txt" => Some("meson"),
136 "justfile" => Some("just"),
137 "flake.nix" => Some("nix"),
138 ".terraformrc" => Some("hcl"),
139 "workspace" => Some("starlark"),
140 "build.sbt" => Some("scala"),
141 "deps.edn" => Some("clojure"),
142 ".zshrc" if cfg!(feature = "agent-languages") => Some("zsh"),
143 ".bashrc" | ".zshrc" => Some("bash"),
144 _ => None,
145 }
146}
147
148fn shebang_id(source: &str) -> Option<&'static str> {
149 let line = source.lines().next()?;
150 if !line.starts_with("#!") {
151 return None;
152 }
153 let lower = line.to_ascii_lowercase();
154 if lower.contains("python") {
155 Some("python")
156 } else if lower.contains("node") {
157 Some("javascript")
158 } else if lower.contains("bash") || lower.contains("/sh") || lower.contains("zsh") {
159 Some("bash")
160 } else if lower.contains("fish") {
161 Some("fish")
162 } else if lower.contains("pwsh") || lower.contains("powershell") {
163 Some("powershell")
164 } else {
165 None
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn resolves_aliases_paths_special_files_and_shebangs() {
175 for (hint, source, expected) in [
176 ("RUST", "", Some("rust")),
177 (".rs", "", Some("rust")),
178 ("src/lib.rs", "", Some("rust")),
179 ("src\\lib.rs", "", Some("rust")),
180 ("view.tsx", "", Some("tsx")),
181 ("x.d.ts", "", Some("typescript")),
182 ("x.d.mts", "", Some("typescript")),
183 ("x.d.cts", "", Some("typescript")),
184 ("foo.jsx", "", Some("javascript")),
185 ("foo.jsonc", "", Some("json")),
186 ("foo.yml", "", Some("yaml")),
187 ("Dockerfile", "", Some("dockerfile")),
188 ("Containerfile", "", Some("dockerfile")),
189 ("Program.cs", "", Some("c-sharp")),
190 ("Main.java", "", Some("java")),
191 ("Main.kt", "", Some("kotlin")),
192 ("script.rb", "", Some("ruby")),
193 ("query.sql", "", Some("sql")),
194 (".bashrc", "", Some("bash")),
195 (
196 ".zshrc",
197 "",
198 Some(if cfg!(feature = "agent-languages") {
199 "zsh"
200 } else {
201 "bash"
202 }),
203 ),
204 ("go.mod", "", Some("go")),
205 ("", "#!/usr/bin/env python3\n", Some("python")),
206 ("", "#!/usr/bin/env node\n", Some("javascript")),
207 ("unknown.bin", "bytes", None),
208 ("markdown-inline", "", None),
209 ] {
210 assert_eq!(resolve_language(hint, source), expected, "{hint}");
211 }
212 }
213}