diff --git a/Cargo.lock b/Cargo.lock index 4529be0f..3de30e31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,24 @@ # It is not intended for manual editing. [[package]] name = "addr2line" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49806b9dadc843c61e7c97e72490ad7f7220ae249012fbda9ad0609457c0543" +checksum = "602d785912f476e480434627e8732e6766b760c045bbf897d9dfaa9f4fbd399c" dependencies = [ "gimli", ] +[[package]] +name = "adler32" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d" + [[package]] name = "aho-corasick" -version = "0.7.10" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" +checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" dependencies = [ "memchr", ] @@ -79,13 +85,14 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" [[package]] name = "backtrace" -version = "0.3.48" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df2f85c8a2abbe3b7d7e748052fdd9b76a0458fdeb16ad4223f5eca78c7c130" +checksum = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c" dependencies = [ "addr2line", "cfg-if", "libc", + "miniz_oxide", "object", "rustc-demangle", ] @@ -310,12 +317,13 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6bffe714b6bb07e42f201352c34f51fefd355ace793f9e638ebd52d23f98d2" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" dependencies = [ "cfg-if", "crossbeam-utils", + "maybe-uninit", ] [[package]] @@ -446,9 +454,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71" +checksum = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909" dependencies = [ "libc", ] @@ -493,9 +501,9 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" [[package]] name = "jobserver" @@ -606,11 +614,20 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + [[package]] name = "num-integer" -version = "0.1.42" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" +checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" dependencies = [ "autocfg", "num-traits", @@ -618,9 +635,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" dependencies = [ "autocfg", ] @@ -637,9 +654,9 @@ dependencies = [ [[package]] name = "object" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cbca9424c482ee628fa549d9c812e2cd22f1180b9222c9200fdfa6eb31aecb2" +checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" [[package]] name = "openssl-probe" @@ -649,18 +666,18 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" [[package]] name = "openssl-src" -version = "111.9.0+1.1.1g" +version = "111.10.0+1.1.1g" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2dbe10ddd1eb335aba3780eb2eaa13e1b7b441d2562fd962398740927f39ec4" +checksum = "47cd4a96d49c3abf4cac8e8a80cba998a030c75608f158fb1c5f609772f265e6" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.57" +version = "0.9.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7410fef80af8ac071d4f63755c0ab89ac3df0fd1ea91f1d1f37cf5cec4395990" +checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de" dependencies = [ "autocfg", "cc", @@ -744,9 +761,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" dependencies = [ "proc-macro2", ] @@ -800,10 +817,11 @@ dependencies = [ [[package]] name = "rayon" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6ce3297f9c85e16621bb8cca38a06779ffc31bb8184e1be4bed2be4678a098" +checksum = "62f02856753d04e03e26929f820d0a0a337ebe71f849801eea335d464b349080" dependencies = [ + "autocfg", "crossbeam-deque", "either", "rayon-core", @@ -811,9 +829,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a89b46efaf957e52b18062fb2f4660f8b8a4dde1807ca002690868ef2c85a9" +checksum = "e92e15d89083484e11353891f1af602cc661426deb9564c298b270c726973280" dependencies = [ "crossbeam-deque", "crossbeam-queue", @@ -848,9 +866,9 @@ checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" [[package]] name = "remove_dir_all" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ "winapi", ] @@ -884,9 +902,9 @@ checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" [[package]] name = "rustsec" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98a9fdef7445a6747fc2288b0d3f66de53dc38f1f4f3e232214bfec2f3deb333" +checksum = "0bcdbdffa54886a5cd9025a2881e148351183d5a8ec83b4b9f87a00427756d44" dependencies = [ "cargo-lock", "chrono", @@ -938,18 +956,18 @@ checksum = "b46e1121e8180c12ff69a742aabc4f310542b6ccb69f1691689ac17fdf8618aa" [[package]] name = "serde" -version = "1.0.111" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9124df5b40cbd380080b2cc6ab894c040a3070d995f5c9dc77e18c34a8ae37d" +checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.111" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2c3ac8e6ca1e9c80b8be1023940162bf81ae3cffbb1809474152f2ce1eb250" +checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" dependencies = [ "proc-macro2", "quote", @@ -958,9 +976,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.53" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" +checksum = "ec2c5d7e739bc07a3e73381a39d61fdb5f671c60c1df26a130690665803d8226" dependencies = [ "itoa", "ryu", @@ -1001,9 +1019,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "structopt" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863246aaf5ddd0d6928dfeb1a9ca65f505599e4e1b399935ef7e75107516b4ef" +checksum = "de2f5e239ee807089b62adce73e48c625e0ed80df02c7ab3f068f5db5281065c" dependencies = [ "clap", "lazy_static", @@ -1012,9 +1030,9 @@ dependencies = [ [[package]] name = "structopt-derive" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d239ca4b13aee7a2142e6795cbd69e457665ff8037aed33b3effdc430d2f927a" +checksum = "510413f9de616762a4fbeab62509bf15c729603b72d7cd71280fbca431b1c118" dependencies = [ "heck", "proc-macro-error", @@ -1025,9 +1043,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.30" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93a56fabc59dce20fe48b6c832cc249c713e7ed88fa28b0ee0a3bfcaae5fe4e2" +checksum = "e8d5d96e8cbb005d6959f119f773bfaebb5684296108fb32600c00cde305b2cd" dependencies = [ "proc-macro2", "quote", @@ -1047,9 +1065,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" dependencies = [ "proc-macro2", "quote", @@ -1097,18 +1115,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13f926965ad00595dd129fa12823b04bbf866e9085ab0a5f2b05b850fbfc344" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "893582086c2f98cde18f906265a65b5030a074b1046c674ae898be6519a7f479" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" dependencies = [ "proc-macro2", "quote", @@ -1134,6 +1152,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "tinyvec" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" + [[package]] name = "toml" version = "0.5.6" @@ -1160,11 +1184,11 @@ dependencies = [ [[package]] name = "unicode-normalization" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" +checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" dependencies = [ - "smallvec", + "tinyvec", ] [[package]] @@ -1198,9 +1222,9 @@ dependencies = [ [[package]] name = "vcpkg" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d1e41d56121e07f1e223db0a4def204e45c85425f6a16d462fd07c8d10d74c" +checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" [[package]] name = "vec_map" @@ -1253,18 +1277,18 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "zstd" -version = "0.5.2+zstd.1.4.5" +version = "0.5.3+zstd.1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644352b10ce7f333d6e0af85bd4f5322dc449416dc1211c6308e95bca8923db4" +checksum = "01b32eaf771efa709e8308605bbf9319bf485dc1503179ec0469b611937c0cd8" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "2.0.4+zstd.1.4.5" +version = "2.0.5+zstd.1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7113c0c9aed2c55181f2d9f5b0a36e7d2c0183b11c058ab40b35987479efe4d7" +checksum = "1cfb642e0d27f64729a639c52db457e0ae906e7bc6f5fe8f5c453230400f1055" dependencies = [ "libc", "zstd-sys", @@ -1272,9 +1296,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "1.4.16+zstd.1.4.5" +version = "1.4.17+zstd.1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c442965efc45353be5a9b9969c9b0872fff6828c7e06d118dda2cb2d0bb11d5a" +checksum = "b89249644df056b522696b1bb9e7c18c87e8ffa3e2f0dc3b0155875d6498f01b" dependencies = [ "cc", "glob", diff --git a/Cargo.toml b/Cargo.toml index c10500d1..28013b1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ krates = { version = "0.3.0", features = ["targets"] } log = "0.4.8" rayon = "1.3.0" regex = { version = "1.3", default-features = true } -semver = "0.9" # cargo_metadata uses this +semver = "0.9" # cargo_metadata uses this version, but it has been updated upstream serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" smallvec = "1.4" diff --git a/deny.toml b/deny.toml index c9c6c74f..800129f3 100644 --- a/deny.toml +++ b/deny.toml @@ -38,3 +38,8 @@ allow = [ "Apache-2.0 WITH LLVM-exception", "MIT", ] + +exceptions = [ + { allow = ["Zlib"], name = "adler32" }, + { allow = ["Zlib"], name = "tinyvec" }, +] diff --git a/src/cargo-deny/check.rs b/src/cargo-deny/check.rs index a2072d2f..f291ca66 100644 --- a/src/cargo-deny/check.rs +++ b/src/cargo-deny/check.rs @@ -1,3 +1,4 @@ +use crate::stats::{AllStats, Stats}; use anyhow::{Context, Error}; use cargo_deny::{advisories, bans, diag::Diagnostic, licenses, sources, CheckCtx}; use clap::arg_enum; @@ -71,7 +72,12 @@ struct ValidConfig { } impl ValidConfig { - fn load(cfg_path: Option, files: &mut codespan::Files) -> Result { + fn load( + cfg_path: Option, + files: &mut codespan::Files, + format: crate::Format, + color: crate::Color, + ) -> Result { let (cfg_contents, cfg_path) = match cfg_path { Some(cfg_path) if cfg_path.exists() => ( std::fs::read_to_string(&cfg_path).with_context(|| { @@ -130,18 +136,87 @@ impl ValidConfig { }; let print = |diags: Vec| { - use codespan_reporting::term; - if diags.is_empty() { return; } - let writer = - term::termcolor::StandardStream::stderr(term::termcolor::ColorChoice::Auto); - let config = term::Config::default(); - let mut writer = writer.lock(); - for diag in &diags { - term::emit(&mut writer, &config, files, &diag).unwrap(); + match format { + crate::Format::Human => { + use codespan_reporting::term::{self, termcolor::ColorChoice}; + let writer = term::termcolor::StandardStream::stderr(match color { + crate::Color::Auto => { + // The termcolor crate doesn't check the stream to see if it's a TTY + // which doesn't really fit with how the rest of the coloring works + if atty::is(atty::Stream::Stderr) { + ColorChoice::Auto + } else { + ColorChoice::Never + } + } + crate::Color::Always => ColorChoice::Always, + crate::Color::Never => ColorChoice::Never, + }); + + let config = term::Config::default(); + let mut writer = writer.lock(); + for diag in diags { + term::emit(&mut writer, &config, files, &diag).unwrap(); + } + } + crate::Format::Json => { + use codespan_reporting::diagnostic::Severity; + use std::io::Write; + + let mut json = Vec::new(); + let stderr = std::io::stderr(); + let mut el = stderr.lock(); + + for diag in diags { + let mut to_print = serde_json::json!({ + "type": "diagnostic", + "fields": { + "severity": match diag.severity { + Severity::Error => "error", + Severity::Warning => "warning", + Severity::Note => "note", + Severity::Help => "help", + Severity::Bug => "bug", + }, + "message": diag.message, + }, + }); + + { + let obj = to_print.as_object_mut().unwrap(); + let obj = obj.get_mut("fields").unwrap().as_object_mut().unwrap(); + + if !diag.labels.is_empty() { + let mut labels = Vec::with_capacity(diag.labels.len()); + + for label in diag.labels { + let location = files + .location(label.file_id, label.range.start as u32) + .unwrap(); + labels.push(serde_json::json!({ + "message": label.message, + "span": &files.source(label.file_id)[label.range], + "line": location.line.to_usize(), + "column": location.column.to_usize(), + })); + } + + obj.insert("labels".to_owned(), serde_json::Value::Array(labels)); + } + } + + json.clear(); + let cursor = std::io::Cursor::new(&mut json); + if serde_json::to_writer(cursor, &to_print).is_ok() { + let _ = el.write_all(&json); + let _ = el.write(b"\n"); + } + } + } } }; @@ -162,13 +237,20 @@ impl ValidConfig { } } -pub fn cmd( +pub(crate) fn cmd( log_level: log::LevelFilter, + format: crate::Format, + color: crate::Color, args: Args, krate_ctx: crate::common::KrateContext, ) -> Result { let mut files = codespan::Files::new(); - let mut cfg = ValidConfig::load(krate_ctx.get_config_path(args.config.clone()), &mut files)?; + let mut cfg = ValidConfig::load( + krate_ctx.get_config_path(args.config.clone()), + &mut files, + format, + color, + )?; let check_advisories = args.which.is_empty() || args @@ -273,11 +355,6 @@ pub fn cmd( let (tx, rx) = crossbeam::channel::unbounded(); let krates = &krates; - let inc_grapher = if args.hide_inclusion_graph { - None - } else { - Some(cargo_deny::diag::Grapher::new(krates)) - }; use cargo_deny::diag::Severity; @@ -308,10 +385,21 @@ pub fn cmd( stats.sources = Some(Stats::default()); } + let show_inclusion_graphs = !args.hide_inclusion_graph; + rayon::scope(|s| { // Asynchronously displays messages sent from the checks s.spawn(|_| { - print_diagnostics(rx, inc_grapher, max_severity, files, &mut stats); + print_diagnostics( + rx, + show_inclusion_graphs, + format, + krates, + max_severity, + files, + &mut stats, + color, + ); }); if let Some(summary) = license_summary { @@ -431,42 +519,44 @@ pub fn cmd( Ok(stats) } -#[derive(Default)] -pub struct Stats { - pub errors: u32, - pub warnings: u32, - pub notes: u32, - pub helps: u32, -} - -#[derive(Default)] -pub struct AllStats { - pub advisories: Option, - pub bans: Option, - pub licenses: Option, - pub sources: Option, -} - -impl AllStats { - pub fn total_errors(&self) -> u32 { - self.advisories.as_ref().map_or(0, |s| s.errors) - + self.bans.as_ref().map_or(0, |s| s.errors) - + self.licenses.as_ref().map_or(0, |s| s.errors) - + self.sources.as_ref().map_or(0, |s| s.errors) - } -} - +#[allow(clippy::too_many_arguments)] fn print_diagnostics( rx: crossbeam::channel::Receiver, - mut inc_grapher: Option>, + show_inclusion_graphs: bool, + format: crate::Format, + krates: &cargo_deny::Krates, max_severity: Option, files: codespan::Files, stats: &mut AllStats, + color: crate::Color, ) { + use crate::Format; use cargo_deny::diag::Check; - use codespan_reporting::{diagnostic::Severity, term}; - - let writer = term::termcolor::StandardStream::stderr(term::termcolor::ColorChoice::Auto); + use codespan_reporting::{ + diagnostic::Severity, + term::{self, termcolor::ColorChoice}, + }; + use std::io::Write; + + let text_grapher = cargo_deny::diag::TextGrapher::new(krates); + let obj_grapher = cargo_deny::diag::ObjectGrapher::new(krates); + + let mut json = Vec::new(); + let stderr = std::io::stderr(); + + let writer = term::termcolor::StandardStream::stderr(match color { + crate::Color::Auto => { + // The termcolor crate doesn't check the stream to see if it's a TTY + // which doesn't really fit with how the rest of the coloring works + if atty::is(atty::Stream::Stderr) { + ColorChoice::Auto + } else { + ColorChoice::Never + } + } + crate::Color::Always => ColorChoice::Always, + crate::Color::Never => ColorChoice::Never, + }); let config = term::Config::default(); for pack in rx { @@ -499,17 +589,97 @@ fn print_diagnostics( None => continue, } - // Add an inclusion graph for each crate identifier attached to the - // diagnostic - if let Some(ref mut grapher) = inc_grapher { - for kid in diag.kids { - inner.notes.push(grapher.write_graph(&kid).unwrap()); - } - } - // We _could_ just take a single lock, but then normal log messages would // not be displayed until after this thread exited - term::emit(&mut lock, &config, &files, &inner).unwrap(); + match format { + Format::Human => { + if show_inclusion_graphs { + for kid in diag.kids { + if let Ok(graph) = text_grapher.write_graph(&kid) { + inner.notes.push(graph); + } + } + } + + let _ = term::emit(&mut lock, &config, &files, &inner); + } + Format::Json => { + let mut to_print = serde_json::json!({ + "type": "diagnostic", + "fields": { + "severity": match inner.severity { + Severity::Error => "error", + Severity::Warning => "warning", + Severity::Note => "note", + Severity::Help => "help", + Severity::Bug => "bug", + }, + "message": inner.message, + }, + }); + + { + let obj = to_print.as_object_mut().unwrap(); + let obj = obj.get_mut("fields").unwrap().as_object_mut().unwrap(); + + if let Some(code) = inner.code { + obj.insert("code".to_owned(), serde_json::Value::String(code)); + } + + if !inner.labels.is_empty() { + let mut labels = Vec::with_capacity(inner.labels.len()); + + for label in inner.labels { + let location = files + .location(label.file_id, label.range.start as u32) + .unwrap(); + labels.push(serde_json::json!({ + "message": label.message, + "span": &files.source(label.file_id)[label.range], + "line": location.line.to_usize(), + "column": location.column.to_usize(), + })); + } + + obj.insert("labels".to_owned(), serde_json::Value::Array(labels)); + } + + if !inner.notes.is_empty() { + obj.insert( + "notes".to_owned(), + serde_json::Value::Array( + inner + .notes + .into_iter() + .map(serde_json::Value::String) + .collect(), + ), + ); + } + + if show_inclusion_graphs { + let mut graphs = Vec::new(); + for kid in diag.kids { + if let Ok(graph) = obj_grapher.write_graph(&kid) { + if let Ok(sgraph) = serde_json::value::to_value(graph) { + graphs.push(sgraph); + } + } + } + + obj.insert("graphs".to_owned(), serde_json::Value::Array(graphs)); + } + } + + json.clear(); + let cursor = std::io::Cursor::new(&mut json); + if serde_json::to_writer(cursor, &to_print).is_ok() { + let mut el = stderr.lock(); + let _ = el.write_all(&json); + let _ = el.write(b"\n"); + } + } + } } } } diff --git a/src/cargo-deny/list.rs b/src/cargo-deny/list.rs index e4653946..ea2bbf84 100644 --- a/src/cargo-deny/list.rs +++ b/src/cargo-deny/list.rs @@ -90,7 +90,12 @@ struct ValidConfig { } impl ValidConfig { - fn load(cfg_path: Option, files: &mut codespan::Files) -> Result { + fn load( + cfg_path: Option, + files: &mut codespan::Files, + format: OutputFormat, + color: ColorWhen, + ) -> Result { let (cfg_contents, cfg_path) = match cfg_path { Some(cfg_path) if cfg_path.exists() => ( std::fs::read_to_string(&cfg_path).with_context(|| { @@ -135,18 +140,87 @@ impl ValidConfig { }; let print = |diags: Vec| { - use codespan_reporting::term; - if diags.is_empty() { return; } - let writer = - term::termcolor::StandardStream::stderr(term::termcolor::ColorChoice::Auto); - let config = term::Config::default(); - let mut writer = writer.lock(); - for diag in &diags { - term::emit(&mut writer, &config, files, &diag).unwrap(); + match format { + OutputFormat::Human | OutputFormat::Tsv => { + use codespan_reporting::term::{self, termcolor::ColorChoice}; + let writer = term::termcolor::StandardStream::stderr(match color { + ColorWhen::Auto => { + // The termcolor crate doesn't check the stream to see if it's a TTY + // which doesn't really fit with how the rest of the coloring works + if atty::is(atty::Stream::Stderr) { + ColorChoice::Auto + } else { + ColorChoice::Never + } + } + ColorWhen::Always => ColorChoice::Always, + ColorWhen::Never => ColorChoice::Never, + }); + + let config = term::Config::default(); + let mut writer = writer.lock(); + for diag in diags { + term::emit(&mut writer, &config, files, &diag).unwrap(); + } + } + OutputFormat::Json => { + use codespan_reporting::diagnostic::Severity; + use std::io::Write; + + let mut json = Vec::new(); + let stderr = std::io::stderr(); + let mut el = stderr.lock(); + + for diag in diags { + let mut to_print = serde_json::json!({ + "type": "diagnostic", + "fields": { + "severity": match diag.severity { + Severity::Error => "error", + Severity::Warning => "warning", + Severity::Note => "note", + Severity::Help => "help", + Severity::Bug => "bug", + }, + "message": diag.message, + }, + }); + + { + let obj = to_print.as_object_mut().unwrap(); + let obj = obj.get_mut("fields").unwrap().as_object_mut().unwrap(); + + if !diag.labels.is_empty() { + let mut labels = Vec::with_capacity(diag.labels.len()); + + for label in diag.labels { + let location = files + .location(label.file_id, label.range.start as u32) + .unwrap(); + labels.push(serde_json::json!({ + "message": label.message, + "span": &files.source(label.file_id)[label.range], + "line": location.line.to_usize(), + "column": location.column.to_usize(), + })); + } + + obj.insert("labels".to_owned(), serde_json::Value::Array(labels)); + } + } + + json.clear(); + let cursor = std::io::Cursor::new(&mut json); + if serde_json::to_writer(cursor, &to_print).is_ok() { + let _ = el.write_all(&json); + let _ = el.write(b"\n"); + } + } + } } }; @@ -173,7 +247,12 @@ pub fn cmd(args: Args, krate_ctx: crate::common::KrateContext) -> Result<(), Err use std::{collections::BTreeMap, fmt::Write}; let mut files = codespan::Files::new(); - let cfg = ValidConfig::load(krate_ctx.get_config_path(args.config), &mut files)?; + let cfg = ValidConfig::load( + krate_ctx.get_config_path(args.config), + &mut files, + args.format, + args.color, + )?; let (krates, store) = rayon::join( || krate_ctx.gather_krates(cfg.targets), diff --git a/src/cargo-deny/main.rs b/src/cargo-deny/main.rs index f0c5330a..c1cf6242 100644 --- a/src/cargo-deny/main.rs +++ b/src/cargo-deny/main.rs @@ -10,6 +10,7 @@ mod common; mod fetch; mod init; mod list; +mod stats; #[derive(StructOpt, Debug)] enum Command { @@ -27,6 +28,60 @@ enum Command { List(list::Args), } +#[derive(StructOpt, Copy, Clone, Debug)] +enum Format { + Human, + Json, +} + +impl Format { + fn variants() -> &'static [&'static str] { + &["human", "json"] + } +} + +impl std::str::FromStr for Format { + type Err = Error; + + fn from_str(s: &str) -> Result { + let lower = s.to_ascii_lowercase(); + + Ok(match lower.as_str() { + "human" => Self::Human, + "json" => Self::Json, + _ => bail!("unknown output format '{}' specified", s), + }) + } +} + +#[derive(StructOpt, Copy, Clone, Debug)] +enum Color { + Auto, + Always, + Never, +} + +impl Color { + fn variants() -> &'static [&'static str] { + &["auto", "always", "never"] + } +} + +impl std::str::FromStr for Color { + type Err = Error; + + fn from_str(s: &str) -> Result { + let lower = s.to_ascii_lowercase(); + + Ok(match lower.as_str() { + "auto" => Self::Auto, + "always" => Self::Always, + "never" => Self::Never, + _ => bail!("unknown color option '{}' specified", s), + }) + } +} + fn parse_level(s: &str) -> Result { s.parse::() .with_context(|| format!("failed to parse level '{}'", s)) @@ -87,34 +142,89 @@ Possible values: * trace ")] log_level: log::LevelFilter, + /// Specify the format of cargo-deny's output + #[structopt(short, long, default_value = "human", possible_values = Format::variants())] + format: Format, + #[structopt(short, long, default_value = "auto", possible_values = Color::variants())] + color: Color, #[structopt(flatten)] ctx: GraphContext, #[structopt(subcommand)] cmd: Command, } -fn setup_logger(level: log::LevelFilter) -> Result<(), fern::InitError> { +fn setup_logger( + level: log::LevelFilter, + format: Format, + color: bool, +) -> Result<(), fern::InitError> { use ansi_term::Color::*; use log::Level::*; - fern::Dispatch::new() - .level(level) - .format(move |out, message, record| { - out.finish(format_args!( - "{date} [{level}] {message}\x1B[0m", - date = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"), - level = match record.level() { - Error => Red.paint("ERROR"), - Warn => Yellow.paint("WARN"), - Info => Green.paint("INFO"), - Debug => Blue.paint("DEBUG"), - Trace => Purple.paint("TRACE"), - }, - message = message, - )); - }) - .chain(std::io::stderr()) - .apply()?; + match format { + Format::Human => { + if color { + fern::Dispatch::new() + .level(level) + .format(move |out, message, record| { + out.finish(format_args!( + "{date} [{level}] {message}\x1B[0m", + date = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"), + level = match record.level() { + Error => Red.paint("ERROR"), + Warn => Yellow.paint("WARN"), + Info => Green.paint("INFO"), + Debug => Blue.paint("DEBUG"), + Trace => Purple.paint("TRACE"), + }, + message = message, + )); + }) + .chain(std::io::stderr()) + .apply()?; + } else { + fern::Dispatch::new() + .level(level) + .format(move |out, message, record| { + out.finish(format_args!( + "{date} [{level}] {message}", + date = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"), + level = match record.level() { + Error => "ERROR", + Warn => "WARN", + Info => "INFO", + Debug => "DEBUG", + Trace => "TRACE", + }, + message = message, + )); + }) + .chain(std::io::stderr()) + .apply()?; + } + } + Format::Json => { + fern::Dispatch::new() + .level(level) + .format(move |out, message, record| { + out.finish(format_args!( + r#"{{"type":"log","fields":{{"timestamp":"{date}","level":"{level}","message":"{message}"}}}}"#, + date = chrono::Utc::now().to_rfc3339(), + level = match record.level() { + Error => "ERROR", + Warn => "WARN", + Info => "INFO", + Debug => "DEBUG", + Trace => "TRACE", + }, + message = message, + )); + }) + .chain(std::io::stderr()) + .apply()?; + } + } + Ok(()) } @@ -132,7 +242,13 @@ fn real_main() -> Result<(), Error> { let log_level = args.log_level; - setup_logger(log_level)?; + let color = match args.color { + Color::Auto => atty::is(atty::Stream::Stderr), + Color::Always => true, + Color::Never => false, + }; + + setup_logger(log_level, args.format, color)?; let manifest_path = match args.ctx.manifest_path { Some(mpath) => mpath, @@ -192,49 +308,11 @@ fn real_main() -> Result<(), Error> { match args.cmd { Command::Check(cargs) => { let show_stats = cargs.show_stats; - let stats = check::cmd(log_level, cargs, krate_ctx)?; + let stats = check::cmd(log_level, args.format, args.color, cargs, krate_ctx)?; let errors = stats.total_errors(); - let mut summary = String::new(); - - // If we're using the default or higher log level, just emit - // a single line, anything else gets a full table - if show_stats || log_level > log::LevelFilter::Warn { - get_full_stats(&mut summary, &stats); - } else if log_level != log::LevelFilter::Off && log_level <= log::LevelFilter::Warn { - let mut print_stats = |check: &str, stats: Option<&check::Stats>| { - use std::fmt::Write; - - if let Some(stats) = stats { - write!( - &mut summary, - "{} {}, ", - check, - if stats.errors > 0 { - ansi_term::Color::Red.paint("FAILED") - } else { - ansi_term::Color::Green.paint("ok") - } - ) - .unwrap(); - } - }; - - print_stats("advisories", stats.advisories.as_ref()); - print_stats("bans", stats.bans.as_ref()); - print_stats("licenses", stats.licenses.as_ref()); - print_stats("sources", stats.sources.as_ref()); - - // Remove trailing , - summary.pop(); - summary.pop(); - summary.push('\n'); - } - - if !summary.is_empty() { - print!("{}", summary); - } + stats::print_stats(stats, show_stats, log_level, args.format, args.color); if errors > 0 { std::process::exit(1); @@ -248,63 +326,6 @@ fn real_main() -> Result<(), Error> { } } -fn get_full_stats(summary: &mut String, stats: &check::AllStats) { - let column = { - let mut max = 0; - let mut count = |check: &str, s: Option<&check::Stats>| { - max = std::cmp::max( - max, - s.map_or(0, |s| { - let status = if s.errors > 0 { - "FAILED".len() - } else { - "ok".len() - }; - - status + check.len() - }), - ); - }; - - count("advisories", stats.advisories.as_ref()); - count("bans", stats.bans.as_ref()); - count("licenses", stats.licenses.as_ref()); - count("sources", stats.sources.as_ref()); - - max + 2 /* spaces */ + 9 /* color escapes */ - }; - - let mut print_stats = |check: &str, stats: Option<&check::Stats>| { - use std::fmt::Write; - - if let Some(stats) = stats { - writeln!( - summary, - "{:>column$}: {} errors, {} warnings, {} notes", - format!( - "{} {}", - check, - if stats.errors > 0 { - ansi_term::Color::Red.paint("FAILED") - } else { - ansi_term::Color::Green.paint("ok") - } - ), - ansi_term::Color::Red.paint(format!("{}", stats.errors)), - ansi_term::Color::Yellow.paint(format!("{}", stats.warnings)), - ansi_term::Color::Blue.paint(format!("{}", stats.notes + stats.helps)), - column = column, - ) - .unwrap(); - } - }; - - print_stats("advisories", stats.advisories.as_ref()); - print_stats("bans", stats.bans.as_ref()); - print_stats("licenses", stats.licenses.as_ref()); - print_stats("sources", stats.sources.as_ref()); -} - fn main() { match real_main() { Ok(_) => {} diff --git a/src/cargo-deny/stats.rs b/src/cargo-deny/stats.rs new file mode 100644 index 00000000..b65f1dae --- /dev/null +++ b/src/cargo-deny/stats.rs @@ -0,0 +1,194 @@ +use crate::{Color, Format}; +use serde::Serialize; + +#[derive(Default, Serialize)] +pub struct Stats { + pub errors: u32, + pub warnings: u32, + pub notes: u32, + pub helps: u32, +} + +#[derive(Default, Serialize)] +pub struct AllStats { + #[serde(skip_serializing_if = "Option::is_none")] + pub advisories: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bans: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub licenses: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sources: Option, +} + +impl AllStats { + pub fn total_errors(&self) -> u32 { + self.advisories.as_ref().map_or(0, |s| s.errors) + + self.bans.as_ref().map_or(0, |s| s.errors) + + self.licenses.as_ref().map_or(0, |s| s.errors) + + self.sources.as_ref().map_or(0, |s| s.errors) + } +} + +pub(crate) fn print_stats( + stats: AllStats, + show_stats: bool, + log_level: log::LevelFilter, + format: Format, + color: Color, +) { + // In the case of human, we print to stdout, to distinguish it from the rest + // of the output, but for JSON we still go to stderr since presumably computers + // will be looking at that output and we don't want to confuse them + match format { + Format::Human => { + let mut summary = String::new(); + + let color = match color { + Color::Auto => atty::is(atty::Stream::Stdout), + Color::Always => true, + Color::Never => false, + }; + + // If we're using the default or higher log level, just emit + // a single line, anything else gets a full table + if show_stats || log_level > log::LevelFilter::Warn { + write_full_stats(&mut summary, &stats, color); + } else if log_level != log::LevelFilter::Off && log_level <= log::LevelFilter::Warn { + write_min_stats(&mut summary, &stats, color); + } + + if !summary.is_empty() { + print!("{}", summary); + } + } + Format::Json => { + let ssummary = serde_json::json!({ + "type": "summary", + "fields": serde_json::to_value(&stats).unwrap(), + }); + + let to_print = serde_json::to_vec(&ssummary).unwrap(); + + use std::io::Write; + let stderr = std::io::stderr(); + let mut el = stderr.lock(); + let _ = el.write_all(&to_print); + let _ = el.write(b"\n"); + } + } +} + +fn write_min_stats(mut summary: &mut String, stats: &AllStats, color: bool) { + let mut print_stats = |check: &str, stats: Option<&Stats>| { + use std::fmt::Write; + + if let Some(stats) = stats { + write!(&mut summary, "{} ", check).unwrap(); + + if color { + write!( + &mut summary, + "{}, ", + if stats.errors > 0 { + ansi_term::Color::Red.paint("FAILED") + } else { + ansi_term::Color::Green.paint("ok") + } + ) + .unwrap(); + } else { + write!( + &mut summary, + "{}, ", + if stats.errors > 0 { "FAILED" } else { "ok" } + ) + .unwrap(); + } + } + }; + + print_stats("advisories", stats.advisories.as_ref()); + print_stats("bans", stats.bans.as_ref()); + print_stats("licenses", stats.licenses.as_ref()); + print_stats("sources", stats.sources.as_ref()); + + // Remove trailing ", " + summary.pop(); + summary.pop(); + summary.push('\n'); +} + +fn write_full_stats(summary: &mut String, stats: &AllStats, color: bool) { + let column = { + let mut max = 0; + let mut count = |check: &str, s: Option<&Stats>| { + max = std::cmp::max( + max, + s.map_or(0, |s| { + let status = if s.errors > 0 { + "FAILED".len() + } else { + "ok".len() + }; + + status + check.len() + }), + ); + }; + + count("advisories", stats.advisories.as_ref()); + count("bans", stats.bans.as_ref()); + count("licenses", stats.licenses.as_ref()); + count("sources", stats.sources.as_ref()); + + max + 2 /* spaces */ + if color { 9 /* color escapes */ } else { 0 } + }; + + let mut print_stats = |check: &str, stats: Option<&Stats>| { + use std::fmt::Write; + + if let Some(stats) = stats { + if color { + writeln!( + summary, + "{:>column$}: {} errors, {} warnings, {} notes", + format!( + "{} {}", + check, + if stats.errors > 0 { + ansi_term::Color::Red.paint("FAILED") + } else { + ansi_term::Color::Green.paint("ok") + } + ), + ansi_term::Color::Red.paint(format!("{}", stats.errors)), + ansi_term::Color::Yellow.paint(format!("{}", stats.warnings)), + ansi_term::Color::Blue.paint(format!("{}", stats.notes + stats.helps)), + column = column, + ) + .unwrap(); + } else { + writeln!( + summary, + "{:>column$}: {} errors, {} warnings, {} notes", + format!( + "{} {}", + check, + if stats.errors > 0 { "FAILED" } else { "ok" } + ), + stats.errors, + stats.warnings, + stats.notes + stats.helps, + column = column, + ) + .unwrap(); + } + } + }; + + print_stats("advisories", stats.advisories.as_ref()); + print_stats("bans", stats.bans.as_ref()); + print_stats("licenses", stats.licenses.as_ref()); + print_stats("sources", stats.sources.as_ref()); +} diff --git a/src/diag.rs b/src/diag.rs index 4f67f2aa..157dc98e 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -152,7 +152,7 @@ use std::collections::HashSet; /// In our case, we only care about the inverted form, ie, not what the /// dependencies of a package are, but rather how a particular package /// is actually pulled in via 1 or more root crates -pub struct Grapher<'a> { +pub struct TextGrapher<'a> { krates: &'a Krates, } @@ -167,12 +167,12 @@ struct NodePrint<'a> { kind: &'static str, } -impl<'a> Grapher<'a> { +impl<'a> TextGrapher<'a> { pub fn new(krates: &'a Krates) -> Self { Self { krates } } - pub fn write_graph(&mut self, id: &Kid) -> Result { + pub fn write_graph(&self, id: &Kid) -> Result { let mut out = String::with_capacity(1024); let mut levels = Vec::new(); let mut visited = HashSet::new(); @@ -273,3 +273,110 @@ impl<'a> Grapher<'a> { Ok(()) } } + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn is_false(v: &bool) -> bool { + !v +} + +#[allow(clippy::ptr_arg)] +fn is_empty(v: &Vec) -> bool { + v.is_empty() +} + +fn is_normal(v: &'static str) -> bool { + v == "" +} + +#[derive(serde::Serialize)] +pub struct GraphNode { + name: String, + version: semver::Version, + #[serde(skip_serializing_if = "is_normal")] + kind: &'static str, + #[serde(skip_serializing_if = "is_false")] + repeat: bool, + #[serde(skip_serializing_if = "is_empty")] + parents: Vec, +} + +/// As with the textgrapher, only crates inclusion graphs, but in the form of +/// a serializable object rather than a text string +pub struct ObjectGrapher<'a> { + krates: &'a Krates, +} + +impl<'a> ObjectGrapher<'a> { + pub fn new(krates: &'a Krates) -> Self { + Self { krates } + } + + pub fn write_graph(&self, id: &Kid) -> Result { + let mut visited = HashSet::new(); + + let node_id = self.krates.nid_for_kid(id).context("unable to find node")?; + let krate = &self.krates[node_id]; + + let np = NodePrint { + krate, + id: node_id, + kind: "", + }; + + Ok(self.write_parent(np, &mut visited)?) + } + + fn write_parent( + &self, + np: NodePrint<'a>, + visited: &mut HashSet, + ) -> Result { + use pg::visit::EdgeRef; + + let repeat = !visited.insert(np.id); + + let mut node = GraphNode { + name: np.krate.name.clone(), + version: np.krate.version.clone(), + kind: np.kind, + repeat, + parents: Vec::new(), + }; + + if repeat { + return Ok(node); + } + + let mut parents = smallvec::SmallVec::<[NodePrint<'a>; 10]>::new(); + let graph = self.krates.graph(); + for edge in graph.edges_directed(np.id, pg::Direction::Incoming) { + let parent_id = edge.source(); + let parent = &graph[parent_id]; + + let kind = match edge.weight().kind { + DepKind::Normal => "", + DepKind::Dev => "dev", + DepKind::Build => "build", + }; + + parents.push(NodePrint { + krate: &parent.krate, + id: parent_id, + kind, + }); + } + + if !parents.is_empty() { + // Resolve uses Hash data types internally but we want consistent output ordering + parents.sort_by_key(|n| &n.krate.id); + node.parents.reserve(parents.len()); + + for parent in parents { + let pnode = self.write_parent(parent, visited)?; + node.parents.push(pnode); + } + } + + Ok(node) + } +}