Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
484c41e
docs(linter): Update DefaultRuleConfig deserialization for more compl…
connorshea Dec 6, 2025
f79c1aa
refactor(linter): Refactor the DefaultRuleConfig implementation to si…
connorshea Dec 6, 2025
2054095
Add more tests for the DefaultRuleConfig behavior.
connorshea Dec 6, 2025
8c59173
Add more tests, simply code a bit hopefully.
connorshea Dec 6, 2025
986d18c
Updates for tests.
connorshea Dec 6, 2025
766f7c7
I'm sure this can be done better, but I've done my best trying to opt…
connorshea Dec 6, 2025
0e5ecc9
refactor(linter): Rewrite SortKeys config options to be a lot simpler…
connorshea Dec 6, 2025
6b7ccaa
fmt
connorshea Dec 6, 2025
d0b25c0
docs(linter): Provide correct config docs for eslint/no-inner-declara…
connorshea Dec 6, 2025
d32281e
Clean the code up a bit.
connorshea Dec 6, 2025
bbfd138
Resolve lint warning and fix feedback.
connorshea Dec 6, 2025
4eeccd3
Resolve feedback.
connorshea Dec 6, 2025
d71a82b
Resolve lint warnings.
connorshea Dec 6, 2025
96cf9ae
doctests lol
connorshea Dec 6, 2025
5c771ef
docs(linter): Update eslint/func-style rule to fix the config option …
connorshea Dec 6, 2025
414a7f5
docs(linter): Add auto-generated config option docs for `eslint/arrow…
connorshea Dec 6, 2025
536781c
Revert no_inner_declarations.rs back to its state on main.
connorshea Dec 6, 2025
962bfe2
Appease the linter.
connorshea Dec 6, 2025
fdc4795
Remove all but one usage of clone in DefaultRuleConfig deserialization.
connorshea Dec 7, 2025
332f8ea
refactor(linter): Remove support for double-object tuples.
connorshea Dec 7, 2025
948e173
Add another test case.
connorshea Dec 7, 2025
3fea757
Fix tuple test to use an actual tuple struct.
connorshea Dec 7, 2025
8375a6d
Remove test for handling of extra elements, no need to support that.
connorshea Dec 7, 2025
43c982a
refactor: Avoid converting into an array and then back for no reason.
connorshea Dec 7, 2025
200d057
Flatten the code a bit.
connorshea Dec 7, 2025
19b5d40
Add a few comments.
connorshea Dec 7, 2025
5d40c28
Create an assert function to simplify testing the DefaultRuleConfig.
connorshea Dec 7, 2025
a6a0eb1
Update one more test to use the custom assert fn
connorshea Dec 7, 2025
5f2ee80
Resolve lint violations.
connorshea Dec 7, 2025
232b8ba
docs(linter): Updated `eslint/yoda` rule with auto-gen config options…
connorshea Dec 10, 2025
48cda35
fix(linter): Fix default behavior of the `eslint/eqeqeq` rule, and al…
connorshea Dec 10, 2025
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
233 changes: 223 additions & 10 deletions crates/oxc_linter/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ pub trait Rule: Sized + Default + fmt::Debug {
/// }
/// }
/// ```
///
/// For rules that take a tuple configuration object, e.g. `["foobar", { param: true, other_param: false }]`, you can also use this with a tuple struct:
/// ```ignore
/// pub struct MyRuleWithTupleConfig(FirstParamType, SecondParamType);
///
/// impl Rule for MyRuleWithTupleConfig {
/// fn from_configuration(value: serde_json::Value) -> Self {
/// serde_json::from_value::<DefaultRuleConfig<MyRuleWithTupleConfig>>(value)
/// .unwrap_or_default()
/// .into_inner()
/// }
/// }
/// ```
#[derive(Debug, Clone)]
pub struct DefaultRuleConfig<T>(T);

Expand Down Expand Up @@ -117,16 +130,48 @@ where

let value = serde_json::Value::deserialize(deserializer)?;

