1use std::fmt;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Location {
10 pub line: usize,
11 pub column: usize,
12}
13
14impl fmt::Display for Location {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 write!(f, "line {}, column {}", self.line, self.column)
17 }
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22#[error("duplicate attribute '{path}' at {duplicate} (first defined at {first})")]
23pub struct DuplicateAttr {
24 pub path: String,
26 pub first: Location,
28 pub duplicate: Location,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[non_exhaustive]
40pub enum Severity {
41 Warning,
42 Error,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
47#[non_exhaustive]
48pub enum ValidationError {
49 #[error("parse error at {location}: {message}")]
51 ParseError { message: String, location: Location },
52 #[error(transparent)]
54 DuplicateAttribute(DuplicateAttr),
55 #[error("follows cycle at {location}: {}", format_edges(&cycle.edges))]
57 FollowsCycle {
58 cycle: crate::follows::Cycle,
59 location: Location,
60 },
61 #[error("stale follows at {location}: {} -> {} (source no longer present in flake.lock)", edge.source, edge.follows)]
65 FollowsStale {
66 edge: crate::follows::Edge,
69 location: Location,
70 },
71 #[error("follows target not a top-level input at {location}: {} -> {}", edge.source, edge.follows)]
74 FollowsTargetNotToplevel {
75 edge: crate::follows::Edge,
76 location: Location,
77 },
78 #[error("contradicting follows at {location}: {}", format_edges(edges))]
81 FollowsContradiction {
82 edges: Vec<crate::follows::Edge>,
83 location: Location,
84 },
85 #[error(
89 "stale-lock follows at {location}: {source_path} -> {declared_target} (flake.lock resolves to {}; run `nix flake lock`)",
90 format_lock_target(lock_target)
91 )]
92 FollowsStaleLock {
93 source_path: crate::follows::AttrPath,
98 declared_target: crate::follows::AttrPath,
100 lock_target: Option<crate::follows::AttrPath>,
103 location: Location,
104 },
105 #[error("follows depth exceeded at {location}: {} -> {} reached depth {depth} (max {max_depth})", edge.source, edge.follows)]
107 FollowsDepthExceeded {
108 edge: crate::follows::Edge,
109 depth: usize,
110 max_depth: usize,
111 location: Location,
112 },
113}
114
115fn format_edges(edges: &[crate::follows::Edge]) -> String {
116 edges
117 .iter()
118 .map(|e| format!("{} -> {}", e.source, e.follows))
119 .collect::<Vec<_>>()
120 .join("; ")
121}
122
123fn format_lock_target(target: &Option<crate::follows::AttrPath>) -> String {
124 match target {
125 Some(t) => t.to_string(),
126 None => "<none>".to_string(),
127 }
128}
129
130impl ValidationError {
131 pub fn severity(&self) -> Severity {
133 match self {
134 ValidationError::FollowsStale { .. } | ValidationError::FollowsStaleLock { .. } => {
135 Severity::Warning
136 }
137 _ => Severity::Error,
138 }
139 }
140}
141
142#[derive(Debug, Default)]
144pub struct ValidationResult {
145 pub errors: Vec<ValidationError>,
146 pub warnings: Vec<ValidationError>,
147}
148
149impl ValidationResult {
150 pub fn is_ok(&self) -> bool {
151 self.errors.is_empty()
152 }
153
154 pub fn has_errors(&self) -> bool {
155 !self.errors.is_empty()
156 }
157
158 pub fn has_warnings(&self) -> bool {
159 !self.warnings.is_empty()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::follows::{AttrPath, Cycle, Edge, EdgeOrigin};
167 use crate::input::Range;
168
169 fn declared_edge(source: &str, follows: &str) -> Edge {
170 Edge {
171 source: AttrPath::parse(source).unwrap(),
172 follows: AttrPath::parse(follows).unwrap(),
173 origin: EdgeOrigin::Declared {
174 range: Range { start: 0, end: 0 },
175 },
176 }
177 }
178
179 fn loc() -> Location {
180 Location { line: 1, column: 1 }
181 }
182
183 #[test]
184 fn severity_classification() {
185 let cases: Vec<(ValidationError, Severity)> = vec![
186 (
187 ValidationError::ParseError {
188 message: "x".into(),
189 location: loc(),
190 },
191 Severity::Error,
192 ),
193 (
194 ValidationError::DuplicateAttribute(DuplicateAttr {
195 path: "a".into(),
196 first: loc(),
197 duplicate: loc(),
198 }),
199 Severity::Error,
200 ),
201 (
202 ValidationError::FollowsCycle {
203 cycle: Cycle {
204 edges: vec![declared_edge("a", "a")],
205 },
206 location: loc(),
207 },
208 Severity::Error,
209 ),
210 (
211 ValidationError::FollowsStale {
212 edge: declared_edge("a.b", "c"),
213 location: loc(),
214 },
215 Severity::Warning,
216 ),
217 (
218 ValidationError::FollowsTargetNotToplevel {
219 edge: declared_edge("a.b", "missing"),
220 location: loc(),
221 },
222 Severity::Error,
223 ),
224 (
225 ValidationError::FollowsContradiction {
226 edges: vec![declared_edge("a.b", "x"), declared_edge("a.b", "y")],
227 location: loc(),
228 },
229 Severity::Error,
230 ),
231 (
232 ValidationError::FollowsStaleLock {
233 source_path: AttrPath::parse("a.b").unwrap(),
234 declared_target: AttrPath::parse("x").unwrap(),
235 lock_target: Some(AttrPath::parse("y").unwrap()),
236 location: loc(),
237 },
238 Severity::Warning,
239 ),
240 (
241 ValidationError::FollowsDepthExceeded {
242 edge: declared_edge("a.b", "x"),
243 depth: 5,
244 max_depth: 4,
245 location: loc(),
246 },
247 Severity::Error,
248 ),
249 ];
250 for (err, want) in cases {
251 assert_eq!(err.severity(), want, "unexpected severity for {err:?}");
252 }
253 }
254}