From e3be5d7a0294df6049ecab9d4e4b444edfc5b64a Mon Sep 17 00:00:00 2001 From: Josh McConnell Date: Tue, 1 Sep 2020 20:51:05 +0100 Subject: [PATCH 1/2] ISSUE-16 - Refactor cli module Split cli module into component parts. Create: * find module * format - table module * format - tree module * rs_file module * scan module * traversal module Signed-off-by: Josh McConnell --- .gitignore | 1 + cargo-geiger/src/cli.rs | 1224 ----------------- cargo-geiger/src/find.rs | 139 ++ cargo-geiger/src/{format/mod.rs => format.rs} | 147 +- cargo-geiger/src/format/parse.rs | 4 + cargo-geiger/src/format/print.rs | 46 + cargo-geiger/src/format/table.rs | 235 ++++ cargo-geiger/src/format/tree.rs | 46 + cargo-geiger/src/graph.rs | 116 +- cargo-geiger/src/main.rs | 23 +- cargo-geiger/src/rs_file.rs | 333 +++++ cargo-geiger/src/scan.rs | 289 ++++ cargo-geiger/src/traversal.rs | 169 +++ geiger/src/lib.rs | 6 + 14 files changed, 1466 insertions(+), 1312 deletions(-) create mode 100644 cargo-geiger/src/find.rs rename cargo-geiger/src/{format/mod.rs => format.rs} (57%) create mode 100644 cargo-geiger/src/format/print.rs create mode 100644 cargo-geiger/src/format/table.rs create mode 100644 cargo-geiger/src/format/tree.rs create mode 100644 cargo-geiger/src/rs_file.rs create mode 100644 cargo-geiger/src/scan.rs create mode 100644 cargo-geiger/src/traversal.rs diff --git a/.gitignore b/.gitignore index 390c0b29..36dbb72a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ /test_crates/**/target/ .DS_Store .vscode/ +.idea/ diff --git a/cargo-geiger/src/cli.rs b/cargo-geiger/src/cli.rs index 7293c79a..8f7eacee 100644 --- a/cargo-geiger/src/cli.rs +++ b/cargo-geiger/src/cli.rs @@ -11,82 +11,19 @@ // TODO: Consider making this a lib.rs (again) and expose a full API, excluding // only the terminal output..? That API would be dependent on cargo. -use cargo::CliError; -use crate::format::Pattern; -use crate::Args; -use crate::graph::{Graph, Node}; -use cargo::core::compiler::CompileMode; -use cargo::core::compiler::Executor; -use cargo::core::compiler::Unit; -use cargo::core::dependency::DepKind; -use cargo::core::manifest::TargetKind; use cargo::core::package::PackageSet; use cargo::core::registry::PackageRegistry; use cargo::core::resolver::ResolveOpts; -use cargo::core::shell::Verbosity; -use cargo::core::InternedString; -use cargo::core::Target; use cargo::core::{Package, PackageId, PackageIdSpec, Resolve, Workspace}; use cargo::ops; -use cargo::ops::CleanOptions; -use cargo::ops::CompileOptions; -use cargo::util::paths; -use cargo::util::ProcessBuilder; use cargo::util::{self, important_paths, CargoResult}; -use cargo::CliResult; use cargo::Config; use cargo_platform::Cfg; -use colored::Colorize; -use geiger::find_unsafe_in_file; -use geiger::Count; -use geiger::CounterBlock; -use geiger::IncludeTests; -use geiger::RsFileMetrics; -use petgraph::EdgeDirection; -use petgraph::visit::EdgeRef; -use std::collections::{HashMap, HashSet}; -use std::ffi::OsString; -use std::io; -use std::path::Path; use std::path::PathBuf; use std::str::{self, FromStr}; -use std::sync::Arc; -use std::sync::Mutex; -use walkdir::DirEntry; -use walkdir::WalkDir; // ---------- BEGIN: Public items ---------- -#[derive(Clone, Copy, PartialEq)] -pub enum Charset { - Utf8, - Ascii, -} - -#[derive(Clone, Copy)] -pub enum Prefix { - None, - Indent, - Depth, -} - -pub struct PrintConfig<'a> { - /// Don't truncate dependencies that have already been displayed. - pub all: bool, - - pub verbosity: Verbosity, - pub direction: EdgeDirection, - pub prefix: Prefix, - - // Is anyone using this? This is a carry-over from cargo-tree. - // TODO: Open a github issue to discuss deprecation. - pub format: &'a Pattern, - - pub charset: Charset, - pub allow_partial_results: bool, - pub include_tests: IncludeTests, -} - /// TODO: Write proper documentation for this. /// This function seems to be looking up the active flags for conditional /// compilation (cargo_platform::Cfg instances). @@ -113,7 +50,6 @@ pub fn get_cfgs( )) } - pub fn get_workspace( config: &Config, manifest_path: Option, @@ -167,1166 +103,6 @@ pub fn resolve<'a, 'cfg>( Ok((packages, resolve)) } -fn colorize( - s: String, - detection_status: &DetectionStatus, -) -> colored::ColoredString { - match detection_status { - DetectionStatus::NoneDetectedForbidsUnsafe => s.green(), - DetectionStatus::NoneDetectedAllowsUnsafe => s.normal(), - DetectionStatus::UnsafeDetected => s.red().bold(), - } -} - -pub fn run_scan_mode_default( - config: &Config, - ws: &Workspace, - packages: &PackageSet, - root_pack_id: PackageId, - graph: &Graph, - pc: &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 progress = cargo::util::Progress::new("Scanning", config); - let emoji_symbols = EmojiSymbols::new(pc.charset); - let geiger_ctx = find_unsafe_in_packages( - &packages, - pc.allow_partial_results, - pc.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!(); - - 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!(); - - println!( - "{}", - UNSAFE_COUNTERS_HEADER - .iter() - .map(|s| s.to_owned()) - .collect::>() - .join(" ") - .bold() - ); - println!(); - - 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; - let mut package_status = HashMap::new(); - let mut total = CounterBlock::default(); - let mut total_unused = CounterBlock::default(); - let tree_lines = walk_dependency_tree(root_pack_id, &graph, &pc); - let mut warning_count = 0; - for tl in tree_lines { - match tl { - TextTreeLine::Package { id, tree_vines } => { - let pack = packages.get_one(id).unwrap_or_else(|_| { - // TODO: Avoid panic, return Result. - panic!("Expected to find package by id: {}", id); - }); - let pack_metrics = match geiger_ctx - .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 (k, v) in &pack_metrics.rs_path_to_metrics { - //println!("{}", k.display()); - let target = if rs_files_used.contains(k) { - &mut total - } else { - &mut total_unused - }; - *target = target.clone() + v.metrics.counters.clone(); - } - match (unsafe_found, crate_forbids_unsafe) { - (false, true) => { - total_packs_none_detected_forbids_unsafe += 1; - DetectionStatus::NoneDetectedForbidsUnsafe - } - (false, false) => { - total_packs_none_detected_allows_unsafe += 1; - DetectionStatus::NoneDetectedAllowsUnsafe - } - (true, _) => { - total_packs_unsafe_detected += 1; - DetectionStatus::UnsafeDetected - } - } - }); - let emoji_symbols = EmojiSymbols::new(pc.charset); - let detection_status = - package_status.get(&id).unwrap_or_else(|| { - panic!("Expected to find package by id: {}", &id) - }); - let icon = match detection_status { - DetectionStatus::NoneDetectedForbidsUnsafe => { - emoji_symbols.emoji(SymbolKind::Lock) - } - DetectionStatus::NoneDetectedAllowsUnsafe => { - emoji_symbols.emoji(SymbolKind::QuestionMark) - } - DetectionStatus::UnsafeDetected => { - emoji_symbols.emoji(SymbolKind::Rads) - } - }; - let pack_name = colorize( - format!( - "{}", - pc.format.display(&id, pack.manifest().metadata()) - ), - &detection_status, - ); - let unsafe_info = colorize( - table_row(&pack_metrics, &rs_files_used), - &detection_status, - ); - let shift_chars = unsafe_info.chars().count() + 4; - print!("{} {: <2}", unsafe_info, icon); - - // Here comes some special control characters to position the cursor - // properly for printing the last column containing the tree vines, after - // the emoji icon. This is a workaround for a potential bug where the - // radiation emoji will visually cover two characters in width but only - // count as a single character if using the column formatting provided by - // 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. - } - - println!(" {}{}", tree_vines, pack_name); - } - TextTreeLine::ExtraDepsGroup { kind, tree_vines } => { - let name = get_kind_group_name(kind); - if name.is_none() { - continue; - } - let name = name.unwrap(); - - // TODO: Fix the alignment on macOS (others too?) - println!("{}{}{}", 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) => DetectionStatus::UnsafeDetected, - (true, false, false) => DetectionStatus::NoneDetectedForbidsUnsafe, - _ => DetectionStatus::NoneDetectedAllowsUnsafe, - }; - println!( - "{}", - table_footer(total, total_unused, total_detection_status) - ); - 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 }), 1)) - } else { - Ok(()) - } -} - -pub fn run_scan_mode_forbid_only( - config: &Config, - packages: &PackageSet, - root_pack_id: PackageId, - graph: &Graph, - pc: &PrintConfig, -) -> CliResult { - let emoji_symbols = EmojiSymbols::new(pc.charset); - let mut progress = cargo::util::Progress::new("Scanning", config); - let geiger_ctx = find_unsafe_in_packages( - &packages, - pc.allow_partial_results, - pc.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); - - println!(" {: <2} = {}", sym_lock, forbids); - println!(" {: <2} = {}", sym_qmark, unknown); - println!(); - - let tree_lines = walk_dependency_tree(root_pack_id, &graph, &pc); - for tl in tree_lines { - match tl { - TextTreeLine::Package { id, tree_vines } => { - let pack = packages.get_one(id).unwrap(); // FIXME - let name = format_package_name(pack, pc.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? - Some(pm) => pm - .rs_path_to_metrics - .iter() - .all(|(_k, v)| v.metrics.forbids_unsafe), - }; - let (symbol, name) = if package_forbids_unsafe { - (&sym_lock, name.green()) - } else { - (&sym_qmark, name.red()) - }; - println!("{} {}{}", symbol, tree_vines, name); - } - TextTreeLine::ExtraDepsGroup { kind, tree_vines } => { - let name = get_kind_group_name(kind); - if name.is_none() { - continue; - } - let name = name.unwrap(); - // TODO: Fix the alignment on macOS (others too?) - println!(" {}{}", tree_vines, name); - } - } - } - - Ok(()) -} - -fn format_package_name(pack: &Package, pat: &Pattern) -> String { - format!( - "{}", - pat.display(&pack.package_id(), pack.manifest().metadata()) - ) -} - -fn get_kind_group_name(k: DepKind) -> Option<&'static str> { - match k { - DepKind::Normal => None, - DepKind::Build => Some("[build-dependencies]"), - DepKind::Development => Some("[dev-dependencies]"), - } -} - // ---------- END: Public items ---------- -#[derive(Debug)] -enum RsResolveError { - Walkdir(walkdir::Error), - - /// Like io::Error but with the related path. - Io(io::Error, PathBuf), - - /// Would like cargo::Error here, but it's private, why? - /// This is still way better than a panic though. - Cargo(String), - - /// This should not happen unless incorrect assumptions have been made in - /// cargo-geiger about how the cargo API works. - ArcUnwrap(), - - /// Failed to get the inner context out of the mutex. - InnerContextMutex(String), - - /// Failed to parse a .dep file. - DepParse(String, PathBuf), -} - -impl Error for RsResolveError {} - -/// Forward Display to Debug. -impl fmt::Display for RsResolveError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(self, f) - } -} - -impl From> for RsResolveError { - fn from(e: PoisonError) -> Self { - RsResolveError::InnerContextMutex(e.to_string()) - } -} - -#[derive(Debug)] -struct FoundWarningsError { - pub warning_count: u64 -} - -impl Error for FoundWarningsError {} - -/// Forward Display to Debug. -impl fmt::Display for FoundWarningsError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(self, f) - } -} - -#[derive(Debug, Default)] -struct RsFileMetricsWrapper { - /// The information returned by the `geiger` crate for a `.rs` file. - pub metrics: RsFileMetrics, - - /// All crate entry points must declare forbid(unsafe_code) to make it count - /// for the crate as a whole. The `geiger` crate is decoupled from `cargo` - /// and cannot know if a file is a crate entry point or not, so we add this - /// information here. - pub is_crate_entry_point: bool, -} - -#[derive(Debug, Default)] -struct PackageMetrics { - /// The key is the canonicalized path to the rs source file. - pub rs_path_to_metrics: HashMap, -} - -/// Provides a more terse and searchable name for the wrapped generic -/// collection. -struct GeigerContext { - pack_id_to_metrics: HashMap, -} - -/// Based on code from cargo-bloat. It seems weird that CompileOptions can be -/// constructed without providing all standard cargo options, TODO: Open an issue -/// in cargo? -fn build_compile_options<'a>( - args: &'a Args, - config: &'a Config, -) -> CompileOptions { - let features = args - .features - .as_ref() - .cloned() - .unwrap_or_else(String::new) - .split(' ') - .map(str::to_owned) - .collect::>(); - let mut opt = - 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; - - // TODO: Investigate if this is relevant to cargo-geiger. - //let mut bins = Vec::new(); - //let mut examples = Vec::new(); - // opt.release = args.release; - // opt.target = args.target.clone(); - // if let Some(ref name) = args.bin { - // bins.push(name.clone()); - // } else if let Some(ref name) = args.example { - // examples.push(name.clone()); - // } - // if args.bin.is_some() || args.example.is_some() { - // opt.filter = ops::CompileFilter::new( - // false, - // bins.clone(), false, - // Vec::new(), false, - // examples.clone(), false, - // Vec::new(), false, - // false, - // ); - // } - - opt -} - -fn is_file_with_ext(entry: &DirEntry, file_ext: &str) -> bool { - if !entry.file_type().is_file() { - return false; - } - let p = entry.path(); - let ext = match p.extension() { - Some(e) => e, - None => return false, - }; - // to_string_lossy is ok since we only want to match against an ASCII - // compatible extension and we do not keep the possibly lossy result - // around. - ext.to_string_lossy() == file_ext -} - // TODO: Make a wrapper type for canonical paths and hide all mutable access. - -/// Provides information needed to scan for crate root -/// `#![forbid(unsafe_code)]`. -/// The wrapped PathBufs are canonicalized. -enum RsFile { - /// Library entry point source file, usually src/lib.rs - LibRoot(PathBuf), - - /// Executable entry point source file, usually src/main.rs - BinRoot(PathBuf), - - /// Not sure if this is relevant but let's be conservative for now. - CustomBuildRoot(PathBuf), - - /// All other .rs files. - Other(PathBuf), -} - -fn find_rs_files_in_package(pack: &Package) -> Vec { - // Find all build target entry point source files. - let mut canon_targets = HashMap::new(); - for t in pack.targets() { - let path = t.src_path().path(); - let path = match path { - None => continue, - Some(p) => p, - }; - if !path.exists() { - // A package published to crates.io is not required to include - // everything. We have to skip this build target. - continue; - } - let canon = path - .canonicalize() // will Err on non-existing paths. - .expect("canonicalize for build target path failed."); // FIXME - let targets = canon_targets.entry(canon).or_insert_with(Vec::new); - targets.push(t); - } - let mut out = Vec::new(); - for p in find_rs_files_in_dir(pack.root()) { - if !canon_targets.contains_key(&p) { - out.push(RsFile::Other(p)); - } - } - for (k, v) in canon_targets.into_iter() { - for target in v { - out.push(into_rs_code_file(target.kind(), k.clone())); - } - } - out -} - -fn into_rs_code_file(kind: &TargetKind, path: PathBuf) -> RsFile { - match kind { - TargetKind::Lib(_) => RsFile::LibRoot(path), - TargetKind::Bin => RsFile::BinRoot(path), - TargetKind::Test => RsFile::Other(path), - TargetKind::Bench => RsFile::Other(path), - TargetKind::ExampleLib(_) => RsFile::Other(path), - TargetKind::ExampleBin => RsFile::Other(path), - TargetKind::CustomBuild => RsFile::CustomBuildRoot(path), - } -} - -fn find_rs_files_in_packages<'a>( - packs: &'a [&Package], -) -> impl Iterator + 'a { - packs.iter().flat_map(|pack| { - find_rs_files_in_package(pack) - .into_iter() - .map(move |path| (pack.package_id(), path)) - }) -} - -enum ScanMode { - // The default scan mode, scan every .rs file. - Full, - - // An optimization to allow skipping everything except the entry points. - // This is only useful for the "--forbid-only" mode since that mode only - // depends on entry point .rs files. - EntryPointsOnly, -} - -fn find_unsafe_in_packages( - packs: &PackageSet, - allow_partial_results: bool, - include_tests: IncludeTests, - mode: ScanMode, - mut progress_step: F, -) -> GeigerContext -where - F: FnMut(usize, usize) -> CargoResult<()>, -{ - let mut pack_id_to_metrics = HashMap::new(); - let packs = packs.get_many(packs.package_ids()).unwrap(); - let pack_code_files: Vec<_> = find_rs_files_in_packages(&packs).collect(); - let pack_code_file_count = pack_code_files.len(); - for (i, (pack_id, rs_code_file)) in pack_code_files.into_iter().enumerate() - { - let (is_entry_point, p) = match rs_code_file { - RsFile::LibRoot(pb) => (true, pb), - RsFile::BinRoot(pb) => (true, pb), - RsFile::CustomBuildRoot(pb) => (true, pb), - RsFile::Other(pb) => (false, pb), - }; - if let (false, ScanMode::EntryPointsOnly) = (is_entry_point, &mode) { - continue; - } - match find_unsafe_in_file(&p, include_tests) { - Err(e) => { - if allow_partial_results { - eprintln!( - "Failed to parse file: {}, {:?} ", - &p.display(), - e - ); - } else { - panic!("Failed to parse file: {}, {:?} ", &p.display(), e); - } - } - Ok(file_metrics) => { - let package_metrics = pack_id_to_metrics - .entry(pack_id) - .or_insert_with(PackageMetrics::default); - let wrapper = package_metrics - .rs_path_to_metrics - .entry(p) - .or_insert_with(RsFileMetricsWrapper::default); - wrapper.metrics = file_metrics; - wrapper.is_crate_entry_point = is_entry_point; - } - } - let _ = progress_step(i, pack_code_file_count); - } - GeigerContext { pack_id_to_metrics } -} - -impl FromStr for Charset { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "utf8" => Ok(Charset::Utf8), - "ascii" => Ok(Charset::Ascii), - _ => Err("invalid charset"), - } - } -} - -struct TreeSymbols { - down: &'static str, - tee: &'static str, - ell: &'static str, - right: &'static str, -} - -const UTF8_TREE_SYMBOLS: TreeSymbols = TreeSymbols { - down: "│", - tee: "├", - ell: "└", - right: "─", -}; - -const ASCII_TREE_SYMBOLS: TreeSymbols = TreeSymbols { - down: "|", - tee: "|", - ell: "`", - right: "-", -}; - -#[derive(Clone, Copy)] -enum SymbolKind { - Lock = 0, - QuestionMark = 1, - Rads = 2, -} - -struct EmojiSymbols { - charset: Charset, - emojis: [&'static str; 3], - fallbacks: [colored::ColoredString; 3], -} - -impl EmojiSymbols { - pub fn new(charset: Charset) -> EmojiSymbols { - Self { - charset, - emojis: ["🔒", "❓", "☢️"], - fallbacks: [":)".green(), "?".normal(), "!".red().bold()], - } - } - - pub fn will_output_emoji(&self) -> bool { - self.charset == Charset::Utf8 - && console::Term::stdout().features().wants_emoji() - } - - pub fn emoji(&self, kind: SymbolKind) -> Box { - let idx = kind as usize; - if self.will_output_emoji() { - Box::new(self.emojis[idx]) - } else { - Box::new(self.fallbacks[idx].clone()) - } - } -} - -/// Trigger a `cargo clean` + `cargo check` and listen to the cargo/rustc -/// communication to figure out which source files were used by the build. -fn resolve_rs_file_deps( - copt: &CompileOptions, - ws: &Workspace, -) -> Result, RsResolveError> { - let config = ws.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 { - config: &config, - spec: vec![], - targets: vec![], - profile_specified: false, - // A temporary hack to get cargo 0.43 to build, TODO: look closer at the updated cargo API - // later. - requested_profile: InternedString::new("dev"), - doc: false, - }; - ops::clean(ws, &clean_opt) - .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()))?; - } - let ws_root = ws.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(); - 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?); - } - } - } - for pb in rs_files { - // rs_files must already be canonicalized - hs.insert(pb); - } - Ok(hs) -} - -/// 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 -/// the dependency data in some other way. -fn parse_rustc_dep_info( - rustc_dep_info: &Path, -) -> CargoResult)>> { - let contents = paths::read(rustc_dep_info)?; - contents - .lines() - .filter_map(|l| l.find(": ").map(|i| (l, i))) - .map(|(line, pos)| { - let target = &line[..pos]; - let mut deps = line[pos + 2..].split_whitespace(); - let mut ret = Vec::new(); - while let Some(s) = deps.next() { - let mut file = s.to_string(); - while file.ends_with('\\') { - file.pop(); - file.push(' '); - //file.push_str(deps.next().ok_or_else(|| { - //internal("malformed dep-info format, trailing \\".to_string()) - //})?); - file.push_str( - deps.next() - .expect("malformed dep-info format, trailing \\"), - ); - } - ret.push(file); - } - Ok((target.to_string(), ret)) - }) - .collect() -} - -#[derive(Debug, Default)] -struct CustomExecutorInnerContext { - /// Stores all lib.rs, main.rs etc. passed to rustc during the build. - rs_file_args: HashSet, - - /// Investigate if this needs to be intercepted like this or if it can be - /// looked up in a nicer way. - out_dir_args: HashSet, -} - -use std::sync::PoisonError; - -/// A cargo Executor to intercept all build tasks and store all ".rs" file -/// paths for later scanning. -/// -/// TODO: This is the place(?) to make rustc perform macro expansion to allow -/// scanning of the the expanded code. (incl. code generated by build.rs). -/// Seems to require nightly rust. -#[derive(Debug)] -struct CustomExecutor { - /// Current work dir - cwd: PathBuf, - - /// Needed since multiple rustc calls can be in flight at the same time. - inner_ctx: Arc>, -} - -use std::error::Error; -use std::fmt; - -#[derive(Debug)] -enum CustomExecutorError { - OutDirKeyMissing(String), - OutDirValueMissing(String), - InnerContextMutex(String), - Io(io::Error, PathBuf), -} - -impl Error for CustomExecutorError {} - -/// Forward Display to Debug. See the crate root documentation. -impl fmt::Display for CustomExecutorError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(self, f) - } -} - -impl Executor for CustomExecutor { - /// In case of an `Err`, Cargo will not continue with the build process for - /// this package. - fn exec( - &self, - cmd: ProcessBuilder, - _id: PackageId, - _target: &Target, - _mode: CompileMode, - _on_stdout_line: &mut dyn FnMut(&str) -> CargoResult<()>, - _on_stderr_line: &mut dyn FnMut(&str) -> CargoResult<()>, - ) -> CargoResult<()> { - let args = cmd.get_args(); - let out_dir_key = OsString::from("--out-dir"); - let out_dir_key_idx = - args.iter().position(|s| *s == out_dir_key).ok_or_else(|| { - CustomExecutorError::OutDirKeyMissing(cmd.to_string()) - })?; - let out_dir = args - .get(out_dir_key_idx + 1) - .ok_or_else(|| { - CustomExecutorError::OutDirValueMissing(cmd.to_string()) - }) - .map(PathBuf::from)?; - - // This can be different from the cwd used to launch the wrapping cargo - // plugin. Discovered while fixing - // https://github.com/anderejd/cargo-geiger/issues/19 - let cwd = cmd - .get_cwd() - .map(PathBuf::from) - .unwrap_or_else(|| self.cwd.to_owned()); - - { - // Scope to drop and release the mutex before calling rustc. - let mut ctx = self.inner_ctx.lock().map_err(|e| { - CustomExecutorError::InnerContextMutex(e.to_string()) - })?; - for tuple in args - .iter() - .map(|s| (s, s.to_string_lossy().to_lowercase())) - .filter(|t| t.1.ends_with(".rs")) - { - let raw_path = cwd.join(tuple.0); - let p = raw_path - .canonicalize() - .map_err(|e| CustomExecutorError::Io(e, raw_path))?; - ctx.rs_file_args.insert(p); - } - ctx.out_dir_args.insert(out_dir); - } - cmd.exec()?; - Ok(()) - } - - /// Queried when queuing each unit of work. If it returns true, then the - /// unit will always be rebuilt, independent of whether it needs to be. - fn force_rebuild(&self, _unit: &Unit) -> bool { - true // Overriding the default to force all units to be processed. - } -} - -/// A step towards decoupling some parts of the table-tree printing from the -/// dependency graph traversal. -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, - /// eg. "[build-dependencies]". - ExtraDepsGroup { kind: DepKind, tree_vines: String }, -} - -/// To print the returned TextTreeLines in order are expectged to produce a nice -/// looking tree structure. -/// -/// TODO: Return a impl Iterator -/// TODO: Consider separating the tree vine building from the tree traversal. -/// -fn walk_dependency_tree( - root_pack_id: PackageId, - graph: &Graph, - pc: &PrintConfig, -) -> Vec { - let mut visited_deps = HashSet::new(); - let mut levels_continue = vec![]; - let node = &graph.graph[graph.nodes[&root_pack_id]]; - walk_dependency_node( - node, - graph, - &mut visited_deps, - &mut levels_continue, - pc, - ) -} - -fn walk_dependency_node( - package: &Node, - graph: &Graph, - visited_deps: &mut HashSet, - levels_continue: &mut Vec, - pc: &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 mut all_out = vec![TextTreeLine::Package { - id: package.id, - tree_vines, - }]; - - if !new { - return all_out; - } - - let mut normal = vec![]; - let mut build = vec![]; - let mut development = vec![]; - for edge in graph - .graph - .edges_directed(graph.nodes[&package.id], pc.direction) - { - let dep = match pc.direction { - EdgeDirection::Incoming => &graph.graph[edge.source()], - EdgeDirection::Outgoing => &graph.graph[edge.target()], - }; - match *edge.weight() { - DepKind::Normal => normal.push(dep), - DepKind::Build => build.push(dep), - DepKind::Development => development.push(dep), - } - } - let mut normal_out = walk_dependency_kind( - DepKind::Normal, - &mut normal, - graph, - visited_deps, - levels_continue, - pc, - ); - let mut build_out = walk_dependency_kind( - DepKind::Build, - &mut build, - graph, - visited_deps, - levels_continue, - pc, - ); - let mut dev_out = walk_dependency_kind( - DepKind::Development, - &mut development, - graph, - visited_deps, - levels_continue, - pc, - ); - all_out.append(&mut normal_out); - all_out.append(&mut build_out); - all_out.append(&mut dev_out); - all_out -} - -fn walk_dependency_kind( - kind: DepKind, - deps: &mut Vec<&Node>, - graph: &Graph, - visited_deps: &mut HashSet, - levels_continue: &mut Vec, - pc: &PrintConfig, -) -> Vec { - if deps.is_empty() { - return Vec::new(); - } - - // 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 mut output = Vec::new(); - if let Prefix::Indent = pc.prefix { - match kind { - DepKind::Normal => (), - _ => { - let mut tree_vines = String::new(); - for &continues in &**levels_continue { - let c = if continues { tree_symbols.down } else { " " }; - tree_vines.push_str(&format!("{} ", c)); - } - output.push(TextTreeLine::ExtraDepsGroup { kind, tree_vines }); - } - } - } - - let mut it = deps.iter().peekable(); - while let Some(dependency) = it.next() { - levels_continue.push(it.peek().is_some()); - output.append(&mut walk_dependency_node( - dependency, - graph, - visited_deps, - levels_continue, - pc, - )); - levels_continue.pop(); - } - output -} - -enum DetectionStatus { - NoneDetectedForbidsUnsafe, - NoneDetectedAllowsUnsafe, - UnsafeDetected, -} - -fn get_tree_symbols(cs: Charset) -> TreeSymbols { - match cs { - Charset::Utf8 => UTF8_TREE_SYMBOLS, - Charset::Ascii => ASCII_TREE_SYMBOLS, - } -} - -// TODO: use a table library, or factor the tableness out in a smarter way. This -// is probably easier now when the tree formatting is separated from the tree -// traversal. -const UNSAFE_COUNTERS_HEADER: [&str; 6] = [ - "Functions ", - "Expressions ", - "Impls ", - "Traits ", - "Methods ", - "Dependency", -]; - -fn table_row_empty() -> String { - " ".repeat( - UNSAFE_COUNTERS_HEADER - .iter() - .take(5) - .map(|s| s.len()) - .sum::() - + UNSAFE_COUNTERS_HEADER.len() - + 1, - ) -} - -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) { - &mut used - } else { - &mut not_used - }; - *target = target.clone() + v.metrics.counters.clone(); - } - let fmt = |used: &Count, not_used: &Count| { - format!("{}/{}", used.unsafe_, used.unsafe_ + not_used.unsafe_) - }; - format!( - "{: <10} {: <12} {: <6} {: <7} {: <7}", - fmt(&used.functions, ¬_used.functions), - fmt(&used.exprs, ¬_used.exprs), - fmt(&used.item_impls, ¬_used.item_impls), - fmt(&used.item_traits, ¬_used.item_traits), - fmt(&used.methods, ¬_used.methods), - ) -} - -fn table_footer( - used: CounterBlock, - not_used: CounterBlock, - status: DetectionStatus, -) -> colored::ColoredString { - let fmt = |used: &Count, not_used: &Count| { - format!("{}/{}", used.unsafe_, used.unsafe_ + not_used.unsafe_) - }; - let output = format!( - "{: <10} {: <12} {: <6} {: <7} {: <7}", - fmt(&used.functions, ¬_used.functions), - fmt(&used.exprs, ¬_used.exprs), - fmt(&used.item_impls, ¬_used.item_impls), - fmt(&used.item_traits, ¬_used.item_traits), - fmt(&used.methods, ¬_used.methods), - ); - colorize(output, &status) -} - -pub fn find_rs_files_in_dir(dir: &Path) -> impl Iterator { - let walker = WalkDir::new(dir).into_iter(); - walker.filter_map(|entry| { - let entry = entry.expect("walkdir error."); // TODO: Return result. - if !is_file_with_ext(&entry, "rs") { - return None; - } - Some( - entry - .path() - .canonicalize() - .expect("Error converting to canonical path"), - ) // TODO: Return result. - }) -} diff --git a/cargo-geiger/src/find.rs b/cargo-geiger/src/find.rs new file mode 100644 index 00000000..3d2b6d21 --- /dev/null +++ b/cargo-geiger/src/find.rs @@ -0,0 +1,139 @@ +use crate::rs_file::{ + into_rs_code_file, is_file_with_ext, PackageMetrics, RsFile, + RsFileMetricsWrapper, +}; +use crate::scan::ScanMode; + +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 std::path::Path; +use std::path::PathBuf; +use walkdir::WalkDir; + +// ---------- BEGIN: Public items ---------- + +/// Provides a more terse and searchable name for the wrapped generic +/// collection. +pub struct GeigerContext { + pub pack_id_to_metrics: HashMap, +} + +pub fn find_unsafe_in_packages( + packs: &PackageSet, + allow_partial_results: bool, + include_tests: IncludeTests, + mode: ScanMode, + mut progress_step: F, +) -> GeigerContext +where + F: FnMut(usize, usize) -> CargoResult<()>, +{ + let mut pack_id_to_metrics = HashMap::new(); + let packs = packs.get_many(packs.package_ids()).unwrap(); + let pack_code_files: Vec<_> = find_rs_files_in_packages(&packs).collect(); + let pack_code_file_count = pack_code_files.len(); + for (i, (pack_id, rs_code_file)) in pack_code_files.into_iter().enumerate() + { + let (is_entry_point, p) = match rs_code_file { + RsFile::LibRoot(pb) => (true, pb), + RsFile::BinRoot(pb) => (true, pb), + RsFile::CustomBuildRoot(pb) => (true, pb), + RsFile::Other(pb) => (false, pb), + }; + if let (false, ScanMode::EntryPointsOnly) = (is_entry_point, &mode) { + continue; + } + match find_unsafe_in_file(&p, include_tests) { + Err(e) => { + if allow_partial_results { + eprintln!( + "Failed to parse file: {}, {:?} ", + &p.display(), + e + ); + } else { + panic!("Failed to parse file: {}, {:?} ", &p.display(), e); + } + } + Ok(file_metrics) => { + let package_metrics = pack_id_to_metrics + .entry(pack_id) + .or_insert_with(PackageMetrics::default); + let wrapper = package_metrics + .rs_path_to_metrics + .entry(p) + .or_insert_with(RsFileMetricsWrapper::default); + wrapper.metrics = file_metrics; + wrapper.is_crate_entry_point = is_entry_point; + } + } + let _ = progress_step(i, pack_code_file_count); + } + GeigerContext { pack_id_to_metrics } +} + +// ---------- END: Public items ---------- + +fn find_rs_files_in_dir(dir: &Path) -> impl Iterator { + let walker = WalkDir::new(dir).into_iter(); + walker.filter_map(|entry| { + let entry = entry.expect("walkdir error."); // TODO: Return result. + if !is_file_with_ext(&entry, "rs") { + return None; + } + Some( + entry + .path() + .canonicalize() + .expect("Error converting to canonical path"), + ) // TODO: Return result. + }) +} + +fn find_rs_files_in_package(pack: &Package) -> Vec { + // Find all build target entry point source files. + let mut canon_targets = HashMap::new(); + for t in pack.targets() { + let path = t.src_path().path(); + let path = match path { + None => continue, + Some(p) => p, + }; + if !path.exists() { + // A package published to crates.io is not required to include + // everything. We have to skip this build target. + continue; + } + let canon = path + .canonicalize() // will Err on non-existing paths. + .expect("canonicalize for build target path failed."); // FIXME + let targets = canon_targets.entry(canon).or_insert_with(Vec::new); + targets.push(t); + } + let mut out = Vec::new(); + for p in find_rs_files_in_dir(pack.root()) { + if !canon_targets.contains_key(&p) { + out.push(RsFile::Other(p)); + } + } + for (k, v) in canon_targets.into_iter() { + for target in v { + out.push(into_rs_code_file(target.kind(), k.clone())); + } + } + out +} + +fn find_rs_files_in_packages<'a>( + packs: &'a [&Package], +) -> impl Iterator + 'a { + packs.iter().flat_map(|pack| { + find_rs_files_in_package(pack) + .into_iter() + .map(move |path| (pack.package_id(), path)) + }) +} diff --git a/cargo-geiger/src/format/mod.rs b/cargo-geiger/src/format.rs similarity index 57% rename from cargo-geiger/src/format/mod.rs rename to cargo-geiger/src/format.rs index 7be26e91..5ad14654 100644 --- a/cargo-geiger/src/format/mod.rs +++ b/cargo-geiger/src/format.rs @@ -1,51 +1,34 @@ +mod parse; +pub mod print; +pub mod table; +pub mod tree; + +use cargo::core::dependency::DepKind; use cargo::core::manifest::ManifestMetadata; use cargo::core::PackageId; +use colored::Colorize; use std::error::Error; use std::fmt; +use std::str::{self, FromStr}; use self::parse::{Parser, RawChunk}; -mod parse; +// ---------- BEGIN: Public items ---------- -enum Chunk { - Raw(String), - Package, - License, - Repository, +#[derive(Clone, Copy, PartialEq)] +pub enum Charset { + Utf8, + Ascii, } -pub struct Pattern(Vec); - -impl Pattern { - pub fn try_build(format: &str) -> Result> { - let mut chunks = vec![]; - - for raw in Parser::new(format) { - let chunk = match raw { - RawChunk::Text(text) => Chunk::Raw(text.to_owned()), - RawChunk::Argument("p") => Chunk::Package, - RawChunk::Argument("l") => Chunk::License, - RawChunk::Argument("r") => Chunk::Repository, - RawChunk::Argument(ref a) => { - return Err(format!("unsupported pattern `{}`", a).into()); - } - RawChunk::Error(err) => return Err(err.into()), - }; - chunks.push(chunk); - } - - Ok(Pattern(chunks)) - } +impl FromStr for Charset { + type Err = &'static str; - pub fn display<'a>( - &'a self, - package: &'a PackageId, - metadata: &'a ManifestMetadata, - ) -> Display<'a> { - Display { - pattern: self, - package, - metadata, + fn from_str(s: &str) -> Result { + match s { + "utf8" => Ok(Charset::Utf8), + "ascii" => Ok(Charset::Ascii), + _ => Err("invalid charset"), } } } @@ -85,3 +68,93 @@ impl<'a> fmt::Display for Display<'a> { Ok(()) } } + +pub struct EmojiSymbols { + charset: Charset, + emojis: [&'static str; 3], + fallbacks: [colored::ColoredString; 3], +} + +impl EmojiSymbols { + pub fn new(charset: Charset) -> EmojiSymbols { + Self { + charset, + emojis: ["🔒", "❓", "☢️"], + fallbacks: [":)".green(), "?".normal(), "!".red().bold()], + } + } + + pub fn will_output_emoji(&self) -> bool { + self.charset == Charset::Utf8 + && console::Term::stdout().features().wants_emoji() + } + + pub fn emoji(&self, kind: SymbolKind) -> Box { + let idx = kind as usize; + if self.will_output_emoji() { + Box::new(self.emojis[idx]) + } else { + Box::new(self.fallbacks[idx].clone()) + } + } +} + +pub struct Pattern(Vec); + +impl Pattern { + pub fn try_build(format: &str) -> Result> { + let mut chunks = vec![]; + + for raw in Parser::new(format) { + let chunk = match raw { + RawChunk::Text(text) => Chunk::Raw(text.to_owned()), + RawChunk::Argument("p") => Chunk::Package, + RawChunk::Argument("l") => Chunk::License, + RawChunk::Argument("r") => Chunk::Repository, + RawChunk::Argument(ref a) => { + return Err(format!("unsupported pattern `{}`", a).into()); + } + RawChunk::Error(err) => return Err(err.into()), + }; + chunks.push(chunk); + } + + 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)] +pub enum SymbolKind { + Lock = 0, + QuestionMark = 1, + Rads = 2, +} + +pub fn get_kind_group_name(k: DepKind) -> Option<&'static str> { + match k { + DepKind::Normal => None, + DepKind::Build => Some("[build-dependencies]"), + DepKind::Development => Some("[dev-dependencies]"), + } +} + +// ---------- END: Public items ---------- + +enum Chunk { + Raw(String), + Package, + License, + Repository, +} diff --git a/cargo-geiger/src/format/parse.rs b/cargo-geiger/src/format/parse.rs index 15f875ca..07f875a8 100644 --- a/cargo-geiger/src/format/parse.rs +++ b/cargo-geiger/src/format/parse.rs @@ -1,6 +1,8 @@ use std::iter; use std::str; +// ---------- BEGIN: Public items ---------- + pub enum RawChunk<'a> { Text(&'a str), Argument(&'a str), @@ -95,3 +97,5 @@ impl<'a> Iterator for Parser<'a> { } } } + +// ---------- END: Public items ---------- diff --git a/cargo-geiger/src/format/print.rs b/cargo-geiger/src/format/print.rs new file mode 100644 index 00000000..c955c3cb --- /dev/null +++ b/cargo-geiger/src/format/print.rs @@ -0,0 +1,46 @@ +use crate::format::{Charset, Pattern}; + +use cargo::core::shell::Verbosity; +use colored::Colorize; +use geiger::DetectionStatus; +use geiger::IncludeTests; +use petgraph::EdgeDirection; + +// ---------- BEGIN: Public items ---------- + +#[derive(Clone, Copy)] +pub enum Prefix { + None, + Indent, + Depth, +} + +pub struct PrintConfig<'a> { + /// Don't truncate dependencies that have already been displayed. + pub all: bool, + + pub verbosity: Verbosity, + pub direction: EdgeDirection, + pub prefix: Prefix, + + // Is anyone using this? This is a carry-over from cargo-tree. + // TODO: Open a github issue to discuss deprecation. + pub format: &'a Pattern, + + pub charset: Charset, + pub allow_partial_results: bool, + pub include_tests: IncludeTests, +} + +pub fn colorize( + s: String, + detection_status: &DetectionStatus, +) -> colored::ColoredString { + match detection_status { + DetectionStatus::NoneDetectedForbidsUnsafe => s.green(), + DetectionStatus::NoneDetectedAllowsUnsafe => s.normal(), + DetectionStatus::UnsafeDetected => s.red().bold(), + } +} + +// ---------- END: Public items ---------- diff --git a/cargo-geiger/src/format/table.rs b/cargo-geiger/src/format/table.rs new file mode 100644 index 00000000..949cf43c --- /dev/null +++ b/cargo-geiger/src/format/table.rs @@ -0,0 +1,235 @@ +use crate::find::GeigerContext; +use crate::format::print::{colorize, PrintConfig}; +use crate::format::tree::TextTreeLine; +use crate::format::{get_kind_group_name, EmojiSymbols, SymbolKind}; +use crate::rs_file::PackageMetrics; + +use cargo::core::package::PackageSet; +use geiger::{Count, CounterBlock, DetectionStatus}; +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; + +// ---------- BEGIN: Public items ---------- + +// TODO: use a table library, or factor the tableness out in a smarter way. This +// is probably easier now when the tree formatting is separated from the tree +// traversal. +pub const UNSAFE_COUNTERS_HEADER: [&str; 6] = [ + "Functions ", + "Expressions ", + "Impls ", + "Traits ", + "Methods ", + "Dependency", +]; + +pub fn print_text_tree_lines_as_table( + 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; + 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 { + TextTreeLine::Package { id, tree_vines } => { + 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 (k, v) in &pack_metrics.rs_path_to_metrics { + //println!("{}", k.display()); + let target = if rs_files_used.contains(k) { + &mut total + } else { + &mut total_unused + }; + *target = target.clone() + v.metrics.counters.clone(); + } + match (unsafe_found, crate_forbids_unsafe) { + (false, true) => { + total_packs_none_detected_forbids_unsafe += 1; + DetectionStatus::NoneDetectedForbidsUnsafe + } + (false, false) => { + total_packs_none_detected_allows_unsafe += 1; + DetectionStatus::NoneDetectedAllowsUnsafe + } + (true, _) => { + total_packs_unsafe_detected += 1; + DetectionStatus::UnsafeDetected + } + } + }); + let emoji_symbols = EmojiSymbols::new(print_config.charset); + let detection_status = + package_status.get(&id).unwrap_or_else(|| { + panic!("Expected to find package by id: {}", &id) + }); + let icon = match detection_status { + DetectionStatus::NoneDetectedForbidsUnsafe => { + emoji_symbols.emoji(SymbolKind::Lock) + } + DetectionStatus::NoneDetectedAllowsUnsafe => { + emoji_symbols.emoji(SymbolKind::QuestionMark) + } + DetectionStatus::UnsafeDetected => { + emoji_symbols.emoji(SymbolKind::Rads) + } + }; + let pack_name = colorize( + format!( + "{}", + print_config + .format + .display(&id, pack.manifest().metadata()) + ), + &detection_status, + ); + let unsafe_info = colorize( + table_row(&pack_metrics, &rs_files_used), + &detection_status, + ); + let shift_chars = unsafe_info.chars().count() + 4; + print!("{} {: <2}", unsafe_info, icon); + + // Here comes some special control characters to position the cursor + // properly for printing the last column containing the tree vines, after + // the emoji icon. This is a workaround for a potential bug where the + // radiation emoji will visually cover two characters in width but only + // count as a single character if using the column formatting provided by + // 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. + } + + println!(" {}{}", tree_vines, pack_name); + } + TextTreeLine::ExtraDepsGroup { kind, tree_vines } => { + let name = get_kind_group_name(kind); + if name.is_none() { + continue; + } + let name = name.unwrap(); + + // TODO: Fix the alignment on macOS (others too?) + println!("{}{}{}", 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) => DetectionStatus::UnsafeDetected, + (true, false, false) => DetectionStatus::NoneDetectedForbidsUnsafe, + _ => DetectionStatus::NoneDetectedAllowsUnsafe, + }; + println!( + "{}", + table_footer(total, total_unused, total_detection_status) + ); + + warning_count +} + +// ---------- END: Public items ---------- + +fn table_footer( + used: CounterBlock, + not_used: CounterBlock, + status: DetectionStatus, +) -> colored::ColoredString { + let fmt = |used: &Count, not_used: &Count| { + format!("{}/{}", used.unsafe_, used.unsafe_ + not_used.unsafe_) + }; + let output = format!( + "{: <10} {: <12} {: <6} {: <7} {: <7}", + fmt(&used.functions, ¬_used.functions), + fmt(&used.exprs, ¬_used.exprs), + fmt(&used.item_impls, ¬_used.item_impls), + fmt(&used.item_traits, ¬_used.item_traits), + fmt(&used.methods, ¬_used.methods), + ); + 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 (k, v) in pms.rs_path_to_metrics.iter() { + let target = if rs_files_used.contains(k) { + &mut used + } else { + &mut not_used + }; + *target = target.clone() + v.metrics.counters.clone(); + } + let fmt = |used: &Count, not_used: &Count| { + format!("{}/{}", used.unsafe_, used.unsafe_ + not_used.unsafe_) + }; + format!( + "{: <10} {: <12} {: <6} {: <7} {: <7}", + fmt(&used.functions, ¬_used.functions), + fmt(&used.exprs, ¬_used.exprs), + fmt(&used.item_impls, ¬_used.item_impls), + fmt(&used.item_traits, ¬_used.item_traits), + fmt(&used.methods, ¬_used.methods), + ) +} + +fn table_row_empty() -> String { + " ".repeat( + UNSAFE_COUNTERS_HEADER + .iter() + .take(5) + .map(|s| s.len()) + .sum::() + + UNSAFE_COUNTERS_HEADER.len() + + 1, + ) +} diff --git a/cargo-geiger/src/format/tree.rs b/cargo-geiger/src/format/tree.rs new file mode 100644 index 00000000..e1df0e2f --- /dev/null +++ b/cargo-geiger/src/format/tree.rs @@ -0,0 +1,46 @@ +use crate::format::Charset; + +use cargo::core::dependency::DepKind; +use cargo::core::PackageId; + +// ---------- BEGIN: Public items ---------- + +/// A step towards decoupling some parts of the table-tree printing from the +/// dependency graph traversal. +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, + /// eg. "[build-dependencies]". + ExtraDepsGroup { kind: DepKind, tree_vines: String }, +} + +pub struct TreeSymbols { + pub down: &'static str, + pub tee: &'static str, + pub ell: &'static str, + pub right: &'static str, +} + +pub fn get_tree_symbols(cs: Charset) -> TreeSymbols { + match cs { + Charset::Utf8 => UTF8_TREE_SYMBOLS, + Charset::Ascii => ASCII_TREE_SYMBOLS, + } +} + +// ---------- END: Public items ---------- + +const ASCII_TREE_SYMBOLS: TreeSymbols = TreeSymbols { + down: "|", + tee: "|", + ell: "`", + right: "-", +}; + +const UTF8_TREE_SYMBOLS: TreeSymbols = TreeSymbols { + down: "│", + tee: "├", + ell: "└", + right: "─", +}; diff --git a/cargo-geiger/src/graph.rs b/cargo-geiger/src/graph.rs index 9ff5cf0b..51023bdc 100644 --- a/cargo-geiger/src/graph.rs +++ b/cargo-geiger/src/graph.rs @@ -1,11 +1,11 @@ -use cargo::core::{PackageId, Resolve}; use cargo::core::dependency::DepKind; use cargo::core::package::PackageSet; +use cargo::core::{PackageId, Resolve}; use cargo::util::CargoResult; use cargo_platform::Cfg; use petgraph::graph::NodeIndex; use std::collections::hash_map::Entry; -use std::collections::{HashMap}; +use std::collections::HashMap; // ---------- BEGIN: Public items ---------- @@ -28,11 +28,13 @@ impl ExtraDeps { } } +/// Representation of the package dependency graph pub struct Graph { pub graph: petgraph::Graph, pub nodes: HashMap, } +/// Representation of a node within the package dependency graph pub struct Node { pub id: PackageId, // TODO: Investigate why this was needed before the separation of printing @@ -40,8 +42,9 @@ pub struct Node { //pack: &'a Package, } -/// Almost unmodified compared to the original in cargo-tree, should be fairly -/// simple to move this and the dependency graph structure out to a library. +// Almost unmodified compared to the original in cargo-tree, should be fairly +// simple to move this and the dependency graph structure out to a library. +/// Function to build a graph of packages dependencies pub fn build_graph<'a>( resolve: &'a Resolve, packages: &'a PackageSet, @@ -62,48 +65,83 @@ pub fn build_graph<'a>( let mut pending = vec![root]; + let graph_configuration = GraphConfiguration { + target, + cfgs, + extra_deps, + }; + while let Some(pkg_id) = pending.pop() { - let idx = graph.nodes[&pkg_id]; - let pkg = packages.get_one(pkg_id)?; + add_package_dependencies_to_graph( + resolve, + pkg_id, + packages, + &graph_configuration, + &mut graph, + &mut pending, + )?; + } + + Ok(graph) +} + +// ---------- END: Public items ---------- - for raw_dep_id in resolve.deps_not_replaced(pkg_id) { - let it = pkg - .dependencies() - .iter() - .filter(|d| d.matches_ignoring_source(raw_dep_id.0)) - .filter(|d| extra_deps.allows(d.kind())) - .filter(|d| { - d.platform() - .and_then(|p| { - target.map(|t| match cfgs { +struct GraphConfiguration<'a> { + target: Option<&'a str>, + cfgs: Option<&'a [Cfg]>, + extra_deps: ExtraDeps, +} + +#[doc(hidden)] +fn add_package_dependencies_to_graph<'a>( + resolve: &'a Resolve, + package_id: PackageId, + packages: &'a PackageSet, + graph_configuration: &GraphConfiguration, + graph: &mut Graph, + pending_packages: &mut Vec, +) -> CargoResult<()> { + let idx = 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 + .dependencies() + .iter() + .filter(|d| d.matches_ignoring_source(raw_dep_id.0)) + .filter(|d| graph_configuration.extra_deps.allows(d.kind())) + .filter(|d| { + d.platform() + .and_then(|p| { + graph_configuration.target.map(|t| { + match graph_configuration.cfgs { None => false, Some(cfgs) => p.matches(t, cfgs), - }) + } }) - .unwrap_or(true) - }); - let dep_id = match resolve.replacement(raw_dep_id.0) { - Some(id) => id, - None => raw_dep_id.0, + }) + .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)) + } }; - for dep in it { - let dep_idx = match graph.nodes.entry(dep_id) { - Entry::Occupied(e) => *e.get(), - Entry::Vacant(e) => { - pending.push(dep_id); - let node = Node { - id: dep_id, - //pack: packages.get_one(dep_id)?, - }; - *e.insert(graph.graph.add_node(node)) - } - }; - graph.graph.add_edge(idx, dep_idx, dep.kind()); - } + graph.graph.add_edge(idx, dep_idx, dep.kind()); } } - Ok(graph) + Ok(()) } - -// ---------- END: Public items ---------- diff --git a/cargo-geiger/src/main.rs b/cargo-geiger/src/main.rs index 03c9ce37..1dddd5a6 100644 --- a/cargo-geiger/src/main.rs +++ b/cargo-geiger/src/main.rs @@ -9,23 +9,25 @@ extern crate colored; extern crate petgraph; mod cli; +mod find; mod format; mod graph; +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::cli::run_scan_mode_default; -use crate::cli::run_scan_mode_forbid_only; -use crate::cli::Charset; -use crate::cli::Prefix; -use crate::cli::PrintConfig; -use crate::format::Pattern; +use crate::format::print::Prefix; +use crate::format::print::PrintConfig; +use crate::format::{Charset, Pattern}; use crate::graph::build_graph; use crate::graph::ExtraDeps; -use cargo::core::shell::Shell; -use cargo::core::shell::Verbosity; +use crate::scan::{run_scan_mode_default, run_scan_mode_forbid_only}; + +use cargo::core::shell::{Shell, Verbosity}; use cargo::util::errors::CliError; use cargo::CliResult; use cargo::Config; @@ -244,10 +246,7 @@ fn real_main(args: &Args, config: &mut Config) -> CliResult { let target = if args.all_targets { None } else { - Some( - args.target.as_deref() - .unwrap_or(&config_host), - ) + Some(args.target.as_deref().unwrap_or(&config_host)) }; let format = Pattern::try_build(&args.format).map_err(|e| { diff --git a/cargo-geiger/src/rs_file.rs b/cargo-geiger/src/rs_file.rs new file mode 100644 index 00000000..fe4f931f --- /dev/null +++ b/cargo-geiger/src/rs_file.rs @@ -0,0 +1,333 @@ +use cargo::core::compiler::{CompileMode, Executor, Unit}; +use cargo::core::manifest::TargetKind; +use cargo::core::{InternedString, PackageId, Target, Workspace}; +use cargo::ops; +use cargo::ops::{CleanOptions, CompileOptions}; +use cargo::util::{paths, CargoResult, ProcessBuilder}; +use geiger::RsFileMetrics; +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::ffi::OsString; +use std::fmt; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex, PoisonError}; +use walkdir::{DirEntry, WalkDir}; + +// ---------- BEGIN: Public items ---------- + +/// Provides information needed to scan for crate root +/// `#![forbid(unsafe_code)]`. +/// The wrapped PathBufs are canonicalized. +pub enum RsFile { + /// Library entry point source file, usually src/lib.rs + LibRoot(PathBuf), + + /// Executable entry point source file, usually src/main.rs + BinRoot(PathBuf), + + /// Not sure if this is relevant but let's be conservative for now. + CustomBuildRoot(PathBuf), + + /// All other .rs files. + Other(PathBuf), +} + +#[derive(Debug, Default)] +pub struct RsFileMetricsWrapper { + /// The information returned by the `geiger` crate for a `.rs` file. + pub metrics: RsFileMetrics, + + /// All crate entry points must declare forbid(unsafe_code) to make it count + /// for the crate as a whole. The `geiger` crate is decoupled from `cargo` + /// and cannot know if a file is a crate entry point or not, so we add this + /// information here. + pub is_crate_entry_point: bool, +} + +#[derive(Debug, Default)] +pub struct PackageMetrics { + /// The key is the canonicalized path to the rs source file. + pub rs_path_to_metrics: HashMap, +} + +pub fn into_rs_code_file(kind: &TargetKind, path: PathBuf) -> RsFile { + match kind { + TargetKind::Lib(_) => RsFile::LibRoot(path), + TargetKind::Bin => RsFile::BinRoot(path), + TargetKind::Test => RsFile::Other(path), + TargetKind::Bench => RsFile::Other(path), + TargetKind::ExampleLib(_) => RsFile::Other(path), + TargetKind::ExampleBin => RsFile::Other(path), + TargetKind::CustomBuild => RsFile::CustomBuildRoot(path), + } +} + +pub fn is_file_with_ext(entry: &DirEntry, file_ext: &str) -> bool { + if !entry.file_type().is_file() { + return false; + } + let p = entry.path(); + let ext = match p.extension() { + Some(e) => e, + None => return false, + }; + // to_string_lossy is ok since we only want to match against an ASCII + // compatible extension and we do not keep the possibly lossy result + // around. + ext.to_string_lossy() == file_ext +} + +/// 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, +) -> Result, RsResolveError> { + let config = ws.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 { + config: &config, + spec: vec![], + targets: vec![], + profile_specified: false, + // A temporary hack to get cargo 0.43 to build, TODO: look closer at the updated cargo API + // later. + requested_profile: InternedString::new("dev"), + doc: false, + }; + ops::clean(ws, &clean_opt) + .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()))?; + } + let ws_root = ws.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(); + 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?); + } + } + } + for pb in rs_files { + // rs_files must already be canonicalized + hs.insert(pb); + } + Ok(hs) +} + +// ---------- END: Public items ---------- + +/// A cargo Executor to intercept all build tasks and store all ".rs" file +/// paths for later scanning. +/// +/// TODO: This is the place(?) to make rustc perform macro expansion to allow +/// scanning of the the expanded code. (incl. code generated by build.rs). +/// Seems to require nightly rust. +#[derive(Debug)] +struct CustomExecutor { + /// Current work dir + cwd: PathBuf, + + /// Needed since multiple rustc calls can be in flight at the same time. + inner_ctx: Arc>, +} + +impl Executor for CustomExecutor { + /// In case of an `Err`, Cargo will not continue with the build process for + /// this package. + fn exec( + &self, + cmd: ProcessBuilder, + _id: PackageId, + _target: &Target, + _mode: CompileMode, + _on_stdout_line: &mut dyn FnMut(&str) -> CargoResult<()>, + _on_stderr_line: &mut dyn FnMut(&str) -> CargoResult<()>, + ) -> CargoResult<()> { + let args = cmd.get_args(); + let out_dir_key = OsString::from("--out-dir"); + let out_dir_key_idx = + args.iter().position(|s| *s == out_dir_key).ok_or_else(|| { + CustomExecutorError::OutDirKeyMissing(cmd.to_string()) + })?; + let out_dir = args + .get(out_dir_key_idx + 1) + .ok_or_else(|| { + CustomExecutorError::OutDirValueMissing(cmd.to_string()) + }) + .map(PathBuf::from)?; + + // This can be different from the cwd used to launch the wrapping cargo + // plugin. Discovered while fixing + // https://github.com/anderejd/cargo-geiger/issues/19 + let cwd = cmd + .get_cwd() + .map(PathBuf::from) + .unwrap_or_else(|| self.cwd.to_owned()); + + { + // Scope to drop and release the mutex before calling rustc. + let mut ctx = self.inner_ctx.lock().map_err(|e| { + CustomExecutorError::InnerContextMutex(e.to_string()) + })?; + for tuple in args + .iter() + .map(|s| (s, s.to_string_lossy().to_lowercase())) + .filter(|t| t.1.ends_with(".rs")) + { + let raw_path = cwd.join(tuple.0); + let p = raw_path + .canonicalize() + .map_err(|e| CustomExecutorError::Io(e, raw_path))?; + ctx.rs_file_args.insert(p); + } + ctx.out_dir_args.insert(out_dir); + } + cmd.exec()?; + Ok(()) + } + + /// Queried when queuing each unit of work. If it returns true, then the + /// unit will always be rebuilt, independent of whether it needs to be. + fn force_rebuild(&self, _unit: &Unit) -> bool { + true // Overriding the default to force all units to be processed. + } +} + +#[derive(Debug)] +enum CustomExecutorError { + OutDirKeyMissing(String), + OutDirValueMissing(String), + InnerContextMutex(String), + Io(io::Error, PathBuf), +} + +impl Error for CustomExecutorError {} + +/// Forward Display to Debug. See the crate root documentation. +impl fmt::Display for CustomExecutorError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +#[derive(Debug, Default)] +struct CustomExecutorInnerContext { + /// Stores all lib.rs, main.rs etc. passed to rustc during the build. + rs_file_args: HashSet, + + /// Investigate if this needs to be intercepted like this or if it can be + /// looked up in a nicer way. + out_dir_args: HashSet, +} + +#[derive(Debug)] +pub enum RsResolveError { + Walkdir(walkdir::Error), + + /// Like io::Error but with the related path. + Io(io::Error, PathBuf), + + /// Would like cargo::Error here, but it's private, why? + /// This is still way better than a panic though. + Cargo(String), + + /// This should not happen unless incorrect assumptions have been made in + /// cargo-geiger about how the cargo API works. + ArcUnwrap(), + + /// Failed to get the inner context out of the mutex. + InnerContextMutex(String), + + /// Failed to parse a .dep file. + DepParse(String, PathBuf), +} + +impl Error for RsResolveError {} + +/// Forward Display to Debug. +impl fmt::Display for RsResolveError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +impl From> for RsResolveError { + fn from(e: PoisonError) -> Self { + RsResolveError::InnerContextMutex(e.to_string()) + } +} + +/// 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 +/// the dependency data in some other way. +fn parse_rustc_dep_info( + rustc_dep_info: &Path, +) -> CargoResult)>> { + let contents = paths::read(rustc_dep_info)?; + contents + .lines() + .filter_map(|l| l.find(": ").map(|i| (l, i))) + .map(|(line, pos)| { + let target = &line[..pos]; + let mut deps = line[pos + 2..].split_whitespace(); + let mut ret = Vec::new(); + while let Some(s) = deps.next() { + let mut file = s.to_string(); + while file.ends_with('\\') { + file.pop(); + file.push(' '); + //file.push_str(deps.next().ok_or_else(|| { + //internal("malformed dep-info format, trailing \\".to_string()) + //})?); + file.push_str( + deps.next() + .expect("malformed dep-info format, trailing \\"), + ); + } + ret.push(file); + } + Ok((target.to_string(), ret)) + }) + .collect() +} diff --git a/cargo-geiger/src/scan.rs b/cargo-geiger/src/scan.rs new file mode 100644 index 00000000..7de8c4d0 --- /dev/null +++ b/cargo-geiger/src/scan.rs @@ -0,0 +1,289 @@ +use crate::find::find_unsafe_in_packages; +use crate::format::print::PrintConfig; +use crate::format::table::{ + print_text_tree_lines_as_table, UNSAFE_COUNTERS_HEADER, +}; +use crate::format::tree::TextTreeLine; +use crate::format::{get_kind_group_name, EmojiSymbols, Pattern, SymbolKind}; +use crate::graph::Graph; +use crate::rs_file::resolve_rs_file_deps; +use crate::traversal::walk_dependency_tree; +use crate::Args; + +use cargo::core::compiler::CompileMode; +use cargo::core::package::PackageSet; +use cargo::core::shell::Verbosity; +use cargo::core::{Package, PackageId, Workspace}; +use cargo::ops::CompileOptions; +use cargo::util::CargoResult; +use cargo::Config; +use cargo::{CliError, CliResult}; +use colored::Colorize; +use std::collections::HashSet; +use std::error::Error; +use std::fmt; +use std::path::PathBuf; + +// ---------- BEGIN: Public items ---------- + +pub enum ScanMode { + // The default scan mode, scan every .rs file. + Full, + + // An optimization to allow skipping everything except the entry points. + // This is only useful for the "--forbid-only" mode since that mode only + // depends on entry point .rs files. + EntryPointsOnly, +} + +pub fn run_scan_mode_default( + config: &Config, + ws: &Workspace, + packages: &PackageSet, + root_pack_id: PackageId, + graph: &Graph, + pc: &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 progress = cargo::util::Progress::new("Scanning", config); + let emoji_symbols = EmojiSymbols::new(pc.charset); + let geiger_ctx = find_unsafe_in_packages( + &packages, + pc.allow_partial_results, + pc.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!(); + + 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!(); + + println!( + "{}", + UNSAFE_COUNTERS_HEADER + .iter() + .map(|s| s.to_owned()) + .collect::>() + .join(" ") + .bold() + ); + println!(); + + 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, + &rs_files_used, + tree_lines, + ); + + 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 }), + 1, + )) + } else { + Ok(()) + } +} + +pub fn run_scan_mode_forbid_only( + config: &Config, + packages: &PackageSet, + root_pack_id: PackageId, + graph: &Graph, + pc: &PrintConfig, +) -> CliResult { + let emoji_symbols = EmojiSymbols::new(pc.charset); + let mut progress = cargo::util::Progress::new("Scanning", config); + let geiger_ctx = find_unsafe_in_packages( + &packages, + pc.allow_partial_results, + pc.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); + + println!(" {: <2} = {}", sym_lock, forbids); + println!(" {: <2} = {}", sym_qmark, unknown); + println!(); + + let tree_lines = walk_dependency_tree(root_pack_id, &graph, &pc); + for tl in tree_lines { + match tl { + TextTreeLine::Package { id, tree_vines } => { + let pack = packages.get_one(id).unwrap(); // FIXME + let name = format_package_name(pack, pc.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? + Some(pm) => pm + .rs_path_to_metrics + .iter() + .all(|(_k, v)| v.metrics.forbids_unsafe), + }; + let (symbol, name) = if package_forbids_unsafe { + (&sym_lock, name.green()) + } else { + (&sym_qmark, name.red()) + }; + println!("{} {}{}", symbol, tree_vines, name); + } + TextTreeLine::ExtraDepsGroup { kind, tree_vines } => { + let name = get_kind_group_name(kind); + if name.is_none() { + continue; + } + let name = name.unwrap(); + // TODO: Fix the alignment on macOS (others too?) + println!(" {}{}", tree_vines, name); + } + } + } + + Ok(()) +} + +// ---------- END: Public items ---------- + +#[derive(Debug)] +struct FoundWarningsError { + pub warning_count: u64, +} + +impl Error for FoundWarningsError {} + +/// Forward Display to Debug. +impl fmt::Display for FoundWarningsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(self, f) + } +} + +/// Based on code from cargo-bloat. It seems weird that CompileOptions can be +/// constructed without providing all standard cargo options, TODO: Open an issue +/// in cargo? +fn build_compile_options<'a>( + args: &'a Args, + config: &'a Config, +) -> CompileOptions { + let features = args + .features + .as_ref() + .cloned() + .unwrap_or_else(String::new) + .split(' ') + .map(str::to_owned) + .collect::>(); + let mut opt = + 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; + + // TODO: Investigate if this is relevant to cargo-geiger. + //let mut bins = Vec::new(); + //let mut examples = Vec::new(); + // opt.release = args.release; + // opt.target = args.target.clone(); + // if let Some(ref name) = args.bin { + // bins.push(name.clone()); + // } else if let Some(ref name) = args.example { + // examples.push(name.clone()); + // } + // if args.bin.is_some() || args.example.is_some() { + // opt.filter = ops::CompileFilter::new( + // false, + // bins.clone(), false, + // Vec::new(), false, + // examples.clone(), false, + // Vec::new(), false, + // false, + // ); + // } + + opt +} + +fn format_package_name(pack: &Package, pat: &Pattern) -> String { + format!( + "{}", + pat.display(&pack.package_id(), pack.manifest().metadata()) + ) +} diff --git a/cargo-geiger/src/traversal.rs b/cargo-geiger/src/traversal.rs new file mode 100644 index 00000000..ac683fb1 --- /dev/null +++ b/cargo-geiger/src/traversal.rs @@ -0,0 +1,169 @@ +use crate::format::print::{Prefix, PrintConfig}; +use crate::format::tree::{get_tree_symbols, TextTreeLine}; +use crate::graph::{Graph, Node}; + +use cargo::core::dependency::DepKind; +use cargo::core::PackageId; +use petgraph::visit::EdgeRef; +use petgraph::EdgeDirection; +use std::collections::HashSet; + +// ---------- BEGIN: Public items ---------- + +/// To print the returned TextTreeLines in order are expected to produce a nice +/// looking tree structure. +/// +/// TODO: Return a impl Iterator +/// TODO: Consider separating the tree vine building from the tree traversal. +/// +pub fn walk_dependency_tree( + root_pack_id: PackageId, + graph: &Graph, + pc: &PrintConfig, +) -> Vec { + let mut visited_deps = HashSet::new(); + let mut levels_continue = vec![]; + let node = &graph.graph[graph.nodes[&root_pack_id]]; + walk_dependency_node( + node, + graph, + &mut visited_deps, + &mut levels_continue, + pc, + ) +} + +// ---------- END: Public items ---------- + +fn walk_dependency_kind( + kind: DepKind, + deps: &mut Vec<&Node>, + graph: &Graph, + visited_deps: &mut HashSet, + levels_continue: &mut Vec, + pc: &PrintConfig, +) -> Vec { + if deps.is_empty() { + return Vec::new(); + } + + // 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 mut output = Vec::new(); + if let Prefix::Indent = pc.prefix { + match kind { + DepKind::Normal => (), + _ => { + let mut tree_vines = String::new(); + for &continues in &**levels_continue { + let c = if continues { tree_symbols.down } else { " " }; + tree_vines.push_str(&format!("{} ", c)); + } + output.push(TextTreeLine::ExtraDepsGroup { kind, tree_vines }); + } + } + } + + let mut it = deps.iter().peekable(); + while let Some(dependency) = it.next() { + levels_continue.push(it.peek().is_some()); + output.append(&mut walk_dependency_node( + dependency, + graph, + visited_deps, + levels_continue, + pc, + )); + levels_continue.pop(); + } + output +} + +fn walk_dependency_node( + package: &Node, + graph: &Graph, + visited_deps: &mut HashSet, + levels_continue: &mut Vec, + pc: &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 mut all_out = vec![TextTreeLine::Package { + id: package.id, + tree_vines, + }]; + + if !new { + return all_out; + } + + let mut normal = vec![]; + let mut build = vec![]; + let mut development = vec![]; + for edge in graph + .graph + .edges_directed(graph.nodes[&package.id], pc.direction) + { + let dep = match pc.direction { + EdgeDirection::Incoming => &graph.graph[edge.source()], + EdgeDirection::Outgoing => &graph.graph[edge.target()], + }; + match *edge.weight() { + DepKind::Normal => normal.push(dep), + DepKind::Build => build.push(dep), + DepKind::Development => development.push(dep), + } + } + let mut normal_out = walk_dependency_kind( + DepKind::Normal, + &mut normal, + graph, + visited_deps, + levels_continue, + pc, + ); + let mut build_out = walk_dependency_kind( + DepKind::Build, + &mut build, + graph, + visited_deps, + levels_continue, + pc, + ); + let mut dev_out = walk_dependency_kind( + DepKind::Development, + &mut development, + graph, + visited_deps, + levels_continue, + pc, + ); + all_out.append(&mut normal_out); + all_out.append(&mut build_out); + all_out.append(&mut dev_out); + all_out +} diff --git a/geiger/src/lib.rs b/geiger/src/lib.rs index 6a36906b..4050f201 100644 --- a/geiger/src/lib.rs +++ b/geiger/src/lib.rs @@ -102,6 +102,12 @@ impl Add for CounterBlock { } } +pub enum DetectionStatus { + NoneDetectedForbidsUnsafe, + NoneDetectedAllowsUnsafe, + UnsafeDetected, +} + /// Scan result for a single `.rs` file. #[derive(Debug, Default)] pub struct RsFileMetrics { From ae3317ad45eb322a8dda7c8df3dada83d72e8a7b Mon Sep 17 00:00:00 2001 From: Josh McConnell Date: Wed, 2 Sep 2020 19:27:19 +0100 Subject: [PATCH 2/2] ISSUE-16 - Make PR requested changes Make the following changes: * Remove all "BEGIN: Public items" comments * Move DetectionStatus enum back to cargo-geiger and rename to CrateDetectionStatus Signed-off-by: Josh McConnell --- cargo-geiger/src/cli.rs | 4 ---- cargo-geiger/src/find.rs | 4 ---- cargo-geiger/src/format.rs | 10 ++++++---- cargo-geiger/src/format/parse.rs | 4 ---- cargo-geiger/src/format/print.rs | 15 +++++---------- cargo-geiger/src/format/table.rs | 28 ++++++++++++---------------- cargo-geiger/src/format/tree.rs | 4 ---- cargo-geiger/src/graph.rs | 4 ---- cargo-geiger/src/rs_file.rs | 4 ---- cargo-geiger/src/scan.rs | 4 ---- cargo-geiger/src/traversal.rs | 4 ---- geiger/src/lib.rs | 6 ------ 12 files changed, 23 insertions(+), 68 deletions(-) diff --git a/cargo-geiger/src/cli.rs b/cargo-geiger/src/cli.rs index 8f7eacee..3ba3ce88 100644 --- a/cargo-geiger/src/cli.rs +++ b/cargo-geiger/src/cli.rs @@ -22,8 +22,6 @@ use cargo_platform::Cfg; use std::path::PathBuf; use std::str::{self, FromStr}; -// ---------- BEGIN: Public items ---------- - /// TODO: Write proper documentation for this. /// This function seems to be looking up the active flags for conditional /// compilation (cargo_platform::Cfg instances). @@ -103,6 +101,4 @@ pub fn resolve<'a, 'cfg>( Ok((packages, resolve)) } -// ---------- END: Public items ---------- - // TODO: Make a wrapper type for canonical paths and hide all mutable access. diff --git a/cargo-geiger/src/find.rs b/cargo-geiger/src/find.rs index 3d2b6d21..b0748a04 100644 --- a/cargo-geiger/src/find.rs +++ b/cargo-geiger/src/find.rs @@ -14,8 +14,6 @@ use std::path::Path; use std::path::PathBuf; use walkdir::WalkDir; -// ---------- BEGIN: Public items ---------- - /// Provides a more terse and searchable name for the wrapped generic /// collection. pub struct GeigerContext { @@ -76,8 +74,6 @@ where GeigerContext { pack_id_to_metrics } } -// ---------- END: Public items ---------- - fn find_rs_files_in_dir(dir: &Path) -> impl Iterator { let walker = WalkDir::new(dir).into_iter(); walker.filter_map(|entry| { diff --git a/cargo-geiger/src/format.rs b/cargo-geiger/src/format.rs index 5ad14654..8e1ed398 100644 --- a/cargo-geiger/src/format.rs +++ b/cargo-geiger/src/format.rs @@ -13,8 +13,6 @@ use std::str::{self, FromStr}; use self::parse::{Parser, RawChunk}; -// ---------- BEGIN: Public items ---------- - #[derive(Clone, Copy, PartialEq)] pub enum Charset { Utf8, @@ -33,6 +31,12 @@ impl FromStr for Charset { } } +pub enum CrateDetectionStatus { + NoneDetectedForbidsUnsafe, + NoneDetectedAllowsUnsafe, + UnsafeDetected, +} + pub struct Display<'a> { pattern: &'a Pattern, package: &'a PackageId, @@ -150,8 +154,6 @@ pub fn get_kind_group_name(k: DepKind) -> Option<&'static str> { } } -// ---------- END: Public items ---------- - enum Chunk { Raw(String), Package, diff --git a/cargo-geiger/src/format/parse.rs b/cargo-geiger/src/format/parse.rs index 07f875a8..15f875ca 100644 --- a/cargo-geiger/src/format/parse.rs +++ b/cargo-geiger/src/format/parse.rs @@ -1,8 +1,6 @@ use std::iter; use std::str; -// ---------- BEGIN: Public items ---------- - pub enum RawChunk<'a> { Text(&'a str), Argument(&'a str), @@ -97,5 +95,3 @@ impl<'a> Iterator for Parser<'a> { } } } - -// ---------- END: Public items ---------- diff --git a/cargo-geiger/src/format/print.rs b/cargo-geiger/src/format/print.rs index c955c3cb..20b88632 100644 --- a/cargo-geiger/src/format/print.rs +++ b/cargo-geiger/src/format/print.rs @@ -1,13 +1,10 @@ -use crate::format::{Charset, Pattern}; +use crate::format::{Charset, CrateDetectionStatus, Pattern}; use cargo::core::shell::Verbosity; use colored::Colorize; -use geiger::DetectionStatus; use geiger::IncludeTests; use petgraph::EdgeDirection; -// ---------- BEGIN: Public items ---------- - #[derive(Clone, Copy)] pub enum Prefix { None, @@ -34,13 +31,11 @@ pub struct PrintConfig<'a> { pub fn colorize( s: String, - detection_status: &DetectionStatus, + detection_status: &CrateDetectionStatus, ) -> colored::ColoredString { match detection_status { - DetectionStatus::NoneDetectedForbidsUnsafe => s.green(), - DetectionStatus::NoneDetectedAllowsUnsafe => s.normal(), - DetectionStatus::UnsafeDetected => s.red().bold(), + CrateDetectionStatus::NoneDetectedForbidsUnsafe => s.green(), + CrateDetectionStatus::NoneDetectedAllowsUnsafe => s.normal(), + CrateDetectionStatus::UnsafeDetected => s.red().bold(), } } - -// ---------- END: Public items ---------- diff --git a/cargo-geiger/src/format/table.rs b/cargo-geiger/src/format/table.rs index 949cf43c..c6e8ec68 100644 --- a/cargo-geiger/src/format/table.rs +++ b/cargo-geiger/src/format/table.rs @@ -1,16 +1,14 @@ use crate::find::GeigerContext; use crate::format::print::{colorize, PrintConfig}; use crate::format::tree::TextTreeLine; -use crate::format::{get_kind_group_name, EmojiSymbols, SymbolKind}; +use crate::format::{CrateDetectionStatus, get_kind_group_name, EmojiSymbols, SymbolKind}; use crate::rs_file::PackageMetrics; use cargo::core::package::PackageSet; -use geiger::{Count, CounterBlock, DetectionStatus}; +use geiger::{Count, CounterBlock}; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; -// ---------- BEGIN: Public items ---------- - // TODO: use a table library, or factor the tableness out in a smarter way. This // is probably easier now when the tree formatting is separated from the tree // traversal. @@ -88,15 +86,15 @@ pub fn print_text_tree_lines_as_table( match (unsafe_found, crate_forbids_unsafe) { (false, true) => { total_packs_none_detected_forbids_unsafe += 1; - DetectionStatus::NoneDetectedForbidsUnsafe + CrateDetectionStatus::NoneDetectedForbidsUnsafe } (false, false) => { total_packs_none_detected_allows_unsafe += 1; - DetectionStatus::NoneDetectedAllowsUnsafe + CrateDetectionStatus::NoneDetectedAllowsUnsafe } (true, _) => { total_packs_unsafe_detected += 1; - DetectionStatus::UnsafeDetected + CrateDetectionStatus::UnsafeDetected } } }); @@ -106,13 +104,13 @@ pub fn print_text_tree_lines_as_table( panic!("Expected to find package by id: {}", &id) }); let icon = match detection_status { - DetectionStatus::NoneDetectedForbidsUnsafe => { + CrateDetectionStatus::NoneDetectedForbidsUnsafe => { emoji_symbols.emoji(SymbolKind::Lock) } - DetectionStatus::NoneDetectedAllowsUnsafe => { + CrateDetectionStatus::NoneDetectedAllowsUnsafe => { emoji_symbols.emoji(SymbolKind::QuestionMark) } - DetectionStatus::UnsafeDetected => { + CrateDetectionStatus::UnsafeDetected => { emoji_symbols.emoji(SymbolKind::Rads) } }; @@ -165,9 +163,9 @@ pub fn print_text_tree_lines_as_table( total_packs_none_detected_allows_unsafe > 0, total_packs_unsafe_detected > 0, ) { - (_, _, true) => DetectionStatus::UnsafeDetected, - (true, false, false) => DetectionStatus::NoneDetectedForbidsUnsafe, - _ => DetectionStatus::NoneDetectedAllowsUnsafe, + (_, _, true) => CrateDetectionStatus::UnsafeDetected, + (true, false, false) => CrateDetectionStatus::NoneDetectedForbidsUnsafe, + _ => CrateDetectionStatus::NoneDetectedAllowsUnsafe, }; println!( "{}", @@ -177,12 +175,10 @@ pub fn print_text_tree_lines_as_table( warning_count } -// ---------- END: Public items ---------- - fn table_footer( used: CounterBlock, not_used: CounterBlock, - status: DetectionStatus, + status: CrateDetectionStatus, ) -> colored::ColoredString { let fmt = |used: &Count, not_used: &Count| { format!("{}/{}", used.unsafe_, used.unsafe_ + not_used.unsafe_) diff --git a/cargo-geiger/src/format/tree.rs b/cargo-geiger/src/format/tree.rs index e1df0e2f..2fcc7a16 100644 --- a/cargo-geiger/src/format/tree.rs +++ b/cargo-geiger/src/format/tree.rs @@ -3,8 +3,6 @@ use crate::format::Charset; use cargo::core::dependency::DepKind; use cargo::core::PackageId; -// ---------- BEGIN: Public items ---------- - /// A step towards decoupling some parts of the table-tree printing from the /// dependency graph traversal. pub enum TextTreeLine { @@ -29,8 +27,6 @@ pub fn get_tree_symbols(cs: Charset) -> TreeSymbols { } } -// ---------- END: Public items ---------- - const ASCII_TREE_SYMBOLS: TreeSymbols = TreeSymbols { down: "|", tee: "|", diff --git a/cargo-geiger/src/graph.rs b/cargo-geiger/src/graph.rs index 51023bdc..d3a1c92e 100644 --- a/cargo-geiger/src/graph.rs +++ b/cargo-geiger/src/graph.rs @@ -7,8 +7,6 @@ use petgraph::graph::NodeIndex; use std::collections::hash_map::Entry; use std::collections::HashMap; -// ---------- BEGIN: Public items ---------- - pub enum ExtraDeps { All, Build, @@ -85,8 +83,6 @@ pub fn build_graph<'a>( Ok(graph) } -// ---------- END: Public items ---------- - struct GraphConfiguration<'a> { target: Option<&'a str>, cfgs: Option<&'a [Cfg]>, diff --git a/cargo-geiger/src/rs_file.rs b/cargo-geiger/src/rs_file.rs index fe4f931f..463d46f8 100644 --- a/cargo-geiger/src/rs_file.rs +++ b/cargo-geiger/src/rs_file.rs @@ -14,8 +14,6 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, PoisonError}; use walkdir::{DirEntry, WalkDir}; -// ---------- BEGIN: Public items ---------- - /// Provides information needed to scan for crate root /// `#![forbid(unsafe_code)]`. /// The wrapped PathBufs are canonicalized. @@ -154,8 +152,6 @@ pub fn resolve_rs_file_deps( Ok(hs) } -// ---------- END: Public items ---------- - /// A cargo Executor to intercept all build tasks and store all ".rs" file /// paths for later scanning. /// diff --git a/cargo-geiger/src/scan.rs b/cargo-geiger/src/scan.rs index 7de8c4d0..16d25c62 100644 --- a/cargo-geiger/src/scan.rs +++ b/cargo-geiger/src/scan.rs @@ -24,8 +24,6 @@ use std::error::Error; use std::fmt; use std::path::PathBuf; -// ---------- BEGIN: Public items ---------- - pub enum ScanMode { // The default scan mode, scan every .rs file. Full, @@ -219,8 +217,6 @@ pub fn run_scan_mode_forbid_only( Ok(()) } -// ---------- END: Public items ---------- - #[derive(Debug)] struct FoundWarningsError { pub warning_count: u64, diff --git a/cargo-geiger/src/traversal.rs b/cargo-geiger/src/traversal.rs index ac683fb1..f80f7c30 100644 --- a/cargo-geiger/src/traversal.rs +++ b/cargo-geiger/src/traversal.rs @@ -8,8 +8,6 @@ use petgraph::visit::EdgeRef; use petgraph::EdgeDirection; use std::collections::HashSet; -// ---------- BEGIN: Public items ---------- - /// To print the returned TextTreeLines in order are expected to produce a nice /// looking tree structure. /// @@ -33,8 +31,6 @@ pub fn walk_dependency_tree( ) } -// ---------- END: Public items ---------- - fn walk_dependency_kind( kind: DepKind, deps: &mut Vec<&Node>, diff --git a/geiger/src/lib.rs b/geiger/src/lib.rs index 4050f201..6a36906b 100644 --- a/geiger/src/lib.rs +++ b/geiger/src/lib.rs @@ -102,12 +102,6 @@ impl Add for CounterBlock { } } -pub enum DetectionStatus { - NoneDetectedForbidsUnsafe, - NoneDetectedAllowsUnsafe, - UnsafeDetected, -} - /// Scan result for a single `.rs` file. #[derive(Debug, Default)] pub struct RsFileMetrics {