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
103fn value_is_attrset(node: &SyntaxNode) -> bool {
105 node.children()
106 .any(|c| c.kind() == SyntaxKind::NODE_ATTR_SET)
107}
108
109impl Validator {
110 pub fn new(source: &str) -> Self {
112 let line_starts = Self::compute_line_starts(source);
113 Self {
114 source: source.to_string(),
115 line_starts,
116 }
117 }
118
119 fn compute_line_starts(source: &str) -> Vec<usize> {
121 let mut starts = vec![0];
122 for (i, c) in source.char_indices() {
123 if c == '\n' {
124 starts.push(i + 1);
125 }
126 }
127 starts
128 }
129
130 fn range_to_location(&self, range: TextRange) -> Location {
132 self.offset_to_location(range.start().into())
133 }
134
135 fn offset_to_location(&self, offset: usize) -> Location {
137 let line = self
138 .line_starts
139 .iter()
140 .rposition(|&start| start <= offset)
141 .unwrap_or(0);
142 let column = offset - self.line_starts[line];
143 Location {
144 line: line + 1,
145 column: column + 1,
146 }
147 }
148
149 pub fn validate(&self) -> ValidationResult {
151 let root = Root::parse(&self.source);
152 let mut errors = Vec::new();
153
154 for error in root.errors() {
156 let location = self.parse_error_location(error);
157 errors.push(ValidationError::ParseError {
158 message: error.to_string(),
159 location,
160 });
161 }
162
163 let syntax = root.syntax();
165 self.check_node(&syntax, &mut errors);
166
167 ValidationResult { errors }
168 }
169
170 fn parse_error_location(&self, error: &rnix::ParseError) -> Location {
172 use rnix::ParseError::*;
173 match error {
174 Unexpected(r)
175 | UnexpectedExtra(r)
176 | UnexpectedWanted(_, r, _)
177 | UnexpectedDoubleBind(r)
178 | DuplicatedArgs(r, _) => self.range_to_location(*r),
179 UnexpectedEOF | UnexpectedEOFWanted(_) | RecursionLimitExceeded | _ => Location {
180 line: self.line_starts.len(),
181 column: 1,
182 },
183 }
184 }
185
186 fn check_node(&self, node: &SyntaxNode, errors: &mut Vec<ValidationError>) {
188 if node.kind() == SyntaxKind::NODE_ATTR_SET {
189 self.check_attr_set(node, errors);
190 }
191
192 for child in node.children() {
193 self.check_node(&child, errors);
194 }
195 }
196
197 fn check_attr_set(&self, attr_set: &SyntaxNode, errors: &mut Vec<ValidationError>) {
210 let mut seen: HashMap<String, (Location, bool, SyntaxNode)> = HashMap::new();
212 let mut merged_attrsets: HashMap<String, Vec<SyntaxNode>> = HashMap::new();
214
215 for child in attr_set.children() {
216 if child.kind() == SyntaxKind::NODE_ATTRPATH_VALUE
217 && let Some(attrpath) = child
218 .children()
219 .find(|c| c.kind() == SyntaxKind::NODE_ATTRPATH)
220 {
221 let path = extract_attrpath(&attrpath);
222 let location = self.range_to_location(attrpath.text_range());
223 let is_attrset = value_is_attrset(&child);
224
225 match seen.entry(path.clone()) {
226 Entry::Occupied(entry) => {
227 let (ref first_loc, first_is_attrset, _) = *entry.get();
228 if first_is_attrset && is_attrset {
229 merged_attrsets.entry(path).or_default().push(child.clone());
232 } else {
233 errors.push(ValidationError::DuplicateAttribute(DuplicateAttr {
235 path: entry.key().clone(),
236 first: first_loc.clone(),
237 duplicate: location,
238 }));
239 }
240 }
241 Entry::Vacant(entry) => {
242 if is_attrset {
243 merged_attrsets.entry(path).or_default().push(child.clone());
244 }
245 entry.insert((location, is_attrset, child.clone()));
246 }
247 }
248 }
249 }
250
251 for nodes in merged_attrsets.values() {
253 if nodes.len() < 2 {
254 continue;
255 }
256 let mut cross_seen: HashMap<String, Location> = HashMap::new();
258 for node in nodes {
259 for attrset_child in node.children() {
261 if attrset_child.kind() != SyntaxKind::NODE_ATTR_SET {
262 continue;
263 }
264 for inner in attrset_child.children() {
265 if inner.kind() == SyntaxKind::NODE_ATTRPATH_VALUE
266 && let Some(inner_path_node) = inner
267 .children()
268 .find(|c| c.kind() == SyntaxKind::NODE_ATTRPATH)
269 {
270 let inner_path = extract_attrpath(&inner_path_node);
271 let inner_loc = self.range_to_location(inner_path_node.text_range());
272
273 match cross_seen.entry(inner_path) {
274 Entry::Occupied(e) => {
275 errors.push(ValidationError::DuplicateAttribute(
276 DuplicateAttr {
277 path: e.key().clone(),
278 first: e.get().clone(),
279 duplicate: inner_loc,
280 },
281 ));
282 }
283 Entry::Vacant(e) => {
284 e.insert(inner_loc);
285 }
286 }
287 }
288 }
289 }
290 }
291 }
292 }
293}
294
295pub fn validate(source: &str) -> ValidationResult {
297 Validator::new(source).validate()
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 fn expect_duplicate(err: &ValidationError) -> &DuplicateAttr {
305 match err {
306 ValidationError::DuplicateAttribute(dup) => dup,
307 ValidationError::ParseError { .. } => {
308 panic!("expected DuplicateAttribute, got ParseError")
309 }
310 }
311 }
312
313 #[test]
314 fn simple_duplicate() {
315 let source = "{ a = 1; a = 2; }";
316 let result = validate(source);
317 assert!(result.has_errors());
318 assert_eq!(result.errors.len(), 1);
319
320 let dup = expect_duplicate(&result.errors[0]);
321 assert_eq!(dup.path, "a");
322 assert_eq!(dup.first.line, 1);
323 assert_eq!(dup.first.column, 3);
324 assert_eq!(dup.duplicate.line, 1);
325 assert_eq!(dup.duplicate.column, 10);
326 }
327
328 #[test]
329 fn nested_path_duplicate() {
330 let source = "{ a.b.c = 1; a.b.c = 2; }";
331 let result = validate(source);
332 assert!(result.has_errors());
333 assert_eq!(result.errors.len(), 1);
334
335 let dup = expect_duplicate(&result.errors[0]);
336 assert_eq!(dup.path, "a.b.c");
337 }
338
339 #[test]
340 fn different_paths_valid() {
341 let source = "{ a.b = 1; a.c = 2; }";
342 let result = validate(source);
343 assert!(result.is_ok());
344 }
345
346 #[test]
347 fn flake_style_duplicate() {
348 let source = r#"{ inputs.nixpkgs.url = "github:nixos/nixpkgs"; inputs.nixpkgs.url = "github:nixos/nixpkgs/unstable"; }"#;
349 let result = validate(source);
350 assert!(result.has_errors());
351 assert_eq!(result.errors.len(), 1);
352
353 let dup = expect_duplicate(&result.errors[0]);
354 assert_eq!(dup.path, "inputs.nixpkgs.url");
355 }
356
357 #[test]
358 fn quoted_attribute_duplicate() {
359 let source = r#"{ "a" = 1; a = 2; }"#;
360 let result = validate(source);
361 assert!(result.has_errors());
362 assert_eq!(result.errors.len(), 1);
363
364 let dup = expect_duplicate(&result.errors[0]);
365 assert_eq!(dup.path, "a");
366 }
367
368 #[test]
369 fn nested_attr_set_duplicate() {
370 let source = "{ outer = { inner = 1; inner = 2; }; }";
371 let result = validate(source);
372 assert!(result.has_errors());
373 assert_eq!(result.errors.len(), 1);
374
375 let dup = expect_duplicate(&result.errors[0]);
376 assert_eq!(dup.path, "inner");
377 }
378
379 #[test]
380 fn multiple_duplicates() {
381 let source = "{ a = 1; a = 2; b = 3; b = 4; }";
382 let result = validate(source);
383 assert!(result.has_errors());
384 assert_eq!(result.errors.len(), 2);
385 }
386
387 #[test]
388 fn multiline_flake() {
389 let source = r#"{
390 inputs.nixpkgs.url = "github:nixos/nixpkgs";
391 inputs.nixpkgs.url = "github:nixos/nixpkgs/unstable";
392 outputs = { ... }: { };
393}"#;
394 let result = validate(source);
395 assert!(result.has_errors());
396 assert_eq!(result.errors.len(), 1);
397
398 let dup = expect_duplicate(&result.errors[0]);
399 assert_eq!(dup.path, "inputs.nixpkgs.url");
400 assert_eq!(dup.first.line, 2);
401 assert_eq!(dup.duplicate.line, 3);
402 }
403
404 #[test]
405 fn valid_flake() {
406 let source = r#"{
407 inputs.nixpkgs.url = "github:nixos/nixpkgs";
408 inputs.flake-utils.url = "github:numtide/flake-utils";
409 outputs = { self, nixpkgs, flake-utils }: { };
410}"#;
411 let result = validate(source);
412 assert!(result.is_ok());
413 }
414
415 #[test]
416 fn empty_attr_set() {
417 let source = "{ }";
418 let result = validate(source);
419 assert!(result.is_ok());
420 }
421
422 #[test]
423 fn single_attribute() {
424 let source = "{ a = 1; }";
425 let result = validate(source);
426 assert!(result.is_ok());
427 }
428
429 #[test]
430 fn parse_error_missing_semicolon() {
431 let source = "{ a = 1 }";
432 let result = validate(source);
433 assert!(result.has_errors());
434 assert!(matches!(
435 &result.errors[0],
436 ValidationError::ParseError { .. }
437 ));
438 }
439
440 #[test]
441 fn parse_error_unclosed_brace() {
442 let source = "{ a = 1;";
443 let result = validate(source);
444 assert!(result.has_errors());
445 assert!(matches!(
446 &result.errors[0],
447 ValidationError::ParseError { .. }
448 ));
449 }
450
451 #[test]
452 fn mergeable_attrsets_valid() {
453 let source = r#"{
455 inputs = {
456 nixpkgs.url = "github:NixOS/nixpkgs";
457 };
458 inputs = {
459 flake-utils.url = "github:numtide/flake-utils";
460 };
461}"#;
462 let result = validate(source);
463 assert!(
464 result.is_ok(),
465 "expected no errors, got: {:?}",
466 result.errors
467 );
468 }
469
470 #[test]
471 fn mergeable_attrsets_with_comments() {
472 let source = r#"{
474 # Common inputs
475 inputs = {
476 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
477 home-manager.url = "github:nix-community/home-manager";
478 };
479
480 # Autofirma sources
481 inputs = {
482 jmulticard-src = {
483 url = "github:ctt-gob-es/jmulticard/v2.0";
484 flake = false;
485 };
486 };
487
488 outputs = { self, nixpkgs, ... }: { };
489}"#;
490 let result = validate(source);
491 assert!(
492 result.is_ok(),
493 "expected no errors, got: {:?}",
494 result.errors
495 );
496 }
497
498 #[test]
499 fn mergeable_attrsets_cross_duplicate() {
500 let source = r#"{
502 inputs = {
503 nixpkgs.url = "github:NixOS/nixpkgs";
504 };
505 inputs = {
506 nixpkgs.url = "github:NixOS/nixpkgs/unstable";
507 };
508}"#;
509 let result = validate(source);
510 assert!(result.has_errors());
511 assert_eq!(result.errors.len(), 1);
512
513 let dup = expect_duplicate(&result.errors[0]);
514 assert_eq!(dup.path, "nixpkgs.url");
515 }
516
517 #[test]
518 fn non_attrset_duplicate_still_errors() {
519 let source = r#"{ a = { x = 1; }; a = 2; }"#;
521 let result = validate(source);
522 assert!(result.has_errors());
523 assert_eq!(result.errors.len(), 1);
524
525 let dup = expect_duplicate(&result.errors[0]);
526 assert_eq!(dup.path, "a");
527 }
528
529 #[test]
530 fn three_mergeable_attrsets() {
531 let source = r#"{
532 inputs = { a.url = "a"; };
533 inputs = { b.url = "b"; };
534 inputs = { c.url = "c"; };
535}"#;
536 let result = validate(source);
537 assert!(
538 result.is_ok(),
539 "expected no errors, got: {:?}",
540 result.errors
541 );
542 }
543}