if let serde_json::Value::Array(arr) = value {
let config = arr
.into_iter()
.next()
.and_then(|v| serde_json::from_value(v).ok())
.unwrap_or_else(T::default);
Ok(DefaultRuleConfig(config))
} else {
Err(D::Error::custom("Expected array for rule configuration"))
// Expect an array for ESLint-style rule configs.
//
// The shape should generally be like one of the following:
// `[{ "bar": "baz" }]`, this is the most common
// `["foo"]`, some rules use a single enum/string value
// `["foo", { "bar": "baz" }]`, some rules use a tuple of values
let serde_json::Value::Array(arr) = value else {
return Err(D::Error::custom("Expected array for rule configuration"));
};

// Empty array => use defaults.
if arr.is_empty() {
return Ok(DefaultRuleConfig(T::default()));
}

// Single-element array.
// - `["foo"]`
// - `[{ "foo": "bar" }]`
if arr.len() == 1 {
let elem = arr.into_iter().next().unwrap();

// If it's an object, parse it directly (most common case).
if elem.is_object() {
let t = serde_json::from_value::<T>(elem).unwrap_or_else(|_| T::default());
return Ok(DefaultRuleConfig(t));
}

// For non-objects, try parsing the element directly (primitives, enums).
// If that fails, try as a single-element array (for partial tuples with defaults).
let t = serde_json::from_value::<T>(elem.clone())
.or_else(|_| serde_json::from_value::<T>(serde_json::Value::Array(vec![elem])))
.unwrap_or_else(|_| T::default());
return Ok(DefaultRuleConfig(t));
}

// Multi-element arrays
// - `[42, { "foo": "abc" }]`
// - `["optionA", { "foo": "bar" }]`
let t = serde_json::from_value::<T>(serde_json::Value::Array(arr))
.unwrap_or_else(|_| T::default());
Comment on lines +171 to +172
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could alternatively do this, but I'm not really sure that's any better/different:

let t = T::deserialize(serde_json::Value::Array(arr))
            .unwrap_or_else(|_| T::default());


Ok(DefaultRuleConfig(t))
}
}

Expand Down Expand Up @@ -391,7 +436,9 @@ impl From<RuleFixMeta> for FixKind {

#[cfg(test)]
mod test {
use crate::{RuleMeta, RuleRunner};
use rustc_hash::FxHashMap;

use crate::{RuleMeta, RuleRunner, rule::DefaultRuleConfig};

use super::RuleCategory;

Expand Down Expand Up @@ -490,6 +537,172 @@ mod test {
);
}

#[test]
fn test_deserialize_default_rule_config_single() {
// single element present
let de: DefaultRuleConfig<u32> = serde_json::from_str("[123]").unwrap();
assert_eq!(de.into_inner(), 123u32);
let de: DefaultRuleConfig<bool> = serde_json::from_str("[true]").unwrap();
assert!(de.into_inner());
let de: DefaultRuleConfig<bool> = serde_json::from_str("[false]").unwrap();
assert!(!de.into_inner());

// empty array should use defaults
let de: DefaultRuleConfig<String> = serde_json::from_str("[]").unwrap();
assert_eq!(de.into_inner(), String::default());
}

#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
#[serde(default)]
struct Obj {
foo: String,
}

impl Default for Obj {
fn default() -> Self {
Self { foo: "defaultval".to_string() }
}
}

#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
#[serde(default)]
struct Pair(u32, Obj);

impl Default for Pair {
fn default() -> Self {
Self(123u32, Obj::default())
}
}

#[test]
fn test_deserialize_default_rule_config_tuple() {
// both elements present
let de: DefaultRuleConfig<Pair> =
serde_json::from_str(r#"[42, { "foo": "abc" }]"#).unwrap();
assert_eq!(de.into_inner(), Pair(42u32, Obj { foo: "abc".to_string() }));

// only first element present -> parsing the entire array into `Pair`
// will fail, so we parse the first element. Since Pair has #[serde(default)],
// serde will use the default value for the missing second field.
let de: DefaultRuleConfig<Pair> = serde_json::from_str("[10]").unwrap();
assert_eq!(de.into_inner(), Pair(10u32, Obj { foo: "defaultval".to_string() }));

// empty array -> both default
let de: DefaultRuleConfig<Pair> = serde_json::from_str("[]").unwrap();
assert_eq!(de.into_inner(), Pair(123u32, Obj { foo: "defaultval".to_string() }));
}

#[test]
fn test_deserialize_default_rule_config_object_in_array() {
// Single-element array containing an object should parse into the object
// configuration (fallback behavior, not the "entire-array as T" path).
let de: DefaultRuleConfig<Obj> = serde_json::from_str(r#"[{ "foo": "xyz" }]"#).unwrap();
assert_eq!(de.into_inner(), Obj { foo: "xyz".to_string() });

// Empty array -> default
let de: DefaultRuleConfig<Obj> = serde_json::from_str("[]").unwrap();
assert_eq!(de.into_inner(), Obj { foo: "defaultval".to_string() });
}

#[derive(serde::Deserialize, Debug, PartialEq, Eq, Default)]
#[serde(default)]
struct ComplexConfig {
foo: FxHashMap<String, serde_json::Value>,
}

#[test]
fn test_deserialize_default_rule_config_with_complex_shape() {
// A complex object shape for the rule config, like
// `[ { "foo": { "obj": "value" } } ]`.
let json = r#"[ { "foo": { "obj": "value" } } ]"#;
let de: DefaultRuleConfig<ComplexConfig> = serde_json::from_str(json).unwrap();
let cfg = de.into_inner();

let val = cfg.foo.get("obj").expect("obj key present");
assert_eq!(val, &serde_json::Value::String("value".to_string()));
}

#[derive(serde::Deserialize, Debug, PartialEq, Eq, Default)]
#[serde(rename_all = "camelCase")]
enum EnumOptions {
#[default]
OptionA,
OptionB,
}

#[test]
fn test_deserialize_default_rule_config_with_enum_config() {
// A basic enum config option.
let json = r#"["optionA"]"#;
let de: DefaultRuleConfig<EnumOptions> = serde_json::from_str(json).unwrap();

assert_eq!(de.into_inner(), EnumOptions::OptionA);

// Works with non-default value as well.
let json = r#"["optionB"]"#;
let de: DefaultRuleConfig<EnumOptions> = serde_json::from_str(json).unwrap();

assert_eq!(de.into_inner(), EnumOptions::OptionB);
}

#[derive(serde::Deserialize, Default, Debug, PartialEq, Eq)]
#[serde(default)]
struct TupleWithEnumAndObjectConfig(EnumOptions, Obj);

#[test]
fn test_deserialize_default_rule_config_with_enum_and_object() {
// A basic enum config option with an object.
let json = r#"["optionA", { "foo": "bar" }]"#;
let de: DefaultRuleConfig<TupleWithEnumAndObjectConfig> =
serde_json::from_str(json).unwrap();

assert_eq!(
de.into_inner(),
TupleWithEnumAndObjectConfig(EnumOptions::OptionA, Obj { foo: "bar".to_string() })
);

// Ensure that we can pass just one value and it'll provide the default for the second.
let json = r#"["optionB"]"#;
let de: DefaultRuleConfig<TupleWithEnumAndObjectConfig> =
serde_json::from_str(json).unwrap();

assert_eq!(
de.into_inner(),
TupleWithEnumAndObjectConfig(
EnumOptions::OptionB,
Obj { foo: "defaultval".to_string() }
)
);
}

#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
#[serde(default)]
struct ExampleObjConfig {
baz: String,
qux: bool,
}

impl Default for ExampleObjConfig {
fn default() -> Self {
Self { baz: "defaultbaz".to_string(), qux: false }
}
}

#[test]
fn test_deserialize_default_rule_with_object_with_multiple_fields() {
// Test a rule config that is a simple object with multiple fields.
let json = r#"[{ "baz": "fooval", "qux": true }]"#;
let de: DefaultRuleConfig<ExampleObjConfig> = serde_json::from_str(json).unwrap();

assert_eq!(de.into_inner(), ExampleObjConfig { baz: "fooval".to_string(), qux: true });

// Ensure that missing fields get their default values.
let json = r#"[{ "qux": true }]"#;
let de: DefaultRuleConfig<ExampleObjConfig> = serde_json::from_str(json).unwrap();

assert_eq!(de.into_inner(), ExampleObjConfig { baz: "defaultbaz".to_string(), qux: true });
}

fn assert_rule_runs_on_node_types<R: RuleMeta + RuleRunner>(
rule: &R,
node_types: &[oxc_ast::AstType],
Expand Down
Loading
Loading