Skip to content

Commit 0585ee8

Browse files
authored
Merge pull request #9082 from acdlite/nobooleanorstringconstructors
Enforce no boolean or string constructors
2 parents 18b86bc + b963396 commit 0585ee8

25 files changed

+166
-44
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ module.exports = {
5555
// CUSTOM RULES
5656
// the second argument of warning/invariant should be a literal string
5757
'react-internal/warning-and-invariant-args': ERROR,
58+
'react-internal/no-primitive-constructors': ERROR,
5859
},
5960

6061
globals: {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Copyright 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @emails react-core
10+
*/
11+
12+
'use strict';
13+
14+
var rule = require('../no-primitive-constructors');
15+
var RuleTester = require('eslint').RuleTester;
16+
var ruleTester = new RuleTester();
17+
18+
ruleTester.run('eslint-rules/no-primitive-constructors', rule, {
19+
valid: [
20+
'!!obj',
21+
"'' + obj",
22+
'+string',
23+
],
24+
invalid: [
25+
{
26+
code: 'Boolean(obj)',
27+
errors: [
28+
{
29+
message: 'Do not use the Boolean constructor. To cast a value to a boolean, use double negation: !!value',
30+
},
31+
],
32+
},
33+
{
34+
code: 'String(obj)',
35+
errors: [
36+
{
37+
message: 'Do not use the String constructor. To cast a value to a string, concat it with the empty string (unless it\'s a symbol, which has different semantics): \'\' + value',
38+
},
39+
],
40+
},
41+
{
42+
code: 'Number(string)',
43+
errors: [
44+
{
45+
message: 'Do not use the Number constructor. To cast a value to a number, use the plus operator: +value',
46+
},
47+
],
48+
},
49+
],
50+
});

eslint-rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
module.exports = {
44
rules: {
55
'warning-and-invariant-args': require('./warning-and-invariant-args'),
6+
'no-primitive-constructors': require('./no-primitive-constructors'),
67
},
78
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @emails react-core
10+
*/
11+
12+
'use strict';
13+
14+
module.exports = function(context) {
15+
function report(node, name, msg) {
16+
context.report(node, `Do not use the ${name} constructor. ${msg}`);
17+
}
18+
19+
function check(node) {
20+
const name = node.callee.name;
21+
switch (name) {
22+
case 'Boolean':
23+
report(
24+
node,
25+
name,
26+
'To cast a value to a boolean, use double negation: !!value'
27+
);
28+
break;
29+
case 'String':
30+
report(
31+
node,
32+
name,
33+
'To cast a value to a string, concat it with the empty string ' +
34+
'(unless it\'s a symbol, which has different semantics): ' +
35+
'\'\' + value'
36+
);
37+
break;
38+
case 'Number':
39+
report(
40+
node,
41+
name,
42+
'To cast a value to a number, use the plus operator: +value'
43+
);
44+
break;
45+
}
46+
}
47+
48+
return {
49+
CallExpression: check,
50+
NewExpression: check,
51+
};
52+
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"eslint-plugin-babel": "^3.3.0",
4747
"eslint-plugin-flowtype": "^2.25.0",
4848
"eslint-plugin-react": "^6.7.1",
49-
"eslint-plugin-react-internal": "file:eslint-rules",
49+
"eslint-plugin-react-internal": "file:./eslint-rules",
5050
"fbjs": "^0.8.9",
5151
"fbjs-scripts": "^0.6.0",
5252
"flow-bin": "^0.37.0",

scripts/fiber/tests-passing.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
eslint-rules/__tests__/no-primitive-constructors-test.js
2+
* !!obj
3+
* '' + obj
4+
* +string
5+
* Boolean(obj)
6+
* String(obj)
7+
* Number(string)
8+
19
eslint-rules/__tests__/warning-and-invariant-args-test.js
210
* warning(true, 'hello, world');
311
* warning(true, 'expected %s, got %s', 42, 24);

src/renderers/dom/fiber/ReactDOMFiber.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,9 @@ var DOMRenderer = ReactFiberReconciler({
196196
typeof props.children === 'string' ||
197197
typeof props.children === 'number'
198198
) {
199+
const string = '' + props.children;
199200
const ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type, null);
200-
validateDOMNesting(null, String(props.children), null, ownAncestorInfo);
201+
validateDOMNesting(null, string, null, ownAncestorInfo);
201202
}
202203
parentNamespace = hostContextDev.namespace;
203204
} else {
@@ -237,8 +238,9 @@ var DOMRenderer = ReactFiberReconciler({
237238
typeof newProps.children === 'string' ||
238239
typeof newProps.children === 'number'
239240
)) {
241+
const string = '' + newProps.children;
240242
const ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type, null);
241-
validateDOMNesting(null, String(newProps.children), null, ownAncestorInfo);
243+
validateDOMNesting(null, string, null, ownAncestorInfo);
242244
}
243245
}
244246
return diffProperties(domElement, type, oldProps, newProps, rootContainerInstance);
@@ -411,9 +413,9 @@ var ReactDOM = {
411413
}
412414

413415
if (__DEV__) {
414-
const isRootRenderedBySomeReact = Boolean(container._reactRootContainer);
416+
const isRootRenderedBySomeReact = !!container._reactRootContainer;
415417
const rootEl = getReactRootElementInContainer(container);
416-
const hasNonRootReactChild = Boolean(rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl));
418+
const hasNonRootReactChild = !!(rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl));
417419

