diff --git a/README.md b/README.md index a077de2a..e068161a 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Name | ✔️ | 🛠 | Description [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 [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 [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 +[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 [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 [test-case-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-property-ordering.md) | | 🛠 | Requires the properties of a test case to be placed in a consistent order [test-case-shorthand-strings](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-shorthand-strings.md) | | 🛠 | Enforce consistent usage of shorthand strings for test cases with no options diff --git a/docs/rules/require-meta-schema.md b/docs/rules/require-meta-schema.md new file mode 100644 index 00000000..24d7a231 --- /dev/null +++ b/docs/rules/require-meta-schema.md @@ -0,0 +1,53 @@ +# require rules to implement a meta.schema property (require-meta-schema) + +Defining a schema for each rule allows eslint to validate that configuration options are passed correctly. Even when there are no options for a rule, a schema should still be defined (as an empty array) so that eslint can validate that no data is passed to the rule. + +## Rule Details + +This rule requires ESLint rules to have a valid `meta.schema` property. + +Examples of **incorrect** code for this rule: + +```js +/* eslint eslint-plugin/require-meta-schema: error */ +module.exports = { + meta: {}, + create: function(context) { /* ... */} +}; + +module.exports = { + meta: { schema: null }, + create: function(context) { /* ... */} +}; +``` + +Examples of **correct** code for this rule: + +```js +/* eslint eslint-plugin/require-meta-type: error */ +module.exports = { + meta: { schema: [] }, // ensures no options are passed to the rule + create: function(context) { /* ... */} +}; + +module.exports = { + meta: { + schema: [ + { + type: 'object', + properties: { + exceptRange: { + type: 'boolean' + } + }, + additionalProperties: false + } + ] + }, + create: function(context) { /* ... */} +}; +``` + +## Further Reading + +* [working-with-rules#options-schemas](https://eslint.org/docs/developer-guide/working-with-rules#options-schemas) diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.js index 4529d280..6975d98e 100644 --- a/lib/rules/fixer-return.js +++ b/lib/rules/fixer-return.js @@ -24,6 +24,7 @@ module.exports = { }, type: 'problem', fixable: null, + schema: [], }, create (context) { diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.js index acb627a8..da211043 100644 --- a/lib/rules/require-meta-docs-url.js +++ b/lib/rules/require-meta-docs-url.js @@ -64,23 +64,6 @@ module.exports = { ); } - /** - * Insert a given property into a given object literal. - * @param {SourceCodeFixer} fixer The fixer. - * @param {Node} node The ObjectExpression node to insert a property. - * @param {string} propertyText The property code to insert. - * @returns {void} - */ - function insertProperty (fixer, node, propertyText) { - if (node.properties.length === 0) { - return fixer.replaceText(node, `{\n${propertyText}\n}`); - } - return fixer.insertTextAfter( - sourceCode.getLastToken(node.properties[node.properties.length - 1]), - `,\n${propertyText}` - ); - } - return { Program (node) { const info = util.getRuleInfo(node); @@ -125,10 +108,10 @@ module.exports = { return fixer.replaceText(urlPropNode.value, urlString); } if (docsPropNode && docsPropNode.value.type === 'ObjectExpression') { - return insertProperty(fixer, docsPropNode.value, `url: ${urlString}`); + return util.insertProperty(fixer, docsPropNode.value, `url: ${urlString}`, sourceCode); } if (!docsPropNode && metaNode && metaNode.type === 'ObjectExpression') { - return insertProperty(fixer, metaNode, `docs: {\nurl: ${urlString}\n}`); + return util.insertProperty(fixer, metaNode, `docs: {\nurl: ${urlString}\n}`, sourceCode); } } return null; diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.js new file mode 100644 index 00000000..a4d5def9 --- /dev/null +++ b/lib/rules/require-meta-schema.js @@ -0,0 +1,65 @@ +'use strict'; + +const utils = require('../utils'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'require rules to implement a meta.schema property', + category: 'Rules', + recommended: false, // TODO: enable it in a major release. + }, + type: 'suggestion', + fixable: 'code', + schema: [ + { + type: 'object', + properties: { + exceptRange: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + messages: { + missing: '`meta.schema` is required (use [] if rule has no schema).', + wrongType: '`meta.schema` should be an array (use [] if rule has no schema).', + }, + }, + + create (context) { + const sourceCode = context.getSourceCode(); + const info = utils.getRuleInfo(sourceCode.ast, sourceCode.scopeManager); + + return { + Program () { + if (info === null || info.meta === null) { + return; + } + + const metaNode = info.meta; + const schemaNode = + metaNode && + metaNode.properties && + metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'schema'); + + if (!schemaNode) { + context.report({ + node: metaNode, + messageId: 'missing', + fix (fixer) { + return utils.insertProperty(fixer, metaNode, 'schema: []', sourceCode); + }, + }); + } else if (schemaNode.value.type !== 'ArrayExpression') { + context.report({ node: schemaNode.value, messageId: 'wrongType' }); + } + }, + }; + }, +}; diff --git a/lib/utils.js b/lib/utils.js index bfc29731..0fd86bd4 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -322,4 +322,21 @@ module.exports = { .reduce((allRefs, refsForVariable) => allRefs.concat(refsForVariable), []) .map(ref => ref.identifier)); }, + + /** + * Insert a given property into a given object literal. + * @param {SourceCodeFixer} fixer The fixer. + * @param {Node} node The ObjectExpression node to insert a property. + * @param {string} propertyText The property code to insert. + * @returns {void} + */ + insertProperty (fixer, node, propertyText, sourceCode) { + if (node.properties.length === 0) { + return fixer.replaceText(node, `{\n${propertyText}\n}`); + } + return fixer.insertTextAfter( + sourceCode.getLastToken(node.properties[node.properties.length - 1]), + `,\n${propertyText}` + ); + }, }; diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.js new file mode 100644 index 00000000..21acd912 --- /dev/null +++ b/tests/lib/rules/require-meta-schema.js @@ -0,0 +1,76 @@ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/require-meta-schema'); +const RuleTester = require('eslint').RuleTester; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +ruleTester.run('require-meta-schema', rule, { + valid: [ + ` + module.exports = { + meta: { schema: [] }, + create(context) {} + }; + `, + ` + module.exports = { + meta: { schema: [ { "enum": ["always", "never"] } ] }, + create(context) {} + }; + `, + ], + + invalid: [ + { + code: ` + module.exports = { + meta: {}, + create(context) {} + }; + `, + output: ` + module.exports = { + meta: { +schema: [] +}, + create(context) {} + }; + `, + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + }, + { + code: ` + module.exports = { + meta: { type: 'problem' }, + create(context) {} + }; + `, + output: ` + module.exports = { + meta: { type: 'problem', +schema: [] }, + create(context) {} + }; + `, + errors: [{ messageId: 'missing', type: 'ObjectExpression' }], + }, + { + code: ` + module.exports = { + meta: { schema: null }, + create(context) {} + }; + `, + output: null, + errors: [{ messageId: 'wrongType', type: 'Literal' }], + }, + ], +});