feat(linter): configure by category in config files (#6120)

> closes #5454

Adds a `categories` property to config files, where each key is a `RuleCategory` and each value is `"allow"/"off"`, `"warn"`, or `"deny"/"error"`.

Note that this change won't come into effect until after #6088 is merged.
This commit is contained in:
DonIsaac 2024-10-08 22:19:06 +00:00
parent d5cf1f823b
commit 6e3224d5fa
8 changed files with 313 additions and 2 deletions

View file

@ -70,7 +70,8 @@ impl LinterBuilder {
/// ```
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 Oxlintrc { plugins, settings, env, globals, categories, rules: oxlintrc_rules } =
oxlintrc;
let config = LintConfig { settings, env, globals };
let options = LintOptions { plugins, ..Default::default() };
@ -79,6 +80,10 @@ impl LinterBuilder {
let cache = RulesCache::new(options.plugins);
let mut builder = Self { rules, options, config, cache };
if !categories.is_empty() {
builder = builder.with_filters(categories.filters());
}
{
let all_rules = builder.cache.borrow();
oxlintrc_rules.override_rules(&mut builder.rules, all_rules.as_slice());
@ -536,4 +541,55 @@ mod test {
let builder = builder.with_plugins(expected_plugins);
assert_eq!(expected_plugins, builder.plugins());
}
#[test]
fn test_categories() {
let oxlintrc: Oxlintrc = serde_json::from_str(
r#"
{
"categories": {
"correctness": "warn",
"suspicious": "deny"
},
"rules": {
"no-const-assign": "error"
}
}
"#,
)
.unwrap();
let builder = LinterBuilder::from_oxlintrc(false, oxlintrc);
for rule in &builder.rules {
let name = rule.name();
let plugin = rule.plugin_name();
let category = rule.category();
match category {
RuleCategory::Correctness => {
if name == "no-const-assign" {
assert_eq!(
rule.severity,
AllowWarnDeny::Deny,
"no-const-assign should be denied",
);
} else {
assert_eq!(
rule.severity,
AllowWarnDeny::Warn,
"{plugin}/{name} should be a warning"
);
}
}
RuleCategory::Suspicious => {
assert_eq!(
rule.severity,
AllowWarnDeny::Deny,
"{plugin}/{name} should be denied"
);
}
invalid => {
panic!("Found rule {plugin}/{name} with an unexpected category {invalid:?}");
}
}
}
}
}

View file

@ -0,0 +1,85 @@
use std::{borrow::Cow, ops::Deref};
use rustc_hash::FxHashMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{AllowWarnDeny, LintFilter, RuleCategory};
/// Configure an entire category of rules all at once.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct OxlintCategories(FxHashMap<RuleCategory, AllowWarnDeny>);
impl Deref for OxlintCategories {
type Target = FxHashMap<RuleCategory, AllowWarnDeny>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl OxlintCategories {
pub fn filters(&self) -> impl Iterator<Item = LintFilter> + '_ {
self.iter().map(|(category, severity)| LintFilter::new(*severity, *category).unwrap())
}
}
impl JsonSchema for OxlintCategories {
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed("OxlintCategories")
}
fn schema_name() -> String {
"OxlintCategories".to_string()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
let severity = gen.subschema_for::<AllowWarnDeny>();
let mut schema =
gen.subschema_for::<FxHashMap<RuleCategory, AllowWarnDeny>>().into_object();
{
schema.object().additional_properties = None;
let properties = &mut schema.object().properties;
properties.insert(RuleCategory::Correctness.as_str().to_string(), severity.clone());
properties.insert(RuleCategory::Suspicious.as_str().to_string(), severity.clone());
properties.insert(RuleCategory::Pedantic.as_str().to_string(), severity.clone());
properties.insert(RuleCategory::Perf.as_str().to_string(), severity.clone());
properties.insert(RuleCategory::Style.as_str().to_string(), severity.clone());
properties.insert(RuleCategory::Restriction.as_str().to_string(), severity.clone());
properties.insert(RuleCategory::Nursery.as_str().to_string(), severity.clone());
}
{
let metadata = schema.metadata();
metadata.title = Some("Rule Categories".to_string());
metadata.description = Some(
r#"
Configure an entire category of rules all at once.
Rules enabled or disabled this way will be overwritten by individual rules in the `rules` field.
# Example
```json
{
"categories": {
"correctness": "warn"
},
"rules": {
"eslint/no-unused-vars": "error"
}
}
```
"#
.trim()
.to_string(),
);
metadata.examples = vec![serde_json::json!({ "correctness": "warn" })];
}
schema.into()
}
}

View file

@ -1,3 +1,4 @@
mod categories;
mod env;
mod globals;
mod oxlintrc;

View file

@ -4,7 +4,10 @@ use oxc_diagnostics::OxcDiagnostic;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::{env::OxlintEnv, globals::OxlintGlobals, rules::OxlintRules, settings::OxlintSettings};
use super::{
categories::OxlintCategories, env::OxlintEnv, globals::OxlintGlobals, rules::OxlintRules,
settings::OxlintSettings,
};
use crate::{options::LintPlugins, utils::read_to_string};
@ -45,6 +48,7 @@ use crate::{options::LintPlugins, utils::read_to_string};
#[non_exhaustive]
pub struct Oxlintrc {
pub plugins: LintPlugins,
pub categories: OxlintCategories,
/// See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html).
pub rules: OxlintRules,
pub settings: OxlintSettings,

View file

@ -100,6 +100,18 @@ impl RuleCategory {
Self::Nursery => "New lints that are still under development.",
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Correctness => "correctness",
Self::Suspicious => "suspicious",
Self::Pedantic => "pedantic",
Self::Perf => "perf",
Self::Style => "style",
Self::Restriction => "restriction",
Self::Nursery => "nursery",
}
}
}
impl TryFrom<&str> for RuleCategory {

View file

@ -8,6 +8,14 @@ expression: json
"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 { \"env\": { \"browser\": true }, \"globals\": { \"foo\": \"readonly\" }, \"settings\": { }, \"rules\": { \"eqeqeq\": \"warn\", \"import/no-cycle\": \"error\" } } ```",
"type": "object",
"properties": {
"categories": {
"default": {},
"allOf": [
{
"$ref": "#/definitions/OxlintCategories"
}
]
},
"env": {
"description": "Environments enable and disable collections of global variables.",
"default": {
@ -267,6 +275,39 @@ expression: json
}
]
},
"OxlintCategories": {
"title": "Rule Categories",
"description": "Configure an entire category of rules all at once.\n\nRules enabled or disabled this way will be overwritten by individual rules in the `rules` field.\n\n# Example\n```json\n{\n \"categories\": {\n \"correctness\": \"warn\"\n },\n \"rules\": {\n \"eslint/no-unused-vars\": \"error\"\n }\n}\n```",
"examples": [
{
"correctness": "warn"
}
],
"type": "object",
"properties": {
"correctness": {
"$ref": "#/definitions/AllowWarnDeny"
},
"nursery": {
"$ref": "#/definitions/AllowWarnDeny"
},
"pedantic": {
"$ref": "#/definitions/AllowWarnDeny"
},
"perf": {
"$ref": "#/definitions/AllowWarnDeny"
},
"restriction": {
"$ref": "#/definitions/AllowWarnDeny"
},
"style": {
"$ref": "#/definitions/AllowWarnDeny"
},
"suspicious": {
"$ref": "#/definitions/AllowWarnDeny"
}
}
},
"OxlintEnv": {
"description": "Predefine global variables.\n\nEnvironments specify what global variables are predefined. See [ESLint's list of environments](https://eslint.org/docs/v8.x/use/configure/language-options#specifying-environments) for what environments are available and what each one provides.",
"type": "object",

View file

@ -4,6 +4,14 @@
"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 { \"env\": { \"browser\": true }, \"globals\": { \"foo\": \"readonly\" }, \"settings\": { }, \"rules\": { \"eqeqeq\": \"warn\", \"import/no-cycle\": \"error\" } } ```",
"type": "object",
"properties": {
"categories": {
"default": {},
"allOf": [
{
"$ref": "#/definitions/OxlintCategories"
}
]
},
"env": {
"description": "Environments enable and disable collections of global variables.",
"default": {
@ -263,6 +271,39 @@
}
]
},
"OxlintCategories": {
"title": "Rule Categories",
"description": "Configure an entire category of rules all at once.\n\nRules enabled or disabled this way will be overwritten by individual rules in the `rules` field.\n\n# Example\n```json\n{\n \"categories\": {\n \"correctness\": \"warn\"\n },\n \"rules\": {\n \"eslint/no-unused-vars\": \"error\"\n }\n}\n```",
"examples": [
{
"correctness": "warn"
}
],
"type": "object",
"properties": {
"correctness": {
"$ref": "#/definitions/AllowWarnDeny"
},
"nursery": {
"$ref": "#/definitions/AllowWarnDeny"
},
"pedantic": {
"$ref": "#/definitions/AllowWarnDeny"
},
"perf": {
"$ref": "#/definitions/AllowWarnDeny"
},
"restriction": {
"$ref": "#/definitions/AllowWarnDeny"
},
"style": {
"$ref": "#/definitions/AllowWarnDeny"
},
"suspicious": {
"$ref": "#/definitions/AllowWarnDeny"
}
}
},
"OxlintEnv": {
"description": "Predefine global variables.\n\nEnvironments specify what global variables are predefined. See [ESLint's list of environments](https://eslint.org/docs/v8.x/use/configure/language-options#specifying-environments) for what environments are available and what each one provides.",
"type": "object",

View file

@ -35,6 +35,77 @@ Example
```
## categories
type: `object`
Configure an entire category of rules all at once.
Rules enabled or disabled this way will be overwritten by individual rules in the `rules` field.
# Example
```json
{
"categories": {
"correctness": "warn"
},
"rules": {
"eslint/no-unused-vars": "error"
}
}
```
### categories.correctness
### categories.nursery
### categories.pedantic
### categories.perf
### categories.restriction
### categories.style
### categories.suspicious
## env
type: `object`