use decomment::{strip, Language, Mode, Options};
const SEED: u64 = 0x00c0_ffee_c0ff_ee00;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.0 >> 17
}
fn below(&mut self, bound: usize) -> usize {
(self.next() % bound as u64) as usize
}
fn pick<T: Copy>(&mut self, items: &[T]) -> T {
items[self.below(items.len())]
}
fn flip(&mut self) -> bool {
self.next() % 2 == 0
}
}
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 FRAGMENTS: &[&str] = &[
"/*",
"*/",
"//",
"/**/",
"/*!",
"//!",
"#",
"#!",
"--",
"--!",
"<!--",
"-->",
"\"",
"'",
"`",
"${",
"}",
"{",
"\\",
"\\\\",
"\\\"",
"\\'",
"r#\"",
"\"#",
"R\"d(",
")d\"",
"@\"",
"\"\"",
"\"\"\"",
"'''",
"b\"",
"br#\"",
"##\"",
"\"##",
"\n",
"\r\n",
"\r",
"\t",
" ",
"\0",
"\u{2028}",
"\u{2029}",
"é",
"→",
"a",
"x = ",
"1",
";",
"/",
"*",
"[",
"]",
"(",
")",
"return ",
"typeof ",
"http://u/x",
"a-b",
"0x1f",
];
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 random_source(rng: &mut Rng) -> String {
let parts = rng.below(14);
let mut source = String::new();
for _ in 0..parts {
source.push_str(rng.pick(FRAGMENTS));
}
source
}
fn random_options(rng: &mut Rng) -> Options {
let mut options = Options::new(rng.pick(&LANGUAGES));
options.mode = rng.pick(&MODES);
options.preserve_newlines = rng.flip();
options.keep_bang_comments = rng.flip();
options
}
#[test]
fn every_invariant_holds_over_random_input() {
let mut rng = Rng(SEED);
for case in 0..60_000 {
let source = random_source(&mut rng);
let options = random_options(&mut rng);
let result = strip(&source, options);
let context = format!("case {case} {options:?} on {source:?}");
assert!(is_subsequence(&result.output, &source), "{context}");
assert!(result.output.len() <= source.len(), "{context}");
if options.preserve_newlines {
assert_eq!(
count(&result.output, b'\n'),
count(&source, b'\n'),
"{context}"
);
assert_eq!(
count(&result.output, b'\r'),
count(&source, b'\r'),
"{context}"
);
}
if let Some(open) = result.unterminated {
assert!(open.offset < source.len(), "{context}");
}
assert_eq!(strip(&source, options), result, "{context}");
}
}
#[test]
fn typescript_matches_javascript_over_random_input() {
let mut rng = Rng(SEED ^ 0x5555_5555);
for _ in 0..20_000 {
let source = random_source(&mut rng);
let mut js = Options::new(Language::JavaScript);
js.mode = rng.pick(&MODES);
js.preserve_newlines = rng.flip();
js.keep_bang_comments = rng.flip();
let mut ts = js;
ts.language = Language::TypeScript;
assert_eq!(strip(&source, js), strip(&source, ts), "{source:?}");
}
}
#[test]
fn arbitrary_bytes_never_panic() {
let mut rng = Rng(SEED ^ 0xaaaa_aaaa);
for _ in 0..20_000 {
let len = rng.below(64);
let bytes: Vec<u8> = (0..len).map(|_| (rng.next() % 256) as u8).collect();
let source = String::from_utf8_lossy(&bytes).into_owned();
let options = random_options(&mut rng);
let result = strip(&source, options);
assert!(is_subsequence(&result.output, &source), "{source:?}");
}
}
fn templates(
language: Language,
) -> (
&'static [&'static str],
&'static [&'static str],
&'static str,
) {
match language {
Language::C => (
&["// {}", "/* {} */"],
&["\"{}\"", "'{}'", "\"a\\\"// {}\""],
"int v{} = 1;",
),
Language::Cpp => (
&["// {}", "/* {} */"],
&["\"{}\"", "'{}'", "R\"d({})d\"", "\"a\\\"// {}\""],
"int v{} = 1;",
),
Language::CSharp => (
&["// {}", "/* {} */"],
&["\"{}\"", "'{}'", "@\"{}\"", "@\"a\"\"// {}\""],
"int v{} = 1;",
),
Language::Css => (
&["/* {} */"],
&["\"{}\"", "'{}'", "\"a\\\"/* {} */\""],
"a_{}: 1",
),
Language::Go => (
&["// {}", "/* {} */"],
&["\"{}\"", "`{}`", "\"a\\\"// {}\""],
"v{} := 1",
),
Language::Html => (&["<!-- {} -->"], &[], "<p>{}</p>"),
Language::Java => (
&["// {}", "/* {} */"],
&["\"{}\"", "\"\"\"\n{}\n\"\"\"", "\"a\\\"// {}\""],
"int v{} = 1;",
),
Language::JavaScript | Language::TypeScript => (
&["// {}", "/* {} */"],
&["\"{}\"", "'{}'", "`{}`", "x = /{}[/]/", "\"a\\\"// {}\""],
"var v{} = 1;",
),
Language::Python => (
&["# {}"],
&["\"{}\"", "'{}'", "'''{}'''", "r\"{}\"", "r'a\\'# {}'"],
"v{} = 1",
),
Language::Rust => (
&["// {}", "/* {} */"],
&["\"{}\"", "r#\"{}\"#", "b\"{}\"", "\"a\\\"// {}\""],
"let v{} = 1;",
),
Language::Sql => (
&["-- {}", "/* {} */"],
&["'{}'", "\"{}\"", "'a''-- {}'"],
"select v{}",
),
Language::Swift => (
&["// {}", "/* {} */"],
&[
"\"{}\"",
"#\"{}\"#",
"\"\"\"\n{}\n\"\"\"",
"#\"a\\#\"// {}\"#",
],
"let v{} = 1",
),
other => panic!("no sentinel templates for {other:?}"),
}
}
#[test]
fn planted_sentinels_survive_outside_comments_and_vanish_inside_them() {
let mut rng = Rng(SEED ^ 0x1234_5678);
let mut serial = 0u32;
for language in LANGUAGES {
let (comments, literals, code) = templates(language);
for _ in 0..2_000 {
let mut source = String::new();
let mut gone = Vec::new();
let mut kept = Vec::new();
for _ in 0..1 + rng.below(6) {
serial += 1;
let sentinel = format!("S{serial:07}");
let (template, target) = match rng.below(3) {
0 => (rng.pick(comments), &mut gone),
1 if !literals.is_empty() => (rng.pick(literals), &mut kept),
_ => (code, &mut kept),
};
source.push_str(&template.replace("{}", &sentinel));
source.push('\n');
target.push(sentinel);
}
let output = strip(&source, Options::new(language)).output;
for sentinel in &gone {
assert!(!output.contains(sentinel), "{language:?} {source:?}");
}
for sentinel in &kept {
assert!(output.contains(sentinel), "{language:?} {source:?}");
}
}
}
}