use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Default)]
pub struct ScanOpts {
pub workspace_only: bool,
pub include_deps: bool,
pub features: Vec<String>,
pub all_features: bool,
pub no_default_features: bool,
pub all_targets: bool,
pub targets: Vec<String>,
pub manifest_path: Option<PathBuf>,
pub plugin_timeout_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Scope {
pub workspace_only: bool,
pub include_deps: bool,
pub features: Vec<String>,
#[serde(default)]
pub all_features: bool,
#[serde(default)]
pub no_default_features: bool,
pub all_targets: bool,
pub targets: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest_path: Option<PathBuf>,
}
impl Scope {
pub fn diff_fields(&self, other: &Scope) -> Vec<String> {
let mut diffs = Vec::new();
if self.workspace_only != other.workspace_only {
diffs.push(format!(
"workspace_only: baseline={}, current={}",
self.workspace_only, other.workspace_only
));
}
if self.include_deps != other.include_deps {
diffs.push(format!(
"include_deps: baseline={}, current={}",
self.include_deps, other.include_deps
));
}
if self.features != other.features {
diffs.push(format!(
"features: baseline={:?}, current={:?}",
self.features, other.features
));
}
if self.all_features != other.all_features {
diffs.push(format!(
"all_features: baseline={}, current={}",
self.all_features, other.all_features
));
}
if self.no_default_features != other.no_default_features {
diffs.push(format!(
"no_default_features: baseline={}, current={}",
self.no_default_features, other.no_default_features
));
}
if self.all_targets != other.all_targets {
diffs.push(format!(
"all_targets: baseline={}, current={}",
self.all_targets, other.all_targets
));
}
if self.targets != other.targets {
diffs.push(format!(
"targets: baseline={:?}, current={:?}",
self.targets, other.targets
));
}
if self.manifest_path != other.manifest_path {
diffs.push(format!(
"manifest_path: baseline={:?}, current={:?}",
self.manifest_path, other.manifest_path
));
}
diffs
}
}
impl From<&ScanOpts> for Scope {
fn from(opts: &ScanOpts) -> Self {
Scope {
workspace_only: opts.workspace_only,
include_deps: opts.include_deps,
features: opts.features.clone(),
all_features: opts.all_features,
no_default_features: opts.no_default_features,
all_targets: opts.all_targets,
targets: opts.targets.clone(),
manifest_path: opts.manifest_path.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UnitKind {
Workspace,
Dep,
}
impl std::fmt::Display for UnitKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnitKind::Workspace => write!(f, "workspace"),
UnitKind::Dep => write!(f, "dep"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Unit {
pub name: String,
pub kind: UnitKind,
pub unsafe_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Occurrence {
pub unit: String,
pub file: PathBuf,
pub line: u32,
pub col: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct Totals {
pub workspace_unsafe: u64,
pub deps_unsafe: u64,
pub overall_unsafe: u64,
}
impl Totals {
pub fn from_units(units: &[Unit]) -> Self {
let workspace_unsafe: u64 = units
.iter()
.filter(|u| u.kind == UnitKind::Workspace)
.map(|u| u.unsafe_count)
.sum();
let deps_unsafe: u64 = units
.iter()
.filter(|u| u.kind == UnitKind::Dep)
.map(|u| u.unsafe_count)
.sum();
Self {
workspace_unsafe,
deps_unsafe,
overall_unsafe: workspace_unsafe + deps_unsafe,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanResult {
pub tool_version: String,
pub analyzer_id: String,
pub language: String,
pub scope: Scope,
pub units: Vec<Unit>,
pub totals: Totals,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub details: Vec<Occurrence>,
}
impl ScanResult {
pub fn from_parts(
analyzer_id: impl Into<String>,
language: impl Into<String>,
opts: &ScanOpts,
units: Vec<Unit>,
details: Vec<Occurrence>,
) -> Self {
let totals = Totals::from_units(&units);
Self {
tool_version: env!("CARGO_PKG_VERSION").into(),
analyzer_id: analyzer_id.into(),
language: language.into(),
scope: Scope::from(opts),
units,
totals,
details,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Violation {
pub unit: String,
pub kind: UnitKind,
pub baseline: u64,
pub actual: u64,
pub delta: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Warning {
pub unit: String,
pub kind: UnitKind,
pub budget: u64,
pub actual: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckResult {
pub scan: ScanResult,
pub violations: Vec<Violation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<Warning>,
pub passed: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unit_kind_display_workspace() {
assert_eq!(format!("{}", UnitKind::Workspace), "workspace");
}
#[test]
fn test_unit_kind_display_dep() {
assert_eq!(format!("{}", UnitKind::Dep), "dep");
}
#[test]
fn test_scope_from_scan_opts() {
let opts = ScanOpts {
workspace_only: true,
include_deps: false,
features: vec!["feature1".into(), "feature2".into()],
all_features: true,
no_default_features: true,
all_targets: true,
targets: vec!["x86_64-unknown-linux-gnu".into()],
manifest_path: Some(PathBuf::from("/path/to/Cargo.toml")),
..Default::default()
};
let scope = Scope::from(&opts);
assert!(scope.workspace_only);
assert!(!scope.include_deps);
assert_eq!(scope.features, vec!["feature1", "feature2"]);
assert!(scope.all_features);
assert!(scope.no_default_features);
assert!(scope.all_targets);
assert_eq!(scope.targets, vec!["x86_64-unknown-linux-gnu"]);
assert_eq!(
scope.manifest_path,
Some(PathBuf::from("/path/to/Cargo.toml"))
);
}
#[test]
fn test_scope_from_scan_opts_defaults() {
let opts = ScanOpts::default();
let scope = Scope::from(&opts);
assert!(!scope.workspace_only);
assert!(!scope.include_deps);
assert!(scope.features.is_empty());
assert!(!scope.all_features);
assert!(!scope.no_default_features);
assert!(!scope.all_targets);
assert!(scope.targets.is_empty());
assert!(scope.manifest_path.is_none());
}
#[test]
fn test_scan_opts_default() {
let opts = ScanOpts::default();
assert!(!opts.workspace_only);
assert!(!opts.include_deps);
assert!(opts.features.is_empty());
assert!(!opts.all_features);
assert!(!opts.no_default_features);
assert!(!opts.all_targets);
assert!(opts.targets.is_empty());
assert!(opts.manifest_path.is_none());
}
#[test]
fn test_totals_default() {
let totals = Totals::default();
assert_eq!(totals.workspace_unsafe, 0);
assert_eq!(totals.deps_unsafe, 0);
assert_eq!(totals.overall_unsafe, 0);
}
#[test]
fn test_totals_from_units_empty() {
let totals = Totals::from_units(&[]);
assert_eq!(totals.workspace_unsafe, 0);
assert_eq!(totals.deps_unsafe, 0);
assert_eq!(totals.overall_unsafe, 0);
}
#[test]
fn test_totals_from_units_workspace_only() {
let units = vec![
Unit {
name: "crate_a".into(),
kind: UnitKind::Workspace,
unsafe_count: 5,
},
Unit {
name: "crate_b".into(),
kind: UnitKind::Workspace,
unsafe_count: 3,
},
];
let totals = Totals::from_units(&units);
assert_eq!(totals.workspace_unsafe, 8);
assert_eq!(totals.deps_unsafe, 0);
assert_eq!(totals.overall_unsafe, 8);
}
#[test]
fn test_totals_from_units_mixed() {
let units = vec![
Unit {
name: "my_crate".into(),
kind: UnitKind::Workspace,
unsafe_count: 10,
},
Unit {
name: "libc".into(),
kind: UnitKind::Dep,
unsafe_count: 100,
},
Unit {
name: "serde".into(),
kind: UnitKind::Dep,
unsafe_count: 5,
},
];
let totals = Totals::from_units(&units);
assert_eq!(totals.workspace_unsafe, 10);
assert_eq!(totals.deps_unsafe, 105);
assert_eq!(totals.overall_unsafe, 115);
}
#[test]
fn test_scope_diff_fields_equal() {
let a = Scope {
workspace_only: false,
include_deps: true,
features: vec!["f1".into()],
all_features: false,
no_default_features: false,
all_targets: false,
targets: vec![],
manifest_path: None,
};
assert!(a.diff_fields(&a.clone()).is_empty());
}
#[test]
fn test_scope_diff_fields_all_different() {
let baseline = Scope {
workspace_only: false,
include_deps: true,
features: vec!["f1".into()],
all_features: false,
no_default_features: false,
all_targets: false,
targets: vec![],
manifest_path: None,
};
let current = Scope {
workspace_only: true,
include_deps: false,
features: vec!["f1".into(), "f2".into()],
all_features: true,
no_default_features: true,
all_targets: true,
targets: vec!["aarch64-unknown-linux-gnu".into()],
manifest_path: Some(PathBuf::from("Cargo.toml")),
};
let diffs = baseline.diff_fields(¤t);
assert_eq!(diffs.len(), 8);
assert!(diffs[0].contains("workspace_only"));
assert!(diffs[1].contains("include_deps"));
assert!(diffs[2].contains("features"));
assert!(diffs[3].contains("all_features"));
assert!(diffs[4].contains("no_default_features"));
assert!(diffs[5].contains("all_targets"));
assert!(diffs[6].contains("targets"));
assert!(diffs[7].contains("manifest_path"));
}
#[test]
fn test_scope_diff_fields_single_change() {
let baseline = Scope {
workspace_only: false,
include_deps: true,
features: vec!["f1".into()],
all_features: false,
no_default_features: false,
all_targets: false,
targets: vec![],
manifest_path: None,
};
let mut current = baseline.clone();
current.features = vec![];
let diffs = baseline.diff_fields(¤t);
assert_eq!(diffs.len(), 1);
assert!(diffs[0].contains("features"));
assert!(diffs[0].contains(r#"baseline=["f1"]"#));
assert!(diffs[0].contains("current=[]"));
}
#[test]
fn test_unit_kind_serialization() {
let workspace = UnitKind::Workspace;
let dep = UnitKind::Dep;
assert_eq!(serde_json::to_string(&workspace).unwrap(), "\"workspace\"");
assert_eq!(serde_json::to_string(&dep).unwrap(), "\"dep\"");
}
#[test]
fn test_unit_kind_deserialization() {
let workspace: UnitKind = serde_json::from_str("\"workspace\"").unwrap();
let dep: UnitKind = serde_json::from_str("\"dep\"").unwrap();
assert_eq!(workspace, UnitKind::Workspace);
assert_eq!(dep, UnitKind::Dep);
}
}