drep-ai 3.1.5

A local commit gate: runs the linters your repo configures, and sends changed code to an LLM for review
Documentation
//! The two link checks, and the blanking they both rest on.
//!
//! Most of these are false-positive tests. Both checks are cheap to make fire
//! and expensive to make *not* fire on ordinary prose, which is where every
//! historical bug in this pair lived.

use crate::docs::Check;
use crate::docs::tests::{fires_once_at, of_kind, silent};

// ---- bare_url ----

#[test]
fn a_bare_url_reports_at_its_first_character() {
    fires_once_at("see https://example.com/x for more", Check::BareUrl, 1, 5);
    fires_once_at("http://example.com", Check::BareUrl, 1, 1);
}

#[test]
fn a_url_inside_a_well_formed_link_is_not_bare() {
    silent("[example](https://example.com)", Check::BareUrl);
    silent(
        "see [the docs](http://example.com/a/b) here",
        Check::BareUrl,
    );
}

#[test]
fn blanking_a_code_span_leaves_the_text_before_it_alone() {
    // Every other inline-code test has the span at the start of the line or
    // covering the whole line, so an implementation that blanked from column 1
    // up to the first backtick passed all of them. This is the case that
    // separates "blank the span" from "blank everything up to the span".
    fires_once_at("see https://example.com and `code`", Check::BareUrl, 1, 5);
}

#[test]
fn a_url_inside_backticks_is_not_bare() {
    // Inline code is a literal a reader is meant to type. Wrapping it in a
    // markdown link would change what they type.
    silent("run `curl https://example.com`", Check::BareUrl);
}

#[test]
fn one_link_does_not_excuse_a_bare_url_beside_it() {
    // The whole reason the line is *blanked* rather than tested for
    // "contains a link". A `contains` guard passes every other test in this
    // file and fails this one.
    fires_once_at(
        "[docs](https://example.com) and https://bare.example.com",
        Check::BareUrl,
        1,
        33,
    );
}

#[test]
fn a_scheme_with_nothing_after_it_is_not_a_url() {
    // Prose naming the scheme itself. There is nothing here to link to.
    silent("the prefix https:// is required", Check::BareUrl);
    silent("https://", Check::BareUrl);
}

#[test]
fn the_url_ends_at_whitespace() {
    let found = of_kind("go to https://example.com/a now", Check::BareUrl);
    assert!(
        found[0].message.ends_with("https://example.com/a"),
        "{}",
        found[0].message
    );
}

#[test]
fn a_very_long_url_is_excerpted_rather_than_printed_whole() {
    // The message lands in a terminal and the URL comes from a file drep does
    // not control.
    let url = format!("https://example.com/{}", "a".repeat(500));
    let found = of_kind(&url, Check::BareUrl);
    assert!(
        found[0].message.chars().count() < 90,
        "{}",
        found[0].message
    );
    assert!(found[0].message.ends_with('…'), "{}", found[0].message);
    // A short one is printed intact, so the truncation is conditional.
    let found = of_kind("https://example.com", Check::BareUrl);
    assert_eq!(found[0].message, "bare URL: https://example.com");
}

#[test]
fn a_url_carrying_an_escape_sequence_cannot_reach_the_terminal_intact() {
    // The URL is copied out of a file drep does not control and printed into a
    // terminal. This is what `text::excerpt` is for, and the local truncation
    // it replaced only shortened.
    let found = of_kind("https://example.com/\u{1b}[31mred", Check::BareUrl);
    assert!(
        !found[0].message.chars().any(char::is_control),
        "{:?}",
        found[0].message
    );
}

#[test]
fn only_the_first_bare_url_on_a_line_is_reported() {
    // One finding per line: two URLs on one line is one thing to fix, while two
    // findings at two columns reads as two.
    assert_eq!(
        of_kind("https://a.example and https://b.example", Check::BareUrl).len(),
        1
    );
}

// An autolink is `<` and `>` around an absolute URI (CommonMark 0.31.2,
// section 6.5, examples 594-610). It is already a link, so telling its author
// to wrap it as `[text](url)` is wrong, and the URL the old message quoted
// carried the closing `>`.

#[test]
fn an_autolink_is_not_a_bare_url() {
    silent("<https://example.com/a>", Check::BareUrl);
    silent("<http://example.com/a>", Check::BareUrl);
    silent("see <https://example.com/a> for more", Check::BareUrl);
}

#[test]
fn an_autolink_hides_a_url_nested_in_its_own_query() {
    silent("<https://a.example/?next=http://b.example>", Check::BareUrl);
}

