Skip to content

Commit

Permalink
refactor(linter): split service code into separate modules (#6437)
Browse files Browse the repository at this point in the history
Refactor in preparation of multi-config/`override` support.
  • Loading branch information
DonIsaac committed Oct 11, 2024
1 parent 702b574 commit c18c6e9
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 151 deletions.
128 changes: 128 additions & 0 deletions crates/oxc_linter/src/service/mod.rs
Original file line number Diff line number Diff line change
@@ -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<Path>,

/// All paths to lint
paths: Vec<Box<Path>>,

/// TypeScript `tsconfig.json` path for reading path alias and project references
tsconfig: Option<PathBuf>,

cross_module: bool,
}

impl LintServiceOptions {
#[must_use]
pub fn new<T>(cwd: T, paths: Vec<Box<Path>>) -> Self
where
T: Into<Box<Path>>,
{
Self { cwd: cwd.into(), paths, tsconfig: None, cross_module: false }
}

#[inline]
#[must_use]
pub fn with_tsconfig<T>(mut self, tsconfig: T) -> Self
where
T: Into<PathBuf>,
{
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<Runtime>,
}

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<crate::Message<'a>> {
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::<Vec<_>>()
}
}
34 changes: 34 additions & 0 deletions crates/oxc_linter/src/service/module_cache.rs
Original file line number Diff line number Diff line change
@@ -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<HashMap>`.
/// 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 <https://medium.com/@polyglot_factotum/rust-concurrency-patterns-condvars-and-locks-e278f18db74f>
/// and the solution is copied here to fix the issue.
pub(super) type CacheState = Mutex<FxHashMap<Box<Path>, Arc<(Mutex<CacheStateEntry>, Condvar)>>>;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum CacheStateEntry {
ReadyToConstruct,
PendingStore(usize),
}

/// Keyed by canonicalized path
pub(super) type ModuleMap = DashMap<Box<Path>, ModuleState>;

#[derive(Clone)]
pub(super) enum ModuleState {
Resolved(Arc<ModuleRecord>),
Ignored,
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,173 +7,35 @@ 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},
utils::read_to_string,
Fixer, Linter, Message,
};

pub struct LintServiceOptions {
/// Current working directory
cwd: Box<Path>,

/// All paths to lint
paths: Vec<Box<Path>>,

/// TypeScript `tsconfig.json` path for reading path alias and project references
tsconfig: Option<PathBuf>,

cross_module: bool,
}

impl LintServiceOptions {
#[must_use]
pub fn new<T>(cwd: T, paths: Vec<Box<Path>>) -> Self
where
T: Into<Box<Path>>,
{
Self { cwd: cwd.into(), paths, tsconfig: None, cross_module: false }
}

#[inline]
#[must_use]
pub fn with_tsconfig<T>(mut self, tsconfig: T) -> Self
where
T: Into<PathBuf>,
{
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<Runtime>,
}

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<Message<'a>> {
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::<Vec<_>>()
}
}

/// `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<HashMap>`.
/// 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 <https://medium.com/@polyglot_factotum/rust-concurrency-patterns-condvars-and-locks-e278f18db74f>
/// and the solution is copied here to fix the issue.
type CacheState = Mutex<FxHashMap<Box<Path>, Arc<(Mutex<CacheStateEntry>, Condvar)>>>;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CacheStateEntry {
ReadyToConstruct,
PendingStore(usize),
}

/// Keyed by canonicalized path
type ModuleMap = DashMap<Box<Path>, ModuleState>;

#[derive(Clone)]
enum ModuleState {
Resolved(Arc<ModuleRecord>),
Ignored,
}
use super::{CacheState, CacheStateEntry, LintServiceOptions, ModuleMap, ModuleState};

pub struct Runtime {
cwd: Box<Path>,
pub(super) cwd: Box<Path>,
/// All paths to lint
paths: FxHashSet<Box<Path>>,
linter: Linter,
resolver: Option<Resolver>,
module_map: ModuleMap,
cache_state: CacheState,
pub(super) paths: FxHashSet<Box<Path>>,
pub(super) linter: Linter,
pub(super) resolver: Option<Resolver>,
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"))))
});
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit c18c6e9

Please sign in to comment.