spec_driven_docs/gates/
comparison_one_reference_per_cell.rs1use crate::domain::finding::Finding;
9use crate::domain::rule_id::RuleId;
10use crate::gates::{GateCtx, GateResult, Violation, read_text};
11
12pub 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
25pub 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}