#![cfg_attr(feature = "document-features", doc = "Feature flags")]
#![cfg_attr(
feature = "document-features",
cfg_attr(doc, doc = ::document_features::document_features!())
)]
mod cargo;
pub use crate::cargo::Lockfile;
mod cli;
pub use cli::Command;
#[cfg(feature = "audit")]
pub mod audit;
#[cfg(feature = "clippy")]
pub mod clippy;
#[cfg(feature = "deny")]
pub mod deny;
#[cfg(feature = "outdated")]
pub mod outdated;
#[cfg(test)]
mod test;
#[cfg(feature = "typos")]
pub mod typos;
#[cfg(feature = "udeps")]
pub mod udeps;
#[cfg(feature = "codeclimate")]
pub mod codeclimate;
use cli::InputPath;
#[cfg(feature = "codeclimate")]
pub use codeclimate::CodeClimate;
#[cfg(feature = "sonar")]
pub mod sonar;
#[cfg(feature = "sonar")]
pub use sonar::Sonar;
use eyre::{eyre, Context as _, Result};
use md5::Digest;
use std::{
fs::File,
io::{Read, Stdin},
marker::PhantomData,
path::PathBuf,
};
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum Severity {
Blocker,
Critical,
Major,
Minor,
Info,
}
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub enum Category {
Bug,
Complexity,
Duplication,
Performance,
Security,
Style,
}
#[derive(Clone, Copy, Debug)]
struct Position {
line: usize,
column: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct TextRange {
start: Position,
end: Position,
}
impl TextRange {
#[inline]
#[must_use]
pub fn new(
(start_line, start_column): (usize, usize),
(end_line, end_column): (usize, usize),
) -> Self {
Self {
start: Position {
line: start_line,
column: start_column,
},
end: Position {
line: end_line,
column: end_column,
},
}
}
}
impl Default for TextRange {
#[inline]
fn default() -> Self {
Self::new((1, 0), (1, 0))
}
}
#[derive(Clone, Debug)]
pub struct Location {
pub path: PathBuf,
pub range: TextRange,
pub message: String,
}
pub trait Issue {
fn analyzer_id(&self) -> String;
fn issue_id(&self) -> String;
#[inline]
fn issue_uid(&self) -> String {
format!("{}::{}", self.analyzer_id(), self.issue_id())
}
fn fingerprint(&self) -> Digest;
fn category(&self) -> Category;
fn severity(&self) -> Severity;
fn location(&self) -> Option<Location>;
#[inline]
fn other_locations(&self) -> Vec<Location> {
Vec::new()
}
}
enum IssueType<'lock> {
#[cfg(feature = "audit")]
Audit(Box<audit::Issue<'lock>>),
#[cfg(feature = "clippy")]
Clippy(Box<clippy::Issue>),
#[cfg(feature = "deny")]
Deny(deny::Issue<'lock>),
#[cfg(feature = "outdated")]
Outdated(outdated::Issue<'lock>),
#[cfg(feature = "typos")]
Typos(typos::Issue),
#[cfg(feature = "udeps")]
Udeps(udeps::Issue<'lock>),
#[cfg(test)]
Test(test::Issue),
#[doc(hidden)]
#[allow(unused)]
Phantom(PhantomData<&'lock ()>),
}
impl crate::Issue for IssueType<'_> {
#[inline]
fn analyzer_id(&self) -> String {
match *self {
#[cfg(feature = "audit")]
IssueType::Audit(ref audit) => audit.analyzer_id(),
#[cfg(feature = "clippy")]
IssueType::Clippy(ref clippy) => clippy.analyzer_id(),
#[cfg(feature = "deny")]
IssueType::Deny(ref deny) => deny.analyzer_id(),
#[cfg(feature = "outdated")]
IssueType::Outdated(ref outdated) => outdated.analyzer_id(),
#[cfg(feature = "typos")]
IssueType::Typos(ref typos) => typos.analyzer_id(),
#[cfg(feature = "udeps")]
IssueType::Udeps(ref udeps) => udeps.analyzer_id(),
#[cfg(test)]
IssueType::Test(ref test) => test.analyzer_id(),
#[allow(clippy::unimplemented)]
_ => unimplemented!(),
}
}
#[inline]
fn issue_id(&self) -> String {
match *self {
#[cfg(feature = "audit")]
IssueType::Audit(ref audit) => audit.issue_id(),
#[cfg(feature = "clippy")]
IssueType::Clippy(ref clippy) => clippy.issue_id(),
#[cfg(feature = "deny")]
IssueType::Deny(ref deny) => deny.issue_id(),
#[cfg(feature = "outdated")]
IssueType::Outdated(ref outdated) => outdated.issue_id(),
#[cfg(feature = "typos")]
IssueType::Typos(ref typos) => typos.issue_id(),
#[cfg(feature = "udeps")]
IssueType::Udeps(ref udeps) => udeps.issue_id(),
#[cfg(test)]
IssueType::Test(ref test) => test.issue_id(),
#[allow(clippy::unimplemented)]
_ => unimplemented!(),
}
}
#[inline]
fn fingerprint(&self) -> Digest {
match *self {
#[cfg(feature = "audit")]
IssueType::Audit(ref audit) => audit.fingerprint(),
#[cfg(feature = "clippy")]
IssueType::Clippy(ref clippy) => clippy.fingerprint(),
#[cfg(feature = "deny")]
IssueType::Deny(ref deny) => deny.fingerprint(),
#[cfg(feature = "outdated")]
IssueType::Outdated(ref outdated) => outdated.fingerprint(),
#[cfg(feature = "typos")]
IssueType::Typos(ref typos) => typos.fingerprint(),
#[cfg(feature = "udeps")]
IssueType::Udeps(ref udeps) => udeps.fingerprint(),
#[cfg(test)]
IssueType::Test(ref test) => test.fingerprint(),
#[allow(clippy::unimplemented)]
_ => unimplemented!(),
}
}
#[inline]
fn category(&self) -> Category {
match *self {
#[cfg(feature = "audit")]
IssueType::Audit(ref audit) => audit.category(),
#[cfg(feature = "clippy")]
IssueType::Clippy(ref clippy) => clippy.category(),
#[cfg(feature = "deny")]
IssueType::Deny(ref deny) => deny.category(),
#[cfg(feature = "outdated")]
IssueType::Outdated(ref outdated) => outdated.category(),
#[cfg(feature = "typos")]
IssueType::Typos(ref typos) => typos.category(),
#[cfg(feature = "udeps")]
IssueType::Udeps(ref udeps) => udeps.category(),
#[cfg(test)]
IssueType::Test(ref test) => test.category(),
#[allow(clippy::unimplemented)]
_ => unimplemented!(),
}
}
#[inline]
fn severity(&self) -> Severity {
match *self {
#[cfg(feature = "audit")]
IssueType::Audit(ref audit) => audit.severity(),
#[cfg(feature = "clippy")]
IssueType::Clippy(ref clippy) => clippy.severity(),
#[cfg(feature = "deny")]
IssueType::Deny(ref deny) => deny.severity(),
#[cfg(feature = "outdated")]
IssueType::Outdated(ref outdated) => outdated.severity(),
#[cfg(feature = "typos")]
IssueType::Typos(ref typos) => typos.severity(),
#[cfg(feature = "udeps")]
IssueType::Udeps(ref udeps) => udeps.severity(),
#[cfg(test)]
IssueType::Test(ref test) => test.severity(),
#[allow(clippy::unimplemented)]
_ => unimplemented!(),
}
}
#[inline]
fn location(&self) -> Option<Location> {
match *self {
#[cfg(feature = "audit")]
IssueType::Audit(ref audit) => audit.location(),
#[cfg(feature = "clippy")]
IssueType::Clippy(ref clippy) => clippy.location(),
#[cfg(feature = "deny")]
IssueType::Deny(ref deny) => deny.location(),
#[cfg(feature = "outdated")]
IssueType::Outdated(ref outdated) => outdated.location(),
#[cfg(feature = "typos")]
IssueType::Typos(ref typos) => typos.location(),
#[cfg(feature = "udeps")]
IssueType::Udeps(ref udeps) => udeps.location(),
#[cfg(test)]
IssueType::Test(ref test) => test.location(),
#[allow(clippy::unimplemented)]
_ => unimplemented!(),
}
}
}
trait FromIssues<'lock> {
type Report;
fn from_issues(issues: impl IntoIterator<Item = IssueType<'lock>>) -> Self::Report;
}
fn read_from_input_path(input_path: &InputPath, binary_name: &str) -> Result<impl Read> {
enum InputPathRead {
Stdin(Stdin),
File(File),
}
impl Read for InputPathRead {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
InputPathRead::Stdin(stdin) => stdin.read(buf),
InputPathRead::File(file) => file.read(buf),
}
}
}
match input_path {
cli::InputPath::Stdin => Ok(InputPathRead::Stdin(std::io::stdin())),
cli::InputPath::File(json_path) => File::open(json_path)
.map(InputPathRead::File)
.with_context(|| {
format!(
"failed to open '{}' report from '{}' file",
binary_name,
json_path.display(),
)
}),
}
}
pub struct Converter {
#[cfg_attr(
all(
feature = "clippy",
not(any(
feature = "audit",
feature = "deny",
feature = "outdated",
feature = "typos",
feature = "udeps"
))
),
allow(unused)
)]
lockfile: Lockfile,
}
impl Converter {
#[inline]
pub fn try_new() -> Result<Self> {
let path = PathBuf::from("Cargo.lock");
let lockfile = Lockfile::try_from(path.as_path())?;
Ok(Self { lockfile })
}
fn report<'c, F>(&'c self, options: &Command) -> Result<F::Report>
where
F: FromIssues<'c>,
{
if [
#[cfg(feature = "audit")]
matches!(options.audit_path, cli::InputPath::Stdin),
#[cfg(feature = "clippy")]
matches!(options.clippy_path, cli::InputPath::Stdin),
#[cfg(feature = "deny")]
matches!(options.deny_path, cli::InputPath::Stdin),
#[cfg(feature = "outdated")]
matches!(options.outdated_path, cli::InputPath::Stdin),
#[cfg(feature = "typos")]
matches!(options.typos_path, cli::InputPath::Stdin),
#[cfg(feature = "udeps")]
matches!(options.udeps_path, cli::InputPath::Stdin),
]
.into_iter()
.map(usize::from)
.sum::<usize>()
> 1
{
return Err(eyre!(
"cannot have “stdin” (‘-’) be configured for two different reports at once"
));
}
let mut issues: Box<dyn Iterator<Item = IssueType<'_>>> = Box::new(std::iter::empty());
#[cfg(feature = "audit")]
if options.audit {
let audit_read = read_from_input_path(&options.audit_path, "cargo-audit")?;
issues = Box::new(
issues.chain(
audit::Audit::try_new(audit_read, &self.lockfile)?
.map(Box::new)
.map(IssueType::Audit),
),
);
}
#[cfg(feature = "clippy")]
if options.clippy {
let json_read = read_from_input_path(&options.clippy_path, "cargo-clippy")?;
issues = Box::new(
issues.chain(
clippy::Clippy::try_new(json_read)?
.map(Box::new)
.map(IssueType::Clippy),
),
);
}
#[cfg(feature = "deny")]
if options.deny {
let json_read = read_from_input_path(&options.deny_path, "cargo-deny")?;
issues = Box::new(
issues.chain(deny::Deny::try_new(json_read, &self.lockfile)?.map(IssueType::Deny)),
);
}
#[cfg(feature = "outdated")]
if options.outdated {
let json_read = read_from_input_path(&options.outdated_path, "cargo-outdated")?;
issues = Box::new(issues.chain(
outdated::Outdated::try_new(json_read, &self.lockfile)?.map(IssueType::Outdated),
));
}
#[cfg(feature = "typos")]
if options.typos {
let json_read = read_from_input_path(&options.typos_path, "typos")?;
issues =
Box::new(issues.chain(typos::Typos::try_new(json_read)?.map(IssueType::Typos)));
}
#[cfg(feature = "udeps")]
if options.udeps {
let json_read = read_from_input_path(&options.udeps_path, "cargo-udeps")?;
issues = Box::new(
issues
.chain(udeps::Udeps::try_new(json_read, &self.lockfile)?.map(IssueType::Udeps)),
);
}
Ok(F::from_issues(issues))
}
#[cfg(feature = "sonar")]
#[inline]
pub fn sonarize(&self, options: &Command) -> Result<sonar::Issues> {
self.report::<Sonar>(options)
}
#[cfg(feature = "codeclimate")]
#[inline]
pub fn codeclimatize(&self, options: &Command) -> Result<codeclimate::Issues> {
self.report::<CodeClimate>(options)
}
}