From ba05f8259f7e562ee6c3015612c45a11da07d086 Mon Sep 17 00:00:00 2001 From: Bryan Mishkin <698306+bmish@users.noreply.github.com> Date: Sat, 19 Jun 2021 11:02:52 -0700 Subject: [PATCH] fix: Flag a violation when rule options are used but an empty schema is present in `require-meta-schema` rule This new check is similar to how: * `require-meta-fixable` checks that `fixable: true` is specified when an autofixer is found * `require-meta-has-suggestions` checks that `hasSuggestions: true` is specified when suggestions are provided Note: this is *not* a breaking change, since ESLint itself already prevents rules from using options when an empty rule schema is specified, so it's not possible that this new violation could be flagged on existing rules. --- docs/rules/require-meta-schema.md | 7 +++++ lib/rules/require-meta-schema.js | 37 +++++++++++++++++++--- tests/lib/rules/require-meta-schema.js | 43 ++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/docs/rules/require-meta-schema.md b/docs/rules/require-meta-schema.md index 41e5fd84..0783317f 100644 --- a/docs/rules/require-meta-schema.md +++ b/docs/rules/require-meta-schema.md @@ -20,6 +20,13 @@ module.exports = { meta: { schema: null }, create (context) {/* ... */}, }; + +module.exports = { + meta: { schema: [] }, + create (context) { + const options = context.options; /* using options when schema is empty */ + }, +}; ``` Examples of **correct** code for this rule: diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.js index 143563c8..0b54909c 100644 --- a/lib/rules/require-meta-schema.js +++ b/lib/rules/require-meta-schema.js @@ -28,6 +28,7 @@ module.exports = { }, ], messages: { + foundOptionsUsage: 'Found usage of rule options but no options are defined in `meta.schema`.', missing: '`meta.schema` is required (use [] if rule has no schema).', wrongType: '`meta.schema` should be an array or object (use [] if rule has no schema).', }, @@ -37,15 +38,20 @@ module.exports = { const sourceCode = context.getSourceCode(); const { scopeManager } = sourceCode; const info = utils.getRuleInfo(sourceCode); + if (info === null || info.meta === null) { + return {}; + } + + let contextIdentifiers; + let hasEmptySchema = false; + let schemaNode; return { - Program () { - if (info === null || info.meta === null) { - return; - } + Program (ast) { + contextIdentifiers = utils.getContextIdentifiers(context, ast); const metaNode = info.meta; - const schemaNode = + schemaNode = metaNode && metaNode.properties && metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'schema'); @@ -83,10 +89,31 @@ module.exports = { value = variable.defs[0].node.init; } + if ( + (value.type === 'ArrayExpression' && value.elements.length === 0) || + (value.type === 'ObjectExpression' && value.properties.length === 0) + ) { + // Schema is explicitly defined as having no options. + hasEmptySchema = true; + } + if (!['ArrayExpression', 'ObjectExpression'].includes(value.type)) { context.report({ node: value, messageId: 'wrongType' }); } }, + + MemberExpression (node) { + // Check if `context.options` was used when no options were defined in `meta.schema`. + if ( + hasEmptySchema && + node.object.type === 'Identifier' && + contextIdentifiers.has(node.object) && + node.property.type === 'Identifier' && + node.property.name === 'options' + ) { + context.report({ node: schemaNode, messageId: 'foundOptionsUsage' }); + } + }, }; }, }; diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.js index 25621162..0a32c60f 100644 --- a/tests/lib/rules/require-meta-schema.js +++ b/tests/lib/rules/require-meta-schema.js @@ -32,6 +32,27 @@ ruleTester.run('require-meta-schema', rule, { create(context) {} }; `, + // Schema with options and using `context.options`. + ` + module.exports = { + meta: { schema: { "enum": ["always", "never"] } }, + create(context) { const options = context.options; } + }; + `, + // Empty schema, using arbitrary property of `context`. + ` + module.exports = { + meta: { schema: [] }, + create(context) { const foo = context.foo; } + }; + `, + // Empty schema, using arbitrary `options` property. + ` + module.exports = { + meta: { schema: [] }, + create(context) { const options = foo.options; } + }; + `, ` const schema = []; module.exports = { @@ -119,5 +140,27 @@ schema: [] }, output: null, errors: [{ messageId: 'wrongType', type: 'Literal' }], }, + { + // Empty schema (array), but using rule options. + code: ` + module.exports = { + meta: { schema: [] }, + create(context) { const options = context.options; } + }; + `, + output: null, + errors: [{ messageId: 'foundOptionsUsage', type: 'Property' }], + }, + { + // Empty schema (object), but using rule options. + code: ` + module.exports = { + meta: { schema: {} }, + create(context) { const options = context.options; } + }; + `, + output: null, + errors: [{ messageId: 'foundOptionsUsage', type: 'Property' }], + }, ], });