Skip to content

Commit 875200b

Browse files
feat: add require-meta-schema-description rule (#490)
* feat: add require-meta-schema-description rule * Update lib/rules/require-meta-schema-description.js Co-authored-by: Brad Zacher <[email protected]> * Added tests too * recommended: false * Fill in test coverage * npm run update:eslint-docs --------- Co-authored-by: Brad Zacher <[email protected]>
1 parent a3b3c05 commit 875200b

12 files changed

+665
-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,88 @@
1+
# Require rules `meta.schema` properties to include descriptions (`eslint-plugin/require-meta-schema-description`)
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Defining a description in the schema for each rule option helps explain that option to users.
6+
It also allows documentation generators such as [`eslint-doc-generator`](https://github.com/bmish/eslint-doc-generator) to generate more informative documentation for users.
7+
8+
## Rule Details
9+
10+
This rule requires that if a rule option has a property in the rule's `meta.schema`, it should have a `description`.
11+
12+
Examples of **incorrect** code for this rule:
13+
14+
```js
15+
/* eslint eslint-plugin/require-meta-schema-description: error */
16+
17+
module.exports = {
18+
meta: {
19+
schema: [
20+
{
21+
elements: { type: 'string' },
22+
type: 'array',
23+
},
24+
],
25+
},
26+
create() {},
27+
};
28+
29+
module.exports = {
30+
meta: {
31+
schema: [
32+
{
33+
properties: {
34+
something: {
35+
type: 'string',
36+
},
37+
},
38+
type: 'object',
39+
},
40+
],
41+
},
42+
create() {},
43+
};
44+
```
45+
46+
Examples of **correct** code for this rule:
47+
48+
```js
49+
/* eslint eslint-plugin/require-meta-schema-description: error */
50+
51+
module.exports = {
52+
meta: {
53+
schema: [
54+
{
55+
description: 'Elements to allow.',
56+
elements: { type: 'string' },
57+
type: 'array',
58+
},
59+
],
60+
},
61+
create() {},
62+
};
63+
64+
module.exports = {
65+
meta: {
66+
schema: [
67+
{
68+
oneOf: [
69+
{
70+
description: 'Elements to allow.',
71+
elements: { type: 'string' },
72+
type: 'array',
73+
},
74+
],
75+
},
76+
],
77+
},
78+
create() {},
79+
};
80+
```
81+
82+
## When Not To Use It
83+
84+
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.
85+
86+
## Further Reading
87+
88+
- [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,109 @@
1+
'use strict';
2+
3+
const { getStaticValue } = require('@eslint-community/eslint-utils');
4+
const utils = require('../utils');
5+
6+
// ------------------------------------------------------------------------------
7+
// Rule Definition
8+
// ------------------------------------------------------------------------------
9+
10+
/** @type {import('eslint').Rule.RuleModule} */
11+
module.exports = {
12+
meta: {
13+
type: 'suggestion',
14+
docs: {
15+
description:
16+
'require rules `meta.schema` properties to include descriptions',
17+
category: 'Rules',
18+
recommended: false,
19+
url: 'https://github.com/eslint-community/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/require-meta-schema-description.md',
20+
},
21+
schema: [],
22+
messages: {
23+
missingDescription: 'Schema option is missing an ajv description.',
24+
},
25+
},
26+
27+
create(context) {
28+
const sourceCode = context.sourceCode || context.getSourceCode(); // TODO: just use context.sourceCode when dropping eslint < v9
29+
const { scopeManager } = sourceCode;
30+
const ruleInfo = utils.getRuleInfo(sourceCode);
31+
if (!ruleInfo) {
32+
return {};
33+
}
34+
35+
const schemaNode = utils.getMetaSchemaNode(ruleInfo.meta, scopeManager);
36+
if (!schemaNode) {
37+
return {};
38+
}
39+
40+
const schemaProperty = utils.getMetaSchemaNodeProperty(
41+
schemaNode,
42+
scopeManager,
43+
);
44+
if (schemaProperty?.type !== 'ArrayExpression') {
45+
return {};
46+
}
47+
48+
for (const element of schemaProperty.elements) {
49+
checkSchemaElement(element, true);
50+
}
51+
52+
return {};
53+
54+
function checkSchemaElement(node, isRoot) {
55+
if (node.type !== 'ObjectExpression') {
56+
return;
57+
}
58+
59+
let hadChildren = false;
60+
let hadDescription = false;
61+
62+
for (const { key, value } of node.properties) {
63+
const staticKey =
64+
key.type === 'Identifier' ? { value: key.name } : getStaticValue(key);
65+
if (!staticKey?.value) {
66+
continue;
67+
}
68+
69+
switch (key.name ?? key.value) {
70+
case 'description': {
71+
hadDescription = true;
72+
break;
73+
}
74+
75+
case 'oneOf': {
76+
hadChildren = true;
77+
78+
if (value.type === 'ArrayExpression') {
79+
for (const element of value.elements) {
80+
checkSchemaElement(element, isRoot);
81+
}
82+
}
83+
84+
break;
85+
}
86+
87+
case 'properties': {
88+
hadChildren = true;
89+
90+
for (const property of value.properties) {
91+
if (property.value?.type === 'ObjectExpression') {
92+
checkSchemaElement(property.value);
93+
}
94+
}
95+
96+
break;
97+
}
98+
}
99+
}
100+
101+
if (!hadDescription && !(hadChildren && isRoot)) {
102+
context.report({
103+
messageId: 'missingDescription',
104+
node,
105+
});
106+
}
107+
}
108+
},
109+
};

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)