normalize_manifest/
nuget.rs1use crate::{DeclaredDep, DepKind, ManifestError, ManifestParser, ParsedManifest};
7
8pub struct PackagesConfigParser;
12
13impl ManifestParser for PackagesConfigParser {
14 fn filename(&self) -> &'static str {
15 "packages.config"
16 }
17
18 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
19 let doc = roxmltree::Document::parse(content).map_err(|e| ManifestError(e.to_string()))?;
20
21 let mut deps = Vec::new();
22
23 for node in doc.descendants().filter(|n| n.has_tag_name("package")) {
24 let id = node.attribute("id");
25 let version = node.attribute("version");
26 let dev_dep = node.attribute("developmentDependency");
27
28 if let Some(name) = id {
29 let kind = if dev_dep == Some("true") {
30 DepKind::Dev
31 } else {
32 DepKind::Normal
33 };
34 deps.push(DeclaredDep {
35 name: name.to_string(),
36 version_req: version.map(|v| v.to_string()),
37 kind,
38 });
39 }
40 }
41
42 Ok(ParsedManifest {
43 ecosystem: "nuget",
44 name: None,
45 version: None,
46 dependencies: deps,
47 })
48 }
49}
50
51pub struct CsprojParser;
57
58impl CsprojParser {
59 pub fn parse_content(content: &str) -> Result<ParsedManifest, ManifestError> {
61 CsprojParser.parse(content)
62 }
63}
64
65impl ManifestParser for CsprojParser {
66 fn filename(&self) -> &'static str {
67 "*.csproj"
68 }
69
70 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
71 let doc = roxmltree::Document::parse(content).map_err(|e| ManifestError(e.to_string()))?;
72
73 let name = doc
75 .descendants()
76 .find(|n| n.has_tag_name("AssemblyName"))
77 .and_then(|n| n.text())
78 .map(|s| s.trim().to_string());
79
80 let version = doc
81 .descendants()
82 .find(|n| n.has_tag_name("Version"))
83 .and_then(|n| n.text())
84 .map(|s| s.trim().to_string());
85
86 let mut deps = Vec::new();
87
88 for node in doc
89 .descendants()
90 .filter(|n| n.has_tag_name("PackageReference"))
91 {
92 let Some(pkg_name) = node
93 .attribute("Include")
94 .or_else(|| node.attribute("include"))
95 else {
96 continue;
97 };
98 let pkg_name = pkg_name.to_string();
99
100 let version_req = node
102 .attribute("Version")
103 .or_else(|| node.attribute("version"))
104 .map(|v| v.to_string())
105 .or_else(|| {
106 node.children()
107 .find(|n| n.has_tag_name("Version"))
108 .and_then(|n| n.text())
109 .map(|v| v.trim().to_string())
110 });
111
112 let private_assets = node
114 .attribute("PrivateAssets")
115 .or_else(|| node.attribute("privateAssets"));
116 let kind = if private_assets == Some("all") {
117 DepKind::Dev
118 } else {
119 DepKind::Normal
120 };
121
122 deps.push(DeclaredDep {
123 name: pkg_name,
124 version_req,
125 kind,
126 });
127 }
128
129 Ok(ParsedManifest {
130 ecosystem: "nuget",
131 name,
132 version,
133 dependencies: deps,
134 })
135 }
136}
137
138pub struct DirectoryPackagesPropsParser;
143
144impl ManifestParser for DirectoryPackagesPropsParser {
145 fn filename(&self) -> &'static str {
146 "Directory.Packages.props"
147 }
148
149 fn parse(&self, content: &str) -> Result<ParsedManifest, ManifestError> {
150 let doc = roxmltree::Document::parse(content).map_err(|e| ManifestError(e.to_string()))?;
151
152 let mut deps = Vec::new();
153
154 for node in doc
155 .descendants()
156 .filter(|n| n.has_tag_name("PackageVersion"))
157 {
158 let pkg_name = node
159 .attribute("Include")
160 .or_else(|| node.attribute("include"));
161 let Some(pkg_name) = pkg_name else {
162 continue;
163 };
164
165 let version_req = node
166 .attribute("Version")
167 .or_else(|| node.attribute("version"))
168 .map(|v| v.to_string());
169
170 deps.push(DeclaredDep {
171 name: pkg_name.to_string(),
172 version_req,
173 kind: DepKind::Normal,
174 });
175 }
176
177 Ok(ParsedManifest {
178 ecosystem: "nuget",
179 name: None,
180 version: None,
181 dependencies: deps,
182 })
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::ManifestParser;
190
191 #[test]
192 fn test_packages_config() {
193 let content = r#"<?xml version="1.0" encoding="utf-8"?>
194<packages>
195 <package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
196 <package id="NUnit" version="3.13.3" targetFramework="net48" />
197 <package id="StyleCop.Analyzers" version="1.1.118" developmentDependency="true" />
198</packages>"#;
199
200 let m = PackagesConfigParser.parse(content).unwrap();
201 assert_eq!(m.ecosystem, "nuget");
202 assert_eq!(m.dependencies.len(), 3);
203
204 let json = m
205 .dependencies
206 .iter()
207 .find(|d| d.name == "Newtonsoft.Json")
208 .unwrap();
209 assert_eq!(json.version_req.as_deref(), Some("13.0.3"));
210 assert_eq!(json.kind, DepKind::Normal);
211
212 let style = m
213 .dependencies
214 .iter()
215 .find(|d| d.name == "StyleCop.Analyzers")
216 .unwrap();
217 assert_eq!(style.kind, DepKind::Dev);
218 }
219
220 #[test]
221 fn test_csproj_package_reference() {
222 let content = r#"<Project Sdk="Microsoft.NET.Sdk">
223 <PropertyGroup>
224 <OutputType>Exe</OutputType>
225 <TargetFramework>net8.0</TargetFramework>
226 <AssemblyName>MyApp</AssemblyName>
227 <Version>2.0.0</Version>
228 </PropertyGroup>
229
230 <ItemGroup>
231 <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
232 <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
233 <PackageReference Include="coverlet.collector" Version="6.0.0" PrivateAssets="all" />
234 </ItemGroup>
235</Project>"#;
236
237 let m = CsprojParser.parse(content).unwrap();
238 assert_eq!(m.ecosystem, "nuget");
239 assert_eq!(m.name.as_deref(), Some("MyApp"));
240 assert_eq!(m.version.as_deref(), Some("2.0.0"));
241 assert_eq!(m.dependencies.len(), 3);
242
243 let efcore = m
244 .dependencies
245 .iter()
246 .find(|d| d.name == "Microsoft.EntityFrameworkCore")
247 .unwrap();
248 assert_eq!(efcore.version_req.as_deref(), Some("8.0.0"));
249 assert_eq!(efcore.kind, DepKind::Normal);
250
251 let coverlet = m
252 .dependencies
253 .iter()
254 .find(|d| d.name == "coverlet.collector")
255 .unwrap();
256 assert_eq!(coverlet.kind, DepKind::Dev);
257 }
258
259 #[test]
260 fn test_directory_packages_props() {
261 let content = r#"<Project>
262 <PropertyGroup>
263 <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
264 </PropertyGroup>
265 <ItemGroup>
266 <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
267 <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
268 <PackageVersion Include="coverlet.collector" Version="6.0.0" />
269 </ItemGroup>
270</Project>"#;
271
272 let m = DirectoryPackagesPropsParser.parse(content).unwrap();
273 assert_eq!(m.ecosystem, "nuget");
274 assert_eq!(m.dependencies.len(), 3);
275
276 let json_dep = m
277 .dependencies
278 .iter()
279 .find(|d| d.name == "Newtonsoft.Json")
280 .unwrap();
281 assert_eq!(json_dep.version_req.as_deref(), Some("13.0.3"));
282 assert_eq!(json_dep.kind, DepKind::Normal);
283
284 let efcore = m
285 .dependencies
286 .iter()
287 .find(|d| d.name == "Microsoft.EntityFrameworkCore")
288 .unwrap();
289 assert_eq!(efcore.version_req.as_deref(), Some("8.0.0"));
290 assert_eq!(efcore.kind, DepKind::Normal);
291
292 let coverlet = m
293 .dependencies
294 .iter()
295 .find(|d| d.name == "coverlet.collector")
296 .unwrap();
297 assert_eq!(coverlet.version_req.as_deref(), Some("6.0.0"));
298 assert_eq!(coverlet.kind, DepKind::Normal);
299 }
300}