Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger-ui

tests/money/mod.rs

//! **The money judge, the lexer and the scanner, shared** by holger-ui's two halves' guards:
//! the open server's (`crates/holger-ui-server/tests/open_build_speaks_no_money.rs`) and the
//! open bundle's (`tests/open_bundle_speaks_no_money.rs`). Moved here from the server's guard
//! (A28 M13), so the two halves judge with ONE text. The judge and the lexer are Gunnar's,
//! verbatim, with the words Holger adds (holger's `server/lib/tests/money/judge.rs`, the same
//! text). When one changes, the others are changed in the same breath.
#![allow(dead_code)]

use std::path::{Path, PathBuf};
pub const PAID_FEATURES: &[&str] = &["metered"];

/// Words the open product has no use for. See the module docs.
pub const MONEY: &[&str] = &[
    "price", "prices", "priced", "pricing",
    "pay", "pays", "paying", "paid", "payable", "payment", "payments", "paywall",
    "buy", "buys", "buying", "bought", "buyer", "buyers",
    "purchase", "purchases", "purchased", "purchasing",
    "sell", "sells", "selling", "sold", "seller", "sellers",
    "bill", "bills", "billed", "billing", "billable",
    "invoice", "invoices", "invoicing",
    "checkout", "checkouts",
    "money", "currency", "currencies", "sek", "eur", "usd",
    "stripe",
    "plan", "plans",
    "quota", "quotas",
    "subscription", "subscriptions", "subscribe",
    "renew", "renews", "renewal", "renewals",
    "refund", "refunds", "revenue",
    // Holger's own (its first guard's list, 2026-09-15).
    "unpaid",
];

/// Words that are money only when nothing in the same text makes them
/// technical. See the module docs for the rule.
pub const CONTEXTUAL: &[&str] = &[
    "plan", "plans", "checkout", "checkouts", "renew", "renews", "renewal", "renewals",
    "subscription", "subscriptions", "subscribe",
];

/// What makes a [`CONTEXTUAL`] word technical: networking and DHCP, git's
/// worktree, the event bus (a `Watch` subscription, a replication or webhook
/// subscriber), the data-set grow and the console reset.
pub const TECHNICAL_CONTEXT: &[&str] = &[
    // network / DHCP
    "interface", "interfaces", "lease", "leases", "dhcp", "dhcprequest", "address", "addresses",
    "route", "routes", "network", "net",
    // the event bus
    "event", "events", "bus", "watch", "watchrequest", "replication", "webhook", "runner",
    "subscriber", "stream",
    // git
    "git", "tree", "worktree", "commit", "branch", "clone", "entry", "build",
    // grow / install / reset
    "grow", "growth", "sectors", "blocks", "resize", "layout", "install", "reset", "passkeys",
    // Holger's (A28 M13): a RETENTION plan, a MIGRATION plan and its dry run, the download
    // ADMISSION plan, a CERTIFICATE renewal, a box's DOOR.
    "retention", "migration", "dry", "admission", "admit", "certificate", "cert", "door",
];

/// `"a lapsed tenant renews"` -> `["a", "lapsed", "tenant", "renews"]`.
pub fn words(text: &str) -> Vec<String> {
    text.split(|c: char| !c.is_ascii_alphanumeric())
        .filter(|w| !w.is_empty())
        .map(|w| w.to_ascii_lowercase())
        .collect()
}

/// **The one judge**: the money words in `text`, after the contextual rule.
pub fn leaks_in(text: &str) -> Vec<String> {
    let ws = words(text);
    let technical = ws.iter().any(|w| TECHNICAL_CONTEXT.contains(&w.as_str()));
    let mut out = Vec::new();
    for w in &ws {
        if !MONEY.contains(&w.as_str()) {
            continue;
        }
        if CONTEXTUAL.contains(&w.as_str()) && technical {
            continue;
        }
        if w == "stripe" && !(text.contains("Stripe") || text.contains("STRIPE") || text.contains("stripe.com")) {
            continue;
        }
        out.push(w.clone());
    }
    out.sort();
    out.dedup();
    out
}


