Repository: jsx-eslint/eslint-plugin-react
Pull request #3675 (2024-01-09 19:34:47)

[New] `jsx-boolean-value`: add `assumeUndefinedIsFalse` option
Fixes https://github.com/jsx-eslint/eslint-plugin-react/issues/3234


A new option has been added referring to the comments on this issue.

I think there are some parts that need explanation, so I'll list them below.

1. As you can see in the test case, if you remove both properties and values, a space remains, but that space can be modified with other rules such as jsx-props-no-multi-spaces, so it was left behind.

2. While adding new options, I noticed that there was some issue with the options in the error message. After setting never or always in the second option, if the modification is made not only to the first option but also to the second option, an error message containing the properties of the second option is displayed as shown below. If it is confirmed to be an issue, a new mr will be created after working on the current issue. An example is below:

```javascript
{
      code: '<App foo={true} bar={true} baz />;',
      output: '<App foo bar baz={true} />;',
      options: ['always', { never: ['foo', 'bar'] }],
      errors: [
        {
          messageId: 'omitBoolean',
          data: { exceptionsMessage: ' for the following props: `foo`, `bar`' },
        },
        {
          messageId: 'omitBoolean',
          data: { exceptionsMessage: ' for the following props: `foo`, `bar`' },
        },
        {
          messageId: 'setBoolean',
          data: { exceptionsMessage: ' for the following props: `foo`, `bar`' },
          // setBoolean attribute is not foo or bar but baz
        }
      ],
    },
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f468f92..8bd7d075 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 * [`no-unknown-property`]: support `onResize` on audio/video tags ([#3662][] @caesar1030)
 * [`jsx-wrap-multilines`]: add `never` option to prohibit wrapping parens on multiline JSX ([#3668][] @reedws)
 * [`jsx-filename-extension`]: add `ignoreFilesWithoutCode` option to allow empty files ([#3674][] @burtek)
+* [`jsx-boolean-value`]: add `assumeUndefinedIsFalse` option ([#3675][] @developer-bandi)
 
 ### Fixed
 * [`jsx-no-leaked-render`]: preserve RHS parens for multiline jsx elements while fixing ([#3623][] @akulsr0)
@@ -29,6 +30,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 * [Docs] [`jsx-key`]: fix correct example ([#3656][] @developer-bandi)
 * [Tests] `jsx-wrap-multilines`: passing tests ([#3545][] @burtek)
 
+[#3675]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3675
 [#3674]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3674
 [#3668]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3668
 [#3666]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3666
diff --git a/docs/rules/jsx-boolean-value.md b/docs/rules/jsx-boolean-value.md
index d96285b9..1a0c2bde 100644
--- a/docs/rules/jsx-boolean-value.md
+++ b/docs/rules/jsx-boolean-value.md
@@ -14,7 +14,11 @@ This rule will enforce one or the other to keep consistency in your code.
 
 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 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 second argument is optional. If provided, it must be an object. These properties are supported:
+
+First, the `"never"` and `"always"` properties are one set. The two properties cannot be set together. `"never"` must be used when the first argument is `"always"` and `"always"`  must be used when the first argument is `"never"`. This property’s value must be an array of strings representing prop names.
+
+When the first argument is `"never"`, a boolean `"assumeUndefinedIsFalse"` may be provided, which defaults to `false`. When `true`, an absent boolean prop will be treated as if it were explicitly set to `false`.
 
 Examples of **incorrect** code for this rule, when configured with `"never"`, or with `"always", { "never": ["personal"] }`:
 
@@ -40,6 +44,18 @@ Examples of **correct** code for this rule, when configured with `"always"`, or
 var Hello = <Hello personal={true} />;
 ```
 
+Examples of **incorrect** code for this rule, when configured with `"never", { "assumeUndefinedIsFalse": true }`, or with  `"always", { "never": ["personal"], "assumeUndefinedIsFalse": true }`:
+
+```jsx
+var Hello = <Hello personal={false} />;
+```
+
+Examples of **correct** code for this rule, when configured with `"never", { "assumeUndefinedIsFalse": true }`, or with  `"always", { "never": ["personal"], "assumeUndefinedIsFalse": true }`:
+
+```jsx
+var Hello = <Hello />;
+```
+
 ## When Not To Use It
 
 If you do not want to enforce any style for boolean attributes, then you can disable this rule.
