Skip to main content

flake_edit/validate/
error.rs

1//! [`ValidationError`], [`Severity`], and [`Location`].
2//!
3//! [`Severity`] separates fatal errors from non-fatal warnings.
4
5use std::fmt;
6
7/// 1-indexed line/column for error reporting.
8#[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/// A duplicate attribute and where its two definitions sit.
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22#[error("duplicate attribute '{path}' at {duplicate} (first defined at {first})")]
23pub struct DuplicateAttr {
24    /// Attribute path, e.g. `a.b.c` or `inputs.nixpkgs.url`.
25    pub path: String,
26    /// Location of the first occurrence.
27    pub first: Location,
28    /// Location of the duplicate occurrence.
29    pub duplicate: Location,
30}
31
32/// Severity classification for [`ValidationError`].
33///
34/// Errors abort the edit; warnings do not. Both flavours land in
35/// [`ValidationResult::errors`] and [`ValidationResult::warnings`]
36/// respectively, populated by [`super::validate_full`] and
37/// [`super::validate_speculative`].
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[non_exhaustive]
40pub enum Severity {
41    Warning,
42    Error,
43}
44
45/// Errors raised while parsing or analysing a flake.
46#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
47#[non_exhaustive]
48pub enum ValidationError {
49    /// rnix could not parse the source.
50    #[error("parse error at {location}: {message}")]
51    ParseError { message: String, location: Location },
52    /// Duplicate attribute in an attribute set.
53    #[error(transparent)]
54    DuplicateAttribute(DuplicateAttr),
55    /// A declared `inputs.X.inputs.Y.follows` chain forms a cycle.
56    #[error("follows cycle at {location}: {}", format_edges(&cycle.edges))]
57    FollowsCycle {
58        cycle: crate::follows::Cycle,
59        location: Location,
60    },
61    /// A follows declaration in `flake.nix` points at a nested input that no
62    /// longer exists in `flake.lock`. Warning: the auto-follow pass should
63    /// drop the declaration on the next run.
64    #[error("stale follows at {location}: {} -> {} (source no longer present in flake.lock)", edge.source, edge.follows)]
65    FollowsStale {
66        /// Declared edge whose source has dropped out of `flake.lock`'s
67        /// nested-input universe.
68        edge: crate::follows::Edge,
69        location: Location,
70    },
71    /// A follows target points at something that is not a top-level input,
72    /// e.g. `inputs.foo.inputs.bar.follows = "does-not-exist"`.
73    #[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    /// Two follows declarations share a source path but disagree on the
79    /// target.
80    #[error("contradicting follows at {location}: {}", format_edges(edges))]
81    FollowsContradiction {
82        edges: Vec<crate::follows::Edge>,
83        location: Location,
84    },
85    /// A declared follows whose target diverges from the lockfile's
86    /// resolution of the same source path. Warning: the user edited
87    /// `flake.nix` but never ran `nix flake lock`.
88    #[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 of the declared follows, e.g. `crane.nixpkgs`.
94        ///
95        /// Renamed from `source` because thiserror treats a field named
96        /// `source` as the error's `#[source]`.
97        source_path: crate::follows::AttrPath,
98        /// Target the declaration in `flake.nix` asks for.
99        declared_target: crate::follows::AttrPath,
100        /// Target the lockfile resolves the same source to. `None` if the
101        /// lockfile has the path but no follows attached.
102        lock_target: Option<crate::follows::AttrPath>,
103        location: Location,
104    },
105    /// A follows path is deeper than the configured graph traversal bound.
106    #[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    /// Severity for this variant.
132    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/// Errors and warnings collected during a single validation pass.
143#[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}