// ── Gunnar's lexer and scanner, verbatim ─────────────────────────────────────

/// One string literal, where it is, and whether it sits in an attribute.
#[derive(Debug)]
pub struct Literal {
    pub line: usize,
    pub text: String,
}

/// What one file yields: its literals outside skipped items, and the `mod`
/// names declared under a skipping attribute (their files are skipped whole).
#[derive(Default)]
pub struct Scanned {
    pub literals: Vec<Literal>,
    pub skipped_mods: Vec<String>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Tok {
    Str(String),
    Ident(String),
    Punct(char),
}

/// A small Rust lexer: comments dropped, string/char/raw/byte literals as
/// single tokens, lifetimes told apart from char literals. Enough to find
/// every literal and every attribute; not a parser.
pub fn lex(src: &str) -> Vec<(usize, Tok)> {
    let b: Vec<char> = src.chars().collect();
    let mut out = Vec::new();
    let (mut i, mut line) = (0usize, 1usize);
    while i < b.len() {
        let c = b[i];
        if c == '\n' {
            line += 1;
            i += 1;
            continue;
        }
        if c.is_whitespace() {
            i += 1;
            continue;
        }
        // comments
        if c == '/' && b.get(i + 1) == Some(&'/') {
            while i < b.len() && b[i] != '\n' {
                i += 1;
            }
            continue;
        }
        if c == '/' && b.get(i + 1) == Some(&'*') {
            let mut depth = 0;
            while i < b.len() {
                if b[i] == '/' && b.get(i + 1) == Some(&'*') {
                    depth += 1;
                    i += 2;
                } else if b[i] == '*' && b.get(i + 1) == Some(&'/') {
                    depth -= 1;
                    i += 2;
                    if depth == 0 {
                        break;
                    }
                } else {
                    if b[i] == '\n' {
                        line += 1;
                    }
                    i += 1;
                }
            }
            continue;
        }
        // raw strings: r"..", r#".."#, br#".."#
        let raw_start = if c == 'r' && matches!(b.get(i + 1), Some('"') | Some('#')) {
            Some(i + 1)
        } else if c == 'b' && b.get(i + 1) == Some(&'r') && matches!(b.get(i + 2), Some('"') | Some('#')) {
            Some(i + 2)
        } else {
            None
        };
        if let Some(mut j) = raw_start {
            let mut hashes = 0;
            while b.get(j) == Some(&'#') {
                hashes += 1;
                j += 1;
            }
            if b.get(j) == Some(&'"') {
                let start_line = line;
                j += 1;
                let mut text = String::new();
                'raw: while j < b.len() {
                    if b[j] == '"' && (0..hashes).all(|k| b.get(j + 1 + k) == Some(&'#')) {
                        j += 1 + hashes;
                        break 'raw;
                    }
                    if b[j] == '\n' {
                        line += 1;
                    }
                    text.push(b[j]);
                    j += 1;
                }
                out.push((start_line, Tok::Str(text)));
                i = j;
                continue;
            }
        }
        // strings, byte strings
        if c == '"' || (c == 'b' && b.get(i + 1) == Some(&'"')) {
            let start_line = line;
            let mut j = if c == 'b' { i + 2 } else { i + 1 };
            let mut text = String::new();
            while j < b.len() {
                match b[j] {
                    '\\' => {
                        // `\` + newline is a continuation: the indentation
                        // that follows is not part of the text.
                        if b.get(j + 1) == Some(&'\n') {
                            line += 1;
                            j += 2;
                            while j < b.len() && b[j].is_whitespace() {
                                if b[j] == '\n' {
                                    line += 1;
                                }
                                j += 1;
                            }
                            text.push(' ');
                            continue;
                        }
                        if let Some(n) = b.get(j + 1) {
                            text.push(match n {
                                'n' => '\n',
                                't' => ' ',
                                other => *other,
                            });
                        }
                        j += 2;
                    }
                    '"' => {
                        j += 1;
                        break;
                    }
                    ch => {
                        if ch == '\n' {
                            line += 1;
                        }
                        text.push(ch);
                        j += 1;
                    }
                }
            }
            out.push((start_line, Tok::Str(text)));
            i = j;
            continue;
        }
        // char literal or lifetime
        if c == '\'' {
            if b.get(i + 1) == Some(&'\\') {
                let mut j = i + 2;
                while j < b.len() && b[j] != '\'' {
                    j += 1;
                }
                i = j + 1;
                continue;
            }
            if b.get(i + 2) == Some(&'\'') {
                i += 3;
                continue;
            }
            // a lifetime: skip the tick, the ident follows
            i += 1;
            continue;
        }
        if c.is_alphanumeric() || c == '_' {
            let mut j = i;
            let mut ident = String::new();
            while j < b.len() && (b[j].is_alphanumeric() || b[j] == '_') {
                ident.push(b[j]);
                j += 1;
            }
            out.push((line, Tok::Ident(ident)));
            i = j;
            continue;
        }
        out.push((line, Tok::Punct(c)));
        i += 1;
    }
    out
}

