1use crate::config::{Config, RuleSeverity};
10use crate::diagnostic::Finding;
11use crate::lock::Lock;
12use crate::model::Graph;
13use crate::rules::{staleness, structural};
14
15pub fn run(graph: &Graph, lock: Option<&Lock>, config: &Config) -> Vec<Finding> {
18 let mut findings = Vec::new();
19 if let Some(lock) = lock {
20 findings.extend(staleness::evaluate(graph, lock));
21 }
22 findings.extend(structural::evaluate(graph));
23
24 findings.retain_mut(|finding| {
25 let severity = config
27 .rules
28 .get(&finding.name)
29 .map(|r| r.severity)
30 .unwrap_or(RuleSeverity::Warn);
31 if severity == RuleSeverity::Off {
32 return false;
33 }
34 if config.is_rule_ignored(&finding.name, &finding.subject) {
35 return false;
36 }
37 finding.severity = severity;
38 true
39 });
40
41 findings.sort_by(|a, b| {
42 a.name
43 .cmp(&b.name)
44 .then_with(|| a.subject.cmp(&b.subject))
45 .then_with(|| a.message.cmp(&b.message))
46 });
47 findings
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use crate::compose::compose;
54 use crate::model::{Edge, GraphSet, Node};
55 use serde_json::json;
56
57 fn fs_node() -> Node {
58 Node::new(
59 json!({ "type": "file", "hash": "b3:x" })
60 .as_object()
61 .unwrap()
62 .clone(),
63 )
64 }
65
66 fn composed_unresolved() -> Graph {
67 let mut fs = Graph::labeled("fs");
68 fs.set_node("index.md", fs_node());
69 fs.add_edge(Edge::new("index.md", "gone.md"));
70 compose(&GraphSet::new(vec![fs]))
71 }
72
73 #[test]
74 fn applies_default_warn_severity() {
75 let graph = composed_unresolved();
76 let findings = run(&graph, None, &Config::defaults());
77 let unresolved = findings
78 .iter()
79 .find(|f| f.name == "unresolved-edge")
80 .unwrap();
81 assert_eq!(unresolved.severity, RuleSeverity::Warn);
82 }
83
84 #[test]
85 fn config_promotes_to_error() {
86 let dir = tempfile::TempDir::new().unwrap();
87 std::fs::write(
88 dir.path().join("drft.toml"),
89 "[rules]\nunresolved-edge = \"error\"\n",
90 )
91 .unwrap();
92 let config = Config::load(dir.path()).unwrap();
93 let findings = run(&composed_unresolved(), None, &config);
94 let unresolved = findings
95 .iter()
96 .find(|f| f.name == "unresolved-edge")
97 .unwrap();
98 assert_eq!(unresolved.severity, RuleSeverity::Error);
99 }
100
101 #[test]
102 fn config_off_silences_rule() {
103 let dir = tempfile::TempDir::new().unwrap();
104 std::fs::write(
105 dir.path().join("drft.toml"),
106 "[rules]\nunresolved-edge = \"off\"\n",
107 )
108 .unwrap();
109 let config = Config::load(dir.path()).unwrap();
110 let findings = run(&composed_unresolved(), None, &config);
111 assert!(!findings.iter().any(|f| f.name == "unresolved-edge"));
112 }
113
114 #[test]
115 fn ignore_glob_drops_subject() {
116 let dir = tempfile::TempDir::new().unwrap();
117 std::fs::write(
118 dir.path().join("drft.toml"),
119 "[rules.unresolved-edge]\nignore = [\"index.md\"]\n",
120 )
121 .unwrap();
122 let config = Config::load(dir.path()).unwrap();
123 let findings = run(&composed_unresolved(), None, &config);
124 assert!(!findings.iter().any(|f| f.name == "unresolved-edge"));
125 }
126}