-
-
Notifications
You must be signed in to change notification settings - Fork 438
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
15 changed files
with
555 additions
and
41 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
use crate::LintPlugins; | ||
use crate::{rules::RULES, RuleWithSeverity}; | ||
use assert_unchecked::assert_unchecked; | ||
use oxc_diagnostics::OxcDiagnostic; | ||
use rustc_hash::FxHashSet; | ||
use std::{ | ||
hash::{BuildHasher, Hash, Hasher}, | ||
path::Path, | ||
sync::Arc, | ||
}; | ||
|
||
use super::{ | ||
overrides::{OverrideId, OxlintOverrides}, | ||
LintConfig, | ||
}; | ||
use dashmap::DashMap; | ||
use heapless::Vec as StackVec; | ||
use rustc_hash::FxBuildHasher; | ||
|
||
type AppliedOverrideHash = u64; | ||
|
||
// bigger = more overrides in oxlintrc files, but more stack space taken when resolving configs. We | ||
// should tune this value based on real-world usage, but we don't collect telemetry yet. | ||
pub const MAX_OVERRIDE_COUNT: usize = 128; | ||
const _: () = { | ||
assert!(MAX_OVERRIDE_COUNT.is_power_of_two()); | ||
}; | ||
|
||
// TODO: support `categories` et. al. in overrides. | ||
#[derive(Debug)] | ||
pub(crate) struct ResolvedLinterState { | ||
// TODO: Arc + Vec -> SyncVec? It would save a pointer dereference. | ||
pub rules: Arc<[RuleWithSeverity]>, | ||
pub config: Arc<LintConfig>, | ||
} | ||
|
||
impl Clone for ResolvedLinterState { | ||
fn clone(&self) -> Self { | ||
Self { rules: Arc::clone(&self.rules), config: Arc::clone(&self.config) } | ||
} | ||
} | ||
|
||
/// Keeps track of a list of config deltas, lazily applying them to a base config as requested by | ||
/// [`ConfigStore::resolve`]. This struct is [`Sync`] + [`Send`] since the linter runs on each file | ||
/// in parallel. | ||
#[derive(Debug)] | ||
pub struct ConfigStore { | ||
// TODO: flatten base config + overrides into a single "flat" config. Similar idea to ESLint's | ||
// flat configs, but we would still support v8 configs. Doing this could open the door to | ||
// supporting flat configs (e.g. eslint.config.js). Still need to figure out how this plays | ||
// with nested configs. | ||
/// Resolved override cache. The key is a hash of each override's ID that matched the list of | ||
/// file globs in order to avoid re-allocating the same set of rules multiple times. | ||
cache: DashMap<AppliedOverrideHash, ResolvedLinterState, FxBuildHasher>, | ||
/// "root" level configuration. In the future this may just be the first entry in `overrides`. | ||
base: ResolvedLinterState, | ||
/// Config deltas applied to `base`. | ||
overrides: OxlintOverrides, | ||
} | ||
|
||
impl ConfigStore { | ||
pub fn new( | ||
base_rules: Vec<RuleWithSeverity>, | ||
base_config: LintConfig, | ||
overrides: OxlintOverrides, | ||
) -> Result<Self, OxcDiagnostic> { | ||
if overrides.len() > MAX_OVERRIDE_COUNT { | ||
return Err(OxcDiagnostic::error(format!( | ||
"Oxlint only supports up to {} overrides, but {} were provided", | ||
overrides.len(), | ||
MAX_OVERRIDE_COUNT | ||
))); | ||
} | ||
let base = ResolvedLinterState { | ||
rules: Arc::from(base_rules.into_boxed_slice()), | ||
config: Arc::new(base_config), | ||
}; | ||
// best-best case: no overrides are provided & config is initialized with 0 capacity best | ||
// case: each file matches only a single override, so we only need `overrides.len()` | ||
// capacity worst case: files match more than one override. In the most ridiculous case, we | ||
// could end up needing (overrides.len() ** 2) capacity. I don't really want to | ||
// pre-allocate that much space unconditionally. Better to re-alloc if we end up needing | ||
// it. | ||
let cache = DashMap::with_capacity_and_hasher(overrides.len(), FxBuildHasher); | ||
|
||
Ok(Self { cache, base, overrides }) | ||
} | ||
|
||
/// Set the base rules, replacing all existing rules. | ||
#[cfg(test)] | ||
#[inline] | ||
pub fn set_rules(&mut self, new_rules: Vec<RuleWithSeverity>) { | ||
self.base.rules = Arc::from(new_rules.into_boxed_slice()); | ||
} | ||
|
||
pub fn number_of_rules(&self) -> usize { | ||
self.base.rules.len() | ||
} | ||
|
||
pub fn rules(&self) -> &Arc<[RuleWithSeverity]> { | ||
&self.base.rules | ||
} | ||
|
||
pub(crate) fn resolve(&self, path: &Path) -> ResolvedLinterState { | ||
if self.overrides.is_empty() { | ||
return self.base.clone(); | ||
} | ||
|
||
// SAFETY: number of overrides is checked in constructor, and overrides cannot be added | ||
// after ConfigStore is created. | ||
unsafe { assert_unchecked!(self.overrides.len() <= MAX_OVERRIDE_COUNT) }; | ||
// Resolution gets run in a relatively tight loop. This vec allocates on the stack, kind | ||
// of like `int buf[MAX_OVERRIDE_COUNT]` in C (but we also keep track of a len). This | ||
// prevents lots of malloc/free calls, reducing heap fragmentation and system call | ||
// overhead. | ||
let mut overrides_to_apply: StackVec<OverrideId, MAX_OVERRIDE_COUNT> = StackVec::new(); | ||
let mut hasher = FxBuildHasher.build_hasher(); | ||
|
||
for (id, override_config) in self.overrides.iter_enumerated() { | ||
// SAFETY: we know that overrides_to_apply's length will always be less than or equal | ||
// to the maximum override count, which is what the capacity is set to. This assertion | ||
// helps rustc optimize away bounds checks in the loop's `.push()` calls. Rustc is | ||
// notoriously bad at optimizing away loop bounds checks, so we do it for it. | ||
unsafe { assert_unchecked!(overrides_to_apply.len() < overrides_to_apply.capacity()) }; | ||
if override_config.files.is_match(path) { | ||
overrides_to_apply.push(id).unwrap(); | ||
id.hash(&mut hasher); | ||
} | ||
} | ||
|
||
if overrides_to_apply.is_empty() { | ||
return self.base.clone(); | ||
} | ||
|
||
let key = hasher.finish(); | ||
self.cache | ||
.entry(key) | ||
.or_insert_with(|| self.apply_overrides(&overrides_to_apply)) | ||
.value() | ||
.clone() | ||
} | ||
|
||
/// NOTE: this function must not borrow any entries from `self.cache` or DashMap will deadlock. | ||
fn apply_overrides(&self, override_ids: &[OverrideId]) -> ResolvedLinterState { | ||
let plugins = self | ||
.overrides | ||
.iter() | ||
.rev() | ||
.find_map(|cfg| cfg.plugins) | ||
.unwrap_or(self.base.config.plugins); | ||
|
||
let all_rules = RULES | ||
.iter() | ||
.filter(|rule| plugins.contains(LintPlugins::from(rule.plugin_name()))) | ||
.cloned() | ||
.collect::<Vec<_>>(); | ||
let mut rules = self.base.rules.iter().cloned().collect::<FxHashSet<_>>(); | ||
|
||
let overrides = override_ids.iter().map(|id| &self.overrides[*id]); | ||
for override_config in overrides { | ||
if override_config.rules.is_empty() { | ||
continue; | ||
} | ||
override_config.rules.override_rules(&mut rules, &all_rules); | ||
} | ||
|
||
let rules = rules.into_iter().collect::<Vec<_>>(); | ||
let config = if plugins == self.base.config.plugins { | ||
Arc::clone(&self.base.config) | ||
} else { | ||
let mut config = (*self.base.config.as_ref()).clone(); | ||
|
||
config.plugins = plugins; | ||
Arc::new(config) | ||
}; | ||
|
||
ResolvedLinterState { rules: Arc::from(rules.into_boxed_slice()), config } | ||
} | ||
} |
Oops, something went wrong.