From 80266d85c9e192ebf91ef0e2c687b6f769d0b8b2 Mon Sep 17 00:00:00 2001 From: DonIsaac <22823424+DonIsaac@users.noreply.github.com> Date: Thu, 10 Oct 2024 19:21:50 +0000 Subject: [PATCH] feat(linter)!: support plugins in oxlint config files (#6088) > Closes #5046 This PR migrates the linter crate and oxlint app to use the new `LinterBuilder` API. This PR has the following effects: 1. `plugins` in oxlint config files are now supported 2. irons out weirdness when using CLI args and config files together. CLI args are now always applied on top of config file settings, overriding them. # Breaking Changes Before, config files would override rules set in CLI arguments. For example, running this command: ```sh oxlint -A correctness -c oxlintrc.json ``` With this config file ```jsonc // oxlintrc.json { "rules": { "no-const-assign": "error" } } ``` Would result in a single rule, `no-const-assign` being turned on at an error level with all other rules disabled (i.e. set to "allow"). Now, **CLI arguments will override config files**. That same command with the same config file will result with **all rules being disabled**. ## Details For a more in-depth explanation, assume we are running the below command using the `oxlintrc.json` file above: ```sh oxlint -A all -W correctness -A all -W suspicious -c oxlintrc.json ``` ### Before > Note: GitHub doesn't seem to like deeply nested lists. Apologies for the formatting. Here was the config resolution process _before_ this PR:
Before Steps 1. Start with a default set of filters (["correctness", "warn"]) if no filters were passed to the CLI. Since some were, the filter list starts out empty. 2. Apply each filter taken from the CLI from left to right. When a filter allows a rule or category, it clears the configured set of rules. So applying those filters looks like this a. start with an empty list `[]` b. `("all", "allow")` -> `[]` c. `("correctness", "warn")` -> `[ ]` d. `("all", "allow")` -> `[]` e. `("suspicious", "warn")` -> `[ ]`. This is the final rule set for this step 3. Apply overrides from `oxlintrc.json`. This is where things get a little funky, as mentioned in point 2 of what this PR does. At this point, all rules in the rules list are only from the CLI. a. If a rule is only set in the CLI and is not present in the config file, there's no effect b. If a rule is in the config file but not the CLI, it gets inserted into the list. c. If a rule is already in the list and in the config file i. If the rule is only present once (e.g. `"no-loss-of-precision": "error"`), unconditionally override whatever was in the CLI with what was set in the config file ii. If the rule is present twice (e.g. `"no-loss-of-precision": "off", "@typescript-eslint/no-loss-of-precision": "error"`), a. if all rules in the config file are set to `allow`, then turn the rule off b. If one of them is `warn` or `deny`, then update the currently-set rule's config object, but _leave its severity alone_. So, for our example, the final rule set would be `[, no-const-assign: "error"]`
### After Rule filters were completely re-worked in a previous PR. Now, lint filters aren't kept on hand-only the rule list is.
After Steps 1. Start with the default rule set, which is all correctness rules for all enabled plugins (`[]`) 2. Apply configuration from `oxlintrc.json` _first_. a. If the rule is warn/deny exists in the list already, update its severity and config object. If it's not in the list, add it. b. If the rule is set to allow, remove it from the list. The list is now `[, no-const-assign: "error"]`. 3. Apply each filter taken from the CLI from left to right. This works the same way as before. So, after they're applied, the list is now `[]`
--- apps/oxlint/fixtures/import/.oxlintrc.json | 7 + apps/oxlint/fixtures/import/test.js | 9 + .../fixtures/typescript_eslint/eslintrc.json | 2 + apps/oxlint/src/command/lint.rs | 199 +++++++++++++++--- apps/oxlint/src/lint/mod.rs | 64 +++--- crates/oxc_linter/src/config/env.rs | 1 + crates/oxc_linter/src/config/oxlintrc.rs | 45 ++++ crates/oxc_linter/src/config/rules.rs | 2 + .../oxc_linter/src/config/settings/jsdoc.rs | 2 + .../src/config/settings/jsx_a11y.rs | 1 + crates/oxc_linter/src/config/settings/mod.rs | 1 + crates/oxc_linter/src/config/settings/next.rs | 1 + .../oxc_linter/src/config/settings/react.rs | 2 + crates/oxc_linter/src/lib.rs | 4 +- crates/oxc_linter/src/options/plugins.rs | 39 +++- tasks/benchmark/benches/linter.rs | 19 +- 16 files changed, 317 insertions(+), 81 deletions(-) create mode 100644 apps/oxlint/fixtures/import/.oxlintrc.json create mode 100644 apps/oxlint/fixtures/import/test.js diff --git a/apps/oxlint/fixtures/import/.oxlintrc.json b/apps/oxlint/fixtures/import/.oxlintrc.json new file mode 100644 index 0000000000000..fc645f90991be --- /dev/null +++ b/apps/oxlint/fixtures/import/.oxlintrc.json @@ -0,0 +1,7 @@ +{ + "plugins": ["import"], + "rules": { + "import/no-default-export": "error", + "import/namespace": "allow" + } +} diff --git a/apps/oxlint/fixtures/import/test.js b/apps/oxlint/fixtures/import/test.js new file mode 100644 index 0000000000000..68f0e20a25e7e --- /dev/null +++ b/apps/oxlint/fixtures/import/test.js @@ -0,0 +1,9 @@ +import * as foo from './foo'; +// ^ import/namespace + +console.log(foo); + +// import/no-default-export +export default function foo() {} + + diff --git a/apps/oxlint/fixtures/typescript_eslint/eslintrc.json b/apps/oxlint/fixtures/typescript_eslint/eslintrc.json index c7773313f7cd0..2920935a9110c 100644 --- a/apps/oxlint/fixtures/typescript_eslint/eslintrc.json +++ b/apps/oxlint/fixtures/typescript_eslint/eslintrc.json @@ -1,4 +1,6 @@ { + // NOTE: enabled by default + "plugins": ["@typescript-eslint"], "rules": { "no-loss-of-precision": "off", "@typescript-eslint/no-loss-of-precision": "error", diff --git a/apps/oxlint/src/command/lint.rs b/apps/oxlint/src/command/lint.rs index b4e7bfcf4981b..f404714428505 100644 --- a/apps/oxlint/src/command/lint.rs +++ b/apps/oxlint/src/command/lint.rs @@ -1,7 +1,7 @@ use std::{path::PathBuf, str::FromStr}; use bpaf::Bpaf; -use oxc_linter::{AllowWarnDeny, FixKind}; +use oxc_linter::{AllowWarnDeny, FixKind, LintPlugins}; use super::{ expand_glob, @@ -211,64 +211,203 @@ impl FromStr for OutputFormat { /// Enable Plugins #[allow(clippy::struct_field_names)] -#[derive(Debug, Clone, Bpaf)] +#[derive(Debug, Default, Clone, Bpaf)] pub struct EnablePlugins { /// Disable react plugin, which is turned on by default - #[bpaf(long("disable-react-plugin"), flag(false, true), hide_usage)] - pub react_plugin: bool, + #[bpaf( + long("disable-react-plugin"), + flag(OverrideToggle::Disable, OverrideToggle::NotSet), + hide_usage + )] + pub react_plugin: OverrideToggle, /// Disable unicorn plugin, which is turned on by default - #[bpaf(long("disable-unicorn-plugin"), flag(false, true), hide_usage)] - pub unicorn_plugin: bool, + #[bpaf( + long("disable-unicorn-plugin"), + flag(OverrideToggle::Disable, OverrideToggle::NotSet), + hide_usage + )] + pub unicorn_plugin: OverrideToggle, /// Disable oxc unique rules, which is turned on by default - #[bpaf(long("disable-oxc-plugin"), flag(false, true), hide_usage)] - pub oxc_plugin: bool, + #[bpaf( + long("disable-oxc-plugin"), + flag(OverrideToggle::Disable, OverrideToggle::NotSet), + hide_usage + )] + pub oxc_plugin: OverrideToggle, /// Disable TypeScript plugin, which is turned on by default - #[bpaf(long("disable-typescript-plugin"), flag(false, true), hide_usage)] - pub typescript_plugin: bool, + #[bpaf( + long("disable-typescript-plugin"), + flag(OverrideToggle::Disable, OverrideToggle::NotSet), + hide_usage + )] + pub typescript_plugin: OverrideToggle, /// Enable the experimental import plugin and detect ESM problems. /// It is recommended to use along side with the `--tsconfig` option. - #[bpaf(switch, hide_usage)] - pub import_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub import_plugin: OverrideToggle, /// Enable the experimental jsdoc plugin and detect JSDoc problems - #[bpaf(switch, hide_usage)] - pub jsdoc_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub jsdoc_plugin: OverrideToggle, /// Enable the Jest plugin and detect test problems - #[bpaf(switch, hide_usage)] - pub jest_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub jest_plugin: OverrideToggle, /// Enable the Vitest plugin and detect test problems - #[bpaf(switch, hide_usage)] - pub vitest_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub vitest_plugin: OverrideToggle, /// Enable the JSX-a11y plugin and detect accessibility problems - #[bpaf(switch, hide_usage)] - pub jsx_a11y_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub jsx_a11y_plugin: OverrideToggle, /// Enable the Next.js plugin and detect Next.js problems - #[bpaf(switch, hide_usage)] - pub nextjs_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub nextjs_plugin: OverrideToggle, /// Enable the React performance plugin and detect rendering performance problems - #[bpaf(switch, hide_usage)] - pub react_perf_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub react_perf_plugin: OverrideToggle, /// Enable the promise plugin and detect promise usage problems - #[bpaf(switch, hide_usage)] - pub promise_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub promise_plugin: OverrideToggle, /// Enable the node plugin and detect node usage problems - #[bpaf(switch, hide_usage)] - pub node_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub node_plugin: OverrideToggle, /// Enable the security plugin and detect security problems - #[bpaf(switch, hide_usage)] - pub security_plugin: bool, + #[bpaf(flag(OverrideToggle::Enable, OverrideToggle::NotSet), hide_usage)] + pub security_plugin: OverrideToggle, +} + +/// Enables or disables a boolean option, or leaves it unset. +/// +/// We want CLI flags to modify whatever's set in the user's config file, but we don't want them +/// changing default behavior if they're not explicitly passed by the user. This scheme is a bit +/// convoluted, but needed due to architectural constraints imposed by `bpaf`. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +#[allow(clippy::enum_variant_names)] +pub enum OverrideToggle { + /// Override the option to enabled + Enable, + /// Override the option to disabled + Disable, + /// Do not override. + #[default] + NotSet, +} + +impl From> for OverrideToggle { + fn from(value: Option) -> Self { + match value { + Some(true) => Self::Enable, + Some(false) => Self::Disable, + None => Self::NotSet, + } + } +} + +impl From for Option { + fn from(value: OverrideToggle) -> Self { + match value { + OverrideToggle::Enable => Some(true), + OverrideToggle::Disable => Some(false), + OverrideToggle::NotSet => None, + } + } +} + +impl OverrideToggle { + #[inline] + pub fn is_enabled(self) -> bool { + matches!(self, Self::Enable) + } + + #[inline] + pub fn is_not_set(self) -> bool { + matches!(self, Self::NotSet) + } + + pub fn inspect(self, f: F) + where + F: FnOnce(bool), + { + if let Some(v) = self.into() { + f(v); + } + } +} + +impl EnablePlugins { + pub fn apply_overrides(&self, plugins: &mut LintPlugins) { + self.react_plugin.inspect(|yes| plugins.set(LintPlugins::REACT, yes)); + self.unicorn_plugin.inspect(|yes| plugins.set(LintPlugins::UNICORN, yes)); + self.oxc_plugin.inspect(|yes| plugins.set(LintPlugins::OXC, yes)); + self.typescript_plugin.inspect(|yes| plugins.set(LintPlugins::TYPESCRIPT, yes)); + self.import_plugin.inspect(|yes| plugins.set(LintPlugins::IMPORT, yes)); + self.jsdoc_plugin.inspect(|yes| plugins.set(LintPlugins::JSDOC, yes)); + self.jest_plugin.inspect(|yes| plugins.set(LintPlugins::JEST, yes)); + self.vitest_plugin.inspect(|yes| plugins.set(LintPlugins::VITEST, yes)); + self.jsx_a11y_plugin.inspect(|yes| plugins.set(LintPlugins::JSX_A11Y, yes)); + self.nextjs_plugin.inspect(|yes| plugins.set(LintPlugins::NEXTJS, yes)); + self.react_perf_plugin.inspect(|yes| plugins.set(LintPlugins::REACT_PERF, yes)); + self.promise_plugin.inspect(|yes| plugins.set(LintPlugins::PROMISE, yes)); + self.node_plugin.inspect(|yes| plugins.set(LintPlugins::NODE, yes)); + self.security_plugin.inspect(|yes| plugins.set(LintPlugins::SECURITY, yes)); + + // Without this, jest plugins adapted to vitest will not be enabled. + if self.vitest_plugin.is_enabled() && self.jest_plugin.is_not_set() { + plugins.set(LintPlugins::JEST, true); + } + } +} + +#[cfg(test)] +mod plugins { + use super::{EnablePlugins, OverrideToggle}; + use oxc_linter::LintPlugins; + + #[test] + fn test_override_default() { + let mut plugins = LintPlugins::default(); + let enable = EnablePlugins::default(); + + enable.apply_overrides(&mut plugins); + assert_eq!(plugins, LintPlugins::default()); + } + + #[test] + fn test_overrides() { + let mut plugins = LintPlugins::default(); + let enable = EnablePlugins { + react_plugin: OverrideToggle::Enable, + unicorn_plugin: OverrideToggle::Disable, + ..EnablePlugins::default() + }; + let expected = + LintPlugins::default().union(LintPlugins::REACT).difference(LintPlugins::UNICORN); + + enable.apply_overrides(&mut plugins); + assert_eq!(plugins, expected); + } + + #[test] + fn test_override_vitest() { + let mut plugins = LintPlugins::default(); + let enable = + EnablePlugins { vitest_plugin: OverrideToggle::Enable, ..EnablePlugins::default() }; + let expected = LintPlugins::default() | LintPlugins::VITEST | LintPlugins::JEST; + + enable.apply_overrides(&mut plugins); + assert_eq!(plugins, expected); + } } #[cfg(test)] diff --git a/apps/oxlint/src/lint/mod.rs b/apps/oxlint/src/lint/mod.rs index abbd0ed9a73ad..918b6c806f22a 100644 --- a/apps/oxlint/src/lint/mod.rs +++ b/apps/oxlint/src/lint/mod.rs @@ -4,7 +4,7 @@ use ignore::gitignore::Gitignore; use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler}; use oxc_linter::{ loader::LINT_PARTIAL_LOADER_EXT, AllowWarnDeny, InvalidFilterKind, LintFilter, LintService, - LintServiceOptions, Linter, OxlintOptions, + LintServiceOptions, Linter, LinterBuilder, Oxlintrc, }; use oxc_span::VALID_EXTENSIONS; @@ -98,39 +98,32 @@ impl Runner for LintRunner { let number_of_files = paths.len(); let cwd = std::env::current_dir().unwrap(); - let mut options = - LintServiceOptions::new(cwd, paths).with_cross_module(enable_plugins.import_plugin); - let lint_options = OxlintOptions::default() - .with_filter(filter) - .with_config_path(basic_options.config) - .with_fix(fix_options.fix_kind()) - .with_react_plugin(enable_plugins.react_plugin) - .with_unicorn_plugin(enable_plugins.unicorn_plugin) - .with_typescript_plugin(enable_plugins.typescript_plugin) - .with_oxc_plugin(enable_plugins.oxc_plugin) - .with_import_plugin(enable_plugins.import_plugin) - .with_jsdoc_plugin(enable_plugins.jsdoc_plugin) - .with_jest_plugin(enable_plugins.jest_plugin) - .with_vitest_plugin(enable_plugins.vitest_plugin) - .with_jsx_a11y_plugin(enable_plugins.jsx_a11y_plugin) - .with_nextjs_plugin(enable_plugins.nextjs_plugin) - .with_react_perf_plugin(enable_plugins.react_perf_plugin) - .with_promise_plugin(enable_plugins.promise_plugin) - .with_node_plugin(enable_plugins.node_plugin) - .with_security_plugin(enable_plugins.security_plugin); - - let linter = match Linter::from_options(lint_options) { - Ok(lint_service) => lint_service, - Err(diagnostic) => { - let handler = GraphicalReportHandler::new(); - let mut err = String::new(); - handler.render_report(&mut err, diagnostic.as_ref()).unwrap(); - return CliRunResult::InvalidOptions { - message: format!("Failed to parse configuration file.\n{err}"), - }; + + let mut oxlintrc = if let Some(config_path) = basic_options.config.as_ref() { + match Oxlintrc::from_file(config_path) { + Ok(config) => config, + Err(diagnostic) => { + let handler = GraphicalReportHandler::new(); + let mut err = String::new(); + handler.render_report(&mut err, &diagnostic).unwrap(); + return CliRunResult::InvalidOptions { + message: format!("Failed to parse configuration file.\n{err}"), + }; + } } + } else { + Oxlintrc::default() }; + enable_plugins.apply_overrides(&mut oxlintrc.plugins); + let builder = LinterBuilder::from_oxlintrc(false, oxlintrc) + .with_filters(filter) + .with_fix(fix_options.fix_kind()); + + let mut options = + LintServiceOptions::new(cwd, paths).with_cross_module(builder.plugins().has_import()); + let linter = builder.build(); + let tsconfig = basic_options.tsconfig; if let Some(path) = tsconfig.as_ref() { if path.is_file() { @@ -562,4 +555,13 @@ mod test { assert_eq!(result.number_of_files, 1); assert_eq!(result.number_of_errors, 1); } + + #[test] + fn test_import_plugin_enabled_in_config() { + let args = &["-c", "fixtures/import/.oxlintrc.json", "fixtures/import/test.js"]; + let result = test(args); + assert_eq!(result.number_of_files, 1); + assert_eq!(result.number_of_warnings, 0); + assert_eq!(result.number_of_errors, 1); + } } diff --git a/crates/oxc_linter/src/config/env.rs b/crates/oxc_linter/src/config/env.rs index 8649a01590fa8..9f11447315421 100644 --- a/crates/oxc_linter/src/config/env.rs +++ b/crates/oxc_linter/src/config/env.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; /// environments](https://eslint.org/docs/v8.x/use/configure/language-options#specifying-environments) /// for what environments are available and what each one provides. #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct OxlintEnv(FxHashMap); impl OxlintEnv { diff --git a/crates/oxc_linter/src/config/oxlintrc.rs b/crates/oxc_linter/src/config/oxlintrc.rs index 437ca80fb3497..e9754cebcd7d3 100644 --- a/crates/oxc_linter/src/config/oxlintrc.rs +++ b/crates/oxc_linter/src/config/oxlintrc.rs @@ -94,3 +94,48 @@ impl Oxlintrc { Ok(config) } } + +#[cfg(test)] +mod test { + use super::*; + use serde_json::json; + + #[test] + fn test_oxlintrc_de_empty() { + let config: Oxlintrc = serde_json::from_value(json!({})).unwrap(); + assert_eq!(config.plugins, LintPlugins::default()); + assert_eq!(config.rules, OxlintRules::default()); + assert!(config.rules.is_empty()); + assert_eq!(config.settings, OxlintSettings::default()); + assert_eq!(config.env, OxlintEnv::default()); + } + + #[test] + fn test_oxlintrc_de_plugins_empty_array() { + let config: Oxlintrc = serde_json::from_value(json!({ "plugins": [] })).unwrap(); + assert_eq!(config.plugins, LintPlugins::default()); + } + + #[test] + fn test_oxlintrc_de_plugins_enabled_by_default() { + // NOTE(@DonIsaac): creating a Value with `json!` then deserializing it with serde_json::from_value + // Errs with "invalid type: string \"eslint\", expected a borrowed string" and I can't + // figure out why. This seems to work. Why??? + let configs = [ + r#"{ "plugins": ["eslint"] }"#, + r#"{ "plugins": ["oxc"] }"#, + r#"{ "plugins": ["deepscan"] }"#, // alias for oxc + ]; + // ^ these plugins are enabled by default already + for oxlintrc in configs { + let config: Oxlintrc = serde_json::from_str(oxlintrc).unwrap(); + assert_eq!(config.plugins, LintPlugins::default()); + } + } + + #[test] + fn test_oxlintrc_de_plugins_new() { + let config: Oxlintrc = serde_json::from_str(r#"{ "plugins": ["import"] }"#).unwrap(); + assert_eq!(config.plugins, LintPlugins::default().union(LintPlugins::IMPORT)); + } +} diff --git a/crates/oxc_linter/src/config/rules.rs b/crates/oxc_linter/src/config/rules.rs index a53792f3fd435..3c17880247879 100644 --- a/crates/oxc_linter/src/config/rules.rs +++ b/crates/oxc_linter/src/config/rules.rs @@ -23,9 +23,11 @@ type RuleSet = FxHashSet; // // Note: when update document comment, also update `DummyRuleMap`'s description in this file. #[derive(Debug, Clone, Default)] +#[cfg_attr(test, derive(PartialEq))] pub struct OxlintRules(Vec); #[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] pub struct ESLintRule { pub plugin_name: String, pub rule_name: String, diff --git a/crates/oxc_linter/src/config/settings/jsdoc.rs b/crates/oxc_linter/src/config/settings/jsdoc.rs index 2f581897cb409..e7bf9d284f742 100644 --- a/crates/oxc_linter/src/config/settings/jsdoc.rs +++ b/crates/oxc_linter/src/config/settings/jsdoc.rs @@ -8,6 +8,7 @@ use crate::utils::default_true; // #[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct JSDocPluginSettings { /// For all rules but NOT apply to `check-access` and `empty-tags` rule #[serde(default, rename = "ignorePrivate")] @@ -181,6 +182,7 @@ impl JSDocPluginSettings { } #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] #[serde(untagged)] enum TagNamePreference { TagNameOnly(String), diff --git a/crates/oxc_linter/src/config/settings/jsx_a11y.rs b/crates/oxc_linter/src/config/settings/jsx_a11y.rs index e6c73f7e3f686..11b823ee38957 100644 --- a/crates/oxc_linter/src/config/settings/jsx_a11y.rs +++ b/crates/oxc_linter/src/config/settings/jsx_a11y.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; // #[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct JSXA11yPluginSettings { #[serde(rename = "polymorphicPropName")] pub polymorphic_prop_name: Option, diff --git a/crates/oxc_linter/src/config/settings/mod.rs b/crates/oxc_linter/src/config/settings/mod.rs index 10bfc1d8fd8eb..4a496bc9b554c 100644 --- a/crates/oxc_linter/src/config/settings/mod.rs +++ b/crates/oxc_linter/src/config/settings/mod.rs @@ -13,6 +13,7 @@ use self::{ /// Shared settings for plugins #[derive(Debug, Deserialize, Serialize, Default, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct OxlintSettings { #[serde(default)] #[serde(rename = "jsx-a11y")] diff --git a/crates/oxc_linter/src/config/settings/next.rs b/crates/oxc_linter/src/config/settings/next.rs index 8246e3fc14503..4f289641531af 100644 --- a/crates/oxc_linter/src/config/settings/next.rs +++ b/crates/oxc_linter/src/config/settings/next.rs @@ -4,6 +4,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize, Serializer}; #[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct NextPluginSettings { #[serde(default)] #[serde(rename = "rootDir")] diff --git a/crates/oxc_linter/src/config/settings/react.rs b/crates/oxc_linter/src/config/settings/react.rs index 40b0219bd43e3..9cd913dea0fcf 100644 --- a/crates/oxc_linter/src/config/settings/react.rs +++ b/crates/oxc_linter/src/config/settings/react.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; // #[derive(Debug, Deserialize, Default, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] pub struct ReactPluginSettings { #[serde(default)] #[serde(rename = "formComponents")] @@ -31,6 +32,7 @@ impl ReactPluginSettings { // Deserialize helper types #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[cfg_attr(test, derive(PartialEq))] #[serde(untagged)] enum CustomComponent { NameOnly(CompactStr), diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index b75e629d59ba2..fec6cb9de306a 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -35,7 +35,9 @@ pub use crate::{ context::LintContext, fixer::FixKind, frameworks::FrameworkFlags, - options::{AllowWarnDeny, InvalidFilterKind, LintFilter, LintFilterKind, OxlintOptions}, + options::{ + AllowWarnDeny, InvalidFilterKind, LintFilter, LintFilterKind, LintPlugins, OxlintOptions, + }, rule::{RuleCategory, RuleFixMeta, RuleMeta, RuleWithSeverity}, service::{LintService, LintServiceOptions}, }; diff --git a/crates/oxc_linter/src/options/plugins.rs b/crates/oxc_linter/src/options/plugins.rs index d991a9f50e8f0..f06d9e4e86a3f 100644 --- a/crates/oxc_linter/src/options/plugins.rs +++ b/crates/oxc_linter/src/options/plugins.rs @@ -1,6 +1,10 @@ use bitflags::bitflags; use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema}; -use serde::{de::Deserializer, ser::Serializer, Deserialize, Serialize}; +use serde::{ + de::{self, Deserializer}, + ser::Serializer, + Deserialize, Serialize, +}; bitflags! { // NOTE: may be increased to a u32 if needed @@ -151,7 +155,38 @@ impl> FromIterator for LintPlugins { impl<'de> Deserialize<'de> for LintPlugins { fn deserialize>(deserializer: D) -> Result { - Vec::<&str>::deserialize(deserializer).map(|vec| vec.into_iter().collect()) + struct LintPluginsVisitor; + impl<'de> de::Visitor<'de> for LintPluginsVisitor { + type Value = LintPlugins; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a list of plugin names") + } + + fn visit_str(self, value: &str) -> Result { + Ok(LintPlugins::from(value)) + } + + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + Ok(LintPlugins::from(v.as_str())) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: de::SeqAccess<'de>, + { + let mut plugins = LintPlugins::default(); + while let Some(plugin) = seq.next_element::<&str>()? { + plugins |= plugin.into(); + } + Ok(plugins) + } + } + + deserializer.deserialize_any(LintPluginsVisitor) } } diff --git a/tasks/benchmark/benches/linter.rs b/tasks/benchmark/benches/linter.rs index 8cb9709474e74..19089074e7aa1 100644 --- a/tasks/benchmark/benches/linter.rs +++ b/tasks/benchmark/benches/linter.rs @@ -2,7 +2,7 @@ use std::{env, path::Path, rc::Rc}; use oxc_allocator::Allocator; use oxc_benchmark::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use oxc_linter::{AllowWarnDeny, FixKind, LintFilter, Linter, OxlintOptions}; +use oxc_linter::{FixKind, LinterBuilder}; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; @@ -34,22 +34,7 @@ fn bench_linter(criterion: &mut Criterion) { .with_cfg(true) .build_module_record(Path::new(""), program) .build(program); - let filter = vec![ - LintFilter::new(AllowWarnDeny::Deny, "all").unwrap(), - LintFilter::new(AllowWarnDeny::Deny, "nursery").unwrap(), - ]; - let lint_options = OxlintOptions::default() - .with_filter(filter) - .with_fix(FixKind::All) - .with_import_plugin(true) - .with_jsdoc_plugin(true) - .with_jest_plugin(true) - .with_jsx_a11y_plugin(true) - .with_nextjs_plugin(true) - .with_react_perf_plugin(true) - .with_vitest_plugin(true) - .with_node_plugin(true); - let linter = Linter::from_options(lint_options).unwrap(); + let linter = LinterBuilder::all().with_fix(FixKind::All).build(); let semantic = Rc::new(semantic_ret.semantic); b.iter(|| linter.run(Path::new(std::ffi::OsStr::new("")), Rc::clone(&semantic))); },