1use std::{
2 cell::RefCell,
3 collections::{BTreeSet, HashMap},
4 fmt::{self, Display},
5 path::PathBuf,
6};
7
8use comfy_table::{Attribute, Cell, Color};
9use crossterm::style::Stylize;
10use itertools::Itertools;
11use solp::{
12 api::Solution,
13 msbuild::{self, PackagesConfig, Project},
14};
15
16use crate::{Consume, error::Collector, ux};
17
18pub struct Nuget {
19 show_only_mismatched: bool,
20 pub mismatches_found: bool,
21 errors: RefCell<Collector>,
22}
23
24struct MsbuildProject {
25 pub project: Option<msbuild::Project>,
26 pub path: PathBuf,
27}
28
29impl Nuget {
30 #[must_use]
31 pub fn new(show_only_mismatched: bool) -> Self {
32 Self {
33 show_only_mismatched,
34 mismatches_found: false,
35 errors: RefCell::new(Collector::new()),
36 }
37 }
38}
39
40fn collect_msbuild_projects(solution: &Solution) -> Vec<MsbuildProject> {
41 let dir = crate::parent_of(solution.path);
42
43 solution
44 .iterate_projects_without_web_sites()
45 .filter_map(|p| crate::try_make_local_path(dir, p.path_or_uri))
46 .filter_map(|path| match Project::from_path(&path) {
47 Ok(project) => Some(MsbuildProject {
48 path,
49 project: Some(project),
50 }),
51 Err(e) => {
52 if cfg!(debug_assertions) {
53 let p = path.to_str().unwrap_or_default();
54 println!("{p}: {e:?}");
55 }
56 None
57 }
58 })
59 .collect()
60}
61
62fn has_mismatches(versions: &BTreeSet<(Option<&String>, &String)>) -> bool {
63 versions
64 .iter()
65 .into_group_map_by(|x| x.0)
66 .iter()
67 .any(|(_, v)| v.len() > 1)
68}
69
70impl Consume for Nuget {
71 fn ok(&mut self, solution: &solp::api::Solution) {
72 let projects = collect_msbuild_projects(solution);
73
74 let mut nugets = nugets(&projects);
75 let nugets_from_packages_config = nugets_from_packages_configs(&projects);
76
77 let nugets_from_packages_config = nugets_from_packages_config
78 .iter()
79 .map(|(k, v)| (k, v.iter().map(|v1| (None, v1)).collect()));
80
81 nugets.extend(nugets_from_packages_config);
83
84 if nugets.is_empty() {
85 return;
86 }
87
88 let mut table = ux::new_table();
89
90 table.set_header([
91 Cell::new("Package").add_attribute(Attribute::Bold),
92 Cell::new("Version(s)").add_attribute(Attribute::Bold),
93 ]);
94
95 let mut solutions_mismatches = false;
96 nugets
97 .iter()
98 .filter(|(_, versions)| !self.show_only_mismatched || has_mismatches(versions))
99 .sorted_unstable_by(|(a, _), (b, _)| Ord::cmp(&a.to_lowercase(), &b.to_lowercase()))
100 .for_each(|(pkg, versions)| {
101 let grouped = versions.iter().into_group_map_by(|x| x.0);
102 let rows = grouped
103 .iter()
104 .sorted_unstable_by_key(|x| x.0)
105 .map(|(c, v)| {
106 let mismatch = v.len() > 1;
107 let comma_separated = v.iter().map(|(_, v)| v).join(", ");
108 let line = if c.is_some() {
109 format!("{comma_separated} if {}", c.as_ref().unwrap())
110 } else {
111 comma_separated
112 };
113 let mut line = Cell::new(line).add_attribute(Attribute::Italic);
114 if mismatch {
115 line = line.fg(Color::Red);
116 }
117 solutions_mismatches |= mismatch;
118 [Cell::new(pkg), line]
119 });
120 table.add_rows(rows);
121 });
122
123 self.mismatches_found |= solutions_mismatches;
124
125 if self.show_only_mismatched && !solutions_mismatches {
126 return;
127 }
128
129 ux::print_solution_path(solution.path);
130 println!("{table}");
131 println!();
132 }
133
134 fn err(&self, path: &str) {
135 self.errors.borrow_mut().add_path(path);
136 }
137}
138
139impl Display for Nuget {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 if self.mismatches_found && !self.show_only_mismatched {
142 writeln!(
143 f,
144 " {}",
145 "Solutions with nuget packages inconsistency found"
146 .dark_red()
147 .bold()
148 )?;
149 writeln!(f)?;
150 }
151 if self.errors.borrow().count() > 0 {
152 write!(f, "{}", self.errors.borrow())
153 } else {
154 Ok(())
155 }
156 }
157}
158
159fn nugets(projects: &[MsbuildProject]) -> HashMap<&String, BTreeSet<(Option<&String>, &String)>> {
164 projects
165 .iter()
166 .filter_map(|p| p.project.as_ref())
167 .filter_map(|p| p.item_group.as_ref())
168 .flatten()
169 .filter_map(|ig| {
170 Some(
171 ig.package_reference
172 .as_ref()?
173 .iter()
174 .map(|p| (ig.condition.as_ref(), p)),
175 )
176 })
177 .flatten()
178 .into_grouping_map_by(|(_, pack)| &pack.name)
179 .fold(BTreeSet::new(), |mut acc, _key, (cond, val)| {
180 acc.insert((cond, &val.version));
181 acc
182 })
183}
184
185fn nugets_from_packages_configs(projects: &[MsbuildProject]) -> HashMap<String, BTreeSet<String>> {
186 projects
187 .iter()
188 .filter_map(|mp| {
189 let parent = mp.path.parent()?;
190 let packages_config = parent.join("packages.config");
191 PackagesConfig::from_path(packages_config).ok()
192 })
193 .flat_map(|p| p.packages)
194 .into_grouping_map_by(|p| p.name.clone())
195 .fold(BTreeSet::new(), |mut acc, _key, val| {
196 acc.insert(val.version);
197 acc
198 })
199}
200
201#[cfg(test)]
202mod tests {
203 use std::path::PathBuf;
204
205 use solp::msbuild::{ItemGroup, PackageReference, Project};
206
207 use super::*;
208
209 #[test]
210 fn nugets_no_mismatches() {
211 let mut projects = vec![];
213 let packs1 = vec![
214 PackageReference {
215 name: "a".to_string(),
216 version: "1.0.0".to_string(),
217 },
218 PackageReference {
219 name: "b".to_string(),
220 version: "1.0.0".to_string(),
221 },
222 ];
223 let packs2 = vec![
224 PackageReference {
225 name: "c".to_string(),
226 version: "1.0.0".to_string(),
227 },
228 PackageReference {
229 name: "d".to_string(),
230 version: "1.0.0".to_string(),
231 },
232 ];
233 projects.push(create_msbuild_project(packs1, None));
234 projects.push(create_msbuild_project(packs2, None));
235
236 let actual = nugets(&projects);
238
239 assert_eq!(4, actual.len());
241 let has_mismatches = actual.iter().any(|(_, v)| has_mismatches(v));
242 assert!(!has_mismatches);
243 }
244
245 #[test]
246 fn nugets_no_mismatches_same_pgk_in_different_projects() {
247 let mut projects = vec![];
249 let packs1 = vec![
250 PackageReference {
251 name: "a".to_string(),
252 version: "1.0.0".to_string(),
253 },
254 PackageReference {
255 name: "b".to_string(),
256 version: "1.0.0".to_string(),
257 },
258 ];
259 let packs2 = vec![
260 PackageReference {
261 name: "c".to_string(),
262 version: "1.0.0".to_string(),
263 },
264 PackageReference {
265 name: "a".to_string(),
266 version: "1.0.0".to_string(),
267 },
268 ];
269 projects.push(create_msbuild_project(packs1, None));
270 projects.push(create_msbuild_project(packs2, None));
271
272 let actual = nugets(&projects);
274
275 assert_eq!(3, actual.len());
277 let has_mismatches = actual.iter().any(|(_, v)| has_mismatches(v));
278 assert!(!has_mismatches);
279 }
280
281 #[test]
282 fn nugets_has_mismatches() {
283 let mut projects = vec![];
285 let packs1 = vec![
286 PackageReference {
287 name: "a".to_string(),
288 version: "1.0.0".to_string(),
289 },
290 PackageReference {
291 name: "b".to_string(),
292 version: "1.0.0".to_string(),
293 },
294 ];
295 let packs2 = vec![
296 PackageReference {
297 name: "c".to_string(),
298 version: "1.0.0".to_string(),
299 },
300 PackageReference {
301 name: "a".to_string(),
302 version: "2.0.0".to_string(),
303 },
304 ];
305 projects.push(create_msbuild_project(packs1, None));
306 projects.push(create_msbuild_project(packs2, None));
307
308 let actual = nugets(&projects);
310
311 assert_eq!(3, actual.len());
313 let has_mismatches = actual.iter().any(|(_, v)| has_mismatches(v));
314 assert!(has_mismatches);
315 }
316
317 #[test]
318 fn nugets_no_mismatches_by_conditions() {
319 let mut projects = vec![];
321 let packs1 = vec![
322 PackageReference {
323 name: "a".to_string(),
324 version: "1.0.0".to_string(),
325 },
326 PackageReference {
327 name: "b".to_string(),
328 version: "1.0.0".to_string(),
329 },
330 ];
331 let packs2 = vec![
332 PackageReference {
333 name: "c".to_string(),
334 version: "1.0.0".to_string(),
335 },
336 PackageReference {
337 name: "a".to_string(),
338 version: "2.0.0".to_string(),
339 },
340 ];
341 projects.push(create_msbuild_project(packs1, None));
342 projects.push(create_msbuild_project(packs2, Some("1".to_owned())));
343
344 let actual = nugets(&projects);
346
347 assert_eq!(3, actual.len());
349 let has_mismatches = actual.iter().any(|(_, v)| has_mismatches(v));
350 assert!(!has_mismatches);
351 let different_vers_key = "a".to_owned();
352 assert!(actual.contains_key(&different_vers_key));
353 assert_eq!(2, actual.get(&different_vers_key).unwrap().len());
354 }
355
356 fn create_msbuild_project(
357 packs: Vec<PackageReference>,
358 condition: Option<String>,
359 ) -> MsbuildProject {
360 MsbuildProject {
361 project: Some(Project {
362 sdk: Some("5".to_owned()),
363 item_group: Some(vec![ItemGroup {
364 project_reference: None,
365 package_reference: Some(packs),
366 condition,
367 }]),
368 imports: None,
369 import_group: None,
370 }),
371 path: PathBuf::new(),
372 }
373 }
374}