From f6417d3186cdd656fac185fc922c9cafd55c7fca Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Sat, 10 Jun 2017 22:28:09 -0700 Subject: [PATCH] [New] `jsx-boolean-value`: add inverse option for always/never Fixes #1249 --- docs/rules/jsx-boolean-value.md | 12 ++-- lib/rules/jsx-boolean-value.js | 103 +++++++++++++++++++-------- tests/lib/rules/jsx-boolean-value.js | 30 ++++++-- 3 files changed, 106 insertions(+), 39 deletions(-) diff --git a/docs/rules/jsx-boolean-value.md b/docs/rules/jsx-boolean-value.md index 5dcfabe9e3..c888e7af70 100644 --- a/docs/rules/jsx-boolean-value.md +++ b/docs/rules/jsx-boolean-value.md @@ -6,27 +6,29 @@ ## Rule Details -This rule takes one argument. If it is `"always"` then it warns whenever an attribute is missing its value. If `"never"` then it warns if an attribute has a `true` value. The default value of this option is `"never"`. +This rule takes two arguments. If the first argument is `"always"` then it warns whenever an attribute is missing its value. If `"never"` then it warns if an attribute has a `true` value. The default value of this option is `"never"`. -The following patterns are considered warnings when configured `"never"`: +The second argument is optional: if provided, it must be an object with a `"never"` property (if the first argument is `"always"`), or an `"always"` property (if the first argument is `"never"`). This property’s value must be an array of strings representing prop names. + +The following patterns are considered warnings when configured `"never"`, or with `"always", { "never": ["personal"] }`: ```jsx var Hello = ; ``` -The following patterns are not considered warnings when configured `"never"`: +The following patterns are not considered warnings when configured `"never"`, or with `"always", { "never": ["personal"] }`: ```jsx var Hello = ; ``` -The following patterns are considered warnings when configured `"always"`: +The following patterns are considered warnings when configured `"always"`, or with `"never", { "always": ["personal"] }`: ```jsx var Hello = ; ``` -The following patterns are not considered warnings when configured `"always"`: +The following patterns are not considered warnings when configured `"always"`, or with `"never", { "always": ["personal"] }`: ```jsx var Hello = ; diff --git a/lib/rules/jsx-boolean-value.js b/lib/rules/jsx-boolean-value.js index 3a824e5ab9..7581be7eb3 100644 --- a/lib/rules/jsx-boolean-value.js +++ b/lib/rules/jsx-boolean-value.js @@ -8,6 +8,15 @@ // Rule Definition // ------------------------------------------------------------------------------ +const exceptionsSchema = { + type: 'array', + items: {type: 'string', minLength: 1}, + uniqueItems: true +}; + +const ALWAYS = 'always'; +const NEVER = 'never'; + module.exports = { meta: { docs: { @@ -17,44 +26,78 @@ module.exports = { }, fixable: 'code', - schema: [{ - enum: ['always', 'never'] - }] + schema: { + anyOf: [{ + type: 'array', + items: [{enum: [ALWAYS, NEVER]}], + additionalItems: false + }, { + type: 'array', + items: [{ + enum: [ALWAYS] + }, { + type: 'object', + additionalProperties: false, + properties: { + [NEVER]: exceptionsSchema + } + }], + additionalItems: false + }, { + type: 'array', + items: [{ + enum: [NEVER] + }, { + type: 'object', + additionalProperties: false, + properties: { + [ALWAYS]: exceptionsSchema + } + }], + additionalItems: false + }] + } }, create: function(context) { - var configuration = context.options[0] || 'never'; + const configuration = context.options[0] || NEVER; + const configObject = context.options[1] || {}; + const exceptions = new Set((configuration === ALWAYS ? configObject[NEVER] : configObject[ALWAYS]) || []); + const exceptionProps = Array.from(exceptions, (name) => `\`${name}\``).join(', '); + const exceptionsMessage = exceptions.size > 0 ? ` for the following props: ${exceptionProps}` : ''; + const data = {exceptionsMessage: exceptionsMessage}; - var NEVER_MESSAGE = 'Value must be omitted for boolean attributes'; - var ALWAYS_MESSAGE = 'Value must be set for boolean attributes'; + const NEVER_MESSAGE = 'Value must be omitted for boolean attributes{{exceptionsMessage}}'; + const ALWAYS_MESSAGE = 'Value must be set for boolean attributes{{exceptionsMessage}}'; return { - JSXAttribute: function(node) { - switch (configuration) { - case 'always': - if (node.value === null) { - context.report({ - node: node, - message: ALWAYS_MESSAGE, - fix: function(fixer) { - return fixer.insertTextAfter(node, '={true}'); - } - }); + JSXAttribute(node) { + const propName = node.name && node.name.name; + const value = node.value; + const isException = exceptions.has(propName); + + const isAlways = configuration === ALWAYS ? !isException : isException; + const isNever = configuration === NEVER ? !isException : isException; + + if (isAlways && value === null) { + context.report({ + node: node, + message: ALWAYS_MESSAGE, + data: data, + fix(fixer) { + return fixer.insertTextAfter(node, '={true}'); } - break; - case 'never': - if (node.value && node.value.type === 'JSXExpressionContainer' && node.value.expression.value === true) { - context.report({ - node: node, - message: NEVER_MESSAGE, - fix: function(fixer) { - return fixer.removeRange([node.name.range[1], node.value.range[1]]); - } - }); + }); + } + if (isNever && value && value.type === 'JSXExpressionContainer' && value.expression.value === true) { + context.report({ + node: node, + message: NEVER_MESSAGE, + data: data, + fix(fixer) { + return fixer.removeRange([node.name.range[1], value.range[1]]); } - break; - default: - break; + }); } } }; diff --git a/tests/lib/rules/jsx-boolean-value.js b/tests/lib/rules/jsx-boolean-value.js index 67ab4ddaed..d737ef8d5f 100644 --- a/tests/lib/rules/jsx-boolean-value.js +++ b/tests/lib/rules/jsx-boolean-value.js @@ -28,20 +28,42 @@ var ruleTester = new RuleTester({parserOptions}); ruleTester.run('jsx-boolean-value', rule, { valid: [ {code: ';', options: ['never']}, + {code: ';', options: ['always', {never: ['foo']}]}, {code: ';'}, - {code: ';', options: ['always']} + {code: ';', options: ['always']}, + {code: ';', options: ['never', {always: ['foo']}]} ], invalid: [{ - code: ';', output: ';', options: ['never'], + code: ';', + output: ';', + options: ['never'], errors: [{message: 'Value must be omitted for boolean attributes'}] }, { - code: ';', output: ';', + code: ';', + output: ';', + options: ['always', {never: ['foo', 'bar']}], + errors: [ + {message: 'Value must be omitted for boolean attributes for the following props: `foo`, `bar`'}, + {message: 'Value must be omitted for boolean attributes for the following props: `foo`, `bar`'} + ] + }, { + code: ';', + output: ';', errors: [{message: 'Value must be omitted for boolean attributes'}] }, { - code: ';', output: ';', + code: ';', + output: ';', errors: [{message: 'Value must be omitted for boolean attributes'}] }, { code: ';', output: ';', options: ['always'], errors: [{message: 'Value must be set for boolean attributes'}] + }, { + code: ';', + output: ';', + options: ['never', {always: ['foo', 'bar']}], + errors: [ + {message: 'Value must be set for boolean attributes for the following props: `foo`, `bar`'}, + {message: 'Value must be set for boolean attributes for the following props: `foo`, `bar`'} + ] }] });