/// Does this attribute's token text remove the item from an OPEN, non-test
/// build? `cfg(test)`, `cfg(all(test, …))`, `test`, `tokio::test`,
/// `cfg(feature = "metered")` — and never anything under `not(…)`.
pub fn removes_from_open_build(attr: &[Tok]) -> bool {
    let text: String = attr
        .iter()
        .map(|t| match t {
            Tok::Str(s) => format!("\"{s}\""),
            Tok::Ident(s) => format!("{s} "),
            Tok::Punct(c) => c.to_string(),
        })
        .collect();
    let t = text.replace(' ', "");
    if t == "test" || t == "tokio::test" || t.starts_with("tokio::test(") {
        return true;
    }
    if !t.starts_with("cfg(") || t.contains("not(") {
        return false;
    }
    t == "cfg(test)"
        || t.starts_with("cfg(all(test")
        || PAID_FEATURES.iter().any(|f| t.contains(&format!("feature=\"{f}\"")))
        || (t.starts_with("cfg(all(") && t.contains("test,"))
}

pub fn scan_source(src: &str) -> Scanned {
    let toks = lex(src);
    let mut out = Scanned::default();
    let mut i = 0usize;
    // Inner attribute `#![cfg(...)]` at the top that removes the whole file.
    while i < toks.len() {
        match &toks[i].1 {
            Tok::Punct('#') => {
                let inner = matches!(toks.get(i + 1), Some((_, Tok::Punct('!'))));
                let open = if inner { i + 2 } else { i + 1 };
                if !matches!(toks.get(open), Some((_, Tok::Punct('[')))) {
                    i += 1;
                    continue;
                }
                // the attribute's tokens
                let mut depth = 0i32;
                let mut j = open;
                let start = open + 1;
                loop {
                    match toks.get(j).map(|t| &t.1) {
                        Some(Tok::Punct('[')) => depth += 1,
                        Some(Tok::Punct(']')) => {
                            depth -= 1;
                            if depth == 0 {
                                break;
                            }
                        }
                        None => break,
                        _ => {}
                    }
                    j += 1;
                }
                let attr: Vec<Tok> = toks[start..j.min(toks.len())].iter().map(|t| t.1.clone()).collect();
                // `doc` is a comment by another spelling; `cfg`/`cfg_attr`
                // name features, which no user or operator reads.
                let is_doc = matches!(attr.first(), Some(Tok::Ident(s)) if s == "doc" || s == "cfg" || s == "cfg_attr");
                if removes_from_open_build(&attr) {
                    if inner {
                        return Scanned::default();
                    }
                    i = skip_item(&toks, j + 1, &mut out.skipped_mods);
                    continue;
                }
                if !is_doc {
                    for (line, t) in &toks[start..j.min(toks.len())] {
                        if let Tok::Str(s) = t {
                            out.literals.push(Literal { line: *line, text: s.clone() });
                        }
                    }
                }
                i = j + 1;
            }
            Tok::Str(s) => {
                out.literals.push(Literal { line: toks[i].0, text: s.clone() });
                i += 1;
            }
            _ => i += 1,
        }
    }
    out
}

