Skip to content

Commit e6a2f42

Browse files
committed
Add new require-meta-default-options rule
1 parent e9c75eb commit e6a2f42

File tree

4 files changed

+382
-0
lines changed

4 files changed

+382
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ module.exports = [
9797
| [prefer-placeholders](docs/rules/prefer-placeholders.md) | require using placeholders for dynamic report messages | | | | |
9898
| [prefer-replace-text](docs/rules/prefer-replace-text.md) | require using `replaceText()` instead of `replaceTextRange()` | | | | |
9999
| [report-message-format](docs/rules/report-message-format.md) | enforce a consistent format for rule report messages | | | | |
100+
| [require-meta-default-options](docs/rules/require-meta-default-options.md) | require rules with options to implement a `meta.defaultOptions` property | | 🔧 | | |
100101
| [require-meta-docs-description](docs/rules/require-meta-docs-description.md) | require rules to implement a `meta.docs.description` property with the correct format | | | | |
101102
| [require-meta-docs-recommended](docs/rules/require-meta-docs-recommended.md) | require rules to implement a `meta.docs.recommended` property | | | | |
102103
| [require-meta-docs-url](docs/rules/require-meta-docs-url.md) | require rules to implement a `meta.docs.url` property | | 🔧 | | |
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Require rules with options to implement a `meta.defaultOptions` property (`eslint-plugin/require-meta-default-options`)
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Defining default options declaratively in a rule's `meta.defaultOptions` property enables ESLint v9.15.0+ to merge any user-provided options with the default options, simplifying the rule's implementation. It can also be useful for other tools like [eslint-doc-generator](https://github.com/bmish/eslint-doc-generator) to generate documentation for the rule's options.
8+
9+
## Rule Details
10+
11+
This rule requires ESLint rules to have a valid `meta.defaultOptions` property if and only if the rule has options defined in its `meta.schema` property.
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```js
16+
/* eslint eslint-plugin/require-meta-default-options: error */
17+
18+
module.exports = {
19+
meta: {
20+
schema: [
21+
{
22+
type: 'object',
23+
/* ... */
24+
},
25+
],
26+
// defaultOptions is missing
27+
},
28+
create(context) {
29+
/* ... */
30+
},
31+
};
32+
33+
module.exports = {
34+
meta: {
35+
schema: [],
36+
defaultOptions: [{}], // defaultOptions is not needed when schema is empty
37+
},
38+
create(context) {
39+
/* ... */
40+
},
41+
};
42+
43+
module.exports = {
44+
meta: {
45+
schema: [
46+
{
47+
/* ... */
48+
},
49+
],
50+
defaultOptions: {}, // defaultOptions should be an array
51+
},
52+
create(context) {
53+
/* ... */
54+
},
55+
};
56+
57+
module.exports = {
58+
meta: {
59+
schema: [
60+
{
61+
/* ... */
62+
},
63+
],
64+
defaultOptions: [], // defaultOptions should not be empty
65+
},
66+
create(context) {
67+
/* ... */
68+
},
69+
};
70+
```
71+
72+
Examples of **correct** code for this rule:
73+
74+
```js
75+
/* eslint eslint-plugin/require-meta-default-options: error */
76+
77+
module.exports = {
78+
meta: { schema: [] }, // no defaultOptions needed when schema is empty
79+
create(context) {
80+
/* ... */
81+
},
82+
};
83+
84+
module.exports = {
85+
meta: {
86+
schema: [
87+
{
88+
type: 'object',
89+
properties: {
90+
exceptRange: {
91+
type: 'boolean',
92+
},
93+
},
94+
additionalProperties: false,
95+
},
96+
],
97+
defaultOptions: [{ exceptRange: false }],
98+
},
99+
create(context) {
100+
/* ... */
101+
},
102+
};
103+
```
104+
105+
## Further Reading
106+
107+
- [ESLint rule docs: Option Defaults](https://eslint.org/docs/latest/extend/custom-rules#option-defaults)
108+
- [RFC introducing `meta.defaultOptions`](https://github.com/eslint/rfcs/blob/main/designs/2023-rule-options-defaults/README.md)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
'use strict';
2+
3+
const utils = require('../utils');
4+
5+
/** @type {import('eslint').Rule.RuleModule} */
6+
module.exports = {
7+
meta: {
8+
type: 'suggestion',
9+
docs: {
10+
description:
11+
'require rules with options to implement a `meta.defaultOptions` property',
12+
category: 'Rules',
13+
recommended: false,
14+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-default-options.md',
15+
},
16+
fixable: 'code',
17+
schema: [],
18+
messages: {
19+
missingDefaultOptions:
20+
'Rule with non-empty schema is missing a `meta.defaultOptions` property.',
21+
unnecessaryDefaultOptions:
22+
'Rule with empty schema should not have a `meta.defaultOptions` property.',
23+
defaultOptionsMustBeArray: 'Default options must be an array.',
24+
defaultOptionsMustNotBeEmpty: 'Default options must not be empty.',
25+
},
26+
},
27+
28+
create(context) {
29+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
30+
const { scopeManager } = sourceCode;
31+
const ruleInfo = utils.getRuleInfo(sourceCode);
32+
if (!ruleInfo) {
33+
return {};
34+
}
35+
36+
const metaNode = ruleInfo.meta;
37+
38+
const schemaNode = utils.getMetaSchemaNode(metaNode, scopeManager);
39+
const schemaProperty = utils.getMetaSchemaNodeProperty(
40+
schemaNode,
41+
scopeManager,
42+
);
43+
if (!schemaProperty) {
44+
return {};
45+
}
46+
47+
const metaDefaultOptions = utils
48+
.evaluateObjectProperties(metaNode, scopeManager)
49+
.find(
50+
(p) =>
51+
p.type === 'Property' && utils.getKeyName(p) === 'defaultOptions',
52+
);
53+
54+
if (
55+
schemaProperty.type === 'ArrayExpression' &&
56+
schemaProperty.elements.length === 0
57+
) {
58+
if (metaDefaultOptions) {
59+
context.report({
60+
node: metaDefaultOptions,
61+
messageId: 'unnecessaryDefaultOptions',
62+
fix(fixer) {
63+
return fixer.remove(metaDefaultOptions);
64+
},
65+
});
66+
}
67+
return {};
68+
}
69+
70+
if (!metaDefaultOptions) {
71+
context.report({
72+
node: metaNode,
73+
messageId: 'missingDefaultOptions',
74+
fix(fixer) {
75+
return fixer.insertTextAfter(schemaProperty, ', defaultOptions: []');
76+
},
77+
});
78+
return {};
79+
}
80+
81+
if (metaDefaultOptions.value.type !== 'ArrayExpression') {
82+
context.report({
83+
node: metaDefaultOptions.value,
84+
messageId: 'defaultOptionsMustBeArray',
85+
});
86+
return {};
87+
}
88+
89+
if (metaDefaultOptions.value.elements.length === 0) {
90+
context.report({
91+
node: metaDefaultOptions.value,
92+
messageId: 'defaultOptionsMustNotBeEmpty',
93+
});
94+
return {};
95+
}
96+
97+
return {};
98+
},
99+
};
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
'use strict';
2+
3+
const rule = require('../../../lib/rules/require-meta-default-options');
4+
const RuleTester = require('../eslint-rule-tester').RuleTester;
5+
6+
const ruleTester = new RuleTester({
7+
languageOptions: { sourceType: 'commonjs' },
8+
});
9+
10+
ruleTester.run('require-meta-default-options', rule, {
11+
valid: [
12+
'foo()',
13+
'module.exports = {};',
14+
`
15+
module.exports = {
16+
meta: {},
17+
create(context) {}
18+
};
19+
`,
20+
`
21+
module.exports = {
22+
meta: { schema: [] },
23+
create(context) {}
24+
};
25+
`,
26+
{
27+
code: `
28+
export default {
29+
meta: { schema: [] },
30+
create(context) {}
31+
};
32+
`,
33+
languageOptions: { sourceType: 'module' },
34+
},
35+
`
36+
const mySchema = [];
37+
module.exports = {
38+
meta: { docs: { schema: mySchema } },
39+
create(context) {}
40+
};
41+
`,
42+
`
43+
const meta = { schema: [] };
44+
module.exports = {
45+
meta,
46+
create(context) {}
47+
};
48+
`,
49+
`
50+
module.exports = {
51+
meta: { schema: [{}], defaultOptions: [1] },
52+
create(context) {}
53+
};
54+
`,
55+
`
56+
module.exports = {
57+
meta: { schema: [{}, {}], defaultOptions: [1] },
58+
create(context) {}
59+
};
60+
`,
61+
`
62+
module.exports = {
63+
meta: { schema: {}, defaultOptions: [1] },
64+
create(context) {}
65+
};
66+
`,
67+
],
68+
69+
invalid: [
70+
{
71+
code: `
72+
module.exports = {
73+
meta: { schema: [], defaultOptions: [1] },
74+
create(context) {}
75+
};
76+
`,
77+
output: `
78+
module.exports = {
79+
meta: { schema: [], },
80+
create(context) {}
81+
};
82+
`,
83+
errors: [{ messageId: 'unnecessaryDefaultOptions', type: 'Property' }],
84+
},
85+
{
86+
code: `
87+
module.exports = {
88+
meta: { schema: [{}] },
89+
create(context) {}
90+
};
91+
`,
92+
output: `
93+
module.exports = {
94+
meta: { schema: [{}], defaultOptions: [] },
95+
create(context) {}
96+
};
97+
`,
98+
errors: [
99+
{ messageId: 'missingDefaultOptions', type: 'ObjectExpression' },
100+
],
101+
},
102+
{
103+
code: `
104+
module.exports = {
105+
meta: { schema: [{}], defaultOptions: {} },
106+
create(context) {}
107+
};
108+
`,
109+
output: null,
110+
errors: [
111+
{ messageId: 'defaultOptionsMustBeArray', type: 'ObjectExpression' },
112+
],
113+
},
114+
{
115+
code: `
116+
module.exports = {
117+
meta: { schema: [{}], defaultOptions: undefined },
118+
create(context) {}
119+
};
120+
`,
121+
output: null,
122+
errors: [{ messageId: 'defaultOptionsMustBeArray', type: 'Identifier' }],
123+
},
124+
{
125+
code: `
126+
module.exports = {
127+
meta: { schema: [{}], defaultOptions: [] },
128+
create(context) {}
129+
};
130+
`,
131+
output: null,
132+
errors: [
133+
{ messageId: 'defaultOptionsMustNotBeEmpty', type: 'ArrayExpression' },
134+
],
135+
},
136+
],
137+
});
138+
139+
const ruleTesterTypeScript = new RuleTester({
140+
languageOptions: {
141+
parser: require('@typescript-eslint/parser'),
142+
parserOptions: { sourceType: 'module' },
143+
},
144+
});
145+
146+
ruleTesterTypeScript.run('require-meta-default-options (TypeScript)', rule, {
147+
valid: [
148+
`
149+
export default createESLintRule<Options, MessageIds>({
150+
meta: { schema: [] },
151+
create(context) {}
152+
});
153+
`,
154+
],
155+
invalid: [
156+
{
157+
code: `
158+
export default createESLintRule<Options, MessageIds>({
159+
meta: { schema: [{}] },
160+
create(context) {}
161+
});
162+
`,
163+
output: `
164+
export default createESLintRule<Options, MessageIds>({
165+
meta: { schema: [{}], defaultOptions: [] },
166+
create(context) {}
167+
});
168+
`,
169+
errors: [
170+
{ messageId: 'missingDefaultOptions', type: 'ObjectExpression' },
171+
],
172+
},
173+
],
174+
});

0 commit comments

Comments
 (0)