mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(cli): Basic recursive implementation of module based linting (#468)
This commit is contained in:
parent
b8f5e3ad92
commit
b31819d7a1
13 changed files with 696 additions and 13 deletions
215
Cargo.lock
generated
215
Cargo.lock
generated
|
|
@ -433,6 +433,25 @@ dependencies = [
|
|||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "daachorse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b7ef7a4be509357f4804d0a22e830daddb48f19fd604e4ad32ddce04a94c36"
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive-debug-extras"
|
||||
version = "0.2.2"
|
||||
|
|
@ -463,6 +482,12 @@ dependencies = [
|
|||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.8.1"
|
||||
|
|
@ -825,6 +850,15 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonc-parser"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b56a20e76235284255a09fcd1f45cf55d3c524ea657ebd3854735925c57743d"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
|
@ -881,6 +915,16 @@ version = "0.3.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.19"
|
||||
|
|
@ -896,6 +940,15 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
|
|
@ -1019,6 +1072,36 @@ dependencies = [
|
|||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodejs-resolver"
|
||||
version = "0.0.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7259edee7a18be2bdc9802f3357044a964d6e0624030201849ed734b8901a23b"
|
||||
dependencies = [
|
||||
"daachorse",
|
||||
"dashmap",
|
||||
"dunce",
|
||||
"indexmap",
|
||||
"jsonc-parser",
|
||||
"once_cell",
|
||||
"path-absolutize",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.3"
|
||||
|
|
@ -1080,6 +1163,12 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "3.5.0"
|
||||
|
|
@ -1147,10 +1236,13 @@ version = "0.0.0"
|
|||
dependencies = [
|
||||
"clap",
|
||||
"codespan-reporting",
|
||||
"crossbeam-channel",
|
||||
"dashmap",
|
||||
"ignore",
|
||||
"jemallocator",
|
||||
"miette",
|
||||
"mimalloc",
|
||||
"nodejs-resolver",
|
||||
"num_cpus",
|
||||
"oxc_allocator",
|
||||
"oxc_diagnostics",
|
||||
|
|
@ -1424,6 +1516,19 @@ dependencies = [
|
|||
"wasm-bindgen-test",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "path-absolutize"
|
||||
version = "3.1.0"
|
||||
|
|
@ -1616,6 +1721,15 @@ dependencies = [
|
|||
"num_cpus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.8.4"
|
||||
|
|
@ -1624,9 +1738,24 @@ checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
|
|||
dependencies = [
|
||||
"aho-corasick 1.0.2",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.2"
|
||||
|
|
@ -1797,6 +1926,7 @@ version = "1.0.96"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
|
|
@ -1826,6 +1956,15 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.2.1"
|
||||
|
|
@ -1838,6 +1977,12 @@ version = "0.3.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.1"
|
||||
|
|
@ -2066,6 +2211,68 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8803eee176538f94ae9a14b55b2804eb7e1441f8210b1c31290b3bccdccff73b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.16.0"
|
||||
|
|
@ -2171,6 +2378,12 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ clap = { version = "4.3.4" }
|
|||
compact_str = { version = "0.7.0" }
|
||||
convert_case = { version = "0.6.0" }
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
crossbeam-channel = { version = "0.5.8" }
|
||||
dashmap = { version = "5.4.0" }
|
||||
flate2 = { version = "1.0.26" }
|
||||
glob = { version = "0.3.1" }
|
||||
ignore = { version = "0.4.20" }
|
||||
|
|
@ -52,6 +54,7 @@ jemallocator = { version = "0.5.0" }
|
|||
lazy_static = { version = "1.4.0" }
|
||||
miette = { version = "5.9.0" }
|
||||
mimalloc = { version = "0.1.37" }
|
||||
nodejs-resolver = { version = "0.0.88" }
|
||||
num-bigint = { version = "0.4.3" }
|
||||
num-traits = { version = "0.2.15" }
|
||||
num_cpus = { version = "1.15.0" }
|
||||
|
|
|
|||
|
|
@ -28,10 +28,13 @@ oxc_span = { workspace = true }
|
|||
# TODO temp, for type check output, replace with Miette
|
||||
codespan-reporting = "0.11.1"
|
||||
|
||||
clap = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
||||
rustc-hash = { workspace = true }
|
||||
num_cpus = { workspace = true }
|
||||
ignore = { workspace = true, features = ["simd-accel"] }
|
||||
clap = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
||||
rustc-hash = { workspace = true }
|
||||
num_cpus = { workspace = true }
|
||||
ignore = { workspace = true, features = ["simd-accel"] }
|
||||
nodejs-resolver = { workspace = true }
|
||||
# git2 = { version = "0.16.1", default_features = false }
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use clap::{Arg, Command};
|
|||
|
||||
use crate::{lint::lint_command, type_check::type_check_command};
|
||||
pub use crate::{
|
||||
lint::{LintOptions, LintRunner},
|
||||
lint::{LintOptions, LintRunner, LintRunnerWithModuleTree},
|
||||
result::CliRunResult,
|
||||
type_check::{TypeCheckOptions, TypeCheckRunner},
|
||||
walk::Walk,
|
||||
|
|
|
|||
43
crates/oxc_cli/src/lint/error.rs
Normal file
43
crates/oxc_cli/src/lint/error.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use std::{
|
||||
error::Error as STDError,
|
||||
fmt::{self, Display},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
path: PathBuf,
|
||||
error: io::Error,
|
||||
}
|
||||
|
||||
impl STDError for Error {}
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} when accessing \"{}\"", self.error, self.path.display())
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
fn new(path: PathBuf, error: io::Error) -> Self {
|
||||
Self { path, error }
|
||||
}
|
||||
}
|
||||
|
||||
// allow simple conversion from io::Result using the `with_path` method
|
||||
pub trait ErrorWithPath<T> {
|
||||
/// Convert an io::Result to lint::error, which allows associating a path with the error for
|
||||
/// better error messages.
|
||||
fn with_path(self, path: impl AsRef<Path>) -> Result<T>;
|
||||
}
|
||||
|
||||
impl<T> ErrorWithPath<T> for io::Result<T> {
|
||||
fn with_path(self, path: impl AsRef<Path>) -> Result<T> {
|
||||
match self {
|
||||
Ok(t) => Ok(t),
|
||||
Err(error) => Err(Error::new(path.as_ref().to_path_buf(), error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
|
@ -1,11 +1,17 @@
|
|||
mod command;
|
||||
mod error;
|
||||
mod resolver;
|
||||
mod runner;
|
||||
mod runner_with_module_tree;
|
||||
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
|
||||
use clap::ArgMatches;
|
||||
|
||||
pub use self::{command::lint_command, runner::LintRunner};
|
||||
pub use self::{
|
||||
command::lint_command, error::Error, runner::LintRunner,
|
||||
runner_with_module_tree::LintRunnerWithModuleTree,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
|
|
|
|||
26
crates/oxc_cli/src/lint/resolver.rs
Normal file
26
crates/oxc_cli/src/lint/resolver.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use nodejs_resolver::{EnforceExtension, Options, Resolver as NodeJSResolver};
|
||||
pub use nodejs_resolver::{ResolveResult, Resource};
|
||||
use oxc_span::VALID_EXTENSIONS;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Resolver(NodeJSResolver);
|
||||
|
||||
impl Default for Resolver {
|
||||
fn default() -> Self {
|
||||
Self(NodeJSResolver::new(Options {
|
||||
enforce_extension: EnforceExtension::Enabled,
|
||||
extensions: VALID_EXTENSIONS.into_iter().map(|ext| String::from(".") + ext).collect(),
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Resolver {
|
||||
type Target = NodeJSResolver;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
347
crates/oxc_cli/src/lint/runner_with_module_tree.rs
Normal file
347
crates/oxc_cli/src/lint/runner_with_module_tree.rs
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
io::{self, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
use miette::NamedSource;
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_diagnostics::{
|
||||
miette::{self, Diagnostic},
|
||||
thiserror::Error,
|
||||
Error, GraphicalReportHandler, Severity,
|
||||
};
|
||||
use oxc_linter::{FixResult, Fixer, Linter, RuleCategory, RuleEnum, RULES};
|
||||
use oxc_parser::{Parser, ParserReturn};
|
||||
use oxc_semantic::{SemanticBuilder, SemanticBuilderReturn};
|
||||
use oxc_span::{SourceType, VALID_EXTENSIONS};
|
||||
use rayon::prelude::*;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use super::{
|
||||
error::{ErrorWithPath, Result},
|
||||
resolver::{ResolveResult, Resolver, Resource},
|
||||
AllowWarnDeny, LintOptions,
|
||||
};
|
||||
use crate::CliRunResult;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LinterRuntimeData {
|
||||
linter: Arc<Linter>,
|
||||
visited: Arc<DashSet<PathBuf>>,
|
||||
tx_error: Sender<(PathBuf, Vec<Error>)>,
|
||||
}
|
||||
|
||||
pub struct LintRunnerWithModuleTree {
|
||||
options: LintOptions,
|
||||
}
|
||||
use dashmap::DashSet;
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("File is too long to fit on the screen")]
|
||||
#[diagnostic(help("{0:?} seems like a minified file"))]
|
||||
pub struct MinifiedFileError(pub PathBuf);
|
||||
|
||||
impl LintRunnerWithModuleTree {
|
||||
pub fn new(options: LintOptions) -> Self {
|
||||
Self { options }
|
||||
}
|
||||
|
||||
pub fn print_rules() {
|
||||
let mut stdout = BufWriter::new(io::stdout());
|
||||
Linter::print_rules(&mut stdout);
|
||||
}
|
||||
|
||||
fn derive_rules(options: &LintOptions) -> Vec<RuleEnum> {
|
||||
let mut rules: FxHashSet<RuleEnum> = FxHashSet::default();
|
||||
|
||||
for (allow_warn_deny, name_or_category) in &options.rules {
|
||||
let maybe_category = RuleCategory::from(name_or_category.as_str());
|
||||
match allow_warn_deny {
|
||||
AllowWarnDeny::Deny => {
|
||||
match maybe_category {
|
||||
Some(category) => rules.extend(
|
||||
RULES.iter().filter(|rule| rule.category() == category).cloned(),
|
||||
),
|
||||
None => {
|
||||
if name_or_category == "all" {
|
||||
rules.extend(RULES.iter().cloned());
|
||||
} else {
|
||||
rules.extend(
|
||||
RULES
|
||||
.iter()
|
||||
.filter(|rule| rule.name() == name_or_category)
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
AllowWarnDeny::Allow => {
|
||||
match maybe_category {
|
||||
Some(category) => rules.retain(|rule| rule.category() != category),
|
||||
None => {
|
||||
if name_or_category == "all" {
|
||||
rules.clear();
|
||||
} else {
|
||||
rules.retain(|rule| rule.name() == name_or_category);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut rules = rules.into_iter().collect::<Vec<_>>();
|
||||
// for stable diagnostics output ordering
|
||||
rules.sort_unstable_by_key(|rule| rule.name());
|
||||
rules
|
||||
}
|
||||
|
||||
pub fn run(&self) -> CliRunResult {
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
let linter =
|
||||
Linter::from_rules(Self::derive_rules(&self.options)).with_fix(self.options.fix);
|
||||
let linter = Arc::new(linter);
|
||||
|
||||
// Unless other panic happens, calling `Sender::send` can't fail, because we hold the
|
||||
// receiver until all senders are dropped. This allows us to safely unwrap all calls to send.
|
||||
let (tx_error, rx_error) = unbounded();
|
||||
|
||||
// we can ignore the result because nothing bad happens if the resolver is already set
|
||||
// TODO: make sure this is still true once we allow options to be set
|
||||
// during runtime (config file, args, etc.)
|
||||
let _ = RESOLVER.set(Resolver::default());
|
||||
|
||||
let visited = Arc::new(DashSet::new());
|
||||
|
||||
// TODO: try to process as many files as possible even if some of them fail
|
||||
let result = process_paths(
|
||||
&self.options.paths,
|
||||
LinterRuntimeData {
|
||||
linter: Arc::clone(&linter),
|
||||
visited: Arc::clone(&visited),
|
||||
tx_error,
|
||||
},
|
||||
);
|
||||
|
||||
let (number_of_warnings, number_of_diagnostics) = self.process_diagnostics(&rx_error);
|
||||
|
||||
if let Err(err) = result {
|
||||
return CliRunResult::IOError(err);
|
||||
}
|
||||
|
||||
CliRunResult::LintResult {
|
||||
duration: now.elapsed(),
|
||||
number_of_rules: linter.number_of_rules(),
|
||||
number_of_files: visited.len(),
|
||||
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_diagnostics(&self, rx_error: &Receiver<(PathBuf, Vec<Error>)>) -> (usize, usize) {
|
||||
let mut number_of_warnings = 0;
|
||||
let mut number_of_diagnostics = 0;
|
||||
let mut buf_writer = BufWriter::new(io::stdout());
|
||||
let handler = GraphicalReportHandler::new();
|
||||
|
||||
for (path, diagnostics) in rx_error.iter() {
|
||||
number_of_diagnostics += diagnostics.len();
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
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 mut err = String::new();
|
||||
handler
|
||||
.render_report(&mut err, diagnostic.as_ref())
|
||||
.expect("Writing to a string can't fail");
|
||||
|
||||
if err.lines().all(|line| line.len() < 400) {
|
||||
output.push_str(&err);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the error is too long, we assume it's a minified file and print it as only error
|
||||
output = format!("{:?}", Error::new(MinifiedFileError(path.clone())));
|
||||
break;
|
||||
}
|
||||
|
||||
// write operations on stdout can't fail according to RFC 1014
|
||||
// https://rust-lang.github.io/rfcs/1014-stdout-existential-crisis.html
|
||||
buf_writer.write_all(output.as_bytes()).expect("Writing to stdout can't fail");
|
||||
}
|
||||
|
||||
// see comment above
|
||||
buf_writer.flush().expect("Flushing stdout can't fail");
|
||||
(number_of_warnings, number_of_diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
fn process_paths(paths: &[PathBuf], runtime_data: LinterRuntimeData) -> Result<()> {
|
||||
paths.par_iter().try_for_each(move |path| {
|
||||
let path = path.canonicalize().with_path(path)?;
|
||||
|
||||
if path.is_file() {
|
||||
run_for_file(&path, &runtime_data)
|
||||
} else if path.is_dir() {
|
||||
run_for_dir(&path, &runtime_data)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn run_for_dir(path: &Path, runtime_data: &LinterRuntimeData) -> Result<()> {
|
||||
fs::read_dir(path).with_path(path)?.par_bridge().try_for_each(|entry| {
|
||||
let path = entry.with_path(path)?.path();
|
||||
|
||||
if path.is_file() {
|
||||
if !runtime_data.visited.contains(&path) {
|
||||
run_for_file(&path, runtime_data)?;
|
||||
}
|
||||
} else if path.is_dir() {
|
||||
run_for_dir(&path, runtime_data)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
static RESOLVER: OnceLock<Resolver> = OnceLock::new();
|
||||
|
||||
fn run_for_file(path: &Path, runtime_data: &LinterRuntimeData) -> Result<()> {
|
||||
let LinterRuntimeData { linter, visited, tx_error } = &runtime_data;
|
||||
|
||||
if visited.contains(path) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
visited.insert(path.to_path_buf());
|
||||
|
||||
let Ok(source_type) = SourceType::from_path(path) else {
|
||||
// skip unsupported file types (e.g. .css or .json)
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let source = fs::read_to_string(path).with_path(path)?;
|
||||
|
||||
let allocator = Allocator::default();
|
||||
|
||||
let (program, trivias) = {
|
||||
let ParserReturn { program, errors, trivias, .. } =
|
||||
Parser::new(&allocator, &source, source_type).parse();
|
||||
|
||||
if !errors.is_empty() {
|
||||
tx_error.send(wrap_diagnostics(path, &source, errors)).unwrap();
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
(allocator.alloc(program), trivias)
|
||||
};
|
||||
|
||||
let semantic = {
|
||||
let SemanticBuilderReturn { errors, semantic } = SemanticBuilder::new(&source, source_type)
|
||||
.with_trivias(&trivias)
|
||||
.with_check_syntax_error(true)
|
||||
.with_module_record_builder(true)
|
||||
.build(program);
|
||||
|
||||
if !errors.is_empty() {
|
||||
tx_error.send(wrap_diagnostics(path, &source, errors)).unwrap();
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
semantic
|
||||
};
|
||||
|
||||
// this is ok to unwrap because we know that the resolver is initialized, otherwise this function wouldn't be called
|
||||
let resolver = RESOLVER.get().unwrap();
|
||||
|
||||
let resolve_path = path.parent().expect("Absolute file path always has a parent");
|
||||
|
||||
let imported_modules = semantic.module_record().module_requests.keys();
|
||||
|
||||
imported_modules
|
||||
.par_bridge()
|
||||
.filter(|name| name.starts_with('.'))
|
||||
.filter_map(|name| {
|
||||
resolver.resolve(resolve_path, name).map_or_else(
|
||||
|_| {
|
||||
eprintln!("Couldn't resolve '{name}' in '{}'.", resolve_path.display());
|
||||
None
|
||||
},
|
||||
Some,
|
||||
)
|
||||
})
|
||||
.filter_map(|resolved| match resolved {
|
||||
ResolveResult::Resource(Resource { path, .. }) => Some(path),
|
||||
ResolveResult::Ignored => None,
|
||||
})
|
||||
.filter(|path| {
|
||||
path.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.is_some_and(|ext| VALID_EXTENSIONS.contains(&ext))
|
||||
})
|
||||
.filter(|path| !visited.contains(path))
|
||||
.try_for_each(|path| run_for_file(&path, runtime_data))?;
|
||||
|
||||
let result = linter.run(&Rc::new(semantic));
|
||||
|
||||
if result.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let messages = if linter.has_fix() {
|
||||
let FixResult { messages, fixed_code, .. } = Fixer::new(&source, result).fix();
|
||||
fs::write(path, fixed_code.as_bytes()).with_path(path)?;
|
||||
messages
|
||||
} else {
|
||||
result
|
||||
};
|
||||
|
||||
let errors = messages.into_iter().map(|m| m.error).collect();
|
||||
let diagnostic = wrap_diagnostics(path, &source, errors);
|
||||
tx_error.send(diagnostic).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -8,7 +8,12 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
|
|||
#[global_allocator]
|
||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
|
||||
use oxc_cli::{command, CliRunResult, LintOptions, LintRunner, TypeCheckOptions, TypeCheckRunner};
|
||||
use std::env;
|
||||
|
||||
use oxc_cli::{
|
||||
command, CliRunResult, LintOptions, LintRunner, LintRunnerWithModuleTree, TypeCheckOptions,
|
||||
TypeCheckRunner,
|
||||
};
|
||||
|
||||
fn main() -> CliRunResult {
|
||||
let matches = command().get_matches();
|
||||
|
|
@ -30,7 +35,11 @@ fn main() -> CliRunResult {
|
|||
return CliRunResult::None;
|
||||
}
|
||||
|
||||
LintRunner::new(options).run()
|
||||
if matches!(env::var("OXC_MODULE_TREE"), Ok(x) if x == "true" || x == "1") {
|
||||
LintRunnerWithModuleTree::new(options).run()
|
||||
} else {
|
||||
LintRunner::new(options).run()
|
||||
}
|
||||
}
|
||||
"check" => {
|
||||
let options = TypeCheckOptions::from(matches);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use std::{
|
|||
#[derive(Debug)]
|
||||
pub enum CliRunResult {
|
||||
None,
|
||||
IOError(crate::lint::Error),
|
||||
PathNotFound {
|
||||
paths: Vec<PathBuf>,
|
||||
},
|
||||
|
|
@ -31,6 +32,10 @@ impl Termination for CliRunResult {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ mod rules;
|
|||
|
||||
use std::{fs, io::Write, rc::Rc};
|
||||
|
||||
pub use fixer::{Fixer, Message};
|
||||
pub use fixer::{FixResult, Fixer, Message};
|
||||
pub(crate) use oxc_semantic::AstNode;
|
||||
use oxc_semantic::Semantic;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ mod symbol;
|
|||
|
||||
use std::rc::Rc;
|
||||
|
||||
pub use builder::SemanticBuilder;
|
||||
pub use builder::{SemanticBuilder, SemanticBuilderReturn};
|
||||
pub use jsdoc::{JSDoc, JSDocComment, JSDocTag};
|
||||
use oxc_ast::{ast::IdentifierReference, module_record::ModuleRecord, AstKind, Trivias};
|
||||
use oxc_span::SourceType;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,34 @@ impl ModuleRecordBuilder {
|
|||
if let Statement::ModuleDeclaration(module_decl) = stmt {
|
||||
self.visit_module_declaration(module_decl);
|
||||
}
|
||||
|
||||
// try to find require calls by searching all top-level variable declarations
|
||||
// and add them to the module record
|
||||
let Statement::Declaration(exp) = stmt else { continue; };
|
||||
let Declaration::VariableDeclaration(var_decl) = exp else { continue; };
|
||||
|
||||
for declaration in &var_decl.declarations {
|
||||
let Some(init) = &declaration.init else { continue; };
|
||||
let Expression::CallExpression(call) = &init else { continue; };
|
||||
let Expression::Identifier(ident) = &call.callee else { continue; };
|
||||
if ident.name == "require" {
|
||||
let Some(Argument::Expression(Expression::StringLiteral(module))) = call.arguments.get(0) else { continue; };
|
||||
|
||||
let module_request = NameSpan::new(module.value.clone(), module.span);
|
||||
|
||||
declaration.id.bound_names(&mut |identifier| {
|
||||
let identifier = NameSpan::new(identifier.name.clone(), identifier.span);
|
||||
|
||||
self.add_import_entry(ImportEntry {
|
||||
module_request: module_request.clone(),
|
||||
import_name: ImportImportName::Name(identifier.clone()),
|
||||
local_name: identifier,
|
||||
});
|
||||
});
|
||||
|
||||
self.add_module_request(&module_request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The `ParseModule` algorithm requires `importedBoundNames` (import entries) to be
|
||||
|
|
|
|||
Loading…
Reference in a new issue