oxc/crates/oxc_cli/src/command.rs
Boshen a978639718
feat(cli): disallow path pointing to parent directories (#2125)
Linting parent directories introduces undefined behavior because
the intention is not clear. This is a future proof precaution.

closes #1887
2024-01-22 17:52:57 +08:00

424 lines
12 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use bpaf::{doc::Style, Bpaf};
use oxc_linter::AllowWarnDeny;
use std::{ffi::OsString, path::PathBuf};
#[derive(Debug, Clone, Bpaf)]
#[bpaf(options)]
pub enum CliCommand {
/// Lint this repository
#[bpaf(command)]
Lint(#[bpaf(external(lint_options))] LintOptions),
/// Format this repository
#[bpaf(command)]
Format(#[bpaf(external(format_options))] FormatOptions),
}
impl CliCommand {
pub fn handle_threads(&self) {
match self {
Self::Lint(options) => {
Self::set_rayon_threads(options.misc_options.threads);
}
Self::Format(options) => {
Self::set_rayon_threads(options.misc_options.threads);
}
}
}
fn set_rayon_threads(threads: Option<usize>) {
if let Some(threads) = threads {
rayon::ThreadPoolBuilder::new().num_threads(threads).build_global().unwrap();
}
}
}
// To add a header or footer, see
// <https://docs.rs/bpaf/latest/bpaf/struct.OptionParser.html#method.descr>
/// Linter for the JavaScript Oxidation Compiler
#[derive(Debug, Clone, Bpaf)]
#[bpaf(options)]
pub struct LintCommand {
#[bpaf(external(lint_options))]
pub lint_options: LintOptions,
}
impl LintCommand {
pub fn handle_threads(&self) {
CliCommand::set_rayon_threads(self.lint_options.misc_options.threads);
}
}
/// Formatter for the JavaScript Oxidation Compiler
#[derive(Debug, Clone, Bpaf)]
#[bpaf(options)]
pub struct FormatCommand {
#[bpaf(external(format_options))]
pub format_options: FormatOptions,
}
impl FormatCommand {
pub fn handle_threads(&self) {
CliCommand::set_rayon_threads(self.format_options.misc_options.threads);
}
}
/// Miscellaneous
#[derive(Debug, Clone, Bpaf)]
pub struct MiscOptions {
/// list all the rules that are currently registered
#[bpaf(switch, hide_usage)]
pub rules: bool,
/// Number of threads to use. Set to 1 for using only 1 CPU core
#[bpaf(argument("INT"), hide_usage)]
pub threads: Option<usize>,
}
/// Enable Plugins
#[derive(Debug, Clone, Bpaf)]
pub struct EnablePlugins {
/// Enable the experimental import plugin and detect ESM problems
#[bpaf(switch, hide_usage)]
pub import_plugin: bool,
/// Enable the Jest plugin and detect test problems
#[bpaf(switch, hide_usage)]
pub jest_plugin: bool,
/// Enable the JSX-a11y plugin and detect accessibility problems
#[bpaf(switch, hide_usage)]
pub jsx_a11y_plugin: bool,
/// Enable the Next.js plugin and detect Next.js problems
#[bpaf(switch, hide_usage)]
pub nextjs_plugin: bool,
/// Enable the React performance plugin and detect rendering performance problems
#[bpaf(switch, hide_usage)]
pub react_perf_plugin: bool,
}
#[derive(Debug, Clone, Bpaf)]
pub struct LintOptions {
#[bpaf(external(lint_filter), map(LintFilter::into_tuple), many)]
pub filter: Vec<(AllowWarnDeny, String)>,
#[bpaf(external)]
pub enable_plugins: EnablePlugins,
#[bpaf(external)]
pub fix_options: FixOptions,
#[bpaf(external)]
pub ignore_options: IgnoreOptions,
#[bpaf(external)]
pub warning_options: WarningOptions,
#[bpaf(external)]
pub misc_options: MiscOptions,
#[bpaf(external)]
pub codeowner_options: CodeownerOptions,
/// ESLint configuration file (experimental)
///
/// * only `.json` extension is supported
#[bpaf(long("config"), short('c'), argument("PATH"))]
pub config: Option<PathBuf>,
/// Single file, single path or list of paths
#[bpaf(positional("PATH"), many, guard(validate_paths, PATHS_ERROR_MESSAGE))]
pub paths: Vec<PathBuf>,
}
fn validate_paths(paths: &Vec<PathBuf>) -> bool {
if paths.is_empty() {
true
} else {
paths.iter().all(|p| p.components().next() != Some(std::path::Component::ParentDir))
}
}
const PATHS_ERROR_MESSAGE: &str = "PATH cannot start with \"..\"";
// This is formatted according to
// <https://docs.rs/bpaf/latest/bpaf/params/struct.NamedArg.html#method.help>
/// Allowing / Denying Multiple Lints
/// For example `-D correctness -A no-debugger` or `-A all -D no-debugger`.
///
/// The default category is "-D correctness".
/// Use "--rules" for rule names.
/// Use "--help --help" for rule categories.
///
/// The categories are:
/// * correctness - code that is outright wrong or useless
/// * suspicious - code that is most likely wrong or useless
/// * pedantic - lints which are rather strict or have occasional false positives
/// * style - code that should be written in a more idiomatic way
/// * nursery - new lints that are still under development
/// * restriction - lints which prevent the use of language and library features
/// * all - all the categories listed above
#[derive(Debug, Clone, Bpaf)]
pub enum LintFilter {
Allow(
/// Allow the rule or category (suppress the lint)
#[bpaf(short('A'), long("allow"), argument("NAME"))]
String,
),
Deny(
/// Deny the rule or category (emit an error)
#[bpaf(short('D'), long("deny"), argument("NAME"))]
String,
),
}
impl LintFilter {
fn into_tuple(self) -> (AllowWarnDeny, String) {
match self {
Self::Allow(s) => (AllowWarnDeny::Allow, s),
Self::Deny(s) => (AllowWarnDeny::Deny, s),
}
}
}
#[derive(Debug, Clone, Bpaf)]
pub struct FormatOptions {
#[bpaf(external)]
pub misc_options: MiscOptions,
#[bpaf(external)]
pub ignore_options: IgnoreOptions,
/// Single file, single path or list of paths
#[bpaf(positional("PATH"), many)]
pub paths: Vec<PathBuf>,
}
/// Codeowners
#[derive(Debug, Clone, Bpaf)]
pub struct CodeownerOptions {
/// Path to CODEOWNERS file
#[bpaf(argument("PATH"), hide_usage)]
pub codeowners_file: Option<OsString>,
/// Code owner names, e.g. @Boshen
#[bpaf(argument("NAME"), hide_usage)]
pub codeowners: Vec<String>,
}
/// Fix Problems
#[derive(Debug, Clone, Bpaf)]
pub struct FixOptions {
/// Fix as many issues as possible. Only unfixed issues are reported in the output
#[bpaf(switch)]
pub fix: bool,
}
const NO_IGNORE_HELP: &[(&str, Style)] = &[
("Disables excluding of files from .eslintignore files, ", Style::Text),
("--ignore-path", Style::Literal),
(" flags and ", Style::Text),
("--ignore-pattern", Style::Literal),
(" flags", Style::Text),
];
/// Ignore Files
#[derive(Debug, Clone, Bpaf)]
pub struct IgnoreOptions {
/// Specify the file to use as your .eslintignore
#[bpaf(argument("PATH"), fallback(".eslintignore".into()), hide_usage)]
pub ignore_path: OsString,
/// Specify patterns of files to ignore (in addition to those in .eslintignore)
///
/// The supported syntax is the same as for .eslintignore and .gitignore files
/// You should quote your patterns in order to avoid shell interpretation of glob patterns
#[bpaf(argument("PAT"), many, hide_usage)]
pub ignore_pattern: Vec<String>,
///
#[bpaf(switch, hide_usage, help(NO_IGNORE_HELP))]
pub no_ignore: bool,
}
/// Handle Warnings
#[derive(Debug, Clone, Bpaf)]
pub struct WarningOptions {
/// Disable reporting on warnings, only errors are reported
#[bpaf(switch, hide_usage)]
pub quiet: bool,
/// Ensure warnings produce a non-zero exit code
#[bpaf(switch, hide_usage)]
pub deny_warnings: bool,
/// Specify a warning threshold,
/// which can be used to force exit with an error status if there are too many warning-level rule violations in your project
#[bpaf(argument("INT"), hide_usage)]
pub max_warnings: Option<usize>,
}
#[cfg(test)]
mod misc_options {
use super::{lint_command, MiscOptions};
fn get_misc_options(arg: &str) -> MiscOptions {
let args = arg.split(' ').map(std::string::ToString::to_string).collect::<Vec<_>>();
lint_command().run_inner(args.as_slice()).unwrap().lint_options.misc_options
}
#[test]
fn default() {
let options = get_misc_options(".");
assert!(!options.rules);
assert!(options.threads.is_none());
}
#[test]
fn threads() {
let options = get_misc_options("--threads 4 .");
assert_eq!(options.threads, Some(4));
}
#[test]
fn list_rules() {
let options = get_misc_options("--rules");
assert!(options.rules);
}
}
#[cfg(test)]
mod warning_options {
use super::{lint_command, WarningOptions};
fn get_warning_options(arg: &str) -> WarningOptions {
let args = arg.split(' ').map(std::string::ToString::to_string).collect::<Vec<_>>();
lint_command().run_inner(args.as_slice()).unwrap().lint_options.warning_options
}
#[test]
fn default() {
let options = get_warning_options(".");
assert!(!options.quiet);
assert_eq!(options.max_warnings, None);
}
#[test]
fn quiet() {
let options = get_warning_options("--quiet .");
assert!(options.quiet);
}
#[test]
fn max_warnings() {
let options = get_warning_options("--max-warnings 10 .");
assert_eq!(options.max_warnings, Some(10));
}
}
#[cfg(test)]
mod lint_options {
use super::{lint_command, LintOptions};
use oxc_linter::AllowWarnDeny;
use std::path::PathBuf;
fn get_lint_options(arg: &str) -> LintOptions {
let args = arg.split(' ').map(std::string::ToString::to_string).collect::<Vec<_>>();
lint_command().run_inner(args.as_slice()).unwrap().lint_options
}
#[test]
fn default() {
let options = get_lint_options(".");
assert_eq!(options.paths, vec![PathBuf::from(".")]);
assert!(!options.fix_options.fix);
}
#[test]
fn multiple_paths() {
let options = get_lint_options("foo bar baz");
assert_eq!(
options.paths,
[PathBuf::from("foo"), PathBuf::from("bar"), PathBuf::from("baz")]
);
}
#[test]
fn no_parent_path() {
match lint_command().run_inner(&["../parent_dir"]) {
Ok(_) => panic!("Should not allow parent dir"),
Err(err) => match err {
bpaf::ParseFailure::Stderr(doc) => {
assert_eq!("`../parent_dir`: PATH cannot start with \"..\"", format!("{doc}"));
}
_ => unreachable!(),
},
}
}
#[test]
fn fix() {
let options = get_lint_options("--fix test.js");
assert!(options.fix_options.fix);
}
#[test]
fn filter() {
let options =
get_lint_options("-D suspicious --deny pedantic -A no-debugger --allow no-var src");
assert_eq!(
options.filter,
[
(AllowWarnDeny::Deny, "suspicious".into()),
(AllowWarnDeny::Deny, "pedantic".into()),
(AllowWarnDeny::Allow, "no-debugger".into()),
(AllowWarnDeny::Allow, "no-var".into())
]
);
}
}
#[cfg(test)]
mod ignore_options {
use super::{lint_command, IgnoreOptions};
use std::{ffi::OsString, path::PathBuf};
fn get_ignore_options(arg: &str) -> IgnoreOptions {
let args = arg.split(' ').map(std::string::ToString::to_string).collect::<Vec<_>>();
lint_command().run_inner(args.as_slice()).unwrap().lint_options.ignore_options
}
#[test]
fn default() {
let options = get_ignore_options(".");
assert_eq!(options.ignore_path, OsString::from(".eslintignore"));
assert!(!options.no_ignore);
assert!(options.ignore_pattern.is_empty());
}
#[test]
fn ignore_path() {
let options = get_ignore_options("--ignore-path .xxx foo.js");
assert_eq!(options.ignore_path, PathBuf::from(".xxx"));
}
#[test]
fn no_ignore() {
let options = get_ignore_options("--no-ignore foo.js");
assert!(options.no_ignore);
}
#[test]
fn single_ignore_pattern() {
let options = get_ignore_options("--ignore-pattern ./test foo.js");
assert_eq!(options.ignore_pattern, vec![String::from("./test")]);
}
#[test]
fn multiple_ignore_pattern() {
let options = get_ignore_options("--ignore-pattern ./test --ignore-pattern bar.js foo.js");
assert_eq!(options.ignore_pattern, vec![String::from("./test"), String::from("bar.js")]);
}
}