From c7ca2db85b4453e4eb74fedd44fb99ce81921980 Mon Sep 17 00:00:00 2001 From: Joshua Clayton Date: Wed, 20 May 2020 18:31:16 -0400 Subject: [PATCH] Introduce a 'doctor' subcommand What? ===== This introduces a 'doctor' subcommand which runs a small set of checks to provide context to the developer about the status of the codebase and analysis. This includes information like whether the tags file generated was generated via Universal Ctags, how many tokens and files are being processed, and the types of projects made available from configuration. --- Cargo.lock | 1 + crates/cli/Cargo.toml | 1 + crates/cli/src/cli_configuration.rs | 24 +----- crates/cli/src/doctor.rs | 86 +++++++++++++++++++ crates/cli/src/doctor/check_up.rs | 10 +++ crates/cli/src/doctor/files_count.rs | 26 ++++++ .../doctor/loaded_project_configurations.rs | 36 ++++++++ .../doctor/tags_included_in_files_searched.rs | 54 ++++++++++++ crates/cli/src/doctor/tokens_count.rs | 36 ++++++++ .../cli/src/doctor/using_universal_ctags.rs | 33 +++++++ crates/cli/src/flags.rs | 9 ++ crates/cli/src/lib.rs | 12 ++- .../cli/src/project_configurations_loader.rs | 22 +++++ crates/codebase_files/src/lib.rs | 7 +- crates/project_configuration/src/loader.rs | 4 + crates/token_search/src/token_search.rs | 13 +-- src/bin/codebase_files.rs | 2 +- 17 files changed, 343 insertions(+), 33 deletions(-) create mode 100644 crates/cli/src/doctor.rs create mode 100644 crates/cli/src/doctor/check_up.rs create mode 100644 crates/cli/src/doctor/files_count.rs create mode 100644 crates/cli/src/doctor/loaded_project_configurations.rs create mode 100644 crates/cli/src/doctor/tags_included_in_files_searched.rs create mode 100644 crates/cli/src/doctor/tokens_count.rs create mode 100644 crates/cli/src/doctor/using_universal_ctags.rs create mode 100644 crates/cli/src/project_configurations_loader.rs diff --git a/Cargo.lock b/Cargo.lock index 50dbe92..47e5296 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,7 @@ dependencies = [ name = "cli" version = "0.1.0" dependencies = [ + "codebase_files", "colored", "dirs", "itertools", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 5ecb7ae..6a3fdca 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" [dependencies] serde_json = "1.0.50" +codebase_files = { path = "../../crates/codebase_files/" } read_ctags = { path = "../../crates/read_ctags/" } token_search = { path = "../../crates/token_search/" } token_analysis = { path = "../../crates/token_analysis/" } diff --git a/crates/cli/src/cli_configuration.rs b/crates/cli/src/cli_configuration.rs index d45ef28..b579560 100644 --- a/crates/cli/src/cli_configuration.rs +++ b/crates/cli/src/cli_configuration.rs @@ -1,13 +1,10 @@ use super::analyzed_token::AnalyzedToken; use super::formatters; +use super::project_configurations_loader::load_and_parse_config; use super::{Flags, Format}; -use dirs; -use project_configuration::{AssertionConflict, ProjectConfiguration, ProjectConfigurations}; +use project_configuration::{AssertionConflict, ProjectConfiguration}; use std::collections::{HashMap, HashSet}; -use std::fs; -use std::io; use std::iter::FromIterator; -use std::path::Path; use token_analysis::{ AnalysisFilter, SortOrder, TokenUsage, TokenUsageResults, UsageLikelihoodStatus, }; @@ -120,23 +117,6 @@ impl CliConfiguration { } } -fn file_path_in_home_dir(file_name: &str) -> Option { - dirs::home_dir().and_then(|ref p| Path::new(p).join(file_name).to_str().map(|v| v.to_owned())) -} - -fn load_and_parse_config() -> ProjectConfigurations { - let contents = file_path_in_home_dir(".config/unused/unused.yml") - .and_then(|path| read_file(&path).ok()) - .unwrap_or(ProjectConfigurations::default_yaml()); - ProjectConfigurations::parse(&contents) -} - -fn read_file(filename: &str) -> Result { - let contents = fs::read_to_string(filename)?; - - Ok(contents) -} - fn build_token_search_config(cmd: &Flags, token_results: &[Token]) -> TokenSearchConfig { let mut search_config = TokenSearchConfig::default(); search_config.tokens = token_results.to_vec(); diff --git a/crates/cli/src/doctor.rs b/crates/cli/src/doctor.rs new file mode 100644 index 0000000..f6999b7 --- /dev/null +++ b/crates/cli/src/doctor.rs @@ -0,0 +1,86 @@ +mod check_up; +mod files_count; +mod loaded_project_configurations; +mod tags_included_in_files_searched; +mod tokens_count; +mod using_universal_ctags; + +use super::doctor::{ + check_up::*, files_count::*, loaded_project_configurations::*, + tags_included_in_files_searched::*, tokens_count::*, using_universal_ctags::*, +}; +use colored::*; + +pub struct Doctor { + checks: Vec>, +} + +impl Doctor { + pub fn new() -> Self { + Self { + checks: vec![ + Box::new(IncludingTagsInFilesSearched::new()), + Box::new(TokensCount::new()), + Box::new(FilesCount::new()), + Box::new(UsingUniversalCtags::new()), + Box::new(LoadedProjectConfigurations::new()), + ], + } + } + + pub fn render(&self) { + println!("Unused Doctor"); + println!(""); + + let mut oks = 0; + let mut warnings = 0; + let mut errors = 0; + + for check in self.checks.iter() { + match check.status() { + Status::OK(_) => oks += 1, + Status::Warn(_) => warnings += 1, + Status::Error(_) => errors += 1, + } + + Self::render_check_up(check) + } + + println!(""); + println!( + "{}: {}, {}, {}", + Self::colorized_outcome(warnings, errors), + format!("{} OK", oks).green(), + format!("{} warnings", warnings).yellow(), + format!("{} errors", errors).red(), + ); + } + + fn colorized_outcome(warnings: u16, errors: u16) -> colored::ColoredString { + if errors > 0 { + "Outcome".red() + } else { + if warnings > 0 { + "Outcome".yellow() + } else { + "Outcome".green() + } + } + } + + fn render_check_up(check_up: &Box) { + match check_up.status() { + Status::OK(message) => Self::render_status("OK".green(), check_up.name(), message), + Status::Warn(message) => { + Self::render_status("Warning".yellow(), check_up.name(), message) + } + Status::Error(message) => Self::render_status("Error".red(), check_up.name(), message), + } + } + + fn render_status(status: colored::ColoredString, name: &str, message: String) { + print!("[{}] ", status); + println!("Check: {}", name.cyan()); + println!(" {}", message.yellow()); + } +} diff --git a/crates/cli/src/doctor/check_up.rs b/crates/cli/src/doctor/check_up.rs new file mode 100644 index 0000000..7f03bd4 --- /dev/null +++ b/crates/cli/src/doctor/check_up.rs @@ -0,0 +1,10 @@ +pub enum Status { + OK(String), + Warn(String), + Error(String), +} + +pub trait CheckUp { + fn name(&self) -> &str; + fn status(&self) -> Status; +} diff --git a/crates/cli/src/doctor/files_count.rs b/crates/cli/src/doctor/files_count.rs new file mode 100644 index 0000000..a4abf04 --- /dev/null +++ b/crates/cli/src/doctor/files_count.rs @@ -0,0 +1,26 @@ +use super::check_up::{CheckUp, Status}; +use codebase_files::CodebaseFiles; + +pub struct FilesCount(usize); + +impl FilesCount { + pub fn new() -> Self { + let file_paths = CodebaseFiles::all().paths; + Self(file_paths.len()) + } +} + +impl CheckUp for FilesCount { + fn name(&self) -> &str { + "Are files found in the application?" + } + + fn status(&self) -> Status { + let message = format!("{} file(s) found", self.0); + if self.0 == 0 { + Status::Warn(message) + } else { + Status::OK(message) + } + } +} diff --git a/crates/cli/src/doctor/loaded_project_configurations.rs b/crates/cli/src/doctor/loaded_project_configurations.rs new file mode 100644 index 0000000..d31194a --- /dev/null +++ b/crates/cli/src/doctor/loaded_project_configurations.rs @@ -0,0 +1,36 @@ +use super::{ + super::project_configurations_loader::load_and_parse_config, + check_up::{CheckUp, Status}, +}; +use project_configuration::ProjectConfigurations; + +pub struct LoadedProjectConfigurations(ProjectConfigurations); + +impl LoadedProjectConfigurations { + pub fn new() -> Self { + LoadedProjectConfigurations(load_and_parse_config()) + } + + fn config_keys(&self) -> Vec { + self.0.project_config_names() + } +} + +impl CheckUp for LoadedProjectConfigurations { + fn name(&self) -> &str { + "Does the loaded configuration have available project types?" + } + + fn status(&self) -> Status { + if self.config_keys().is_empty() { + Status::Warn( + "No project configurations were loaded; using default config instead.".to_string(), + ) + } else { + Status::OK(format!( + "Loaded the following project configurations: {}", + self.config_keys().join(", ") + )) + } + } +} diff --git a/crates/cli/src/doctor/tags_included_in_files_searched.rs b/crates/cli/src/doctor/tags_included_in_files_searched.rs new file mode 100644 index 0000000..d0164a5 --- /dev/null +++ b/crates/cli/src/doctor/tags_included_in_files_searched.rs @@ -0,0 +1,54 @@ +use super::check_up::{CheckUp, Status}; +use codebase_files::CodebaseFiles; +use std::path::PathBuf; +use token_search::Token; + +pub enum IncludingTagsInFilesSearched { + Success { + ctags_path: PathBuf, + files_searched: Vec, + }, + Failure(String), +} + +impl IncludingTagsInFilesSearched { + pub fn new() -> Self { + match Token::all() { + Ok((ctags_path, _)) => IncludingTagsInFilesSearched::Success { + files_searched: CodebaseFiles::all().paths, + ctags_path, + }, + Err(e) => IncludingTagsInFilesSearched::Failure(format!("{}", e)), + } + } + + fn tags_searched(&self) -> Result<(&PathBuf, bool), String> { + match &self { + Self::Success { + files_searched, + ctags_path, + } => Ok((ctags_path, files_searched.iter().any(|v| v == ctags_path))), + Self::Failure(e) => Err(e.to_string()), + } + } +} + +impl CheckUp for IncludingTagsInFilesSearched { + fn name(&self) -> &str { + "Is the tags file not present in the list of files searched?" + } + + fn status(&self) -> Status { + match self.tags_searched() { + Ok((ctags_path, true)) => Status::Warn(format!( + "The tags file loaded ({:?}) is present in the list of files searched", + ctags_path + )), + Ok((ctags_path, false)) => Status::OK(format!( + "The tags file loaded ({:?}) is not present in the list of files searched", + ctags_path + )), + Err(e) => Status::Error(e), + } + } +} diff --git a/crates/cli/src/doctor/tokens_count.rs b/crates/cli/src/doctor/tokens_count.rs new file mode 100644 index 0000000..400e16d --- /dev/null +++ b/crates/cli/src/doctor/tokens_count.rs @@ -0,0 +1,36 @@ +use super::check_up::{CheckUp, Status}; +use token_search::Token; + +pub enum TokensCount { + Success(usize), + Failure(String), +} + +impl TokensCount { + pub fn new() -> Self { + match Token::all() { + Ok((_, results)) => Self::Success(results.len()), + Err(e) => Self::Failure(format!("{}", e)), + } + } +} + +impl CheckUp for TokensCount { + fn name(&self) -> &str { + "Are tokens found in the application?" + } + + fn status(&self) -> Status { + match &self { + Self::Success(ct) => { + let message = format!("{} token(s) found", ct); + if ct < &5 { + Status::Warn(message) + } else { + Status::OK(message) + } + } + Self::Failure(e) => Status::Error(e.to_string()), + } + } +} diff --git a/crates/cli/src/doctor/using_universal_ctags.rs b/crates/cli/src/doctor/using_universal_ctags.rs new file mode 100644 index 0000000..38ec525 --- /dev/null +++ b/crates/cli/src/doctor/using_universal_ctags.rs @@ -0,0 +1,33 @@ +use super::check_up::{CheckUp, Status}; +use read_ctags::TagsReader; + +pub struct UsingUniversalCtags(Option); + +impl UsingUniversalCtags { + pub fn new() -> Self { + match TagsReader::default().load() { + Ok(outcome) => Self(outcome.program.name), + Err(_) => Self(None), + } + } +} + +impl CheckUp for UsingUniversalCtags { + fn name(&self) -> &str { + "Is the tags file generated with Universal Ctags?" + } + + fn status(&self) -> Status { + match &self.0 { + None => Status::Error("Could not determine tags program name".to_string()), + Some(v) => { + let message = format!("Using tags program: {}", v); + if v.contains("Universal Ctags") { + Status::OK(message) + } else { + Status::Warn(message) + } + } + } + } +} diff --git a/crates/cli/src/flags.rs b/crates/cli/src/flags.rs index e77e5a1..b6c7a4d 100644 --- a/crates/cli/src/flags.rs +++ b/crates/cli/src/flags.rs @@ -3,6 +3,12 @@ use std::str::FromStr; use structopt::StructOpt; use token_analysis::{OrderField, UsageLikelihoodStatus}; +#[derive(Debug, StructOpt)] +pub enum Command { + /// Run diagnostics to identify any potential issues running unused + Doctor, +} + #[derive(Debug, StructOpt)] #[structopt( name = "unused-rs", @@ -61,6 +67,9 @@ pub struct Flags { /// This supports providing multiple values with a comma-delimited list #[structopt(long, use_delimiter = true)] pub ignore: Vec, + + #[structopt(subcommand)] + pub cmd: Option, } #[derive(Debug)] diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 93613e3..5ec17a8 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,11 +1,14 @@ mod analyzed_token; mod cli_configuration; +mod doctor; mod error_message; mod flags; mod formatters; +mod project_configurations_loader; use cli_configuration::CliConfiguration; use colored::*; +use doctor::Doctor; use flags::{Flags, Format}; use structopt::StructOpt; use token_search::Token; @@ -21,8 +24,11 @@ pub fn run() { control::set_override(false); } - match Token::all() { - Ok((_, results)) => CliConfiguration::new(flags, &results).render(), - Err(e) => error_message::failed_token_parse(e), + match flags.cmd { + Some(flags::Command::Doctor) => Doctor::new().render(), + _ => match Token::all() { + Ok((_, results)) => CliConfiguration::new(flags, &results).render(), + Err(e) => error_message::failed_token_parse(e), + }, } } diff --git a/crates/cli/src/project_configurations_loader.rs b/crates/cli/src/project_configurations_loader.rs new file mode 100644 index 0000000..593d482 --- /dev/null +++ b/crates/cli/src/project_configurations_loader.rs @@ -0,0 +1,22 @@ +use dirs; +use project_configuration::ProjectConfigurations; +use std::fs; +use std::io; +use std::path::Path; + +pub fn load_and_parse_config() -> ProjectConfigurations { + let contents = file_path_in_home_dir(".config/unused/unused.yml") + .and_then(|path| read_file(&path).ok()) + .unwrap_or(ProjectConfigurations::default_yaml()); + ProjectConfigurations::parse(&contents) +} + +fn file_path_in_home_dir(file_name: &str) -> Option { + dirs::home_dir().and_then(|ref p| Path::new(p).join(file_name).to_str().map(|v| v.to_owned())) +} + +fn read_file(filename: &str) -> Result { + let contents = fs::read_to_string(filename)?; + + Ok(contents) +} diff --git a/crates/codebase_files/src/lib.rs b/crates/codebase_files/src/lib.rs index 7671ca5..6a12314 100644 --- a/crates/codebase_files/src/lib.rs +++ b/crates/codebase_files/src/lib.rs @@ -1,7 +1,8 @@ +use std::path::PathBuf; use std::process::{Command, Output}; pub struct CodebaseFiles { - pub paths: Vec, + pub paths: Vec, } impl CodebaseFiles { @@ -18,7 +19,9 @@ impl CodebaseFiles { paths.sort(); paths.dedup(); - CodebaseFiles { paths } + CodebaseFiles { + paths: paths.into_iter().map(PathBuf::from).collect(), + } } fn process_ls_files(output: Result) -> Vec { diff --git a/crates/project_configuration/src/loader.rs b/crates/project_configuration/src/loader.rs index c8802ec..69e047f 100644 --- a/crates/project_configuration/src/loader.rs +++ b/crates/project_configuration/src/loader.rs @@ -45,6 +45,10 @@ impl ProjectConfigurations { ProjectConfigurations { configs } } + pub fn project_config_names(&self) -> Vec { + self.configs.keys().map(|v| v.to_owned()).collect() + } + pub fn best_match(&self, results: &TokenSearchResults) -> Option { self.configs .iter() diff --git a/crates/token_search/src/token_search.rs b/crates/token_search/src/token_search.rs index a3941e5..1d8ef6e 100644 --- a/crates/token_search/src/token_search.rs +++ b/crates/token_search/src/token_search.rs @@ -13,6 +13,7 @@ use std::convert::TryInto; use std::fs; use std::io; use std::iter::FromIterator; +use std::path::PathBuf; /// A TokenSearchConfig is necessary to construct the list of tokens and files to search against /// when generating results. @@ -25,7 +26,7 @@ pub struct TokenSearchConfig { /// Tokens to be used when searching pub tokens: Vec, /// Filenames to search against - pub files: Vec, + pub files: Vec, /// Should a progress bar be displayed? pub display_progress: bool, /// Restrict languages searched (based on file extension) @@ -181,7 +182,9 @@ impl TokenSearchResults { { let file_with_occurrences = results.entry(key).or_insert(HashMap::new()); - file_with_occurrences.insert(f.to_string(), res); + if let Some(fp) = f.to_str() { + file_with_occurrences.insert(fp.to_string(), res); + } } results @@ -207,10 +210,10 @@ impl TokenSearchResults { TokenSearchResults(final_results) } - fn load_all_files(filenames: &[String]) -> HashMap<&str, String> { + fn load_all_files(filenames: &[PathBuf]) -> HashMap<&PathBuf, String> { filenames .par_iter() - .fold(HashMap::new, |mut acc: HashMap<&str, String>, f| { + .fold(HashMap::new, |mut acc: HashMap<&PathBuf, String>, f| { if let Ok(contents) = Self::read_file(&f) { acc.insert(&f, contents); } @@ -225,7 +228,7 @@ impl TokenSearchResults { }) } - fn read_file(filename: &str) -> Result { + fn read_file(filename: &PathBuf) -> Result { let contents = fs::read_to_string(filename)?; Ok(contents) diff --git a/src/bin/codebase_files.rs b/src/bin/codebase_files.rs index 0f82430..67a2454 100644 --- a/src/bin/codebase_files.rs +++ b/src/bin/codebase_files.rs @@ -2,7 +2,7 @@ use codebase_files::CodebaseFiles; fn main() { let files = CodebaseFiles::all(); - for f in files.paths.iter() { + for f in files.paths.iter().filter_map(|v| v.to_str()) { println!("{}", f); } }