Repository: yannickcr/eslint-plugin-react (js)

Issue:
Extend "jsx-boolean-value" to support optional "ignore" list
It would be great if we could configure `jsx-boolean-value` to ignore a list of props.

### Example
```js
{
  'react/jsx-boolean-value': ['error', 'never', { ignore: ['value']}]
}
```

This allows the following expression to be valid

```jsx
<Toggle value={true} />
```

And if you wonder why I'm not using a prop like `toggled` instead of `value`, it's because we want to have a set of components that support the same prop.

I'm more than happy to support the idea with a PR.

Pull request description:
[New] `jsx-boolean-value`: add inverse option for always/never
Fixes #1249

Patch:
diff --git a/docs/rules/jsx-boolean-value.md b/docs/rules/jsx-boolean-value.md
index 5dcfabe9..c888e7af 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 = <Hello personal={true} />;
 ```
 
-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 = <Hello personal />;
 ```
 
-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 = <Hello personal />;
 ```
 
-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 = <Hello personal={true} />;
diff --git a/lib/rules/jsx-boolean-value.js b/lib/rules/jsx-boolean-value.js
index 3a824e5a..21bfa6cd 100644
--- a/lib/rules/jsx-boolean-value.js
+++ b/lib/rules/jsx-boolean-value.js
@@ -8,6 +8,41 @@
 // Rule Definition
 // ------------------------------------------------------------------------------
 
+const exceptionsSchema = {
+  type: 'array',
+  items: {type: 'string', minLength: 1},
+  uniqueItems: true
+};
+
+const ALWAYS = 'always';
+const NEVER = 'never';
+
+const errorData = new WeakMap();
+function getErrorData(exceptions) {
+  if (!errorData.has(exceptions)) {
+    const exceptionProps = Array.from(exceptions, (name) => `\`${name}\``).join(', ');
+    const exceptionsMessage = exceptions.size > 0 ? ` for the following props: ${exceptionProps}` : '';
+    errorData.set(exceptions, {exceptionsMessage: exceptionsMessage});
+  }
+  return errorData.get(exceptions);
+}
+
+function isAlways(configuration, exceptions, propName) {
+  const isException = exceptions.has(propName);
+  if (configuration === ALWAYS) {
+    return !isException;
+  }
+  return isException;
+}
+
+function isNever(configuration, exceptions, propName) {
+  const isException = exceptions.has(propName);
+  if (configuration === NEVER) {
+    return !isException;
+  }
+  return isException;
+}
+
 module.exports = {
   meta: {
     docs: {
@@ -17,44 +52,73 @@ 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';
+  create(context) {
+    const configuration = context.options[0] || NEVER;
+    const configObject = context.options[1] || {};
+    const exceptions = new Set((configuration === ALWAYS ? configObject[NEVER] : configObject[ALWAYS]) || []);
 
-    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;
+
+        if (isAlways(configuration, exceptions, propName) && value === null) {
+          const data = getErrorData(exceptions);
+          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(configuration, exceptions, propName) && value && value.type === 'JSXExpressionContainer' && value.expression.value === true) {
+          const data = getErrorData(exceptions);
+          context.report({
+            node: node,
+            message: NEVER_MESSAGE,
+            data: data,
+            fix(fixer) {
+              return fixer.removeRange([node.name.range[1], value.range[1]]);
             }
-            break;
-          default:
-            break;
+          });
         }
       }
     };


Test patch:
diff --git a/tests/lib/rules/jsx-boolean-value.js b/tests/lib/rules/jsx-boolean-value.js
index 67ab4dda..d737ef8d 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: '<App foo />;', options: ['never']},
+    {code: '<App foo bar={true} />;', options: ['always', {never: ['foo']}]},
     {code: '<App foo />;'},
-    {code: '<App foo={true} />;', options: ['always']}
+    {code: '<App foo={true} />;', options: ['always']},
+    {code: '<App foo={true} bar />;', options: ['never', {always: ['foo']}]}
   ],
   invalid: [{
-    code: '<App foo={true} />;', output: '<App foo />;', options: ['never'],
+    code: '<App foo={true} />;',
+    output: '<App foo />;',
+    options: ['never'],
     errors: [{message: 'Value must be omitted for boolean attributes'}]
   }, {
-    code: '<App foo={true} />;', output: '<App foo />;',
+    code: '<App foo={true} bar={true} baz={true} />;',
+    output: '<App foo bar baz={true} />;',
+    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: '<App foo={true} />;',
+    output: '<App foo />;',
     errors: [{message: 'Value must be omitted for boolean attributes'}]
   }, {
-    code: '<App foo = {true} />;', output: '<App foo />;',
+    code: '<App foo = {true} />;',
+    output: '<App foo />;',
     errors: [{message: 'Value must be omitted for boolean attributes'}]
   }, {
     code: '<App foo />;', output: '<App foo={true} />;', options: ['always'],
     errors: [{message: 'Value must be set for boolean attributes'}]
+  }, {
+    code: '<App foo bar baz />;',
+    output: '<App foo={true} bar={true} baz />;',
+    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`'}
+    ]
   }]
 });