Skip to content

Commit

Permalink
Merge pull request #9082 from acdlite/nobooleanorstringconstructors
Browse files Browse the repository at this point in the history
Enforce no boolean or string constructors
  • Loading branch information
acdlite authored Mar 1, 2017
2 parents 18b86bc + b963396 commit 0585ee8
Show file tree
Hide file tree
Showing 25 changed files with 166 additions and 44 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ module.exports = {
// CUSTOM RULES
// the second argument of warning/invariant should be a literal string
'react-internal/warning-and-invariant-args': ERROR,
'react-internal/no-primitive-constructors': ERROR,
},

globals: {
Expand Down
50 changes: 50 additions & 0 deletions eslint-rules/__tests__/no-primitive-constructors-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/

'use strict';

var rule = require('../no-primitive-constructors');
var RuleTester = require('eslint').RuleTester;
var ruleTester = new RuleTester();

ruleTester.run('eslint-rules/no-primitive-constructors', rule, {
valid: [
'!!obj',
"'' + obj",
'+string',
],
invalid: [
{
code: 'Boolean(obj)',
errors: [
{
message: 'Do not use the Boolean constructor. To cast a value to a boolean, use double negation: !!value',
},
],
},
{
code: 'String(obj)',
errors: [
{
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',
},
],
},
{
code: 'Number(string)',
errors: [
{
message: 'Do not use the Number constructor. To cast a value to a number, use the plus operator: +value',
},
],
},
],
});
1 change: 1 addition & 0 deletions eslint-rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
module.exports = {
rules: {
'warning-and-invariant-args': require('./warning-and-invariant-args'),
'no-primitive-constructors': require('./no-primitive-constructors'),
},
};
52 changes: 52 additions & 0 deletions eslint-rules/no-primitive-constructors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/

'use strict';

module.exports = function(context) {
function report(node, name, msg) {
context.report(node, `Do not use the ${name} constructor. ${msg}`);
}

function check(node) {
const name = node.callee.name;
switch (name) {
case 'Boolean':
report(
node,
name,
'To cast a value to a boolean, use double negation: !!value'
);
break;
case 'String':
report(
node,
name,
'To cast a value to a string, concat it with the empty string ' +
'(unless it\'s a symbol, which has different semantics): ' +
'\'\' + value'
);
break;
case 'Number':
report(
node,
name,
'To cast a value to a number, use the plus operator: +value'
);
break;
}
}

return {
CallExpression: check,
NewExpression: check,
};
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"eslint-plugin-babel": "^3.3.0",
"eslint-plugin-flowtype": "^2.25.0",
"eslint-plugin-react": "^6.7.1",
"eslint-plugin-react-internal": "file:eslint-rules",
"eslint-plugin-react-internal": "file:./eslint-rules",
"fbjs": "^0.8.9",
"fbjs-scripts": "^0.6.0",
"flow-bin": "^0.37.0",
Expand Down
8 changes: 8 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
eslint-rules/__tests__/no-primitive-constructors-test.js
* !!obj
* '' + obj
* +string
* Boolean(obj)
* String(obj)
* Number(string)

eslint-rules/__tests__/warning-and-invariant-args-test.js
* warning(true, 'hello, world');
* warning(true, 'expected %s, got %s', 42, 24);
Expand Down
10 changes: 6 additions & 4 deletions src/renderers/dom/fiber/ReactDOMFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,9 @@ var DOMRenderer = ReactFiberReconciler({
typeof props.children === 'string' ||
typeof props.children === 'number'
) {
const string = '' + props.children;
const ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type, null);
validateDOMNesting(null, String(props.children), null, ownAncestorInfo);
validateDOMNesting(null, string, null, ownAncestorInfo);
}
parentNamespace = hostContextDev.namespace;
} else {
Expand Down Expand Up @@ -237,8 +238,9 @@ var DOMRenderer = ReactFiberReconciler({
typeof newProps.children === 'string' ||
typeof newProps.children === 'number'
)) {
const string = '' + newProps.children;
const ownAncestorInfo = updatedAncestorInfo(hostContextDev.ancestorInfo, type, null);
validateDOMNesting(null, String(newProps.children), null, ownAncestorInfo);
validateDOMNesting(null, string, null, ownAncestorInfo);
}
}
return diffProperties(domElement, type, oldProps, newProps, rootContainerInstance);
Expand Down Expand Up @@ -411,9 +413,9 @@ var ReactDOM = {
}

if (__DEV__) {
const isRootRenderedBySomeReact = Boolean(container._reactRootContainer);
const isRootRenderedBySomeReact = !!container._reactRootContainer;
const rootEl = getReactRootElementInContainer(container);
const hasNonRootReactChild = Boolean(rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl));
const hasNonRootReactChild = !!(rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl));

