decomment 0.2.0

Remove comments from source text in 13 languages without touching strings, templates, or regex literals.
Documentation
//! One test per row of the language table. Each covers every comment form,
//! every string form, a marker inside every string form, and the nesting rule.

use decomment::{strip, Language, Options, UnterminatedKind};

fn out(source: &str, language: Language) -> String {
    strip(source, Options::new(language)).output
}

#[test]
fn c_has_two_comment_forms_and_two_string_forms() {
    assert_eq!(out("int x = 1; // c\n", Language::C), "int x = 1; \n");
    assert_eq!(out("int x = /* c */ 1;", Language::C), "int x =  1;");
    assert_eq!(
        out("s = \"// not\"; // yes", Language::C),
        "s = \"// not\"; "
    );
    assert_eq!(out("c = '/'; // yes", Language::C), "c = '/'; ");
    assert_eq!(
        out("s = \"a\\\"// no\"; // yes", Language::C),
        "s = \"a\\\"// no\"; "
    );
    // A string marker inside a comment has no effect.
    assert_eq!(out("/* \" */ x", Language::C), " x");
}

#[test]
fn cpp_adds_raw_strings() {
    let source = "auto s = R\"d(// not a comment)d\"; // yes";
    assert_eq!(
        out(source, Language::Cpp),
        "auto s = R\"d(// not a comment)d\"; "
    );
    // An empty delimiter is legal.
    assert_eq!(
        out("R\"(/* no */)\" // yes", Language::Cpp),
        "R\"(/* no */)\" "
    );
    // A raw string with no closing sequence is reported.
    let result = strip("auto s = R\"d(unclosed", Options::new(Language::Cpp));
    assert_eq!(
        result.unterminated.unwrap().kind,
        UnterminatedKind::StringLiteral
    );
}

#[test]
fn csharp_adds_verbatim_strings() {
    let source = "var s = @\"a // b\nc\"\"d\"; // yes";
    assert_eq!(
        out(source, Language::CSharp),
        "var s = @\"a // b\nc\"\"d\"; "
    );
    assert_eq!(
        out("var s = @\"x\"; /* c */", Language::CSharp),
        "var s = @\"x\"; "
    );
}

#[test]
fn css_has_one_comment_form() {
    assert_eq!(
        out("a { color: red } /* c */", Language::Css),
        "a { color: red } "
    );
    assert_eq!(
        out("a { b: '/* not */' }", Language::Css),
        "a { b: '/* not */' }"
    );
    assert_eq!(
        out("a { b: \"//\" } // kept", Language::Css),
        "a { b: \"//\" } // kept"
    );
    assert_eq!(out("/* a /* b */ c */", Language::Css), " c */");
}

#[test]
fn go_adds_raw_backtick_strings() {
    let source = "s := `a // b\nc \\ d` // yes";
    assert_eq!(out(source, Language::Go), "s := `a // b\nc \\ d` ");
    assert_eq!(out("s := \"//\" /* c */", Language::Go), "s := \"//\" ");
    let result = strip("s := `unclosed", Options::new(Language::Go));
    assert_eq!(
        result.unterminated.unwrap().kind,
        UnterminatedKind::StringLiteral
    );
}

#[test]
fn html_strips_only_its_own_comment_form() {
    assert_eq!(
        out("<p>a</p><!-- c --><p>b</p>", Language::Html),
        "<p>a</p><p>b</p>"
    );
    // Quotes carry no meaning, so an attribute value cannot hide a comment.
    assert_eq!(
        out("<a href=\"x\"><!-- c -->", Language::Html),
        "<a href=\"x\">"
    );
    assert_eq!(
        out("<p>a // b /* c */</p>", Language::Html),
        "<p>a // b /* c */</p>"
    );
    // A script body is left alone. Extract it and call again with JavaScript.
    let source = "<script>var x = 1; // keep</script>";
    assert_eq!(out(source, Language::Html), source);
}

#[test]
fn java_adds_text_blocks() {
    let source = "var s = \"\"\"\n  a // b\n  \"\"\"; // yes";
    assert_eq!(
        out(source, Language::Java),
        "var s = \"\"\"\n  a // b\n  \"\"\"; "
    );
    assert_eq!(
        out("char c = '/'; /* c */", Language::Java),
        "char c = '/'; "
    );
    assert_eq!(out("/* a /* b */ c */", Language::Java), " c */");
}

#[test]
fn javascript_covers_templates_regex_and_both_quote_forms() {
    assert_eq!(
        out("var s = '//'; // yes", Language::JavaScript),
        "var s = '//'; "
    );
    assert_eq!(
        out("var s = \"/*\"; /* c */", Language::JavaScript),
        "var s = \"/*\"; "
    );
    assert_eq!(
        out("var t = `a // b`; // yes", Language::JavaScript),
        "var t = `a // b`; "
    );
    assert_eq!(
        out("var r = /\\/[/]/g; // yes", Language::JavaScript),
        "var r = /\\/[/]/g; "
    );
    assert_eq!(out("/* a /* b */ c */", Language::JavaScript), " c */");
}

