decomment 0.1.0

Remove comments from source text in 13 languages without touching strings, templates, or regex literals.
Documentation
//! Inputs built to make a scanner go quadratic. Each has a wall clock bound.
//!
//! The bounds are loose on purpose. They catch a change from linear to
//! quadratic, which is orders of magnitude, not a few percent of drift.

use std::time::{Duration, Instant};

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

const BUDGET: Duration = Duration::from_secs(10);

fn timed(source: &str, options: Options) -> (String, Duration) {
    let start = Instant::now();
    let result = strip(source, options);
    (result.output, start.elapsed())
}

#[test]
fn two_hundred_thousand_regex_candidates_stay_linear() {
    // Every `/` after `=` opens a regex candidate. The first one fails at the
    // end of the line and suppresses the rest, so the line is scanned twice
    // rather than once per candidate.
    let source = "= /".repeat(200_000);
    let (output, elapsed) = timed(&source, Options::new(Language::JavaScript));
    assert_eq!(output, source);
    assert!(elapsed < BUDGET, "took {elapsed:?}");
}

#[test]
fn two_hundred_thousand_nested_openers_stay_linear() {
    let source = "/*".repeat(200_000);
    let (output, elapsed) = timed(&source, Options::new(Language::Rust));
    assert_eq!(output, "");
    assert!(elapsed < BUDGET, "took {elapsed:?}");
}

#[test]
fn a_four_megabyte_line_with_no_terminator_stays_linear() {
    let source = "abcdefgh".repeat(512 * 1024);
    let (output, elapsed) = timed(&source, Options::new(Language::JavaScript));
    assert_eq!(output.len(), 4 * 1024 * 1024);
    assert!(elapsed < BUDGET, "took {elapsed:?}");
}

#[test]
fn a_file_that_is_one_unterminated_string_stays_linear() {
    let mut source = String::from("`");
    source.push_str(&"template body ".repeat(200_000));
    let start = Instant::now();
    let result = strip(&source, Options::new(Language::JavaScript));
    let elapsed = start.elapsed();
    assert_eq!(result.output, source);
    assert_eq!(
        result.unterminated.unwrap().kind,
        UnterminatedKind::StringLiteral
    );
    assert!(elapsed < BUDGET, "took {elapsed:?}");
}

#[test]
fn two_hundred_thousand_empty_block_comments_stay_linear() {
    let source = "a/**/".repeat(200_000);
    let (output, elapsed) = timed(&source, Options::new(Language::JavaScript));
    assert_eq!(output, "a".repeat(200_000));
    assert!(elapsed < BUDGET, "took {elapsed:?}");
}

#[test]
fn deeply_nested_template_substitutions_stay_bounded() {
    // The substitution stack is 128 bits wide. Past that a `${` stays template
    // text, which keeps the region string data and deletes nothing.
    let source = format!("{}{}", "`${".repeat(5_000), "`".repeat(5_000));
    let (output, elapsed) = timed(&source, Options::new(Language::JavaScript));
    assert_eq!(output, source);
    assert!(elapsed < BUDGET, "took {elapsed:?}");
}