use std::collections::HashMap;
use std::sync::LazyLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CommentPattern {
pub single_line: &'static [&'static str],
pub multi_line_start: &'static [&'static str],
pub multi_line_end: &'static [&'static str],
pub doc_patterns: &'static [&'static str],
}
impl CommentPattern {
pub const EMPTY: Self = Self {
single_line: &[],
multi_line_start: &[],
multi_line_end: &[],
doc_patterns: &[],
};
const fn new(
single_line: &'static [&'static str],
multi_line_start: &'static [&'static str],
multi_line_end: &'static [&'static str],
doc_patterns: &'static [&'static str],
) -> Self {
Self {
single_line,
multi_line_start,
multi_line_end,
doc_patterns,
}
}
}
const C_STYLE: CommentPattern = CommentPattern::new(&["//"], &["/*"], &["*/"], &["/**", "/*!"]);
const JS_STYLE: CommentPattern = CommentPattern::new(&["//"], &["/*"], &["*/"], &["/**", "//!"]);
const JAVADOC_STYLE: CommentPattern = CommentPattern::new(&["//"], &["/*"], &["*/"], &["/**"]);
const TRIPLE_SLASH_STYLE: CommentPattern =
CommentPattern::new(&["//"], &["/*"], &["*/"], &["///", "/**"]);
const HASH_ONLY: CommentPattern = CommentPattern::new(&["#"], &[], &[], &["##"]);
const XML_STYLE: CommentPattern = CommentPattern::new(&[], &["<!--"], &["-->"], &[]);
const FSHARP_STYLE: CommentPattern =
CommentPattern::new(&["//"], &["(*"], &["*)"], &["///", "(**"]);
const PERL_STYLE: CommentPattern = CommentPattern::new(&["#"], &["=pod"], &["=cut"], &["=pod"]);
const R_STYLE: CommentPattern = CommentPattern::new(&["#"], &[], &[], &["#'"]);
const RMD_STYLE: CommentPattern = CommentPattern::new(&["#"], &["<!--"], &["-->"], &[]);
const MATLAB_STYLE: CommentPattern = CommentPattern::new(&["%"], &["%{"], &["%}"], &["%%"]);
const BATCH_STYLE: CommentPattern = CommentPattern::new(&["REM", "rem", "::"], &[], &[], &["REM"]);
const ELIXIR_STYLE: CommentPattern = CommentPattern::new(&["#"], &[], &[], &["@doc", "@moduledoc"]);
const LISP_STYLE: CommentPattern = CommentPattern::new(&[";"], &["#_"], &[], &[";;"]);
const ML_STYLE: CommentPattern = CommentPattern::new(&[], &["(*"], &["*)"], &["(**"]);
const HASKELL_STYLE: CommentPattern =
CommentPattern::new(&["--"], &["{-"], &["-}"], &["-- |", "-- ^"]);
const WEB_COMPONENT_STYLE: CommentPattern =
CommentPattern::new(&["//"], &["<!--", "/*"], &["-->", "*/"], &["/**"]);
const ASCIIDOC_STYLE: CommentPattern = CommentPattern::new(&["//"], &["////"], &["////"], &[]);
const MARKDOWN_STYLE: CommentPattern = CommentPattern::new(&[], &["<!--"], &["-->"], &[]);
const PATTERN_TABLE: &[(&str, CommentPattern)] = &[
(
"rs",
CommentPattern::new(&["//"], &["/*"], &["*/"], &["///", "//!", "/**"]),
),
("js", JS_STYLE),
("ts", JS_STYLE),
("jsx", JS_STYLE),
("tsx", JS_STYLE),
("mjs", JS_STYLE),
("cjs", JS_STYLE),
("mts", JS_STYLE),
("cts", JS_STYLE),
(
"py",
CommentPattern::new(
&["#"],
&["\"\"\"", "'''"],
&["\"\"\"", "'''"],
&["\"\"\"", "'''"],
),
),
(
"pyi",
CommentPattern::new(
&["#"],
&["\"\"\"", "'''"],
&["\"\"\"", "'''"],
&["\"\"\"", "'''"],
),
),
("java", JAVADOC_STYLE),
("kt", JAVADOC_STYLE),
("kts", JAVADOC_STYLE),
("scala", JAVADOC_STYLE),
("groovy", JAVADOC_STYLE),
("c", C_STYLE),
("cpp", C_STYLE),
("cc", C_STYLE),
("cxx", C_STYLE),
("h", C_STYLE),
("hpp", C_STYLE),
("hxx", C_STYLE),
("mm", JAVADOC_STYLE),
("cs", TRIPLE_SLASH_STYLE),
(
"php",
CommentPattern::new(&["//", "#"], &["/*"], &["*/"], &["/**"]),
),
(
"rb",
CommentPattern::new(&["#"], &["=begin"], &["=end"], &["##"]),
),
(
"rake",
CommentPattern::new(&["#"], &["=begin"], &["=end"], &["##"]),
),
(
"go",
CommentPattern::new(&["//"], &["/*"], &["*/"], &["//"]),
),
("swift", TRIPLE_SLASH_STYLE),
("sh", HASH_ONLY),
("bash", HASH_ONLY),
("zsh", HASH_ONLY),
("fish", HASH_ONLY),
("yaml", HASH_ONLY),
("yml", HASH_ONLY),
("toml", HASH_ONLY),
("ini", CommentPattern::new(&[";", "#"], &[], &[], &[";;"])),
("json", CommentPattern::new(&["//"], &["/*"], &["*/"], &[])),
("jsonc", CommentPattern::new(&["//"], &["/*"], &["*/"], &[])),
("xml", XML_STYLE),
("html", XML_STYLE),
("htm", XML_STYLE),
("svg", XML_STYLE),
("css", CommentPattern::new(&[], &["/*"], &["*/"], &["/**"])),
(
"scss",
CommentPattern::new(&["//"], &["/*"], &["*/"], &["/**", "///"]),
),
("sass", CommentPattern::new(&["//"], &[], &[], &["///"])),
("less", JAVADOC_STYLE),
("vue", WEB_COMPONENT_STYLE),
("svelte", WEB_COMPONENT_STYLE),
("md", MARKDOWN_STYLE),
("markdown", MARKDOWN_STYLE),
("rst", CommentPattern::new(&[".."], &[], &[], &[])),
("adoc", ASCIIDOC_STYLE),
("asciidoc", ASCIIDOC_STYLE),
("hs", HASKELL_STYLE),
("lhs", HASKELL_STYLE),
(
"elm",
CommentPattern::new(&["--"], &["{-"], &["-}"], &["{-|"]),
),
("ml", ML_STYLE),
("mli", ML_STYLE),
("fs", FSHARP_STYLE),
("fsx", FSHARP_STYLE),
("fsi", FSHARP_STYLE),
("clj", LISP_STYLE),
("cljs", LISP_STYLE),
("cljc", LISP_STYLE),
("erl", CommentPattern::new(&["%"], &[], &[], &["%%"])),
("hrl", CommentPattern::new(&["%"], &[], &[], &["%%"])),
("ex", ELIXIR_STYLE),
("exs", ELIXIR_STYLE),
(
"jl",
CommentPattern::new(&["#"], &["#="], &["=#"], &["\"\"\""]),
),
("r", R_STYLE),
("R", R_STYLE),
("rmd", RMD_STYLE),
("Rmd", RMD_STYLE),
("m", MATLAB_STYLE),
("mlx", MATLAB_STYLE),
(
"lua",
CommentPattern::new(&["--"], &["--[["], &["]]"], &["---"]),
),
("pl", PERL_STYLE),
("pm", PERL_STYLE),
("pod", PERL_STYLE),
(
"ps1",
CommentPattern::new(&["#"], &["<#"], &["#>"], &["<#"]),
),
(
"psm1",
CommentPattern::new(&["#"], &["<#"], &["#>"], &["<#"]),
),
("bat", BATCH_STYLE),
("cmd", BATCH_STYLE),
(
"sql",
CommentPattern::new(&["--"], &["/*"], &["*/"], &["--"]),
),
("dart", TRIPLE_SLASH_STYLE),
(
"zig",
CommentPattern::new(&["//"], &[], &[], &["///", "//!"]),
),
("proto", JAVADOC_STYLE),
("tf", HASH_ONLY),
("nix", HASH_ONLY),
("gradle", JAVADOC_STYLE),
("dockerfile", HASH_ONLY),
("containerfile", HASH_ONLY),
("makefile", HASH_ONLY),
("gnumakefile", HASH_ONLY),
("justfile", HASH_ONLY),
("rakefile", HASH_ONLY),
("gemfile", HASH_ONLY),
("podfile", HASH_ONLY),
("brewfile", HASH_ONLY),
("procfile", HASH_ONLY),
("vagrantfile", HASH_ONLY),
("berksfile", HASH_ONLY),
("fastfile", HASH_ONLY),
("appfile", HASH_ONLY),
("jenkinsfile", JAVADOC_STYLE),
("dockerignore", HASH_ONLY),
("gitignore", HASH_ONLY),
("gitattributes", HASH_ONLY),
("gitmodules", HASH_ONLY),
("npmignore", HASH_ONLY),
("prettierignore", HASH_ONLY),
("eslintignore", HASH_ONLY),
("codeowners", HASH_ONLY),
("editorconfig", HASH_ONLY),
("readme", MARKDOWN_STYLE),
("changelog", MARKDOWN_STYLE),
("contributing", MARKDOWN_STYLE),
];
static PATTERNS: LazyLock<HashMap<&'static str, CommentPattern>> =
LazyLock::new(|| PATTERN_TABLE.iter().copied().collect());
pub fn lookup(extension: &str) -> Option<CommentPattern> {
PATTERNS.get(extension).copied()
}
pub fn lookup_or_empty(extension: &str) -> CommentPattern {
lookup(extension).unwrap_or(CommentPattern::EMPTY)
}
pub fn is_known(extension: &str) -> bool {
PATTERNS.contains_key(extension)
}
const PROSE_FORMATS: &[&str] = &[
"md",
"markdown",
"rst",
"adoc",
"asciidoc",
"readme",
"changelog",
"contributing",
];
pub fn is_prose_format(key: &str) -> bool {
PROSE_FORMATS.contains(&key)
}
pub fn known_extensions() -> impl Iterator<Item = &'static str> {
PATTERN_TABLE.iter().map(|(ext, _)| *ext)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn table_has_no_conflicting_duplicates() {
let mut seen: HashMap<&str, CommentPattern> = HashMap::new();
for (ext, pattern) in PATTERN_TABLE {
if let Some(existing) = seen.insert(ext, *pattern) {
assert_eq!(
existing, *pattern,
"extension {ext:?} is listed twice with different comment syntax"
);
}
}
}
#[test]
fn block_delimiters_are_paired_or_deliberately_unpaired() {
for (ext, p) in PATTERN_TABLE {
if p.multi_line_start.is_empty() {
assert!(
p.multi_line_end.is_empty(),
"{ext:?} has block closers but no openers"
);
continue;
}
if *ext == "clj" || *ext == "cljs" || *ext == "cljc" {
continue;
}
assert_eq!(
p.multi_line_start.len(),
p.multi_line_end.len(),
"{ext:?} has {} block openers but {} closers",
p.multi_line_start.len(),
p.multi_line_end.len()
);
}
}
#[test]
fn no_pattern_is_an_empty_string() {
for (ext, p) in PATTERN_TABLE {
for group in [
p.single_line,
p.multi_line_start,
p.multi_line_end,
p.doc_patterns,
] {
assert!(
group.iter().all(|s| !s.is_empty()),
"{ext:?} contains an empty pattern, which would match every line"
);
}
}
}
#[test]
fn lookups_resolve_expected_languages() {
assert_eq!(lookup("rs").unwrap().single_line, &["//"]);
assert!(lookup("rs").unwrap().doc_patterns.contains(&"///"));
assert_eq!(lookup("py").unwrap().single_line, &["#"]);
assert!(lookup("py").unwrap().multi_line_start.contains(&"\"\"\""));
assert!(lookup("unknown-ext").is_none());
assert_eq!(lookup_or_empty("unknown-ext"), CommentPattern::EMPTY);
}
#[test]
fn every_extension_is_lowercase_or_deliberately_cased() {
let allowed_uppercase = ["R", "Rmd"];
for (ext, _) in PATTERN_TABLE {
assert!(
ext.chars().all(|c| !c.is_ascii_uppercase()) || allowed_uppercase.contains(ext),
"{ext:?} would never be found: lookups use a lowercase extension"
);
}
}
}