Skip to main content

solv/
nuget.rs

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        // merging packages from packages.config if any
82        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
159/// returns hashmap where<br/>
160/// key - package name<br/>
161/// value - (condition, version) tuples set<br/>
162/// condition is optional
163fn 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        // arrange
212        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        // act
237        let actual = nugets(&projects);
238
239        // assert
240        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        // arrange
248        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        // act
273        let actual = nugets(&projects);
274
275        // assert
276        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        // arrange
284        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        // act
309        let actual = nugets(&projects);
310
311        // assert
312        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        // arrange
320        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        // act
345        let actual = nugets(&projects);
346
347        // assert
348        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}