#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FileType {
Json,
Javascript,
Typescript,
Html,
Css,
Csv,
Toml,
Dotenv,
Yaml,
Unknown,
}
pub(crate) fn determine_file_type(language_id: &str) -> FileType {
match language_id {
"csv" => FileType::Csv,
"toml" => FileType::Toml,
"dotenv" | "env" => FileType::Dotenv,
"javascript" | "javascriptreact" => FileType::Javascript,
"typescript" | "typescriptreact" => FileType::Typescript,
"json" | "jsonc" => FileType::Json,
"html" => FileType::Html,
"css" | "scss" | "less" => FileType::Css,
"yaml" => FileType::Yaml,
_ => FileType::Unknown,
}
}
pub(crate) const SUPPORTED_FORMATS: [&str; 10] = [
"csv",
"toml",
"dotenv",
"javascript",
"typescript",
"json",
"html",
"css",
"yaml",
"markdown",
];
pub(crate) const FALLBACK_FORMAT: &str = "unknown";
const ALIASES: [(&str, &str); 31] = [
("csv", "csv"),
("tsv", "csv"),
("toml", "toml"),
("dotenv", "dotenv"),
("env", "dotenv"),
("javascript", "javascript"),
("js", "javascript"),
("jsx", "javascript"),
("mjs", "javascript"),
("cjs", "javascript"),
("javascriptreact", "javascript"),
("typescript", "typescript"),
("ts", "typescript"),
("tsx", "typescript"),
("mts", "typescript"),
("cts", "typescript"),
("typescriptreact", "typescript"),
("json", "json"),
("jsonc", "json"),
("html", "html"),
("htm", "html"),
("xhtml", "html"),
("css", "css"),
("scss", "scss"),
("sass", "scss"),
("less", "less"),
("yaml", "yaml"),
("yml", "yaml"),
("markdown", "markdown"),
("md", "markdown"),
("xml", "xml"),
];
fn normalise(value: &str) -> String {
let trimmed = super::js::trim(value).to_lowercase();
trimmed.strip_prefix('.').unwrap_or(&trimmed).to_string()
}
fn alias(key: &str) -> Option<&'static str> {
ALIASES
.iter()
.find(|(from, _)| *from == key)
.map(|(_, to)| *to)
}
pub(crate) fn resolve_format(format: Option<&str>, filename: Option<&str>) -> &'static str {
if let Some(format) = format
&& let Some(direct) = alias(&normalise(format))
{
return direct;
}
let Some(filename) = filename else {
return FALLBACK_FORMAT;
};
let bare = normalise(filename);
if let Some(whole) = alias(bare.strip_prefix('.').unwrap_or(&bare)) {
return whole;
}
filename
.rsplit_once('.')
.and_then(|(_, extension)| alias(&normalise(extension)))
.unwrap_or(FALLBACK_FORMAT)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn language_ids_map_to_file_types() {
assert_eq!(determine_file_type("json"), FileType::Json);
assert_eq!(determine_file_type("jsonc"), FileType::Json);
assert_eq!(determine_file_type("typescriptreact"), FileType::Typescript);
assert_eq!(determine_file_type("scss"), FileType::Css);
assert_eq!(determine_file_type("env"), FileType::Dotenv);
assert_eq!(determine_file_type("yaml"), FileType::Yaml);
assert_eq!(determine_file_type("python"), FileType::Unknown);
}
#[test]
fn an_explicit_format_wins() {
assert_eq!(resolve_format(Some("tsx"), None), "typescript");
assert_eq!(resolve_format(Some(".TOML"), None), "toml");
assert_eq!(resolve_format(Some(" js "), None), "javascript");
}
#[test]
fn a_format_name_is_trimmed_with_javascripts_whitespace() {
assert_eq!(resolve_format(Some("\u{feff}json\u{feff}"), None), "json");
assert_eq!(
resolve_format(Some("\u{85}json"), None),
FALLBACK_FORMAT,
"U+0085 is not whitespace to JavaScript, so it is part of the name"
);
}
#[test]
fn a_filename_resolves_by_extension() {
assert_eq!(resolve_format(None, Some("tsconfig.json")), "json");
assert_eq!(resolve_format(None, Some("a/b/style.SCSS")), "scss");
}
#[test]
fn yaml_resolves_under_both_spellings() {
assert_eq!(resolve_format(None, Some("ci.yml")), "yaml");
assert_eq!(resolve_format(None, Some("deployment.yaml")), "yaml");
assert_eq!(resolve_format(Some("yml"), None), "yaml");
}
#[test]
fn a_dotfile_resolves_by_its_whole_name() {
assert_eq!(resolve_format(None, Some(".env")), "dotenv");
assert_eq!(resolve_format(None, Some("env")), "dotenv");
}
#[test]
fn an_unrecognised_format_falls_through_to_the_filename() {
assert_eq!(resolve_format(Some("python"), Some("a.json")), "json");
}
#[test]
fn nothing_recognisable_falls_back() {
for name in ["python", "rust", "", "jso"] {
assert_eq!(resolve_format(Some(name), None), FALLBACK_FORMAT, "{name}");
}
assert_eq!(resolve_format(None, Some("script.py")), FALLBACK_FORMAT);
assert_eq!(resolve_format(None, Some("Makefile")), FALLBACK_FORMAT);
assert_eq!(resolve_format(None, None), FALLBACK_FORMAT);
}
#[test]
fn the_fallback_name_round_trips() {
assert_eq!(determine_file_type(FALLBACK_FORMAT), FileType::Unknown);
}
#[test]
fn every_advertised_format_resolves_to_itself() {
for format in SUPPORTED_FORMATS {
assert_eq!(resolve_format(Some(format), None), format, "{format}");
}
}
#[test]
fn every_alias_lands_on_a_language_the_engine_dispatches() {
for (from, to) in ALIASES {
let generic = determine_file_type(to) == FileType::Unknown;
assert!(
!generic || matches!(to, "markdown" | "xml"),
"{from} -> {to} is neither typed nor a declared generic scan"
);
}
}
}