Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(linter): Add LinterBuilder #5714

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 294 additions & 0 deletions crates/oxc_linter/src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
use std::{
cell::{Ref, RefCell},
fmt,
};

use rustc_hash::FxHashSet;

use crate::{
options::LintPlugins, rules::RULES, AllowWarnDeny, FixKind, FrameworkFlags, LintConfig,
LintFilter, LintFilterKind, LintOptions, Linter, Oxlintrc, RuleCategory, RuleEnum,
RuleWithSeverity,
};

#[must_use = "You dropped your builder without building a Linter! Did you mean to call .build()?"]
pub struct LinterBuilder {
rules: FxHashSet<RuleWithSeverity>,
options: LintOptions,
config: LintConfig,
cache: RulesCache,
}

impl Default for LinterBuilder {
fn default() -> Self {
Self { rules: Self::warn_correctness(LintPlugins::default()), ..Self::empty() }
}
}

impl LinterBuilder {
/// Create a [`LinterBuilder`] with default plugins enabled and no
/// configured rules.
///
/// You can think of this as `oxlint -A all`.
pub fn empty() -> Self {
let options = LintOptions::default();
let cache = RulesCache::new(options.plugins);
Self { rules: FxHashSet::default(), options, config: LintConfig::default(), cache }
}

/// Warn on all rules in all plugins and categories, including those in `nursery`.
/// This is the kitchen sink.
///
/// You can think of this as `oxlint -W all -W nursery`.
pub fn all() -> Self {
let options = LintOptions { plugins: LintPlugins::all(), ..LintOptions::default() };
let cache = RulesCache::new(options.plugins);
Self {
rules: RULES
.iter()
.map(|rule| RuleWithSeverity { rule: rule.clone(), severity: AllowWarnDeny::Warn })
.collect(),
options,
config: LintConfig::default(),
cache,
}
}

/// Create a [`LinterBuilder`] from a loaded or manually built [`Oxlintrc`].
/// `start_empty` will configure the builder to contain only the
/// configuration settings from the config. When this is `false`, the config
/// will be applied on top of a default [`Oxlintrc`].
///
/// # Example
/// Here's how to create a [`Linter`] from a `.oxlintrc.json` file.
/// ```
/// use oxc_linter::{LinterBuilder, Oxlintrc};
/// let oxlintrc = Oxlintrc::from_file("path/to/.oxlintrc.json").unwrap();
/// let linter = LinterBuilder::from_oxlintrc(true, oxlintrc).build();
/// // you can use `From` as a shorthand for `from_oxlintrc(false, oxlintrc)`
/// let linter = LinterBuilder::from(oxlintrc).build();
/// ```
pub fn from_oxlintrc(start_empty: bool, oxlintrc: Oxlintrc) -> Self {
// TODO: monorepo config merging, plugin-based extends, etc.
let Oxlintrc { plugins, settings, env, globals, rules: oxlintrc_rules } = oxlintrc;

let config = LintConfig { settings, env, globals };
let options = LintOptions { plugins, ..Default::default() };
let rules =
if start_empty { FxHashSet::default() } else { Self::warn_correctness(plugins) };
let cache = RulesCache::new(options.plugins);
let mut builder = Self { rules, options, config, cache };

{
let all_rules = builder.cache.borrow();
oxlintrc_rules.override_rules(&mut builder.rules, all_rules.as_slice());
}

builder
}

#[inline]
pub fn with_framework_hints(mut self, flags: FrameworkFlags) -> Self {
self.options.framework_hints = flags;
self
}

#[inline]
pub fn and_framework_hints(mut self, flags: FrameworkFlags) -> Self {
self.options.framework_hints |= flags;
self
}

#[inline]
pub fn with_fix(mut self, fix: FixKind) -> Self {
self.options.fix = fix;
self
}

#[inline]
pub fn with_plugins(mut self, plugins: LintPlugins) -> Self {
self.options.plugins = plugins;
self.cache.set_plugins(plugins);
self
}

#[inline]
pub fn and_plugins(mut self, plugins: LintPlugins, enabled: bool) -> Self {
self.options.plugins.set(plugins, enabled);
self.cache.set_plugins(self.options.plugins);
self
}

#[cfg(test)]
pub(crate) fn with_rule(mut self, rule: RuleWithSeverity) -> Self {
self.rules.insert(rule);
self
}

pub fn with_filters<I: IntoIterator<Item = LintFilter>>(mut self, filters: I) -> Self {
for filter in filters {
self = self.with_filter(filter);
}
self
}

pub fn with_filter(mut self, filter: LintFilter) -> Self {
let (severity, filter) = filter.into();
let all_rules = self.cache.borrow();

match severity {
AllowWarnDeny::Deny | AllowWarnDeny::Warn => match filter {
LintFilterKind::Category(category) => {
self.rules.extend(
all_rules
.iter()
.filter(|rule| rule.category() == category)
.map(|rule| RuleWithSeverity::new(rule.clone(), severity)),
);
}
LintFilterKind::Rule(_, name) => {
self.rules.extend(
all_rules
.iter()
.filter(|rule| rule.name() == name)
.map(|rule| RuleWithSeverity::new(rule.clone(), severity)),
);
}
LintFilterKind::Generic(name_or_category) => {
if name_or_category == "all" {
self.rules.extend(
all_rules
.iter()
.filter(|rule| rule.category() != RuleCategory::Nursery)
.map(|rule| RuleWithSeverity::new(rule.clone(), severity)),
);
} else {
self.rules.extend(
all_rules
.iter()
.filter(|rule| rule.name() == name_or_category)
.map(|rule| RuleWithSeverity::new(rule.clone(), severity)),
);
}
}
},
AllowWarnDeny::Allow => match filter {
LintFilterKind::Category(category) => {
self.rules.retain(|rule| rule.category() != category);
}
LintFilterKind::Rule(_, name) => {
self.rules.retain(|rule| rule.name() != name);
}
LintFilterKind::Generic(name_or_category) => {
if name_or_category == "all" {
self.rules.clear();
} else {
self.rules.retain(|rule| rule.name() != name_or_category);
}
}
},
}
drop(all_rules);

self
}

#[must_use]
pub fn build(self) -> Linter {
let mut rules = self.rules.into_iter().collect::<Vec<_>>();
rules.sort_unstable_by_key(|r| r.id());
Linter::new(rules, self.options, self.config)
}

/// Warn for all correctness rules in the given set of plugins.
fn warn_correctness(plugins: LintPlugins) -> FxHashSet<RuleWithSeverity> {
RULES
.iter()
.filter(|rule| {
// NOTE: this logic means there's no way to disable ESLint
// correctness rules. I think that's fine for now.
rule.category() == RuleCategory::Correctness
&& plugins.contains(LintPlugins::from(rule.plugin_name()))
})
.map(|rule| RuleWithSeverity { rule: rule.clone(), severity: AllowWarnDeny::Warn })
.collect()
}
}

