flake_edit/
validate.rs

1//! Validation for Nix expressions.
2
3use std::collections::HashMap;
4use std::collections::hash_map::Entry;
5use std::fmt;
6
7use rnix::{Root, SyntaxKind, SyntaxNode, TextRange};
8
9/// Location information for error reporting (1-indexed).
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Location {
12    pub line: usize,
13    pub column: usize,
14}
15
16impl fmt::Display for Location {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        write!(f, "line {}, column {}", self.line, self.column)
19    }
20}
21
22/// Information about a duplicate attribute.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct DuplicateAttr {
25    /// The attribute path, e.g., "a.b.c" or "inputs.nixpkgs.url".
26    pub path: String,
27    /// Location of the first occurrence.
28    pub first: Location,
29    /// Location of the duplicate occurrence.
30    pub duplicate: Location,
31}
32
33impl fmt::Display for DuplicateAttr {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(
36            f,
37            "duplicate attribute '{}' at {} (first defined at {})",
38            self.path, self.duplicate, self.first
39        )
40    }
41}
42
43/// Validation errors that can occur when parsing Nix expressions.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum ValidationError {
46    /// Syntax error from rnix parser.
47    ParseError { message: String, location: Location },
48    /// Duplicate attribute in an attribute set.
49    DuplicateAttribute(DuplicateAttr),
50}
51
52impl fmt::Display for ValidationError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            ValidationError::ParseError { message, location } => {
56                write!(f, "parse error at {}: {}", location, message)
57            }
58            ValidationError::DuplicateAttribute(dup) => write!(f, "{}", dup),
59        }
60    }
61}
62
63/// Result of validation containing any errors found.
64#[derive(Debug, Default)]
65pub struct ValidationResult {
66    pub errors: Vec<ValidationError>,
67}
68
69impl ValidationResult {
70    pub fn is_ok(&self) -> bool {
71        self.errors.is_empty()
72    }
73
74    pub fn has_errors(&self) -> bool {
75        !self.errors.is_empty()
76    }
77}
78
79/// Validator for Nix expressions.
80pub struct Validator {
81    source: String,
82    /// Byte offsets where each line starts (for computing line/column).
83    line_starts: Vec<usize>,
84}
85
86/// Extract the full attribute path as a string, e.g., "a.b.c".
87fn extract_attrpath(attrpath: &SyntaxNode) -> String {
88    attrpath
89        .children()
90        .map(|child| {
91            let s = child.to_string();
92            // Unquote string attribute names: `"a"` -> `a`
93            if child.kind() == SyntaxKind::NODE_STRING {
94                s.trim_matches('"').to_string()
95            } else {
96                s
97            }
98        })
99        .collect::<Vec<_>>()
100        .join(".")
101}
102
103impl Validator {
104    /// Create a new validator for the given source.
105    pub fn new(source: &str) -> Self {
106        let line_starts = Self::compute_line_starts(source);
107        Self {
108            source: source.to_string(),
109            line_starts,
110        }
111    }
112
113    /// Compute byte offsets for the start of each line.
114    fn compute_line_starts(source: &str) -> Vec<usize> {
115        let mut starts = vec![0];
116        for (i, c) in source.char_indices() {
117            if c == '\n' {
118                starts.push(i + 1);
119            }
120        }
121        starts
122    }
123
124    /// Convert a TextRange to a Location (using the start position).
125    fn range_to_location(&self, range: TextRange) -> Location {
126        self.offset_to_location(range.start().into())
127    }
128
129    /// Convert a byte offset to line and column (1-indexed).
130    fn offset_to_location(&self, offset: usize) -> Location {
131        let line = self
132            .line_starts
133            .iter()
134            .rposition(|&start| start <= offset)
135            .unwrap_or(0);
136        let column = offset - self.line_starts[line];
137        Location {
138            line: line + 1,
139            column: column + 1,
140        }
141    }
142
143    /// Validate the source and return any errors found.
144    pub fn validate(&self) -> ValidationResult {
145        let root = Root::parse(&self.source);
146        let mut errors = Vec::new();
147
148        // Collect rnix parse errors
149        for error in root.errors() {
150            let location = self.parse_error_location(error);
151            errors.push(ValidationError::ParseError {
152                message: error.to_string(),
153                location,
154            });
155        }
156
157        // Check for duplicate attributes
158        let syntax = root.syntax();
159        self.check_node(&syntax, &mut errors);
160
161        ValidationResult { errors }
162    }
163
164    /// Extract location from an rnix ParseError.
165    fn parse_error_location(&self, error: &rnix::parser::ParseError) -> Location {
166        use rnix::parser::ParseError::*;
167        match error {
168            Unexpected(r)
169            | UnexpectedExtra(r)
170            | UnexpectedWanted(_, r, _)
171            | UnexpectedDoubleBind(r)
172            | DuplicatedArgs(r, _) => self.range_to_location(*r),
173            UnexpectedEOF | UnexpectedEOFWanted(_) | RecursionLimitExceeded | _ => Location {
174                line: self.line_starts.len(),
175                column: 1,
176            },
177        }
178    }
179
180    /// Recursively check a node and its descendants for duplicate attributes.
181    fn check_node(&self, node: &SyntaxNode, errors: &mut Vec<ValidationError>) {
182        if node.kind() == SyntaxKind::NODE_ATTR_SET {
183            self.check_attr_set(node, errors);
184        }
185
186        for child in node.children() {
187            self.check_node(&child, errors);
188        }
189    }
190
191    /// Check an attribute set for duplicate attributes.
192    fn check_attr_set(&self, attr_set: &SyntaxNode, errors: &mut Vec<ValidationError>) {
193        let mut seen: HashMap<String, Location> = HashMap::new();
194
195        for child in attr_set.children() {
196            if child.kind() == SyntaxKind::NODE_ATTRPATH_VALUE
197                && let Some(attrpath) = child
198                    .children()
199                    .find(|c| c.kind() == SyntaxKind::NODE_ATTRPATH)
200            {
201                let path = extract_attrpath(&attrpath);
202                let location = self.range_to_location(attrpath.text_range());
203
204                match seen.entry(path) {
205                    Entry::Occupied(entry) => {
206                        errors.push(ValidationError::DuplicateAttribute(DuplicateAttr {
207                            path: entry.key().clone(),
208                            first: entry.get().clone(),
209                            duplicate: location,
210                        }));
211                    }
212                    Entry::Vacant(entry) => {
213                        entry.insert(location);
214                    }
215                }
216            }
217        }
218    }
219}
220
221/// Convenience function to validate source and return errors.
222pub fn validate(source: &str) -> ValidationResult {
223    Validator::new(source).validate()
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    fn expect_duplicate(err: &ValidationError) -> &DuplicateAttr {
231        match err {
232            ValidationError::DuplicateAttribute(dup) => dup,
233            ValidationError::ParseError { .. } => {
234                panic!("expected DuplicateAttribute, got ParseError")
235            }
236        }
237    }
238
239    #[test]
240    fn simple_duplicate() {
241        let source = "{ a = 1; a = 2; }";
242        let result = validate(source);
243        assert!(result.has_errors());
244        assert_eq!(result.errors.len(), 1);
245
246        let dup = expect_duplicate(&result.errors[0]);
247        assert_eq!(dup.path, "a");
248        assert_eq!(dup.first.line, 1);
249        assert_eq!(dup.first.column, 3);
250        assert_eq!(dup.duplicate.line, 1);
251        assert_eq!(dup.duplicate.column, 10);
252    }
253
254    #[test]
255    fn nested_path_duplicate() {
256        let source = "{ a.b.c = 1; a.b.c = 2; }";
257        let result = validate(source);
258        assert!(result.has_errors());
259        assert_eq!(result.errors.len(), 1);
260
261        let dup = expect_duplicate(&result.errors[0]);
262        assert_eq!(dup.path, "a.b.c");
263    }
264
265    #[test]
266    fn different_paths_valid() {
267        let source = "{ a.b = 1; a.c = 2; }";
268        let result = validate(source);
269        assert!(result.is_ok());
270    }
271
272    #[test]
273    fn flake_style_duplicate() {
274        let source = r#"{ inputs.nixpkgs.url = "github:nixos/nixpkgs"; inputs.nixpkgs.url = "github:nixos/nixpkgs/unstable"; }"#;
275        let result = validate(source);
276        assert!(result.has_errors());
277        assert_eq!(result.errors.len(), 1);
278
279        let dup = expect_duplicate(&result.errors[0]);
280        assert_eq!(dup.path, "inputs.nixpkgs.url");
281    }
282
283    #[test]
284    fn quoted_attribute_duplicate() {
285        let source = r#"{ "a" = 1; a = 2; }"#;
286        let result = validate(source);
287        assert!(result.has_errors());
288        assert_eq!(result.errors.len(), 1);
289
290        let dup = expect_duplicate(&result.errors[0]);
291        assert_eq!(dup.path, "a");
292    }
293
294    #[test]
295    fn nested_attr_set_duplicate() {
296        let source = "{ outer = { inner = 1; inner = 2; }; }";
297        let result = validate(source);
298        assert!(result.has_errors());
299        assert_eq!(result.errors.len(), 1);
300
301        let dup = expect_duplicate(&result.errors[0]);
302        assert_eq!(dup.path, "inner");
303    }
304
305    #[test]
306    fn multiple_duplicates() {
307        let source = "{ a = 1; a = 2; b = 3; b = 4; }";
308        let result = validate(source);
309        assert!(result.has_errors());
310        assert_eq!(result.errors.len(), 2);
311    }
312
313    #[test]
314    fn multiline_flake() {
315        let source = r#"{
316  inputs.nixpkgs.url = "github:nixos/nixpkgs";
317  inputs.nixpkgs.url = "github:nixos/nixpkgs/unstable";
318  outputs = { ... }: { };
319}"#;
320        let result = validate(source);
321        assert!(result.has_errors());
322        assert_eq!(result.errors.len(), 1);
323
324        let dup = expect_duplicate(&result.errors[0]);
325        assert_eq!(dup.path, "inputs.nixpkgs.url");
326        assert_eq!(dup.first.line, 2);
327        assert_eq!(dup.duplicate.line, 3);
328    }
329
330    #[test]
331    fn valid_flake() {
332        let source = r#"{
333  inputs.nixpkgs.url = "github:nixos/nixpkgs";
334  inputs.flake-utils.url = "github:numtide/flake-utils";
335  outputs = { self, nixpkgs, flake-utils }: { };
336}"#;
337        let result = validate(source);
338        assert!(result.is_ok());
339    }
340
341    #[test]
342    fn empty_attr_set() {
343        let source = "{ }";
344        let result = validate(source);
345        assert!(result.is_ok());
346    }
347
348    #[test]
349    fn single_attribute() {
350        let source = "{ a = 1; }";
351        let result = validate(source);
352        assert!(result.is_ok());
353    }
354
355    #[test]
356    fn parse_error_missing_semicolon() {
357        let source = "{ a = 1 }";
358        let result = validate(source);
359        assert!(result.has_errors());
360        assert!(matches!(
361            &result.errors[0],
362            ValidationError::ParseError { .. }
363        ));
364    }
365
366    #[test]
367    fn parse_error_unclosed_brace() {
368        let source = "{ a = 1;";
369        let result = validate(source);
370        assert!(result.has_errors());
371        assert!(matches!(
372            &result.errors[0],
373            ValidationError::ParseError { .. }
374        ));
375    }
376}