Skip to main content

provenant/parsers/
nix.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4
5use crate::parser_warn as warn;
6use packageurl::PackageUrl;
7use serde_json::Value as JsonValue;
8
9use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
10
11use super::PackageParser;
12
13pub struct NixFlakeLockParser;
14
15impl PackageParser for NixFlakeLockParser {
16    const PACKAGE_TYPE: PackageType = PackageType::Nix;
17
18    fn is_match(path: &Path) -> bool {
19        path.file_name().is_some_and(|name| name == "flake.lock")
20    }
21
22    fn extract_packages(path: &Path) -> Vec<PackageData> {
23        let content = match fs::read_to_string(path) {
24            Ok(content) => content,
25            Err(error) => {
26                warn!("Failed to read flake.lock at {:?}: {}", path, error);
27                return vec![default_flake_lock_package_data()];
28            }
29        };
30
31        let json: JsonValue = match serde_json::from_str(&content) {
32            Ok(json) => json,
33            Err(error) => {
34                warn!("Failed to parse flake.lock at {:?}: {}", path, error);
35                return vec![default_flake_lock_package_data()];
36            }
37        };
38
39        match parse_flake_lock(path, &json) {
40            Ok(package) => vec![package],
41            Err(error) => {
42                warn!("Failed to interpret flake.lock at {:?}: {}", path, error);
43                vec![default_flake_lock_package_data()]
44            }
45        }
46    }
47}
48
49pub struct NixFlakeParser;
50
51impl PackageParser for NixFlakeParser {
52    const PACKAGE_TYPE: PackageType = PackageType::Nix;
53
54    fn is_match(path: &Path) -> bool {
55        path.file_name().is_some_and(|name| name == "flake.nix")
56    }
57
58    fn extract_packages(path: &Path) -> Vec<PackageData> {
59        let content = match fs::read_to_string(path) {
60            Ok(content) => content,
61            Err(error) => {
62                warn!("Failed to read flake.nix at {:?}: {}", path, error);
63                return vec![default_flake_package_data()];
64            }
65        };
66
67        match parse_flake_nix(path, &content) {
68            Ok(package) => vec![package],
69            Err(_) => vec![default_flake_package_data()],
70        }
71    }
72}
73
74pub struct NixDefaultParser;
75
76impl PackageParser for NixDefaultParser {
77    const PACKAGE_TYPE: PackageType = PackageType::Nix;
78
79    fn is_match(path: &Path) -> bool {
80        path.file_name().is_some_and(|name| name == "default.nix")
81    }
82
83    fn extract_packages(path: &Path) -> Vec<PackageData> {
84        let content = match fs::read_to_string(path) {
85            Ok(content) => content,
86            Err(error) => {
87                warn!("Failed to read default.nix at {:?}: {}", path, error);
88                return vec![default_default_nix_package_data()];
89            }
90        };
91
92        match parse_default_nix(path, &content) {
93            Ok(package) => vec![package],
94            Err(_) => vec![default_default_nix_package_data()],
95        }
96    }
97}
98
99#[derive(Clone, Debug)]
100enum Expr {
101    AttrSet(Vec<(Vec<String>, Expr)>),
102    List(Vec<Expr>),
103    String(String),
104    Symbol(String),
105    Application(Vec<Expr>),
106    Let {
107        bindings: Vec<(Vec<String>, Expr)>,
108        body: Box<Expr>,
109    },
110    Select {
111        target: Box<Expr>,
112        path: Vec<String>,
113    },
114}
115
116type NixAttrEntries = [(Vec<String>, Expr)];
117type NixAttrEntriesRef<'a> = &'a NixAttrEntries;
118type NixScopeStack<'a> = Vec<NixAttrEntriesRef<'a>>;
119
120#[derive(Clone, Debug, PartialEq, Eq)]
121enum Token {
122    LBrace,
123    RBrace,
124    LBracket,
125    RBracket,
126    LParen,
127    RParen,
128    Equals,
129    Semicolon,
130    Colon,
131    Dot,
132    Comma,
133    String(String),
134    Ident(String),
135}
136
137#[derive(Default)]
138struct FlakeInputInfo {
139    requirement: Option<String>,
140    follows: Vec<String>,
141    flake: Option<bool>,
142}
143
144struct Lexer {
145    chars: Vec<char>,
146    index: usize,
147}
148
149impl Lexer {
150    fn new(input: &str) -> Self {
151        Self {
152            chars: input.chars().collect(),
153            index: 0,
154        }
155    }
156
157    fn tokenize(mut self) -> Result<Vec<Token>, String> {
158        let mut tokens = Vec::new();
159
160        while let Some(ch) = self.peek() {
161            if ch.is_whitespace() {
162                self.index += 1;
163                continue;
164            }
165
166            if ch == '#' {
167                self.skip_line_comment();
168                continue;
169            }
170
171            if ch == '/' && self.peek_n(1) == Some('*') {
172                self.skip_block_comment()?;
173                continue;
174            }
175
176            match ch {
177                '$' if self.peek_n(1) == Some('{') => {
178                    tokens.push(Token::Ident(self.read_interpolation_literal()?));
179                }
180                '.' if self.peek_n(1) == Some('/') => {
181                    tokens.push(Token::Ident(self.read_path_literal()?));
182                }
183                '.' if self.peek_n(1) == Some('.') && self.peek_n(2) == Some('/') => {
184                    tokens.push(Token::Ident(self.read_path_literal()?));
185                }
186                '{' => {
187                    self.index += 1;
188                    tokens.push(Token::LBrace);
189                }
190                '}' => {
191                    self.index += 1;
192                    tokens.push(Token::RBrace);
193                }
194                '[' => {
195                    self.index += 1;
196                    tokens.push(Token::LBracket);
197                }
198                ']' => {
199                    self.index += 1;
200                    tokens.push(Token::RBracket);
201                }
202                '(' => {
203                    self.index += 1;
204                    tokens.push(Token::LParen);
205                }
206                ')' => {
207                    self.index += 1;
208                    tokens.push(Token::RParen);
209                }
210                '=' => {
211                    self.index += 1;
212                    tokens.push(Token::Equals);
213                }
214                ';' => {
215                    self.index += 1;
216                    tokens.push(Token::Semicolon);
217                }
218                ':' => {
219                    self.index += 1;
220                    tokens.push(Token::Colon);
221                }
222                '.' => {
223                    self.index += 1;
224                    tokens.push(Token::Dot);
225                }
226                ',' => {
227                    self.index += 1;
228                    tokens.push(Token::Comma);
229                }
230                '"' => tokens.push(Token::String(self.read_double_quoted_string()?)),
231                '\'' if self.peek_n(1) == Some('\'') => {
232                    tokens.push(Token::String(self.read_indented_string()?));
233                }
234                _ => tokens.push(Token::Ident(self.read_ident()?)),
235            }
236        }
237
238        Ok(tokens)
239    }
240
241    fn peek(&self) -> Option<char> {
242        self.chars.get(self.index).copied()
243    }
244
245    fn peek_n(&self, offset: usize) -> Option<char> {
246        self.chars.get(self.index + offset).copied()
247    }
248
249    fn skip_line_comment(&mut self) {
250        while let Some(ch) = self.peek() {
251            self.index += 1;
252            if ch == '\n' {
253                break;
254            }
255        }
256    }
257
258    fn skip_block_comment(&mut self) -> Result<(), String> {
259        self.index += 2;
260        while let Some(ch) = self.peek() {
261            if ch == '*' && self.peek_n(1) == Some('/') {
262                self.index += 2;
263                return Ok(());
264            }
265            self.index += 1;
266        }
267        Err("unterminated block comment".to_string())
268    }
269
270    fn read_double_quoted_string(&mut self) -> Result<String, String> {
271        self.index += 1;
272        let mut result = String::new();
273        let mut escaped = false;
274
275        while let Some(ch) = self.peek() {
276            self.index += 1;
277            if escaped {
278                result.push(match ch {
279                    'n' => '\n',
280                    'r' => '\r',
281                    't' => '\t',
282                    '"' => '"',
283                    '\\' => '\\',
284                    other => other,
285                });
286                escaped = false;
287                continue;
288            }
289
290            if ch == '\\' {
291                escaped = true;
292                continue;
293            }
294
295            if ch == '$' && self.peek() == Some('{') {
296                result.push(ch);
297                result.push('{');
298                self.index += 1;
299                let mut interpolation_depth = 1usize;
300
301                while let Some(inner) = self.peek() {
302                    self.index += 1;
303                    result.push(inner);
304
305                    match inner {
306                        '{' => interpolation_depth += 1,
307                        '}' => {
308                            interpolation_depth = interpolation_depth.saturating_sub(1);
309                            if interpolation_depth == 0 {
310                                break;
311                            }
312                        }
313                        _ => {}
314                    }
315                }
316
317                if interpolation_depth != 0 {
318                    return Err("unterminated string interpolation".to_string());
319                }
320
321                continue;
322            }
323
324            if ch == '"' {
325                return Ok(result);
326            }
327
328            result.push(ch);
329        }
330
331        Err("unterminated string".to_string())
332    }
333
334    fn read_path_literal(&mut self) -> Result<String, String> {
335        let start = self.index;
336
337        while let Some(ch) = self.peek() {
338            if ch.is_whitespace()
339                || matches!(
340                    ch,
341                    '{' | '}' | '[' | ']' | '(' | ')' | '=' | ';' | ':' | ',' | '"'
342                )
343                || (ch == '\'' && self.peek_n(1) == Some('\''))
344                || ch == '#'
345            {
346                break;
347            }
348
349            if ch == '/' && self.peek_n(1) == Some('*') {
350                break;
351            }
352
353            self.index += 1;
354        }
355
356        if self.index == start {
357            return Err("unexpected token".to_string());
358        }
359
360        Ok(self.chars[start..self.index].iter().collect())
361    }
362
363    fn read_interpolation_literal(&mut self) -> Result<String, String> {
364        let start = self.index;
365        self.index += 2;
366        let mut depth = 1usize;
367
368        while let Some(ch) = self.peek() {
369            self.index += 1;
370
371            match ch {
372                '{' => depth += 1,
373                '}' => {
374                    depth = depth.saturating_sub(1);
375                    if depth == 0 {
376                        return Ok(self.chars[start..self.index].iter().collect());
377                    }
378                }
379                _ => {}
380            }
381        }
382
383        Err("unterminated interpolation literal".to_string())
384    }
385
386    fn read_indented_string(&mut self) -> Result<String, String> {
387        self.index += 2;
388        let mut result = String::new();
389
390        while let Some(ch) = self.peek() {
391            if ch == '\'' && self.peek_n(1) == Some('\'') {
392                self.index += 2;
393                return Ok(result);
394            }
395            result.push(ch);
396            self.index += 1;
397        }
398
399        Err("unterminated indented string".to_string())
400    }
401
402    fn read_ident(&mut self) -> Result<String, String> {
403        let start = self.index;
404
405        while let Some(ch) = self.peek() {
406            if ch.is_whitespace()
407                || matches!(
408                    ch,
409                    '{' | '}' | '[' | ']' | '(' | ')' | '=' | ';' | ':' | ',' | '.' | '"'
410                )
411                || (ch == '\'' && self.peek_n(1) == Some('\''))
412                || ch == '#'
413            {
414                break;
415            }
416
417            if ch == '/' && self.peek_n(1) == Some('*') {
418                break;
419            }
420
421            self.index += 1;
422        }
423
424        if self.index == start {
425            return Err("unexpected token".to_string());
426        }
427
428        Ok(self.chars[start..self.index].iter().collect())
429    }
430}
431
432struct Parser {
433    tokens: Vec<Token>,
434    index: usize,
435}
436
437impl Parser {
438    fn new(tokens: Vec<Token>) -> Self {
439        Self { tokens, index: 0 }
440    }
441
442    fn parse(mut self) -> Result<Expr, String> {
443        let expr = self.parse_expr()?;
444        if self.peek().is_some() {
445            return Err("unexpected trailing tokens".to_string());
446        }
447        Ok(expr)
448    }
449
450    fn parse_expr(&mut self) -> Result<Expr, String> {
451        if self.peek() == Some(&Token::LBrace) && self.looks_like_lambda_binder_set()? {
452            self.skip_lambda_binder_set()?;
453            self.expect(&Token::Colon)?;
454            return self.parse_expr();
455        }
456
457        if self.looks_like_prefixed_lambda_binder_set()? {
458            self.index += 1;
459            self.skip_lambda_binder_set()?;
460            self.expect(&Token::Colon)?;
461            return self.parse_expr();
462        }
463
464        let first = self.parse_term()?;
465        if self.consume(&Token::Colon) {
466            return self.parse_expr();
467        }
468
469        let mut terms = vec![first];
470        while self.can_start_term() {
471            terms.push(self.parse_term()?);
472        }
473
474        let expr = if terms.len() == 1 {
475            terms.pop().expect("single term")
476        } else {
477            Expr::Application(terms)
478        };
479
480        self.parse_postfix(expr)
481    }
482
483    fn parse_postfix(&mut self, mut expr: Expr) -> Result<Expr, String> {
484        while self.consume(&Token::Dot) {
485            let mut path = vec![self.take_attr_key()?];
486            while self.consume(&Token::Dot) {
487                path.push(self.take_attr_key()?);
488            }
489            expr = Expr::Select {
490                target: Box::new(expr),
491                path,
492            };
493        }
494
495        Ok(expr)
496    }
497
498    fn parse_term(&mut self) -> Result<Expr, String> {
499        match self.peek() {
500            Some(Token::Ident(keyword)) if keyword == "let" => self.parse_let_in_expr(),
501            Some(Token::Ident(keyword)) if keyword == "with" => {
502                self.index += 1;
503                let _ = self.parse_expr()?;
504                self.expect(&Token::Semicolon)?;
505                self.parse_expr()
506            }
507            Some(Token::Ident(keyword)) if keyword == "rec" => {
508                if matches!(self.peek_n(1), Some(Token::LBrace)) {
509                    self.index += 1;
510                    self.parse_attrset()
511                } else {
512                    self.parse_symbol()
513                }
514            }
515            Some(Token::LBrace) => self.parse_attrset(),
516            Some(Token::LBracket) => self.parse_list(),
517            Some(Token::LParen) => {
518                self.index += 1;
519                let expr = self.parse_expr()?;
520                self.expect(&Token::RParen)?;
521                Ok(expr)
522            }
523            Some(Token::String(_)) => self.parse_string(),
524            Some(Token::Ident(_)) => self.parse_symbol(),
525            _ => Err("expected expression".to_string()),
526        }
527    }
528
529    fn parse_let_in_expr(&mut self) -> Result<Expr, String> {
530        self.take_exact_ident("let")?;
531        let mut bindings = Vec::new();
532
533        while !matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "in") {
534            if self.peek().is_none() {
535                return Err("unterminated let expression".to_string());
536            }
537
538            if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
539                bindings.extend(self.parse_inherit_entries()?);
540                continue;
541            }
542
543            let key = self.parse_attr_path()?;
544            self.expect(&Token::Equals)?;
545            let value = self.parse_expr()?;
546            self.expect(&Token::Semicolon)?;
547            bindings.push((key, value));
548        }
549
550        self.take_exact_ident("in")?;
551        Ok(Expr::Let {
552            bindings,
553            body: Box::new(self.parse_expr()?),
554        })
555    }
556
557    fn parse_attrset(&mut self) -> Result<Expr, String> {
558        self.expect(&Token::LBrace)?;
559        let mut entries = Vec::new();
560
561        loop {
562            if self.consume(&Token::RBrace) {
563                return Ok(Expr::AttrSet(entries));
564            }
565
566            if self.peek().is_none() {
567                return Err("unterminated attribute set".to_string());
568            }
569
570            if matches!(self.peek(), Some(Token::Ident(keyword)) if keyword == "inherit") {
571                entries.extend(self.parse_inherit_entries()?);
572                continue;
573            }
574
575            let key = self.parse_attr_path()?;
576            self.expect(&Token::Equals)?;
577            let value = self.parse_expr()?;
578            self.expect(&Token::Semicolon)?;
579            entries.push((key, value));
580        }
581    }
582
583    fn parse_attr_path(&mut self) -> Result<Vec<String>, String> {
584        let mut path = vec![self.take_attr_key()?];
585        while self.consume(&Token::Dot) {
586            path.push(self.take_attr_key()?);
587        }
588        Ok(path)
589    }
590
591    fn parse_inherit_entries(&mut self) -> Result<Vec<(Vec<String>, Expr)>, String> {
592        self.take_exact_ident("inherit")?;
593
594        let inherit_from = if self.consume(&Token::LParen) {
595            let expr = self.parse_expr()?;
596            self.expect(&Token::RParen)?;
597            Some(expr)
598        } else {
599            None
600        };
601
602        let mut entries = Vec::new();
603        while !self.consume(&Token::Semicolon) {
604            if self.peek().is_none() {
605                return Err("unterminated inherit statement".to_string());
606            }
607
608            let name = self.take_attr_key()?;
609            let value = match &inherit_from {
610                Some(source) => Expr::Select {
611                    target: Box::new(source.clone()),
612                    path: vec![name.clone()],
613                },
614                None => Expr::Symbol(name.clone()),
615            };
616            entries.push((vec![name], value));
617        }
618
619        Ok(entries)
620    }
621
622    fn parse_list(&mut self) -> Result<Expr, String> {
623        self.expect(&Token::LBracket)?;
624        let mut items = Vec::new();
625        while !self.consume(&Token::RBracket) {
626            if self.peek().is_none() {
627                return Err("unterminated list".to_string());
628            }
629            items.push(self.parse_expr()?);
630        }
631        Ok(Expr::List(items))
632    }
633
634    fn parse_string(&mut self) -> Result<Expr, String> {
635        match self.next() {
636            Some(Token::String(value)) => Ok(Expr::String(value)),
637            _ => Err("expected string".to_string()),
638        }
639    }
640
641    fn parse_symbol(&mut self) -> Result<Expr, String> {
642        let mut parts = vec![self.take_ident()?];
643        while self.consume(&Token::Dot) {
644            parts.push(self.take_ident()?);
645        }
646        Ok(Expr::Symbol(parts.join(".")))
647    }
648
649    fn take_ident(&mut self) -> Result<String, String> {
650        match self.next() {
651            Some(Token::Ident(value)) => Ok(value),
652            _ => Err("expected identifier".to_string()),
653        }
654    }
655
656    fn take_exact_ident(&mut self, expected: &str) -> Result<(), String> {
657        match self.next() {
658            Some(Token::Ident(value)) if value == expected => Ok(()),
659            _ => Err(format!("expected {expected}")),
660        }
661    }
662
663    fn take_attr_key(&mut self) -> Result<String, String> {
664        match self.next() {
665            Some(Token::Ident(value)) | Some(Token::String(value)) => Ok(value),
666            _ => Err("expected attribute key".to_string()),
667        }
668    }
669
670    fn can_start_term(&self) -> bool {
671        matches!(
672            self.peek(),
673            Some(Token::LBrace)
674                | Some(Token::LBracket)
675                | Some(Token::LParen)
676                | Some(Token::String(_))
677                | Some(Token::Ident(_))
678        )
679    }
680
681    fn looks_like_lambda_binder_set(&self) -> Result<bool, String> {
682        if self.peek() != Some(&Token::LBrace) {
683            return Ok(false);
684        }
685
686        self.looks_like_lambda_binder_set_from(self.index)
687    }
688
689    fn looks_like_prefixed_lambda_binder_set(&self) -> Result<bool, String> {
690        match (self.peek(), self.peek_n(1)) {
691            (Some(Token::Ident(prefix)), Some(Token::LBrace)) if prefix.ends_with('@') => {
692                self.looks_like_lambda_binder_set_from(self.index + 1)
693            }
694            _ => Ok(false),
695        }
696    }
697
698    fn looks_like_lambda_binder_set_from(&self, start_index: usize) -> Result<bool, String> {
699        if self.tokens.get(start_index) != Some(&Token::LBrace) {
700            return Ok(false);
701        }
702
703        let mut depth = 0usize;
704        let mut index = start_index;
705
706        while let Some(token) = self.tokens.get(index) {
707            match token {
708                Token::LBrace => depth += 1,
709                Token::RBrace => {
710                    depth = depth.saturating_sub(1);
711                    if depth == 0 {
712                        return Ok(matches!(self.tokens.get(index + 1), Some(Token::Colon)));
713                    }
714                }
715                Token::Equals | Token::Semicolon if depth == 1 => return Ok(false),
716                _ => {}
717            }
718
719            index += 1;
720        }
721
722        Err("unterminated lambda binder set".to_string())
723    }
724
725    fn skip_lambda_binder_set(&mut self) -> Result<(), String> {
726        self.expect(&Token::LBrace)?;
727        let mut depth = 1usize;
728
729        while depth > 0 {
730            match self.next() {
731                Some(Token::LBrace) => depth += 1,
732                Some(Token::RBrace) => depth = depth.saturating_sub(1),
733                Some(_) => {}
734                None => return Err("unterminated lambda binder set".to_string()),
735            }
736        }
737
738        Ok(())
739    }
740
741    fn expect(&mut self, expected: &Token) -> Result<(), String> {
742        if self.consume(expected) {
743            Ok(())
744        } else {
745            Err(format!("expected {:?}", expected))
746        }
747    }
748
749    fn consume(&mut self, expected: &Token) -> bool {
750        if self.peek() == Some(expected) {
751            self.index += 1;
752            true
753        } else {
754            false
755        }
756    }
757
758    fn peek(&self) -> Option<&Token> {
759        self.tokens.get(self.index)
760    }
761
762    fn peek_n(&self, offset: usize) -> Option<&Token> {
763        self.tokens.get(self.index + offset)
764    }
765
766    fn next(&mut self) -> Option<Token> {
767        let token = self.tokens.get(self.index).cloned();
768        if token.is_some() {
769            self.index += 1;
770        }
771        token
772    }
773}
774
775fn parse_flake_nix(path: &Path, content: &str) -> Result<PackageData, String> {
776    let expr = parse_nix_expr(content)?;
777    let scopes = Vec::new();
778    let (root, scopes) = root_attrset_with_scopes(&expr, &scopes)
779        .ok_or_else(|| "flake.nix root was not an attribute set".to_string())?;
780
781    let mut package = default_flake_package_data();
782    package.name = fallback_name(path);
783    package.description = find_string_attr_with_scopes(root, &["description"], &scopes);
784    package.purl = package
785        .name
786        .as_deref()
787        .and_then(|name| build_nix_purl(name, None));
788    package.dependencies = build_flake_dependencies(root, &scopes);
789
790    Ok(package)
791}
792
793fn parse_default_nix(path: &Path, content: &str) -> Result<PackageData, String> {
794    match parse_nix_expr(content) {
795        Ok(expr) => extract_default_nix_package(path, &expr, &Vec::new(), 0)
796            .or_else(|_| extract_flake_compat_default_package_from_content(path, content)),
797        Err(parse_error) => extract_flake_compat_default_package_from_content(path, content)
798            .map_err(|_| parse_error),
799    }
800}
801
802fn parse_flake_lock(path: &Path, json: &JsonValue) -> Result<PackageData, String> {
803    let version = json
804        .get("version")
805        .and_then(JsonValue::as_i64)
806        .ok_or_else(|| "flake.lock missing integer version".to_string())?;
807    let root = json
808        .get("root")
809        .and_then(JsonValue::as_str)
810        .ok_or_else(|| "flake.lock missing root".to_string())?;
811    let nodes = json
812        .get("nodes")
813        .and_then(JsonValue::as_object)
814        .ok_or_else(|| "flake.lock missing nodes".to_string())?;
815    let root_node = nodes
816        .get(root)
817        .and_then(JsonValue::as_object)
818        .ok_or_else(|| "flake.lock root node missing".to_string())?;
819    let root_inputs = root_node
820        .get("inputs")
821        .and_then(JsonValue::as_object)
822        .ok_or_else(|| "flake.lock root node missing inputs".to_string())?;
823
824    let mut package = default_flake_lock_package_data();
825    package.name = fallback_name(path);
826    package.purl = package
827        .name
828        .as_deref()
829        .and_then(|name| build_nix_purl(name, None));
830
831    let mut extra_data = HashMap::new();
832    extra_data.insert("lock_version".to_string(), JsonValue::from(version));
833    extra_data.insert("root".to_string(), JsonValue::String(root.to_string()));
834    package.extra_data = Some(extra_data);
835
836    package.dependencies = root_inputs
837        .iter()
838        .filter_map(|(input_name, node_ref)| build_lock_dependency(input_name, node_ref, nodes))
839        .collect();
840    package
841        .dependencies
842        .sort_by(|left, right| left.purl.cmp(&right.purl));
843
844    Ok(package)
845}
846
847fn build_lock_dependency(
848    input_name: &str,
849    node_ref: &JsonValue,
850    nodes: &serde_json::Map<String, JsonValue>,
851) -> Option<Dependency> {
852    let node_id = node_ref.as_str()?;
853    let node = nodes.get(node_id)?.as_object()?;
854    let locked = node.get("locked").and_then(JsonValue::as_object)?;
855    let revision = locked.get("rev").and_then(JsonValue::as_str);
856
857    let mut extra_data = HashMap::new();
858    for key in [
859        "type",
860        "owner",
861        "repo",
862        "narHash",
863        "lastModified",
864        "revCount",
865        "url",
866        "path",
867        "dir",
868        "host",
869    ] {
870        if let Some(value) = locked.get(key) {
871            extra_data.insert(normalize_extra_key(key), value.clone());
872        }
873    }
874    if let Some(value) = node.get("flake").and_then(JsonValue::as_bool) {
875        extra_data.insert("flake".to_string(), JsonValue::Bool(value));
876    }
877    if let Some(original) = node.get("original").and_then(JsonValue::as_object) {
878        if let Some(value) = original.get("type") {
879            extra_data.insert("original_type".to_string(), value.clone());
880        }
881        if let Some(value) = original.get("id") {
882            extra_data.insert("original_id".to_string(), value.clone());
883        }
884        if let Some(value) = original.get("ref") {
885            extra_data.insert("original_ref".to_string(), value.clone());
886        }
887    }
888
889    Some(Dependency {
890        purl: build_nix_purl(input_name, revision),
891        extracted_requirement: build_locked_requirement(locked, node.get("original")),
892        scope: Some("inputs".to_string()),
893        is_runtime: Some(false),
894        is_optional: Some(false),
895        is_pinned: Some(revision.is_some()),
896        is_direct: Some(true),
897        resolved_package: None,
898        extra_data: (!extra_data.is_empty()).then_some(extra_data),
899    })
900}
901
902fn build_locked_requirement(
903    locked: &serde_json::Map<String, JsonValue>,
904    original: Option<&JsonValue>,
905) -> Option<String> {
906    let source_type = locked.get("type").and_then(JsonValue::as_str).or_else(|| {
907        original
908            .and_then(|value| value.get("type"))
909            .and_then(JsonValue::as_str)
910    });
911
912    match source_type {
913        Some("github") => {
914            let owner = locked.get("owner").and_then(JsonValue::as_str)?;
915            let repo = locked.get("repo").and_then(JsonValue::as_str)?;
916            Some(format!("github:{owner}/{repo}"))
917        }
918        Some("indirect") => original
919            .and_then(|value| value.get("id"))
920            .and_then(JsonValue::as_str)
921            .map(ToOwned::to_owned),
922        _ => locked
923            .get("url")
924            .and_then(JsonValue::as_str)
925            .map(ToOwned::to_owned),
926    }
927}
928
929fn normalize_extra_key(key: &str) -> String {
930    match key {
931        "type" => "source_type".to_string(),
932        "narHash" => "nar_hash".to_string(),
933        "lastModified" => "last_modified".to_string(),
934        "revCount" => "rev_count".to_string(),
935        other => other.to_string(),
936    }
937}
938
939fn build_flake_dependencies(
940    root: &[(Vec<String>, Expr)],
941    scopes: &[&[(Vec<String>, Expr)]],
942) -> Vec<Dependency> {
943    let mut inputs: HashMap<String, FlakeInputInfo> = HashMap::new();
944
945    for (path, expr) in root {
946        if path.first().map(String::as_str) != Some("inputs") {
947            continue;
948        }
949
950        if path.len() == 1 {
951            if let Some(entries) = attrset_entries(expr) {
952                collect_input_entries(entries, scopes, &mut inputs, None);
953            }
954            continue;
955        }
956
957        collect_input_path(&path[1..], expr, scopes, &mut inputs);
958    }
959
960    let mut dependencies = inputs
961        .into_iter()
962        .map(|(name, info)| {
963            let mut extra_data = HashMap::new();
964            if info.follows.len() == 1 {
965                extra_data.insert(
966                    "follows".to_string(),
967                    JsonValue::String(info.follows[0].clone()),
968                );
969            } else if !info.follows.is_empty() {
970                extra_data.insert(
971                    "follows".to_string(),
972                    JsonValue::Array(
973                        info.follows
974                            .iter()
975                            .cloned()
976                            .map(JsonValue::String)
977                            .collect(),
978                    ),
979                );
980            }
981            if let Some(flake) = info.flake {
982                extra_data.insert("flake".to_string(), JsonValue::Bool(flake));
983            }
984
985            Dependency {
986                purl: build_nix_purl(&name, None),
987                extracted_requirement: info.requirement,
988                scope: Some("inputs".to_string()),
989                is_runtime: Some(false),
990                is_optional: Some(false),
991                is_pinned: Some(false),
992                is_direct: Some(true),
993                resolved_package: None,
994                extra_data: (!extra_data.is_empty()).then_some(extra_data),
995            }
996        })
997        .collect::<Vec<_>>();
998
999    dependencies.sort_by(|left, right| left.purl.cmp(&right.purl));
1000    dependencies
1001}
1002
1003fn collect_input_entries(
1004    entries: &[(Vec<String>, Expr)],
1005    scopes: &[&[(Vec<String>, Expr)]],
1006    inputs: &mut HashMap<String, FlakeInputInfo>,
1007    current_input: Option<&str>,
1008) {
1009    for (path, expr) in entries {
1010        if let Some(input_name) = current_input {
1011            apply_input_field(
1012                inputs.entry(input_name.to_string()).or_default(),
1013                path,
1014                expr,
1015                scopes,
1016            );
1017            continue;
1018        }
1019
1020        collect_input_path(path, expr, scopes, inputs);
1021    }
1022}
1023
1024fn collect_input_path(
1025    path: &[String],
1026    expr: &Expr,
1027    scopes: &[&[(Vec<String>, Expr)]],
1028    inputs: &mut HashMap<String, FlakeInputInfo>,
1029) {
1030    let Some(input_name) = path.first() else {
1031        return;
1032    };
1033
1034    if path.len() == 1 {
1035        match expr {
1036            Expr::AttrSet(entries) => {
1037                collect_input_entries(entries, scopes, inputs, Some(input_name))
1038            }
1039            Expr::String(value) => {
1040                inputs.entry(input_name.clone()).or_default().requirement = Some(value.clone())
1041            }
1042            Expr::Symbol(value) => {
1043                inputs.entry(input_name.clone()).or_default().requirement =
1044                    expr_as_string_with_scopes(&Expr::Symbol(value.clone()), scopes)
1045            }
1046            _ => {}
1047        }
1048        return;
1049    }
1050
1051    apply_input_field(
1052        inputs.entry(input_name.clone()).or_default(),
1053        &path[1..],
1054        expr,
1055        scopes,
1056    );
1057}
1058
1059fn apply_input_field(
1060    info: &mut FlakeInputInfo,
1061    path: &[String],
1062    expr: &Expr,
1063    scopes: &[&[(Vec<String>, Expr)]],
1064) {
1065    if path == ["url"] {
1066        info.requirement = expr_as_string_with_scopes(expr, scopes);
1067        return;
1068    }
1069
1070    if path == ["flake"] {
1071        info.flake = expr_as_bool_with_scopes(expr, scopes);
1072        return;
1073    }
1074
1075    if path.len() == 3
1076        && path[0] == "inputs"
1077        && path[2] == "follows"
1078        && let Some(value) = expr_as_string_with_scopes(expr, scopes)
1079    {
1080        info.follows.push(value);
1081    }
1082}
1083
1084fn build_list_dependencies(
1085    entries: &[(Vec<String>, Expr)],
1086    field_name: &str,
1087    runtime: bool,
1088    scopes: &[&[(Vec<String>, Expr)]],
1089) -> Vec<Dependency> {
1090    let Some(expr) = find_attr(entries, &[field_name]) else {
1091        return Vec::new();
1092    };
1093    let Some(items) = list_items_with_scopes(expr, scopes) else {
1094        return Vec::new();
1095    };
1096
1097    items
1098        .iter()
1099        .flat_map(|expr| expr_to_dependency_symbols_with_scopes(expr, scopes))
1100        .filter_map(|symbol| {
1101            let name = symbol.rsplit('.').next()?.to_string();
1102            Some(Dependency {
1103                purl: build_nix_purl(&name, None),
1104                extracted_requirement: None,
1105                scope: Some(field_name.to_string()),
1106                is_runtime: Some(runtime),
1107                is_optional: Some(false),
1108                is_pinned: Some(false),
1109                is_direct: Some(true),
1110                resolved_package: None,
1111                extra_data: None,
1112            })
1113        })
1114        .collect()
1115}
1116
1117fn expr_to_dependency_symbols_with_scopes(
1118    expr: &Expr,
1119    scopes: &[&[(Vec<String>, Expr)]],
1120) -> Vec<String> {
1121    match expr {
1122        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, 0)
1123            .map(|resolved| expr_to_dependency_symbols_with_scopes(resolved, scopes))
1124            .unwrap_or_else(|| vec![symbol.clone()]),
1125        Expr::Application(parts) => parts
1126            .iter()
1127            .filter_map(|expr| expr_as_symbol_with_scopes(expr, scopes))
1128            .collect(),
1129        Expr::Let { bindings, body } => {
1130            let scopes = extend_scopes(scopes, bindings);
1131            expr_to_dependency_symbols_with_scopes(body, &scopes)
1132        }
1133        Expr::Select { .. } => expr_as_symbol_with_scopes(expr, scopes)
1134            .into_iter()
1135            .collect(),
1136        _ => Vec::new(),
1137    }
1138}
1139
1140fn fallback_name(path: &Path) -> Option<String> {
1141    path.parent()
1142        .and_then(|parent| parent.file_name())
1143        .and_then(|name| name.to_str())
1144        .map(ToOwned::to_owned)
1145}
1146
1147fn build_nix_purl(name: &str, version: Option<&str>) -> Option<String> {
1148    let mut purl = PackageUrl::new(PackageType::Nix.as_str(), name).ok()?;
1149    if let Some(version) = version {
1150        purl.with_version(version).ok()?;
1151    }
1152    Some(purl.to_string())
1153}
1154
1155fn parse_nix_expr(content: &str) -> Result<Expr, String> {
1156    let tokens = Lexer::new(content).tokenize()?;
1157    Parser::new(tokens).parse()
1158}
1159
1160fn attrset_entries(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1161    match expr {
1162        Expr::AttrSet(entries) => Some(entries),
1163        _ => None,
1164    }
1165}
1166
1167fn list_items_with_scopes<'a>(
1168    expr: &'a Expr,
1169    scopes: &[&'a [(Vec<String>, Expr)]],
1170) -> Option<&'a [Expr]> {
1171    match expr {
1172        Expr::List(items) => Some(items),
1173        Expr::Let { bindings, body } => {
1174            let scopes = extend_scopes(scopes, bindings);
1175            list_items_with_scopes(body, &scopes)
1176        }
1177        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, 0)
1178            .and_then(|resolved| list_items_with_scopes(resolved, scopes)),
1179        Expr::Select { target, path } => resolve_select(target, path, scopes, 0)
1180            .and_then(|resolved| list_items_with_scopes(resolved, scopes)),
1181        _ => None,
1182    }
1183}
1184
1185fn expr_as_symbol(expr: &Expr) -> Option<String> {
1186    match expr {
1187        Expr::Symbol(value) => Some(value.clone()),
1188        _ => None,
1189    }
1190}
1191
1192fn expr_as_symbol_with_scopes(expr: &Expr, scopes: &[&[(Vec<String>, Expr)]]) -> Option<String> {
1193    match expr {
1194        Expr::Symbol(value) => resolve_symbol(value, scopes, 0)
1195            .and_then(|resolved| expr_as_symbol_with_scopes(resolved, scopes))
1196            .or_else(|| Some(value.clone())),
1197        Expr::Select { target, path } => resolve_select(target, path, scopes, 0)
1198            .and_then(|resolved| expr_as_symbol_with_scopes(resolved, scopes)),
1199        Expr::Let { bindings, body } => {
1200            let scopes = extend_scopes(scopes, bindings);
1201            expr_as_symbol_with_scopes(body, &scopes)
1202        }
1203        _ => expr_as_symbol(expr),
1204    }
1205}
1206
1207fn expr_as_bool(expr: &Expr) -> Option<bool> {
1208    match expr {
1209        Expr::Symbol(value) if value == "true" => Some(true),
1210        Expr::Symbol(value) if value == "false" => Some(false),
1211        _ => None,
1212    }
1213}
1214
1215fn expr_as_bool_with_scopes(expr: &Expr, scopes: &[&[(Vec<String>, Expr)]]) -> Option<bool> {
1216    match expr {
1217        Expr::Let { bindings, body } => {
1218            let scopes = extend_scopes(scopes, bindings);
1219            expr_as_bool_with_scopes(body, &scopes)
1220        }
1221        Expr::Symbol(value) => resolve_symbol(value, scopes, 0)
1222            .and_then(|resolved| expr_as_bool_with_scopes(resolved, scopes))
1223            .or_else(|| expr_as_bool(expr)),
1224        Expr::Select { target, path } => resolve_select(target, path, scopes, 0)
1225            .and_then(|resolved| expr_as_bool_with_scopes(resolved, scopes)),
1226        _ => expr_as_bool(expr),
1227    }
1228}
1229
1230fn expr_as_string_with_scopes(expr: &Expr, scopes: &[&[(Vec<String>, Expr)]]) -> Option<String> {
1231    match expr {
1232        Expr::String(value) => Some(interpolate_string(value, scopes)),
1233        Expr::Symbol(value) => resolve_symbol(value, scopes, 0)
1234            .and_then(|resolved| expr_as_string_with_scopes(resolved, scopes))
1235            .or_else(|| Some(value.clone())),
1236        Expr::Application(parts) => parts
1237            .last()
1238            .and_then(|expr| expr_as_string_with_scopes(expr, scopes)),
1239        Expr::Let { bindings, body } => {
1240            let scopes = extend_scopes(scopes, bindings);
1241            expr_as_string_with_scopes(body, &scopes)
1242        }
1243        Expr::Select { target, path } => resolve_select(target, path, scopes, 0)
1244            .and_then(|resolved| expr_as_string_with_scopes(resolved, scopes)),
1245        _ => None,
1246    }
1247}
1248
1249fn expr_to_scalar_string_with_scopes(
1250    expr: &Expr,
1251    scopes: &[&[(Vec<String>, Expr)]],
1252) -> Option<String> {
1253    match expr {
1254        Expr::Application(parts) => parts
1255            .last()
1256            .and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes)),
1257        _ => expr_as_string_with_scopes(expr, scopes),
1258    }
1259}
1260
1261fn find_attr<'a>(entries: &'a [(Vec<String>, Expr)], path: &[&str]) -> Option<&'a Expr> {
1262    for (key, value) in entries {
1263        if key.iter().map(String::as_str).eq(path.iter().copied()) {
1264            return Some(value);
1265        }
1266
1267        if key.len() < path.len()
1268            && key
1269                .iter()
1270                .map(String::as_str)
1271                .eq(path[..key.len()].iter().copied())
1272            && let Expr::AttrSet(child_entries) = value
1273            && let Some(found) = find_attr(child_entries, &path[key.len()..])
1274        {
1275            return Some(found);
1276        }
1277    }
1278
1279    None
1280}
1281
1282fn find_string_attr_with_scopes(
1283    entries: &[(Vec<String>, Expr)],
1284    path: &[&str],
1285    scopes: &[&[(Vec<String>, Expr)]],
1286) -> Option<String> {
1287    find_attr(entries, path).and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes))
1288}
1289
1290fn find_mk_derivation_attrset(expr: &Expr) -> Option<&[(Vec<String>, Expr)]> {
1291    match expr {
1292        Expr::Application(parts) => {
1293            let is_derivation = parts
1294                .first()
1295                .and_then(expr_as_symbol)
1296                .is_some_and(|symbol| symbol.ends_with("mkDerivation"));
1297            if is_derivation {
1298                return parts.iter().rev().find_map(attrset_entries);
1299            }
1300            None
1301        }
1302        _ => None,
1303    }
1304}
1305
1306fn extend_scopes<'a>(
1307    scopes: &[NixAttrEntriesRef<'a>],
1308    bindings: NixAttrEntriesRef<'a>,
1309) -> NixScopeStack<'a> {
1310    let mut extended = scopes.to_vec();
1311    extended.push(bindings);
1312    extended
1313}
1314
1315fn root_attrset_with_scopes<'a>(
1316    expr: &'a Expr,
1317    scopes: &[NixAttrEntriesRef<'a>],
1318) -> Option<(NixAttrEntriesRef<'a>, NixScopeStack<'a>)> {
1319    match expr {
1320        Expr::AttrSet(entries) => Some((entries, scopes.to_vec())),
1321        Expr::Let { bindings, body } => {
1322            let scopes = extend_scopes(scopes, bindings);
1323            root_attrset_with_scopes(body, &scopes)
1324        }
1325        _ => None,
1326    }
1327}
1328
1329fn lookup_binding<'a>(scopes: &[NixAttrEntriesRef<'a>], name: &str) -> Option<&'a Expr> {
1330    scopes
1331        .iter()
1332        .rev()
1333        .find_map(|bindings| find_attr(bindings, &[name]))
1334}
1335
1336fn resolve_symbol<'a>(
1337    symbol: &str,
1338    scopes: &[NixAttrEntriesRef<'a>],
1339    depth: usize,
1340) -> Option<&'a Expr> {
1341    if depth > 8 {
1342        return None;
1343    }
1344
1345    let mut parts = symbol.split('.');
1346    let head = parts.next()?;
1347    let mut expr = lookup_binding(scopes, head)?;
1348    let rest = parts.collect::<Vec<_>>();
1349    if rest.is_empty() {
1350        return Some(expr);
1351    }
1352
1353    for segment in rest {
1354        expr = resolve_select(expr, &[segment.to_string()], scopes, depth + 1)?;
1355    }
1356
1357    Some(expr)
1358}
1359
1360fn resolve_select<'a>(
1361    target: &'a Expr,
1362    path: &[String],
1363    scopes: &[NixAttrEntriesRef<'a>],
1364    depth: usize,
1365) -> Option<&'a Expr> {
1366    if depth > 8 {
1367        return None;
1368    }
1369
1370    match target {
1371        Expr::AttrSet(entries) => find_attr(
1372            entries,
1373            &path.iter().map(String::as_str).collect::<Vec<_>>(),
1374        ),
1375        Expr::Let { bindings, body } => {
1376            let scopes = extend_scopes(scopes, bindings);
1377            resolve_select(body, path, &scopes, depth + 1)
1378        }
1379        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, depth + 1)
1380            .and_then(|resolved| resolve_select(resolved, path, scopes, depth + 1)),
1381        Expr::Select {
1382            target: inner_target,
1383            path: inner_path,
1384        } => resolve_select(inner_target, inner_path, scopes, depth + 1)
1385            .and_then(|resolved| resolve_select(resolved, path, scopes, depth + 1)),
1386        _ => None,
1387    }
1388}
1389
1390fn interpolate_string(value: &str, scopes: &[&[(Vec<String>, Expr)]]) -> String {
1391    let mut result = String::new();
1392    let mut index = 0usize;
1393
1394    while let Some(relative_start) = value[index..].find("${") {
1395        let start = index + relative_start;
1396        result.push_str(&value[index..start]);
1397
1398        let placeholder_start = start + 2;
1399        let Some(relative_end) = value[placeholder_start..].find('}') else {
1400            result.push_str(&value[start..]);
1401            return result;
1402        };
1403        let end = placeholder_start + relative_end;
1404        let placeholder = value[placeholder_start..end].trim();
1405        if !placeholder.is_empty()
1406            && placeholder
1407                .chars()
1408                .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))
1409            && let Some(resolved) = resolve_symbol(placeholder, scopes, 0)
1410            && let Some(replacement) = expr_as_string_with_scopes(resolved, scopes)
1411        {
1412            result.push_str(&replacement);
1413        } else {
1414            result.push_str(&value[start..=end]);
1415        }
1416
1417        index = end + 1;
1418    }
1419
1420    result.push_str(&value[index..]);
1421    result
1422}
1423
1424fn extract_default_nix_package(
1425    path: &Path,
1426    expr: &Expr,
1427    scopes: &[&[(Vec<String>, Expr)]],
1428    depth: usize,
1429) -> Result<PackageData, String> {
1430    if depth > 2 {
1431        return Err("default.nix exceeded supported local import depth".to_string());
1432    }
1433
1434    match expr {
1435        Expr::Let { bindings, body } => {
1436            let scopes = extend_scopes(scopes, bindings);
1437            extract_default_nix_package(path, body, &scopes, depth)
1438        }
1439        Expr::Application(parts) => {
1440            if let Some(derivation) = find_mk_derivation_attrset(expr) {
1441                return build_default_package_from_attrset(path, derivation, scopes);
1442            }
1443
1444            if let Some((imported_expr, imported_path)) =
1445                try_follow_local_nix_application(path, parts, scopes)
1446            {
1447                return extract_default_nix_package(
1448                    &imported_path,
1449                    &imported_expr,
1450                    &Vec::new(),
1451                    depth + 1,
1452                );
1453            }
1454
1455            if let Some(package) = parts
1456                .first()
1457                .and_then(|part| extract_flake_compat_package_from_expr(path, part, scopes, depth))
1458            {
1459                return Ok(package);
1460            }
1461
1462            Err("default.nix did not contain a supported mkDerivation call".to_string())
1463        }
1464        Expr::Select {
1465            target,
1466            path: select_path,
1467        } => {
1468            if let Some(package) =
1469                extract_flake_compat_package_from_select(path, target, select_path, scopes, depth)
1470            {
1471                return Ok(package);
1472            }
1473
1474            if let Some((imported_expr, imported_path)) =
1475                try_follow_selected_local_import(path, target, select_path, scopes)
1476            {
1477                return extract_default_nix_package(
1478                    &imported_path,
1479                    &imported_expr,
1480                    &Vec::new(),
1481                    depth + 1,
1482                );
1483            }
1484
1485            if let Some(resolved) = resolve_select(target, select_path, scopes, 0) {
1486                return extract_default_nix_package(path, resolved, scopes, depth);
1487            }
1488
1489            Err("default.nix did not contain a supported mkDerivation call".to_string())
1490        }
1491        Expr::Symbol(_) => extract_flake_compat_package_from_expr(path, expr, scopes, depth)
1492            .ok_or_else(|| "default.nix did not contain a supported mkDerivation call".to_string()),
1493        _ => Err("default.nix did not contain a supported mkDerivation call".to_string()),
1494    }
1495}
1496
1497fn build_default_package_from_attrset(
1498    path: &Path,
1499    derivation: &[(Vec<String>, Expr)],
1500    scopes: &[&[(Vec<String>, Expr)]],
1501) -> Result<PackageData, String> {
1502    let mut package = default_default_nix_package_data();
1503    package.name = find_string_attr_with_scopes(derivation, &["pname"], scopes).or_else(|| {
1504        find_string_attr_with_scopes(derivation, &["name"], scopes)
1505            .map(|name| split_derivation_name(&name).0)
1506    });
1507    package.version =
1508        find_string_attr_with_scopes(derivation, &["version"], scopes).or_else(|| {
1509            find_string_attr_with_scopes(derivation, &["name"], scopes)
1510                .and_then(|name| split_derivation_name(&name).1)
1511        });
1512    package.description =
1513        find_string_attr_with_scopes(derivation, &["meta", "description"], scopes)
1514            .or_else(|| find_string_attr_with_scopes(derivation, &["description"], scopes));
1515    package.homepage_url = find_string_attr_with_scopes(derivation, &["meta", "homepage"], scopes)
1516        .or_else(|| find_string_attr_with_scopes(derivation, &["homepage"], scopes));
1517    package.extracted_license_statement = find_attr(derivation, &["meta", "license"])
1518        .and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes))
1519        .or_else(|| {
1520            find_attr(derivation, &["license"])
1521                .and_then(|expr| expr_to_scalar_string_with_scopes(expr, scopes))
1522        });
1523    package.dependencies = [
1524        build_list_dependencies(derivation, "nativeBuildInputs", false, scopes),
1525        build_list_dependencies(derivation, "buildInputs", true, scopes),
1526        build_list_dependencies(derivation, "propagatedBuildInputs", true, scopes),
1527        build_list_dependencies(derivation, "checkInputs", false, scopes),
1528    ]
1529    .concat();
1530    if package.name.is_none() {
1531        package.name = fallback_name(path);
1532    }
1533    package.purl = package
1534        .name
1535        .as_deref()
1536        .and_then(|name| build_nix_purl(name, package.version.as_deref()));
1537
1538    Ok(package)
1539}
1540
1541fn try_follow_local_nix_application(
1542    path: &Path,
1543    parts: &[Expr],
1544    scopes: &[&[(Vec<String>, Expr)]],
1545) -> Option<(Expr, std::path::PathBuf)> {
1546    let head = parts.first().and_then(expr_as_symbol)?;
1547    let is_supported_wrapper = head == "import" || head.ends_with("callPackage");
1548    if !is_supported_wrapper {
1549        return None;
1550    }
1551
1552    let local_path = parts
1553        .get(1)
1554        .and_then(|expr| expr_as_symbol_with_scopes(expr, scopes))?;
1555    if !is_local_nix_path(&local_path) {
1556        return None;
1557    }
1558
1559    let resolved_path = resolve_local_nix_path(path, &local_path)?;
1560    let content = fs::read_to_string(&resolved_path).ok()?;
1561    let expr = parse_nix_expr(&content).ok()?;
1562    Some((expr, resolved_path))
1563}
1564
1565fn try_follow_selected_local_import(
1566    path: &Path,
1567    target: &Expr,
1568    select_path: &[String],
1569    scopes: &[&[(Vec<String>, Expr)]],
1570) -> Option<(Expr, std::path::PathBuf)> {
1571    let Expr::Application(parts) = target else {
1572        return None;
1573    };
1574
1575    let (imported_expr, imported_path) = try_follow_local_nix_application(path, parts, scopes)?;
1576    let selected = attrset_entries(&imported_expr).and_then(|entries| {
1577        find_attr(
1578            entries,
1579            &select_path.iter().map(String::as_str).collect::<Vec<_>>(),
1580        )
1581    })?;
1582    Some((selected.clone(), imported_path))
1583}
1584
1585fn extract_flake_compat_package_from_expr(
1586    path: &Path,
1587    expr: &Expr,
1588    scopes: &[&[(Vec<String>, Expr)]],
1589    depth: usize,
1590) -> Option<PackageData> {
1591    if depth > 2 {
1592        return None;
1593    }
1594
1595    match expr {
1596        Expr::Select {
1597            target,
1598            path: select_path,
1599        } => extract_flake_compat_package_from_select(path, target, select_path, scopes, depth),
1600        Expr::Let { bindings, body } => {
1601            let scopes = extend_scopes(scopes, bindings);
1602            extract_flake_compat_package_from_expr(path, body, &scopes, depth)
1603        }
1604        Expr::Symbol(symbol) => {
1605            if let Some((head, rest)) = symbol.split_once('.') {
1606                let select_path = rest.split('.').map(ToOwned::to_owned).collect::<Vec<_>>();
1607                resolve_symbol(head, scopes, 0)
1608                    .and_then(|resolved| {
1609                        extract_flake_compat_package_from_select(
1610                            path,
1611                            resolved,
1612                            &select_path,
1613                            scopes,
1614                            depth,
1615                        )
1616                    })
1617                    .or_else(|| {
1618                        let target = Expr::Symbol(head.to_string());
1619                        extract_flake_compat_package_from_select(
1620                            path,
1621                            &target,
1622                            &select_path,
1623                            scopes,
1624                            depth,
1625                        )
1626                    })
1627                    .or_else(|| {
1628                        resolve_symbol(symbol, scopes, 0).and_then(|resolved| {
1629                            extract_flake_compat_package_from_expr(path, resolved, scopes, depth)
1630                        })
1631                    })
1632            } else {
1633                resolve_symbol(symbol, scopes, 0).and_then(|resolved| {
1634                    extract_flake_compat_package_from_expr(path, resolved, scopes, depth)
1635                })
1636            }
1637        }
1638        _ => None,
1639    }
1640}
1641
1642fn extract_flake_compat_package_from_select(
1643    path: &Path,
1644    target: &Expr,
1645    select_path: &[String],
1646    scopes: &[&[(Vec<String>, Expr)]],
1647    depth: usize,
1648) -> Option<PackageData> {
1649    if depth > 2 || select_path.first().map(String::as_str) != Some("defaultNix") {
1650        return None;
1651    }
1652
1653    let source_root = resolve_flake_compat_source_root(path, target, scopes, 0)?;
1654    let mut package = default_default_nix_package_data();
1655    package.name = source_root
1656        .file_name()
1657        .and_then(|name| name.to_str())
1658        .map(ToOwned::to_owned)
1659        .or_else(|| fallback_name(path));
1660    package.purl = package
1661        .name
1662        .as_deref()
1663        .and_then(|name| build_nix_purl(name, None));
1664    mark_flake_compat_wrapper(&mut package);
1665    Some(package)
1666}
1667
1668fn resolve_flake_compat_source_root(
1669    path: &Path,
1670    target: &Expr,
1671    scopes: &[&[(Vec<String>, Expr)]],
1672    depth: usize,
1673) -> Option<std::path::PathBuf> {
1674    if depth > 8 {
1675        return None;
1676    }
1677
1678    match target {
1679        Expr::Application(parts) => source_root_from_flake_compat_application(path, parts, scopes),
1680        Expr::Symbol(symbol) => resolve_symbol(symbol, scopes, depth + 1).and_then(|resolved| {
1681            resolve_flake_compat_source_root(path, resolved, scopes, depth + 1)
1682        }),
1683        Expr::Let { bindings, body } => {
1684            let scopes = extend_scopes(scopes, bindings);
1685            resolve_flake_compat_source_root(path, body, &scopes, depth + 1)
1686        }
1687        Expr::Select {
1688            target: inner_target,
1689            path: inner_path,
1690        } => resolve_select(inner_target, inner_path, scopes, depth + 1).and_then(|resolved| {
1691            resolve_flake_compat_source_root(path, resolved, scopes, depth + 1)
1692        }),
1693        _ => None,
1694    }
1695}
1696
1697fn source_root_from_flake_compat_application(
1698    path: &Path,
1699    parts: &[Expr],
1700    scopes: &[&[(Vec<String>, Expr)]],
1701) -> Option<std::path::PathBuf> {
1702    let head = parts.first().and_then(expr_as_symbol)?;
1703    if head != "import" {
1704        return None;
1705    }
1706
1707    let import_path = parts
1708        .get(1)
1709        .and_then(|expr| expr_as_symbol_with_scopes(expr, scopes))?;
1710    if !is_local_nix_path(&import_path) {
1711        return None;
1712    }
1713
1714    let args = parts.iter().find_map(attrset_entries)?;
1715    let src_value =
1716        find_attr(args, &["src"]).and_then(|expr| expr_as_symbol_with_scopes(expr, scopes))?;
1717    if !is_local_path(&src_value) {
1718        return None;
1719    }
1720
1721    resolve_local_path(path, &src_value)
1722}
1723
1724fn is_local_path(value: &str) -> bool {
1725    value.starts_with("./") || value.starts_with("../")
1726}
1727
1728fn is_local_nix_path(value: &str) -> bool {
1729    is_local_path(value) && value.ends_with(".nix")
1730}
1731
1732fn resolve_local_path(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1733    let base = path.parent()?;
1734    let resolved = base.join(value);
1735    resolved.exists().then_some(resolved)
1736}
1737
1738fn resolve_local_nix_path(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1739    resolve_local_path(path, value).filter(|resolved| resolved.is_file())
1740}
1741
1742fn extract_flake_compat_default_package_from_content(
1743    path: &Path,
1744    content: &str,
1745) -> Result<PackageData, String> {
1746    if !content.contains("defaultNix") || !content.contains("flake-compat.nix") {
1747        return Err("default.nix did not contain a supported mkDerivation call".to_string());
1748    }
1749
1750    let src_value = extract_local_flake_compat_src_value(content).unwrap_or("./.".to_string());
1751    let mut package = default_default_nix_package_data();
1752    package.name = normalize_local_source_root(path, &src_value)
1753        .and_then(|source_root| {
1754            source_root
1755                .file_name()
1756                .and_then(|name| name.to_str())
1757                .filter(|name| *name != ".")
1758                .map(ToOwned::to_owned)
1759        })
1760        .or_else(|| fallback_name(path));
1761    if package.name.is_none() {
1762        return Err("default.nix did not contain a supported mkDerivation call".to_string());
1763    }
1764    package.purl = package
1765        .name
1766        .as_deref()
1767        .and_then(|name| build_nix_purl(name, None));
1768    mark_flake_compat_wrapper(&mut package);
1769    Ok(package)
1770}
1771
1772fn mark_flake_compat_wrapper(package: &mut PackageData) {
1773    let mut extra_data = package.extra_data.clone().unwrap_or_default();
1774    extra_data.insert(
1775        "nix_wrapper_kind".to_string(),
1776        JsonValue::String("flake_compat".to_string()),
1777    );
1778    package.extra_data = Some(extra_data);
1779}
1780
1781fn extract_local_flake_compat_src_value(content: &str) -> Option<String> {
1782    let src_index = content.find("src")?;
1783    let after_src = &content[src_index + 3..];
1784    let equals_index = after_src.find('=')?;
1785    let remainder = after_src[equals_index + 1..].trim_start();
1786    let end_index = remainder.find([';', '}', '\n']).unwrap_or(remainder.len());
1787    let candidate = remainder[..end_index].trim();
1788    if is_local_path(candidate) {
1789        Some(candidate.to_string())
1790    } else {
1791        None
1792    }
1793}
1794
1795fn normalize_local_source_root(path: &Path, value: &str) -> Option<std::path::PathBuf> {
1796    match value {
1797        "." | "./." => path.parent().map(|parent| parent.to_path_buf()),
1798        _ if value.ends_with("/.") => resolve_local_path(path, value.trim_end_matches("/.")),
1799        _ => resolve_local_path(path, value),
1800    }
1801}
1802
1803fn split_derivation_name(name: &str) -> (String, Option<String>) {
1804    let mut parts = name.rsplitn(2, '-');
1805    let maybe_version = parts
1806        .next()
1807        .filter(|value| value.chars().any(|ch| ch.is_ascii_digit()));
1808    let maybe_name = parts.next();
1809
1810    match (maybe_name, maybe_version) {
1811        (Some(package_name), Some(version)) => {
1812            (package_name.to_string(), Some(version.to_string()))
1813        }
1814        _ => (name.to_string(), None),
1815    }
1816}
1817
1818fn default_flake_package_data() -> PackageData {
1819    PackageData {
1820        package_type: Some(PackageType::Nix),
1821        primary_language: Some("Nix".to_string()),
1822        datasource_id: Some(DatasourceId::NixFlakeNix),
1823        ..Default::default()
1824    }
1825}
1826
1827fn default_flake_lock_package_data() -> PackageData {
1828    PackageData {
1829        package_type: Some(PackageType::Nix),
1830        primary_language: Some("JSON".to_string()),
1831        datasource_id: Some(DatasourceId::NixFlakeLock),
1832        ..Default::default()
1833    }
1834}
1835
1836fn default_default_nix_package_data() -> PackageData {
1837    PackageData {
1838        package_type: Some(PackageType::Nix),
1839        primary_language: Some("Nix".to_string()),
1840        datasource_id: Some(DatasourceId::NixDefaultNix),
1841        ..Default::default()
1842    }
1843}
1844
1845crate::register_parser!(
1846    "Nix flake manifest",
1847    &["**/flake.nix"],
1848    "nix",
1849    "Nix",
1850    Some("https://nix.dev/manual/nix/stable/command-ref/new-cli/nix3-flake.html"),
1851);
1852
1853crate::register_parser!(
1854    "Nix flake lockfile",
1855    &["**/flake.lock"],
1856    "nix",
1857    "JSON",
1858    Some("https://nix.dev/manual/nix/latest/command-ref/new-cli/nix3-flake.html"),
1859);
1860
1861crate::register_parser!(
1862    "Nix derivation manifest",
1863    &["**/default.nix"],
1864    "nix",
1865    "Nix",
1866    Some("https://nix.dev/manual/nix/stable/language/derivations.html"),
1867);