diff --git a/Cargo.lock b/Cargo.lock index 90ffdb78d..fca474344 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 35794bd7b..b7764fa0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/crates/oxc_cli/Cargo.toml b/crates/oxc_cli/Cargo.toml index 3d9c96bbf..7c1efc4f5 100644 --- a/crates/oxc_cli/Cargo.toml +++ b/crates/oxc_cli/Cargo.toml @@ -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 } diff --git a/crates/oxc_cli/src/lib.rs b/crates/oxc_cli/src/lib.rs index d6f72ce5e..9750c6cc0 100644 --- a/crates/oxc_cli/src/lib.rs +++ b/crates/oxc_cli/src/lib.rs @@ -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, diff --git a/crates/oxc_cli/src/lint/error.rs b/crates/oxc_cli/src/lint/error.rs new file mode 100644 index 000000000..69207ac66 --- /dev/null +++ b/crates/oxc_cli/src/lint/error.rs @@ -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 { + /// 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) -> Result; +} + +impl ErrorWithPath for io::Result { + fn with_path(self, path: impl AsRef) -> Result { + match self { + Ok(t) => Ok(t), + Err(error) => Err(Error::new(path.as_ref().to_path_buf(), error)), + } + } +} + +pub type Result = std::result::Result; diff --git a/crates/oxc_cli/src/lint/mod.rs b/crates/oxc_cli/src/lint/mod.rs index b23add5bf..af18c0a28 100644 --- a/crates/oxc_cli/src/lint/mod.rs +++ b/crates/oxc_cli/src/lint/mod.rs @@ -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)] diff --git a/crates/oxc_cli/src/lint/resolver.rs b/crates/oxc_cli/src/lint/resolver.rs new file mode 100644 index 000000000..8d2122949 --- /dev/null +++ b/crates/oxc_cli/src/lint/resolver.rs @@ -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 + } +} diff --git a/crates/oxc_cli/src/lint/runner_with_module_tree.rs b/crates/oxc_cli/src/lint/runner_with_module_tree.rs new file mode 100644 index 000000000..acac4eb79 --- /dev/null +++ b/crates/oxc_cli/src/lint/runner_with_module_tree.rs @@ -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, + visited: Arc>, + tx_error: Sender<(PathBuf, Vec)>, +} + +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 { + let mut rules: FxHashSet = 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::>(); + // 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)>) -> (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, +) -> (PathBuf, Vec) { + 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 = 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(()) +} diff --git a/crates/oxc_cli/src/main.rs b/crates/oxc_cli/src/main.rs index f3673ab27..900873da6 100644 --- a/crates/oxc_cli/src/main.rs +++ b/crates/oxc_cli/src/main.rs @@ -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); diff --git a/crates/oxc_cli/src/result.rs b/crates/oxc_cli/src/result.rs index bc2823ef1..ae43ddf0b 100644 --- a/crates/oxc_cli/src/result.rs +++ b/crates/oxc_cli/src/result.rs @@ -6,6 +6,7 @@ use std::{ #[derive(Debug)] pub enum CliRunResult { None, + IOError(crate::lint::Error), PathNotFound { paths: Vec, }, @@ -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, diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index ce417619b..20b9e5d65 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -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; diff --git a/crates/oxc_semantic/src/lib.rs b/crates/oxc_semantic/src/lib.rs index 3819ae653..d6e4a5f66 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -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; diff --git a/crates/oxc_semantic/src/module_record/builder.rs b/crates/oxc_semantic/src/module_record/builder.rs index 595a66392..d9b28385a 100644 --- a/crates/oxc_semantic/src/module_record/builder.rs +++ b/crates/oxc_semantic/src/module_record/builder.rs @@ -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