alef 0.103.6

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Ownership marker recognition shared by stamping and verification.

/// The substring shared by every marker spelling alef emits or must recognize on read-back,
/// matched case-insensitively (see [`line_has_marker`]). A single substring subsumes every
/// capitalization variant produced across backends -- `"auto-generated by alef"`, `"Auto-generated
/// by alef"`, `"Generated by alef"`, `"generated by alef"` -- and also Go's own `"Code generated
/// ... by alef"` convention, which alef should honour rather than fight. Previously this was two
/// case-sensitive literals (`"auto-generated by alef"` / `"Generated by alef"`); that check missed
/// every other capitalization a backend or a hand-adopted file might use, silently refusing to ever
/// stamp or verify the file again. ~keep
const MARKER_SUBSTRING: &str = "generated by alef";

/// The alternate marker used by generated package metadata. ~keep
const MANAGED_MARKER_SUBSTRING: &str = "managed by alef";

/// The alternate marker used by generated R entrypoints. It is only an ownership marker when it
/// appears in a comment header that also says "do not edit". ~keep
const REGENERATION_MARKER_SUBSTRING: &str = "regenerate with `alef";

/// Comment prefixes accepted for regeneration ownership headers. ~keep
const COMMENT_OPENERS: &[&str] = &["//", "#", "/*", "*", "<!--", ";"];

/// Case-insensitive ASCII substring search for `needle` within `haystack`.
///
/// Used instead of `haystack.to_lowercase().contains(needle)` to avoid allocating a lowercased
/// copy of every scanned line: this only ever runs against the first [`super::MARKER_SCAN_LINES`] lines
/// of a file, so the cost is already bounded, but a byte-window scan with
/// [`u8::eq_ignore_ascii_case`] gets the same correctness for free. Marker text is always plain
/// ASCII, so ASCII-only case-folding is sufficient here. ~keep
pub(super) fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
    let haystack = haystack.as_bytes();
    let needle = needle.as_bytes();
    if needle.is_empty() {
        return true;
    }
    if needle.len() > haystack.len() {
        return false;
    }
    haystack
        .windows(needle.len())
        .any(|window| window.eq_ignore_ascii_case(needle))
}

/// Negation cues that, found as the single word immediately before a matched marker substring,
/// mean the phrase is being denied rather than claimed as an ownership marker (`"This file is
/// NOT generated by alef"`). Deliberately narrow -- see the module doc on
/// [`immediately_preceded_by_negation`] and `tests::marker_negation` for exactly which negated
/// forms this catches and which it does not. Over-widening this list risks the opposite,
/// strictly worse failure: a false NEGATIVE that makes alef refuse to ever manage a file it
/// genuinely authored, versus this false POSITIVE's cost of one avoidable write refusal a human
/// reviews. ~keep
const NEGATION_CUES: &[&str] = &[
    "not", "never", "isn't", "wasn't", "aren't", "weren't", "doesn't", "didn't",
];

/// Whether the single word immediately before byte offset `match_start` in `line` is one of
/// [`NEGATION_CUES`], case-insensitively.
///
/// Deliberately scoped to exactly one adjacent word, with no tolerance for intervening
/// punctuation or words -- `"is not generated by alef"` is caught, `"was not, in any real
/// sense, generated by alef"` is not (documented gap; see `tests::marker_negation`). A wider
/// window (scanning the whole line for a negation word anywhere) would defeat alef's own
/// standard header, which pairs the marker with "DO NOT EDIT" on the same line. ~keep
fn immediately_preceded_by_negation(line: &str, match_start: usize) -> bool {
    // ~keep Back up over the leading fragment of the word the match starts *inside* before
    // looking left. The needle can begin mid-word -- `"generated by alef"` is a substring of
    // `"NOT regenerated by alef"` -- and without this the "preceding word" is the fragment
    // `"re"`, so the negation one word further left is never inspected and a file that says it
    // is NOT regenerated by alef reads as an ownership claim. `-` counts as a word character
    // here for the same reason: `"not auto-generated by alef"` matches at `generated`, and
    // stopping at the hyphen would inspect `"auto-"` instead of the cue. This predicate decides whether
    // the write guard may overwrite the file, so that misread is a data-loss path, not a
    // cosmetic one: it stamped a hash line into a hand-written downstream binding module,
    // splitting its `//!` doc block.
    let containing_word_start = line[..match_start]
        .char_indices()
        .rev()
        .take_while(|(_, character)| character.is_alphanumeric() || *character == '_' || *character == '-')
        .last()
        .map_or(match_start, |(index, _)| index);
    let prefix = line[..containing_word_start].trim_end_matches(|character: char| character.is_ascii_whitespace());
    let word_start = prefix
        .rfind(|character: char| character.is_ascii_whitespace())
        .map_or(0, |index| index + 1);
    let word = &prefix[word_start..];
    NEGATION_CUES.iter().any(|cue| word.eq_ignore_ascii_case(cue))
}

/// Case-insensitive ASCII substring search for `needle` within `haystack`, true only when at
/// least one match is not [`immediately_preceded_by_negation`].
///
/// [`contains_ignore_ascii_case`]'s marker-detection call sites route through this instead: a
/// bare substring match accepted `"This file is NOT generated by alef"` as an ownership claim,
/// which is a data-loss path -- that claim decides whether alef's write guard is willing to
/// overwrite the file (`write_files_report`, `crate::cli::pipeline::generate::write`). ~keep
fn contains_unnegated_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
    let haystack_bytes = haystack.as_bytes();
    let needle_bytes = needle.as_bytes();
    if needle_bytes.is_empty() {
        return true;
    }
    if needle_bytes.len() > haystack_bytes.len() {
        return false;
    }
    haystack_bytes
        .windows(needle_bytes.len())
        .enumerate()
        .filter(|(_, window)| window.eq_ignore_ascii_case(needle_bytes))
        .any(|(start, _)| !immediately_preceded_by_negation(haystack, start))
}

/// Whether a single line carries [`MARKER_SUBSTRING`], case-insensitively, and not merely as a
/// negated denial (see [`contains_unnegated_ignore_ascii_case`]).
///
/// The one predicate every marker-scanning function in the parent module shares --
/// [`super::content_has_alef_marker`], [`super::inject_hash_line`], [`super::inject_stamp_line`], and
/// [`super::generated_hash_line`] (which backs [`super::extract_hash`]/[`super::strip_hash_line`]) -- so a line that one
/// of them treats as a marker is treated as one by all of them. They must agree: a marker
/// `content_has_alef_marker` accepts but `inject_hash_line` cannot find is a file claimed by `alef
/// verify` and never stamped, permanently stale. ~keep
pub(super) fn line_has_marker(line: &str) -> bool {
    contains_unnegated_ignore_ascii_case(line, MARKER_SUBSTRING)
        || contains_unnegated_ignore_ascii_case(line, MANAGED_MARKER_SUBSTRING)
        || (is_regeneration_header(line) && contains_unnegated_ignore_ascii_case(line, REGENERATION_MARKER_SUBSTRING))
}

/// Regeneration advice is an ownership marker only in a comment header that explicitly says not
/// to edit the generated output. Handwritten prose may otherwise discuss the command freely.
fn is_regeneration_header(line: &str) -> bool {
    COMMENT_OPENERS
        .iter()
        .any(|prefix| line.trim_start().starts_with(prefix))
        && contains_ignore_ascii_case(line, "do not edit")
}