impl From<Oxlintrc> for LinterBuilder {
#[inline]
fn from(oxlintrc: Oxlintrc) -> Self {
Self::from_oxlintrc(false, oxlintrc)
}
}

impl fmt::Debug for LinterBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LinterBuilder")
.field("rules", &self.rules)
.field("options", &self.options)
.field("config", &self.config)
.finish_non_exhaustive()
}
}

struct RulesCache(RefCell<Option<Vec<RuleEnum>>>, LintPlugins);
impl RulesCache {
#[inline]
#[must_use]
pub fn new(plugins: LintPlugins) -> Self {
Self(RefCell::new(None), plugins)
}

pub fn set_plugins(&mut self, plugins: LintPlugins) {
self.1 = plugins;
self.clear();
}

#[must_use]
fn borrow(&self) -> Ref<'_, Vec<RuleEnum>> {
let cached = self.0.borrow();
if cached.is_some() {
Ref::map(cached, |cached| cached.as_ref().unwrap())
} else {
drop(cached);
self.initialize();
Ref::map(self.0.borrow(), |cached| cached.as_ref().unwrap())
}
}

/// # Panics
/// If the cache cell is currently borrowed.
fn clear(&self) {
*self.0.borrow_mut() = None;
}

/// Forcefully initialize this cache with all rules in all plugins currently
/// enabled.
///
/// This will clobber whatever value is currently stored. It should only be
/// called when the cache is not populated, either because it has not been
/// initialized yet or it was cleared with [`Self::clear`].
///
/// # Panics
/// If the cache cell is currently borrowed.
fn initialize(&self) {
debug_assert!(
self.0.borrow().is_none(),
"Cannot re-initialize a populated rules cache. It must be cleared first."
);

let mut all_rules: Vec<_> = if self.1.is_all() {
RULES.clone()
} else {
RULES
.iter()
.filter(|rule| self.1.contains(LintPlugins::from(rule.plugin_name())))
.cloned()
.collect()
};
all_rules.sort_unstable(); // TODO: do we need to sort? is is already sorted?

*self.0.borrow_mut() = Some(all_rules);
}
}
2 changes: 1 addition & 1 deletion crates/oxc_linter/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ mod test {
}));
assert!(config.is_ok());

