Skip to main content

spec_driven_docs/gates/
comparison_one_reference_per_cell.rs

1//! Gate: a comparison cell carries at most one reference.
2//!
3//! A cell with two links is two claims sharing one verdict, and the reader
4//! cannot tell which reference backs which half. Cells are the fields
5//! between the table's pipes; what a reference points at is review's
6//! business.
7
8use crate::domain::finding::Finding;
9use crate::domain::rule_id::RuleId;
10use crate::gates::{GateCtx, GateResult, Violation, read_text};
11
12/// The rules this gate can cite.
13pub const CITES: &[RuleId] = &[RuleId::CellCarriesOneReference];
14
15fn cell_has_extra_references(line: &str) -> bool {
16    let fields: Vec<&str> = line.split('|').collect();
17    if fields.len() < 3 {
18        return false;
19    }
20    fields[1..fields.len() - 1]
21        .iter()
22        .any(|cell| cell.matches("](").count() > 1)
23}
24
25/// Judge every file pre-commit passed.
26///
27/// # Errors
28///
29/// [`crate::gates::GateError::Io`] when a file cannot be read.
30pub fn run(ctx: &GateCtx, files: &[String]) -> GateResult {
31    let mut violations = Vec::new();
32    for file in files {
33        for (number, line) in read_text(ctx, file)?.lines().enumerate() {
34            if line.starts_with('|') && cell_has_extra_references(line) {
35                violations.push(Violation::Finding(Finding::on_line(
36                    RuleId::CellCarriesOneReference,
37                    file,
38                    number + 1,
39                    "",
40                )));
41            }
42        }
43    }
44    Ok(violations)
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    fn run_on(text: &str) -> Vec<String> {
52        let dir = tempfile::tempdir().unwrap();
53        std::fs::write(dir.path().join("comparison.md"), text).unwrap();
54        let ctx = GateCtx::new(dir.path().to_str().unwrap());
55        run(&ctx, &["comparison.md".to_string()])
56            .unwrap()
57            .iter()
58            .map(ToString::to_string)
59            .collect()
60    }
61
62    #[test]
63    fn accepts_one_reference_per_cell() {
64        assert!(run_on("| [Case](#case) | Subject |\n| Runs | ✅ yes |\n").is_empty());
65    }
66
67    #[test]
68    fn rejects_two_references_in_one_cell() {
69        let out = run_on("| Runs [a](#a) [b](#b) | ✅ yes |\n");
70        assert_eq!(
71            out,
72            vec!["FAIL comparison-docs:a-cell-carries-one-reference comparison.md:1".to_string()]
73        );
74    }
75}