dotnet_parser/
outdated.rs

1//! This parses the output of dotnet-outdated
2use std::process::Command;
3use std::str::from_utf8;
4use tracing::{debug, trace, warn};
5
6/// should upgrades be locked to a specific major/minor/patch level only
7#[derive(Debug, Clone, Default, clap::ValueEnum)]
8pub enum VersionLock {
9    /// do not lock the version when considering upgrades
10    #[default]
11    None,
12    /// lock the version to the current major version (i.e. only consider minor versions and patch levels)
13    Major,
14    /// lock the version to the current minor version (i.e. only consider patch levels)
15    Minor,
16}
17
18impl std::fmt::Display for VersionLock {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        match self {
21            VersionLock::None => {
22                write!(f, "None")
23            }
24            VersionLock::Major => {
25                write!(f, "Major")
26            }
27            VersionLock::Minor => {
28                write!(f, "Minor")
29            }
30        }
31    }
32}
33
34/// Should dotnet-outdated look for pre-release versions of packages?
35#[derive(Debug, Clone, Default, clap::ValueEnum)]
36pub enum PreRelease {
37    /// Never look for pre-releases
38    Never,
39    /// automatically let dotnet-outdated determine if pre-releases are appropriate to look for
40    #[default]
41    Auto,
42    /// Always look for pre-releases
43    Always,
44}
45
46impl std::fmt::Display for PreRelease {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            PreRelease::Never => {
50                write!(f, "Never")
51            }
52            PreRelease::Auto => {
53                write!(f, "Auto")
54            }
55            PreRelease::Always => {
56                write!(f, "Always")
57            }
58        }
59    }
60}
61
62/// These are options to modify the behaviour of the program.
63#[derive(Debug, Default, clap::Parser)]
64pub struct DotnetOutdatedOptions {
65    /// Include auto referenced packages
66    #[clap(
67        short = 'i',
68        long = "include-auto-references",
69        help = "Include auto-referenced packages"
70    )]
71    include_auto_references: bool,
72    /// Should dotnet-outdated look for pre-release version of packages
73    #[clap(
74        long = "pre-release",
75        value_name = "VALUE",
76        default_value = "auto",
77        help = "Should dotnet-outdated look for pre-release versions of packages",
78        value_enum
79    )]
80    pre_release: PreRelease,
81    /// Dependencies that should be included in the consideration
82    #[clap(
83        long = "include",
84        value_name = "PACKAGE_NAME",
85        number_of_values = 1,
86        help = "Dependencies that should be included in the consideration"
87    )]
88    include: Vec<String>,
89    /// Dependencies that should be excluded from consideration
90    #[clap(
91        long = "exclude",
92        value_name = "PACKAGE_NAME",
93        number_of_values = 1,
94        help = "Dependencies that should be excluded from consideration"
95    )]
96    exclude: Vec<String>,
97    /// should transitive dependencies be considered
98    #[clap(
99        short = 't',
100        long = "transitive",
101        help = "Should dotnet-outdated consider transitiv dependencies"
102    )]
103    transitive: bool,
104    /// if transitive dependencies are considered, to which depth
105    #[clap(
106        long = "transitive-depth",
107        value_name = "DEPTH",
108        default_value = "1",
109        requires = "transitive",
110        help = "If transitive dependencies are considered, to which depth in the dependency tree"
111    )]
112    transitive_depth: u64,
113    /// should we consider all upgrades or limit to minor and/or patch levels only
114    #[clap(
115        long = "version-lock",
116        value_name = "LOCK",
117        default_value = "none",
118        help = "Should we consider all updates or just minor versions and/or patch levels",
119        value_enum
120    )]
121    version_lock: VersionLock,
122    /// path to pass to dotnet-outdated, defaults to current directory
123    #[clap(
124        long = "input-dir",
125        value_name = "DIRECTORY",
126        help = "The input directory to pass to dotnet outdated"
127    )]
128    input_dir: Option<std::path::PathBuf>,
129}
130
131/// Outer structure for parsing donet-outdated output
132#[derive(Debug, serde::Serialize, serde::Deserialize)]
133#[serde(rename_all = "PascalCase")]
134pub struct DotnetOutdatedData {
135    /// one per .csproj file (e.g. binaries, tests,...)
136    pub projects: Vec<Project>,
137}
138
139/// Per project data
140#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
141#[serde(rename_all = "PascalCase")]
142pub struct Project {
143    /// Name of the project
144    pub name: String,
145    /// absolute path to the .csproj file for it
146    pub file_path: String,
147    /// frameworks this targets with dependencies
148    pub target_frameworks: Vec<Framework>,
149}
150
151/// Per project per target framework data
152#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
153#[serde(rename_all = "PascalCase")]
154pub struct Framework {
155    /// Name of the framework, e.g. net5.0
156    pub name: String,
157    /// dependencies of the project when targeted for this framework
158    pub dependencies: Vec<Dependency>,
159}
160
161/// Data about each outdated dependency
162#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
163#[serde(rename_all = "PascalCase")]
164pub struct Dependency {
165    /// Name of the dependency
166    pub name: String,
167    /// the version that is currently in use
168    pub resolved_version: String,
169    /// the latest version as limited by the version lock parameter
170    pub latest_version: String,
171    /// severity of this upgrade
172    pub upgrade_severity: Severity,
173}
174
175/// Severity of a required upgrade
176#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
177pub enum Severity {
178    /// a major version upgrade
179    Major,
180    /// a minor version uprade
181    Minor,
182    /// a patch level upgrade
183    Patch,
184}
185
186impl std::fmt::Display for Severity {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        match self {
189            Severity::Major => {
190                write!(f, "Major")
191            }
192            Severity::Minor => {
193                write!(f, "Minor")
194            }
195            Severity::Patch => {
196                write!(f, "Patch")
197            }
198        }
199    }
200}
201
202/// What the exit code indicated about required updates
203#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
204pub enum IndicatedUpdateRequirement {
205    /// No update is required
206    UpToDate,
207    /// An update is required
208    UpdateRequired,
209}
210
211impl std::fmt::Display for IndicatedUpdateRequirement {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        match self {
214            IndicatedUpdateRequirement::UpToDate => {
215                write!(f, "up-to-date")
216            }
217            IndicatedUpdateRequirement::UpdateRequired => {
218                write!(f, "update-required")
219            }
220        }
221    }
222}
223
224/// main entry point for the dotnet-oudated call
225pub fn outdated(
226    options: &DotnetOutdatedOptions,
227) -> Result<(IndicatedUpdateRequirement, DotnetOutdatedData), crate::Error> {
228    let output_dir = tempfile::tempdir()?;
229    let output_file = output_dir.path().join("outdated.json");
230    let output_file = output_file
231        .to_str()
232        .ok_or(crate::Error::PathConversionError)?;
233
234    let mut cmd = Command::new("dotnet");
235
236    cmd.args([
237        "outdated",
238        "--fail-on-updates",
239        "--output",
240        output_file,
241        "--output-format",
242        "json",
243    ]);
244
245    if options.include_auto_references {
246        cmd.args(["--include-auto-references"]);
247    }
248
249    cmd.args(["--pre-release", &options.pre_release.to_string()]);
250
251    if !options.include.is_empty() {
252        for i in &options.include {
253            cmd.args(["--include", i]);
254        }
255    }
256
257    if !options.exclude.is_empty() {
258        for e in &options.exclude {
259            cmd.args(["--exclude", e]);
260        }
261    }
262
263    if options.transitive {
264        cmd.args([
265            "--transitive",
266            "--transitive-depth",
267            &options.transitive_depth.to_string(),
268        ]);
269    }
270
271    cmd.args(["--version-lock", &options.version_lock.to_string()]);
272
273    if let Some(ref input_dir) = options.input_dir {
274        cmd.args([&input_dir]);
275    }
276
277    let output = cmd.output()?;
278
279    if !output.status.success() {
280        warn!(
281            "dotnet outdated did not return with a successful exit code: {}",
282            output.status
283        );
284        debug!("stdout:\n{}", from_utf8(&output.stdout)?);
285        if !output.stderr.is_empty() {
286            warn!("stderr:\n{}", from_utf8(&output.stderr)?);
287        }
288    }
289
290    let update_requirement = if output.status.success() {
291        IndicatedUpdateRequirement::UpToDate
292    } else {
293        IndicatedUpdateRequirement::UpdateRequired
294    };
295
296    let output_file_content = std::fs::read_to_string(output_file)?;
297
298    trace!("Read output file content:\n{}", output_file_content);
299
300    let jd = &mut serde_json::Deserializer::from_str(&output_file_content);
301    let data: DotnetOutdatedData = serde_path_to_error::deserialize(jd)?;
302    Ok((update_requirement, data))
303}
304
305#[cfg(test)]
306mod test {
307    use super::*;
308    use crate::Error;
309
310    /// this test requires a .sln and/or .csproj files in the current
311    /// directory (working dir of the tests)
312    #[test]
313    fn test_run_dotnet_outdated() -> Result<(), Error> {
314        outdated(&Default::default())?;
315        Ok(())
316    }
317}