let Oxlintrc { rules, settings, env, globals } = config.unwrap();
let Oxlintrc { rules, settings, env, globals, .. } = config.unwrap();
assert!(!rules.is_empty());
assert_eq!(
settings.jsx_a11y.polymorphic_prop_name.as_ref().map(CompactStr::as_str),
Expand Down
4 changes: 3 additions & 1 deletion crates/oxc_linter/src/config/oxlintrc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};

use super::{env::OxlintEnv, globals::OxlintGlobals, rules::OxlintRules, settings::OxlintSettings};

use crate::utils::read_to_string;
use crate::{options::LintPlugins, utils::read_to_string};

/// Oxlint Configuration File
///
Expand Down Expand Up @@ -42,7 +42,9 @@ use crate::utils::read_to_string;
/// ```
#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)]
#[serde(default)]
#[non_exhaustive]
pub struct Oxlintrc {
pub plugins: LintPlugins,
/// See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html).
pub rules: OxlintRules,
pub settings: OxlintSettings,
Expand Down
20 changes: 11 additions & 9 deletions crates/oxc_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
mod tester;

mod ast_util;
mod builder;
mod config;
mod context;
mod disable_directives;
Expand All @@ -29,11 +30,12 @@ use oxc_diagnostics::Error;
use oxc_semantic::{AstNode, Semantic};

pub use crate::{
builder::LinterBuilder,
config::Oxlintrc,
context::LintContext,
fixer::FixKind,
frameworks::FrameworkFlags,
options::{AllowWarnDeny, InvalidFilterKind, LintFilter, OxlintOptions},
options::{AllowWarnDeny, InvalidFilterKind, LintFilter, LintFilterKind, OxlintOptions},
rule::{RuleCategory, RuleFixMeta, RuleMeta, RuleWithSeverity},
service::{LintService, LintServiceOptions},
};
Expand Down Expand Up @@ -67,6 +69,14 @@ impl Default for Linter {
}

impl Linter {
pub(crate) fn new(
rules: Vec<RuleWithSeverity>,
options: LintOptions,
config: LintConfig,
) -> Self {
Self { rules, options, config: Arc::new(config) }
}

/// # Errors
///
/// Returns `Err` if there are any errors parsing the configuration file.
Expand All @@ -82,14 +92,6 @@ impl Linter {
self
}

/// Used for testing
#[cfg(test)]
#[must_use]
pub(crate) fn with_eslint_config(mut self, config: LintConfig) -> Self {
self.config = Arc::new(config);
self
}

/// Set the kind of auto fixes to apply.
///
/// # Example
Expand Down
3 changes: 1 addition & 2 deletions crates/oxc_linter/src/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ mod plugins;

use std::{convert::From, path::PathBuf};

use filter::LintFilterKind;
use oxc_diagnostics::Error;
use rustc_hash::FxHashSet;

pub use allow_warn_deny::AllowWarnDeny;
pub use filter::{InvalidFilterKind, LintFilter};
pub use filter::{InvalidFilterKind, LintFilter, LintFilterKind};
pub use plugins::{LintPluginOptions, LintPlugins};

use crate::{
Expand Down
Loading