Skip to content

Commit 32de690

Browse files
CopilotSysix
andauthored
feat: add options parameter with withNursery to buildFromOxlintConfig function (#545)
Nursery rules specified in `.oxlintrc.json` were not being output by `buildFromOxlintConfigFile`, preventing ESLint from disabling them. Nursery rules are unstable and excluded by default by design, but users need opt-in access when explicitly configuring them. ## Changes **Type & API** - Added `BuildFromOxlintConfigOptions` type with optional `withNursery: boolean` flag - Updated `buildFromOxlintConfig()` and `buildFromOxlintConfigFile()` to accept optional `options` parameter (defaults to `{ withNursery: false }`) **Rule Filtering** - Modified `scripts/constants.ts`: removed 'nursery' from `ignoreCategories` to generate nursery rules - Updated `RulesGenerator` and `ConfigGenerator`: filter nursery rules when grouping by scope (but include when grouping by category) - Updated `handleCategoriesScope()` and `handleRulesScope()` to filter nursery rules unless `withNursery: true` - Modified `src/configs.ts`: explicitly exclude nursery rules from `all` and `flat/all` configs **Generated Files** - `rules-by-category.ts` and `configs-by-category.ts`: Include nursery rules (in `nurseryRules` and `flat/nursery` config) - `rules-by-scope.ts` and `configs-by-scope.ts`: Exclude nursery rules (no nursery in scope-based groupings like `flat/eslint`, `flat/react`, etc.) **Testing** - Added 6 tests verifying nursery rules behavior with/without `withNursery` option - Added 3 tests confirming nursery rules absent from `all`, `flat/all`, and scope-based configs ## Usage ```typescript // Default: nursery rules excluded (backward compatible) buildFromOxlintConfig({ rules: { 'import/named': 'error' } }); // => import/named NOT in output // Opt-in: nursery rules included buildFromOxlintConfig({ rules: { 'import/named': 'error' } }, { withNursery: true }); // => { 'import/named': 'off' } ``` Fully backward compatible—existing behavior unchanged. Fixes #412 <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Nursery rules are not output by buildFromOxlintConfigFile</issue_title> > <issue_description><details> > <summary>.oxlintrc.json</summary> > > ``` > { > "$schema": "./node_modules/oxlint/configuration_schema.json", > "plugins": [], > "rules": { > "import/no-cycle": "error", > "import/named": "error" > } > } > > ``` > </details> > > > <details> > <summary>Output from eslint-plugin-oxlint</summary> > > ``` > // output from oxlint.buildFromOxlintConfigFile('./.oxlintrc.json') > > [ > { > name: 'oxlint/from-oxlint-config', > rules: { > 'for-direction': 'off', > 'no-async-promise-executor': 'off', > 'no-caller': 'off', > 'no-class-assign': 'off', > 'no-useless-backreference': 'off', > 'no-compare-neg-zero': 'off', > 'no-cond-assign': 'off', > 'no-const-assign': 'off', > 'no-constant-binary-expression': 'off', > 'no-constant-condition': 'off', > 'no-control-regex': 'off', > 'no-debugger': 'off', > 'no-delete-var': 'off', > 'no-dupe-class-members': 'off', > 'no-dupe-else-if': 'off', > 'no-dupe-keys': 'off', > 'no-duplicate-case': 'off', > 'no-empty-character-class': 'off', > 'no-empty-pattern': 'off', > 'no-empty-static-block': 'off', > 'no-eval': 'off', > 'no-ex-assign': 'off', > 'no-extra-boolean-cast': 'off', > 'no-func-assign': 'off', > 'no-global-assign': 'off', > 'no-import-assign': 'off', > 'no-invalid-regexp': 'off', > 'no-irregular-whitespace': 'off', > 'no-loss-of-precision': 'off', > 'no-new-native-nonconstructor': 'off', > 'no-nonoctal-decimal-escape': 'off', > 'no-obj-calls': 'off', > 'no-self-assign': 'off', > 'no-setter-return': 'off', > 'no-shadow-restricted-names': 'off', > 'no-sparse-arrays': 'off', > 'no-this-before-super': 'off', > 'no-unsafe-finally': 'off', > 'no-unsafe-negation': 'off', > 'no-unsafe-optional-chaining': 'off', > 'no-unused-labels': 'off', > 'no-unused-private-class-members': 'off', > 'no-useless-catch': 'off', > 'no-useless-escape': 'off', > 'no-useless-rename': 'off', > 'no-with': 'off', > 'require-yield': 'off', > 'use-isnan': 'off', > 'valid-typeof': 'off', > 'import/no-cycle': 'off' > } > }, > { > name: 'oxlint/vue-svelte-exceptions', > ignores: [ '**/*.vue', '**/*.svelte' ], > rules: { 'no-unused-vars': 'off' } > } > ] > ``` > </details> > > > When explicitly specifying a rule within the [Nursery category](https://oxc.rs/docs/guide/usage/linter/rules.html#nursery-10) (in the above example, `import/named`), the plugin doesn't return the rule. This means it will not be turned off in the ESLint config. > > I would expect that any rules defined in the `.oxlintrc.json` file to be output as 'off'. The example above sees that non-Nursery rules (`import/no-cycle`) do this, but Nursery rules are not output. > </issue_description> > > <agent_instructions>`buildFromOxlintConfig` and `buildFromOxlintConfigFile` should have a second optional `options` parameter for nursery rules. > Do not ignore nursery rules in https://github.com/oxc-project/eslint-plugin-oxlint/blob/main/scripts/constants.ts > But do not implement them to the general config in `all` and `flat/all` config: > https://github.com/oxc-project/eslint-plugin-oxlint/blob/ae96e4995816341987448e836acbda732ec561cb/src/configs.ts#L30-L37 > Create tests for: > - nursery rules are not in the `all` and `flat/all` config > - nursery rules are not outputted with default `buildFromOxlintConfig` > - nursery rules are outputted with `options: withNursery=true` and `buildFromOxlintConfig`</agent_instructions> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > <comment_new><author>@Sysix</author><body> > Nursery rules are rules which are still under development. > Disabling the rule in ESLint will maybe not report warnings in oxlint as expected. > This was a design decision: #255 (review) and #256. > > ~~This decision was made when `buildFromOxlintConfigFile` was not created.~~ > Happy to see this implement with a second optional options parameter for `buildFromOxlintConfigFile`.</body></comment_new> > </comments> > </details> - Fixes #412 <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/oxc-project/eslint-plugin-oxlint/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Sysix <3897725+Sysix@users.noreply.github.com> Co-authored-by: Sysix <sysix@sysix-coding.de>
1 parent 02dcb2c commit 32de690

File tree

14 files changed

+175
-29
lines changed

14 files changed

+175
-29
lines changed

scripts/config-generator.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ export class ConfigGenerator {
4141
console.log(`Generating config, grouped by ${this.rulesGrouping}`);
4242

4343
const rulesGrouping = this.rulesGrouping;
44-
const rulesArray = this.rulesArray;
44+
// Filter out nursery rules when grouping by scope
45+
const rulesArray =
46+
this.rulesGrouping === RulesGrouping.SCOPE
47+
? this.rulesArray.filter((rule) => rule.category !== 'nursery')
48+
: this.rulesArray;
4549

4650
const rulesMap = this.groupItemsBy(rulesArray, rulesGrouping);
4751
const exportName = pascalCase(this.rulesGrouping);

scripts/constants.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
// these are the rules that don't have a direct equivalent in the eslint rules
22
export const ignoreScope = new Set(['oxc', 'deepscan', 'security']);
33

4-
// these are the rules that are not fully implemented in oxc
5-
export const ignoreCategories = new Set(['nursery']);
6-
74
// we are ignoring typescript type-aware rules for now, until it is stable.
85
// When support it with a flag, do the same for `ignoreCategories`.
96
// List copied from:

scripts/rules-generator.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ export class RulesGenerator {
4444
console.log(`Generating rules, grouped by ${this.rulesGrouping}`);
4545

4646
const rulesGrouping = this.rulesGrouping;
47-
const rulesArray = this.rulesArray;
47+
// Filter out nursery rules when grouping by scope
48+
const rulesArray =
49+
this.rulesGrouping === RulesGrouping.SCOPE
50+
? this.rulesArray.filter((rule) => rule.category !== 'nursery')
51+
: this.rulesArray;
4852

4953
const rulesMap = this.groupItemsBy(rulesArray, rulesGrouping);
5054

scripts/traverse-rules.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import { execSync } from 'node:child_process';
2-
import {
3-
ignoreCategories,
4-
ignoreScope,
5-
typescriptTypeAwareRules,
6-
} from './constants.js';
2+
import { ignoreScope, typescriptTypeAwareRules } from './constants.js';
73
import {
84
aliasPluginNames,
95
reactHookRulesInsideReactScope,
@@ -98,7 +94,6 @@ export function traverseRules(): Rule[] {
9894
// get all rules and filter the ignored one
9995
const rules = readRulesFromCommand().filter(
10096
(rule) =>
101-
!ignoreCategories.has(rule.category) &&
10297
!ignoreScope.has(rule.scope) &&
10398
// ignore type-aware rules
10499
!(

src/build-from-oxlint-config.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,46 @@ describe('integration test with oxlint', () => {
192192
}
193193
});
194194

195+
describe('nursery rules', () => {
196+
it('should not output nursery rules by default', () => {
197+
const config = {
198+
rules: {
199+
'import/named': 'error',
200+
'no-undef': 'error',
201+
},
202+
};
203+
204+
const configs = buildFromOxlintConfig(config);
205+
206+
expect(configs.length).toBeGreaterThanOrEqual(1);
207+
expect(configs[0].rules).not.toBeUndefined();
208+
209+
// nursery rules should NOT be present
210+
expect('import/named' in configs[0].rules!).toBe(false);
211+
expect('no-undef' in configs[0].rules!).toBe(false);
212+
});
213+
214+
it('should output nursery rules when withNursery option is true', () => {
215+
const config = {
216+
rules: {
217+
'import/named': 'error',
218+
'no-undef': 'error',
219+
},
220+
};
221+
222+
const configs = buildFromOxlintConfig(config, { withNursery: true });
223+
224+
expect(configs.length).toBeGreaterThanOrEqual(1);
225+
expect(configs[0].rules).not.toBeUndefined();
226+
227+
// nursery rules SHOULD be present when withNursery is true
228+
expect('import/named' in configs[0].rules!).toBe(true);
229+
expect('no-undef' in configs[0].rules!).toBe(true);
230+
expect(configs[0].rules!['import/named']).toBe('off');
231+
expect(configs[0].rules!['no-undef']).toBe('off');
232+
});
233+
});
234+
195235
const createConfigFileAndBuildFromIt = (
196236
filename: string,
197237
content: string

src/build-from-oxlint-config/categories.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { aliasPluginNames } from '../constants.js';
22
import configByCategory from '../generated/configs-by-category.js';
33
import {
4+
BuildFromOxlintConfigOptions,
45
OxlintConfig,
56
OxlintConfigCategories,
67
OxlintConfigPlugins,
@@ -18,11 +19,17 @@ export const defaultCategories: OxlintConfigCategories = {
1819
export const handleCategoriesScope = (
1920
plugins: OxlintConfigPlugins,
2021
categories: OxlintConfigCategories,
21-
rules: Record<string, 'off'>
22+
rules: Record<string, 'off'>,
23+
options: BuildFromOxlintConfigOptions = {}
2224
): void => {
2325
for (const category in categories) {
2426
const configName = `flat/${category}`;
2527

28+
// Skip nursery category unless explicitly enabled
29+
if (category === 'nursery' && !options.withNursery) {
30+
continue;
31+
}
32+
2633
// category is not enabled or not in found categories
2734
if (categories[category] === 'off' || !(configName in configByCategory)) {
2835
continue;

src/build-from-oxlint-config/index.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { EslintPluginOxlintConfig, OxlintConfig } from './types.js';
1+
import {
2+
BuildFromOxlintConfigOptions,
3+
EslintPluginOxlintConfig,
4+
OxlintConfig,
5+
} from './types.js';
26
import { handleRulesScope, readRulesFromConfig } from './rules.js';
37
import {
48
defaultCategories,
@@ -25,7 +29,8 @@ import path from 'node:path';
2529
* It accepts an object similar to the .oxlintrc.json file.
2630
*/
2731
export const buildFromOxlintConfig = (
28-
config: OxlintConfig
32+
config: OxlintConfig,
33+
options: BuildFromOxlintConfigOptions = {}
2934
): EslintPluginOxlintConfig[] => {
3035
resolveRelativeExtendsPaths(config);
3136

@@ -47,12 +52,12 @@ export const buildFromOxlintConfig = (
4752
plugins.push('react-hooks');
4853
}
4954

50-
handleCategoriesScope(plugins, categories, rules);
55+
handleCategoriesScope(plugins, categories, rules, options);
5156

5257
const configRules = readRulesFromConfig(config);
5358

5459
if (configRules !== undefined) {
55-
handleRulesScope(configRules, rules);
60+
handleRulesScope(configRules, rules, options);
5661
}
5762

5863
const baseConfig = {
@@ -72,7 +77,7 @@ export const buildFromOxlintConfig = (
7277
) as EslintPluginOxlintConfig[];
7378

7479
if (overrides !== undefined) {
75-
handleOverridesScope(overrides, configs, categories);
80+
handleOverridesScope(overrides, configs, categories, options);
7681
}
7782

7883
return configs;
@@ -86,7 +91,8 @@ export const buildFromOxlintConfig = (
8691
* no rules will be deactivated and an error to `console.error` will be emitted
8792
*/
8893
export const buildFromOxlintConfigFile = (
89-
oxlintConfigFile: string
94+
oxlintConfigFile: string,
95+
options: BuildFromOxlintConfigOptions = {}
9096
): EslintPluginOxlintConfig[] => {
9197
const config = getConfigContent(oxlintConfigFile);
9298

@@ -100,5 +106,5 @@ export const buildFromOxlintConfigFile = (
100106
filePath: path.resolve(oxlintConfigFile),
101107
};
102108

103-
return buildFromOxlintConfig(config);
109+
return buildFromOxlintConfig(config, options);
104110
};

src/build-from-oxlint-config/overrides.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { handleCategoriesScope } from './categories.js';
22
import { readPluginsFromConfig } from './plugins.js';
33
import { handleRulesScope, readRulesFromConfig } from './rules.js';
44
import {
5+
BuildFromOxlintConfigOptions,
56
EslintPluginOxlintConfig,
67
OxlintConfig,
78
OxlintConfigCategories,
@@ -11,7 +12,8 @@ import {
1112
export const handleOverridesScope = (
1213
overrides: OxlintConfigOverride[],
1314
configs: EslintPluginOxlintConfig[],
14-
baseCategories?: OxlintConfigCategories
15+
baseCategories?: OxlintConfigCategories,
16+
options: BuildFromOxlintConfigOptions = {}
1517
): void => {
1618
for (const [overrideIndex, override] of overrides.entries()) {
1719
const eslintRules: Record<string, 'off'> = {};
@@ -23,12 +25,12 @@ export const handleOverridesScope = (
2325

2426
const plugins = readPluginsFromConfig(override);
2527
if (baseCategories !== undefined && plugins !== undefined) {
26-
handleCategoriesScope(plugins, baseCategories, eslintRules);
28+
handleCategoriesScope(plugins, baseCategories, eslintRules, options);
2729
}
2830

2931
const rules = readRulesFromConfig(override);
3032
if (rules !== undefined) {
31-
handleRulesScope(rules, eslintRules);
33+
handleRulesScope(rules, eslintRules, options);
3234
}
3335

3436
eslintConfig.rules = eslintRules;

src/build-from-oxlint-config/rules.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import {
33
reactHookRulesInsideReactScope,
44
} from '../constants.js';
55
import {
6+
BuildFromOxlintConfigOptions,
67
OxlintConfig,
78
OxlintConfigOverride,
89
OxlintConfigRules,
910
} from './types.js';
1011
import configByCategory from '../generated/configs-by-category.js';
12+
import { nurseryRules } from '../generated/rules-by-category.js';
1113
import { isObject } from './utilities.js';
1214

1315
const allRulesObjects = Object.values(configByCategory).map(
@@ -17,12 +19,22 @@ const allRules: string[] = allRulesObjects.flatMap((rulesObject) =>
1719
Object.keys(rulesObject)
1820
);
1921

20-
const getEsLintRuleName = (rule: string): string | undefined => {
22+
const getEsLintRuleName = (
23+
rule: string,
24+
options: BuildFromOxlintConfigOptions = {}
25+
): string | undefined => {
2126
// there is no plugin prefix, it can be all plugin
2227
if (!rule.includes('/')) {
23-
return allRules.find(
28+
const found = allRules.find(
2429
(search) => search.endsWith(`/${rule}`) || search === rule
2530
);
31+
32+
// Filter out nursery rules unless explicitly enabled
33+
if (found && !options.withNursery && found in nurseryRules) {
34+
return undefined;
35+
}
36+
37+
return found;
2638
}
2739

2840
// greedy works with `@next/next/no-img-element` as an example
@@ -51,7 +63,14 @@ const getEsLintRuleName = (rule: string): string | undefined => {
5163
const expectedRule =
5264
esPluginName === '' ? ruleName : `${esPluginName}/${ruleName}`;
5365

54-
return allRules.find((rule) => rule === expectedRule);
66+
const found = allRules.find((rule) => rule === expectedRule);
67+
68+
// Filter out nursery rules unless explicitly enabled
69+
if (found && !options.withNursery && found in nurseryRules) {
70+
return undefined;
71+
}
72+
73+
return found;
5574
};
5675

5776
/**
@@ -77,10 +96,11 @@ const isActiveValue = (value: unknown) =>
7796
*/
7897
export const handleRulesScope = (
7998
oxlintRules: OxlintConfigRules,
80-
rules: Record<string, 'off'>
99+
rules: Record<string, 'off'>,
100+
options: BuildFromOxlintConfigOptions = {}
81101
): void => {
82102
for (const rule in oxlintRules) {
83-
const eslintName = getEsLintRuleName(rule);
103+
const eslintName = getEsLintRuleName(rule, options);
84104

85105
if (eslintName === undefined) {
86106
continue;

src/build-from-oxlint-config/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { Linter } from 'eslint';
22

3+
export type BuildFromOxlintConfigOptions = {
4+
withNursery?: boolean;
5+
};
6+
37
export type OxlintConfigExtends = string[];
48

59
export type OxlintConfigPlugins = string[];

0 commit comments

Comments
 (0)