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 #[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}