From 77c4f92177caaaa727c004e10d556cbdece0c3dc Mon Sep 17 00:00:00 2001 From: Stephane Raux Date: Wed, 23 Sep 2020 12:21:05 -0700 Subject: [PATCH 1/5] Add json output for detailed scan mode --- Cargo.lock | 28 +++--- cargo-geiger/Cargo.toml | 4 +- cargo-geiger/src/find.rs | 39 +++++++- cargo-geiger/src/format/print.rs | 6 ++ cargo-geiger/src/format/table.rs | 128 ++++++++----------------- cargo-geiger/src/main.rs | 15 ++- cargo-geiger/src/report.rs | 42 +++++++++ cargo-geiger/src/rs_file.rs | 6 +- cargo-geiger/src/scan.rs | 155 ++++++++++++++++++++++++------- cargo-geiger/src/traversal.rs | 1 + geiger/Cargo.toml | 1 + geiger/src/lib.rs | 16 ++-- 12 files changed, 293 insertions(+), 148 deletions(-) create mode 100644 cargo-geiger/src/report.rs 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..194f9135 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,37 @@ 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, + } +} 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..05c719eb 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_unsafe, run_scan_mode_forbid_only}; 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,6 +312,7 @@ 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 { @@ -315,7 +324,7 @@ fn real_main(args: &Args, config: &mut Config) -> CliResult { &print_config, ) } 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..6f88e2ec --- /dev/null +++ b/cargo-geiger/src/report.rs @@ -0,0 +1,42 @@ +use cargo::core::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 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(), + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct UnsafeInfo { + pub used: CounterBlock, + pub unused: CounterBlock, + 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..d25772c3 100644 --- a/cargo-geiger/src/scan.rs +++ b/cargo-geiger/src/scan.rs @@ -1,16 +1,18 @@ -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::report::{ReportEntry, SafetyReport}; use crate::rs_file::resolve_rs_file_deps; use crate::traversal::walk_dependency_tree; use crate::Args; use cargo::core::compiler::CompileMode; +use cargo::core::dependency::DepKind; use cargo::core::package::PackageSet; use cargo::core::shell::Verbosity; use cargo::core::{Package, PackageId, Workspace}; @@ -19,6 +21,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,29 +37,25 @@ pub enum ScanMode { EntryPointsOnly, } -pub fn run_scan_mode_default( +struct ScanDetails { + rs_files_used: HashSet, + geiger_context: GeigerContext, +} + + +fn scan( 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(); - +) -> Result { let compile_options = build_compile_options(args, config); let rs_files_used = - resolve_rs_file_deps(&compile_options, &workspace).unwrap(); - 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); - } + resolve_rs_file_deps(&compile_options, workspace).unwrap(); 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, + packages, print_config.allow_partial_results, print_config.include_tests, ScanMode::Full, @@ -64,7 +63,50 @@ pub fn run_scan_mode_default( ); progress.clear(); config.shell().status("Scanning", "done")?; + Ok(ScanDetails { + rs_files_used, + geiger_context, + }) +} +pub fn scan_unsafe( + config: &Config, + workspace: &Workspace, + packages: &PackageSet, + root_pack_id: PackageId, + graph: &Graph, + print_config: &PrintConfig, + args: &Args, +) -> CliResult { + 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) + } + } +} + +pub 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 emoji_symbols = EmojiSymbols::new(print_config.charset); let mut output_key_lines = construct_scan_mode_default_output_key_lines(&emoji_symbols); scan_output_lines.append(&mut output_key_lines); @@ -84,11 +126,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,6 +145,58 @@ pub fn run_scan_mode_default( } } +pub 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(); + let mut visited = HashSet::new(); + let mut ids = vec![root_pack_id]; + while let Some(id) = ids.pop() { + let index = *graph.nodes.get(&id).expect("Package ID should be in the dependency graph"); + let mut package = crate::report::PackageInfo::new(id); + for edge in graph.graph.edges(index) { + let dep = graph.graph[edge.target()].id; + if visited.insert(dep) { + ids.push(dep); + } + match edge.weight() { + DepKind::Normal => package.dependencies.push(dep), + DepKind::Development => package.dev_dependencies.push(dep), + DepKind::Build => package.build_dependencies.push(dep), + } + } + let pack_metrics = match geiger_context.pack_id_to_metrics.get(&id) { + Some(m) => m, + None => { + eprintln!("WARNING: No metrics found for 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 run_scan_mode_forbid_only( config: &Config, packages: &PackageSet, @@ -344,24 +441,15 @@ 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()) .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() } #[cfg(test)] @@ -406,6 +494,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..fcc3eb1a 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, 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, 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 { From 8e35019ded9db6a31c3c55ffa2d8f6613389d548 Mon Sep 17 00:00:00 2001 From: Stephane Raux Date: Wed, 23 Sep 2020 21:14:24 -0700 Subject: [PATCH 2/5] Add json output when scanning for forbid_unsafe --- cargo-geiger/src/main.rs | 5 +- cargo-geiger/src/report.rs | 24 +++++++- cargo-geiger/src/scan.rs | 116 +++++++++++++++++++++++++++---------- 3 files changed, 113 insertions(+), 32 deletions(-) diff --git a/cargo-geiger/src/main.rs b/cargo-geiger/src/main.rs index 05c719eb..0f83fb1c 100644 --- a/cargo-geiger/src/main.rs +++ b/cargo-geiger/src/main.rs @@ -23,7 +23,7 @@ use crate::cli::{get_cfgs, get_registry, get_workspace, resolve}; use crate::format::print::{OutputFormat, Prefix, PrintConfig}; use crate::format::{Charset, Pattern}; use crate::graph::{build_graph, ExtraDeps}; -use crate::scan::{scan_unsafe, 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; @@ -316,12 +316,13 @@ fn real_main(args: &Args, config: &mut Config) -> CliResult { }; if args.forbid_only { - run_scan_mode_forbid_only( + scan_forbid_unsafe( &config, &packages, root_pack_id, &graph, &print_config, + &args, ) } else { scan_unsafe( diff --git a/cargo-geiger/src/report.rs b/cargo-geiger/src/report.rs index 6f88e2ec..ba89b764 100644 --- a/cargo-geiger/src/report.rs +++ b/cargo-geiger/src/report.rs @@ -1,4 +1,7 @@ -use cargo::core::PackageId; +use cargo::core::{ + dependency::DepKind, + PackageId, +}; use geiger::CounterBlock; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -32,6 +35,14 @@ impl PackageInfo { 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, Serialize)] @@ -40,3 +51,14 @@ pub struct UnsafeInfo { pub unused: CounterBlock, pub forbids_unsafe: bool, } + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct QuickSafetyReport { + pub packages: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct QuickReportEntry { + pub package: PackageInfo, + pub forbids_unsafe: bool, +} diff --git a/cargo-geiger/src/scan.rs b/cargo-geiger/src/scan.rs index d25772c3..9400b91a 100644 --- a/cargo-geiger/src/scan.rs +++ b/cargo-geiger/src/scan.rs @@ -6,13 +6,12 @@ use crate::format::table::{ use crate::format::tree::TextTreeLine; use crate::format::{get_kind_group_name, EmojiSymbols, Pattern, SymbolKind}; use crate::graph::Graph; -use crate::report::{ReportEntry, SafetyReport}; +use crate::report::{QuickReportEntry, QuickSafetyReport, ReportEntry, SafetyReport}; use crate::rs_file::resolve_rs_file_deps; use crate::traversal::walk_dependency_tree; use crate::Args; use cargo::core::compiler::CompileMode; -use cargo::core::dependency::DepKind; use cargo::core::package::PackageSet; use cargo::core::shell::Verbosity; use cargo::core::{Package, PackageId, Workspace}; @@ -42,27 +41,35 @@ struct ScanDetails { geiger_context: GeigerContext, } - -fn scan( +fn find_unsafe( + mode: ScanMode, 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(); +) -> 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, - ScanMode::Full, + 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, @@ -89,7 +96,7 @@ pub fn scan_unsafe( } } -pub fn scan_to_table( +fn scan_to_table( config: &Config, workspace: &Workspace, packages: &PackageSet, @@ -145,7 +152,7 @@ pub fn scan_to_table( } } -pub fn scan_to_report( +fn scan_to_report( config: &Config, workspace: &Workspace, packages: &PackageSet, @@ -168,11 +175,7 @@ pub fn scan_to_report( if visited.insert(dep) { ids.push(dep); } - match edge.weight() { - DepKind::Normal => package.dependencies.push(dep), - DepKind::Development => package.dev_dependencies.push(dep), - DepKind::Build => package.build_dependencies.push(dep), - } + package.push_dependency(dep, *edge.weight()); } let pack_metrics = match geiger_context.pack_id_to_metrics.get(&id) { Some(m) => m, @@ -197,7 +200,25 @@ pub fn scan_to_report( Ok(()) } -pub fn run_scan_mode_forbid_only( +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, @@ -210,16 +231,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); @@ -267,6 +279,52 @@ 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_ctx = find_unsafe(ScanMode::EntryPointsOnly, config, packages, print_config)?; + let mut report = QuickSafetyReport::default(); + let mut visited = HashSet::new(); + let mut ids = vec![root_pack_id]; + while let Some(id) = ids.pop() { + let index = *graph.nodes.get(&id).expect("Package ID should be in the dependency graph"); + let mut package = crate::report::PackageInfo::new(id); + for edge in graph.graph.edges(index) { + let dep = graph.graph[edge.target()].id; + if visited.insert(dep) { + ids.push(dep); + } + package.push_dependency(dep, *edge.weight()); + } + let pack_metrics = match geiger_ctx.pack_id_to_metrics.get(&id) { + Some(m) => m, + None => { + eprintln!("WARNING: No metrics found for 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, From 66d0a6d64699fc72f5eeeddc8cef1723db149d00 Mon Sep 17 00:00:00 2001 From: Stephane Raux Date: Wed, 23 Sep 2020 23:56:40 -0700 Subject: [PATCH 3/5] Share code iterating over packages and their metrics --- cargo-geiger/src/scan.rs | 73 ++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 41 deletions(-) diff --git a/cargo-geiger/src/scan.rs b/cargo-geiger/src/scan.rs index 9400b91a..215a05bf 100644 --- a/cargo-geiger/src/scan.rs +++ b/cargo-geiger/src/scan.rs @@ -6,8 +6,8 @@ use crate::format::table::{ use crate::format::tree::TextTreeLine; use crate::format::{get_kind_group_name, EmojiSymbols, Pattern, SymbolKind}; use crate::graph::Graph; -use crate::report::{QuickReportEntry, QuickSafetyReport, ReportEntry, SafetyReport}; -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; @@ -165,25 +165,7 @@ fn scan_to_report( let ScanDetails { rs_files_used, geiger_context } = scan(config, workspace, packages, print_config, args)?; let mut report = SafetyReport::default(); - let mut visited = HashSet::new(); - let mut ids = vec![root_pack_id]; - while let Some(id) = ids.pop() { - let index = *graph.nodes.get(&id).expect("Package ID should be in the dependency graph"); - let mut package = crate::report::PackageInfo::new(id); - for edge in graph.graph.edges(index) { - let dep = graph.graph[edge.target()].id; - if visited.insert(dep) { - ids.push(dep); - } - package.push_dependency(dep, *edge.weight()); - } - let pack_metrics = match geiger_context.pack_id_to_metrics.get(&id) { - Some(m) => m, - None => { - eprintln!("WARNING: No metrics found for package: {}", id); - continue; - } - }; + for (package, pack_metrics) in package_metrics(&geiger_context, graph, root_pack_id) { let unsafety = unsafe_stats(pack_metrics, &rs_files_used); let entry = ReportEntry { package, @@ -287,27 +269,9 @@ fn scan_forbid_to_report( print_config: &PrintConfig, output_format: OutputFormat, ) -> CliResult { - let geiger_ctx = find_unsafe(ScanMode::EntryPointsOnly, config, packages, print_config)?; + let geiger_context = find_unsafe(ScanMode::EntryPointsOnly, config, packages, print_config)?; let mut report = QuickSafetyReport::default(); - let mut visited = HashSet::new(); - let mut ids = vec![root_pack_id]; - while let Some(id) = ids.pop() { - let index = *graph.nodes.get(&id).expect("Package ID should be in the dependency graph"); - let mut package = crate::report::PackageInfo::new(id); - for edge in graph.graph.edges(index) { - let dep = graph.graph[edge.target()].id; - if visited.insert(dep) { - ids.push(dep); - } - package.push_dependency(dep, *edge.weight()); - } - let pack_metrics = match geiger_ctx.pack_id_to_metrics.get(&id) { - Some(m) => m, - None => { - eprintln!("WARNING: No metrics found for package: {}", id); - continue; - } - }; + for (package, pack_metrics) in package_metrics(&geiger_context, graph, root_pack_id) { let forbids_unsafe = pack_metrics .rs_path_to_metrics .iter() @@ -510,6 +474,33 @@ fn list_files_used_but_not_scanned( 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 || loop { + 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) => break Some((package, m)), + None => eprintln!("WARNING: No metrics found for package: {}", id), + } + }) +} + #[cfg(test)] mod scan_tests { use super::*; From 251e27d9506a94d9e9e006aabf3c9e8e2fc33623 Mon Sep 17 00:00:00 2001 From: Stephane Raux Date: Thu, 24 Sep 2020 10:52:33 -0700 Subject: [PATCH 4/5] Report packages without metrics If a package is made of a single `lib.rs` file and it fails to parse, it won't have any metrics. --- cargo-geiger/src/report.rs | 2 ++ cargo-geiger/src/scan.rs | 25 +++++++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/cargo-geiger/src/report.rs b/cargo-geiger/src/report.rs index ba89b764..e5509089 100644 --- a/cargo-geiger/src/report.rs +++ b/cargo-geiger/src/report.rs @@ -9,6 +9,7 @@ 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, } @@ -55,6 +56,7 @@ pub struct UnsafeInfo { #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct QuickSafetyReport { pub packages: Vec, + pub packages_without_metrics: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/cargo-geiger/src/scan.rs b/cargo-geiger/src/scan.rs index 215a05bf..e1d54b4c 100644 --- a/cargo-geiger/src/scan.rs +++ b/cargo-geiger/src/scan.rs @@ -166,6 +166,13 @@ fn scan_to_report( 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, @@ -272,6 +279,13 @@ fn scan_forbid_to_report( 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() @@ -478,11 +492,11 @@ fn package_metrics<'a>( geiger_context: &'a GeigerContext, graph: &'a Graph, root_id: PackageId, -) -> impl Iterator { +) -> 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 || loop { + std::iter::from_fn(move || { let i = indices.pop()?; let id = graph.graph[i].id; let mut package = PackageInfo::new(id); @@ -495,8 +509,11 @@ fn package_metrics<'a>( package.push_dependency(dep, *edge.weight()); } match geiger_context.pack_id_to_metrics.get(&id) { - Some(m) => break Some((package, m)), - None => eprintln!("WARNING: No metrics found for package: {}", id), + Some(m) => Some((package, Some(m))), + None => { + eprintln!("WARNING: No metrics found for package: {}", id); + Some((package, None)) + } } }) } From 64a104a29268dec4ec5f55b35350964b931e461e Mon Sep 17 00:00:00 2001 From: Stephane Raux Date: Fri, 25 Sep 2020 10:03:10 -0700 Subject: [PATCH 5/5] Add some unit tests --- cargo-geiger/src/find.rs | 120 +++++++++++++++++++++++++++++++++++++ cargo-geiger/src/report.rs | 2 +- cargo-geiger/src/scan.rs | 2 +- geiger/src/lib.rs | 4 +- 4 files changed, 124 insertions(+), 4 deletions(-) diff --git a/cargo-geiger/src/find.rs b/cargo-geiger/src/find.rs index 194f9135..37c692d4 100644 --- a/cargo-geiger/src/find.rs +++ b/cargo-geiger/src/find.rs @@ -168,3 +168,123 @@ pub fn unsafe_stats( 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/report.rs b/cargo-geiger/src/report.rs index e5509089..d9f39e4c 100644 --- a/cargo-geiger/src/report.rs +++ b/cargo-geiger/src/report.rs @@ -46,7 +46,7 @@ impl PackageInfo { } } -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct UnsafeInfo { pub used: CounterBlock, pub unused: CounterBlock, diff --git a/cargo-geiger/src/scan.rs b/cargo-geiger/src/scan.rs index e1d54b4c..4f2ad0a1 100644 --- a/cargo-geiger/src/scan.rs +++ b/cargo-geiger/src/scan.rs @@ -483,7 +483,7 @@ fn list_files_used_but_not_scanned( 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::>(); rs_files_used.iter().cloned().filter(|p| !scanned_files.contains(p)).collect() } diff --git a/geiger/src/lib.rs b/geiger/src/lib.rs index fcc3eb1a..5cf6e7a4 100644 --- a/geiger/src/lib.rs +++ b/geiger/src/lib.rs @@ -36,7 +36,7 @@ impl fmt::Display for ScanFileError { } } -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct Count { /// Number of safe items pub safe: u64, @@ -67,7 +67,7 @@ impl Add for Count { } /// Unsafe usage metrics collection. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct CounterBlock { pub functions: Count, pub exprs: Count,