Skip to content

Commit 01ff574

Browse files
committed
feat: add new rule require-meta-docs-description
The eslint core plugin has an internal lint rule to enforce this same thing: https://github.com/eslint/eslint/blob/master/tools/internal-rules/consistent-docs-description.js
1 parent 8c74f24 commit 01ff574

File tree

4 files changed

+285
-0
lines changed

4 files changed

+285
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Name | ✔️ | 🛠 | Description
6262
[prefer-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-placeholders.md) | | | disallow template literals as report messages
6363
[prefer-replace-text](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-replace-text.md) | | | prefer using replaceText instead of replaceTextRange.
6464
[report-message-format](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/report-message-format.md) | | | enforce a consistent format for rule report messages
65+
[require-meta-docs-description](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-description.md) | | | require rules to implement a meta.docs.description property with the correct format
6566
[require-meta-docs-url](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-url.md) | | 🛠 | require rules to implement a meta.docs.url property
6667
[require-meta-fixable](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-fixable.md) | ✔️ | | require rules to implement a meta.fixable property
6768
[require-meta-schema](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-schema.md) | | 🛠 | require rules to implement a meta.schema property
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# require rules to implement a meta.docs.description property (require-meta-docs-description)
2+
3+
Defining a clear and consistent description for each rule helps developers understand what they're used for.
4+
5+
In particular, each rule description should begin with an allowed prefix:
6+
* `enforce`
7+
* `require`
8+
* `disallow`
9+
10+
## Rule Details
11+
12+
This rule requires ESLint rules to have a valid `meta.docs.description` property.
13+
14+
Examples of **incorrect** code for this rule:
15+
16+
```js
17+
/* eslint eslint-plugin/require-meta-docs-description: error */
18+
module.exports = {
19+
meta: {},
20+
create: function(context) { /* ... */}
21+
};
22+
23+
module.exports = {
24+
meta: { description: 'this rule does ...' }, // missing allowed prefix
25+
create: function(context) { /* ... */}
26+
};
27+
```
28+
29+
Examples of **correct** code for this rule:
30+
31+
```js
32+
/* eslint eslint-plugin/require-meta-docs-description: error */
33+
module.exports = {
34+
meta: { description: 'disallow unused variables' },
35+
create: function(context) { /* ... */}
36+
};
37+
```
38+
39+
## Options
40+
41+
This rule takes an optional object containing:
42+
43+
- `String``pattern` — A regular expression that the description must match. Use `'.+'` to allow anything. Defaults to `^(enforce|require|disallow)`.
44+
45+
## Further Reading
46+
47+
* [working-with-rules#options-schemas](https://eslint.org/docs/developer-guide/working-with-rules#options-schemas)
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use strict';
2+
3+
const utils = require('../utils');
4+
5+
// ------------------------------------------------------------------------------
6+
// Rule Definition
7+
// ------------------------------------------------------------------------------
8+
9+
const DEFAULT_PATTERN = new RegExp('^(enforce|require|disallow)');
10+
11+
/**
12+
* Whether or not the node is a string literal
13+
*
14+
* @param {object} node
15+
* @returns {boolean} whether or not the node is a string literal
16+
*/
17+
function isStringLiteral (node) {
18+
return node.type === 'Literal' && typeof node.value === 'string';
19+
}
20+
21+
module.exports = {
22+
meta: {
23+
docs: {
24+
description: 'require rules to implement a meta.docs.description property with the correct format',
25+
category: 'Rules',
26+
recommended: false, // TODO: enable it in a major release.
27+
},
28+
type: 'suggestion',
29+
fixable: null,
30+
schema: [
31+
{
32+
type: 'object',
33+
properties: {
34+
pattern: {
35+
type: 'string',
36+
},
37+
},
38+
additionalProperties: false,
39+
},
40+
],
41+
messages: {
42+
missing: '`meta.docs.description` is required.',
43+
wrongType: '`meta.docs.description` must be a non-empty string.',
44+
extraWhitespace: '`meta.docs.description` must not have leading nor trailing whitespace.',
45+
},
46+
},
47+
48+
create (context) {
49+
const sourceCode = context.getSourceCode();
50+
const info = utils.getRuleInfo(sourceCode.ast, sourceCode.scopeManager);
51+
52+
return {
53+
Program () {
54+
if (info === null || info.meta === null) {
55+
return;
56+
}
57+
58+
const pattern = context.options[0] && context.options[0].pattern ? new RegExp(context.options[0].pattern) : DEFAULT_PATTERN;
59+
60+
const metaNode = info.meta;
61+
const docsNode =
62+
metaNode &&
63+
metaNode.properties &&
64+
metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'docs');
65+
66+
const descriptionNode =
67+
docsNode &&
68+
docsNode.value.properties &&
69+
docsNode.value.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'description');
70+
71+
if (!descriptionNode) {
72+
context.report({ node: docsNode ? docsNode : metaNode, messageId: 'missing' });
73+
} else if (!isStringLiteral(descriptionNode.value) || descriptionNode.value.value === '') {
74+
context.report({ node: descriptionNode.value, messageId: 'wrongType' });
75+
} else if (descriptionNode.value.value !== descriptionNode.value.value.trim()) {
76+
context.report({ node: descriptionNode.value, messageId: 'extraWhitespace' });
77+
} else if (!pattern.test(descriptionNode.value.value)) {
78+
context.report({
79+
node: descriptionNode.value,
80+
message: '`meta.docs.description` must match the regexp {{pattern}}.',
81+
data: { pattern },
82+
});
83+
}
84+
},
85+
};
86+
},
87+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
'use strict';
2+
3+
// ------------------------------------------------------------------------------
4+
// Requirements
5+
// ------------------------------------------------------------------------------
6+
7+
const rule = require('../../../lib/rules/require-meta-docs-description');
8+
const RuleTester = require('eslint').RuleTester;
9+
10+
// ------------------------------------------------------------------------------
11+
// Tests
12+
// ------------------------------------------------------------------------------
13+
14+
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
15+
ruleTester.run('require-meta-docs-description', rule, {
16+
valid: [
17+
`
18+
module.exports = {
19+
meta: { docs: { description: 'disallow unused variables' } },
20+
create(context) {}
21+
};
22+
`,
23+
`
24+
module.exports = {
25+
meta: { docs: { description: 'enforce a maximum line length' } },
26+
create(context) {}
27+
};
28+
`,
29+
`
30+
module.exports = {
31+
meta: { docs: { description: 'require or disallow newline at the end of files' } },
32+
create(context) {}
33+
};
34+
`,
35+
{
36+
code:
37+
`
38+
module.exports = {
39+
meta: { docs: { description: 'myPrefix foo bar' } },
40+
create(context) {}
41+
};
42+
`,
43+
options: [{ pattern: '^myPrefix' }],
44+
},
45+
{
46+
code:
47+
`
48+
module.exports = {
49+
meta: { docs: { description: 'random message' } },
50+
create(context) {}
51+
};
52+
`,
53+
options: [{ pattern: '.+' }], // any description allowed
54+
},
55+
],
56+
57+
invalid: [
58+
{
59+
code: `
60+
module.exports = {
61+
meta: {},
62+
create(context) {}
63+
};
64+
`,
65+
output: null,
66+
errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
67+
},
68+
{
69+
code: `
70+
module.exports = {
71+
meta: { docs: {} },
72+
create(context) {}
73+
};
74+
`,
75+
output: null,
76+
errors: [{ messageId: 'missing', type: 'Property' }],
77+
},
78+
{
79+
code: `
80+
module.exports = {
81+
meta: { docs: { description: [] } },
82+
create(context) {}
83+
};
84+
`,
85+
output: null,
86+
errors: [{ messageId: 'wrongType', type: 'ArrayExpression' }],
87+
},
88+
{
89+
code: `
90+
module.exports = {
91+
meta: { docs: { description: \`enforce with template literal\` } },
92+
create(context) {}
93+
};
94+
`,
95+
output: null,
96+
errors: [{ messageId: 'wrongType', type: 'TemplateLiteral' }],
97+
},
98+
{
99+
code: `
100+
module.exports = {
101+
meta: { docs: { description: SOME_DESCRIPTION } },
102+
create(context) {}
103+
};
104+
`,
105+
output: null,
106+
errors: [{ messageId: 'wrongType', type: 'Identifier' }],
107+
},
108+
{
109+
code: `
110+
module.exports = {
111+
meta: { docs: { description: '' } },
112+
create(context) {}
113+
};
114+
`,
115+
output: null,
116+
errors: [{ messageId: 'wrongType', type: 'Literal' }],
117+
},
118+
{
119+
code: `
120+
module.exports = {
121+
meta: { docs: { description: 'enforce something with trailing whitespace ' } },
122+
create(context) {}
123+
};
124+
`,
125+
output: null,
126+
errors: [{ messageId: 'extraWhitespace', type: 'Literal' }],
127+
},
128+
{
129+
code: `
130+
module.exports = {
131+
meta: { docs: { description: 'this rule does ...' } },
132+
create(context) {}
133+
};
134+
`,
135+
output: null,
136+
errors: [{ message: '`meta.docs.description` must match the regexp /^(enforce|require|disallow)/.', type: 'Literal' }],
137+
},
138+
{
139+
code: `
140+
module.exports = {
141+
meta: { docs: { description: 'this rule does ...' } },
142+
create(context) {}
143+
};
144+
`,
145+
output: null,
146+
options: [{ pattern: '^myPrefix' }],
147+
errors: [{ message: '`meta.docs.description` must match the regexp /^myPrefix/.', type: 'Literal' }],
148+
},
149+
],
150+
});

0 commit comments

Comments
 (0)