diff --git a/crates/oxc_linter/src/service/mod.rs b/crates/oxc_linter/src/service/mod.rs new file mode 100644 index 0000000000000..1566db3fb0a70 --- /dev/null +++ b/crates/oxc_linter/src/service/mod.rs @@ -0,0 +1,128 @@ +mod module_cache; +mod runtime; + +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use oxc_diagnostics::DiagnosticSender; +use rayon::{iter::ParallelBridge, prelude::ParallelIterator}; + +use crate::Linter; + +use module_cache::{CacheState, CacheStateEntry, ModuleMap, ModuleState}; +use runtime::Runtime; + +pub struct LintServiceOptions { + /// Current working directory + cwd: Box, + + /// All paths to lint + paths: Vec>, + + /// TypeScript `tsconfig.json` path for reading path alias and project references + tsconfig: Option, + + cross_module: bool, +} + +impl LintServiceOptions { + #[must_use] + pub fn new(cwd: T, paths: Vec>) -> Self + where + T: Into>, + { + Self { cwd: cwd.into(), paths, tsconfig: None, cross_module: false } + } + + #[inline] + #[must_use] + pub fn with_tsconfig(mut self, tsconfig: T) -> Self + where + T: Into, + { + let tsconfig = tsconfig.into(); + // Should this be canonicalized? + let tsconfig = if tsconfig.is_relative() { self.cwd.join(tsconfig) } else { tsconfig }; + debug_assert!(tsconfig.is_file()); + + self.tsconfig = Some(tsconfig); + self + } + + #[inline] + #[must_use] + pub fn with_cross_module(mut self, cross_module: bool) -> Self { + self.cross_module = cross_module; + self + } + + #[inline] + pub fn cwd(&self) -> &Path { + &self.cwd + } +} + +#[derive(Clone)] +pub struct LintService { + runtime: Arc, +} + +impl LintService { + pub fn new(linter: Linter, options: LintServiceOptions) -> Self { + let runtime = Arc::new(Runtime::new(linter, options)); + Self { runtime } + } + + #[cfg(test)] + pub(crate) fn from_linter(linter: Linter, options: LintServiceOptions) -> Self { + let runtime = Arc::new(Runtime::new(linter, options)); + Self { runtime } + } + + pub fn linter(&self) -> &Linter { + &self.runtime.linter + } + + pub fn number_of_dependencies(&self) -> usize { + self.runtime.module_map.len() - self.runtime.paths.len() + } + + /// # Panics + pub fn run(&self, tx_error: &DiagnosticSender) { + self.runtime + .paths + .iter() + .par_bridge() + .for_each_with(&self.runtime, |runtime, path| runtime.process_path(path, tx_error)); + tx_error.send(None).unwrap(); + } + + /// For tests + #[cfg(test)] + pub(crate) fn run_source<'a>( + &self, + allocator: &'a oxc_allocator::Allocator, + source_text: &'a str, + check_syntax_errors: bool, + tx_error: &DiagnosticSender, + ) -> Vec> { + self.runtime + .paths + .iter() + .flat_map(|path| { + let source_type = oxc_span::SourceType::from_path(path).unwrap(); + self.runtime.init_cache_state(path); + self.runtime.process_source( + path, + allocator, + source_text, + source_type, + check_syntax_errors, + tx_error, + ) + }) + .collect::>() + } +} diff --git a/crates/oxc_linter/src/service/module_cache.rs b/crates/oxc_linter/src/service/module_cache.rs new file mode 100644 index 0000000000000..bf2e7d6a89666 --- /dev/null +++ b/crates/oxc_linter/src/service/module_cache.rs @@ -0,0 +1,34 @@ +use std::{ + path::Path, + sync::{Arc, Condvar, Mutex}, +}; + +use dashmap::DashMap; +use oxc_semantic::ModuleRecord; +use rustc_hash::FxHashMap; + +/// `CacheState` and `CacheStateEntry` are used to fix the problem where +/// there is a brief moment when a concurrent fetch can miss the cache. +/// +/// Given `ModuleMap` is a `DashMap`, which conceptually is a `RwLock`. +/// When two requests read the map at the exact same time from different threads, +/// both will miss the cache so both thread will make a request. +/// +/// See the "problem section" in +/// and the solution is copied here to fix the issue. +pub(super) type CacheState = Mutex, Arc<(Mutex, Condvar)>>>; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum CacheStateEntry { + ReadyToConstruct, + PendingStore(usize), +} + +/// Keyed by canonicalized path +pub(super) type ModuleMap = DashMap, ModuleState>; + +#[derive(Clone)] +pub(super) enum ModuleState { + Resolved(Arc), + Ignored, +} diff --git a/crates/oxc_linter/src/service.rs b/crates/oxc_linter/src/service/runtime.rs similarity index 74% rename from crates/oxc_linter/src/service.rs rename to crates/oxc_linter/src/service/runtime.rs index 4564dd9099b73..3dab419dd9bf1 100644 --- a/crates/oxc_linter/src/service.rs +++ b/crates/oxc_linter/src/service/runtime.rs @@ -7,15 +7,14 @@ use std::{ sync::{Arc, Condvar, Mutex}, }; -use dashmap::DashMap; use oxc_allocator::Allocator; use oxc_diagnostics::{DiagnosticSender, DiagnosticService, Error, OxcDiagnostic}; use oxc_parser::{ParseOptions, Parser}; use oxc_resolver::Resolver; -use oxc_semantic::{ModuleRecord, SemanticBuilder}; +use oxc_semantic::SemanticBuilder; use oxc_span::{SourceType, VALID_EXTENSIONS}; use rayon::{iter::ParallelBridge, prelude::ParallelIterator}; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashSet; use crate::{ loader::{JavaScriptSource, PartialLoader, LINT_PARTIAL_LOADER_EXT}, @@ -23,157 +22,20 @@ use crate::{ Fixer, Linter, Message, }; -pub struct LintServiceOptions { - /// Current working directory - cwd: Box, - - /// All paths to lint - paths: Vec>, - - /// TypeScript `tsconfig.json` path for reading path alias and project references - tsconfig: Option, - - cross_module: bool, -} - -impl LintServiceOptions { - #[must_use] - pub fn new(cwd: T, paths: Vec>) -> Self - where - T: Into>, - { - Self { cwd: cwd.into(), paths, tsconfig: None, cross_module: false } - } - - #[inline] - #[must_use] - pub fn with_tsconfig(mut self, tsconfig: T) -> Self - where - T: Into, - { - let tsconfig = tsconfig.into(); - // Should this be canonicalized? - let tsconfig = if tsconfig.is_relative() { self.cwd.join(tsconfig) } else { tsconfig }; - debug_assert!(tsconfig.is_file()); - - self.tsconfig = Some(tsconfig); - self - } - - #[inline] - #[must_use] - pub fn with_cross_module(mut self, cross_module: bool) -> Self { - self.cross_module = cross_module; - self - } - - #[inline] - pub fn cwd(&self) -> &Path { - &self.cwd - } -} - -#[derive(Clone)] -pub struct LintService { - runtime: Arc, -} - -impl LintService { - pub fn new(linter: Linter, options: LintServiceOptions) -> Self { - let runtime = Arc::new(Runtime::new(linter, options)); - Self { runtime } - } - - #[cfg(test)] - pub(crate) fn from_linter(linter: Linter, options: LintServiceOptions) -> Self { - let runtime = Arc::new(Runtime::new(linter, options)); - Self { runtime } - } - - pub fn linter(&self) -> &Linter { - &self.runtime.linter - } - - pub fn number_of_dependencies(&self) -> usize { - self.runtime.module_map.len() - self.runtime.paths.len() - } - - /// # Panics - pub fn run(&self, tx_error: &DiagnosticSender) { - self.runtime - .paths - .iter() - .par_bridge() - .for_each_with(&self.runtime, |runtime, path| runtime.process_path(path, tx_error)); - tx_error.send(None).unwrap(); - } - - /// For tests - #[cfg(test)] - pub(crate) fn run_source<'a>( - &self, - allocator: &'a Allocator, - source_text: &'a str, - check_syntax_errors: bool, - tx_error: &DiagnosticSender, - ) -> Vec> { - self.runtime - .paths - .iter() - .flat_map(|path| { - let source_type = SourceType::from_path(path).unwrap(); - self.runtime.init_cache_state(path); - self.runtime.process_source( - path, - allocator, - source_text, - source_type, - check_syntax_errors, - tx_error, - ) - }) - .collect::>() - } -} - -/// `CacheState` and `CacheStateEntry` are used to fix the problem where -/// there is a brief moment when a concurrent fetch can miss the cache. -/// -/// Given `ModuleMap` is a `DashMap`, which conceptually is a `RwLock`. -/// When two requests read the map at the exact same time from different threads, -/// both will miss the cache so both thread will make a request. -/// -/// See the "problem section" in -/// and the solution is copied here to fix the issue. -type CacheState = Mutex, Arc<(Mutex, Condvar)>>>; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum CacheStateEntry { - ReadyToConstruct, - PendingStore(usize), -} - -/// Keyed by canonicalized path -type ModuleMap = DashMap, ModuleState>; - -#[derive(Clone)] -enum ModuleState { - Resolved(Arc), - Ignored, -} +use super::{CacheState, CacheStateEntry, LintServiceOptions, ModuleMap, ModuleState}; pub struct Runtime { - cwd: Box, + pub(super) cwd: Box, /// All paths to lint - paths: FxHashSet>, - linter: Linter, - resolver: Option, - module_map: ModuleMap, - cache_state: CacheState, + pub(super) paths: FxHashSet>, + pub(super) linter: Linter, + pub(super) resolver: Option, + pub(super) module_map: ModuleMap, + pub(super) cache_state: CacheState, } impl Runtime { - fn new(linter: Linter, options: LintServiceOptions) -> Self { + pub(super) fn new(linter: Linter, options: LintServiceOptions) -> Self { let resolver = options.cross_module.then(|| { Self::get_resolver(options.tsconfig.or_else(|| Some(options.cwd.join("tsconfig.json")))) }); @@ -230,7 +92,7 @@ impl Runtime { // clippy: the source field is checked and assumed to be less than 4GB, and // we assume that the fix offset will not exceed 2GB in either direction #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - fn process_path(&self, path: &Path, tx_error: &DiagnosticSender) { + pub(super) fn process_path(&self, path: &Path, tx_error: &DiagnosticSender) { if self.init_cache_state(path) { return; } @@ -318,7 +180,7 @@ impl Runtime { } #[allow(clippy::too_many_arguments)] - fn process_source<'a>( + pub(super) fn process_source<'a>( &self, path: &Path, allocator: &'a Allocator, @@ -434,7 +296,7 @@ impl Runtime { self.linter.run(path, Rc::new(semantic_ret.semantic)) } - fn init_cache_state(&self, path: &Path) -> bool { + pub(super) fn init_cache_state(&self, path: &Path) -> bool { if self.resolver.is_none() { return false; }