Skip to content

Commit 69312a0

Browse files
feat: add require-meta-schema-description rule
1 parent a3b3c05 commit 69312a0

12 files changed

+491
-69
lines changed

README.md

+25-24
Original file line numberDiff line numberDiff line change
@@ -80,30 +80,31 @@ module.exports = [
8080

8181
### Rules
8282

83-
| Name                          | Description | 💼 | 🔧 | 💡 | 💭 |
84-
| :--------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- | :-- | :-- | :-- | :-- |
85-
| [fixer-return](docs/rules/fixer-return.md) | require fixer functions to return a fix || | | |
86-
| [meta-property-ordering](docs/rules/meta-property-ordering.md) | enforce the order of meta properties | | 🔧 | | |
87-
| [no-deprecated-context-methods](docs/rules/no-deprecated-context-methods.md) | disallow usage of deprecated methods on rule context objects || 🔧 | | |
88-
| [no-deprecated-report-api](docs/rules/no-deprecated-report-api.md) | disallow the version of `context.report()` with multiple arguments || 🔧 | | |
89-
| [no-missing-message-ids](docs/rules/no-missing-message-ids.md) | disallow `messageId`s that are missing from `meta.messages` || | | |
90-
| [no-missing-placeholders](docs/rules/no-missing-placeholders.md) | disallow missing placeholders in rule report messages || | | |
91-
| [no-property-in-node](docs/rules/no-property-in-node.md) | disallow using `in` to narrow node types instead of looking at properties | | | | 💭 |
92-
| [no-unused-message-ids](docs/rules/no-unused-message-ids.md) | disallow unused `messageId`s in `meta.messages` || | | |
93-
| [no-unused-placeholders](docs/rules/no-unused-placeholders.md) | disallow unused placeholders in rule report messages || | | |
94-
| [no-useless-token-range](docs/rules/no-useless-token-range.md) | disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` || 🔧 | | |
95-
| [prefer-message-ids](docs/rules/prefer-message-ids.md) | require using `messageId` instead of `message` or `desc` to report rule violations || | | |
96-
| [prefer-object-rule](docs/rules/prefer-object-rule.md) | disallow function-style rules || 🔧 | | |
97-
| [prefer-placeholders](docs/rules/prefer-placeholders.md) | require using placeholders for dynamic report messages | | | | |
98-
| [prefer-replace-text](docs/rules/prefer-replace-text.md) | require using `replaceText()` instead of `replaceTextRange()` | | | | |
99-
| [report-message-format](docs/rules/report-message-format.md) | enforce a consistent format for rule report messages | | | | |
100-
| [require-meta-docs-description](docs/rules/require-meta-docs-description.md) | require rules to implement a `meta.docs.description` property with the correct format | | | | |
101-
| [require-meta-docs-recommended](docs/rules/require-meta-docs-recommended.md) | require rules to implement a `meta.docs.recommended` property | | | | |
102-
| [require-meta-docs-url](docs/rules/require-meta-docs-url.md) | require rules to implement a `meta.docs.url` property | | 🔧 | | |
103-
| [require-meta-fixable](docs/rules/require-meta-fixable.md) | require rules to implement a `meta.fixable` property || | | |
104-
| [require-meta-has-suggestions](docs/rules/require-meta-has-suggestions.md) | require suggestable rules to implement a `meta.hasSuggestions` property || 🔧 | | |
105-
| [require-meta-schema](docs/rules/require-meta-schema.md) | require rules to implement a `meta.schema` property || | 💡 | |
106-
| [require-meta-type](docs/rules/require-meta-type.md) | require rules to implement a `meta.type` property || | | |
83+
| Name                            | Description | 💼 | 🔧 | 💡 | 💭 |
84+
| :------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------- | :-- | :-- | :-- | :-- |
85+
| [fixer-return](docs/rules/fixer-return.md) | require fixer functions to return a fix || | | |
86+
| [meta-property-ordering](docs/rules/meta-property-ordering.md) | enforce the order of meta properties | | 🔧 | | |
87+
| [no-deprecated-context-methods](docs/rules/no-deprecated-context-methods.md) | disallow usage of deprecated methods on rule context objects || 🔧 | | |
88+
| [no-deprecated-report-api](docs/rules/no-deprecated-report-api.md) | disallow the version of `context.report()` with multiple arguments || 🔧 | | |
89+
| [no-missing-message-ids](docs/rules/no-missing-message-ids.md) | disallow `messageId`s that are missing from `meta.messages` || | | |
90+
| [no-missing-placeholders](docs/rules/no-missing-placeholders.md) | disallow missing placeholders in rule report messages || | | |
91+
| [no-property-in-node](docs/rules/no-property-in-node.md) | disallow using `in` to narrow node types instead of looking at properties | | | | 💭 |
92+
| [no-unused-message-ids](docs/rules/no-unused-message-ids.md) | disallow unused `messageId`s in `meta.messages` || | | |
93+
| [no-unused-placeholders](docs/rules/no-unused-placeholders.md) | disallow unused placeholders in rule report messages || | | |
94+
| [no-useless-token-range](docs/rules/no-useless-token-range.md) | disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` || 🔧 | | |
95+
| [prefer-message-ids](docs/rules/prefer-message-ids.md) | require using `messageId` instead of `message` or `desc` to report rule violations || | | |
96+
| [prefer-object-rule](docs/rules/prefer-object-rule.md) | disallow function-style rules || 🔧 | | |
97+
| [prefer-placeholders](docs/rules/prefer-placeholders.md) | require using placeholders for dynamic report messages | | | | |
98+
| [prefer-replace-text](docs/rules/prefer-replace-text.md) | require using `replaceText()` instead of `replaceTextRange()` | | | | |
99+
| [report-message-format](docs/rules/report-message-format.md) | enforce a consistent format for rule report messages | | | | |
100+
| [require-meta-docs-description](docs/rules/require-meta-docs-description.md) | require rules to implement a `meta.docs.description` property with the correct format | | | | |
101+
| [require-meta-docs-recommended](docs/rules/require-meta-docs-recommended.md) | require rules to implement a `meta.docs.recommended` property | | | | |
102+
| [require-meta-docs-url](docs/rules/require-meta-docs-url.md) | require rules to implement a `meta.docs.url` property | | 🔧 | | |
103+
| [require-meta-fixable](docs/rules/require-meta-fixable.md) | require rules to implement a `meta.fixable` property || | | |
104+
| [require-meta-has-suggestions](docs/rules/require-meta-has-suggestions.md) | require suggestable rules to implement a `meta.hasSuggestions` property || 🔧 | | |
105+
| [require-meta-schema](docs/rules/require-meta-schema.md) | require rules to implement a `meta.schema` property || | 💡 | |
106+
| [require-meta-schema-description](docs/rules/require-meta-schema-description.md) | require rules `meta.schema` properties to include descriptions || | | |
107+
| [require-meta-type](docs/rules/require-meta-type.md) | require rules to implement a `meta.type` property || | | |
107108

108109
### Tests
109110

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Require rules `meta.schema` properties to include descriptions (`eslint-plugin/require-meta-schema-description`)
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/eslint-community/eslint-plugin-eslint-plugin#presets).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Defining a description in the schema for each rule option helps explain that option to users.
8+
It also allows documentation generators such as [`eslint-doc-generator`](https://github.com/bmish/eslint-doc-generator) to generate more informative documentation for users.
9+
10+
## Rule Details
11+
12+
This rule requires that if a rule option has a property in the rule's `meta.schema`, it should have a `description`.
13+
14+
Examples of **incorrect** code for this rule:
15+
16+
```js
17+
/* eslint eslint-plugin/require-meta-schema-description: error */
18+
19+
module.exports = {
20+
meta: {
21+
schema: [
22+
{
23+
elements: { type: 'string' },
24+
type: 'array',
25+
},
26+
],
27+
},
28+
create() {},
29+
};
30+
31+
module.exports = {
32+
meta: {
33+
schema: [
34+
{
35+
properties: {
36+
something: {
37+
type: 'string',
38+
},
39+
},
40+
type: 'object',
41+
},
42+
],
43+
},
44+
create() {},
45+
};
46+
```
47+
48+
Examples of **correct** code for this rule:
49+
50+
```js
51+
/* eslint eslint-plugin/require-meta-schema-description: error */
52+
53+
module.exports = {
54+
meta: {
55+
schema: [
56+
{
57+
description: 'Elements to allow.',
58+
elements: { type: 'string' },
59+
type: 'array',
60+
},
61+
],
62+
},
63+
create() {},
64+
};
65+
66+
module.exports = {
67+
meta: {
68+
schema: [
69+
{
70+
oneOf: [
71+
{
72+
description: 'Elements to allow.',
73+
elements: { type: 'string' },
74+
type: 'array',
75+
},
76+
],
77+
},
78+
],
79+
},
80+
create() {},
81+
};
82+
```
83+
84+
## When Not To Use It
85+
86+
If your rule options are very simple and well-named, and your rule isn't used by developers outside of your immediate team, you may not need this rule.
87+
88+
## Further Reading
89+
90+
- [working-with-rules#options-schemas](https://eslint.org/docs/developer-guide/working-with-rules#options-schemas)

lib/rules/consistent-output.js

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ module.exports = {
2626
schema: [
2727
{
2828
type: 'string',
29+
description:
30+
"Whether to enforce having output assertions 'always' or to be 'consistent' when some cases have them.",
2931
enum: ['always', 'consistent'],
3032
default: 'consistent',
3133
},

lib/rules/meta-property-ordering.js

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ module.exports = {
2424
schema: [
2525
{
2626
type: 'array',
27+
description: 'What order to enforce for meta properties.',
2728
elements: { type: 'string' },
2829
},
2930
],

lib/rules/report-message-format.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ module.exports = {
2323
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/report-message-format.md',
2424
},
2525
fixable: null,
26-
schema: [{ type: 'string' }],
26+
schema: [
27+
{
28+
description: 'Format that all report messages must match.',
29+
type: 'string',
30+
},
31+
],
2732
messages: {
2833
noMatch: "Report message does not match the pattern '{{pattern}}'.",
2934
},

lib/rules/require-meta-docs-description.js

-8
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,6 @@ module.exports = {
7070
return;
7171
}
7272

73-
if (!descriptionNode) {
74-
context.report({
75-
node: docsNode || metaNode || ruleInfo.create,
76-
messageId: 'missing',
77-
});
78-
return;
79-
}
80-
8173
const staticValue = getStaticValue(descriptionNode.value, scope);
8274
if (!staticValue) {
8375
// Ignore non-static values since we can't determine what they look like.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use strict';
2+
3+
const utils = require('../utils');
4+
5+
// ------------------------------------------------------------------------------
6+
// Rule Definition
7+
// ------------------------------------------------------------------------------
8+
9+
/** @type {import('eslint').Rule.RuleModule} */
10+
module.exports = {
11+
meta: {
12+
type: 'suggestion',
13+
docs: {
14+
description:
15+
'require rules `meta.schema` properties to include descriptions',
16+
category: 'Rules',
17+
recommended: true,
18+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-schema-description.md',
19+
},
20+
schema: [],
21+
messages: {
22+
missingDescription: 'Schema option is missing an ajv description.',
23+
},
24+
},
25+
26+
create(context) {
27+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
28+
const { scopeManager } = sourceCode;
29+
const ruleInfo = utils.getRuleInfo(sourceCode);
30+
if (!ruleInfo) {
31+
return {};
32+
}
33+
34+
const schemaNode = utils.getMetaSchemaNode(ruleInfo.meta, scopeManager);
35+
if (!schemaNode) {
36+
return {};
37+
}
38+
39+
const schemaProperty = utils.getMetaSchemaNodeProperty(
40+
schemaNode,
41+
scopeManager,
42+
);
43+
if (schemaProperty?.type !== 'ArrayExpression') {
44+
return {};
45+
}
46+
47+
for (const element of schemaProperty.elements) {
48+
checkSchemaElement(element, true);
49+
}
50+
51+
return {};
52+
53+
function checkSchemaElement(node, isRoot) {
54+
if (node.type !== 'ObjectExpression') {
55+
return;
56+
}
57+
58+
let hadChildren = false;
59+
let hadDescription = false;
60+
61+
for (const { key, value } of node.properties) {
62+
switch (key?.name) {
63+
case 'description': {
64+
hadDescription = true;
65+
break;
66+
}
67+
68+
case 'oneOf': {
69+
hadChildren = true;
70+
71+
if (value.type === 'ArrayExpression') {
72+
for (const element of value.elements) {
73+
checkSchemaElement(element, isRoot);
74+
}
75+
}
76+
77+
break;
78+
}
79+
80+
case 'properties': {
81+
hadChildren = true;
82+
83+
for (const property of value.properties) {
84+
if (property.value?.type === 'ObjectExpression') {
85+
checkSchemaElement(property.value);
86+
}
87+
}
88+
89+
break;
90+
}
91+
}
92+
}
93+
94+
if (!hadDescription && !(hadChildren && isRoot)) {
95+
context.report({
96+
messageId: 'missingDescription',
97+
node,
98+
});
99+
}
100+
}
101+
},
102+
};

lib/rules/require-meta-schema.js

+16-35
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use strict';
22

3-
const { findVariable } = require('@eslint-community/eslint-utils');
43
const utils = require('../utils');
54

65
// ------------------------------------------------------------------------------
@@ -52,7 +51,6 @@ module.exports = {
5251

5352
let contextIdentifiers;
5453
const metaNode = ruleInfo.meta;
55-
let schemaNode;
5654

5755
// Options
5856
const requireSchemaPropertyWhenOptionless =
@@ -62,52 +60,35 @@ module.exports = {
6260
let hasEmptySchema = false;
6361
let isUsingOptions = false;
6462

63+
const schemaNode = utils.getMetaSchemaNode(metaNode, scopeManager);
64+
const schemaProperty = utils.getMetaSchemaNodeProperty(
65+
schemaNode,
66+
scopeManager,
67+
);
68+
6569
return {
6670
Program(ast) {
6771
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
6872

69-
schemaNode = utils
70-
.evaluateObjectProperties(metaNode, scopeManager)
71-
.find(
72-
(p) => p.type === 'Property' && utils.getKeyName(p) === 'schema',
73-
);
74-
75-
if (!schemaNode) {
73+
if (!schemaProperty) {
7674
return;
7775
}
7876

79-
let { value } = schemaNode;
80-
if (value.type === 'Identifier' && value.name !== 'undefined') {
81-
const variable = findVariable(
82-
scopeManager.acquire(value) || scopeManager.globalScope,
83-
value,
84-
);
85-
86-
// If we can't find the declarator, we have to assume it's in correct type
87-
if (
88-
!variable ||
89-
!variable.defs ||
90-
!variable.defs[0] ||
91-
!variable.defs[0].node ||
92-
variable.defs[0].node.type !== 'VariableDeclarator' ||
93-
!variable.defs[0].node.init
94-
) {
95-
return;
96-
}
97-
98-
value = variable.defs[0].node.init;
99-
}
100-
10177
if (
102-
(value.type === 'ArrayExpression' && value.elements.length === 0) ||
103-
(value.type === 'ObjectExpression' && value.properties.length === 0)
78+
(schemaProperty.type === 'ArrayExpression' &&
79+
schemaProperty.elements.length === 0) ||
80+
(schemaProperty.type === 'ObjectExpression' &&
81+
schemaProperty.properties.length === 0)
10482
) {
10583
// Schema is explicitly defined as having no options.
10684
hasEmptySchema = true;
10785
}
10886

109-
if (value.type === 'Literal' || utils.isUndefinedIdentifier(value)) {
110-
context.report({ node: value, messageId: 'wrongType' });
87+
if (
88+
schemaProperty.type === 'Literal' ||
89+
utils.isUndefinedIdentifier(schemaProperty)
90+
) {
91+
context.report({ node: schemaProperty, messageId: 'wrongType' });
11192
}
11293
},
11394

lib/rules/test-case-property-ordering.js

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module.exports = {
2626
schema: [
2727
{
2828
type: 'array',
29+
description: 'What order to enforce for test case properties.',
2930
elements: { type: 'string' },
3031
},
3132
],

0 commit comments

Comments
 (0)