use std::path::Path;
fn language_from_filename(path: &Path) -> Option<String> {
let name = path.file_name()?.to_str()?.to_ascii_lowercase();
let language = if name == "dockerfile" || name.starts_with("dockerfile.")
|| name == "containerfile" || name.starts_with("containerfile.")
{
"dockerfile"
} else if name == "makefile" || name == "gnumakefile" {
"make"
} else {
return None;
};
Some(language.to_string())
}
pub fn language_from_path(path: &Path) -> Option<String> {
language_from_filename(path).or_else(|| language_from_extension(path))
}
pub fn language_from_extension(path: &Path) -> Option<String> {
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
let language = match ext.as_str() {
"md" | "markdown" => "markdown",
"py" | "pyi" | "pyw" => "python",
"rb" => "ruby",
"sh" | "bash" | "zsh" => "sh",
"fish" => "fish",
"ps1" | "psm1" => "powershell",
"js" | "mjs" | "cjs" | "jsx" => "javascript",
"ts" | "mts" | "cts" | "tsx" => "typescript",
"c" | "h" => "c",
"cpp" | "cc" | "cxx" | "hpp" | "hh" | "hxx" => "cpp",
"cs" => "cs",
"rs" => "rust",
"go" => "go",
"java" => "java",
"kt" | "kts" => "kotlin",
"swift" => "swift",
"dart" => "dart",
"zig" => "zig",
"lua" => "lua",
"vim" => "vim",
"nix" => "nix",
"php" => "php",
"pl" | "pm" => "perl",
"r" => "r",
"jl" => "julia",
"hs" => "haskell",
"ex" | "exs" => "elixir",
"erl" | "hrl" => "erlang",
"clj" | "cljs" | "cljc" | "edn" => "clojure",
"sql" => "sql",
"html" | "htm" => "html",
"vue" => "vue",
"svelte" => "svelte",
"erb" | "html.erb" => "erb",
"hbs" | "handlebars" => "handlebars",
"css" => "css",
"scss" => "scss",
"json" | "jsonc" => "json",
"yaml" | "yml" => "yaml",
"toml" => "toml",
"ini" | "cfg" | "conf" | "properties" => "ini",
"xml" => "xml",
"txt" | "text" | "log" => "text",
_ => return None,
};
Some(language.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn lang(name: &str) -> Option<String> {
language_from_extension(Path::new(name))
}
#[test]
fn common_extensions() {
assert_eq!(lang("README.md").as_deref(), Some("markdown"));
assert_eq!(lang("app.py").as_deref(), Some("python"));
assert_eq!(lang("run.sh").as_deref(), Some("sh"));
assert_eq!(lang("main.rs").as_deref(), Some("rust"));
assert_eq!(lang("conf.ini").as_deref(), Some("ini"));
assert_eq!(lang("x.toml").as_deref(), Some("toml"));
}
#[test]
fn case_and_paths() {
assert_eq!(lang("/a/b/NOTES.MD").as_deref(), Some("markdown"));
assert_eq!(lang("archive.TAR.GZ"), None);
}
#[test]
fn unknown_extensions() {
assert_eq!(lang("Makefile"), None);
assert_eq!(lang("data.bin"), None);
assert_eq!(lang("no_extension"), None);
}
}
#[cfg(test)]
mod filename_tests {
use super::*;
#[test]
fn extensionless_filenames() {
assert_eq!(
language_from_path(Path::new("docker/Dockerfile")).as_deref(),
Some("dockerfile")
);
assert_eq!(
language_from_path(Path::new("deploy/Containerfile.prod")).as_deref(),
Some("dockerfile")
);
assert_eq!(
language_from_path(Path::new("Makefile")).as_deref(),
Some("make")
);
assert_eq!(language_from_path(Path::new("src/main.rs")).as_deref(), Some("rust"));
}
#[test]
fn case_insensitive_names() {
assert_eq!(
language_from_path(Path::new("docker/dockerfile")).as_deref(),
Some("dockerfile")
);
}
}