Skip to main content

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            Self::None => {
22                write!(f, "None")
23            }
24            Self::Major => {
25                write!(f, "Major")
26            }
27            Self::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            Self::Never => {
50                write!(f, "Never")
51            }
52            Self::Auto => {
53                write!(f, "Auto")
54            }
55            Self::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 upgrade
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            Self::Major => {
190                write!(f, "Major")
191            }
192            Self::Minor => {
193                write!(f, "Minor")
194            }
195            Self::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            Self::UpToDate => {
215                write!(f, "up-to-date")
216            }
217            Self::UpdateRequired => {
218                write!(f, "update-required")
219            }
220        }
221    }
222}
223
224/// main entry point for the dotnet-outdated call
225///
226/// # Errors
227///
228/// fails if the dotnet outdated command fails or if the parsing of the output fails
229pub fn outdated(
230    options: &DotnetOutdatedOptions,
231) -> Result<(IndicatedUpdateRequirement, DotnetOutdatedData), crate::Error> {
232    let output_dir = tempfile::tempdir()?;
233    let output_file = output_dir.path().join("outdated.json");
234    let output_file = output_file
235        .to_str()
236        .ok_or(crate::Error::PathConversionError)?;
237
238    let mut cmd = Command::new("dotnet");
239
240    cmd.args([
241        "outdated",
242        "--fail-on-updates",
243        "--output",
244        output_file,
245        "--output-format",
246        "json",
247    ]);
248
249    if options.include_auto_references {
250        cmd.args(["--include-auto-references"]);
251    }
252
253    cmd.args(["--pre-release", &options.pre_release.to_string()]);
254
255    if !options.include.is_empty() {
256        for i in &options.include {
257            cmd.args(["--include", i]);
258        }
259    }
260
261    if !options.exclude.is_empty() {
262        for e in &options.exclude {
263            cmd.args(["--exclude", e]);
264        }
265    }
266
267    if options.transitive {
268        cmd.args([
269            "--transitive",
270            "--transitive-depth",
271            &options.transitive_depth.to_string(),
272        ]);
273    }
274
275    cmd.args(["--version-lock", &options.version_lock.to_string()]);
276
277    if let Some(ref input_dir) = options.input_dir {
278        cmd.args([&input_dir]);
279    }
280
281    let output = cmd.output()?;
282
283    if !output.status.success() {
284        warn!(
285            "dotnet outdated did not return with a successful exit code: {}",
286            output.status
287        );
288        debug!("stdout:\n{}", from_utf8(&output.stdout)?);
289        if !output.stderr.is_empty() {
290            warn!("stderr:\n{}", from_utf8(&output.stderr)?);
291        }
292    }
293
294    let update_requirement = if output.status.success() {
295        IndicatedUpdateRequirement::UpToDate
296    } else {
297        IndicatedUpdateRequirement::UpdateRequired
298    };
299
300    let output_file_content = std::fs::read_to_string(output_file)?;
301
302    trace!("Read output file content:\n{}", output_file_content);
303
304    let jd = &mut serde_json::Deserializer::from_str(&output_file_content);
305    let data: DotnetOutdatedData = serde_path_to_error::deserialize(jd)?;
306    Ok((update_requirement, data))
307}
308
309#[cfg(test)]
310mod test {
311    // use super::*;
312    // use crate::Error;
313
314    // /// this test requires a .sln and/or .csproj files in the current
315    // /// directory (working dir of the tests)
316    // #[test]
317    // fn test_run_dotnet_outdated() -> Result<(), Error> {
318    //     outdated(&Default::default())?;
319    //     Ok(())
320    // }
321}