Skip to content

Commit

Permalink
Introduce a 'doctor' subcommand
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
joshuaclayton committed May 23, 2020
1 parent 9ce77a3 commit c7ca2db
Show file tree
Hide file tree
Showing 17 changed files with 343 additions and 33 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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/" }
Expand Down
24 changes: 2 additions & 22 deletions crates/cli/src/cli_configuration.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -120,23 +117,6 @@ impl CliConfiguration {
}
}

fn file_path_in_home_dir(file_name: &str) -> Option<String> {
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<String, io::Error> {
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();
Expand Down
86 changes: 86 additions & 0 deletions crates/cli/src/doctor.rs
Original file line number Diff line number Diff line change
@@ -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<Box<dyn CheckUp>>,
}

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<dyn CheckUp>) {
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());
}
}
10 changes: 10 additions & 0 deletions crates/cli/src/doctor/check_up.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
pub enum Status {
OK(String),
Warn(String),
Error(String),
}

pub trait CheckUp {
fn name(&self) -> &str;
fn status(&self) -> Status;
}
26 changes: 26 additions & 0 deletions crates/cli/src/doctor/files_count.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
36 changes: 36 additions & 0 deletions crates/cli/src/doctor/loaded_project_configurations.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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(", ")
))
}
}
}
54 changes: 54 additions & 0 deletions crates/cli/src/doctor/tags_included_in_files_searched.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf>,
},
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),
}
}
}
36 changes: 36 additions & 0 deletions crates/cli/src/doctor/tokens_count.rs
Original file line number Diff line number Diff line change
@@ -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()),
}
}
}
33 changes: 33 additions & 0 deletions crates/cli/src/doctor/using_universal_ctags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use super::check_up::{CheckUp, Status};
use read_ctags::TagsReader;

pub struct UsingUniversalCtags(Option<String>);

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)
}
}
}
}
}
9 changes: 9 additions & 0 deletions crates/cli/src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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<String>,

#[structopt(subcommand)]
pub cmd: Option<Command>,
}

#[derive(Debug)]
Expand Down
12 changes: 9 additions & 3 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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),
},
}
}
Loading

0 comments on commit c7ca2db

Please sign in to comment.