mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
refactor(cli): move lint command and options into modules
This commit is contained in:
parent
0ec5f0b111
commit
5135118bc8
8 changed files with 387 additions and 427 deletions
|
|
@ -1,172 +0,0 @@
|
|||
use clap::{builder::ValueParser, Arg, ArgAction, Command};
|
||||
|
||||
#[must_use]
|
||||
pub fn command() -> Command {
|
||||
Command::new("oxc")
|
||||
.bin_name("oxc")
|
||||
.version("alpha")
|
||||
.author("Boshen")
|
||||
.about("The JavaScript Oxidation Compiler")
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(lint_subcommand())
|
||||
}
|
||||
|
||||
fn lint_subcommand() -> Command {
|
||||
Command::new("lint")
|
||||
.alias("check")
|
||||
.about("Lint this repository.")
|
||||
.arg_required_else_help(true)
|
||||
.arg(
|
||||
Arg::new("fix")
|
||||
.long("fix")
|
||||
.required(false)
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("This option allows you to enable oxc to fix as many issues as possible. If enabled, only unfixed issues are reported in the output")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("quiet")
|
||||
.long("quiet")
|
||||
.required(false)
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("This option allows you to disable reporting on warnings. If you enable this option, only errors are reported by oxc_lint.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore-path")
|
||||
.long("ignore-path")
|
||||
.required(false)
|
||||
.help("This option allows you to specify the file to use as your .eslintignore.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-ignore")
|
||||
.long("no-ignore")
|
||||
.required(false)
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Disables excluding of files from .eslintignore files, --ignore-path flags, --ignore-pattern flags.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore-pattern")
|
||||
.long("ignore-pattern")
|
||||
.required(false)
|
||||
.action(ArgAction::Append)
|
||||
.help("This option allows you to specify patterns of files to ignore (in addition to those in .eslintignore).")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("max-warnings")
|
||||
.long("max-warnings")
|
||||
.value_parser(clap::value_parser!(usize))
|
||||
.required(false)
|
||||
.help("This option allows you to specify a warning threshold, which can be used to force oxc_lint to exit with an error status if there are too many warning-level rule violations in your project.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("path")
|
||||
.value_name("PATH")
|
||||
.num_args(1..)
|
||||
.required(true)
|
||||
.help("File or Directory paths to scan. Directories are scanned recursively.")
|
||||
.value_parser(ValueParser::path_buf()),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::ArgMatches;
|
||||
|
||||
use super::command;
|
||||
|
||||
#[test]
|
||||
fn verify_command() {
|
||||
command().debug_assert();
|
||||
}
|
||||
|
||||
fn get_lint_matches(arg: &str) -> ArgMatches {
|
||||
let matches = command().try_get_matches_from(arg.split(' ')).unwrap();
|
||||
let matches = matches.subcommand_matches("lint");
|
||||
assert!(matches.is_some());
|
||||
matches.unwrap().clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_default() {
|
||||
let matches = get_lint_matches("oxc lint .");
|
||||
assert_eq!(matches.get_one::<PathBuf>("path"), Some(&PathBuf::from(".")));
|
||||
assert_eq!(matches.get_flag("fix"), false);
|
||||
assert_eq!(matches.get_flag("quiet"), false);
|
||||
assert_eq!(matches.get_one::<String>("ignore-path"), None);
|
||||
assert_eq!(matches.get_flag("no-ignore"), false);
|
||||
assert_eq!(matches.get_one::<String>("ignore-pattern"), None);
|
||||
assert_eq!(matches.get_one::<usize>("max-warnings"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_path() {
|
||||
let matches = get_lint_matches("oxc lint .");
|
||||
assert_eq!(matches.get_one::<PathBuf>("path"), Some(&PathBuf::from(".")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_multiple_paths() {
|
||||
let matches = get_lint_matches("oxc lint foo bar baz");
|
||||
assert_eq!(
|
||||
matches.get_many::<PathBuf>("path").unwrap().collect::<Vec<_>>(),
|
||||
[&PathBuf::from("foo"), &PathBuf::from("bar"), &PathBuf::from("baz")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_check_path() {
|
||||
let matches = get_lint_matches("oxc check /path/to/dir");
|
||||
assert_eq!(matches.get_one::<PathBuf>("path"), Some(&PathBuf::from("/path/to/dir")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_quiet_true() {
|
||||
let matches = get_lint_matches("oxc lint foo.js --quiet");
|
||||
assert!(matches.get_flag("quiet"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_fix_true() {
|
||||
let matches = get_lint_matches("oxc lint foo.js --fix");
|
||||
assert!(matches.get_flag("fix"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_max_warnings() {
|
||||
let matches = get_lint_matches("oxc lint --max-warnings 10 foo.js");
|
||||
assert_eq!(matches.get_one::<usize>("max-warnings"), Some(&10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_ignore_path() {
|
||||
let matches = get_lint_matches("oxc lint --ignore-path .gitignore foo.js");
|
||||
assert_eq!(matches.get_one::<String>("ignore-path"), Some(&".gitignore".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_no_ignore() {
|
||||
let matches = get_lint_matches("oxc lint --no-ignore foo.js");
|
||||
assert!(matches.get_flag("no-ignore"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_single_ignore_pattern() {
|
||||
let matches = get_lint_matches("oxc lint --ignore-pattern \"./test\" foo.js");
|
||||
assert_eq!(matches.get_one::<String>("ignore-pattern"), Some(&"\"./test\"".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lint_multiple_ignore_pattern() {
|
||||
let matches = get_lint_matches(
|
||||
"oxc lint --ignore-pattern \"./test\" --ignore-pattern \"bar.js\" foo.js",
|
||||
);
|
||||
let ignore_pattern = matches.get_many::<String>("ignore-pattern").unwrap();
|
||||
let mut compare = vec![];
|
||||
for pattern in ignore_pattern {
|
||||
compare.push(pattern);
|
||||
}
|
||||
assert_eq!(compare, vec!["\"./test\"", "\"bar.js\""]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,180 +1,25 @@
|
|||
mod command;
|
||||
// mod git;
|
||||
mod options;
|
||||
mod lint;
|
||||
mod result;
|
||||
mod walk;
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
io::{BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{mpsc, Arc},
|
||||
use clap::Command;
|
||||
|
||||
use crate::lint::lint_command;
|
||||
pub use crate::{
|
||||
lint::{LintOptions, LintRunner},
|
||||
result::CliRunResult,
|
||||
walk::Walk,
|
||||
};
|
||||
|
||||
use miette::NamedSource;
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::SourceType;
|
||||
use oxc_diagnostics::{Error, MinifiedFileError, Severity};
|
||||
use oxc_linter::{Fixer, Linter};
|
||||
use oxc_parser::Parser;
|
||||
use oxc_semantic::SemanticBuilder;
|
||||
|
||||
pub use crate::{command::command, options::CliOptions, result::CliRunResult, walk::Walk};
|
||||
|
||||
pub struct Cli {
|
||||
pub cli_options: CliOptions,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
#[must_use]
|
||||
pub fn new(cli_options: CliOptions) -> Self {
|
||||
Self { cli_options }
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// * When `mpsc::channel` fails to send.
|
||||
#[must_use]
|
||||
pub fn lint(&self) -> CliRunResult {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let (tx_error, rx_error) = mpsc::channel::<(PathBuf, Vec<Error>)>();
|
||||
let (tx_path, rx_path) = mpsc::channel::<Box<Path>>();
|
||||
|
||||
let mut number_of_files = 0;
|
||||
rayon::join(
|
||||
|| {
|
||||
let paths = self
|
||||
.cli_options
|
||||
.paths
|
||||
.iter()
|
||||
.flat_map(|path| Walk::new(path, &self.cli_options).iter())
|
||||
.filter(|path| {
|
||||
if self.cli_options.no_ignore {
|
||||
return true;
|
||||
}
|
||||
for pattern in &self.cli_options.ignore_pattern {
|
||||
if pattern.matches_path(path) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
for path in paths {
|
||||
number_of_files += 1;
|
||||
tx_path.send(path).unwrap();
|
||||
}
|
||||
drop(tx_path);
|
||||
},
|
||||
move || {
|
||||
let fix = self.cli_options.fix;
|
||||
while let Ok(path) = rx_path.recv() {
|
||||
let tx_error = tx_error.clone();
|
||||
rayon::spawn(move || {
|
||||
if let Some(diagnostics) = Self::lint_path(&path, fix) {
|
||||
tx_error.send(diagnostics).unwrap();
|
||||
}
|
||||
drop(tx_error);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let mut buf_writer = BufWriter::new(std::io::stdout());
|
||||
let mut number_of_warnings = 0;
|
||||
let mut number_of_diagnostics = 0;
|
||||
|
||||
while let Ok((path, diagnostics)) = rx_error.recv() {
|
||||
number_of_diagnostics += diagnostics.len();
|
||||
for diagnostic in diagnostics {
|
||||
if diagnostic.severity() == Some(Severity::Warning) {
|
||||
number_of_warnings += 1;
|
||||
// The --quiet flag follows ESLint's --quiet behavior as documented here: https://eslint.org/docs/latest/use/command-line-interface#--quiet
|
||||
// Note that it does not disable ALL diagnostics, only Warning diagnostics
|
||||
if self.cli_options.quiet {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(max_warnings) = self.cli_options.max_warnings {
|
||||
if number_of_warnings > max_warnings {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = format!("{diagnostic:?}");
|
||||
// Skip large output and print only once
|
||||
if output.lines().any(|line| line.len() >= 400) {
|
||||
let minified_diagnostic = Error::new(MinifiedFileError(path.clone()));
|
||||
buf_writer.write_all(format!("{minified_diagnostic:?}").as_bytes()).unwrap();
|
||||
break;
|
||||
}
|
||||
buf_writer.write_all(output.as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
buf_writer.flush().unwrap();
|
||||
|
||||
CliRunResult::LintResult {
|
||||
duration: now.elapsed(),
|
||||
number_of_files,
|
||||
number_of_diagnostics,
|
||||
number_of_warnings,
|
||||
max_warnings_exceeded: self
|
||||
.cli_options
|
||||
.max_warnings
|
||||
.map_or(false, |max_warnings| number_of_warnings > max_warnings),
|
||||
}
|
||||
}
|
||||
|
||||
fn lint_path(path: &Path, fix: bool) -> Option<(PathBuf, Vec<Error>)> {
|
||||
let source_text = fs::read_to_string(path).unwrap_or_else(|_| panic!("{path:?} not found"));
|
||||
let allocator = Allocator::default();
|
||||
let source_type =
|
||||
SourceType::from_path(path).unwrap_or_else(|_| panic!("incorrect {path:?}"));
|
||||
let parser_source_text = source_text.clone();
|
||||
let ret = Parser::new(&allocator, &parser_source_text, source_type).parse();
|
||||
|
||||
if !ret.errors.is_empty() {
|
||||
return Some(Self::wrap_diagnostics(path, &source_text, ret.errors));
|
||||
};
|
||||
|
||||
let program = allocator.alloc(ret.program);
|
||||
let trivias = Rc::new(ret.trivias);
|
||||
let semantic_ret = SemanticBuilder::new(source_type).build(program, &trivias);
|
||||
|
||||
if !semantic_ret.errors.is_empty() {
|
||||
return Some(Self::wrap_diagnostics(path, &source_text, semantic_ret.errors));
|
||||
};
|
||||
|
||||
let result = Linter::new().with_fix(fix).run(&Rc::new(semantic_ret.semantic), &source_text);
|
||||
|
||||
if result.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if fix {
|
||||
let fix_result = Fixer::new(&source_text, result).fix();
|
||||
fs::write(path, fix_result.fixed_code.as_bytes()).unwrap();
|
||||
let errors = fix_result.messages.into_iter().map(|m| m.error).collect();
|
||||
return Some(Self::wrap_diagnostics(path, &source_text, errors));
|
||||
}
|
||||
|
||||
let errors = result.into_iter().map(|diagnostic| diagnostic.error).collect();
|
||||
Some(Self::wrap_diagnostics(path, &source_text, errors))
|
||||
}
|
||||
|
||||
fn wrap_diagnostics(
|
||||
path: &Path,
|
||||
source_text: &str,
|
||||
diagnostics: Vec<Error>,
|
||||
) -> (PathBuf, Vec<Error>) {
|
||||
let source = Arc::new(NamedSource::new(path.to_string_lossy(), source_text.to_owned()));
|
||||
let diagnostics = diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source)))
|
||||
.collect();
|
||||
(path.to_path_buf(), diagnostics)
|
||||
}
|
||||
#[must_use]
|
||||
pub fn command() -> Command {
|
||||
Command::new("oxc")
|
||||
.bin_name("oxc")
|
||||
.version("alpha")
|
||||
.author("Boshen")
|
||||
.about("The JavaScript Oxidation Compiler")
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(lint_command())
|
||||
}
|
||||
|
|
|
|||
59
crates/oxc_cli/src/lint/command.rs
Normal file
59
crates/oxc_cli/src/lint/command.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use clap::{builder::ValueParser, Arg, ArgAction, Command};
|
||||
|
||||
pub fn lint_command() -> Command {
|
||||
Command::new("lint")
|
||||
.alias("check")
|
||||
.about("Lint this repository.")
|
||||
.arg_required_else_help(true)
|
||||
.arg(
|
||||
Arg::new("path")
|
||||
.value_name("PATH")
|
||||
.num_args(1..)
|
||||
.required(true)
|
||||
.value_parser(ValueParser::path_buf())
|
||||
.help("File or Directory paths to scan. Directories are scanned recursively.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("fix")
|
||||
.long("fix")
|
||||
.required(false)
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("This option allows you to enable oxc to fix as many issues as possible. If enabled, only unfixed issues are reported in the output")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("quiet")
|
||||
.long("quiet")
|
||||
.required(false)
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("This option allows you to disable reporting on warnings. If you enable this option, only errors are reported by oxc_lint.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore-path")
|
||||
.long("ignore-path")
|
||||
.required(false)
|
||||
.value_parser(ValueParser::path_buf())
|
||||
.help("This option allows you to specify the file to use as your .eslintignore.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("no-ignore")
|
||||
.long("no-ignore")
|
||||
.required(false)
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Disables excluding of files from .eslintignore files, --ignore-path flags, --ignore-pattern flags.")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("ignore-pattern")
|
||||
.long("ignore-pattern")
|
||||
.required(false)
|
||||
.action(ArgAction::Append)
|
||||
.help("This option allows you to specify patterns of files to ignore (in addition to those in .eslintignore).")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("max-warnings")
|
||||
.long("max-warnings")
|
||||
.value_parser(clap::value_parser!(usize))
|
||||
.default_value(None)
|
||||
.required(false)
|
||||
.help("This option allows you to specify a warning threshold, which can be used to force oxc_lint to exit with an error status if there are too many warning-level rule violations in your project.")
|
||||
)
|
||||
}
|
||||
120
crates/oxc_cli/src/lint/mod.rs
Normal file
120
crates/oxc_cli/src/lint/mod.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
use std::path::PathBuf;
|
||||
mod command;
|
||||
mod runner;
|
||||
|
||||
use clap::ArgMatches;
|
||||
|
||||
pub use self::{command::lint_command, runner::LintRunner};
|
||||
|
||||
pub struct LintOptions {
|
||||
pub paths: Vec<PathBuf>,
|
||||
pub fix: bool,
|
||||
pub quiet: bool,
|
||||
pub ignore_path: PathBuf,
|
||||
pub no_ignore: bool,
|
||||
pub ignore_pattern: Vec<String>,
|
||||
pub max_warnings: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ArgMatches> for LintOptions {
|
||||
fn from(matches: &'a ArgMatches) -> Self {
|
||||
Self {
|
||||
paths: matches.get_many("path").map_or_else(
|
||||
|| vec![PathBuf::from(".")],
|
||||
|paths| paths.into_iter().cloned().collect(),
|
||||
),
|
||||
fix: matches.get_flag("fix"),
|
||||
quiet: matches.get_flag("quiet"),
|
||||
ignore_path: matches
|
||||
.get_one::<PathBuf>("ignore-path")
|
||||
.map_or_else(|| PathBuf::from(".eslintignore"), Clone::clone),
|
||||
no_ignore: matches.get_flag("no-ignore"),
|
||||
ignore_pattern: matches
|
||||
.get_many::<String>("ignore-pattern")
|
||||
.map(|patterns| patterns.into_iter().cloned().collect())
|
||||
.unwrap_or_default(),
|
||||
max_warnings: matches.get_one("max-warnings").copied(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{lint_command, LintOptions};
|
||||
|
||||
#[test]
|
||||
fn verify_command() {
|
||||
lint_command().debug_assert();
|
||||
}
|
||||
|
||||
fn get_lint_options(arg: &str) -> LintOptions {
|
||||
let matches = lint_command().try_get_matches_from(arg.split(' ')).unwrap();
|
||||
LintOptions::from(&matches)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
let options = get_lint_options("lint .");
|
||||
assert_eq!(options.paths, vec![PathBuf::from(".")]);
|
||||
assert!(!options.fix);
|
||||
assert!(!options.quiet);
|
||||
assert_eq!(options.ignore_path, PathBuf::from(".eslintignore"));
|
||||
assert!(!options.no_ignore);
|
||||
assert!(options.ignore_pattern.is_empty());
|
||||
assert_eq!(options.max_warnings, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_paths() {
|
||||
let options = get_lint_options("lint foo bar baz");
|
||||
assert_eq!(
|
||||
options.paths,
|
||||
[PathBuf::from("foo"), PathBuf::from("bar"), PathBuf::from("baz")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quiet_true() {
|
||||
let options = get_lint_options("lint foo.js --quiet");
|
||||
assert!(options.quiet);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fix_true() {
|
||||
let options = get_lint_options("lint foo.js --fix");
|
||||
assert!(options.fix);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_warnings() {
|
||||
let options = get_lint_options("lint --max-warnings 10 foo.js");
|
||||
assert_eq!(options.max_warnings, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignore_path() {
|
||||
let options = get_lint_options("lint --ignore-path .xxx foo.js");
|
||||
assert_eq!(options.ignore_path, PathBuf::from(".xxx"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_ignore() {
|
||||
let options = get_lint_options("lint --no-ignore foo.js");
|
||||
assert!(options.no_ignore);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_ignore_pattern() {
|
||||
let options = get_lint_options("lint --ignore-pattern ./test foo.js");
|
||||
assert_eq!(options.ignore_pattern, vec![String::from("./test")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_ignore_pattern() {
|
||||
let options =
|
||||
get_lint_options("lint --ignore-pattern ./test --ignore-pattern bar.js foo.js");
|
||||
assert_eq!(options.ignore_pattern, vec![String::from("./test"), String::from("bar.js")]);
|
||||
}
|
||||
}
|
||||
178
crates/oxc_cli/src/lint/runner.rs
Normal file
178
crates/oxc_cli/src/lint/runner.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
use std::{
|
||||
fs,
|
||||
io::{BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{mpsc, Arc},
|
||||
};
|
||||
|
||||
use miette::NamedSource;
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_ast::SourceType;
|
||||
use oxc_diagnostics::{Error, MinifiedFileError, Severity};
|
||||
use oxc_linter::{Fixer, Linter};
|
||||
use oxc_parser::Parser;
|
||||
use oxc_semantic::SemanticBuilder;
|
||||
|
||||
use super::LintOptions;
|
||||
use crate::{CliRunResult, Walk};
|
||||
|
||||
pub struct LintRunner {
|
||||
options: LintOptions,
|
||||
}
|
||||
|
||||
impl LintRunner {
|
||||
#[must_use]
|
||||
pub fn new(options: LintOptions) -> Self {
|
||||
Self { options }
|
||||
}
|
||||
|
||||
/// # Panics
|
||||
///
|
||||
/// * When `mpsc::channel` fails to send.
|
||||
#[must_use]
|
||||
pub fn run(&self) -> CliRunResult {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let mut number_of_files = 0;
|
||||
let mut number_of_warnings = 0;
|
||||
let mut number_of_diagnostics = 0;
|
||||
|
||||
let (tx_error, rx_error) = mpsc::channel::<(PathBuf, Vec<Error>)>();
|
||||
|
||||
self.process_paths(&mut number_of_files, tx_error);
|
||||
self.process_diagnostics(&mut number_of_warnings, &mut number_of_diagnostics, &rx_error);
|
||||
|
||||
CliRunResult::LintResult {
|
||||
duration: now.elapsed(),
|
||||
number_of_files,
|
||||
number_of_diagnostics,
|
||||
number_of_warnings,
|
||||
max_warnings_exceeded: self
|
||||
.options
|
||||
.max_warnings
|
||||
.map_or(false, |max_warnings| number_of_warnings > max_warnings),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_paths(
|
||||
&self,
|
||||
number_of_files: &mut usize,
|
||||
tx_error: mpsc::Sender<(PathBuf, Vec<Error>)>,
|
||||
) {
|
||||
let (tx_path, rx_path) = mpsc::channel::<Box<Path>>();
|
||||
let options = &self.options;
|
||||
let fix = options.fix;
|
||||
rayon::join(
|
||||
move || {
|
||||
options.paths.iter().flat_map(|path| Walk::new(path, options).iter()).for_each(
|
||||
|path| {
|
||||
*number_of_files += 1;
|
||||
tx_path.send(path).unwrap();
|
||||
},
|
||||
)
|
||||
},
|
||||
move || {
|
||||
while let Ok(path) = rx_path.recv() {
|
||||
let tx_error = tx_error.clone();
|
||||
rayon::spawn(move || {
|
||||
if let Some(diagnostics) = Self::lint_path(&path, fix) {
|
||||
tx_error.send(diagnostics).unwrap();
|
||||
}
|
||||
drop(tx_error);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn process_diagnostics(
|
||||
&self,
|
||||
number_of_warnings: &mut usize,
|
||||
number_of_diagnostics: &mut usize,
|
||||
rx_error: &mpsc::Receiver<(PathBuf, Vec<Error>)>,
|
||||
) {
|
||||
let mut buf_writer = BufWriter::new(std::io::stdout());
|
||||
|
||||
while let Ok((path, diagnostics)) = rx_error.recv() {
|
||||
*number_of_diagnostics += diagnostics.len();
|
||||
for diagnostic in diagnostics {
|
||||
if diagnostic.severity() == Some(Severity::Warning) {
|
||||
*number_of_warnings += 1;
|
||||
// The --quiet flag follows ESLint's --quiet behavior as documented here: https://eslint.org/docs/latest/use/command-line-interface#--quiet
|
||||
// Note that it does not disable ALL diagnostics, only Warning diagnostics
|
||||
if self.options.quiet {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(max_warnings) = self.options.max_warnings {
|
||||
if *number_of_warnings > max_warnings {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = format!("{diagnostic:?}");
|
||||
// Skip large output and print only once
|
||||
if output.lines().any(|line| line.len() >= 400) {
|
||||
let minified_diagnostic = Error::new(MinifiedFileError(path.clone()));
|
||||
buf_writer.write_all(format!("{minified_diagnostic:?}").as_bytes()).unwrap();
|
||||
break;
|
||||
}
|
||||
buf_writer.write_all(output.as_bytes()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
buf_writer.flush().unwrap();
|
||||
}
|
||||
|
||||
fn lint_path(path: &Path, fix: bool) -> Option<(PathBuf, Vec<Error>)> {
|
||||
let source_text = fs::read_to_string(path).unwrap_or_else(|_| panic!("{path:?} not found"));
|
||||
let allocator = Allocator::default();
|
||||
let source_type =
|
||||
SourceType::from_path(path).unwrap_or_else(|_| panic!("incorrect {path:?}"));
|
||||
let parser_source_text = source_text.clone();
|
||||
let ret = Parser::new(&allocator, &parser_source_text, source_type).parse();
|
||||
|
||||
if !ret.errors.is_empty() {
|
||||
return Some(Self::wrap_diagnostics(path, &source_text, ret.errors));
|
||||
};
|
||||
|
||||
let program = allocator.alloc(ret.program);
|
||||
let trivias = Rc::new(ret.trivias);
|
||||
let semantic_ret = SemanticBuilder::new(source_type).build(program, &trivias);
|
||||
|
||||
if !semantic_ret.errors.is_empty() {
|
||||
return Some(Self::wrap_diagnostics(path, &source_text, semantic_ret.errors));
|
||||
};
|
||||
|
||||
let result = Linter::new().with_fix(fix).run(&Rc::new(semantic_ret.semantic), &source_text);
|
||||
|
||||
if result.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if fix {
|
||||
let fix_result = Fixer::new(&source_text, result).fix();
|
||||
fs::write(path, fix_result.fixed_code.as_bytes()).unwrap();
|
||||
let errors = fix_result.messages.into_iter().map(|m| m.error).collect();
|
||||
return Some(Self::wrap_diagnostics(path, &source_text, errors));
|
||||
}
|
||||
|
||||
let errors = result.into_iter().map(|diagnostic| diagnostic.error).collect();
|
||||
Some(Self::wrap_diagnostics(path, &source_text, errors))
|
||||
}
|
||||
|
||||
fn wrap_diagnostics(
|
||||
path: &Path,
|
||||
source_text: &str,
|
||||
diagnostics: Vec<Error>,
|
||||
) -> (PathBuf, Vec<Error>) {
|
||||
let source = Arc::new(NamedSource::new(path.to_string_lossy(), source_text.to_owned()));
|
||||
let diagnostics = diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source)))
|
||||
.collect();
|
||||
(path.to_path_buf(), diagnostics)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
|||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
use oxc_cli::{command, Cli, CliOptions, CliRunResult};
|
||||
use oxc_cli::{command, CliRunResult, LintOptions, LintRunner};
|
||||
use oxc_diagnostics::miette;
|
||||
|
||||
fn main() -> CliRunResult {
|
||||
|
|
@ -17,20 +17,16 @@ fn main() -> CliRunResult {
|
|||
miette::set_hook(Box::new(|_| Box::new(miette::MietteHandlerOpts::new().tab_width(4).build())))
|
||||
.unwrap();
|
||||
|
||||
if let Some((subcommand, matches)) = command().get_matches().subcommand() {
|
||||
let cli_options = CliOptions::try_from(matches);
|
||||
if let Ok(cli_options) = cli_options {
|
||||
// if cli_options.fix {
|
||||
// Git::new().verify()?;
|
||||
// }
|
||||
let matches = command().get_matches();
|
||||
let Some((subcommand, matches)) = matches.subcommand() else {
|
||||
return CliRunResult::None
|
||||
};
|
||||
|
||||
let cli = Cli::new(cli_options);
|
||||
|
||||
if subcommand == "lint" {
|
||||
return cli.lint();
|
||||
}
|
||||
return CliRunResult::None;
|
||||
match subcommand {
|
||||
"lint" => {
|
||||
let options = LintOptions::from(matches);
|
||||
LintRunner::new(options).run()
|
||||
}
|
||||
_ => CliRunResult::None,
|
||||
}
|
||||
CliRunResult::None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use glob::Pattern;
|
||||
|
||||
pub struct CliOptions {
|
||||
pub quiet: bool,
|
||||
pub fix: bool,
|
||||
pub max_warnings: Option<usize>,
|
||||
pub paths: Vec<PathBuf>,
|
||||
pub ignore_path: String,
|
||||
pub no_ignore: bool,
|
||||
pub ignore_pattern: Vec<Pattern>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches> for CliOptions {
|
||||
type Error = &'a str;
|
||||
|
||||
fn try_from(matches: &ArgMatches) -> Result<Self, Self::Error> {
|
||||
let mut paths = vec![];
|
||||
|
||||
for path in matches.get_many::<PathBuf>("path").unwrap() {
|
||||
let glob_result =
|
||||
glob::glob(&path.to_string_lossy()).map_err(|_| "Failed to read glob pattern")?;
|
||||
let globbed = glob_result
|
||||
.map(|path_result| path_result.map_err(|_| "Failed to read path"))
|
||||
.collect::<Result<Vec<PathBuf>, &str>>()?;
|
||||
|
||||
if globbed.is_empty() && path.canonicalize().is_err() {
|
||||
return Err("Unable to find globbed files");
|
||||
}
|
||||
|
||||
paths.extend(globbed);
|
||||
}
|
||||
|
||||
let ignore_path = get_ignore_path(matches);
|
||||
let no_ignore = matches.get_flag("no-ignore");
|
||||
let ignore_pattern = get_ignore_pattern(matches);
|
||||
|
||||
Ok(Self {
|
||||
quiet: matches.get_flag("quiet"),
|
||||
fix: matches.get_flag("fix"),
|
||||
max_warnings: matches.get_one("max-warnings").copied(),
|
||||
paths,
|
||||
ignore_path,
|
||||
no_ignore,
|
||||
ignore_pattern,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ignore_path(matches: &ArgMatches) -> String {
|
||||
matches.get_one::<String>("ignore-path").map_or(".eslintignore".to_string(), ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn get_ignore_pattern(matches: &ArgMatches) -> Vec<Pattern> {
|
||||
let mut result = vec![];
|
||||
let Some(ignore_pattern) = matches.get_many::<String>("ignore-pattern") else {return result};
|
||||
for pattern in ignore_pattern {
|
||||
if let Ok(pattern) = Pattern::new(pattern) {
|
||||
result.push(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
|
@ -3,14 +3,14 @@ use std::path::Path;
|
|||
use ignore::{DirEntry, WalkBuilder};
|
||||
use oxc_ast::VALID_EXTENSIONS;
|
||||
|
||||
use crate::CliOptions;
|
||||
use crate::LintOptions;
|
||||
|
||||
pub struct Walk {
|
||||
inner: ignore::Walk,
|
||||
}
|
||||
|
||||
impl Walk {
|
||||
pub fn new<P: AsRef<Path>>(path: P, options: &CliOptions) -> Self {
|
||||
pub fn new<P: AsRef<Path>>(path: P, options: &LintOptions) -> Self {
|
||||
let mut inner = WalkBuilder::new(path);
|
||||
if !options.no_ignore {
|
||||
inner.add_custom_ignore_filename(&options.ignore_path);
|
||||
|
|
|
|||
Loading…
Reference in a new issue