diff --git a/Cargo.lock b/Cargo.lock index 6ca5daed..fee3aa96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,9 +125,9 @@ checksum = "81a18687293a1546b67c246452202bbbf143d239cb43494cc163da14979082da" [[package]] name = "cargo" -version = "0.46.0" +version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7bc456671b4cacf55d0682a9e39b01b571a499664a8467c610dc1957bc77ca" +checksum = "6d435f32d32f191cdf788ef94403d7566dc5fcdadbcf7cd191cc0d2f9079d0a4" dependencies = [ "anyhow", "atty", @@ -164,7 +164,6 @@ dependencies = [ "opener", "openssl", "percent-encoding", - "remove_dir_all", "rustc-workspace-hack", "rustfix", "same-file", @@ -204,6 +203,8 @@ dependencies = [ "rand", "regex", "rstest", + "serde", + "serde_json", "strum", "strum_macros", "walkdir", @@ -323,9 +324,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.7.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" dependencies = [ "core-foundation-sys", "libc", @@ -333,9 +334,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" +checksum = "c0af3b5e4601de3837c9332e29e0aae47a0d46ebfa246d12b82f564bac233393" [[package]] name = "crates-io" @@ -519,6 +520,7 @@ name = "geiger" version = "0.4.5" dependencies = [ "proc-macro2", + "serde", "syn", ] @@ -1177,18 +1179,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.114" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5317f7588f0a5078ee60ef675ef96735a1442132dc645eb1d12c018620ed8cd3" +checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.114" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0be94b04690fbaed37cddffc5c134bf537c8e3329d53e982fe04c374978f8e" +checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" dependencies = [ "proc-macro2", "quote", @@ -1206,9 +1208,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3433e879a558dde8b5e8feb2a04899cf34fdde1fafb894687e52105fc1162ac3" +checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" dependencies = [ "itoa", "ryu", diff --git a/cargo-geiger/Cargo.toml b/cargo-geiger/Cargo.toml index 25fd2117..68f9a171 100644 --- a/cargo-geiger/Cargo.toml +++ b/cargo-geiger/Cargo.toml @@ -14,7 +14,7 @@ version = "0.10.2" maintenance = { status = "experimental" } [dependencies] -cargo = "0.46.0" +cargo = "0.47.0" cargo-platform = "0.1.1" colored = "2.0.0" console = "0.11.3" @@ -22,6 +22,8 @@ env_logger = "0.7.1" geiger = { path = "../geiger", version = "0.4.5" } petgraph = "0.5.1" pico-args = "0.3.3" +serde = { version = "1.0.116", features = ["derive"] } +serde_json = "1.0.57" strum = "0.19.2" strum_macros = "0.19.2" walkdir = "2.3.1" diff --git a/cargo-geiger/src/find.rs b/cargo-geiger/src/find.rs index b0748a04..37c692d4 100644 --- a/cargo-geiger/src/find.rs +++ b/cargo-geiger/src/find.rs @@ -1,3 +1,4 @@ +use crate::report::UnsafeInfo; use crate::rs_file::{ into_rs_code_file, is_file_with_ext, PackageMetrics, RsFile, RsFileMetricsWrapper, @@ -8,8 +9,8 @@ use cargo::core::package::PackageSet; use cargo::core::{Package, PackageId}; use cargo::util::CargoResult; use geiger::find_unsafe_in_file; -use geiger::IncludeTests; -use std::collections::HashMap; +use geiger::{CounterBlock, IncludeTests}; +use std::collections::{HashMap, HashSet}; use std::path::Path; use std::path::PathBuf; use walkdir::WalkDir; @@ -133,3 +134,157 @@ fn find_rs_files_in_packages<'a>( .map(move |path| (pack.package_id(), path)) }) } + +pub fn unsafe_stats( + pack_metrics: &PackageMetrics, + rs_files_used: &HashSet, +) -> UnsafeInfo { + // The crate level "forbids unsafe code" metric __used to__ only + // depend on entry point source files that were __used by the + // build__. This was too subtle in my opinion. For a crate to be + // classified as forbidding unsafe code, all entry point source + // files must declare `forbid(unsafe_code)`. Either a crate + // forbids all unsafe code or it allows it _to some degree_. + let forbids_unsafe = pack_metrics + .rs_path_to_metrics + .iter() + .filter(|(_, v)| v.is_crate_entry_point) + .all(|(_, v)| v.metrics.forbids_unsafe); + + let mut used = CounterBlock::default(); + let mut unused = CounterBlock::default(); + + for (path_buf, rs_file_metrics_wrapper) in &pack_metrics.rs_path_to_metrics { + let target = if rs_files_used.contains(path_buf) { + &mut used + } else { + &mut unused + }; + *target += rs_file_metrics_wrapper.metrics.counters.clone(); + } + UnsafeInfo { + used, + unused, + forbids_unsafe, + } +} + +#[cfg(test)] +mod tests { + use crate::{ + find::unsafe_stats, + report::UnsafeInfo, + rs_file::{PackageMetrics, RsFileMetricsWrapper}, + }; + use geiger::Count; + use std::{ + collections::HashSet, + path::PathBuf, + }; + + #[test] + fn unsafe_stats_from_nothing_are_empty() { + let stats = unsafe_stats(&Default::default(), &Default::default()); + let expected = UnsafeInfo { forbids_unsafe: true, ..Default::default() }; + assert_eq!(stats, expected); + } + + #[test] + fn unsafe_stats_report_forbid_unsafe_as_true_if_all_entry_points_forbid_unsafe() { + let metrics = metrics_from_iter(vec![ + ( + "foo.rs", + MetricsBuilder::default().forbids_unsafe(true).is_entry_point(true).build(), + ), + ]); + let stats = unsafe_stats(&metrics, &set_of_paths(&["foo.rs"])); + assert!(stats.forbids_unsafe) + } + + #[test] + fn unsafe_stats_report_forbid_unsafe_as_false_if_one_entry_point_allows_unsafe() { + let metrics = metrics_from_iter(vec![ + ( + "foo.rs", + MetricsBuilder::default().forbids_unsafe(true).is_entry_point(true).build(), + ), + ( + "bar.rs", + MetricsBuilder::default().forbids_unsafe(false).is_entry_point(true).build(), + ), + ]); + let stats = unsafe_stats(&metrics, &set_of_paths(&["foo.rs", "bar.rs"])); + assert!(!stats.forbids_unsafe) + } + + #[test] + fn unsafe_stats_accumulate_counters() { + let metrics = metrics_from_iter(vec![ + ( + "foo.rs", + MetricsBuilder::default().functions(2, 1).build(), + ), + ( + "bar.rs", + MetricsBuilder::default().functions(5, 3).build(), + ), + ( + "baz.rs", + MetricsBuilder::default().functions(20, 10).build(), + ), + ( + "quux.rs", + MetricsBuilder::default().functions(200, 100).build(), + ), + ]); + let stats = unsafe_stats(&metrics, &set_of_paths(&["foo.rs", "bar.rs"])); + assert_eq!(stats.used.functions.safe, 7); + assert_eq!(stats.used.functions.unsafe_, 4); + assert_eq!(stats.unused.functions.safe, 220); + assert_eq!(stats.unused.functions.unsafe_, 110); + } + + fn metrics_from_iter(it: I) -> PackageMetrics + where + I: IntoIterator, + P: Into, + { + PackageMetrics { + rs_path_to_metrics: it.into_iter().map(|(p, m)| (p.into(), m)).collect(), + } + } + + fn set_of_paths(it: I) -> HashSet + where + I: IntoIterator, + I::Item: Into, + { + it.into_iter().map(Into::into).collect() + } + + #[derive(Default)] + struct MetricsBuilder { + inner: RsFileMetricsWrapper, + } + + impl MetricsBuilder { + fn forbids_unsafe(mut self, yes: bool) -> Self { + self.inner.metrics.forbids_unsafe = yes; + self + } + + fn functions(mut self, safe: u64, unsafe_: u64) -> Self { + self.inner.metrics.counters.functions = Count { safe, unsafe_ }; + self + } + + fn is_entry_point(mut self, yes: bool) -> Self { + self.inner.is_crate_entry_point = yes; + self + } + + fn build(self) -> RsFileMetricsWrapper { + self.inner + } + } +} diff --git a/cargo-geiger/src/format/print.rs b/cargo-geiger/src/format/print.rs index 9b924820..c45a3a61 100644 --- a/cargo-geiger/src/format/print.rs +++ b/cargo-geiger/src/format/print.rs @@ -12,6 +12,11 @@ pub enum Prefix { None, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OutputFormat { + Json, +} + pub struct PrintConfig<'a> { /// Don't truncate dependencies that have already been displayed. pub all: bool, @@ -27,6 +32,7 @@ pub struct PrintConfig<'a> { pub charset: Charset, pub allow_partial_results: bool, pub include_tests: IncludeTests, + pub output_format: Option, } pub fn colorize( diff --git a/cargo-geiger/src/format/table.rs b/cargo-geiger/src/format/table.rs index a8210c41..7c9f396f 100644 --- a/cargo-geiger/src/format/table.rs +++ b/cargo-geiger/src/format/table.rs @@ -1,14 +1,13 @@ -use crate::find::GeigerContext; +use crate::find::{unsafe_stats, GeigerContext}; use crate::format::print::{colorize, PrintConfig}; use crate::format::tree::TextTreeLine; use crate::format::{ get_kind_group_name, CrateDetectionStatus, EmojiSymbols, SymbolKind, }; -use crate::rs_file::PackageMetrics; use cargo::core::package::PackageSet; use geiger::{Count, CounterBlock}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::path::PathBuf; // TODO: use a table library, or factor the tableness out in a smarter way. This @@ -31,86 +30,49 @@ pub fn create_table_from_text_tree_lines( text_tree_lines: Vec, ) -> (Vec, u64) { let mut table_lines = Vec::::new(); - let mut total_package_counts = TotalPackageCounts::new(); - - let mut package_status = HashMap::new(); let mut warning_count = 0; - + let mut visited = HashSet::new(); + let emoji_symbols = EmojiSymbols::new(print_config.charset); for text_tree_line in text_tree_lines { match text_tree_line { TextTreeLine::Package { id, tree_vines } => { + let package_is_new = visited.insert(id); let pack = package_set.get_one(id).unwrap_or_else(|_| { // TODO: Avoid panic, return Result. panic!("Expected to find package by id: {}", id); }); - let pack_metrics = - match geiger_context.pack_id_to_metrics.get(&id) { - Some(m) => m, - None => { - eprintln!( - "WARNING: No metrics found for package: {}", - id - ); - warning_count += 1; - continue; - } - }; - package_status.entry(id).or_insert_with(|| { - let unsafe_found = pack_metrics - .rs_path_to_metrics - .iter() - .filter(|(k, _)| rs_files_used.contains(k.as_path())) - .any(|(_, v)| v.metrics.counters.has_unsafe()); - - // The crate level "forbids unsafe code" metric __used to__ only - // depend on entry point source files that were __used by the - // build__. This was too subtle in my opinion. For a crate to be - // classified as forbidding unsafe code, all entry point source - // files must declare `forbid(unsafe_code)`. Either a crate - // forbids all unsafe code or it allows it _to some degree_. - let crate_forbids_unsafe = pack_metrics - .rs_path_to_metrics - .iter() - .filter(|(_, v)| v.is_crate_entry_point) - .all(|(_, v)| v.metrics.forbids_unsafe); - - for (path_buf, rs_file_metrics_wrapper) in - &pack_metrics.rs_path_to_metrics - { - let target = if rs_files_used.contains(path_buf) { - &mut total_package_counts.total_counter_block - } else { - &mut total_package_counts.total_unused_counter_block - }; - *target = target.clone() - + rs_file_metrics_wrapper.metrics.counters.clone(); + let pack_metrics = match geiger_context.pack_id_to_metrics.get(&id) { + Some(m) => m, + None => { + warning_count += package_is_new as u64; + eprintln!("WARNING: No metrics found for package: {}", id); + continue; } - - match (unsafe_found, crate_forbids_unsafe) { - (false, true) => { - total_package_counts - .none_detected_forbids_unsafe += 1; - CrateDetectionStatus::NoneDetectedForbidsUnsafe - } - (false, false) => { - total_package_counts.none_detected_allows_unsafe += - 1; - CrateDetectionStatus::NoneDetectedAllowsUnsafe - } - (true, _) => { - total_package_counts.unsafe_detected += 1; - CrateDetectionStatus::UnsafeDetected - } + }; + let unsafety = unsafe_stats(pack_metrics, rs_files_used); + if package_is_new { + total_package_counts.total_counter_block += unsafety.used.clone(); + total_package_counts.total_unused_counter_block += unsafety.unused.clone(); + } + let unsafe_found = unsafety.used.has_unsafe(); + let crate_forbids_unsafe = unsafety.forbids_unsafe; + let total_inc = package_is_new as i32; + let crate_detection_status = match (unsafe_found, crate_forbids_unsafe) { + (false, true) => { + total_package_counts.none_detected_forbids_unsafe += total_inc; + CrateDetectionStatus::NoneDetectedForbidsUnsafe } - }); - - let emoji_symbols = EmojiSymbols::new(print_config.charset); + (false, false) => { + total_package_counts.none_detected_allows_unsafe += total_inc; + CrateDetectionStatus::NoneDetectedAllowsUnsafe + } + (true, _) => { + total_package_counts.unsafe_detected += total_inc; + CrateDetectionStatus::UnsafeDetected + } + }; - let crate_detection_status = - package_status.get(&id).unwrap_or_else(|| { - panic!("Expected to find package by id: {}", &id) - }); let icon = match crate_detection_status { CrateDetectionStatus::NoneDetectedForbidsUnsafe => { emoji_symbols.emoji(SymbolKind::Lock) @@ -133,7 +95,7 @@ pub fn create_table_from_text_tree_lines( &crate_detection_status, ); let unsafe_info = colorize( - table_row(&pack_metrics, &rs_files_used), + table_row(&unsafety.used, &unsafety.unused), &crate_detection_status, ); @@ -248,18 +210,10 @@ fn table_footer( colorize(output, &status) } -fn table_row(pms: &PackageMetrics, rs_files_used: &HashSet) -> String { - let mut used = CounterBlock::default(); - let mut not_used = CounterBlock::default(); - for (path_buf, rs_file_metrics_wrapper) in pms.rs_path_to_metrics.iter() { - let target = if rs_files_used.contains(path_buf) { - &mut used - } else { - &mut not_used - }; - *target = - target.clone() + rs_file_metrics_wrapper.metrics.counters.clone(); - } +fn table_row( + used: &CounterBlock, + not_used: &CounterBlock, +) -> String { let fmt = |used: &Count, not_used: &Count| { format!("{}/{}", used.unsafe_, used.unsafe_ + not_used.unsafe_) }; @@ -289,7 +243,7 @@ fn table_row_empty() -> String { mod table_tests { use super::*; - use crate::rs_file::RsFileMetricsWrapper; + use crate::rs_file::{PackageMetrics, RsFileMetricsWrapper}; use geiger::RsFileMetrics; use std::collections::HashMap; @@ -339,7 +293,6 @@ mod table_tests { ); let package_metrics = PackageMetrics { rs_path_to_metrics }; - let rs_files_used: HashSet = [ Path::new("package_1_path").to_path_buf(), Path::new("package_3_path").to_path_buf(), @@ -347,8 +300,9 @@ mod table_tests { .iter() .cloned() .collect(); + let unsafety = unsafe_stats(&package_metrics, &rs_files_used); - let table_row = table_row(&package_metrics, &rs_files_used); + let table_row = table_row(&unsafety.used, &unsafety.unused); assert_eq!(table_row, "4/6 8/12 12/18 16/24 20/30 "); } diff --git a/cargo-geiger/src/main.rs b/cargo-geiger/src/main.rs index 40b14b12..0f83fb1c 100644 --- a/cargo-geiger/src/main.rs +++ b/cargo-geiger/src/main.rs @@ -14,15 +14,16 @@ mod cli; mod find; mod format; mod graph; +mod report; mod rs_file; mod scan; mod traversal; use crate::cli::{get_cfgs, get_registry, get_workspace, resolve}; -use crate::format::print::{Prefix, PrintConfig}; +use crate::format::print::{OutputFormat, Prefix, PrintConfig}; use crate::format::{Charset, Pattern}; use crate::graph::{build_graph, ExtraDeps}; -use crate::scan::{run_scan_mode_default, run_scan_mode_forbid_only}; +use crate::scan::{scan_forbid_unsafe, scan_unsafe}; use cargo::core::shell::{ColorChoice, Shell, Verbosity}; use cargo::util::errors::CliError; @@ -60,6 +61,7 @@ OPTIONS: [default: utf8]. --format Format string used for printing dependencies [default: {p}]. + --json Output in JSON format. -v, --verbose Use verbose output (-vv very verbose/build.rs output). -q, --quiet No output printed to stdout other than the @@ -112,6 +114,7 @@ pub struct Args { pub unstable_flags: Vec, pub verbose: u32, pub version: bool, + pub output_format: Option, } fn parse_args() -> Result> { @@ -158,6 +161,11 @@ fn parse_args() -> Result> { (true, _) => 2, }, version: args.contains(["-V", "--version"]), + output_format: if args.contains("--json") { + Some(OutputFormat::Json) + } else { + None + } }; Ok(args) } @@ -304,18 +312,20 @@ fn real_main(args: &Args, config: &mut Config) -> CliResult { charset: args.charset, allow_partial_results, include_tests, + output_format: args.output_format, }; if args.forbid_only { - run_scan_mode_forbid_only( + scan_forbid_unsafe( &config, &packages, root_pack_id, &graph, &print_config, + &args, ) } else { - run_scan_mode_default( + scan_unsafe( &config, &ws, &packages, diff --git a/cargo-geiger/src/report.rs b/cargo-geiger/src/report.rs new file mode 100644 index 00000000..d9f39e4c --- /dev/null +++ b/cargo-geiger/src/report.rs @@ -0,0 +1,66 @@ +use cargo::core::{ + dependency::DepKind, + PackageId, +}; +use geiger::CounterBlock; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct SafetyReport { + pub packages: Vec, + pub packages_without_metrics: Vec, + pub used_but_not_scanned_files: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ReportEntry { + pub package: PackageInfo, + pub unsafety: UnsafeInfo, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PackageInfo { + pub id: PackageId, + pub dependencies: Vec, + pub dev_dependencies: Vec, + pub build_dependencies: Vec, +} + +impl PackageInfo { + pub fn new(id: PackageId) -> Self { + PackageInfo { + id, + dependencies: Vec::new(), + dev_dependencies: Vec::new(), + build_dependencies: Vec::new(), + } + } + + pub fn push_dependency(&mut self, dep: PackageId, kind: DepKind) { + match kind { + DepKind::Normal => self.dependencies.push(dep), + DepKind::Development => self.dev_dependencies.push(dep), + DepKind::Build => self.build_dependencies.push(dep), + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct UnsafeInfo { + pub used: CounterBlock, + pub unused: CounterBlock, + pub forbids_unsafe: bool, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct QuickSafetyReport { + pub packages: Vec, + pub packages_without_metrics: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct QuickReportEntry { + pub package: PackageInfo, + pub forbids_unsafe: bool, +} diff --git a/cargo-geiger/src/rs_file.rs b/cargo-geiger/src/rs_file.rs index b9df50b3..c3addfc3 100644 --- a/cargo-geiger/src/rs_file.rs +++ b/cargo-geiger/src/rs_file.rs @@ -1,9 +1,9 @@ use cargo::core::compiler::{CompileMode, Executor, Unit}; use cargo::core::manifest::TargetKind; -use cargo::core::{InternedString, PackageId, Target, Workspace}; +use cargo::core::{PackageId, Target, Workspace}; use cargo::ops; use cargo::ops::{CleanOptions, CompileOptions}; -use cargo::util::{paths, CargoResult, ProcessBuilder}; +use cargo::util::{interning::InternedString, paths, CargoResult, ProcessBuilder}; use cargo::Config; use geiger::RsFileMetrics; use std::collections::{HashMap, HashSet}; @@ -159,7 +159,7 @@ impl Executor for CustomExecutor { /// this package. fn exec( &self, - cmd: ProcessBuilder, + cmd: &ProcessBuilder, _id: PackageId, _target: &Target, _mode: CompileMode, diff --git a/cargo-geiger/src/scan.rs b/cargo-geiger/src/scan.rs index 52bd28e0..4f2ad0a1 100644 --- a/cargo-geiger/src/scan.rs +++ b/cargo-geiger/src/scan.rs @@ -1,12 +1,13 @@ -use crate::find::{find_unsafe_in_packages, GeigerContext}; -use crate::format::print::PrintConfig; +use crate::find::{find_unsafe_in_packages, unsafe_stats, GeigerContext}; +use crate::format::print::{OutputFormat, PrintConfig}; use crate::format::table::{ create_table_from_text_tree_lines, UNSAFE_COUNTERS_HEADER, }; use crate::format::tree::TextTreeLine; use crate::format::{get_kind_group_name, EmojiSymbols, Pattern, SymbolKind}; use crate::graph::Graph; -use crate::rs_file::resolve_rs_file_deps; +use crate::report::{PackageInfo, QuickReportEntry, QuickSafetyReport, ReportEntry, SafetyReport}; +use crate::rs_file::{resolve_rs_file_deps, PackageMetrics}; use crate::traversal::walk_dependency_tree; use crate::Args; @@ -19,6 +20,7 @@ use cargo::util::CargoResult; use cargo::Config; use cargo::{CliError, CliResult}; use colored::Colorize; +use petgraph::visit::EdgeRef; use std::collections::HashSet; use std::error::Error; use std::fmt; @@ -34,7 +36,47 @@ pub enum ScanMode { EntryPointsOnly, } -pub fn run_scan_mode_default( +struct ScanDetails { + rs_files_used: HashSet, + geiger_context: GeigerContext, +} + +fn find_unsafe( + mode: ScanMode, + config: &Config, + packages: &PackageSet, + print_config: &PrintConfig, +) -> Result { + let mut progress = cargo::util::Progress::new("Scanning", config); + let geiger_context = find_unsafe_in_packages( + packages, + print_config.allow_partial_results, + print_config.include_tests, + mode, + |i, count| -> CargoResult<()> { progress.tick(i, count) }, + ); + progress.clear(); + config.shell().status("Scanning", "done")?; + Ok(geiger_context) +} + +fn scan( + config: &Config, + workspace: &Workspace, + packages: &PackageSet, + print_config: &PrintConfig, + args: &Args, +) -> Result { + let compile_options = build_compile_options(args, config); + let rs_files_used = resolve_rs_file_deps(&compile_options, workspace).unwrap(); + let geiger_context = find_unsafe(ScanMode::Full, config, packages, print_config)?; + Ok(ScanDetails { + rs_files_used, + geiger_context, + }) +} + +pub fn scan_unsafe( config: &Config, workspace: &Workspace, packages: &PackageSet, @@ -43,28 +85,35 @@ pub fn run_scan_mode_default( print_config: &PrintConfig, args: &Args, ) -> CliResult { - let mut scan_output_lines = Vec::::new(); + match args.output_format { + Some(format) => { + scan_to_report(config, workspace, packages, root_pack_id, graph, print_config, args, + format) + } + None => { + scan_to_table(config, workspace, packages, root_pack_id, graph, print_config, args) + } + } +} - let compile_options = build_compile_options(args, config); - let rs_files_used = - resolve_rs_file_deps(&compile_options, &workspace).unwrap(); +fn scan_to_table( + config: &Config, + workspace: &Workspace, + packages: &PackageSet, + root_pack_id: PackageId, + graph: &Graph, + print_config: &PrintConfig, + args: &Args, +) -> CliResult { + let mut scan_output_lines = Vec::::new(); + let ScanDetails { rs_files_used, geiger_context } = + scan(config, workspace, packages, print_config, args)?; if print_config.verbosity == Verbosity::Verbose { let mut rs_files_used_lines = construct_rs_files_used_lines(&rs_files_used); scan_output_lines.append(&mut rs_files_used_lines); } - let mut progress = cargo::util::Progress::new("Scanning", config); let emoji_symbols = EmojiSymbols::new(print_config.charset); - let geiger_context = find_unsafe_in_packages( - &packages, - print_config.allow_partial_results, - print_config.include_tests, - ScanMode::Full, - |i, count| -> CargoResult<()> { progress.tick(i, count) }, - ); - progress.clear(); - config.shell().status("Scanning", "done")?; - let mut output_key_lines = construct_scan_mode_default_output_key_lines(&emoji_symbols); scan_output_lines.append(&mut output_key_lines); @@ -84,11 +133,14 @@ pub fn run_scan_mode_default( println!("{}", scan_output_line); } - list_files_used_but_not_scanned( - geiger_context, - &rs_files_used, - &mut warning_count, - ); + let used_but_not_scanned = list_files_used_but_not_scanned(&geiger_context, &rs_files_used); + warning_count += used_but_not_scanned.len() as u64; + for path in &used_but_not_scanned { + eprintln!( + "WARNING: Dependency file was never scanned: {}", + path.display() + ); + } if warning_count > 0 { Err(CliError::new( @@ -100,7 +152,62 @@ pub fn run_scan_mode_default( } } -pub fn run_scan_mode_forbid_only( +fn scan_to_report( + config: &Config, + workspace: &Workspace, + packages: &PackageSet, + root_pack_id: PackageId, + graph: &Graph, + print_config: &PrintConfig, + args: &Args, + output_format: OutputFormat, +) -> CliResult { + let ScanDetails { rs_files_used, geiger_context } = + scan(config, workspace, packages, print_config, args)?; + let mut report = SafetyReport::default(); + for (package, pack_metrics) in package_metrics(&geiger_context, graph, root_pack_id) { + let pack_metrics = match pack_metrics { + Some(m) => m, + None => { + report.packages_without_metrics.push(package.id); + continue; + } + }; + let unsafety = unsafe_stats(pack_metrics, &rs_files_used); + let entry = ReportEntry { + package, + unsafety, + }; + report.packages.push(entry); + } + report.used_but_not_scanned_files = + list_files_used_but_not_scanned(&geiger_context, &rs_files_used); + let s = match output_format { + OutputFormat::Json => serde_json::to_string(&report).unwrap(), + }; + println!("{}", s); + Ok(()) +} + +pub fn scan_forbid_unsafe( + config: &Config, + packages: &PackageSet, + root_pack_id: PackageId, + graph: &Graph, + print_config: &PrintConfig, + args: &Args, +) -> CliResult { + match args.output_format { + Some(format) => { + scan_forbid_to_report(config, packages, root_pack_id, graph, print_config, format) + } + None => { + scan_forbid_to_table(config, packages, root_pack_id, graph, print_config) + } + } +} + +fn scan_forbid_to_table( config: &Config, packages: &PackageSet, root_pack_id: PackageId, @@ -113,16 +220,7 @@ pub fn run_scan_mode_forbid_only( let sym_lock = emoji_symbols.emoji(SymbolKind::Lock); let sym_qmark = emoji_symbols.emoji(SymbolKind::QuestionMark); - let mut progress = cargo::util::Progress::new("Scanning", config); - let geiger_ctx = find_unsafe_in_packages( - &packages, - print_config.allow_partial_results, - print_config.include_tests, - ScanMode::EntryPointsOnly, - |i, count| -> CargoResult<()> { progress.tick(i, count) }, - ); - progress.clear(); - config.shell().status("Scanning", "done")?; + let geiger_ctx = find_unsafe(ScanMode::EntryPointsOnly, config, packages, print_config)?; let mut output_key_lines = construct_scan_mode_forbid_only_output_key_lines(&emoji_symbols); @@ -170,6 +268,41 @@ pub fn run_scan_mode_forbid_only( Ok(()) } +fn scan_forbid_to_report( + config: &Config, + packages: &PackageSet, + root_pack_id: PackageId, + graph: &Graph, + print_config: &PrintConfig, + output_format: OutputFormat, +) -> CliResult { + let geiger_context = find_unsafe(ScanMode::EntryPointsOnly, config, packages, print_config)?; + let mut report = QuickSafetyReport::default(); + for (package, pack_metrics) in package_metrics(&geiger_context, graph, root_pack_id) { + let pack_metrics = match pack_metrics { + Some(m) => m, + None => { + report.packages_without_metrics.push(package.id); + continue; + } + }; + let forbids_unsafe = pack_metrics + .rs_path_to_metrics + .iter() + .all(|(_, v)| v.metrics.forbids_unsafe); + let entry = QuickReportEntry { + package, + forbids_unsafe, + }; + report.packages.push(entry); + } + let s = match output_format { + OutputFormat::Json => serde_json::to_string(&report).unwrap(), + }; + println!("{}", s); + Ok(()) +} + #[derive(Debug)] struct FoundWarningsError { pub warning_count: u64, @@ -344,24 +477,45 @@ fn format_package_name(package: &Package, pattern: &Pattern) -> String { } fn list_files_used_but_not_scanned( - geiger_context: GeigerContext, + geiger_context: &GeigerContext, rs_files_used: &HashSet, - warning_count: &mut u64, -) { +) -> Vec { let scanned_files = geiger_context .pack_id_to_metrics .iter() - .flat_map(|(_k, v)| v.rs_path_to_metrics.keys()) + .flat_map(|(_, v)| v.rs_path_to_metrics.keys()) .collect::>(); - let used_but_not_scanned = - rs_files_used.iter().filter(|p| !scanned_files.contains(p)); - for path in used_but_not_scanned { - eprintln!( - "WARNING: Dependency file was never scanned: {}", - path.display() - ); - *warning_count += 1; - } + rs_files_used.iter().cloned().filter(|p| !scanned_files.contains(p)).collect() +} + +fn package_metrics<'a>( + geiger_context: &'a GeigerContext, + graph: &'a Graph, + root_id: PackageId, +) -> impl Iterator)> { + let root_index = graph.nodes[&root_id]; + let mut indices = vec![root_index]; + let mut visited = HashSet::new(); + std::iter::from_fn(move || { + let i = indices.pop()?; + let id = graph.graph[i].id; + let mut package = PackageInfo::new(id); + for edge in graph.graph.edges(i) { + let dep_index = edge.target(); + if visited.insert(dep_index) { + indices.push(dep_index); + } + let dep = graph.graph[dep_index].id; + package.push_dependency(dep, *edge.weight()); + } + match geiger_context.pack_id_to_metrics.get(&id) { + Some(m) => Some((package, Some(m))), + None => { + eprintln!("WARNING: No metrics found for package: {}", id); + Some((package, None)) + } + } + }) } #[cfg(test)] @@ -406,6 +560,7 @@ mod scan_tests { unstable_flags: vec![], verbose: 0, version: false, + output_format: None, }; let config = Config::default().unwrap(); diff --git a/cargo-geiger/src/traversal.rs b/cargo-geiger/src/traversal.rs index e045c0e4..f847675c 100644 --- a/cargo-geiger/src/traversal.rs +++ b/cargo-geiger/src/traversal.rs @@ -219,6 +219,7 @@ mod traversal_tests { charset: Charset::Ascii, allow_partial_results: false, include_tests: IncludeTests::Yes, + output_format: None, } } } diff --git a/geiger/Cargo.toml b/geiger/Cargo.toml index ea4f4d75..21425b58 100644 --- a/geiger/Cargo.toml +++ b/geiger/Cargo.toml @@ -14,5 +14,6 @@ license = "Apache-2.0/MIT" maintenance = { status = "experimental" } [dependencies] +serde = { version = "1.0.116", features = ["derive"] } syn = { version = "1.0.34", features = ["parsing", "printing", "clone-impls", "full", "extra-traits", "visit"] } proc-macro2 = "1.0.18" diff --git a/geiger/src/lib.rs b/geiger/src/lib.rs index 6a36906b..5cf6e7a4 100644 --- a/geiger/src/lib.rs +++ b/geiger/src/lib.rs @@ -7,15 +7,13 @@ #![forbid(unsafe_code)] #![forbid(warnings)] -extern crate proc_macro2; -extern crate syn; - +use serde::{Deserialize, Serialize}; use std::error::Error; use std::fmt; use std::fs::File; use std::io; use std::io::Read; -use std::ops::Add; +use std::ops::{Add, AddAssign}; use std::path::Path; use std::path::PathBuf; use std::string::FromUtf8Error; @@ -38,7 +36,7 @@ impl fmt::Display for ScanFileError { } } -#[derive(Debug, Default, Clone)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct Count { /// Number of safe items pub safe: u64, @@ -69,7 +67,7 @@ impl Add for Count { } /// Unsafe usage metrics collection. -#[derive(Debug, Default, Clone)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct CounterBlock { pub functions: Count, pub exprs: Count, @@ -102,6 +100,12 @@ impl Add for CounterBlock { } } +impl AddAssign for CounterBlock { + fn add_assign(&mut self, rhs: Self) { + *self = self.clone() + rhs; + } +} + /// Scan result for a single `.rs` file. #[derive(Debug, Default)] pub struct RsFileMetrics {