Skip to main content

cargo_sonar/
outdated.rs

1use crate::{cargo::Lockfile, Category, Location, Severity};
2use dyn_iter::IntoDynIterator as _;
3use eyre::Result;
4use std::io::{BufRead as _, BufReader};
5
6const OUTDATED_ENGINE: &str = "outdated";
7
8#[derive(Debug, serde::Deserialize)]
9pub struct CrateMetadata {
10    pub crate_name: String,
11    pub dependencies: Vec<Metadata>,
12}
13
14#[derive(Debug, serde::Deserialize)]
15pub struct Metadata {
16    pub name: String,
17    pub project: String,
18    pub compat: String,
19    pub latest: String,
20    pub kind: Option<String>,
21    pub platform: Option<String>,
22}
23
24#[derive(Debug)]
25pub struct Outdated<'lock> {
26    issues: dyn_iter::DynIter<'lock, Issue<'lock>>,
27}
28
29impl<'lock> Iterator for Outdated<'lock> {
30    type Item = Issue<'lock>;
31
32    #[inline]
33    fn next(&mut self) -> Option<Self::Item> {
34        self.issues.next()
35    }
36}
37
38impl<'lock> Outdated<'lock> {
39    /// Create a Outdated parser for issues
40    ///
41    /// # Errors
42    /// May fail reading and parsing the file (IO errors).
43    #[inline]
44    pub fn try_new<R>(json_read: R, lockfile: &'lock Lockfile) -> Result<Self>
45    where
46        R: std::io::Read + 'static,
47    {
48        let reader = BufReader::new(json_read);
49        let issues = reader
50            .lines()
51            .map_while(Result::ok)
52            .flat_map(|line| serde_json::from_str::<CrateMetadata>(&line))
53            .flat_map(|crate_metadata| {
54                crate_metadata
55                    .dependencies
56                    .into_iter()
57                    .map(move |dependency| (crate_metadata.crate_name.clone(), dependency))
58            })
59            .map(move |(crate_name, dependency)| (lockfile, crate_name, dependency))
60            .into_dyn_iter();
61        let outdated = Self { issues };
62        Ok(outdated)
63    }
64}
65
66pub type CrateName = String;
67pub type Issue<'lock> = (&'lock Lockfile, CrateName, Metadata);
68
69impl crate::Issue for Issue<'_> {
70    #[inline]
71    fn analyzer_id(&self) -> String {
72        OUTDATED_ENGINE.to_owned()
73    }
74
75    #[inline]
76    fn issue_id(&self) -> String {
77        self.2.name.clone()
78    }
79
80    #[inline]
81    fn fingerprint(&self) -> md5::Digest {
82        md5::compute(format!("{}:{}", self.1, self.2.name))
83    }
84
85    #[inline]
86    fn category(&self) -> Category {
87        Category::Security
88    }
89
90    #[inline]
91    fn severity(&self) -> Severity {
92        Severity::Minor
93    }
94
95    #[inline]
96    fn location(&self) -> Option<Location> {
97        let message = format!(
98            "'{}' in crate '{}' is outdated and can be updated up to '{}'",
99            &self.2.name, &self.1, &self.2.latest
100        );
101        let path = self.0.lockfile_path.clone();
102        let range = self.0.dependency_range(self.2.name.as_str());
103        let location = Location {
104            path,
105            range,
106            message,
107        };
108        Some(location)
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use crate::{
115        Category, Issue as _, Severity, TextRange,
116        {cargo::PackageRange, outdated::Outdated, Lockfile},
117    };
118    use std::{io::Write as _, path::PathBuf};
119    use test_log::test;
120
121    #[test]
122    fn single_issue() {
123        let json = r#"{
124          "crate_name": "cargo-sonar",
125          "dependencies": [
126            {
127              "name": "clap",
128              "project": "4.3.8",
129              "compat": "4.3.16",
130              "latest": "4.3.16",
131              "kind": "Normal",
132              "platform": null
133            }
134          ]
135        }
136        <NEW_LINE>
137        {
138          "crate_name": "cargo-codeclimate",
139          "dependencies": [
140            {
141              "name": "clap",
142              "project": "4.3.8",
143              "compat": "4.3.16",
144              "latest": "4.3.16",
145              "kind": "Normal",
146              "platform": null
147            }
148          ]
149        }"#;
150        let json = json
151            .to_owned()
152            .replace('\n', "")
153            .replace("<NEW_LINE>", "\n");
154        let mut outdated_json = tempfile::NamedTempFile::new().unwrap();
155        write!(outdated_json, "{}", json).unwrap();
156        let outdated_json = outdated_json.reopen().unwrap();
157
158        let lockfile = Lockfile {
159            lockfile_path: PathBuf::from("Cargo.lock"),
160            dependencies: [(
161                "clap".to_owned(),
162                PackageRange {
163                    range: TextRange::new((175, 1), (184, 2)),
164                    name_range: TextRange::new((176, 9), (176, 12)),
165                    version_range: TextRange::new((177, 12), (177, 16)),
166                },
167            )]
168            .into_iter()
169            .collect(),
170        };
171
172        let mut outdated = Outdated::try_new(outdated_json, &lockfile).unwrap();
173        let issue = outdated.next().unwrap();
174        assert_eq!(issue.analyzer_id(), "outdated");
175        assert_eq!(issue.issue_uid(), "outdated::clap");
176        assert!(matches!(issue.severity(), Severity::Minor));
177        assert!(matches!(issue.category(), Category::Security));
178        let location = issue.location().unwrap();
179        assert_eq!(location.path, PathBuf::from("Cargo.lock"));
180        assert_eq!(
181            location.message,
182            "'clap' in crate 'cargo-sonar' is outdated and can be updated up to '4.3.16'"
183        );
184        assert_eq!(location.range.start.line, 175);
185        assert_eq!(location.range.end.line, 184);
186        assert_eq!(location.range.start.column, 1);
187        assert_eq!(location.range.end.column, 2);
188
189        let issue = outdated.next().unwrap();
190        assert_eq!(issue.analyzer_id(), "outdated");
191        assert_eq!(issue.issue_uid(), "outdated::clap");
192        assert!(matches!(issue.severity(), Severity::Minor));
193        assert!(matches!(issue.category(), Category::Security));
194        let location = issue.location().unwrap();
195        assert_eq!(location.path, PathBuf::from("Cargo.lock"));
196        assert_eq!(
197            location.message,
198            "'clap' in crate 'cargo-codeclimate' is outdated and can be updated up to '4.3.16'"
199        );
200        assert_eq!(location.range.start.line, 175);
201        assert_eq!(location.range.end.line, 184);
202        assert_eq!(location.range.start.column, 1);
203        assert_eq!(location.range.end.column, 2);
204    }
205}