warning(
!hasNonRootReactChild ||
Expand Down
20 changes: 10 additions & 10 deletions src/renderers/dom/fiber/wrappers/ReactDOMFiberSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ var ReactDOMSelect = {
var value = props.value;
node._wrapperState = {
initialValue: value != null ? value : props.defaultValue,
wasMultiple: Boolean(props.multiple),
wasMultiple: !!props.multiple,
};

if (
Expand All @@ -153,11 +153,11 @@ var ReactDOMSelect = {
didWarnValueDefaultValue = true;
}

node.multiple = Boolean(props.multiple);
node.multiple = !!props.multiple;
if (value != null) {
updateOptions(node, Boolean(props.multiple), value);
updateOptions(node, !!props.multiple, value);
} else if (props.defaultValue != null) {
updateOptions(node, Boolean(props.multiple), props.defaultValue);
updateOptions(node, !!props.multiple, props.defaultValue);
}
},

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

var wasMultiple = node._wrapperState.wasMultiple;
node._wrapperState.wasMultiple = Boolean(props.multiple);
node._wrapperState.wasMultiple = !!props.multiple;

var value = props.value;
if (value != null) {
updateOptions(node, Boolean(props.multiple), value);
} else if (wasMultiple !== Boolean(props.multiple)) {
updateOptions(node, !!props.multiple, value);
} else if (wasMultiple !== !!props.multiple) {
// For simplicity, reapply `defaultValue` if `multiple` is toggled.
if (props.defaultValue != null) {
updateOptions(node, Boolean(props.multiple), props.defaultValue);
updateOptions(node, !!props.multiple, props.defaultValue);
} else {
// Revert the select back to its default unselected state.
updateOptions(node, Boolean(props.multiple), props.multiple ? [] : '');
updateOptions(node, !!props.multiple, props.multiple ? [] : '');
}
}
},
Expand All @@ -189,7 +189,7 @@ var ReactDOMSelect = {
var value = props.value;

if (value != null) {
updateOptions(node, Boolean(props.multiple), value);
updateOptions(node, !!props.multiple, value);
}
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/dom/shared/ReactBrowserEventEmitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ var topEventMapping = {
/**
* To ensure no conflicts with other potential React instances on the page
*/
var topListenersIDKey = '_reactListenersID' + String(Math.random()).slice(2);
var topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2);

function getListeningForDocument(mountAt) {
// In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/dom/shared/ReactDOMComponentTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var internalEventHandlersKey = '__reactEventHandlers$' + randomKey;
*/
function shouldPrecacheNode(node, nodeID) {
return (node.nodeType === 1 &&
node.getAttribute(ATTR_NAME) === String(nodeID)) ||
node.getAttribute(ATTR_NAME) === ('' + nodeID)) ||
(node.nodeType === 8 &&
node.nodeValue === ' react-text: ' + nodeID + ' ') ||
(node.nodeType === 8 &&
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/dom/shared/validateDOMNesting.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ if (__DEV__) {
childTag,
ancestorInstance,
ancestorTag,
Boolean(invalidParent)
!!invalidParent
) + '.';
} else {
addendum = getCurrentFiberStackAddendum();
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/dom/stack/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ if (__DEV__) {
return;
}

validateDOMNesting(null, String(content), this, this._ancestorInfo);
validateDOMNesting(null, '' + content, this, this._ancestorInfo);
this._contentDebugID = contentDebugID;
if (hasExistingContent) {
ReactInstrumentation.debugTool.onBeforeUpdateComponent(contentDebugID, content);
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/dom/stack/client/ReactMount.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ var ReactMount = {
callback === null || typeof callback === 'function',
'render(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
String(callback)
'' + callback
);
}
if (!React.isValidElement(nextElement)) {
Expand Down
14 changes: 7 additions & 7 deletions src/renderers/dom/stack/client/wrappers/ReactDOMSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ var ReactDOMSelect = {
inst._wrapperState = {
initialValue: value != null ? value : props.defaultValue,
listeners: null,
wasMultiple: Boolean(props.multiple),
wasMultiple: !!props.multiple,
};

if (
Expand Down Expand Up @@ -169,18 +169,18 @@ var ReactDOMSelect = {
inst._wrapperState.initialValue = undefined;

var wasMultiple = inst._wrapperState.wasMultiple;
inst._wrapperState.wasMultiple = Boolean(props.multiple);
inst._wrapperState.wasMultiple = !!props.multiple;

var value = props.value;
if (value != null) {
updateOptions(inst, Boolean(props.multiple), value);
} else if (wasMultiple !== Boolean(props.multiple)) {
updateOptions(inst, !!props.multiple, value);
} else if (wasMultiple !== !!props.multiple) {
// For simplicity, reapply `defaultValue` if `multiple` is toggled.
if (props.defaultValue != null) {
updateOptions(inst, Boolean(props.multiple), props.defaultValue);
updateOptions(inst, !!props.multiple, props.defaultValue);
} else {
// Revert the select back to its default unselected state.
updateOptions(inst, Boolean(props.multiple), props.multiple ? [] : '');
updateOptions(inst, !!props.multiple, props.multiple ? [] : '');
}
}
},
Expand All @@ -191,7 +191,7 @@ var ReactDOMSelect = {
var value = props.value;

if (value != null) {
updateOptions(inst, Boolean(props.multiple), value);
updateOptions(inst, !!props.multiple, value);
}
}
},
Expand Down
5 changes: 3 additions & 2 deletions src/renderers/shared/fiber/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function coerceRef(current: Fiber | null, element: ReactElement) {
'bug in React. Please file an issue.',
mixedRef
);
const stringRef = String(mixedRef);
const stringRef = '' + mixedRef;
// Check if previous string ref matches new string ref
if (current !== null && current.ref !== null && current.ref._stringRef === stringRef) {
return current.ref;
Expand All @@ -117,7 +117,8 @@ function coerceRef(current: Fiber | null, element: ReactElement) {

function throwOnInvalidObjectType(returnFiber : Fiber, newChild : Object) {
if (returnFiber.type !== 'textarea') {
const childrenString = String(newChild);
// $FlowFixMe - Intentional cast to string
const childrenString = '' + newChild;
let addendum = '';
if (__DEV__) {
addendum =
Expand Down
3 changes: 2 additions & 1 deletion src/renderers/shared/fiber/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ if (__DEV__) {
'%s(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
callerName,
String(callback)
// $FlowFixMe - Intentional cast to string
'' + callback
);
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/renderers/shared/fiber/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
callback === null || typeof callback === 'function',
'render(...): Expected the last optional `callback` argument to be a ' +
'function. Instead received: %s.',
String(callback)
// $FlowFixMe - Intentional cast to string
'' + callback
);
}
addTopLevelUpdate(current, nextState, callback, priorityLevel);
Expand Down
6 changes: 3 additions & 3 deletions src/renderers/shared/fiber/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
'by a bug in React. Please file an issue.'
);
isPerformingWork = true;
const isPerformingDeferredWork = Boolean(deadline);
const isPerformingDeferredWork = !!deadline;

// This outer loop exists so that we can restart the work loop after
// catching an error. It also lets us flush Task work at the end of a
Expand Down Expand Up @@ -1034,7 +1034,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
function hasCapturedError(fiber : Fiber) : boolean {
// TODO: capturedErrors should store the boundary instance, to avoid needing
// to check the alternate.
return Boolean(
return (
capturedErrors !== null &&
(capturedErrors.has(fiber) || (fiber.alternate !== null && capturedErrors.has(fiber.alternate)))
);
Expand All @@ -1043,7 +1043,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
function isFailedBoundary(fiber : Fiber) : boolean {
// TODO: failedBoundaries should store the boundary instance, to avoid
// needing to check the alternate.
return Boolean(
return (
failedBoundaries !== null &&
(failedBoundaries.has(fiber) || (fiber.alternate !== null && failedBoundaries.has(fiber.alternate)))
);
Expand Down
Loading

0 comments on commit 0585ee8

Please sign in to comment.