use decomment::{strip, Language, Mode, Options, Stripped, UnterminatedKind};
const LANGUAGES: [Language; 13] = [
Language::C,
Language::Cpp,
Language::CSharp,
Language::Css,
Language::Go,
Language::Html,
Language::Java,
Language::JavaScript,
Language::Python,
Language::Rust,
Language::Sql,
Language::Swift,
Language::TypeScript,
];
const MODES: [Mode; 4] = [Mode::All, Mode::Line, Mode::Block, Mode::First];
const VECTORS: &[&str] = &[
"",
" ",
"/",
"/*",
"/*/",
"/**/",
"//",
"*/",
"-->",
"<!--",
"#!/usr/bin/env sh\n",
"a // b\r\nc",
"a /* b */ c",
"/* a /* b */ c */",
"/*/*/*/* x */*/*/*/",
"var re = /a//b/; var x = 1;",
"tag`raw // text`; // real",
"a\"raw // text\"; // real",
"`a ${ `b ${ c /* x */ }` } d`",
"s = r#\"a // b\"#; // c",
"select 'a''-- not' -- yes",
"R\"d(// not)d\" // yes",
"@\"a\"\"b\" // yes",
"'''# not\n''' # yes",
"\"\"\"\na // b\n\"\"\" // yes",
"let s = \"héllo → wörld\"; // ünicode\n",
"a\0b // c\0d\ne\0",
"x\u{2028}// y\u{2029}z",
"\"unterminated\n// gone\n",
"`unterminated ${ // x\n",
"#\"swift raw // no\"# // yes",
"\\\\ // trailing escape \\",
"a/**/b/**/c",
"//! banner\n/*! banner */\n// drop\n",
];
fn is_subsequence(needle: &str, haystack: &str) -> bool {
let mut bytes = haystack.bytes();
needle.bytes().all(|byte| bytes.any(|other| other == byte))
}
fn count(text: &str, byte: u8) -> usize {
text.bytes().filter(|b| *b == byte).count()
}
fn every_setting(mut check: impl FnMut(&str, Options, Stripped)) {
for &source in VECTORS {
for language in LANGUAGES {
for mode in MODES {
for preserve in [false, true] {
for bang in [false, true] {
let mut options = Options::new(language);
options.mode = mode;
options.preserve_newlines = preserve;
options.keep_bang_comments = bang;
check(source, options, strip(source, options));
}
}
}
}
}
}
#[test]
fn the_output_is_always_a_byte_subsequence_of_the_input() {
every_setting(|source, options, result| {
assert!(
is_subsequence(&result.output, source),
"{options:?} on {source:?} gave {:?}",
result.output
);
});
}
#[test]
fn the_output_is_never_longer_than_the_input() {
every_setting(|source, options, result| {
assert!(
result.output.len() <= source.len(),
"{options:?} on {source:?}"
);
});
}
#[test]
fn preserve_newlines_keeps_the_line_feed_and_carriage_return_counts() {
every_setting(|source, options, result| {
if !options.preserve_newlines {
return;
}
assert_eq!(
count(&result.output, b'\n'),
count(source, b'\n'),
"{options:?} on {source:?}"
);
assert_eq!(
count(&result.output, b'\r'),
count(source, b'\r'),
"{options:?} on {source:?}"
);
});
}
#[test]
fn a_reported_offset_is_always_inside_the_input() {
every_setting(|source, options, result| {
if let Some(open) = result.unterminated {
assert!(open.offset < source.len(), "{options:?} on {source:?}");
}
});
}
#[test]
fn the_result_depends_only_on_the_input_and_the_options() {
every_setting(|source, options, result| {
assert_eq!(strip(source, options), result);
});
}
#[test]
fn typescript_and_javascript_agree_on_every_vector() {
for &source in VECTORS {
for mode in MODES {
for preserve in [false, true] {
for bang in [false, true] {
let mut js = Options::new(Language::JavaScript);
js.mode = mode;
js.preserve_newlines = preserve;
js.keep_bang_comments = bang;
let mut ts = js;
ts.language = Language::TypeScript;
assert_eq!(strip(source, js), strip(source, ts), "{source:?}");
}
}
}
}
}
#[test]
fn mode_first_changes_at_most_one_contiguous_region() {
for &source in VECTORS {
for language in LANGUAGES {
let mut options = Options::new(language);
options.mode = Mode::First;
let output = strip(source, options).output;
if output == source {
continue;
}
let prefix = source
.bytes()
.zip(output.bytes())
.take_while(|(a, b)| a == b)
.count();
let suffix = source
.bytes()
.rev()
.zip(output.bytes().rev())
.take_while(|(a, b)| a == b)
.count();
assert!(
prefix + suffix >= output.len(),
"{language:?} {source:?} gave {output:?}"
);
}
}
}
const OPENERS: &[(Language, &str, UnterminatedKind)] = &[
(Language::C, "/*", UnterminatedKind::BlockComment),
(Language::Cpp, "/*", UnterminatedKind::BlockComment),
(Language::Cpp, "R\"d(", UnterminatedKind::StringLiteral),
(Language::CSharp, "/*", UnterminatedKind::BlockComment),
(Language::CSharp, "@\"", UnterminatedKind::StringLiteral),
(Language::Css, "/*", UnterminatedKind::BlockComment),
(Language::Go, "/*", UnterminatedKind::BlockComment),
(Language::Go, "`", UnterminatedKind::StringLiteral),
(Language::Html, "<!--", UnterminatedKind::BlockComment),
(Language::Java, "/*", UnterminatedKind::BlockComment),
(Language::Java, "\"\"\"", UnterminatedKind::StringLiteral),
(Language::JavaScript, "/*", UnterminatedKind::BlockComment),
(Language::JavaScript, "`", UnterminatedKind::StringLiteral),
(Language::Python, "'''", UnterminatedKind::StringLiteral),
(Language::Python, "\"\"\"", UnterminatedKind::StringLiteral),
(Language::Rust, "/*", UnterminatedKind::BlockComment),
(Language::Rust, "\"", UnterminatedKind::StringLiteral),
(Language::Rust, "r#\"", UnterminatedKind::StringLiteral),
(Language::Sql, "/*", UnterminatedKind::BlockComment),
(Language::Sql, "'", UnterminatedKind::StringLiteral),
(Language::Sql, "\"", UnterminatedKind::StringLiteral),
(Language::Swift, "/*", UnterminatedKind::BlockComment),
(Language::Swift, "#\"", UnterminatedKind::StringLiteral),
(Language::Swift, "\"\"\"", UnterminatedKind::StringLiteral),
(Language::TypeScript, "/*", UnterminatedKind::BlockComment),
(Language::TypeScript, "`", UnterminatedKind::StringLiteral),
];
#[test]
fn a_construct_left_open_at_end_of_input_is_always_reported() {
for &(language, opener, kind) in OPENERS {
for body in ["", "x", "x\ny"] {
let source = format!("v = {opener}{body}");
let result = strip(&source, Options::new(language));
let context = format!("{language:?} on {source:?}");
let open = result.unterminated.unwrap_or_else(|| panic!("{context}"));
assert_eq!(open.kind, kind, "{context}");
assert_eq!(open.offset, 4, "{context}");
}
}
}
#[test]
fn both_diagnostic_types_implement_error() {
fn message<T: core::error::Error>(value: T) -> String {
value.to_string()
}
assert_eq!(message(UnterminatedKind::BlockComment), "block comment");
assert_eq!(message(UnterminatedKind::StringLiteral), "string literal");
let result = strip("var x = 1; /* open", Options::new(Language::JavaScript));
let open = result.unterminated.unwrap();
assert_eq!(open.kind, UnterminatedKind::BlockComment);
assert_eq!(open.offset, 11);
assert_eq!(
message(open),
"unterminated block comment opened at byte 11"
);
}
#[test]
fn input_with_no_comment_marker_comes_back_unchanged() {
let clean = "function f(a, b) { return a + b; }\nlet s = \"text\";\n";
for language in LANGUAGES {
let result = strip(clean, Options::new(language));
assert_eq!(result.output, clean, "{language:?}");
assert_eq!(result.unterminated, None, "{language:?}");
}
}