Skip to content

Commit abefd1e

Browse files
authored
feat: handle properties behind spread syntax in require-meta-* rules (#251)
* fix: handle meta properties in spread syntax * add unit tests
1 parent f8268f2 commit abefd1e

14 files changed

+246
-58
lines changed

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ module.exports = {
4545
return {
4646
Program() {
4747
const sourceCode = context.getSourceCode();
48+
const { scopeManager } = sourceCode;
4849
const info = utils.getRuleInfo(sourceCode);
4950

5051
if (info === null) {
@@ -57,17 +58,13 @@ module.exports = {
5758
: DEFAULT_PATTERN;
5859

5960
const metaNode = info.meta;
60-
const docsNode =
61-
metaNode &&
62-
metaNode.properties &&
63-
metaNode.properties.find(
64-
(p) => p.type === 'Property' && utils.getKeyName(p) === 'docs'
65-
);
61+
const docsNode = utils
62+
.evaluateObjectProperties(metaNode, scopeManager)
63+
.find((p) => p.type === 'Property' && utils.getKeyName(p) === 'docs');
6664

67-
const descriptionNode =
68-
docsNode &&
69-
docsNode.value.properties &&
70-
docsNode.value.properties.find(
65+
const descriptionNode = utils
66+
.evaluateObjectProperties(docsNode && docsNode.value, scopeManager)
67+
.find(
7168
(p) =>
7269
p.type === 'Property' && utils.getKeyName(p) === 'description'
7370
);

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ module.exports = {
5050
*/
5151
create(context) {
5252
const options = context.options[0] || {};
53-
const sourceCode = context.getSourceCode();
5453
const filename = context.getFilename();
5554
const ruleName =
5655
filename === '<input>'
@@ -75,24 +74,24 @@ module.exports = {
7574

7675
return {
7776
Program() {
77+
const sourceCode = context.getSourceCode();
78+
const { scopeManager } = sourceCode;
79+
7880
const info = util.getRuleInfo(sourceCode);
7981
if (info === null) {
8082
return;
8183
}
8284

8385
const metaNode = info.meta;
84-
const docsPropNode =
85-
metaNode &&
86-
metaNode.properties &&
87-
metaNode.properties.find(
88-
(p) => p.type === 'Property' && util.getKeyName(p) === 'docs'
89-
);
90-
const urlPropNode =
91-
docsPropNode &&
92-
docsPropNode.value.properties &&
93-
docsPropNode.value.properties.find(
94-
(p) => p.type === 'Property' && util.getKeyName(p) === 'url'
95-
);
86+
const docsPropNode = util
87+
.evaluateObjectProperties(metaNode, scopeManager)
88+
.find((p) => p.type === 'Property' && util.getKeyName(p) === 'docs');
89+
const urlPropNode = util
90+
.evaluateObjectProperties(
91+
docsPropNode && docsPropNode.value,
92+
scopeManager
93+
)
94+
.find((p) => p.type === 'Property' && util.getKeyName(p) === 'url');
9695

9796
const staticValue = urlPropNode
9897
? getStaticValue(urlPropNode.value, context.getScope())

lib/rules/require-meta-fixable.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ module.exports = {
4848
context.options[0] && context.options[0].catchNoFixerButFixableProperty;
4949

5050
const sourceCode = context.getSourceCode();
51+
const { scopeManager } = sourceCode;
5152
const ruleInfo = utils.getRuleInfo(sourceCode);
5253
let contextIdentifiers;
5354
let usesFixFunctions;
@@ -62,10 +63,7 @@ module.exports = {
6263

6364
return {
6465
Program(ast) {
65-
contextIdentifiers = utils.getContextIdentifiers(
66-
sourceCode.scopeManager,
67-
ast
68-
);
66+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
6967
},
7068
CallExpression(node) {
7169
if (
@@ -86,11 +84,9 @@ module.exports = {
8684
'Program:exit'() {
8785
const metaFixableProp =
8886
ruleInfo &&
89-
ruleInfo.meta &&
90-
ruleInfo.meta.type === 'ObjectExpression' &&
91-
ruleInfo.meta.properties.find(
92-
(prop) => utils.getKeyName(prop) === 'fixable'
93-
);
87+
utils
88+
.evaluateObjectProperties(ruleInfo.meta, scopeManager)
89+
.find((prop) => utils.getKeyName(prop) === 'fixable');
9490

9591
if (metaFixableProp) {
9692
const staticValue = getStaticValue(

lib/rules/require-meta-has-suggestions.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,14 @@ module.exports = {
3030

3131
create(context) {
3232
const sourceCode = context.getSourceCode();
33+
const { scopeManager } = sourceCode;
3334
const ruleInfo = utils.getRuleInfo(sourceCode);
3435
let contextIdentifiers;
3536
let ruleReportsSuggestions;
3637

3738
return {
3839
Program(ast) {
39-
contextIdentifiers = utils.getContextIdentifiers(
40-
sourceCode.scopeManager,
41-
ast
42-
);
40+
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
4341
},
4442
CallExpression(node) {
4543
if (
@@ -78,12 +76,9 @@ module.exports = {
7876
},
7977
'Program:exit'() {
8078
const metaNode = ruleInfo && ruleInfo.meta;
81-
const hasSuggestionsProperty =
82-
metaNode && metaNode.type === 'ObjectExpression'
83-
? metaNode.properties.find(
84-
(prop) => utils.getKeyName(prop) === 'hasSuggestions'
85-
)
86-
: undefined;
79+
const hasSuggestionsProperty = utils
80+
.evaluateObjectProperties(metaNode, scopeManager)
81+
.find((prop) => utils.getKeyName(prop) === 'hasSuggestions');
8782
const hasSuggestionsStaticValue =
8883
hasSuggestionsProperty &&
8984
getStaticValue(hasSuggestionsProperty.value, context.getScope());

lib/rules/require-meta-schema.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,9 @@ module.exports = {
6464
Program(ast) {
6565
contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
6666

67-
schemaNode =
68-
metaNode &&
69-
metaNode.properties &&
70-
metaNode.properties.find(
67+
schemaNode = utils
68+
.evaluateObjectProperties(metaNode, scopeManager)
69+
.find(
7170
(p) => p.type === 'Property' && utils.getKeyName(p) === 'schema'
7271
);
7372

lib/rules/require-meta-type.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,17 @@ module.exports = {
4141
return {
4242
Program() {
4343
const sourceCode = context.getSourceCode();
44+
const { scopeManager } = sourceCode;
4445
const info = utils.getRuleInfo(sourceCode);
4546

4647
if (info === null) {
4748
return;
4849
}
4950

5051
const metaNode = info.meta;
51-
const typeNode =
52-
metaNode &&
53-
metaNode.properties &&
54-
metaNode.properties.find(
55-
(p) => p.type === 'Property' && utils.getKeyName(p) === 'type'
56-
);
52+
const typeNode = utils
53+
.evaluateObjectProperties(metaNode, scopeManager)
54+
.find((p) => p.type === 'Property' && utils.getKeyName(p) === 'type');
5755

5856
if (!typeNode) {
5957
context.report({

lib/utils.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,4 +738,28 @@ module.exports = {
738738
).suggest === parent.parent.parent
739739
);
740740
},
741+
742+
/**
743+
* List all properties contained in an object.
744+
* Evaluates and includes any properties that may be behind spreads.
745+
* @param {Node} objectNode
746+
* @param {ScopeManager} scopeManager
747+
* @returns {Node[]} the list of all properties that could be found
748+
*/
749+
evaluateObjectProperties(objectNode, scopeManager) {
750+
if (!objectNode || objectNode.type !== 'ObjectExpression') {
751+
return [];
752+
}
753+
754+
return objectNode.properties.flatMap((property) => {
755+
if (property.type === 'SpreadElement') {
756+
const value = findVariableValue(property.argument, scopeManager);
757+
if (value && value.type === 'ObjectExpression') {
758+
return value.properties;
759+
}
760+
return [];
761+
}
762+
return [property];
763+
});
764+
},
741765
};

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const RuleTester = require('eslint').RuleTester;
1111
// Tests
1212
// ------------------------------------------------------------------------------
1313

14-
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
14+
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 9 } });
1515
ruleTester.run('require-meta-docs-description', rule, {
1616
valid: [
1717
'foo()',
@@ -107,6 +107,15 @@ ruleTester.run('require-meta-docs-description', rule, {
107107
create(context) {}
108108
};
109109
`,
110+
// Spread.
111+
`
112+
const extraDocs = { description: 'enforce foo' };
113+
const extraMeta = { docs: { ...extraDocs } };
114+
module.exports = {
115+
meta: { ...extraMeta },
116+
create(context) {}
117+
};
118+
`,
110119
],
111120

112121
invalid: [

tests/lib/rules/require-meta-docs-url.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,19 @@ tester.run('require-meta-docs-url', rule, {
155155
},
156156
],
157157
},
158+
{
159+
// Spread.
160+
filename: 'test-rule',
161+
code: `
162+
const extraDocs = { url: "path/to/test-rule.md" };
163+
const extraMeta = { docs: { ...extraDocs } };
164+
module.exports = {
165+
meta: { ...extraMeta },
166+
create() {}
167+
}
168+
`,
169+
options: [{ pattern: 'path/to/{{name}}.md' }],
170+
},
158171
],
159172

160173
invalid: [
@@ -624,6 +637,51 @@ url: "plugin-name/test.md"
624637
],
625638
errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
626639
},
640+
{
641+
// URL missing, spreads present.
642+
filename: 'test.js',
643+
code: `
644+
const extraDocs = { };
645+
const extraMeta = { docs: { ...extraDocs } };
646+
module.exports = {
647+
meta: { ...extraMeta },
648+
create() {}
649+
}
650+
`,
651+
output: `
652+
const extraDocs = { };
653+
const extraMeta = { docs: { ...extraDocs,
654+
url: "plugin-name/test.md" } };
655+
module.exports = {
656+
meta: { ...extraMeta },
657+
create() {}
658+
}
659+
`,
660+
options: [{ pattern: 'plugin-name/{{ name }}.md' }],
661+
errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
662+
},
663+
{
664+
// URL wrong inside spreads.
665+
filename: 'test.js',
666+
code: `
667+
const extraDocs = { url: 'wrong' };
668+
const extraMeta = { docs: { ...extraDocs } };
669+
module.exports = {
670+
meta: { ...extraMeta },
671+
create() {}
672+
}
673+
`,
674+
output: `
675+
const extraDocs = { url: "plugin-name/test.md" };
676+
const extraMeta = { docs: { ...extraDocs } };
677+
module.exports = {
678+
meta: { ...extraMeta },
679+
create() {}
680+
}
681+
`,
682+
options: [{ pattern: 'plugin-name/{{ name }}.md' }],
683+
errors: [{ messageId: 'mismatch', type: 'Literal' }],
684+
},
627685
{
628686
// CJS file extension
629687
filename: 'test.cjs',

tests/lib/rules/require-meta-fixable.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const RuleTester = require('eslint').RuleTester;
1616
// Tests
1717
// ------------------------------------------------------------------------------
1818

19-
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
19+
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 9 } });
2020
ruleTester.run('require-meta-fixable', rule, {
2121
valid: [
2222
// No `meta`.
@@ -189,6 +189,16 @@ ruleTester.run('require-meta-fixable', rule, {
189189
`,
190190
options: [{ catchNoFixerButFixableProperty: true }],
191191
},
192+
// Spread.
193+
`
194+
const extra = { 'fixable': 'code' };
195+
module.exports = {
196+
meta: { ...extra },
197+
create(context) {
198+
context.report({node, message, fix: foo});
199+
}
200+
};
201+
`,
192202
],
193203

194204
invalid: [

0 commit comments

Comments
 (0)