Skip to content

Commit fd2fba1

Browse files
authored
Merge pull request #1250 from ljharb/boolean_inverse
[New] `jsx-boolean-value`: add inverse option for always/never
2 parents f70b417 + a794101 commit fd2fba1

File tree

3 files changed

+128
-40
lines changed

3 files changed

+128
-40
lines changed

docs/rules/jsx-boolean-value.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,29 @@
66

77
## Rule Details
88

9-
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"`.
9+
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"`.
1010

11-
The following patterns are considered warnings when configured `"never"`:
11+
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.
12+
13+
The following patterns are considered warnings when configured `"never"`, or with `"always", { "never": ["personal"] }`:
1214

1315
```jsx
1416
var Hello = <Hello personal={true} />;
1517
```
1618

17-
The following patterns are not considered warnings when configured `"never"`:
19+
The following patterns are not considered warnings when configured `"never"`, or with `"always", { "never": ["personal"] }`:
1820

1921
```jsx
2022
var Hello = <Hello personal />;
2123
```
2224

23-
The following patterns are considered warnings when configured `"always"`:
25+
The following patterns are considered warnings when configured `"always"`, or with `"never", { "always": ["personal"] }`:
2426

2527
```jsx
2628
var Hello = <Hello personal />;
2729
```
2830

29-
The following patterns are not considered warnings when configured `"always"`:
31+
The following patterns are not considered warnings when configured `"always"`, or with `"never", { "always": ["personal"] }`:
3032