#[test]
fn python_has_one_comment_form_and_four_string_forms() {
    assert_eq!(out("x = 1  # c\n", Language::Python), "x = 1  \n");
    assert_eq!(out("s = '# not'  # yes", Language::Python), "s = '# not'  ");
    assert_eq!(
        out("s = \"# not\"  # yes", Language::Python),
        "s = \"# not\"  "
    );
    assert_eq!(
        out("s = '''# not\n'''  # yes", Language::Python),
        "s = '''# not\n'''  "
    );
    assert_eq!(
        out("s = \"\"\"# not\n\"\"\"  # yes", Language::Python),
        "s = \"\"\"# not\n\"\"\"  "
    );
    // A raw prefix keeps the backslash in the value, and the backslash still
    // stops the next quote from closing the literal.
    assert_eq!(
        out("s = r'a\\'# not'  # yes", Language::Python),
        "s = r'a\\'# not'  "
    );
    // Nothing closes that literal here, so the `#` is string data.
    let open = "s = r'a\\'  # yes";
    assert_eq!(out(open, Language::Python), open);
    assert_eq!(
        out("s = f'{x}# not'  # yes", Language::Python),
        "s = f'{x}# not'  "
    );
    // A slash is not a comment marker in Python.
    assert_eq!(out("x = a // b  # yes", Language::Python), "x = a // b  ");
}

#[test]
fn rust_covers_raw_strings_byte_strings_and_lifetimes() {
    assert_eq!(out("let x = 1; // c\n", Language::Rust), "let x = 1; \n");
    assert_eq!(
        out("let s = r#\"a // b\"#; // yes", Language::Rust),
        "let s = r#\"a // b\"#; "
    );
    assert_eq!(
        out("let s = r\"a // b\"; // yes", Language::Rust),
        "let s = r\"a // b\"; "
    );
    assert_eq!(
        out("let s = br#\"//\"#; /* c */", Language::Rust),
        "let s = br#\"//\"#; "
    );
    assert_eq!(
        out("let b = b'/'; // yes", Language::Rust),
        "let b = b'/'; "
    );
    // A lifetime is not a character literal, so scanning stays in code state.
    assert_eq!(
        out("fn f<'a>(x: &'a str) {} // yes", Language::Rust),
        "fn f<'a>(x: &'a str) {} "
    );
    assert_eq!(
        out("let c = '\\u{1F600}'; // yes", Language::Rust),
        "let c = '\\u{1F600}'; "
    );
    assert_eq!(
        out("let c = '\\n'; // yes", Language::Rust),
        "let c = '\\n'; "
    );
    // A hash count must match to close.
    assert_eq!(
        out("let s = r##\"a \"# b\"##; // yes", Language::Rust),
        "let s = r##\"a \"# b\"##; "
    );
}

#[test]
fn sql_uses_dash_dash_and_doubled_quotes() {
    assert_eq!(
        out("select 1 -- c\nfrom t", Language::Sql),
        "select 1 \nfrom t"
    );
    assert_eq!(
        out("select '-- not' -- yes", Language::Sql),
        "select '-- not' "
    );
    assert_eq!(
        out("select 'a''-- not' -- yes", Language::Sql),
        "select 'a''-- not' "
    );
    assert_eq!(
        out("select \"col--x\" /* c */", Language::Sql),
        "select \"col--x\" "
    );
    // A backslash is an ordinary byte, so it cannot escape the closing quote.
    assert_eq!(out("select 'a\\' -- yes", Language::Sql), "select 'a\\' ");
    assert_eq!(out("/* a /* b */ c */", Language::Sql), " c */");
}

#[test]
fn swift_covers_raw_strings_and_nesting() {
    assert_eq!(out("let x = 1 // c\n", Language::Swift), "let x = 1 \n");
    assert_eq!(
        out("let s = #\"a // b\"# // yes", Language::Swift),
        "let s = #\"a // b\"# "
    );
    assert_eq!(
        out("let s = ##\"a \"# b\"## // yes", Language::Swift),
        "let s = ##\"a \"# b\"## "
    );
    assert_eq!(
        out("let s = \"\"\"\na // b\n\"\"\" // yes", Language::Swift),
        "let s = \"\"\"\na // b\n\"\"\" "
    );
    // A `#` that no quote follows is a directive, not a raw string.
    assert_eq!(
        out("#if DEBUG\nlet x = 1 // yes\n#endif", Language::Swift),
        "#if DEBUG\nlet x = 1 \n#endif"
    );
    assert_eq!(out("/* a /* b */ c */", Language::Swift), "");
}

#[test]
fn typescript_is_an_alias_of_javascript() {
    let sources = [
        "var re = /a//b/; var x = 1;",
        "tag`raw // text`; // real",
        "/* a /* b */ c */",
        "let x: number = 1; // c\n",
        "type T = { a: string }; /* c */",
    ];
    for source in sources {
        assert_eq!(
            out(source, Language::TypeScript),
            out(source, Language::JavaScript)
        );
    }
}

#[test]
fn the_same_text_reads_differently_per_language() {
    assert_eq!(out("/* a /* b */ c */", Language::Rust), "");
    assert_eq!(out("/* a /* b */ c */", Language::C), " c */");

    assert_eq!(out("a -- b", Language::Sql), "a ");
    assert_eq!(out("a -- b", Language::JavaScript), "a -- b");

    assert_eq!(out("a // b", Language::Go), "a ");
    assert_eq!(out("a // b", Language::Css), "a // b");
    assert_eq!(out("a // b", Language::Html), "a // b");
    assert_eq!(out("a // b", Language::Python), "a // b");

    let shebang = "#!/usr/bin/env python3\nx = 1";
    assert_eq!(out(shebang, Language::Python), "\nx = 1");
    assert_eq!(out(shebang, Language::JavaScript), shebang);
}