feat(linter): support overrides config field (#6974)

Part of #5653
This commit is contained in:
DonIsaac 2024-11-13 05:40:59 +00:00
parent c6a48684c3
commit 2268a0ef90
20 changed files with 757 additions and 44 deletions

2
Cargo.lock generated
View file

@ -710,6 +710,7 @@ dependencies = [
"log",
"regex-automata",
"regex-syntax",
"serde",
]
[[package]]
@ -1709,6 +1710,7 @@ dependencies = [
"markdown",
"memchr",
"mime_guess",
"nonmax",
"once_cell",
"oxc_allocator",
"oxc_ast",

View file

@ -0,0 +1,26 @@
{
"rules": {
"no-var": "error",
"no-console": "error"
},
"overrides": [
{
"files": ["*.js"],
"rules": {
"no-console": "warn"
}
},
{
"files": ["*.{js,jsx}"],
"rules": {
"no-console": "off"
}
},
{
"files": ["*.ts"],
"rules": {
"no-console": "warn"
}
}
]
}

View file

@ -0,0 +1,2 @@
var msg = "hello";
console.log(msg);

View file

@ -0,0 +1,2 @@
var msg = "hello";
console.log(msg);

View file

@ -0,0 +1,2 @@
var msg = "hello";
console.log(msg);

View file

@ -629,4 +629,25 @@ mod test {
std::fs::read_to_string("fixtures/print_config/ban_rules/expect.json").unwrap();
assert_eq!(config, expect_json.trim());
}
#[test]
fn test_overrides() {
let result =
test(&["-c", "fixtures/overrides/.oxlintrc.json", "fixtures/overrides/test.js"]);
assert_eq!(result.number_of_files, 1);
assert_eq!(result.number_of_warnings, 0);
assert_eq!(result.number_of_errors, 1);
let result =
test(&["-c", "fixtures/overrides/.oxlintrc.json", "fixtures/overrides/test.ts"]);
assert_eq!(result.number_of_files, 1);
assert_eq!(result.number_of_warnings, 1);
assert_eq!(result.number_of_errors, 1);
let result =
test(&["-c", "fixtures/overrides/.oxlintrc.json", "fixtures/overrides/other.jsx"]);
assert_eq!(result.number_of_files, 1);
assert_eq!(result.number_of_warnings, 0);
assert_eq!(result.number_of_errors, 1);
}
}

View file

@ -97,6 +97,7 @@ pub struct OxcCode {
pub scope: Option<Cow<'static, str>>,
pub number: Option<Cow<'static, str>>,
}
impl OxcCode {
pub fn is_some(&self) -> bool {
self.scope.is_some() || self.number.is_some()

View file

@ -26,27 +26,28 @@ oxc_cfg = { workspace = true }
oxc_codegen = { workspace = true }
oxc_diagnostics = { workspace = true }
oxc_ecmascript = { workspace = true }
oxc_index = { workspace = true }
oxc_index = { workspace = true, features = ["serialize"] }
oxc_macros = { workspace = true }
oxc_parser = { workspace = true }
oxc_regular_expression = { workspace = true }
oxc_resolver = { workspace = true }
oxc_semantic = { workspace = true }
oxc_span = { workspace = true, features = ["schemars", "serialize"] }
oxc_syntax = { workspace = true }
oxc_syntax = { workspace = true, features = ["serialize"] }
aho-corasick = { workspace = true }
bitflags = { workspace = true }
convert_case = { workspace = true }
cow-utils = { workspace = true }
dashmap = { workspace = true }
globset = { workspace = true }
globset = { workspace = true, features = ["serde1"] }
itertools = { workspace = true }
json-strip-comments = { workspace = true }
language-tags = { workspace = true }
lazy_static = { workspace = true }
memchr = { workspace = true }
mime_guess = { workspace = true }
nonmax = { workspace = true }
once_cell = { workspace = true }
phf = { workspace = true, features = ["macros"] }
rayon = { workspace = true }

View file

@ -7,7 +7,7 @@ use oxc_span::CompactStr;
use rustc_hash::FxHashSet;
use crate::{
config::{ESLintRule, LintPlugins, OxlintRules},
config::{ConfigStore, ESLintRule, LintPlugins, OxlintOverrides, OxlintRules},
rules::RULES,
AllowWarnDeny, FixKind, FrameworkFlags, LintConfig, LintFilter, LintFilterKind, LintOptions,
Linter, Oxlintrc, RuleCategory, RuleEnum, RuleWithSeverity,
@ -18,6 +18,7 @@ pub struct LinterBuilder {
pub(super) rules: FxHashSet<RuleWithSeverity>,
options: LintOptions,
config: LintConfig,
overrides: OxlintOverrides,
cache: RulesCache,
}
@ -36,9 +37,10 @@ impl LinterBuilder {
let options = LintOptions::default();
let config = LintConfig::default();
let rules = FxHashSet::default();
let overrides = OxlintOverrides::default();
let cache = RulesCache::new(config.plugins);
Self { rules, options, config, cache }
Self { rules, options, config, overrides, cache }
}
/// Warn on all rules in all plugins and categories, including those in `nursery`.
@ -48,6 +50,7 @@ impl LinterBuilder {
pub fn all() -> Self {
let options = LintOptions::default();
let config = LintConfig { plugins: LintPlugins::all(), ..LintConfig::default() };
let overrides = OxlintOverrides::default();
let cache = RulesCache::new(config.plugins);
Self {
rules: RULES
@ -56,6 +59,7 @@ impl LinterBuilder {
.collect(),
options,
config,
overrides,
cache,
}
}
@ -82,15 +86,22 @@ impl LinterBuilder {
/// match any recognized rules.
pub fn from_oxlintrc(start_empty: bool, oxlintrc: Oxlintrc) -> Self {
// TODO: monorepo config merging, plugin-based extends, etc.
let Oxlintrc { plugins, settings, env, globals, categories, rules: oxlintrc_rules } =
oxlintrc;
let Oxlintrc {
plugins,
settings,
env,
globals,
categories,
rules: oxlintrc_rules,
overrides,
} = oxlintrc;
let config = LintConfig { plugins, settings, env, globals };
let options = LintOptions::default();
let rules =
if start_empty { FxHashSet::default() } else { Self::warn_correctness(plugins) };
let cache = RulesCache::new(config.plugins);
let mut builder = Self { rules, options, config, cache };
let mut builder = Self { rules, options, config, overrides, cache };
if !categories.is_empty() {
builder = builder.with_filters(categories.filters());
@ -240,7 +251,8 @@ impl LinterBuilder {
self.rules.into_iter().collect::<Vec<_>>()
};
rules.sort_unstable_by_key(|r| r.id());
Linter::new(rules, self.options, self.config)
let config = ConfigStore::new(rules, self.config, self.overrides);
Linter::new(self.options, config)
}
/// Warn for all correctness rules in the given set of plugins.
@ -564,7 +576,7 @@ mod test {
desired_plugins.set(LintPlugins::TYPESCRIPT, false);
let linter = LinterBuilder::default().with_plugins(desired_plugins).build();
for rule in linter.rules() {
for rule in linter.rules().iter() {
let name = rule.name();
let plugin = rule.plugin_name();
assert_ne!(

View file

@ -0,0 +1,303 @@
use crate::LintPlugins;
use crate::{rules::RULES, RuleWithSeverity};
use rustc_hash::FxHashSet;
use std::{
hash::{BuildHasher, Hash, Hasher},
path::Path,
sync::Arc,
};
use super::{
overrides::{OverrideId, OxlintOverrides},
LintConfig,
};
use dashmap::DashMap;
use rustc_hash::FxBuildHasher;
type AppliedOverrideHash = u64;
// 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,
) -> Self {
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);
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();
}
let mut overrides_to_apply: Vec<OverrideId> = Vec::new();
let mut hasher = FxBuildHasher.build_hasher();
for (id, override_config) in self.overrides.iter_enumerated() {
if override_config.files.is_match(path) {
overrides_to_apply.push(id);
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()
.filter(|rule| plugins.contains(LintPlugins::from(rule.plugin_name())))
.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 }
}
}
#[cfg(test)]
mod test {
use super::{ConfigStore, OxlintOverrides};
use crate::{config::LintConfig, AllowWarnDeny, LintPlugins, RuleEnum, RuleWithSeverity};
macro_rules! from_json {
($json:tt) => {
serde_json::from_value(serde_json::json!($json)).unwrap()
};
}
#[allow(clippy::default_trait_access)]
fn no_explicit_any() -> RuleWithSeverity {
RuleWithSeverity::new(RuleEnum::NoExplicitAny(Default::default()), AllowWarnDeny::Warn)
}
#[allow(clippy::default_trait_access)]
fn no_cycle() -> RuleWithSeverity {
RuleWithSeverity::new(RuleEnum::NoCycle(Default::default()), AllowWarnDeny::Warn)
}
/// an empty ruleset is a no-op
#[test]
fn test_no_rules() {
let base_rules = vec![no_explicit_any()];
let overrides: OxlintOverrides = from_json!([{
"files": ["*.test.{ts,tsx}"],
"rules": {}
}]);
let store = ConfigStore::new(base_rules, LintConfig::default(), overrides);
let rules_for_source_file = store.resolve("App.tsx".as_ref());
let rules_for_test_file = store.resolve("App.test.tsx".as_ref());
assert_eq!(rules_for_source_file.rules.len(), 1);
assert_eq!(rules_for_test_file.rules.len(), 1);
assert_eq!(
rules_for_test_file.rules[0].rule.id(),
rules_for_source_file.rules[0].rule.id()
);
}
/// adding plugins but no rules is a no-op
#[test]
fn test_no_rules_and_new_plugins() {
let base_rules = vec![no_explicit_any()];
let overrides: OxlintOverrides = from_json!([{
"files": ["*.test.{ts,tsx}"],
"plugins": ["react", "typescript", "unicorn", "oxc", "jsx-a11y"],
"rules": {}
}]);
let store = ConfigStore::new(base_rules, LintConfig::default(), overrides);
let rules_for_source_file = store.resolve("App.tsx".as_ref());
let rules_for_test_file = store.resolve("App.test.tsx".as_ref());
assert_eq!(rules_for_source_file.rules.len(), 1);
assert_eq!(rules_for_test_file.rules.len(), 1);
assert_eq!(
rules_for_test_file.rules[0].rule.id(),
rules_for_source_file.rules[0].rule.id()
);
}
/// removing plugins strips rules from those plugins, even if no rules are
/// added/removed explicitly
#[test]
fn test_no_rules_and_remove_plugins() {
let base_rules = vec![no_cycle()];
let overrides = from_json!([{
"files": ["*.test.{ts,tsx}"],
"plugins": ["jest"],
"rules": {}
}]);
let config = LintConfig {
plugins: LintPlugins::default() | LintPlugins::IMPORT,
..LintConfig::default()
};
let store = ConfigStore::new(base_rules, config, overrides);
assert_eq!(store.resolve("App.tsx".as_ref()).rules.len(), 1);
assert_eq!(store.resolve("App.test.tsx".as_ref()).rules.len(), 0);
}
#[test]
fn test_remove_rule() {
let base_rules = vec![no_explicit_any()];
let overrides: OxlintOverrides = from_json!([{
"files": ["*.test.{ts,tsx}"],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}]);
let store = ConfigStore::new(base_rules, LintConfig::default(), overrides);
assert_eq!(store.number_of_rules(), 1);
let rules_for_source_file = store.resolve("App.tsx".as_ref());
assert_eq!(rules_for_source_file.rules.len(), 1);
assert!(store.resolve("App.test.tsx".as_ref()).rules.is_empty());
assert!(store.resolve("App.test.ts".as_ref()).rules.is_empty());
}
#[test]
fn test_add_rule() {
let base_rules = vec![no_explicit_any()];
let overrides = from_json!([{
"files": ["src/**/*.{ts,tsx}"],
"rules": {
"no-unused-vars": "warn"
}
}]);
let store = ConfigStore::new(base_rules, LintConfig::default(), overrides);
assert_eq!(store.number_of_rules(), 1);
assert_eq!(store.resolve("App.tsx".as_ref()).rules.len(), 1);
assert_eq!(store.resolve("src/App.tsx".as_ref()).rules.len(), 2);
assert_eq!(store.resolve("src/App.ts".as_ref()).rules.len(), 2);
assert_eq!(store.resolve("src/foo/bar/baz/App.tsx".as_ref()).rules.len(), 2);
assert_eq!(store.resolve("src/foo/bar/baz/App.spec.tsx".as_ref()).rules.len(), 2);
}
#[test]
fn test_change_rule_severity() {
let base_rules = vec![no_explicit_any()];
let overrides = from_json!([{
"files": ["src/**/*.{ts,tsx}"],
"rules": {
"no-explicit-any": "error"
}
}]);
let store = ConfigStore::new(base_rules, LintConfig::default(), overrides);
assert_eq!(store.number_of_rules(), 1);
let app = store.resolve("App.tsx".as_ref()).rules;
assert_eq!(app.len(), 1);
assert_eq!(app[0].severity, AllowWarnDeny::Warn);
let src_app = store.resolve("src/App.tsx".as_ref()).rules;
assert_eq!(src_app.len(), 1);
assert_eq!(src_app[0].severity, AllowWarnDeny::Deny);
}
}

View file

@ -1,14 +1,19 @@
mod categories;
mod env;
mod flat;
mod globals;
mod overrides;
mod oxlintrc;
mod plugins;
mod rules;
mod settings;
pub(crate) use self::flat::ResolvedLinterState;
pub use self::{
env::OxlintEnv,
flat::ConfigStore,
globals::OxlintGlobals,
overrides::OxlintOverrides,
oxlintrc::Oxlintrc,
plugins::LintPlugins,
rules::ESLintRule,
@ -16,7 +21,7 @@ pub use self::{
settings::{jsdoc::JSDocPluginSettings, OxlintSettings},
};
#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
pub(crate) struct LintConfig {
pub(crate) plugins: LintPlugins,
pub(crate) settings: OxlintSettings,

View file

@ -0,0 +1,153 @@
use std::{borrow::Cow, ops::Deref, path::Path};
use nonmax::NonMaxU32;
use schemars::{gen, schema::Schema, JsonSchema};
use serde::{de, ser, Deserialize, Serialize};
use oxc_index::{Idx, IndexVec};
use crate::{config::OxlintRules, LintPlugins};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct OverrideId(NonMaxU32);
impl Idx for OverrideId {
#[allow(clippy::cast_possible_truncation)]
fn from_usize(idx: usize) -> Self {
assert!(idx < u32::MAX as usize);
// SAFETY: We just checked `idx` is a legal value for `NonMaxU32`
Self(unsafe { NonMaxU32::new_unchecked(idx as u32) })
}
fn index(self) -> usize {
self.0.get() as usize
}
}
// nominal wrapper required to add JsonSchema impl
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct OxlintOverrides(IndexVec<OverrideId, OxlintOverride>);
impl Deref for OxlintOverrides {
type Target = IndexVec<OverrideId, OxlintOverride>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl OxlintOverrides {
#[inline]
pub fn empty() -> Self {
Self(IndexVec::new())
}
// must be explicitly defined to make serde happy
/// Returns `true` if the overrides list has no elements.
#[inline]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl JsonSchema for OxlintOverrides {
fn schema_name() -> String {
"OxlintOverrides".to_owned()
}
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed("OxlintOverrides")
}
fn json_schema(gen: &mut gen::SchemaGenerator) -> Schema {
gen.subschema_for::<Vec<OxlintOverride>>()
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
#[non_exhaustive]
pub struct OxlintOverride {
/// A list of glob patterns to override.
///
/// ## Example
/// `[ "*.test.ts", "*.spec.ts" ]`
pub files: GlobSet,
/// Optionally change what plugins are enabled for this override. When
/// omitted, the base config's plugins are used.
#[serde(default)]
pub plugins: Option<LintPlugins>,
#[serde(default)]
pub rules: OxlintRules,
}
/// A glob pattern.
///
/// Thin wrapper around [`globset::GlobSet`] because that struct doesn't implement Serialize or schemars
/// traits.
#[derive(Clone, Debug, Default)]
pub struct GlobSet {
/// Raw patterns from the config. Inefficient, but required for [serialization](Serialize),
/// which in turn is required for `--print-config`.
raw: Vec<String>,
globs: globset::GlobSet,
}
impl GlobSet {
pub fn new<S: AsRef<str>, I: IntoIterator<Item = S>>(
patterns: I,
) -> Result<Self, globset::Error> {
let patterns = patterns.into_iter();
let size_hint = patterns.size_hint();
let mut builder = globset::GlobSetBuilder::new();
let mut raw = Vec::with_capacity(size_hint.1.unwrap_or(size_hint.0));
for pattern in patterns {
let pattern = pattern.as_ref();
let glob = globset::Glob::new(pattern)?;
builder.add(glob);
raw.push(pattern.to_string());
}
let globs = builder.build()?;
Ok(Self { raw, globs })
}
pub fn is_match<P: AsRef<Path>>(&self, path: P) -> bool {
self.globs.is_match(path)
}
}
impl ser::Serialize for GlobSet {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
self.raw.serialize(serializer)
}
}
impl<'de> de::Deserialize<'de> for GlobSet {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let globs = Vec::<String>::deserialize(deserializer)?;
Self::new(globs).map_err(de::Error::custom)
}
}
impl JsonSchema for GlobSet {
fn schema_name() -> String {
Self::schema_id().into()
}
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed("GlobSet")
}
fn json_schema(gen: &mut gen::SchemaGenerator) -> Schema {
gen.subschema_for::<Vec<String>>()
}
}

View file

@ -5,8 +5,8 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{
categories::OxlintCategories, env::OxlintEnv, globals::OxlintGlobals, plugins::LintPlugins,
rules::OxlintRules, settings::OxlintSettings,
categories::OxlintCategories, env::OxlintEnv, globals::OxlintGlobals,
overrides::OxlintOverrides, plugins::LintPlugins, rules::OxlintRules, settings::OxlintSettings,
};
use crate::utils::read_to_string;
@ -30,7 +30,7 @@ use crate::utils::read_to_string;
/// ```json
/// {
/// "$schema": "./node_modules/oxlint/configuration_schema.json",
/// "plugins": ["import", "unicorn"],
/// "plugins": ["import", "typescript", "unicorn"],
/// "env": {
/// "browser": true
/// },
@ -42,7 +42,15 @@ use crate::utils::read_to_string;
/// "rules": {
/// "eqeqeq": "warn",
/// "import/no-cycle": "error"
/// }
/// },
/// "overrides": [
/// {
/// "files": ["*.test.ts", "*.spec.ts"],
/// "rules": {
/// "@typescript-eslint/no-explicit-any": "off"
/// }
/// }
/// ]
/// }
/// ```
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
@ -74,6 +82,9 @@ pub struct Oxlintrc {
pub env: OxlintEnv,
/// Enabled or disabled specific global variables.
pub globals: OxlintGlobals,
/// Add, remove, or otherwise reconfigure rules for specific files or groups of files.
#[serde(skip_serializing_if = "OxlintOverrides::is_empty")]
pub overrides: OxlintOverrides,
}
impl Oxlintrc {

View file

@ -179,9 +179,26 @@ impl<'de> Deserialize<'de> for LintPlugins {
A: de::SeqAccess<'de>,
{
let mut plugins = LintPlugins::default();
while let Some(plugin) = seq.next_element::<&str>()? {
plugins |= plugin.into();
loop {
// serde_json::from_str will provide an &str, while
// serde_json::from_value provides a String. The former is
// used in almost all cases, but the latter is more
// convenient for test cases.
match seq.next_element::<&str>() {
Ok(Some(next)) => {
plugins |= next.into();
}
Ok(None) => break,
Err(_) => {
if let Some(next) = seq.next_element::<String>()? {
plugins |= next.as_str().into();
} else {
break;
}
}
};
}
Ok(plugins)
}
}

View file

@ -135,6 +135,11 @@ impl<'a> ContextHost<'a> {
self.semantic.source_type()
}
#[inline]
pub fn plugins(&self) -> LintPlugins {
self.plugins
}
/// Add a diagnostic message to the end of the list of diagnostics. Can be used
/// by any rule to report issues.
#[inline]

View file

@ -21,9 +21,10 @@ mod utils;
pub mod loader;
pub mod table;
use crate::config::ResolvedLinterState;
use std::{io::Write, path::Path, rc::Rc, sync::Arc};
use config::LintConfig;
use config::{ConfigStore, LintConfig};
use context::ContextHost;
use options::LintOptions;
use oxc_semantic::{AstNode, Semantic};
@ -57,9 +58,10 @@ fn size_asserts() {
#[derive(Debug)]
pub struct Linter {
rules: Vec<RuleWithSeverity>,
// rules: Vec<RuleWithSeverity>,
options: LintOptions,
config: Arc<LintConfig>,
// config: Arc<LintConfig>,
config: ConfigStore,
}
impl Default for Linter {
@ -69,18 +71,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) }
pub(crate) fn new(options: LintOptions, config: ConfigStore) -> Self {
Self { options, config }
}
#[cfg(test)]
#[must_use]
pub fn with_rules(mut self, rules: Vec<RuleWithSeverity>) -> Self {
self.rules = rules;
self.config.set_rules(rules);
self
}
@ -105,20 +103,19 @@ impl Linter {
}
pub fn number_of_rules(&self) -> usize {
self.rules.len()
self.config.number_of_rules()
}
#[cfg(test)]
pub(crate) fn rules(&self) -> &Vec<RuleWithSeverity> {
&self.rules
pub(crate) fn rules(&self) -> &Arc<[RuleWithSeverity]> {
self.config.rules()
}
pub fn run<'a>(&self, path: &Path, semantic: Rc<Semantic<'a>>) -> Vec<Message<'a>> {
let ctx_host =
Rc::new(ContextHost::new(path, semantic, self.options, Arc::clone(&self.config)));
// Get config + rules for this file. Takes base rules and applies glob-based overrides.
let ResolvedLinterState { rules, config } = self.config.resolve(path);
let ctx_host = Rc::new(ContextHost::new(path, semantic, self.options, config));
let rules = self
.rules
let rules = rules
.iter()
.filter(|rule| rule.should_run(&ctx_host))
.map(|rule| (rule, Rc::clone(&ctx_host).spawn(rule)));
@ -126,7 +123,7 @@ impl Linter {
let semantic = ctx_host.semantic();
let should_run_on_jest_node =
self.config.plugins.has_test() && ctx_host.frameworks().is_test();
ctx_host.plugins().has_test() && ctx_host.frameworks().is_test();
// IMPORTANT: We have two branches here for performance reasons:
//

View file

@ -5,7 +5,7 @@ expression: json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Oxlintrc",
"description": "Oxlint Configuration File\n\nThis configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`).\n\nUsage: `oxlint -c oxlintrc.json --import-plugin`\n\n::: danger NOTE\n\nOnly the `.json` format is supported. You can use comments in configuration files.\n\n:::\n\nExample\n\n`.oxlintrc.json`\n\n```json { \"$schema\": \"./node_modules/oxlint/configuration_schema.json\", \"plugins\": [\"import\", \"unicorn\"], \"env\": { \"browser\": true }, \"globals\": { \"foo\": \"readonly\" }, \"settings\": { }, \"rules\": { \"eqeqeq\": \"warn\", \"import/no-cycle\": \"error\" } } ```",
"description": "Oxlint Configuration File\n\nThis configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`).\n\nUsage: `oxlint -c oxlintrc.json --import-plugin`\n\n::: danger NOTE\n\nOnly the `.json` format is supported. You can use comments in configuration files.\n\n:::\n\nExample\n\n`.oxlintrc.json`\n\n```json { \"$schema\": \"./node_modules/oxlint/configuration_schema.json\", \"plugins\": [\"import\", \"typescript\", \"unicorn\"], \"env\": { \"browser\": true }, \"globals\": { \"foo\": \"readonly\" }, \"settings\": { }, \"rules\": { \"eqeqeq\": \"warn\", \"import/no-cycle\": \"error\" }, \"overrides\": [ { \"files\": [\"*.test.ts\", \"*.spec.ts\"], \"rules\": { \"@typescript-eslint/no-explicit-any\": \"off\" } } ] } ```",
"type": "object",
"properties": {
"categories": {
@ -36,6 +36,14 @@ expression: json
}
]
},
"overrides": {
"description": "Add, remove, or otherwise reconfigure rules for specific files or groups of files.",
"allOf": [
{
"$ref": "#/definitions/OxlintOverrides"
}
]
},
"plugins": {
"default": [
"react",
@ -170,6 +178,12 @@ expression: json
"$ref": "#/definitions/DummyRule"
}
},
"GlobSet": {
"type": "array",
"items": {
"type": "string"
}
},
"GlobalValue": {
"type": "string",
"enum": [
@ -322,6 +336,48 @@ expression: json
"$ref": "#/definitions/GlobalValue"
}
},
"OxlintOverride": {
"type": "object",
"required": [
"files"
],
"properties": {
"files": {
"description": "A list of glob patterns to override.\n\n## Example `[ \"*.test.ts\", \"*.spec.ts\" ]`",
"allOf": [
{
"$ref": "#/definitions/GlobSet"
}
]
},
"plugins": {
"description": "Optionally change what plugins are enabled for this override. When omitted, the base config's plugins are used.",
"default": null,
"anyOf": [
{
"$ref": "#/definitions/LintPlugins"
},
{
"type": "null"
}
]
},
"rules": {
"default": {},
"allOf": [
{
"$ref": "#/definitions/OxlintRules"
}
]
}
}
},
"OxlintOverrides": {
"type": "array",
"items": {
"$ref": "#/definitions/OxlintOverride"
}
},
"OxlintRules": {
"$ref": "#/definitions/DummyRuleMap"
},

View file

@ -34,11 +34,8 @@ impl Default for RuleTable {
impl RuleTable {
pub fn new() -> Self {
let default_rules = Linter::default()
.rules
.into_iter()
.map(|rule| rule.name())
.collect::<FxHashSet<&str>>();
let default_rules =
Linter::default().rules().iter().map(|rule| rule.name()).collect::<FxHashSet<&str>>();
let mut rows = RULES
.iter()

View file

@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Oxlintrc",
"description": "Oxlint Configuration File\n\nThis configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`).\n\nUsage: `oxlint -c oxlintrc.json --import-plugin`\n\n::: danger NOTE\n\nOnly the `.json` format is supported. You can use comments in configuration files.\n\n:::\n\nExample\n\n`.oxlintrc.json`\n\n```json { \"$schema\": \"./node_modules/oxlint/configuration_schema.json\", \"plugins\": [\"import\", \"unicorn\"], \"env\": { \"browser\": true }, \"globals\": { \"foo\": \"readonly\" }, \"settings\": { }, \"rules\": { \"eqeqeq\": \"warn\", \"import/no-cycle\": \"error\" } } ```",
"description": "Oxlint Configuration File\n\nThis configuration is aligned with ESLint v8's configuration schema (`eslintrc.json`).\n\nUsage: `oxlint -c oxlintrc.json --import-plugin`\n\n::: danger NOTE\n\nOnly the `.json` format is supported. You can use comments in configuration files.\n\n:::\n\nExample\n\n`.oxlintrc.json`\n\n```json { \"$schema\": \"./node_modules/oxlint/configuration_schema.json\", \"plugins\": [\"import\", \"typescript\", \"unicorn\"], \"env\": { \"browser\": true }, \"globals\": { \"foo\": \"readonly\" }, \"settings\": { }, \"rules\": { \"eqeqeq\": \"warn\", \"import/no-cycle\": \"error\" }, \"overrides\": [ { \"files\": [\"*.test.ts\", \"*.spec.ts\"], \"rules\": { \"@typescript-eslint/no-explicit-any\": \"off\" } } ] } ```",
"type": "object",
"properties": {
"categories": {
@ -32,6 +32,14 @@
}
]
},
"overrides": {
"description": "Add, remove, or otherwise reconfigure rules for specific files or groups of files.",
"allOf": [
{
"$ref": "#/definitions/OxlintOverrides"
}
]
},
"plugins": {
"default": [
"react",
@ -166,6 +174,12 @@
"$ref": "#/definitions/DummyRule"
}
},
"GlobSet": {
"type": "array",
"items": {
"type": "string"
}
},
"GlobalValue": {
"type": "string",
"enum": [
@ -318,6 +332,48 @@
"$ref": "#/definitions/GlobalValue"
}
},
"OxlintOverride": {
"type": "object",
"required": [
"files"
],
"properties": {
"files": {
"description": "A list of glob patterns to override.\n\n## Example `[ \"*.test.ts\", \"*.spec.ts\" ]`",
"allOf": [
{
"$ref": "#/definitions/GlobSet"
}
]
},
"plugins": {
"description": "Optionally change what plugins are enabled for this override. When omitted, the base config's plugins are used.",
"default": null,
"anyOf": [
{
"$ref": "#/definitions/LintPlugins"
},
{
"type": "null"
}
]
},
"rules": {
"default": {},
"allOf": [
{
"$ref": "#/definitions/OxlintRules"
}
]
}
}
},
"OxlintOverrides": {
"type": "array",
"items": {
"$ref": "#/definitions/OxlintOverride"
}
},
"OxlintRules": {
"$ref": "#/definitions/DummyRuleMap"
},

View file

@ -23,6 +23,7 @@ Example
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"import",
"typescript",
"unicorn"
],
"env": {
@ -35,7 +36,18 @@ Example
"rules": {
"eqeqeq": "warn",
"import/no-cycle": "error"
}
},
"overrides": [
{
"files": [
"*.test.ts",
"*.spec.ts"
],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}
]
}
```
@ -148,6 +160,38 @@ Globals can be disabled by setting their value to `"off"`. For example, in an en
You may also use `"readable"` or `false` to represent `"readonly"`, and `"writeable"` or `true` to represent `"writable"`.
## overrides
type: `array`
### overrides[n]
type: `object`
#### overrides[n].files
type: `string[]`
#### overrides[n].rules
type: `object`
See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html)
## plugins
type: `string[]`