mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
refactor(linter): split service code into separate modules (#6437)
Refactor in preparation of multi-config/`override` support.
This commit is contained in:
parent
702b574afb
commit
c18c6e9db1
3 changed files with 175 additions and 151 deletions
128
crates/oxc_linter/src/service/mod.rs
Normal file
128
crates/oxc_linter/src/service/mod.rs
Normal file
|
|
@ -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
crates/oxc_linter/src/service/module_cache.rs
Normal file
34
crates/oxc_linter/src/service/module_cache.rs
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -7,15 +7,14 @@ use std::{
|
||||||
sync::{Arc, Condvar, Mutex},
|
sync::{Arc, Condvar, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
|
||||||
use oxc_allocator::Allocator;
|
use oxc_allocator::Allocator;
|
||||||
use oxc_diagnostics::{DiagnosticSender, DiagnosticService, Error, OxcDiagnostic};
|
use oxc_diagnostics::{DiagnosticSender, DiagnosticService, Error, OxcDiagnostic};
|
||||||
use oxc_parser::{ParseOptions, Parser};
|
use oxc_parser::{ParseOptions, Parser};
|
||||||
use oxc_resolver::Resolver;
|
use oxc_resolver::Resolver;
|
||||||
use oxc_semantic::{ModuleRecord, SemanticBuilder};
|
use oxc_semantic::SemanticBuilder;
|
||||||
use oxc_span::{SourceType, VALID_EXTENSIONS};
|
use oxc_span::{SourceType, VALID_EXTENSIONS};
|
||||||
use rayon::{iter::ParallelBridge, prelude::ParallelIterator};
|
use rayon::{iter::ParallelBridge, prelude::ParallelIterator};
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
loader::{JavaScriptSource, PartialLoader, LINT_PARTIAL_LOADER_EXT},
|
loader::{JavaScriptSource, PartialLoader, LINT_PARTIAL_LOADER_EXT},
|
||||||
|
|
@ -23,157 +22,20 @@ use crate::{
|
||||||
Fixer, Linter, Message,
|
Fixer, Linter, Message,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct LintServiceOptions {
|
use super::{CacheState, CacheStateEntry, LintServiceOptions, ModuleMap, ModuleState};
|
||||||
/// 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Runtime {
|
pub struct Runtime {
|
||||||
cwd: Box<Path>,
|
pub(super) cwd: Box<Path>,
|
||||||
/// All paths to lint
|
/// All paths to lint
|
||||||
paths: FxHashSet<Box<Path>>,
|
pub(super) paths: FxHashSet<Box<Path>>,
|
||||||
linter: Linter,
|
pub(super) linter: Linter,
|
||||||
resolver: Option<Resolver>,
|
pub(super) resolver: Option<Resolver>,
|
||||||
module_map: ModuleMap,
|
pub(super) module_map: ModuleMap,
|
||||||
cache_state: CacheState,
|
pub(super) cache_state: CacheState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Runtime {
|
impl Runtime {
|
||||||
fn new(linter: Linter, options: LintServiceOptions) -> Self {
|
pub(super) fn new(linter: Linter, options: LintServiceOptions) -> Self {
|
||||||
let resolver = options.cross_module.then(|| {
|
let resolver = options.cross_module.then(|| {
|
||||||
Self::get_resolver(options.tsconfig.or_else(|| Some(options.cwd.join("tsconfig.json"))))
|
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
|
// 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
|
// we assume that the fix offset will not exceed 2GB in either direction
|
||||||
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
|
#[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) {
|
if self.init_cache_state(path) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -318,7 +180,7 @@ impl Runtime {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn process_source<'a>(
|
pub(super) fn process_source<'a>(
|
||||||
&self,
|
&self,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
allocator: &'a Allocator,
|
allocator: &'a Allocator,
|
||||||
|
|
@ -434,7 +296,7 @@ impl Runtime {
|
||||||
self.linter.run(path, Rc::new(semantic_ret.semantic))
|
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() {
|
if self.resolver.is_none() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue