Skip to content

Commit ff0ae38

Browse files
authored
New: add new rule require-meta-has-suggestions (#105)
Suggestable ESLint rules should have a `meta.hasSuggestions` property to indicate that they provide [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions). This new rule enforces that the `meta.hasSuggestions` property is correctly enabled when a rule provides suggestions, and not enabled when a rule does not provide suggestions. The change to require suggestable rules to have `meta.hasSuggestions` has been accepted and mentioned in the blog post for the upcoming ESLint 8 breaking changes. So we should adopt this change now to help plugin authors ensure they are compatible with ESLint 8 as soon as possible. The old property `meta.docs.suggestion` was unused anyway. https://eslint.org/blog/2021/06/whats-coming-in-eslint-8.0.0#rules-with-suggestions-now-require-the-metahassuggestions-property This is very similar to the existing [eslint-plugin/require-meta-fixable](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-fixable.md) rule enforcing the correct presence of the `meta.fixable` property.
1 parent b6ce109 commit ff0ae38

File tree

4 files changed

+368
-0
lines changed

4 files changed

+368
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Name | ✔️ | 🛠 | Description
6464
[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
6565
[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
6666
[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
67+
[require-meta-has-suggestions](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-has-suggestions.md) | | | require suggestable rules to implement a `meta.hasSuggestions` 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
6869
[require-meta-type](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-type.md) | | | require rules to implement a meta.type property
6970
[test-case-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-property-ordering.md) | | 🛠 | require the properties of a test case to be placed in a consistent order
+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# require suggestable rules to implement a `meta.hasSuggestions` property (require-meta-has-suggestions)
2+
3+
A suggestable ESLint rule should specify the `meta.hasSuggestions` property with a value of `true`. This makes it easier for both humans and tooling to tell whether a rule provides suggestions. [As of ESLint 8](https://eslint.org/blog/2021/06/whats-coming-in-eslint-8.0.0#rules-with-suggestions-now-require-the-metahassuggestions-property), an exception will be thrown if a suggestable rule is missing this property.
4+
5+
Likewise, rules that do not report suggestions should not enable the `meta.hasSuggestions` property.
6+
7+
## Rule Details
8+
9+
This rule aims to require ESLint rules to have a `meta.hasSuggestions` property if necessary.
10+
11+
The following patterns are considered warnings:
12+
13+
```js
14+
15+
/* eslint eslint-plugin/require-meta-has-suggestions: "error" */
16+
17+
module.exports = {
18+
meta: {}, // Missing `meta.hasSuggestions`.
19+
create(context) {
20+
context.report({
21+
node,
22+
message: 'foo',
23+
suggest: [
24+
{
25+
desc: 'Insert space at the beginning',
26+
fix: fixer => fixer.insertTextBefore(node, " ")
27+
}
28+
]
29+
});
30+
}
31+
};
32+
33+
```
34+
35+
```js
36+
37+
/* eslint eslint-plugin/require-meta-has-suggestions: "error" */
38+
39+
module.exports = {
40+
meta: { hasSuggestions: true }, // Has `meta.hasSuggestions` enabled but never provides suggestions.
41+
create(context) {
42+
context.report({
43+
node,
44+
message: 'foo'
45+
});
46+
}
47+
};
48+
49+
```
50+
51+
The following patterns are not warnings:
52+
53+
```js
54+
55+
/* eslint eslint-plugin/require-meta-has-suggestions: "error" */
56+
57+
module.exports = {
58+
meta: { hasSuggestions: true },
59+
create(context) {
60+
context.report({
61+
node,
62+
message: 'foo',
63+
suggest: [
64+
{
65+
desc: 'Insert space at the beginning',
66+
fix: fixer => fixer.insertTextBefore(node, " ")
67+
}
68+
]
69+
});
70+
}
71+
};
72+
73+
```
74+
75+
```js
76+
77+
/* eslint eslint-plugin/require-meta-has-suggestions: "error" */
78+
79+
module.exports = {
80+
meta: {},
81+
create(context) {
82+
context.report({
83+
node,
84+
message: 'foo'
85+
});
86+
}
87+
};
88+
89+
```
90+
91+
## Further Reading
92+
93+
* [ESLint's suggestion API](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions)
94+
* [ESLint rule basics describing the `meta.hasSuggestions` property](https://eslint.org/docs/developer-guide/working-with-rules#rule-basics)
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use strict';
2+
3+
const utils = require('../utils');
4+
const { getStaticValue } = require('eslint-utils');
5+
6+
// ------------------------------------------------------------------------------
7+
// Rule Definition
8+
// ------------------------------------------------------------------------------
9+
10+
module.exports = {
11+
meta: {
12+
docs: {
13+
description: 'require suggestable rules to implement a `meta.hasSuggestions` property',
14+
category: 'Rules',
15+
recommended: false,
16+
},
17+
type: 'problem',
18+
messages: {
19+
shouldBeSuggestable: 'Suggestable rules should specify a `meta.hasSuggestions` property with value `true`.',
20+
shouldNotBeSuggestable: 'Non-suggestable rules should not specify a `meta.hasSuggestions` property with value `true`.',
21+
},
22+
schema: [],
23+
},
24+
25+
create (context) {
26+
const sourceCode = context.getSourceCode();
27+
const ruleInfo = utils.getRuleInfo(sourceCode);
28+
let contextIdentifiers;
29+
let ruleReportsSuggestions;
30+
31+
return {
32+
Program (node) {
33+
contextIdentifiers = utils.getContextIdentifiers(context, node);
34+
},
35+
CallExpression (node) {
36+
if (
37+
node.callee.type === 'MemberExpression' &&
38+
contextIdentifiers.has(node.callee.object) &&
39+
node.callee.property.type === 'Identifier' &&
40+
node.callee.property.name === 'report' &&
41+
(node.arguments.length > 4 || (
42+
node.arguments.length === 1 &&
43+
node.arguments[0].type === 'ObjectExpression'
44+
))
45+
) {
46+
const suggestProp = node.arguments[0].properties.find(prop => utils.getKeyName(prop) === 'suggest');
47+
if (suggestProp) {
48+
const staticValue = getStaticValue(suggestProp.value, context.getScope());
49+
if (!staticValue || (Array.isArray(staticValue.value) && staticValue.value.length > 0)) {
50+
// These are all considered reporting suggestions:
51+
// suggest: [{...}]
52+
// suggest: getSuggestions()
53+
// suggest: MY_SUGGESTIONS
54+
ruleReportsSuggestions = true;
55+
}
56+
}
57+
}
58+
},
59+
'Program:exit' () {
60+
const metaNode = ruleInfo && ruleInfo.meta;
61+
const hasSuggestionsProperty = metaNode && metaNode.type === 'ObjectExpression' ? metaNode.properties.find(prop => utils.getKeyName(prop) === 'hasSuggestions') : undefined;
62+
const hasSuggestionsStaticValue = hasSuggestionsProperty && getStaticValue(hasSuggestionsProperty.value, context.getScope());
63+
64+
if (ruleReportsSuggestions) {
65+
if (!hasSuggestionsProperty) {
66+
// Rule reports suggestions but is missing the `meta.hasSuggestions` property altogether.
67+
context.report({ node: metaNode ? metaNode : ruleInfo.create, messageId: 'shouldBeSuggestable' });
68+
} else if (hasSuggestionsStaticValue.value !== true) {
69+
// Rule reports suggestions but does not have `meta.hasSuggestions` property enabled.
70+
context.report({ node: hasSuggestionsProperty.value, messageId: 'shouldBeSuggestable' });
71+
}
72+
} else if (!ruleReportsSuggestions && hasSuggestionsProperty && hasSuggestionsStaticValue.value === true) {
73+
// Rule does not report suggestions but has the `meta.hasSuggestions` property enabled.
74+
context.report({ node: hasSuggestionsProperty.value, messageId: 'shouldNotBeSuggestable' });
75+
}
76+
},
77+
};
78+
},
79+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
'use strict';
2+
3+
// ------------------------------------------------------------------------------
4+
// Requirements
5+
// ------------------------------------------------------------------------------
6+
7+
const rule = require('../../../lib/rules/require-meta-has-suggestions');
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-has-suggestions', rule, {
16+
valid: [
17+
'module.exports = context => {};',
18+
// No suggestions reported, no violations reported, no meta object.
19+
`
20+
module.exports = {
21+
create(context) {}
22+
};
23+
`,
24+
// No suggestions reported, no violations reported, empty meta object.
25+
`
26+
module.exports = {
27+
meta: {},
28+
create(context) {}
29+
};
30+
`,
31+
// No suggestions reported, violation reported, empty meta object.
32+
`
33+
module.exports = {
34+
meta: {},
35+
create(context) {
36+
context.report({node, message});
37+
}
38+
};
39+
`,
40+
// No suggestions reported, no suggestion property, non-object style of reporting.
41+
`
42+
module.exports = {
43+
meta: {},
44+
create(context) {
45+
context.report(node, message);
46+
}
47+
};
48+
`,
49+
// No suggestions reported (empty suggest array), no suggestion property.
50+
`
51+
module.exports = {
52+
meta: {},
53+
create(context) {
54+
context.report({node, message, suggest:[]});
55+
}
56+
};
57+
`,
58+
// No suggestions reported (empty suggest array in variable), no suggestion property.
59+
`
60+
const SUGGESTIONS = [];
61+
module.exports = {
62+
meta: {},
63+
create(context) {
64+
context.report({node, message, suggest: SUGGESTIONS});
65+
}
66+
};
67+
`,
68+
// No suggestions reported, hasSuggestions property set to false.
69+
`
70+
module.exports = {
71+
meta: { hasSuggestions: false },
72+
create(context) {
73+
context.report({node, message});
74+
}
75+
};
76+
`,
77+
// No suggestions reported, hasSuggestions property set to false (as variable).
78+
`
79+
const hasSuggestions = false;
80+
module.exports = {
81+
meta: { hasSuggestions },
82+
create(context) {
83+
context.report({node, message});
84+
}
85+
};
86+
`,
87+
// Provides suggestions, has hasSuggestions property.
88+
`
89+
module.exports = {
90+
meta: { hasSuggestions: true },
91+
create(context) {
92+
context.report({node, message, suggest: [{}]});
93+
}
94+
};
95+
`,
96+
// Provides suggestions, has hasSuggestions property (as variable).
97+
`
98+
const hasSuggestions = true;
99+
module.exports = {
100+
meta: { hasSuggestions },
101+
create(context) {
102+
context.report({node, message, suggest: [{}]});
103+
}
104+
};
105+
`,
106+
// Provides *dynamic* suggestions, has hasSuggestions property.
107+
`
108+
module.exports = {
109+
meta: { hasSuggestions: true },
110+
create(context) {
111+
context.report({node, message, suggest: getSuggestions()});
112+
}
113+
};
114+
`,
115+
// Spread syntax.
116+
{
117+
code: `
118+
const meta = {};
119+
module.exports = {
120+
...meta,
121+
meta: {},
122+
create(context) { context.report(node, message, data, fix); }
123+
};
124+
`,
125+
parserOptions: {
126+
ecmaVersion: 9,
127+
},
128+
},
129+
],
130+
131+
invalid: [
132+
{
133+
// Reports suggestions, no meta object, violation should be on `create` function.
134+
code: `
135+
module.exports = {
136+
create(context) { context.report({node, message, suggest: [{}]}); }
137+
};
138+
`,
139+
errors: [{ messageId: 'shouldBeSuggestable', type: 'FunctionExpression', line: 3, column: 17, endLine: 3, endColumn: 78 }],
140+
},
141+
{
142+
// Reports suggestions, no hasSuggestions property, violation should be on `meta` object.
143+
code: `
144+
module.exports = {
145+
meta: {},
146+
create(context) { context.report({node, message, suggest: [{}]}); }
147+
};
148+
`,
149+
errors: [{ messageId: 'shouldBeSuggestable', type: 'ObjectExpression', line: 3, column: 17, endLine: 3, endColumn: 19 }],
150+
},
151+
{
152+
// Reports suggestions (in variable), no hasSuggestions property, violation should be on `meta` object.
153+
code: `
154+
const SUGGESTIONS = [{}];
155+
module.exports = {
156+
meta: {},
157+
create(context) { context.report({node, message, suggest: SUGGESTIONS}); }
158+
};
159+
`,
160+
errors: [{ messageId: 'shouldBeSuggestable', type: 'ObjectExpression', line: 4, column: 17, endLine: 4, endColumn: 19 }],
161+
},
162+
{
163+
// Reports suggestions, hasSuggestions property set to false, violation should be on `false`
164+
code: `
165+
module.exports = {
166+
meta: { hasSuggestions: false },
167+
create(context) { context.report({node, message, suggest: [{}]}); }
168+
};
169+
`,
170+
errors: [{ messageId: 'shouldBeSuggestable', type: 'Literal', line: 3, column: 35, endLine: 3, endColumn: 40 }],
171+
},
172+
{
173+
// Reports suggestions, hasSuggestions property set to false (as variable), violation should be on variable
174+
code: `
175+
const hasSuggestions = false;
176+
module.exports = {
177+
meta: { hasSuggestions },
178+
create(context) { context.report({node, message, suggest: [{}]}); }
179+
};
180+
`,
181+
errors: [{ messageId: 'shouldBeSuggestable', type: 'Identifier', line: 4, column: 19, endLine: 4, endColumn: 33 }],
182+
},
183+
{
184+
// Does not report suggestions, hasSuggestions property set to true, violation should be on `true`
185+
code: `
186+
module.exports = {
187+
meta: { hasSuggestions: true },
188+
create(context) { context.report({node, message}); }
189+
};
190+
`,
191+
errors: [{ messageId: 'shouldNotBeSuggestable', type: 'Literal', line: 3, column: 35, endLine: 3, endColumn: 39 }],
192+
},
193+
],
194+
});

0 commit comments

Comments
 (0)