use crate::{cargo::Lockfile, Category, Location, Severity};
use dyn_iter::IntoDynIterator as _;
use eyre::Result;
use std::io::{BufRead as _, BufReader};
const OUTDATED_ENGINE: &str = "outdated";
#[derive(Debug, serde::Deserialize)]
pub struct CrateMetadata {
pub crate_name: String,
pub dependencies: Vec<Metadata>,
}
#[derive(Debug, serde::Deserialize)]
pub struct Metadata {
pub name: String,
pub project: String,
pub compat: String,
pub latest: String,
pub kind: Option<String>,
pub platform: Option<String>,
}
#[derive(Debug)]
pub struct Outdated<'lock> {
issues: dyn_iter::DynIter<'lock, Issue<'lock>>,
}
impl<'lock> Iterator for Outdated<'lock> {
type Item = Issue<'lock>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.issues.next()
}
}
impl<'lock> Outdated<'lock> {
#[inline]
pub fn try_new<R>(json_read: R, lockfile: &'lock Lockfile) -> Result<Self>
where
R: std::io::Read + 'static,
{
let reader = BufReader::new(json_read);
let issues = reader
.lines()
.map_while(Result::ok)
.flat_map(|line| serde_json::from_str::<CrateMetadata>(&line))
.flat_map(|crate_metadata| {
crate_metadata
.dependencies
.into_iter()
.map(move |dependency| (crate_metadata.crate_name.clone(), dependency))
})
.map(move |(crate_name, dependency)| (lockfile, crate_name, dependency))
.into_dyn_iter();
let outdated = Self { issues };
Ok(outdated)
}
}
pub type CrateName = String;
pub type Issue<'lock> = (&'lock Lockfile, CrateName, Metadata);
impl crate::Issue for Issue<'_> {
#[inline]
fn analyzer_id(&self) -> String {
OUTDATED_ENGINE.to_owned()
}
#[inline]
fn issue_id(&self) -> String {
self.2.name.clone()
}
#[inline]
fn fingerprint(&self) -> md5::Digest {
md5::compute(format!("{}:{}", self.1, self.2.name))
}
#[inline]
fn category(&self) -> Category {
Category::Security
}
#[inline]
fn severity(&self) -> Severity {
Severity::Minor
}
#[inline]
fn location(&self) -> Option<Location> {
let message = format!(
"'{}' in crate '{}' is outdated and can be updated up to '{}'",
&self.2.name, &self.1, &self.2.latest
);
let path = self.0.lockfile_path.clone();
let range = self.0.dependency_range(self.2.name.as_str());
let location = Location {
path,
range,
message,
};
Some(location)
}
}
#[cfg(test)]
mod tests {
use crate::{
Category, Issue as _, Severity, TextRange,
{cargo::PackageRange, outdated::Outdated, Lockfile},
};
use std::{io::Write as _, path::PathBuf};
use test_log::test;
#[test]
fn single_issue() {
let json = r#"{
"crate_name": "cargo-sonar",
"dependencies": [
{
"name": "clap",
"project": "4.3.8",
"compat": "4.3.16",
"latest": "4.3.16",
"kind": "Normal",
"platform": null
}
]
}
<NEW_LINE>
{
"crate_name": "cargo-codeclimate",
"dependencies": [
{
"name": "clap",
"project": "4.3.8",
"compat": "4.3.16",
"latest": "4.3.16",
"kind": "Normal",
"platform": null
}
]
}"#;
let json = json
.to_owned()
.replace('\n', "")
.replace("<NEW_LINE>", "\n");
let mut outdated_json = tempfile::NamedTempFile::new().unwrap();
write!(outdated_json, "{}", json).unwrap();
let outdated_json = outdated_json.reopen().unwrap();
let lockfile = Lockfile {
lockfile_path: PathBuf::from("Cargo.lock"),
dependencies: [(
"clap".to_owned(),
PackageRange {
range: TextRange::new((175, 1), (184, 2)),
name_range: TextRange::new((176, 9), (176, 12)),
version_range: TextRange::new((177, 12), (177, 16)),
},
)]
.into_iter()
.collect(),
};
let mut outdated = Outdated::try_new(outdated_json, &lockfile).unwrap();
let issue = outdated.next().unwrap();
assert_eq!(issue.analyzer_id(), "outdated");
assert_eq!(issue.issue_uid(), "outdated::clap");
assert!(matches!(issue.severity(), Severity::Minor));
assert!(matches!(issue.category(), Category::Security));
let location = issue.location().unwrap();
assert_eq!(location.path, PathBuf::from("Cargo.lock"));
assert_eq!(
location.message,
"'clap' in crate 'cargo-sonar' is outdated and can be updated up to '4.3.16'"
);
assert_eq!(location.range.start.line, 175);
assert_eq!(location.range.end.line, 184);
assert_eq!(location.range.start.column, 1);
assert_eq!(location.range.end.column, 2);
let issue = outdated.next().unwrap();
assert_eq!(issue.analyzer_id(), "outdated");
assert_eq!(issue.issue_uid(), "outdated::clap");
assert!(matches!(issue.severity(), Severity::Minor));
assert!(matches!(issue.category(), Category::Security));
let location = issue.location().unwrap();
assert_eq!(location.path, PathBuf::from("Cargo.lock"));
assert_eq!(
location.message,
"'clap' in crate 'cargo-codeclimate' is outdated and can be updated up to '4.3.16'"
);
assert_eq!(location.range.start.line, 175);
assert_eq!(location.range.end.line, 184);
assert_eq!(location.range.start.column, 1);
assert_eq!(location.range.end.column, 2);
}
}