oxc/crates/oxc_cli/src/runner.rs
Boshen 3110490f36
refactor(cli,linter): move LintOptions from cli to linter (#753)
This also simplifies the Runner trait.
2023-08-17 22:28:34 +08:00

176 lines
5 KiB
Rust

use std::{
path::PathBuf,
process::{ExitCode, Termination},
};
use clap::{ArgMatches, Command};
/// A trait for exposing functionality to the CLI.
pub trait Runner: Send + Sync {
/// The name of the runner. Used to create a subcommand.
const NAME: &'static str;
/// A short description about what the runner does
const ABOUT: &'static str;
fn command() -> Command {
let mut command = Self::init_command();
if command.get_name().is_empty() {
command = command.name(Self::NAME);
}
if command.get_about().is_none() {
command = command.about(Self::ABOUT);
}
command
}
fn new(matches: &ArgMatches) -> Self;
fn init_command() -> Command;
/// Executes the runner, providing some result to the CLI.
fn run(&self) -> CliRunResult;
}
#[derive(Debug)]
pub enum CliRunResult {
None,
IOError(crate::lint::Error),
PathNotFound {
paths: Vec<PathBuf>,
},
LintResult {
duration: std::time::Duration,
number_of_rules: usize,
number_of_files: usize,
number_of_warnings: usize,
number_of_errors: usize,
max_warnings_exceeded: bool,
},
TypeCheckResult {
duration: std::time::Duration,
number_of_diagnostics: usize,
},
}
impl Termination for CliRunResult {
fn report(self) -> ExitCode {
match self {
Self::None => ExitCode::from(0),
Self::PathNotFound { paths } => {
println!("Path {paths:?} does not exist.");
ExitCode::from(1)
}
Self::IOError(e) => {
println!("IO Error: {e}");
ExitCode::from(1)
}
Self::LintResult {
duration,
number_of_rules,
number_of_files,
number_of_warnings,
number_of_errors,
max_warnings_exceeded,
} => {
let ms = duration.as_millis();
let threads = rayon::current_num_threads();
let number_of_diagnostics = number_of_warnings + number_of_errors;
if number_of_diagnostics > 0 {
println!();
}
println!(
"Finished in {ms}ms on {number_of_files} files with {number_of_rules} rules using {threads} threads."
);
if max_warnings_exceeded {
println!("Exceeded maximum number of warnings. Found {number_of_warnings}.");
return ExitCode::from(1);
}
if number_of_diagnostics > 0 {
let warnings = if number_of_warnings == 1 { "warning" } else { "warnings" };
let errors = if number_of_errors == 1 { "error" } else { "errors" };
println!(
"Found {number_of_warnings} {warnings} and {number_of_errors} {errors}."
);
return ExitCode::from(1);
}
// eslint does not print anything after success, so we do the same.
// It is also standard to not print anything after success in the *nix world.
ExitCode::from(0)
}
Self::TypeCheckResult { duration, number_of_diagnostics } => {
let ms = duration.as_millis();
println!("Finished in {ms}ms.");
if number_of_diagnostics > 0 {
println!("Found {number_of_diagnostics} errors.");
return ExitCode::from(1);
}
ExitCode::from(0)
}
}
}
}
#[cfg(test)]
mod tests {
use clap::{Arg, ArgAction, ArgMatches, Command};
use super::*;
#[derive(Debug)]
struct TestRunnerOptions {
foo: bool,
}
#[derive(Debug)]
struct TestRunner {
options: TestRunnerOptions,
}
impl<'a> From<&'a ArgMatches> for TestRunnerOptions {
fn from(value: &'a ArgMatches) -> Self {
let foo = value.get_flag("foo");
Self { foo }
}
}
impl Runner for TestRunner {
const ABOUT: &'static str = "some description";
const NAME: &'static str = "test";
fn init_command() -> Command {
Command::new("")
.arg(Arg::new("foo").short('f').action(ArgAction::SetTrue).required(false))
}
fn new(matches: &ArgMatches) -> Self {
Self { options: TestRunnerOptions::from(matches) }
}
fn run(&self) -> CliRunResult {
CliRunResult::None
}
}
#[test]
fn check_cmd_validity() {
TestRunner::command().debug_assert();
}
#[test]
fn smoke_test_runner() {
let cmd = TestRunner::command();
let matches = cmd.get_matches_from("test -f".split(' '));
let runner = TestRunner::new(&matches);
assert!(runner.options.foo);
let result = runner.run();
assert!(matches!(result, CliRunResult::None));
}
}