docs(linter): Handle rules that use tuple config options with DefaultRuleConfig#16555
docs(linter): Handle rules that use tuple config options with DefaultRuleConfig#16555connorshea wants to merge 31 commits intomainfrom
Conversation
There was a problem hiding this comment.
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
|
…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.
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.
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.
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.
I managed to get rid of almost all the clones, although the code is still jank.
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?
|
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` ```
|
I can merge if @connorshea you are still able to rebase this somehow. |
After trying to rebase and get this working again, I think the best course of action is going to be a separate helper like DefaultRuleConfig, but dedicated to usage with tuple rules. So I'm working on that right now. |
|
I have opened #18372 as the successor for this PR, it will provide the same benefits and reuses a lot of this PR's code, but should not impact performance of config parsing on any non-tuple rules and does not modify the existing DefaultRuleConfig implementation at all. |
…uple config options. (#18372) This was built out of the ashes of #16555. Implements necessary work for #16023. This introduces a new TupleRuleConfig. Rather than forcing DefaultRuleConfig to bend to our will as attempted previously, this PR adds a distinct rule config setup that is built to handle tuple-based rule configs. This PR updates 3 rules to use TupleRuleConfig: - `eslint/eqeqeq` - `eslint/sort_keys` - `eslint/yoda` It also adds snapshot validation tests to ensure that they error as-expected when given an invalid config option. The only real change here is in `eslint/eqeqeq`. We previously allowed passing _only_ an object to the rule config (due to #8790), but that is not valid in the original rule, and also makes implementing proper config option validation quite a bit more difficult. I had removed support for this in #16560, but that was part of the previous, doomed attempt at using DefaultRuleConfig with tuples. So it never got merged, and I'm redoing it here. This is a necessary part of the change for this PR, as the alternative is to make the config options parsing/validation implementation _considerably_ more complex. This also brings us back in line with ESLint's behavior for this rule. --- AI Disclosure: I used the code and tests from #16555 as the basis for the changes in `rule.rs`, then let Claude Opus 4.5 run with it and guided it toward the TupleRuleConfig concept. After I got a satisfactory TupleRuleConfig implementation, I then manually implemented the rest of the changes in this PR by copying the files over from #16555, applying the TupleRuleConfig rename, and then carefully going through the diffs to make sure nothing was deleted that shouldn't have been. I have added tests to ensure that the config validation for tuple rules works, and have entirely avoided changing the unit tests in any of the 3 modified rules (other than the one change for eqeqeq I noted above). --- <details> <summary>Updated docs for the changed rules</summary> Note that the sort-keys docs are entirely unchanged as they were correct prior to this, so I've excluded them. yoda: ```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. ``` eqeqeq: ```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` or `=== null`/`!== null`. ``` </details>
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: