-
-
Notifications
You must be signed in to change notification settings - Fork 740
docs(linter): Handle rules that use tuple config options with DefaultRuleConfig #16555
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR refactors the linter's rule configuration handling by enhancing DefaultRuleConfig to properly support tuple struct configurations, making it easier to implement ESLint-style array-based rule configs. The refactoring simplifies the sort_keys rule implementation and updates no_inner_declarations to use the new pattern.
Key Changes:
- Enhanced
DefaultRuleConfigdeserializer to handle tuple structs with smart fallback behavior for partial configurations - Refactored
sort_keysrule to use the new pattern, removing ~30 lines of manual config parsing - Converted
no_inner_declarationsto use tuple struct configuration withDefaultRuleConfig
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| crates/oxc_linter/src/rule.rs | Enhanced DefaultRuleConfig to handle tuple configs with comprehensive test coverage; updated documentation with tuple struct examples |
| crates/oxc_linter/src/rules/eslint/sort_keys.rs | Simplified from_configuration using DefaultRuleConfig pattern; removed helper methods in favor of direct tuple destructuring |
| crates/oxc_linter/src/rules/eslint/no_inner_declarations.rs | Refactored to tuple struct config with DefaultRuleConfig; introduces breaking change in default block_scoped_functions behavior |
Comments suppressed due to low confidence (1)
crates/oxc_linter/src/rules/eslint/no_inner_declarations.rs:30
- This refactoring changes the default behavior for
block_scoped_functions. Previously, when no second parameter was provided (e.g.,["functions"]), the old implementation defaulted toSome(BlockScopedFunctions::Allow). Now with#[serde(default)], the second field defaults toNone.
This breaks tests like the one on line 218-219 which expects ["functions"] to allow inner declarations in strict mode.
To maintain backward compatibility, you could either:
- Implement
DefaultforNoInnerDeclarationsConfigObjectto returnSome(BlockScopedFunctions::Allow)instead ofNone - Update the logic in the
runmethod to treatNoneasAllow(though this changes the meaning of the field)
The PR description mentions this is being left for review - a decision is needed on whether to match ESLint's behavior (where None might be the correct default) or maintain backward compatibility.
#[derive(Debug, Default, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", default)]
struct NoInnerDeclarationsConfigObject {
/// Controls whether function declarations in nested blocks are allowed in strict mode (ES6+ behavior).
#[schemars(with = "BlockScopedFunctions")]
block_scoped_functions: Option<BlockScopedFunctions>,
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
CodSpeed Performance ReportMerging #16555 will not alter performanceComparing Summary
Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
…ex config shapes. This will make it much easier to handle rules which accept tuples. I still need to fix a few things, though.
…mplify some things. This will hopefully also ensure that the most common case (a default config object for a given rule) is fast. But I still need to simplify the rest of the code here.
…imize it with this.
… now that we can do tuples in DefaultRuleConfig. This is a proof-of-concept to confirm that these updates to the DefaultRuleConfig tuple handling work as intended and simplify the code :)
…tions rule. I'm pretty sure there's a bug in this rule. It doesn't seem like it should've been returning `None` for the `block_scoped_functions` value by default, the original ESLint rule returns "allow" by default. But I left the behavior alone here, we can address that later if we want to.
I'm pretty sure the no_inner_declarations rule has a bug in it right now, so it failing here isn't really indicative of any problem with the . It's very odd to me that it was defaulting to None for the `block_scoped_functions` instead of `Allow`, I don't understand why that was done and it seems like there are behavioral differences vs. the original rule as a result.
…-body-style` rule.
I'd rather just leave it as-is for now so that we can focus on the other changes in this PR. I'll open another PR with the code cleanup on this rule.
ca59885 to
962bfe2
Compare
|
Rebased to make sure the changes to sort-keys on main didn't cause any issues (I ran the tests after trying the rebase locally and they passed fine) |
crates/oxc_linter/src/rule.rs
Outdated
| if let Ok(config) = serde_json::from_value::<T>(first.clone()) { | ||
| return Ok(DefaultRuleConfig(config)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there any way we can avoid the clones here? When i originally wrote this I was super careful about the clones, as this is a hot-ish path, so avoiding them where we can is ideal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I was a bit surprised this didn't impact the benchmarks at all, but I don't know if they're really exercising the config options much. I'll give these improvements a shot, but I may end up needing some help on getting it to work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I managed to get rid of almost all the clones, although the code is still jank.
crates/oxc_linter/src/rule.rs
Outdated
| // Attempt the tuple-of-objects case by appending an empty object | ||
| // (so `[obj]` -> `[obj, {}]`). If that deserializes to T, accept it. | ||
| let arr_two = serde_json::Value::Array(vec![ | ||
| first.clone(), | ||
| serde_json::Value::Object(serde_json::Map::new()), | ||
| ]); | ||
| if let Ok(config) = serde_json::from_value::<T>(arr_two) { | ||
| return Ok(DefaultRuleConfig(config)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this the same behaviour as eslint? I'm wondering whether we can just fallback to default if they only provide one of the config options
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess that depends on what you mean by this question. We can fallback to default for the second option if they only pass the first, but if you're asking to fallback to default for both tuple values when only one is passed, that'd be different from ESLint's implementation.
The three main shapes different rules take (as you know) are generally:
[{ "bar": "baz" }]["foo"]["foo", { "bar": "baz" }]
But a small handful of rules (literally 3 in core ESLint as far as I can tell, and one is stylistic/deprecated) take a tuple of objects, e.g. no-restricted-syntax:
"no-restricted-syntax": [
"error",
{
"selector": "FunctionExpression",
"message": "Function expressions are not allowed."
},
{
"selector": "CallExpression[callee.name='setTimeout'][arguments.length!=2]",
"message": "setTimeout must always be invoked with two arguments."
}
]
For no-restricted-syntax, you can just pass the first object value and it'll use the default for the second value (you can also technically pass an object for the first argument and a string for the second, which I despise). We need to somehow determine whether the shape of T matches this pattern in order to optimize what we do here, which I had trouble doing because I'm a Rust noob lol. So Copilot and I ended up with that current implementation which does wacky serialization attempts.
Since this pattern is only used in a small number of rules, maybe for optimization reasons we should assume that - if the first element in the config array is an object - this should just be treated as an object-only config like [{ "bar": "baz" }]. No need to bother with the case of a two-object tuple rule for now. Almost all the tuple rules are in the shape of ["foo", { "bar": "baz" }] anyway.
And then any rules that need the more complex handling can be dealt with later, or just won't be able to use DefaultRuleConfig/will need some alternative implementation like DefaultRuleConfigWithDoubleObject.
Does skipping the ability to handle two-object config tuples sound fine as a solution here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the logic for two-object tuple support in 332f8ea and that simplified things a ton.
crates/oxc_linter/src/rule.rs
Outdated
| let first = arr.first().cloned(); | ||
|
|
||
| if let Ok(t) = serde_json::from_value::<T>(serde_json::Value::Array(arr)) { | ||
| return Ok(DefaultRuleConfig(t)); | ||
| } | ||
|
|
||
| // Parsing the whole array failed; parse first element if present. | ||
| let config = | ||
| first.and_then(|v| serde_json::from_value(v).ok()).unwrap_or_else(T::default); | ||
|
|
||
| Ok(DefaultRuleConfig(config)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this feels wierd, you go from arr which is an array, clone it, just to make a new serde array?
I think we can be a bit stricter with what we accept here, if it improves performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rewrote this part to mostly fix this problem.
This simplifies the config parsing logic *a ton*.
Maybe we should consider adding a panic if the input array has more than one object passed to it, maybe only in debug mode?
| let t = serde_json::from_value::<T>(serde_json::Value::Array(arr)) | ||
| .unwrap_or_else(|_| T::default()); |
There was a problem hiding this comment.
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, this is all ready for re-review. |
…, I have. (#16562) Uses DefaultRuleConfig now, this rule does. Updated to use a proper tuple config, it has been. Continue to pass, the tests do. Part of #14743 and dependent on #16555, this PR is. Generated docs: ```md ## Configuration ### The 1st option type: `"never" | "always"` #### `"never"` The default `"never"` option can have exception options in an object literal, via `exceptRange` and `onlyEquality`. #### `"always"` The `"always"` option requires that literal values must always come first in comparisons. ### The 2nd option This option is an object with the following properties: #### exceptRange type: `boolean` default: `false` If the `"exceptRange"` property is `true`, the rule _allows_ yoda conditions in range comparisons which are wrapped directly in parentheses, including the parentheses of an `if` or `while` condition. A _range_ comparison tests whether a variable is inside or outside the range between two literal values. #### onlyEquality type: `boolean` default: `false` If the `"onlyEquality"` property is `true`, the rule reports yoda conditions _only_ for the equality operators `==` and `===`. The `onlyEquality` option allows a superset of the exceptions which `exceptRange` allows, thus both options are not useful together. ```
…so the config option docs for it (#16560) This is part of #16023. See [the tests for the original rule](https://github.com/eslint/eslint/blob/b017f094d4e53728f8d335b9cf8b16dc074afda3/tests/lib/rules/eqeqeq.js). We have a test in [the eqeqeq.rs implementation](https://github.com/oxc-project/oxc/blob/e24aabdfa65044a7223e4ea7b294ad3bf5dfb1ec/crates/oxc_linter/src/rules/eslint/eqeqeq.rs) like so: ```rs // Issue: <#8773> ("href != null", Some(json!([{"null": "ignore"}]))), ``` The problem is that this test has an incorrect shape for the config object, see here: ```jsonc // Should always be in one of these three formats, all three work in the original rule as well: "eslint/eqeqeq": ["error", "always", { "null": "never" }], "eslint/eqeqeq": ["error", "always"], "eslint/eqeqeq": ["error"], // But right now the tests have a case where the string arg is skipped, while the ESLint rule does not allow this: "eslint/eqeqeq": ["error", { "null": "ignore" }], ``` The problem is that the code _did_ previously handle this config array as invalid. However, because the implementation of `from` on NullType would fall back to `ignore` if it received bad data, it looked like it worked: ```rs impl NullType { pub fn from(raw: &str) -> Self { match raw { "always" => Self::Always, "never" => Self::Never, _ => Self::Ignore, } } } ``` Because `always` is marked as the default value (and is also the default value in the original ESLint rule), and so should be the default case. The test was just hitting the fallback value, so it looked like it worked, but really the fallback value was incorrect previously and did not match the docs _or_ the ESLint behavior. This fixes that issue by correcting the fallback value, and also fixes the auto-generated config shape/docs, so it correctly represents itself as taking a tuple. Generated docs: ```md ## Configuration ### The 1st option type: `"always" | "smart"` #### `"always"` Always require triple-equal comparisons, `===`/`!==`. This is the default. #### `"smart"` Allow certain safe comparisons to use `==`/`!=` (`typeof`, literals, nullish). ### The 2nd option This option is an object with the following properties: #### null type: `"always" | "never" | "ignore"` ##### `"always"` Always require triple-equals when comparing with null, `=== null`/`!== null`. This is the default. ##### `"never"` Never require triple-equals when comparing with null, always use `== null`/`!= null` ##### `"ignore"` Ignore null comparisons, allow either `== null`/`!= null` and `=== null`/`!== null` ```
This enables DefaultRuleConfig to handle tuple structs properly so we can have an easier time resolving #16023.
This is also part of #14743.
This allows us to refactor the way we handle the config for SortKeys, so it's much simpler. And also updates func-style and arrow-body-style to use the tuple pattern and have correct config option docs. I initially tried to update the no-inner-declarations rule to use the tuple pattern, but it lead to test failures due to what I suspect is a bug in the implementation of that rule. So I split that into its own PR.
This was done with help from GitHub Copilot + their Raptor mini model, but I've added enough tests - and all the rules that currently use DefaultRuleConfig still pass their tests - that I feel pretty good about it being functional, albeit probably not elegant. Happy to take feedback on improving this in that regard especially.
Updated the
eslint/func-stylerule:For
eslint/arrow-body-style: