normalize_manifest/
flake.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
11
12pub struct FlakeParser;
14
15impl ManifestParser for FlakeParser {
16 fn filename(&self) -> &'static str {
17 "flake.nix"
18 }
19
20 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
21 let mut deps = Vec::new();
22 let mut seen = std::collections::HashSet::new();
23 let mut in_inputs_block = false;
24 let mut inputs_depth: i32 = 0;
27
28 for line in content.lines() {
29 let trimmed = line.trim();
30 if trimmed.is_empty() || trimmed.starts_with('#') {
31 continue;
32 }
33
34 if !in_inputs_block
36 && trimmed.starts_with("inputs")
37 && trimmed.contains('=')
38 && trimmed.contains('{')
39 {
40 in_inputs_block = true;
41 inputs_depth = 1; continue;
43 }
44
45 if in_inputs_block {
46 for ch in trimmed.chars() {
48 match ch {
49 '{' => inputs_depth += 1,
50 '}' => {
51 inputs_depth -= 1;
52 if inputs_depth <= 0 {
53 in_inputs_block = false;
54 break;
55 }
56 }
57 _ => {}
58 }
59 }
60 if !in_inputs_block {
61 continue;
62 }
63 }
64
65 let input_name = if let Some(rest) = trimmed.strip_prefix("inputs.") {
70 let name_end = rest.find(['.', ' ', '=', '{']).unwrap_or(rest.len());
72 let n = rest[..name_end].trim().to_string();
73 if rest.contains(".follows") {
74 continue; }
76 n
77 } else if in_inputs_block {
78 if trimmed == "{"
81 || trimmed == "};"
82 || trimmed == "}"
83 || trimmed.starts_with("url")
84 || trimmed.starts_with("inputs.")
85 || trimmed.starts_with("description")
86 {
87 continue;
88 }
89 let name_end = trimmed.find(['.', ' ', '=', '{']).unwrap_or(trimmed.len());
91 let n = trimmed[..name_end].trim().to_string();
92 if trimmed.contains(".follows") {
94 continue;
95 }
96 n
97 } else {
98 continue;
99 };
100
101 if input_name.is_empty() || input_name == "nixpkgs" {
102 continue;
103 }
104
105 if seen.insert(input_name.clone()) {
106 deps.push(DeclaredDep {
107 name: input_name,
108 version_req: None, kind: DepKind::Normal,
110 });
111 }
112 }
113
114 Ok(ParsedManifest {
115 ecosystem: "nix",
116 name: None,
117 version: None,
118 dependencies: deps,
119 })
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::ManifestParser;
127
128 #[test]
129 fn test_parse_flake_nix() {
130 let content = r#"{
131 description = "My Nix flake";
132
133 inputs = {
134 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
135 flake-utils.url = "github:numtide/flake-utils";
136 rust-overlay = {
137 url = "github:oxalica/rust-overlay";
138 inputs.nixpkgs.follows = "nixpkgs";
139 };
140 crane.url = "github:ipetkov/crane";
141 };
142
143 outputs = { self, nixpkgs, flake-utils, rust-overlay, crane, ... }: {};
144}
145"#;
146 let m = FlakeParser.parse(content).unwrap();
147 assert_eq!(m.ecosystem, "nix");
148
149 assert!(!m.dependencies.iter().any(|d| d.name == "nixpkgs"));
151
152 let names: Vec<&str> = m.dependencies.iter().map(|d| d.name.as_str()).collect();
153 assert!(names.contains(&"flake-utils"));
154 assert!(names.contains(&"rust-overlay"));
155 assert!(names.contains(&"crane"));
156
157 assert_eq!(m.dependencies.len(), 3);
159 }
160}