#[test]
fn an_autolink_does_not_excuse_a_bare_url_beside_it() {
    let before = "<https://a.example> and ";
    fires_once_at(
        &format!("{before}https://b.example"),
        Check::BareUrl,
        1,
        before.chars().count() as u32 + 1,
    );
    // Touching it, with nothing between the `>` and the next scheme.
    fires_once_at(
        "<https://a.example>https://b.example",
        Check::BareUrl,
        1,
        20,
    );
    // After several, so the scan resumes past each autolink it blanks.
    let before = "<https://a.example> <https://b.example> ";
    fires_once_at(
        &format!("{before}https://c.example"),
        Check::BareUrl,
        1,
        before.chars().count() as u32 + 1,
    );
}

#[test]
fn text_that_is_not_an_autolink_still_holds_a_bare_url() {
    // Example 608: a space after the `<`.
    fires_once_at("< https://foo.bar >", Check::BareUrl, 1, 3);
    // Example 602: a space inside.
    fires_once_at("<https://foo.bar/baz bim>", Check::BareUrl, 1, 2);
    // No closing `>`.
    fires_once_at("<https://foo.bar", Check::BareUrl, 1, 2);
    // A `<` before the `>`, and a control character before it.
    fires_once_at("<https://foo.bar<baz>", Check::BareUrl, 1, 2);
    fires_once_at("<https://foo.bar\u{7}baz>", Check::BareUrl, 1, 2);
    // A `>` with no `<` before the scheme.
    fires_once_at("https://foo.bar>", Check::BareUrl, 1, 1);
}

// ---- link_syntax_invalid ----

#[test]
fn a_well_formed_link_is_silent() {
    silent("[text](url)", Check::LinkSyntaxInvalid);
    silent("a [one](x) and [two](y) b", Check::LinkSyntaxInvalid);
}

#[test]
fn a_badge_is_one_link_not_two_broken_ones() {
    // `[![alt](img)](href)`. A link scanner whose text may not contain
    // brackets stops at the image's `]` and leaves `](href)` looking broken -
    // which fires on the first line of most READMEs.
    silent(
        "[![build](img.svg)](https://ci.example)",
        Check::LinkSyntaxInvalid,
    );
}

#[test]
fn a_prose_parenthetical_is_not_broken_link_syntax() {
    // Counting parentheses over the raw line flags every sentence that opens a
    // parenthetical and closes it on the next line. The `](` guard is what
    // stops it.
    silent("a sentence (that wraps onto", Check::LinkSyntaxInvalid);
    silent("continued) here", Check::LinkSyntaxInvalid);
    silent("a smiley :-) alone", Check::LinkSyntaxInvalid);
}

#[test]
fn an_unclosed_link_target_is_reported() {
    fires_once_at("[text](url", Check::LinkSyntaxInvalid, 1, 1);
}

#[test]
fn an_unbalanced_bracket_is_reported_in_either_direction() {
    fires_once_at("[text without a close", Check::LinkSyntaxInvalid, 1, 1);
    fires_once_at("text] without an open", Check::LinkSyntaxInvalid, 1, 1);
}

#[test]
fn brackets_inside_backticks_do_not_count() {
    // A literal showing an array index is not a link.
    silent("use `items[0]` here", Check::LinkSyntaxInvalid);
    silent("write `[` to open", Check::LinkSyntaxInvalid);
}

#[test]
fn a_reference_style_link_is_balanced() {
    silent("[text][ref]", Check::LinkSyntaxInvalid);
}

#[test]
fn a_link_reference_definition_declares_a_url_rather_than_leaving_one_bare() {
    // The Keep a Changelog footer is made entirely of these definitions.
    silent(
        "[2.5.0]: https://github.com/slb350/drep/compare/v2.4.0...v2.5.0",
        Check::BareUrl,
    );
    silent("[2.5.0]: https://example.com", Check::LinkSyntaxInvalid);
    // Indented definitions inside a list still count.
    silent("  [ref]: https://example.com", Check::BareUrl);
}

#[test]
fn a_definition_only_exempts_its_own_destination() {
    // Prose that merely *starts* with a bracket is not a definition, and a
    // bare URL later on the same line as a definition is still bare... but a
    // definition's own trailing title is part of the declaration.
    fires_once_at(
        "[not a definition] https://example.com",
        Check::BareUrl,
        1,
        20,
    );
    silent(
        "[ref]: https://a.example \"see https://b.example\"",
        Check::BareUrl,
    );
}

#[test]
fn a_link_target_containing_parentheses_matches_to_the_first_close() {
    // Markdown's own parsers stop at the first `)`. Being stricter here would
    // report a false defect on every link to a disambiguation page.
    silent("[a](b(c)", Check::LinkSyntaxInvalid);
}