diff --git a/lib/rules/jsx-boolean-value.js b/lib/rules/jsx-boolean-value.js
index 43395e24..ae6dda0d 100644
--- a/lib/rules/jsx-boolean-value.js
+++ b/lib/rules/jsx-boolean-value.js
@@ -39,7 +39,7 @@ function getErrorData(exceptions) {
  * @param {Set<string>} exceptions
  * @param {string} propName
  * @returns {boolean} propName
-*/
+ */
 function isAlways(configuration, exceptions, propName) {
   const isException = exceptions.has(propName);
   if (configuration === ALWAYS) {
@@ -66,6 +66,8 @@ const messages = {
   omitBoolean_noMessage: 'Value must be omitted for boolean attributes',
   setBoolean: 'Value must be set for boolean attributes{{exceptionsMessage}}',
   setBoolean_noMessage: 'Value must be set for boolean attributes',
+  omitPropAndBoolean: 'Value and Prop must be omitted for false attributes{{exceptionsMessage}}',
+  omitPropAndBoolean_noMessage: 'Value and Prop must be omitted for false attributes',
 };
 
 module.exports = {
@@ -94,6 +96,9 @@ module.exports = {
           additionalProperties: false,
           properties: {
             [NEVER]: exceptionsSchema,
+            assumeUndefinedIsFalse: {
+              type: 'boolean',
+            },
           },
         }],
         additionalItems: false,
@@ -106,6 +111,9 @@ module.exports = {
           additionalProperties: false,
           properties: {
             [ALWAYS]: exceptionsSchema,
+            assumeUndefinedIsFalse: {
+              type: 'boolean',
+            },
           },
         }],
         additionalItems: false,
@@ -139,6 +147,7 @@ module.exports = {
         }
         if (
           isNever(configuration, exceptions, propName)
+          && !configObject.assumeUndefinedIsFalse
           && value
           && value.type === 'JSXExpressionContainer'
           && value.expression.value === true
@@ -153,6 +162,26 @@ module.exports = {
             },
           });
         }
+        if (
+          isNever(configuration, exceptions, propName)
+          && configObject.assumeUndefinedIsFalse
+          && value
+          && value.type === 'JSXExpressionContainer'
+          && value.expression.value === false
+        ) {
+          const data = getErrorData(exceptions);
+          const messageId = data.exceptionsMessage
+            ? 'omitPropAndBoolean'
+            : 'omitPropAndBoolean_noMessage';
+
+          report(context, messages[messageId], messageId, {
+            node,
+            data,
+            fix(fixer) {
+              return fixer.removeRange([node.name.range[0], value.range[1]]);
+            },
+          });
+        }
       },
     };
   },


Test patch:
diff --git a/tests/lib/rules/jsx-boolean-value.js b/tests/lib/rules/jsx-boolean-value.js
index de7e9f71..678b4927 100644
--- a/tests/lib/rules/jsx-boolean-value.js
+++ b/tests/lib/rules/jsx-boolean-value.js
@@ -48,6 +48,14 @@ ruleTester.run('jsx-boolean-value', rule, {
       code: '<App foo={true} bar />;',
       options: ['never', { always: ['foo'] }],
     },
+    {
+      code: '<App />;',
+      options: ['never', { assumeUndefinedIsFalse: true }],
+    },
+    {
+      code: '<App foo={false} />;',
+      options: ['never', { assumeUndefinedIsFalse: true, always: ['foo'] }],
+    },
   ]),
   invalid: parsers.all([
     {
@@ -110,5 +118,32 @@ ruleTester.run('jsx-boolean-value', rule, {
         },
       ],
     },
+    {
+      code: '<App foo={false} bak={false} />;',
+      output: '<App   />;',
+      options: ['never', { assumeUndefinedIsFalse: true }],
+      errors: [
+        { messageId: 'omitPropAndBoolean_noMessage' },
+        { messageId: 'omitPropAndBoolean_noMessage' },
+      ],
+    },
+    {
+      code: '<App foo={true} bar={false} baz={false} bak={false} />;',
+      output: '<App foo={true} bar={false}   />;',
+      options: [
+        'always',
+        { assumeUndefinedIsFalse: true, never: ['baz', 'bak'] },
+      ],
+      errors: [
+        {
+          messageId: 'omitPropAndBoolean',
+          data: { exceptionsMessage: ' for the following props: `baz`, `bak`' },
+        },
+        {
+          messageId: 'omitPropAndBoolean',
+          data: { exceptionsMessage: ' for the following props: `baz`, `bak`' },
+        },
+      ],
+    },
   ]),
 });