/// Skip the item that starts at `i` (after its attribute): further attributes,
/// then tokens up to a `;` or `,` at depth 0, or through the first balanced
/// `{…}`. Returns the index after it.
pub fn skip_item(toks: &[(usize, Tok)], mut i: usize, mods: &mut Vec<String>) -> usize {
    // stacked attributes
    while matches!(toks.get(i), Some((_, Tok::Punct('#')))) && matches!(toks.get(i + 1), Some((_, Tok::Punct('[')))) {
        let mut depth = 0;
        let mut j = i + 1;
        while j < toks.len() {
            match toks[j].1 {
                Tok::Punct('[') => depth += 1,
                Tok::Punct(']') => {
                    depth -= 1;
                    if depth == 0 {
                        break;
                    }
                }
                _ => {}
            }
            j += 1;
        }
        i = j + 1;
    }
    let mut depth = 0i32;
    // `<…>` at depth 0 — a signature's `Result<A, B>`, a field's `HashMap<K, V>` — so the
    // comma inside generics does not end the item (the item went on, unskipped, into its
    // body). `->` and `=>` are arrows, not a closing bracket.
    let mut angle = 0i32;
    let mut prev_mod = false;
    while i < toks.len() {
        let arrow = matches!(toks.get(i.wrapping_sub(1)).map(|t| &t.1), Some(Tok::Punct('-')) | Some(Tok::Punct('=')));
        match &toks[i].1 {
            Tok::Punct('<') if depth == 0 => angle += 1,
            Tok::Punct('>') if depth == 0 && !arrow && angle > 0 => angle -= 1,
            Tok::Ident(s) if s == "mod" => prev_mod = true,
            Tok::Ident(s) if prev_mod => {
                mods.push(s.clone());
                prev_mod = false;
            }
            Tok::Punct('(') | Tok::Punct('[') => depth += 1,
            Tok::Punct(')') | Tok::Punct(']') => depth -= 1,
            Tok::Punct('{') => {
                depth += 1;
            }
            Tok::Punct('}') => {
                depth -= 1;
                if depth == 0 {
                    return i + 1;
                }
                if depth < 0 {
                    return i;
                }
            }
            Tok::Punct(';') | Tok::Punct(',') if depth == 0 && angle == 0 => return i + 1,
            _ => {}
        }
        i += 1;
    }
    i
}

pub fn sources(dir: &Path) -> Vec<PathBuf> {
    let mut found = Vec::new();
    let mut stack = vec![dir.to_path_buf()];
    while let Some(here) = stack.pop() {
        for entry in std::fs::read_dir(&here).unwrap_or_else(|e| panic!("{}: {e}", here.display())).filter_map(Result::ok) {
            let path = entry.path();
            if path.is_dir() {
                stack.push(path);
            } else if path.extension().is_some_and(|e| e == "rs") {
                found.push(path);
            }
        }
    }
    found.sort();
    found
}

#[test]
fn the_judge_reads_the_sentence() {
    assert_eq!(leaks_in("Pay at Stripe"), vec!["pay", "stripe"]);
    assert!(leaks_in("payload").is_empty());
    assert_eq!(leaks_in("Nothing is sold by this site."), vec!["sold"]);
    assert!(leaks_in("the passkey door").is_empty());
}

#[test]
fn the_scanner_skips_what_the_open_build_does_not_contain() {
    let src = r####"
        fn open() { let _ = "the console"; }
        #[cfg(feature = "metered")]
        pub const BUY_PAGE: &str = "/signup?plan=account";
        #[cfg(feature = "metered")]
        fn relay() { let _ = "Nothing is sold by this site."; }
        #[cfg(test)]
        mod tests { fn t() { let _ = "Pay at Stripe"; } }
        fn tail() { let _ = "after"; }
    "####;
    let texts: Vec<String> = scan_source(src).literals.into_iter().map(|l| l.text).collect();
    assert_eq!(texts, vec!["the console", "after"]);
}