#[test]
fn an_unterminated_code_span_does_not_swallow_the_rest_of_the_line() {
    // A lone backtick is prose. Blanking from it to end-of-line would hide a
    // genuinely broken link sitting after it - the check would go quiet
    // exactly when the line is at its messiest.
    fires_once_at(
        "a ` stray tick then [broken](",
        Check::LinkSyntaxInvalid,
        1,
        1,
    );
}

#[test]
fn a_double_backtick_span_hides_a_url_from_the_bare_url_check() {
    // CommonMark opens a span with a run of backticks and closes it with a run
    // of exactly the same length. Pairing single backticks read the two-tick
    // delimiters as empty spans and left the URL between them in prose.
    silent("run ``curl https://example.com`` first", Check::BareUrl);
}

#[test]
fn a_double_backtick_span_holding_a_backtick_hides_its_brackets() {
    // The documented way to show a literal backtick. The inner tick must not
    // end the span, or the bracket after it counts as an unbalanced link.
    silent("type `` ` [ `` to begin", Check::LinkSyntaxInvalid);
    silent("the span ``[a`` is code", Check::LinkSyntaxInvalid);
}

#[test]
fn a_span_closes_at_the_next_run_of_its_own_length_only() {
    // The single tick inside is a different length, so the span runs to the
    // closing pair and the broken link after it is the only finding.
    fires_once_at("``a`b`` then [broken](", Check::LinkSyntaxInvalid, 1, 1);
    silent("``a`b https://example.com`` end", Check::BareUrl);
}

#[test]
fn a_longer_run_inside_a_span_is_skipped_whole() {
    // Three ticks contain a run of two, but a run is one unit: the span opened
    // by two ticks closes only at the later pair.
    silent("``a ``` https://example.com`` end", Check::BareUrl);
}

#[test]
fn an_opening_run_with_no_closer_is_literal_and_later_spans_still_count() {
    // No run of three follows the opener, so it is plain text; the single-tick
    // span after it is still code.
    silent("``` stray `https://example.com`", Check::BareUrl);
    fires_once_at("`` stray then https://example.com", Check::BareUrl, 1, 15);
}

#[test]
fn two_nested_brackets_in_a_row_terminate() {
    // `[x[a][b](u)`: the link-text scanner consumes `[a]` as a nested pair and
    // resumes on the `[` of `[b]`, which is the only shape where it enters the
    // nested branch twice running. An implementation that resumes one
    // character early re-finds the `]` it just consumed and never advances, so
    // this input hangs rather than reporting anything.
    let findings = of_kind("[x[a][b](u)", Check::LinkSyntaxInvalid);
    assert_eq!(findings.len(), 1, "{findings:?}");
    // `[x[a]` survives the blanking: two `[` against one `]`.
    assert!(
        findings[0].message.contains("2 `[` against 1 `]`"),
        "{}",
        findings[0].message
    );
}

#[test]
fn the_message_names_the_counts_that_disagree() {
    // The counts, not just the fact of a mismatch. A message that reads the
    // same for "one bracket too many" and "one too few" leaves the reader to
    // re-count the line by hand.
    let found = of_kind("[text without a close", Check::LinkSyntaxInvalid);
    assert!(
        found[0].message.contains("1 `[` against 0 `]`"),
        "{}",
        found[0].message
    );
    let found = of_kind("text] without an open", Check::LinkSyntaxInvalid);
    assert!(
        found[0].message.contains("0 `[` against 1 `]`"),
        "{}",
        found[0].message
    );
    let found = of_kind("[text](url", Check::LinkSyntaxInvalid);
    assert!(
        found[0].message.contains("1 `(` against 0 `)`"),
        "{}",
        found[0].message
    );
}

#[test]
fn a_definition_with_no_space_after_the_colon_still_declares_its_url() {
    // CommonMark makes the whitespace between `:` and the destination
    // optional. Finding the destination by looking for *whitespace* rather
    // than for the first non-whitespace character works on every spaced
    // definition and fails on this one, leaving the URL bare.
    silent("[ref]:https://example.com", Check::BareUrl);
}

#[test]
fn a_long_label_does_not_move_where_the_destination_starts() {
    // The destination begins two characters past the `]` - skipping the `]`
    // and the `:`. Any arithmetic that scales with the label's length instead
    // lands inside a long URL and leaves its first half looking bare.
    silent("[unreleased-notes]: https://example.com/x", Check::BareUrl);
}

#[test]
fn link_checks_report_the_right_line_number() {
    let content = "clean\n\n[broken](\n";
    fires_once_at(content, Check::LinkSyntaxInvalid, 3, 1);
}