3133
```jsx
3234
var Hello = <Hello personal={true} />;

lib/rules/jsx-boolean-value.js

Lines changed: 95 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,41 @@
88
// Rule Definition
99
// ------------------------------------------------------------------------------
1010

11+
const exceptionsSchema = {
12+
type: 'array',
13+
items: {type: 'string', minLength: 1},
14+
uniqueItems: true
15+
};
16+
17+
const ALWAYS = 'always';
18+
const NEVER = 'never';
19+
20+
const errorData = new WeakMap();
21+
function getErrorData(exceptions) {
22+
if (!errorData.has(exceptions)) {
23+
const exceptionProps = Array.from(exceptions, (name) => `\`${name}\``).join(', ');
24+
const exceptionsMessage = exceptions.size > 0 ? ` for the following props: ${exceptionProps}` : '';
25+
errorData.set(exceptions, {exceptionsMessage: exceptionsMessage});
26+
}
27+
return errorData.get(exceptions);
28+
}
29+
30+
function isAlways(configuration, exceptions, propName) {
31+
const isException = exceptions.has(propName);
32+
if (configuration === ALWAYS) {
33+
return !isException;
34+
}
35+
return isException;
36+
}
37+
38+
function isNever(configuration, exceptions, propName) {
39+
const isException = exceptions.has(propName);
40+
if (configuration === NEVER) {
41+
return !isException;
42+
}
43+
return isException;
44+
}
45+
1146
module.exports = {
1247
meta: {
1348
docs: {
@@ -17,44 +52,73 @@ module.exports = {
1752
},
1853
fixable: 'code',
1954

20-
schema: [{
21-
enum: ['always', 'never']
22-
}]
55+
schema: {
56+
anyOf: [{
57+
type: 'array',
58+
items: [{enum: [ALWAYS, NEVER]}],
59+
additionalItems: false
60+
}, {
61+
type: 'array',
62+
items: [{
63+
enum: [ALWAYS]
64+
}, {
65+
type: 'object',
66+
additionalProperties: false,
67+
properties: {
68+
[NEVER]: exceptionsSchema
69+
}
70+
}],
71+
additionalItems: false
72+
}, {
73+
type: 'array',
74+
items: [{
75+
enum: [NEVER]
76+
}, {
77+
type: 'object',
78+
additionalProperties: false,
79+
properties: {
80+
[ALWAYS]: exceptionsSchema
81+
}
82+
}],
83+
additionalItems: false
84+
}]
85+
}
2386
},
2487

25-
create: function(context) {
26-
var configuration = context.options[0] || 'never';
88+
create(context) {
89+
const configuration = context.options[0] || NEVER;
90+
const configObject = context.options[1] || {};
91+
const exceptions = new Set((configuration === ALWAYS ? configObject[NEVER] : configObject[ALWAYS]) || []);
2792

28-
var NEVER_MESSAGE = 'Value must be omitted for boolean attributes';
29-
var ALWAYS_MESSAGE = 'Value must be set for boolean attributes';
93+
const NEVER_MESSAGE = 'Value must be omitted for boolean attributes{{exceptionsMessage}}';
94+
const ALWAYS_MESSAGE = 'Value must be set for boolean attributes{{exceptionsMessage}}';
3095

3196
return {
32-
JSXAttribute: function(node) {
33-
switch (configuration) {
34-
case 'always':
35-
if (node.value === null) {
36-
context.report({
37-
node: node,
38-
message: ALWAYS_MESSAGE,
39-
fix: function(fixer) {
40-
return fixer.insertTextAfter(node, '={true}');
41-
}
42-
});
97+
JSXAttribute(node) {
98+
const propName = node.name && node.name.name;
99+
const value = node.value;
100+
101+
if (isAlways(configuration, exceptions, propName) && value === null) {
102+
const data = getErrorData(exceptions);
103+
context.report({
104+
node: node,
105+
message: ALWAYS_MESSAGE,
106+
data: data,
107+
fix(fixer) {
108+
return fixer.insertTextAfter(node, '={true}');
43109
}
44-
break;
45-
case 'never':
46-
if (node.value && node.value.type === 'JSXExpressionContainer' && node.value.expression.value === true) {
47-
context.report({
48-
node: node,
49-
message: NEVER_MESSAGE,
50-
fix: function(fixer) {
51-
return fixer.removeRange([node.name.range[1], node.value.range[1]]);
52-
}
53-
});
110+
});
111+
}
112+
if (isNever(configuration, exceptions, propName) && value && value.type === 'JSXExpressionContainer' && value.expression.value === true) {
113+
const data = getErrorData(exceptions);
114+
context.report({
115+
node: node,
116+
message: NEVER_MESSAGE,
117+
data: data,
118+
fix(fixer) {
119+
return fixer.removeRange([node.name.range[1], value.range[1]]);
54120
}
55-
break;
56-
default:
57-
break;
121+
});
58122
}
59123
}
60124
};

tests/lib/rules/jsx-boolean-value.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,42 @@ var ruleTester = new RuleTester({parserOptions});
2828
ruleTester.run('jsx-boolean-value', rule, {
2929
valid: [
3030
{code: '<App foo />;', options: ['never']},
31+
{code: '<App foo bar={true} />;', options: ['always', {never: ['foo']}]},
3132
{code: '<App foo />;'},
32-
{code: '<App foo={true} />;', options: ['always']}
33+
{code: '<App foo={true} />;', options: ['always']},
34+
{code: '<App foo={true} bar />;', options: ['never', {always: ['foo']}]}
3335
],
3436
invalid: [{
35-
code: '<App foo={true} />;', output: '<App foo />;', options: ['never'],
37+
code: '<App foo={true} />;',
38+
output: '<App foo />;',
39+
options: ['never'],
3640
errors: [{message: 'Value must be omitted for boolean attributes'}]
3741
}, {
38-
code: '<App foo={true} />;', output: '<App foo />;',
42+
code: '<App foo={true} bar={true} baz={true} />;',
43+
output: '<App foo bar baz={true} />;',
44+
options: ['always', {never: ['foo', 'bar']}],
45+
errors: [
46+
{message: 'Value must be omitted for boolean attributes for the following props: `foo`, `bar`'},
47+
{message: 'Value must be omitted for boolean attributes for the following props: `foo`, `bar`'}
48+
]
49+
}, {
50+
code: '<App foo={true} />;',
51+
output: '<App foo />;',
3952
errors: [{message: 'Value must be omitted for boolean attributes'}]
4053
}, {
41-
code: '<App foo = {true} />;', output: '<App foo />;',
54+
code: '<App foo = {true} />;',
55+
output: '<App foo />;',
4256
errors: [{message: 'Value must be omitted for boolean attributes'}]
4357
}, {
4458
code: '<App foo />;', output: '<App foo={true} />;', options: ['always'],
4559
errors: [{message: 'Value must be set for boolean attributes'}]
60+
}, {
61+
code: '<App foo bar baz />;',
62+
output: '<App foo={true} bar={true} baz />;',
63+
options: ['never', {always: ['foo', 'bar']}],
64+
errors: [
65+
{message: 'Value must be set for boolean attributes for the following props: `foo`, `bar`'},
66+
{message: 'Value must be set for boolean attributes for the following props: `foo`, `bar`'}
67+
]
4668
}]
4769
});

0 commit comments

Comments
 (0)