Skip to main content

choir_guards/
lib.rs

1//! Source-scanning tripwires over the whole workspace (D77).
2//!
3//! Two invariants here hold by the *absence* of something — an edge in
4//! the dependency graph, a type named in the wrong crate — and an
5//! absence is what no ordinary test can assert. Both are checked by
6//! reading other crates as text: `quarantine` for D40, `invariant_3`
7//! for the canonical-serialization rule.
8//!
9//! **Why they live in a crate of their own.** A test's inputs decide
10//! two things in `gate`: whether the `touched` lane selects it, and
11//! whether a cached green verdict is still valid. Both are computed
12//! from the crate the test lives in — its own files, plus the
13//! dependency closure cargo reports. A scanner's inputs are neither.
14//! `quarantine` sat in `choir-view` and read `choir-node`, which
15//! `choir-view` does not depend on and must not; so editing
16//! `choir-node` neither selected the test nor invalidated its verdict,
17//! and a green could stand over a tree that broke it. Moving the
18//! scanners to the crate they scan is not open either: `choir-hash` is
19//! depended on by everything, so widening *its* inputs to the whole of
20//! `crates/` would invalidate every verdict in the workspace on every
21//! edit.
22//!
23//! This crate is the shape that works, and D77 is its row. It is a leaf — nothing depends
24//! on it, and it depends on nothing — so the `gate-inputs` file beside
25//! its manifest can name the whole of `crates/` and the cost lands on
26//! this crate alone. That file is one line and cannot rot, because the
27//! scanners read the same directory it names.
28//!
29//! The helpers below are shared by both suites and public so that both
30//! can reach them. They are text predicates, not analysis: read the
31//! stated limits in each before trusting a green.
32//!
33//! # Examples
34//!
35//! ```
36//! use choir_guards::{mentions, strip_comments};
37//!
38//! // A type named only in a doc link is not a use of that type.
39//! assert_eq!(strip_comments("/// see [`TreeEntry`]").trim(), "");
40//! // Whole-word, so a longer name is not a hit.
41//! assert!(mentions("let e: TreeEntry = x;", "TreeEntry"));
42//! assert!(!mentions("struct TreeEntryId;", "TreeEntry"));
43//! ```
44
45/// Everything after `//` on a line, gone.
46///
47/// This is what separates prose from code for every scanner here, and
48/// `//` covers `///` and `//!` as well, so a doc comment that names a
49/// quarantined type — including a working intra-doc link to it — is
50/// not a use of it.
51///
52/// Deliberately crude, with two stated limits: a `//` inside a string
53/// literal truncates the line early, which can only ever *hide* a hit
54/// on that same line; and block comments are not stripped, because the
55/// workspace uses none in item bodies.
56pub fn strip_comments(line: &str) -> &str {
57    match line.find("//") {
58        Some(i) => &line[..i],
59        None => line,
60    }
61}
62
63/// Whether `text` names `ident` as a whole word rather than as a
64/// substring, so `Commit` does not match `CommitId`.
65pub fn mentions(text: &str, ident: &str) -> bool {
66    let mut rest = text;
67    while let Some(i) = rest.find(ident) {
68        let before = rest[..i].chars().next_back();
69        let after = rest[i + ident.len()..].chars().next();
70        let boundary = |c: Option<char>| !c.is_some_and(|c| c.is_alphanumeric() || c == '_');
71        if boundary(before) && boundary(after) {
72            return true;
73        }
74        rest = &rest[i + ident.len()..];
75    }
76    false
77}