1use std::collections::HashMap;
4use std::collections::hash_map::Entry;
5use std::fmt;
6
7use rnix::{Root, SyntaxKind, SyntaxNode, TextRange};
8
9#[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#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct DuplicateAttr {
25 pub path: String,
27 pub first: Location,
29 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#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum ValidationError {
46 ParseError { message: String, location: Location },
48 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#[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
79pub struct Validator {
81 source: String,
82 line_starts: Vec<usize>,
84}
85
86fn extract_attrpath(attrpath: &SyntaxNode) -> String {
88 attrpath
89 .children()
90 .map(|child| {
91 let s = child.to_string();
92 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 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 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 fn range_to_location(&self, range: TextRange) -> Location {
126 self.offset_to_location(range.start().into())
127 }
128
129 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 pub fn validate(&self) -> ValidationResult {
145 let root = Root::parse(&self.source);
146 let mut errors = Vec::new();
147
148 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 let syntax = root.syntax();
159 self.check_node(&syntax, &mut errors);
160
161 ValidationResult { errors }
162 }
163
164 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 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 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
221pub 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}