418420
warning(
419421
!hasNonRootReactChild ||

src/renderers/dom/fiber/wrappers/ReactDOMFiberSelect.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ var ReactDOMSelect = {
134134
var value = props.value;
135135
node._wrapperState = {
136136
initialValue: value != null ? value : props.defaultValue,
137-
wasMultiple: Boolean(props.multiple),
137+
wasMultiple: !!props.multiple,
138138
};
139139

140140
if (
@@ -153,11 +153,11 @@ var ReactDOMSelect = {
153153
didWarnValueDefaultValue = true;
154154
}
155155

156-
node.multiple = Boolean(props.multiple);
156+
node.multiple = !!props.multiple;
157157
if (value != null) {
158-
updateOptions(node, Boolean(props.multiple), value);
158+
updateOptions(node, !!props.multiple, value);
159159
} else if (props.defaultValue != null) {
160-
updateOptions(node, Boolean(props.multiple), props.defaultValue);
160+
updateOptions(node, !!props.multiple, props.defaultValue);
161161
}
162162
},
163163

@@ -168,18 +168,18 @@ var ReactDOMSelect = {
168168
node._wrapperState.initialValue = undefined;
169169

170170
var wasMultiple = node._wrapperState.wasMultiple;
171-
node._wrapperState.wasMultiple = Boolean(props.multiple);
171+
node._wrapperState.wasMultiple = !!props.multiple;
172172

173173
var value = props.value;
174174
if (value != null) {
175-
updateOptions(node, Boolean(props.multiple), value);
176-
} else if (wasMultiple !== Boolean(props.multiple)) {
175+
updateOptions(node, !!props.multiple, value);
176+
} else if (wasMultiple !== !!props.multiple) {
177177
// For simplicity, reapply `defaultValue` if `multiple` is toggled.
178178
if (props.defaultValue != null) {
179-
updateOptions(node, Boolean(props.multiple), props.defaultValue);
179+
updateOptions(node, !!props.multiple, props.defaultValue);
180180
} else {
181181
// Revert the select back to its default unselected state.
182-
updateOptions(node, Boolean(props.multiple), props.multiple ? [] : '');
182+
updateOptions(node, !!props.multiple, props.multiple ? [] : '');
183183
}
184184
}
185185
},
@@ -189,7 +189,7 @@ var ReactDOMSelect = {
189189
var value = props.value;
190190

191191
if (value != null) {
192-
updateOptions(node, Boolean(props.multiple), value);
192+
updateOptions(node, !!props.multiple, value);
193193
}
194194
},
195195
};

src/renderers/dom/shared/ReactBrowserEventEmitter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ var topEventMapping = {
154154
/**
155155
* To ensure no conflicts with other potential React instances on the page
156156
*/
157-
var topListenersIDKey = '_reactListenersID' + String(Math.random()).slice(2);
157+
var topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2);
158158

159159
function getListeningForDocument(mountAt) {
160160
// In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`

src/renderers/dom/shared/ReactDOMComponentTree.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ var internalEventHandlersKey = '__reactEventHandlers$' + randomKey;
3131
*/
3232
function shouldPrecacheNode(node, nodeID) {
3333
return (node.nodeType === 1 &&
34-
node.getAttribute(ATTR_NAME) === String(nodeID)) ||
34+
node.getAttribute(ATTR_NAME) === ('' + nodeID)) ||
3535
(node.nodeType === 8 &&
3636
node.nodeValue === ' react-text: ' + nodeID + ' ') ||
3737
(node.nodeType === 8 &&

0 commit comments

Comments
 (0)