diff --git a/Cargo.lock b/Cargo.lock index 1062cafe..6ca5daed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,8 +201,11 @@ dependencies = [ "insta", "petgraph", "pico-args", + "rand", "regex", "rstest", + "strum", + "strum_macros", "walkdir", ] @@ -591,6 +594,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.15" @@ -1258,6 +1270,24 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strum" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3924a58d165da3b7b2922c667ab0673c7b5fd52b5c19ea3442747bcb3cd15abe" + +[[package]] +name = "strum_macros" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2ab682ecdcae7f5f45ae85cd7c1e6c8e68ea42c8a612d47fedf831c037146a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.34" @@ -1385,6 +1415,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" + [[package]] name = "unicode-width" version = "0.1.8" diff --git a/cargo-geiger/Cargo.toml b/cargo-geiger/Cargo.toml index b335e428..25fd2117 100644 --- a/cargo-geiger/Cargo.toml +++ b/cargo-geiger/Cargo.toml @@ -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" +strum = "0.19.2" +strum_macros = "0.19.2" walkdir = "2.3.1" anyhow = "1.0.31" @@ -32,5 +34,6 @@ vendored-openssl = ["cargo/vendored-openssl"] assert_cmd = "1.0.1" better-panic = "0.2.0" insta = "0.16.1" +rand = "0.7.3" regex = "1.3.9" rstest = "0.6.4" diff --git a/cargo-geiger/src/cli.rs b/cargo-geiger/src/cli.rs index 3ba3ce88..e2cd8c39 100644 --- a/cargo-geiger/src/cli.rs +++ b/cargo-geiger/src/cli.rs @@ -102,3 +102,78 @@ pub fn resolve<'a, 'cfg>( } // TODO: Make a wrapper type for canonical paths and hide all mutable access. + +#[cfg(test)] +mod cli_tests { + use super::*; + + #[test] + fn get_cfgs_test() { + let config = Config::default().unwrap(); + + let target: Option = None; + + let root = + important_paths::find_root_manifest_for_wd(config.cwd()).unwrap(); + let workspace = Workspace::new(&root, &config).unwrap(); + + let cfgs = get_cfgs(&config, &target, &workspace); + + assert!(cfgs.is_ok()); + let cfg_vec_option = cfgs.unwrap(); + assert!(cfg_vec_option.is_some()); + let cfg_vec = cfg_vec_option.unwrap(); + + let names: Vec<&Cfg> = cfg_vec + .iter() + .filter(|cfg| matches!(cfg, Cfg::Name(_))) + .collect(); + + let key_pairs: Vec<&Cfg> = cfg_vec + .iter() + .filter(|cfg| matches!(cfg, Cfg::KeyPair(_, _))) + .collect(); + + assert!(names.len() > 0); + assert!(key_pairs.len() > 0); + } + + #[test] + fn get_workspace_test() { + let config = Config::default().unwrap(); + let manifest_path: Option = None; + + let workspace_cargo_result = get_workspace(&config, manifest_path); + assert!(workspace_cargo_result.is_ok()); + let workspace = workspace_cargo_result.unwrap(); + + let package_result = workspace.current(); + assert!(package_result.is_ok()); + let package = package_result.unwrap(); + + assert_eq!(package.package_id().name(), "cargo-geiger"); + } + + #[test] + fn get_registry_test() { + let config = Config::default().unwrap(); + let workspace = Workspace::new( + &important_paths::find_root_manifest_for_wd(config.cwd()).unwrap(), + &config, + ) + .unwrap(); + let package = workspace.current().unwrap(); + + let registry_result = get_registry(&config, &package); + + assert!(registry_result.is_ok()); + let registry = registry_result.unwrap(); + + let package_ids = vec![package.package_id()]; + let package_set_result = registry.get(&package_ids); + assert!(package_set_result.is_ok()); + let package_set = package_set_result.unwrap(); + + assert_eq!(package_set.sources().len(), 1); + } +} diff --git a/cargo-geiger/src/format.rs b/cargo-geiger/src/format.rs index 8e1ed398..327f542d 100644 --- a/cargo-geiger/src/format.rs +++ b/cargo-geiger/src/format.rs @@ -10,10 +10,11 @@ use colored::Colorize; use std::error::Error; use std::fmt; use std::str::{self, FromStr}; +use strum_macros::EnumIter; use self::parse::{Parser, RawChunk}; -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum Charset { Utf8, Ascii, @@ -31,6 +32,7 @@ impl FromStr for Charset { } } +#[derive(Debug, Clone, EnumIter, PartialEq)] pub enum CrateDetectionStatus { NoneDetectedForbidsUnsafe, NoneDetectedAllowsUnsafe, @@ -106,6 +108,18 @@ impl EmojiSymbols { pub struct Pattern(Vec); impl Pattern { + pub fn display<'a>( + &'a self, + package: &'a PackageId, + metadata: &'a ManifestMetadata, + ) -> Display<'a> { + Display { + pattern: self, + package, + metadata, + } + } + pub fn try_build(format: &str) -> Result> { let mut chunks = vec![]; @@ -125,18 +139,6 @@ impl Pattern { Ok(Pattern(chunks)) } - - pub fn display<'a>( - &'a self, - package: &'a PackageId, - metadata: &'a ManifestMetadata, - ) -> Display<'a> { - Display { - pattern: self, - package, - metadata, - } - } } #[derive(Clone, Copy)] @@ -146,8 +148,8 @@ pub enum SymbolKind { Rads = 2, } -pub fn get_kind_group_name(k: DepKind) -> Option<&'static str> { - match k { +pub fn get_kind_group_name(dep_kind: DepKind) -> Option<&'static str> { + match dep_kind { DepKind::Normal => None, DepKind::Build => Some("[build-dependencies]"), DepKind::Development => Some("[dev-dependencies]"), @@ -160,3 +162,32 @@ enum Chunk { License, Repository, } + +#[cfg(test)] +mod format_tests { + use super::*; + + #[test] + fn charset_from_str_test() { + assert_eq!(Charset::from_str("utf8"), Ok(Charset::Utf8)); + + assert_eq!(Charset::from_str("ascii"), Ok(Charset::Ascii)); + + assert_eq!(Charset::from_str("invalid_str"), Err("invalid charset")); + } + + #[test] + fn get_kind_group_name_test() { + assert_eq!(get_kind_group_name(DepKind::Normal), None); + + assert_eq!( + get_kind_group_name(DepKind::Build), + Some("[build-dependencies]") + ); + + assert_eq!( + get_kind_group_name(DepKind::Development), + Some("[dev-dependencies]") + ); + } +} diff --git a/cargo-geiger/src/format/print.rs b/cargo-geiger/src/format/print.rs index 20b88632..9b924820 100644 --- a/cargo-geiger/src/format/print.rs +++ b/cargo-geiger/src/format/print.rs @@ -7,9 +7,9 @@ use petgraph::EdgeDirection; #[derive(Clone, Copy)] pub enum Prefix { - None, - Indent, Depth, + Indent, + None, } pub struct PrintConfig<'a> { @@ -30,12 +30,43 @@ pub struct PrintConfig<'a> { } pub fn colorize( - s: String, - detection_status: &CrateDetectionStatus, + string: String, + crate_detection_status: &CrateDetectionStatus, ) -> colored::ColoredString { - match detection_status { - CrateDetectionStatus::NoneDetectedForbidsUnsafe => s.green(), - CrateDetectionStatus::NoneDetectedAllowsUnsafe => s.normal(), - CrateDetectionStatus::UnsafeDetected => s.red().bold(), + match crate_detection_status { + CrateDetectionStatus::NoneDetectedForbidsUnsafe => string.green(), + CrateDetectionStatus::NoneDetectedAllowsUnsafe => string.normal(), + CrateDetectionStatus::UnsafeDetected => string.red().bold(), + } +} + +#[cfg(test)] +mod print_tests { + use super::*; + + #[test] + fn colorize_test() { + let string = String::from("string_value"); + + assert_eq!( + string.clone().green(), + colorize( + string.clone(), + &CrateDetectionStatus::NoneDetectedForbidsUnsafe + ) + ); + + assert_eq!( + string.clone().normal(), + colorize( + string.clone(), + &CrateDetectionStatus::NoneDetectedAllowsUnsafe + ) + ); + + assert_eq!( + string.clone().red().bold(), + colorize(string.clone(), &CrateDetectionStatus::UnsafeDetected) + ); } } diff --git a/cargo-geiger/src/format/table.rs b/cargo-geiger/src/format/table.rs index c6e8ec68..a8210c41 100644 --- a/cargo-geiger/src/format/table.rs +++ b/cargo-geiger/src/format/table.rs @@ -1,7 +1,9 @@ use crate::find::GeigerContext; use crate::format::print::{colorize, PrintConfig}; use crate::format::tree::TextTreeLine; -use crate::format::{CrateDetectionStatus, get_kind_group_name, EmojiSymbols, SymbolKind}; +use crate::format::{ + get_kind_group_name, CrateDetectionStatus, EmojiSymbols, SymbolKind, +}; use crate::rs_file::PackageMetrics; use cargo::core::package::PackageSet; @@ -21,23 +23,22 @@ pub const UNSAFE_COUNTERS_HEADER: [&str; 6] = [ "Dependency", ]; -pub fn print_text_tree_lines_as_table( +pub fn create_table_from_text_tree_lines( geiger_context: &GeigerContext, package_set: &PackageSet, print_config: &PrintConfig, rs_files_used: &HashSet, text_tree_lines: Vec, -) -> u64 { - let mut total_packs_none_detected_forbids_unsafe = 0; - let mut total_packs_none_detected_allows_unsafe = 0; - let mut total_packs_unsafe_detected = 0; +) -> (Vec, u64) { + let mut table_lines = Vec::::new(); + + let mut total_package_counts = TotalPackageCounts::new(); + let mut package_status = HashMap::new(); - let mut total = CounterBlock::default(); - let mut total_unused = CounterBlock::default(); let mut warning_count = 0; - for tl in text_tree_lines { - match tl { + for text_tree_line in text_tree_lines { + match text_tree_line { TextTreeLine::Package { id, tree_vines } => { let pack = package_set.get_one(id).unwrap_or_else(|_| { // TODO: Avoid panic, return Result. @@ -74,36 +75,43 @@ pub fn print_text_tree_lines_as_table( .filter(|(_, v)| v.is_crate_entry_point) .all(|(_, v)| v.metrics.forbids_unsafe); - for (k, v) in &pack_metrics.rs_path_to_metrics { - //println!("{}", k.display()); - let target = if rs_files_used.contains(k) { - &mut total + 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_unused + &mut total_package_counts.total_unused_counter_block }; - *target = target.clone() + v.metrics.counters.clone(); + *target = target.clone() + + rs_file_metrics_wrapper.metrics.counters.clone(); } + match (unsafe_found, crate_forbids_unsafe) { (false, true) => { - total_packs_none_detected_forbids_unsafe += 1; + total_package_counts + .none_detected_forbids_unsafe += 1; CrateDetectionStatus::NoneDetectedForbidsUnsafe } (false, false) => { - total_packs_none_detected_allows_unsafe += 1; + total_package_counts.none_detected_allows_unsafe += + 1; CrateDetectionStatus::NoneDetectedAllowsUnsafe } (true, _) => { - total_packs_unsafe_detected += 1; + total_package_counts.unsafe_detected += 1; CrateDetectionStatus::UnsafeDetected } } }); + let emoji_symbols = EmojiSymbols::new(print_config.charset); - let detection_status = + + let crate_detection_status = package_status.get(&id).unwrap_or_else(|| { panic!("Expected to find package by id: {}", &id) }); - let icon = match detection_status { + let icon = match crate_detection_status { CrateDetectionStatus::NoneDetectedForbidsUnsafe => { emoji_symbols.emoji(SymbolKind::Lock) } @@ -114,21 +122,27 @@ pub fn print_text_tree_lines_as_table( emoji_symbols.emoji(SymbolKind::Rads) } }; - let pack_name = colorize( + + let package_name = colorize( format!( "{}", print_config .format .display(&id, pack.manifest().metadata()) ), - &detection_status, + &crate_detection_status, ); let unsafe_info = colorize( table_row(&pack_metrics, &rs_files_used), - &detection_status, + &crate_detection_status, ); + let shift_chars = unsafe_info.chars().count() + 4; - print!("{} {: <2}", unsafe_info, icon); + + let mut line = String::new(); + line.push_str( + format!("{} {: <2}", unsafe_info, icon).as_str(), + ); // Here comes some special control characters to position the cursor // properly for printing the last column containing the tree vines, after @@ -138,11 +152,12 @@ pub fn print_text_tree_lines_as_table( // Rust. This could be unrelated to Rust and a quirk of this particular // symbol or something in the Terminal app on macOS. if emoji_symbols.will_output_emoji() { - print!("\r"); // Return the cursor to the start of the line. - print!("\x1B[{}C", shift_chars); // Move the cursor to the right so that it points to the icon character. + line.push_str("\r"); // Return the cursor to the start of the line. + line.push_str(format!("\x1B[{}C", shift_chars).as_str()); // Move the cursor to the right so that it points to the icon character. } - println!(" {}{}", tree_vines, pack_name); + table_lines + .push(format!("{} {}{}", line, tree_vines, package_name)) } TextTreeLine::ExtraDepsGroup { kind, tree_vines } => { let name = get_kind_group_name(kind); @@ -152,27 +167,66 @@ pub fn print_text_tree_lines_as_table( let name = name.unwrap(); // TODO: Fix the alignment on macOS (others too?) - println!("{}{}{}", table_row_empty(), tree_vines, name); + table_lines.push(format!( + "{}{}{}", + table_row_empty(), + tree_vines, + name + )) } } } - println!(); - let total_detection_status = match ( - total_packs_none_detected_forbids_unsafe > 0, - total_packs_none_detected_allows_unsafe > 0, - total_packs_unsafe_detected > 0, - ) { - (_, _, true) => CrateDetectionStatus::UnsafeDetected, - (true, false, false) => CrateDetectionStatus::NoneDetectedForbidsUnsafe, - _ => CrateDetectionStatus::NoneDetectedAllowsUnsafe, - }; - println!( + table_lines.push(String::new()); + let total_detection_status = + total_package_counts.get_total_detection_status(); + + table_lines.push(format!( "{}", - table_footer(total, total_unused, total_detection_status) - ); + table_footer( + total_package_counts.total_counter_block, + total_package_counts.total_unused_counter_block, + total_detection_status + ) + )); + + table_lines.push(String::new()); + + (table_lines, warning_count) +} - warning_count +struct TotalPackageCounts { + none_detected_forbids_unsafe: i32, + none_detected_allows_unsafe: i32, + unsafe_detected: i32, + total_counter_block: CounterBlock, + total_unused_counter_block: CounterBlock, +} + +impl TotalPackageCounts { + fn new() -> TotalPackageCounts { + TotalPackageCounts { + none_detected_forbids_unsafe: 0, + none_detected_allows_unsafe: 0, + unsafe_detected: 0, + total_counter_block: CounterBlock::default(), + total_unused_counter_block: CounterBlock::default(), + } + } + + fn get_total_detection_status(&self) -> CrateDetectionStatus { + match ( + self.none_detected_forbids_unsafe > 0, + self.none_detected_allows_unsafe > 0, + self.unsafe_detected > 0, + ) { + (_, _, true) => CrateDetectionStatus::UnsafeDetected, + (true, false, false) => { + CrateDetectionStatus::NoneDetectedForbidsUnsafe + } + _ => CrateDetectionStatus::NoneDetectedAllowsUnsafe, + } + } } fn table_footer( @@ -197,13 +251,14 @@ fn table_footer( fn table_row(pms: &PackageMetrics, rs_files_used: &HashSet) -> String { let mut used = CounterBlock::default(); let mut not_used = CounterBlock::default(); - for (k, v) in pms.rs_path_to_metrics.iter() { - let target = if rs_files_used.contains(k) { + 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() + v.metrics.counters.clone(); + *target = + target.clone() + rs_file_metrics_wrapper.metrics.counters.clone(); } let fmt = |used: &Count, not_used: &Count| { format!("{}/{}", used.unsafe_, used.unsafe_ + not_used.unsafe_) @@ -229,3 +284,161 @@ fn table_row_empty() -> String { + 1, ) } + +#[cfg(test)] +mod table_tests { + use super::*; + + use crate::rs_file::RsFileMetricsWrapper; + + use geiger::RsFileMetrics; + use std::collections::HashMap; + use std::path::Path; + use strum::IntoEnumIterator; + + #[test] + fn table_footer_test() { + let used_counter_block = create_counter_block(); + let not_used_counter_block = create_counter_block(); + + let expected_line = + String::from("2/4 4/8 6/12 8/16 10/20 "); + + for crate_detection_status in CrateDetectionStatus::iter() { + let table_footer = table_footer( + used_counter_block.clone(), + not_used_counter_block.clone(), + crate_detection_status.clone(), + ); + + assert_eq!( + table_footer, + colorize(expected_line.clone(), &crate_detection_status) + ); + } + } + + #[test] + fn table_row_test() { + let mut rs_path_to_metrics = + HashMap::::new(); + + rs_path_to_metrics.insert( + Path::new("package_1_path").to_path_buf(), + create_rs_file_metrics_wrapper(true, true), + ); + + rs_path_to_metrics.insert( + Path::new("package_2_path").to_path_buf(), + create_rs_file_metrics_wrapper(true, false), + ); + + rs_path_to_metrics.insert( + Path::new("package_3_path").to_path_buf(), + create_rs_file_metrics_wrapper(false, false), + ); + + 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(), + ] + .iter() + .cloned() + .collect(); + + let table_row = table_row(&package_metrics, &rs_files_used); + assert_eq!(table_row, "4/6 8/12 12/18 16/24 20/30 "); + } + + #[test] + fn table_row_empty_test() { + let empty_table_row = table_row_empty(); + assert_eq!(empty_table_row.len(), 50); + } + + #[test] + fn total_package_counts_get_total_detection_status_tests() { + let total_package_counts_unsafe_detected = TotalPackageCounts { + none_detected_forbids_unsafe: 0, + none_detected_allows_unsafe: 0, + unsafe_detected: 1, + total_counter_block: CounterBlock::default(), + total_unused_counter_block: CounterBlock::default(), + }; + + assert_eq!( + total_package_counts_unsafe_detected.get_total_detection_status(), + CrateDetectionStatus::UnsafeDetected + ); + + let total_package_counts_none_detected_forbids_unsafe = + TotalPackageCounts { + none_detected_forbids_unsafe: 1, + none_detected_allows_unsafe: 0, + unsafe_detected: 0, + total_counter_block: CounterBlock::default(), + total_unused_counter_block: CounterBlock::default(), + }; + + assert_eq!( + total_package_counts_none_detected_forbids_unsafe + .get_total_detection_status(), + CrateDetectionStatus::NoneDetectedForbidsUnsafe + ); + + let total_package_counts_none_detected_allows_unsafe = + TotalPackageCounts { + none_detected_forbids_unsafe: 4, + none_detected_allows_unsafe: 1, + unsafe_detected: 0, + total_counter_block: CounterBlock::default(), + total_unused_counter_block: CounterBlock::default(), + }; + + assert_eq!( + total_package_counts_none_detected_allows_unsafe + .get_total_detection_status(), + CrateDetectionStatus::NoneDetectedAllowsUnsafe + ); + } + + fn create_rs_file_metrics_wrapper( + forbids_unsafe: bool, + is_crate_entry_point: bool, + ) -> RsFileMetricsWrapper { + RsFileMetricsWrapper { + metrics: RsFileMetrics { + counters: create_counter_block(), + forbids_unsafe, + }, + is_crate_entry_point, + } + } + + fn create_counter_block() -> CounterBlock { + CounterBlock { + functions: Count { + safe: 1, + unsafe_: 2, + }, + exprs: Count { + safe: 3, + unsafe_: 4, + }, + item_impls: Count { + safe: 5, + unsafe_: 6, + }, + item_traits: Count { + safe: 7, + unsafe_: 8, + }, + methods: Count { + safe: 9, + unsafe_: 10, + }, + } + } +} diff --git a/cargo-geiger/src/format/tree.rs b/cargo-geiger/src/format/tree.rs index 2fcc7a16..3b3c4c5b 100644 --- a/cargo-geiger/src/format/tree.rs +++ b/cargo-geiger/src/format/tree.rs @@ -8,11 +8,12 @@ use cargo::core::PackageId; pub enum TextTreeLine { /// A text line for a package Package { id: PackageId, tree_vines: String }, - /// There're extra dependencies comming and we should print a group header, + /// There are extra dependencies coming and we should print a group header, /// eg. "[build-dependencies]". ExtraDepsGroup { kind: DepKind, tree_vines: String }, } +#[derive(Debug, PartialEq)] pub struct TreeSymbols { pub down: &'static str, pub tee: &'static str, @@ -20,8 +21,8 @@ pub struct TreeSymbols { pub right: &'static str, } -pub fn get_tree_symbols(cs: Charset) -> TreeSymbols { - match cs { +pub fn get_tree_symbols(charset: Charset) -> TreeSymbols { + match charset { Charset::Utf8 => UTF8_TREE_SYMBOLS, Charset::Ascii => ASCII_TREE_SYMBOLS, } @@ -40,3 +41,13 @@ const UTF8_TREE_SYMBOLS: TreeSymbols = TreeSymbols { ell: "└", right: "─", }; + +#[cfg(test)] +mod tree_tests { + use super::*; + + #[test] + fn get_tree_symbols_test() { + assert_eq!(get_tree_symbols(Charset::Utf8), UTF8_TREE_SYMBOLS); + } +} diff --git a/cargo-geiger/src/graph.rs b/cargo-geiger/src/graph.rs index d3a1c92e..06bf106e 100644 --- a/cargo-geiger/src/graph.rs +++ b/cargo-geiger/src/graph.rs @@ -1,6 +1,6 @@ use cargo::core::dependency::DepKind; use cargo::core::package::PackageSet; -use cargo::core::{PackageId, Resolve}; +use cargo::core::{Dependency, PackageId, Resolve}; use cargo::util::CargoResult; use cargo_platform::Cfg; use petgraph::graph::NodeIndex; @@ -69,10 +69,10 @@ pub fn build_graph<'a>( extra_deps, }; - while let Some(pkg_id) = pending.pop() { + while let Some(package_id) = pending.pop() { add_package_dependencies_to_graph( resolve, - pkg_id, + package_id, packages, &graph_configuration, &mut graph, @@ -89,6 +89,29 @@ struct GraphConfiguration<'a> { extra_deps: ExtraDeps, } +fn add_graph_node_if_not_present_and_edge( + dependency: &Dependency, + dependency_package_id: PackageId, + graph: &mut Graph, + index: NodeIndex, + pending_packages: &mut Vec, +) { + let dependency_index = match graph.nodes.entry(dependency_package_id) { + Entry::Occupied(e) => *e.get(), + Entry::Vacant(e) => { + pending_packages.push(dependency_package_id); + let node = Node { + id: dependency_package_id, + //pack: packages.get_one(dep_id)?, + }; + *e.insert(graph.graph.add_node(node)) + } + }; + graph + .graph + .add_edge(index, dependency_index, dependency.kind()); +} + #[doc(hidden)] fn add_package_dependencies_to_graph<'a>( resolve: &'a Resolve, @@ -98,14 +121,15 @@ fn add_package_dependencies_to_graph<'a>( graph: &mut Graph, pending_packages: &mut Vec, ) -> CargoResult<()> { - let idx = graph.nodes[&package_id]; + let index = graph.nodes[&package_id]; let package = packages.get_one(package_id)?; - for raw_dep_id in resolve.deps_not_replaced(package_id) { - let it = package + for (raw_dependency_package_id, _) in resolve.deps_not_replaced(package_id) + { + let dependency_iterator = package .dependencies() .iter() - .filter(|d| d.matches_ignoring_source(raw_dep_id.0)) + .filter(|d| d.matches_ignoring_source(raw_dependency_package_id)) .filter(|d| graph_configuration.extra_deps.allows(d.kind())) .filter(|d| { d.platform() @@ -119,25 +143,45 @@ fn add_package_dependencies_to_graph<'a>( }) .unwrap_or(true) }); - let dep_id = match resolve.replacement(raw_dep_id.0) { - Some(id) => id, - None => raw_dep_id.0, - }; - for dep in it { - let dep_idx = match graph.nodes.entry(dep_id) { - Entry::Occupied(e) => *e.get(), - Entry::Vacant(e) => { - pending_packages.push(dep_id); - let node = Node { - id: dep_id, - //pack: packages.get_one(dep_id)?, - }; - *e.insert(graph.graph.add_node(node)) - } + + let dependency_package_id = + match resolve.replacement(raw_dependency_package_id) { + Some(id) => id, + None => raw_dependency_package_id, }; - graph.graph.add_edge(idx, dep_idx, dep.kind()); + + for dependency in dependency_iterator { + add_graph_node_if_not_present_and_edge( + dependency, + dependency_package_id, + graph, + index, + pending_packages, + ); } } Ok(()) } + +#[cfg(test)] +mod graph_tests { + use super::*; + + #[test] + fn extra_deps_allows_test() { + assert_eq!(ExtraDeps::All.allows(DepKind::Normal), true); + assert_eq!(ExtraDeps::Build.allows(DepKind::Normal), true); + assert_eq!(ExtraDeps::Dev.allows(DepKind::Normal), true); + assert_eq!(ExtraDeps::NoMore.allows(DepKind::Normal), true); + + assert_eq!(ExtraDeps::All.allows(DepKind::Build), true); + assert_eq!(ExtraDeps::All.allows(DepKind::Development), true); + + assert_eq!(ExtraDeps::Build.allows(DepKind::Build), true); + assert_eq!(ExtraDeps::Build.allows(DepKind::Development), false); + + assert_eq!(ExtraDeps::Dev.allows(DepKind::Build), false); + assert_eq!(ExtraDeps::Dev.allows(DepKind::Development), true); + } +} diff --git a/cargo-geiger/src/main.rs b/cargo-geiger/src/main.rs index 1dddd5a6..40b14b12 100644 --- a/cargo-geiger/src/main.rs +++ b/cargo-geiger/src/main.rs @@ -7,6 +7,8 @@ extern crate cargo; extern crate colored; extern crate petgraph; +extern crate strum; +extern crate strum_macros; mod cli; mod find; @@ -16,21 +18,15 @@ mod rs_file; mod scan; mod traversal; -use crate::cli::get_cfgs; -use crate::cli::get_registry; -use crate::cli::get_workspace; -use crate::cli::resolve; -use crate::format::print::Prefix; -use crate::format::print::PrintConfig; +use crate::cli::{get_cfgs, get_registry, get_workspace, resolve}; +use crate::format::print::{Prefix, PrintConfig}; use crate::format::{Charset, Pattern}; -use crate::graph::build_graph; -use crate::graph::ExtraDeps; +use crate::graph::{build_graph, ExtraDeps}; use crate::scan::{run_scan_mode_default, run_scan_mode_forbid_only}; -use cargo::core::shell::{Shell, Verbosity}; +use cargo::core::shell::{ColorChoice, Shell, Verbosity}; use cargo::util::errors::CliError; -use cargo::CliResult; -use cargo::Config; +use cargo::{CliResult, Config}; use geiger::IncludeTests; use petgraph::EdgeDirection; use std::fmt; @@ -182,8 +178,6 @@ impl fmt::Display for FormatError { } fn real_main(args: &Args, config: &mut Config) -> CliResult { - use cargo::core::shell::ColorChoice; - if args.version { println!("cargo-geiger {}", VERSION.unwrap_or("unknown version")); return Ok(()); @@ -301,7 +295,7 @@ fn real_main(args: &Args, config: &mut Config) -> CliResult { } else { IncludeTests::No }; - let pc = PrintConfig { + let print_config = PrintConfig { all: args.all, verbosity, direction, @@ -313,7 +307,13 @@ fn real_main(args: &Args, config: &mut Config) -> CliResult { }; if args.forbid_only { - run_scan_mode_forbid_only(&config, &packages, root_pack_id, &graph, &pc) + run_scan_mode_forbid_only( + &config, + &packages, + root_pack_id, + &graph, + &print_config, + ) } else { run_scan_mode_default( &config, @@ -321,7 +321,7 @@ fn real_main(args: &Args, config: &mut Config) -> CliResult { &packages, root_pack_id, &graph, - &pc, + &print_config, &args, ) } diff --git a/cargo-geiger/src/rs_file.rs b/cargo-geiger/src/rs_file.rs index 463d46f8..b9df50b3 100644 --- a/cargo-geiger/src/rs_file.rs +++ b/cargo-geiger/src/rs_file.rs @@ -4,6 +4,7 @@ use cargo::core::{InternedString, PackageId, Target, Workspace}; use cargo::ops; use cargo::ops::{CleanOptions, CompileOptions}; use cargo::util::{paths, CargoResult, ProcessBuilder}; +use cargo::Config; use geiger::RsFileMetrics; use std::collections::{HashMap, HashSet}; use std::error::Error; @@ -17,6 +18,7 @@ use walkdir::{DirEntry, WalkDir}; /// Provides information needed to scan for crate root /// `#![forbid(unsafe_code)]`. /// The wrapped PathBufs are canonicalized. +#[derive(Debug, PartialEq)] pub enum RsFile { /// Library entry point source file, usually src/lib.rs LibRoot(PathBuf), @@ -79,14 +81,14 @@ pub fn is_file_with_ext(entry: &DirEntry, file_ext: &str) -> bool { /// Trigger a `cargo clean` + `cargo check` and listen to the cargo/rustc /// communication to figure out which source files were used by the build. pub fn resolve_rs_file_deps( - copt: &CompileOptions, - ws: &Workspace, + compile_options: &CompileOptions, + workspace: &Workspace, ) -> Result, RsResolveError> { - let config = ws.config(); + let config = workspace.config(); // Need to run a cargo clean to identify all new .d deps files. // TODO: Figure out how this can be avoided to improve performance, clean // Rust builds are __slow__. - let clean_opt = CleanOptions { + let clean_options = CleanOptions { config: &config, spec: vec![], targets: vec![], @@ -96,60 +98,45 @@ pub fn resolve_rs_file_deps( requested_profile: InternedString::new("dev"), doc: false, }; - ops::clean(ws, &clean_opt) + + ops::clean(workspace, &clean_options) .map_err(|e| RsResolveError::Cargo(e.to_string()))?; + let inner_arc = Arc::new(Mutex::new(CustomExecutorInnerContext::default())); { - let cust_exec = CustomExecutor { - cwd: config.cwd().to_path_buf(), - inner_ctx: inner_arc.clone(), - }; - let exec: Arc = Arc::new(cust_exec); - ops::compile_with_exec(ws, &copt, &exec) - .map_err(|e| RsResolveError::Cargo(e.to_string()))?; + compile_with_exec( + compile_options, + config, + inner_arc.clone(), + workspace, + )?; } - let ws_root = ws.root().to_path_buf(); + + let workspace_root = workspace.root().to_path_buf(); let inner_mutex = Arc::try_unwrap(inner_arc).map_err(|_| RsResolveError::ArcUnwrap())?; let (rs_files, out_dir_args) = { let ctx = inner_mutex.into_inner()?; (ctx.rs_file_args, ctx.out_dir_args) }; - let mut hs = HashSet::::new(); + let mut path_buf_hash_set = HashSet::::new(); for out_dir in out_dir_args { // TODO: Figure out if the `.d` dep files are used by one or more rustc // calls. It could be useful to know which `.d` dep files belong to // which rustc call. That would allow associating each `.rs` file found // in each dep file with a PackageId. - for ent in WalkDir::new(&out_dir) { - let ent = ent.map_err(RsResolveError::Walkdir)?; - if !is_file_with_ext(&ent, "d") { - continue; - } - let deps = parse_rustc_dep_info(ent.path()).map_err(|e| { - RsResolveError::DepParse( - e.to_string(), - ent.path().to_path_buf(), - ) - })?; - let canon_paths = deps - .into_iter() - .flat_map(|t| t.1) - .map(PathBuf::from) - .map(|pb| ws_root.join(pb)) - .map(|pb| { - pb.canonicalize().map_err(|e| RsResolveError::Io(e, pb)) - }); - for p in canon_paths { - hs.insert(p?); - } - } + add_dir_entries_to_path_buf_hash_set( + out_dir, + &mut path_buf_hash_set, + workspace_root.clone(), + )?; } - for pb in rs_files { + for path_buf in rs_files { // rs_files must already be canonicalized - hs.insert(pb); + path_buf_hash_set.insert(path_buf); } - Ok(hs) + + Ok(path_buf_hash_set) } /// A cargo Executor to intercept all build tasks and store all ".rs" file @@ -293,6 +280,52 @@ impl From> for RsResolveError { } } +fn add_dir_entries_to_path_buf_hash_set( + out_dir: PathBuf, + path_buf_hash_set: &mut HashSet, + workspace_root: PathBuf, +) -> Result<(), RsResolveError> { + for entry in WalkDir::new(&out_dir) { + let entry = entry.map_err(RsResolveError::Walkdir)?; + if !is_file_with_ext(&entry, "d") { + continue; + } + let dependencies = parse_rustc_dep_info(entry.path()).map_err(|e| { + RsResolveError::DepParse(e.to_string(), entry.path().to_path_buf()) + })?; + let canonical_paths = dependencies + .into_iter() + .flat_map(|t| t.1) + .map(PathBuf::from) + .map(|pb| workspace_root.join(pb)) + .map(|pb| pb.canonicalize().map_err(|e| RsResolveError::Io(e, pb))); + for path_buf in canonical_paths { + path_buf_hash_set.insert(path_buf?); + } + } + + Ok(()) +} + +fn compile_with_exec( + compile_options: &CompileOptions, + config: &Config, + inner_arc: Arc>, + workspace: &Workspace, +) -> Result<(), RsResolveError> { + let custom_executor = CustomExecutor { + cwd: config.cwd().to_path_buf(), + inner_ctx: inner_arc, + }; + + let custom_executor_arc: Arc = Arc::new(custom_executor); + + ops::compile_with_exec(workspace, &compile_options, &custom_executor_arc) + .map_err(|e| RsResolveError::Cargo(e.to_string()))?; + + Ok(()) +} + /// Copy-pasted (almost) from the private module cargo::core::compiler::fingerprint. /// /// TODO: Make a PR to the cargo project to expose this function or to expose @@ -327,3 +360,75 @@ fn parse_rustc_dep_info( }) .collect() } + +#[cfg(test)] +mod rs_file_tests { + use super::*; + + #[test] + fn into_rs_code_file_test() { + let path_buf = Path::new("test_path.ext").to_path_buf(); + + assert_eq!( + into_rs_code_file(&TargetKind::Lib(vec![]), path_buf.clone()), + RsFile::LibRoot(path_buf.clone()) + ); + + assert_eq!( + into_rs_code_file(&TargetKind::Bin, path_buf.clone()), + RsFile::BinRoot(path_buf.clone()) + ); + + assert_eq!( + into_rs_code_file(&TargetKind::Test, path_buf.clone()), + RsFile::Other(path_buf.clone()) + ); + + assert_eq!( + into_rs_code_file(&TargetKind::Bench, path_buf.clone()), + RsFile::Other(path_buf.clone()) + ); + + assert_eq!( + into_rs_code_file( + &TargetKind::ExampleLib(vec![]), + path_buf.clone() + ), + RsFile::Other(path_buf.clone()) + ); + + assert_eq!( + into_rs_code_file(&TargetKind::ExampleBin, path_buf.clone()), + RsFile::Other(path_buf.clone()) + ); + + assert_eq!( + into_rs_code_file(&TargetKind::CustomBuild, path_buf.clone()), + RsFile::CustomBuildRoot(path_buf.clone()) + ); + } + + #[test] + fn is_file_with_ext_test() { + let config = Config::default().unwrap(); + let cwd = config.cwd(); + + let walk_dir_rust_files = WalkDir::new(&cwd) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().to_str().unwrap().ends_with(".rs")); + + for entry in walk_dir_rust_files { + assert_eq!(is_file_with_ext(&entry, "rs"), true); + } + + let walk_dir_readme_files = WalkDir::new(&cwd) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.path().to_str().unwrap().contains("README")); + + for entry in walk_dir_readme_files { + assert_eq!(is_file_with_ext(&entry, "rs"), false); + } + } +} diff --git a/cargo-geiger/src/scan.rs b/cargo-geiger/src/scan.rs index 16d25c62..52bd28e0 100644 --- a/cargo-geiger/src/scan.rs +++ b/cargo-geiger/src/scan.rs @@ -1,7 +1,7 @@ -use crate::find::find_unsafe_in_packages; +use crate::find::{find_unsafe_in_packages, GeigerContext}; use crate::format::print::PrintConfig; use crate::format::table::{ - print_text_tree_lines_as_table, UNSAFE_COUNTERS_HEADER, + create_table_from_text_tree_lines, UNSAFE_COUNTERS_HEADER, }; use crate::format::tree::TextTreeLine; use crate::format::{get_kind_group_name, EmojiSymbols, Pattern, SymbolKind}; @@ -36,109 +36,60 @@ pub enum ScanMode { pub fn run_scan_mode_default( config: &Config, - ws: &Workspace, + workspace: &Workspace, packages: &PackageSet, root_pack_id: PackageId, graph: &Graph, - pc: &PrintConfig, + print_config: &PrintConfig, args: &Args, ) -> CliResult { - let copt = build_compile_options(args, config); - let rs_files_used = resolve_rs_file_deps(&copt, &ws).unwrap(); - if pc.verbosity == Verbosity::Verbose { - // Print all .rs files found through the .d files, in sorted order. - let mut paths = rs_files_used - .iter() - .map(std::borrow::ToOwned::to_owned) - .collect::>(); - paths.sort(); - paths - .iter() - .for_each(|p| println!("Used by build (sorted): {}", p.display())); + let mut scan_output_lines = Vec::::new(); + + 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); } let mut progress = cargo::util::Progress::new("Scanning", config); - let emoji_symbols = EmojiSymbols::new(pc.charset); - let geiger_ctx = find_unsafe_in_packages( + let emoji_symbols = EmojiSymbols::new(print_config.charset); + let geiger_context = find_unsafe_in_packages( &packages, - pc.allow_partial_results, - pc.include_tests, + 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")?; - println!(); - println!("Metric output format: x/y"); - println!(" x = unsafe code used by the build"); - println!(" y = total unsafe code found in the crate"); - println!(); + let mut output_key_lines = + construct_scan_mode_default_output_key_lines(&emoji_symbols); + scan_output_lines.append(&mut output_key_lines); - println!("Symbols: "); - let forbids = "No `unsafe` usage found, declares #![forbid(unsafe_code)]"; - let unknown = "No `unsafe` usage found, missing #![forbid(unsafe_code)]"; - let guilty = "`unsafe` usage found"; - - let shift_sequence = if emoji_symbols.will_output_emoji() { - "\r\x1B[7C" // The radiation icon's Unicode width is 2, - // but by most terminals it seems to be rendered at width 1. - } else { - "" - }; - - println!( - " {: <2} = {}", - emoji_symbols.emoji(SymbolKind::Lock), - forbids - ); - println!( - " {: <2} = {}", - emoji_symbols.emoji(SymbolKind::QuestionMark), - unknown - ); - println!( - " {: <2}{} = {}", - emoji_symbols.emoji(SymbolKind::Rads), - shift_sequence, - guilty - ); - println!(); + let tree_lines = walk_dependency_tree(root_pack_id, &graph, &print_config); + let (mut table_lines, mut warning_count) = + create_table_from_text_tree_lines( + &geiger_context, + packages, + print_config, + &rs_files_used, + tree_lines, + ); + scan_output_lines.append(&mut table_lines); - println!( - "{}", - UNSAFE_COUNTERS_HEADER - .iter() - .map(|s| s.to_owned()) - .collect::>() - .join(" ") - .bold() - ); - println!(); + for scan_output_line in scan_output_lines { + println!("{}", scan_output_line); + } - let tree_lines = walk_dependency_tree(root_pack_id, &graph, &pc); - let mut warning_count = print_text_tree_lines_as_table( - &geiger_ctx, - packages, - pc, + list_files_used_but_not_scanned( + geiger_context, &rs_files_used, - tree_lines, + &mut warning_count, ); - println!(); - let scanned_files = geiger_ctx - .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; - } if warning_count > 0 { Err(CliError::new( anyhow::Error::new(FoundWarningsError { warning_count }), @@ -154,39 +105,36 @@ pub fn run_scan_mode_forbid_only( packages: &PackageSet, root_pack_id: PackageId, graph: &Graph, - pc: &PrintConfig, + print_config: &PrintConfig, ) -> CliResult { - let emoji_symbols = EmojiSymbols::new(pc.charset); + let mut scan_output_lines = Vec::::new(); + + let emoji_symbols = EmojiSymbols::new(print_config.charset); + 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, - pc.allow_partial_results, - pc.include_tests, + 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")?; - println!(); - - println!("Symbols: "); - let forbids = "All entry point .rs files declare #![forbid(unsafe_code)]."; - let unknown = "This crate may use unsafe code."; - - let sym_lock = emoji_symbols.emoji(SymbolKind::Lock); - let sym_qmark = emoji_symbols.emoji(SymbolKind::QuestionMark); + let mut output_key_lines = + construct_scan_mode_forbid_only_output_key_lines(&emoji_symbols); - println!(" {: <2} = {}", sym_lock, forbids); - println!(" {: <2} = {}", sym_qmark, unknown); - println!(); + scan_output_lines.append(&mut output_key_lines); - let tree_lines = walk_dependency_tree(root_pack_id, &graph, &pc); - for tl in tree_lines { - match tl { + let tree_lines = walk_dependency_tree(root_pack_id, &graph, &print_config); + for tree_line in tree_lines { + match tree_line { TextTreeLine::Package { id, tree_vines } => { - let pack = packages.get_one(id).unwrap(); // FIXME - let name = format_package_name(pack, pc.format); + let package = packages.get_one(id).unwrap(); // FIXME + let name = format_package_name(package, print_config.format); let pack_metrics = geiger_ctx.pack_id_to_metrics.get(&id); let package_forbids_unsafe = match pack_metrics { None => false, // no metrics available, .rs parsing failed? @@ -200,7 +148,8 @@ pub fn run_scan_mode_forbid_only( } else { (&sym_qmark, name.red()) }; - println!("{} {}{}", symbol, tree_vines, name); + scan_output_lines + .push(format!("{} {}{}", symbol, tree_vines, name)); } TextTreeLine::ExtraDepsGroup { kind, tree_vines } => { let name = get_kind_group_name(kind); @@ -209,11 +158,15 @@ pub fn run_scan_mode_forbid_only( } let name = name.unwrap(); // TODO: Fix the alignment on macOS (others too?) - println!(" {}{}", tree_vines, name); + scan_output_lines.push(format!(" {}{}", tree_vines, name)); } } } + for scan_output_line in scan_output_lines { + println!("{}", scan_output_line); + } + Ok(()) } @@ -246,12 +199,12 @@ fn build_compile_options<'a>( .split(' ') .map(str::to_owned) .collect::>(); - let mut opt = + let mut compile_options = CompileOptions::new(&config, CompileMode::Check { test: false }) .unwrap(); - opt.features = features; - opt.all_features = args.all_features; - opt.no_default_features = args.no_default_features; + compile_options.features = features; + compile_options.all_features = args.all_features; + compile_options.no_default_features = args.no_default_features; // TODO: Investigate if this is relevant to cargo-geiger. //let mut bins = Vec::new(); @@ -274,12 +227,252 @@ fn build_compile_options<'a>( // ); // } - opt + compile_options } -fn format_package_name(pack: &Package, pat: &Pattern) -> String { +fn construct_rs_files_used_lines( + rs_files_used: &HashSet, +) -> Vec { + // Print all .rs files found through the .d files, in sorted order. + let mut paths = rs_files_used + .iter() + .map(std::borrow::ToOwned::to_owned) + .collect::>(); + + paths.sort(); + + paths + .iter() + .map(|p| format!("Used by build (sorted): {}", p.display())) + .collect::>() +} + +fn construct_scan_mode_default_output_key_lines( + emoji_symbols: &EmojiSymbols, +) -> Vec { + let mut output_key_lines = Vec::::new(); + + output_key_lines.push(String::new()); + output_key_lines.push(String::from("Metric output format: x/y")); + output_key_lines + .push(String::from(" x = unsafe code used by the build")); + output_key_lines + .push(String::from(" y = total unsafe code found in the crate")); + output_key_lines.push(String::new()); + + output_key_lines.push(String::from("Symbols: ")); + let forbids = "No `unsafe` usage found, declares #![forbid(unsafe_code)]"; + let unknown = "No `unsafe` usage found, missing #![forbid(unsafe_code)]"; + let guilty = "`unsafe` usage found"; + + let shift_sequence = if emoji_symbols.will_output_emoji() { + "\r\x1B[7C" // The radiation icon's Unicode width is 2, + // but by most terminals it seems to be rendered at width 1. + } else { + "" + }; + + output_key_lines.push(format!( + " {: <2} = {}", + emoji_symbols.emoji(SymbolKind::Lock), + forbids + )); + + output_key_lines.push(format!( + " {: <2} = {}", + emoji_symbols.emoji(SymbolKind::QuestionMark), + unknown + )); + + output_key_lines.push(format!( + " {: <2}{} = {}", + emoji_symbols.emoji(SymbolKind::Rads), + shift_sequence, + guilty + )); + + output_key_lines.push(String::new()); + + output_key_lines.push(format!( + "{}", + UNSAFE_COUNTERS_HEADER + .iter() + .map(|s| s.to_owned()) + .collect::>() + .join(" ") + .bold() + )); + + output_key_lines.push(String::new()); + + output_key_lines +} + +fn construct_scan_mode_forbid_only_output_key_lines( + emoji_symbols: &EmojiSymbols, +) -> Vec { + let mut output_key_lines = Vec::::new(); + + output_key_lines.push(String::new()); + output_key_lines.push(String::from("Symbols: ")); + + let forbids = "All entry point .rs files declare #![forbid(unsafe_code)]."; + let unknown = "This crate may use unsafe code."; + + output_key_lines.push(format!( + " {: <2} = {}", + emoji_symbols.emoji(SymbolKind::Lock), + forbids + )); + + output_key_lines.push(format!( + " {: <2} = {}", + emoji_symbols.emoji(SymbolKind::QuestionMark), + unknown + )); + + output_key_lines.push(String::new()); + + output_key_lines +} + +fn format_package_name(package: &Package, pattern: &Pattern) -> String { format!( "{}", - pat.display(&pack.package_id(), pack.manifest().metadata()) + pattern.display(&package.package_id(), package.manifest().metadata()) ) } + +fn list_files_used_but_not_scanned( + geiger_context: GeigerContext, + rs_files_used: &HashSet, + warning_count: &mut u64, +) { + 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; + } +} + +#[cfg(test)] +mod scan_tests { + use super::*; + + use crate::format::Charset; + + use cargo::util::important_paths; + + #[test] + fn build_compile_options_test() { + let args_all_features = rand::random(); + let args_features = Some(String::from("unit test features")); + let args_no_default_features = rand::random(); + + let args = Args { + all: false, + all_deps: false, + all_features: args_all_features, + all_targets: false, + build_deps: false, + charset: Charset::Utf8, + color: None, + dev_deps: false, + features: args_features, + forbid_only: false, + format: "".to_string(), + frozen: false, + help: false, + include_tests: false, + invert: false, + locked: false, + manifest_path: None, + no_default_features: args_no_default_features, + no_indent: false, + offline: false, + package: None, + prefix_depth: false, + quiet: None, + target: None, + unstable_flags: vec![], + verbose: 0, + version: false, + }; + + let config = Config::default().unwrap(); + + let compile_options = build_compile_options(&args, &config); + + assert_eq!(compile_options.all_features, args_all_features); + assert_eq!(compile_options.features, vec!["unit", "test", "features"]); + assert_eq!( + compile_options.no_default_features, + args_no_default_features + ); + } + + #[test] + fn construct_rs_files_used_lines_test() { + let mut rs_files_used = HashSet::::new(); + + rs_files_used.insert(PathBuf::from("b/path.rs")); + rs_files_used.insert(PathBuf::from("a/path.rs")); + rs_files_used.insert(PathBuf::from("c/path.rs")); + + let rs_files_used_lines = construct_rs_files_used_lines(&rs_files_used); + + assert_eq!( + rs_files_used_lines, + vec![ + String::from("Used by build (sorted): a/path.rs"), + String::from("Used by build (sorted): b/path.rs"), + String::from("Used by build (sorted): c/path.rs"), + ] + ); + } + + #[test] + fn construct_scan_mode_default_output_key_lines_test() { + let emoji_symbols = EmojiSymbols::new(Charset::Utf8); + let output_key_lines = + construct_scan_mode_default_output_key_lines(&emoji_symbols); + + assert_eq!(output_key_lines.len(), 12); + } + + #[test] + fn construct_scan_mode_forbid_only_output_key_lines_test() { + let emoji_symbols = EmojiSymbols::new(Charset::Utf8); + let output_key_lines = + construct_scan_mode_forbid_only_output_key_lines(&emoji_symbols); + + assert_eq!(output_key_lines.len(), 5); + } + + #[test] + fn format_package_name_test() { + let pattern = Pattern::try_build("{p}").unwrap(); + + let config = Config::default().unwrap(); + let workspace = Workspace::new( + &important_paths::find_root_manifest_for_wd(config.cwd()).unwrap(), + &config, + ) + .unwrap(); + + let package = workspace.current().unwrap(); + + let formatted_package_name = format_package_name(&package, &pattern); + + assert_eq!(formatted_package_name, "cargo-geiger 0.10.2"); + } +} diff --git a/cargo-geiger/src/traversal.rs b/cargo-geiger/src/traversal.rs index f80f7c30..e045c0e4 100644 --- a/cargo-geiger/src/traversal.rs +++ b/cargo-geiger/src/traversal.rs @@ -31,13 +31,42 @@ pub fn walk_dependency_tree( ) } +fn construct_tree_vines_string( + levels_continue: &mut Vec, + print_config: &PrintConfig, +) -> String { + let tree_symbols = get_tree_symbols(print_config.charset); + + match print_config.prefix { + Prefix::Depth => format!("{} ", levels_continue.len()), + Prefix::Indent => { + let mut buf = String::new(); + if let Some((&last_continues, rest)) = levels_continue.split_last() + { + for &continues in rest { + let c = if continues { tree_symbols.down } else { " " }; + buf.push_str(&format!("{} ", c)); + } + let c = if last_continues { + tree_symbols.tee + } else { + tree_symbols.ell + }; + buf.push_str(&format!("{0}{1}{1} ", c, tree_symbols.right)); + } + buf + } + Prefix::None => "".into(), + } +} + fn walk_dependency_kind( kind: DepKind, deps: &mut Vec<&Node>, graph: &Graph, visited_deps: &mut HashSet, levels_continue: &mut Vec, - pc: &PrintConfig, + print_config: &PrintConfig, ) -> Vec { if deps.is_empty() { return Vec::new(); @@ -46,9 +75,9 @@ fn walk_dependency_kind( // Resolve uses Hash data types internally but we want consistent output ordering deps.sort_by_key(|n| n.id); - let tree_symbols = get_tree_symbols(pc.charset); + let tree_symbols = get_tree_symbols(print_config.charset); let mut output = Vec::new(); - if let Prefix::Indent = pc.prefix { + if let Prefix::Indent = print_config.prefix { match kind { DepKind::Normal => (), _ => { @@ -70,7 +99,7 @@ fn walk_dependency_kind( graph, visited_deps, levels_continue, - pc, + print_config, )); levels_continue.pop(); } @@ -82,31 +111,10 @@ fn walk_dependency_node( graph: &Graph, visited_deps: &mut HashSet, levels_continue: &mut Vec, - pc: &PrintConfig, + print_config: &PrintConfig, ) -> Vec { - let new = pc.all || visited_deps.insert(package.id); - let tree_symbols = get_tree_symbols(pc.charset); - let tree_vines = match pc.prefix { - Prefix::Depth => format!("{} ", levels_continue.len()), - Prefix::Indent => { - let mut buf = String::new(); - if let Some((&last_continues, rest)) = levels_continue.split_last() - { - for &continues in rest { - let c = if continues { tree_symbols.down } else { " " }; - buf.push_str(&format!("{} ", c)); - } - let c = if last_continues { - tree_symbols.tee - } else { - tree_symbols.ell - }; - buf.push_str(&format!("{0}{1}{1} ", c, tree_symbols.right)); - } - buf - } - Prefix::None => "".into(), - }; + let new = print_config.all || visited_deps.insert(package.id); + let tree_vines = construct_tree_vines_string(levels_continue, print_config); let mut all_out = vec![TextTreeLine::Package { id: package.id, @@ -122,9 +130,9 @@ fn walk_dependency_node( let mut development = vec![]; for edge in graph .graph - .edges_directed(graph.nodes[&package.id], pc.direction) + .edges_directed(graph.nodes[&package.id], print_config.direction) { - let dep = match pc.direction { + let dep = match print_config.direction { EdgeDirection::Incoming => &graph.graph[edge.source()], EdgeDirection::Outgoing => &graph.graph[edge.target()], }; @@ -140,7 +148,7 @@ fn walk_dependency_node( graph, visited_deps, levels_continue, - pc, + print_config, ); let mut build_out = walk_dependency_kind( DepKind::Build, @@ -148,7 +156,7 @@ fn walk_dependency_node( graph, visited_deps, levels_continue, - pc, + print_config, ); let mut dev_out = walk_dependency_kind( DepKind::Development, @@ -156,10 +164,61 @@ fn walk_dependency_node( graph, visited_deps, levels_continue, - pc, + print_config, ); all_out.append(&mut normal_out); all_out.append(&mut build_out); all_out.append(&mut dev_out); all_out } + +#[cfg(test)] +mod traversal_tests { + use super::*; + + use crate::format::{Charset, Pattern}; + + use cargo::core::shell::Verbosity; + use geiger::IncludeTests; + use petgraph::EdgeDirection; + + #[test] + fn construct_tree_vines_test() { + let mut levels_continue = vec![true, false, true]; + let pattern = Pattern::try_build("{p}").unwrap(); + + let print_config = construct_print_config(&pattern, Prefix::Depth); + let tree_vines_string = + construct_tree_vines_string(&mut levels_continue, &print_config); + + assert_eq!(tree_vines_string, "3 "); + + let print_config = construct_print_config(&pattern, Prefix::Indent); + let tree_vines_string = + construct_tree_vines_string(&mut levels_continue, &print_config); + + assert_eq!(tree_vines_string, "| |-- "); + + let print_config = construct_print_config(&pattern, Prefix::None); + let tree_vines_string = + construct_tree_vines_string(&mut levels_continue, &print_config); + + assert_eq!(tree_vines_string, ""); + } + + fn construct_print_config( + pattern: &Pattern, + prefix: Prefix, + ) -> PrintConfig { + PrintConfig { + all: false, + verbosity: Verbosity::Verbose, + direction: EdgeDirection::Outgoing, + prefix, + format: pattern, + charset: Charset::Ascii, + allow_partial_results: false, + include_tests: IncludeTests::Yes, + } + } +}