diff --git a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js index cc09080f0cc96..972093d8f54ad 100644 --- a/src/isomorphic/classic/__tests__/ReactContextValidator-test.js +++ b/src/isomorphic/classic/__tests__/ReactContextValidator-test.js @@ -24,6 +24,10 @@ var ReactTestUtils; var reactComponentExpect; describe('ReactContextValidator', function() { + function normalizeCodeLocInfo(str) { + return str.replace(/\(at .+?:\d+\)/g, '(at **)'); + } + beforeEach(function() { jest.resetModuleRegistry(); @@ -146,9 +150,10 @@ describe('ReactContextValidator', function() { ReactTestUtils.renderIntoDocument(); expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed Context Types: ' + - 'Required context `foo` was not specified in `Component`.' + expect(normalizeCodeLocInfo(console.error.argsForCall[0][0])).toBe( + 'Warning: Failed context type: ' + + 'Required context `foo` was not specified in `Component`.\n' + + ' in Component (at **)' ); var ComponentInFooStringContext = React.createClass({ @@ -193,11 +198,12 @@ describe('ReactContextValidator', function() { ReactTestUtils.renderIntoDocument(); expect(console.error.argsForCall.length).toBe(2); - expect(console.error.argsForCall[1][0]).toBe( - 'Warning: Failed Context Types: ' + + expect(normalizeCodeLocInfo(console.error.argsForCall[1][0])).toBe( + 'Warning: Failed context type: ' + 'Invalid context `foo` of type `number` supplied ' + - 'to `Component`, expected `string`.' + - ' Check the render method of `ComponentInFooNumberContext`.' + 'to `Component`, expected `string`.\n' + + ' in Component (at **)\n' + + ' in ComponentInFooNumberContext (at **)' ); }); @@ -221,18 +227,20 @@ describe('ReactContextValidator', function() { ReactTestUtils.renderIntoDocument(); expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed Context Types: ' + - 'Required child context `foo` was not specified in `Component`.' + expect(normalizeCodeLocInfo(console.error.argsForCall[0][0])).toBe( + 'Warning: Failed childContext type: ' + + 'Required child context `foo` was not specified in `Component`.\n' + + ' in Component (at **)' ); ReactTestUtils.renderIntoDocument(); expect(console.error.argsForCall.length).toBe(2); - expect(console.error.argsForCall[1][0]).toBe( - 'Warning: Failed Context Types: ' + + expect(normalizeCodeLocInfo(console.error.argsForCall[1][0])).toBe( + 'Warning: Failed childContext type: ' + 'Invalid child context `foo` of type `number` ' + - 'supplied to `Component`, expected `string`.' + 'supplied to `Component`, expected `string`.\n' + + ' in Component (at **)' ); ReactTestUtils.renderIntoDocument( diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index bd04c8f122f7b..2b7e9da3d56ad 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -21,12 +21,12 @@ var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool'); var ReactElement = require('ReactElement'); -var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames'); var ReactPropTypeLocations = require('ReactPropTypeLocations'); +var checkReactTypeSpec = require('checkReactTypeSpec'); + var canDefineProperty = require('canDefineProperty'); var getIteratorFn = require('getIteratorFn'); -var invariant = require('invariant'); var warning = require('warning'); function getDeclarationErrorAddendum() { @@ -46,8 +46,6 @@ function getDeclarationErrorAddendum() { */ var ownerHasKeyUseWarning = {}; -var loggedTypeFailures = {}; - function getCurrentComponentErrorInfo(parentType) { var info = getDeclarationErrorAddendum(); @@ -152,66 +150,6 @@ function validateChildKeys(node, parentType) { } } -/** - * Assert that the props are valid - * - * @param {object} element - * @param {string} componentName Name of the component for error messages. - * @param {object} propTypes Map of prop name to a ReactPropType - * @param {string} location e.g. "prop", "context", "child context" - * @private - */ -function checkPropTypes(element, componentName, propTypes, location) { - var props = element.props; - for (var propName in propTypes) { - if (propTypes.hasOwnProperty(propName)) { - var error; - // Prop type validation may throw. In case they do, we don't want to - // fail the render phase where it didn't fail before. So we log it. - // After these have been cleaned up, we'll let them throw. - try { - // This is intentionally an invariant that gets caught. It's the same - // behavior as without this statement except with a better message. - invariant( - typeof propTypes[propName] === 'function', - '%s: %s type `%s` is invalid; it must be a function, usually from ' + - 'React.PropTypes.', - componentName || 'React class', - ReactPropTypeLocationNames[location], - propName - ); - error = propTypes[propName](props, propName, componentName, location); - } catch (ex) { - error = ex; - } - warning( - !error || error instanceof Error, - '%s: type specification of %s `%s` is invalid; the type checker ' + - 'function must return `null` or an `Error` but returned a %s. ' + - 'You may have forgotten to pass an argument to the type checker ' + - 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + - 'shape all require an argument).', - componentName || 'React class', - ReactPropTypeLocationNames[location], - propName, - typeof error - ); - if (error instanceof Error && !(error.message in loggedTypeFailures)) { - // Only monitor this failure once because there tends to be a lot of the - // same error. - loggedTypeFailures[error.message] = true; - - warning( - false, - 'Failed propType: %s%s', - error.message, - ReactComponentTreeDevtool.getCurrentStackAddendum(element) - ); - } - } - } -} - /** * Given an element, validate that its props follow the propTypes definition, * provided by the type. @@ -225,11 +163,13 @@ function validatePropTypes(element) { } var name = componentClass.displayName || componentClass.name; if (componentClass.propTypes) { - checkPropTypes( - element, - name, + checkReactTypeSpec( componentClass.propTypes, - ReactPropTypeLocations.prop + element.props, + ReactPropTypeLocations.prop, + name, + element, + null ); } if (typeof componentClass.getDefaultProps === 'function') { diff --git a/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js b/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js index b062870d17e14..322bcfb174e9f 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js @@ -269,7 +269,7 @@ describe('ReactElementClone', function() { ReactTestUtils.renderIntoDocument(React.createElement(GrandParent)); expect(console.error.argsForCall.length).toBe(1); expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Invalid prop `color` of type `number` supplied to `Component`, ' + 'expected `string`.\n' + ' in Component (created by GrandParent)\n' + diff --git a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js index 5f640d5e96fd7..69872e3dd30d2 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js @@ -281,7 +281,7 @@ describe('ReactElementValidator', function() { }); ReactTestUtils.renderIntoDocument(React.createElement(ParentComp)); expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Invalid prop `color` of type `number` supplied to `MyComp`, ' + 'expected `string`.\n' + ' in MyComp (created by ParentComp)\n' + @@ -360,7 +360,7 @@ describe('ReactElementValidator', function() { expect(console.error.calls.length).toBe(1); expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Required prop `prop` was not specified in `Component`.\n' + ' in Component' ); @@ -385,7 +385,7 @@ describe('ReactElementValidator', function() { expect(console.error.calls.length).toBe(1); expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Required prop `prop` was not specified in `Component`.\n' + ' in Component' ); @@ -412,13 +412,13 @@ describe('ReactElementValidator', function() { expect(console.error.calls.length).toBe(2); expect(console.error.argsForCall[0][0]).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Required prop `prop` was not specified in `Component`.\n' + ' in Component' ); expect(console.error.argsForCall[1][0]).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Invalid prop `prop` of type `number` supplied to ' + '`Component`, expected `string`.\n' + ' in Component' diff --git a/src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js b/src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js index cb778202c334c..b217438abe5c4 100644 --- a/src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js +++ b/src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js @@ -867,7 +867,7 @@ describe('ReactPropTypes', function() { instance = ReactTestUtils.renderIntoDocument(instance); expect(spy.argsForCall.length).toBe(1); - expect(spy.argsForCall[0][1]).toBe('num'); + expect(spy.argsForCall[0][1]).toBe('num'); }); it('should have received the validator\'s return value', function() { @@ -894,7 +894,7 @@ describe('ReactPropTypes', function() { expect( console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)') ).toBe( - 'Warning: Failed propType: num must be 5!\n' + + 'Warning: Failed prop type: num must be 5!\n' + ' in Component (at **)' ); }); diff --git a/src/isomorphic/classic/types/checkReactTypeSpec.js b/src/isomorphic/classic/types/checkReactTypeSpec.js new file mode 100644 index 0000000000000..3b119b395fea2 --- /dev/null +++ b/src/isomorphic/classic/types/checkReactTypeSpec.js @@ -0,0 +1,93 @@ +/** + * Copyright 2013-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. + * + * @providesModule checkReactTypeSpec + */ + +'use strict'; + +var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool'); +var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames'); + +var invariant = require('invariant'); +var warning = require('warning'); + +var loggedTypeFailures = {}; + +/** + * Assert that the values match with the type specs. + * Error messages are memorized and will only be shown once. + * + * @param {object} typeSpecs Map of name to a ReactPropType + * @param {object} values Runtime values that need to be type-checked + * @param {string} location e.g. "prop", "context", "child context" + * @param {string} componentName Name of the component for error messages. + * @param {?object} element The React element that is being type-checked + * @param {?number} debugID The React component instance that is being type-checked + * @private + */ +function checkReactTypeSpec(typeSpecs, values, location, componentName, element, debugID) { + for (var typeSpecName in typeSpecs) { + if (typeSpecs.hasOwnProperty(typeSpecName)) { + var error; + // Prop type validation may throw. In case they do, we don't want to + // fail the render phase where it didn't fail before. So we log it. + // After these have been cleaned up, we'll let them throw. + try { + // This is intentionally an invariant that gets caught. It's the same + // behavior as without this statement except with a better message. + invariant( + typeof typeSpecs[typeSpecName] === 'function', + '%s: %s type `%s` is invalid; it must be a function, usually from ' + + 'React.PropTypes.', + componentName || 'React class', + ReactPropTypeLocationNames[location], + typeSpecName + ); + error = typeSpecs[typeSpecName](values, typeSpecName, componentName, location); + } catch (ex) { + error = ex; + } + warning( + !error || error instanceof Error, + '%s: type specification of %s `%s` is invalid; the type checker ' + + 'function must return `null` or an `Error` but returned a %s. ' + + 'You may have forgotten to pass an argument to the type checker ' + + 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + + 'shape all require an argument).', + componentName || 'React class', + ReactPropTypeLocationNames[location], + typeSpecName, + typeof error + ); + if (error instanceof Error && !(error.message in loggedTypeFailures)) { + // Only monitor this failure once because there tends to be a lot of the + // same error. + loggedTypeFailures[error.message] = true; + + var componentStackInfo = ''; + + if (debugID !== null) { + componentStackInfo = ReactComponentTreeDevtool.getStackAddendumByID(debugID); + } else if (element !== null) { + componentStackInfo = ReactComponentTreeDevtool.getCurrentStackAddendum(element); + } + + warning( + false, + 'Failed %s type: %s%s', + location, + error.message, + componentStackInfo + ); + } + } + } +} + +module.exports = checkReactTypeSpec; diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 427d2340bb305..c6429dfc24097 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -198,7 +198,7 @@ describe('ReactJSXElementValidator', function() { expect( console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)') ).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Invalid prop `color` of type `number` supplied to `MyComp`, ' + 'expected `string`.\n' + ' in MyComp (at **)\n' + @@ -249,7 +249,7 @@ describe('ReactJSXElementValidator', function() { expect( console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)') ).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Required prop `prop` was not specified in `RequiredPropComponent`.\n' + ' in RequiredPropComponent (at **)' ); @@ -264,7 +264,7 @@ describe('ReactJSXElementValidator', function() { expect( console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)') ).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Required prop `prop` was not specified in `RequiredPropComponent`.\n' + ' in RequiredPropComponent (at **)' ); @@ -280,7 +280,7 @@ describe('ReactJSXElementValidator', function() { expect( console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)') ).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Required prop `prop` was not specified in `RequiredPropComponent`.\n' + ' in RequiredPropComponent (at **)' ); @@ -288,7 +288,7 @@ describe('ReactJSXElementValidator', function() { expect( console.error.argsForCall[1][0].replace(/\(at .+?:\d+\)/g, '(at **)') ).toBe( - 'Warning: Failed propType: ' + + 'Warning: Failed prop type: ' + 'Invalid prop `prop` of type `number` supplied to ' + '`RequiredPropComponent`, expected `string`.\n' + ' in RequiredPropComponent (at **)' diff --git a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js index b9ca072e556d4..7a02c1904a7f6 100644 --- a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js +++ b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js @@ -19,26 +19,16 @@ var ReactInstanceMap = require('ReactInstanceMap'); var ReactInstrumentation = require('ReactInstrumentation'); var ReactNodeTypes = require('ReactNodeTypes'); var ReactPropTypeLocations = require('ReactPropTypeLocations'); -var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames'); var ReactReconciler = require('ReactReconciler'); var ReactUpdateQueue = require('ReactUpdateQueue'); +var checkReactTypeSpec = require('checkReactTypeSpec'); + var emptyObject = require('emptyObject'); var invariant = require('invariant'); var shouldUpdateReactComponent = require('shouldUpdateReactComponent'); var warning = require('warning'); -function getDeclarationErrorAddendum(component) { - var owner = component._currentElement._owner || null; - if (owner) { - var name = owner.getName(); - if (name) { - return ' Check the render method of `' + name + '`.'; - } - } - return ''; -} - function StatelessComponent(Component) { } StatelessComponent.prototype.render = function() { @@ -669,45 +659,20 @@ var ReactCompositeComponentMixin = { /** * Assert that the context types are valid * - * @param {object} propTypes Map of context field to a ReactPropType - * @param {object} props + * @param {object} typeSpecs Map of context field to a ReactPropType + * @param {object} values Runtime values that need to be type-checked * @param {string} location e.g. "prop", "context", "child context" * @private */ - _checkContextTypes: function(propTypes, props, location) { - var componentName = this.getName(); - for (var propName in propTypes) { - if (propTypes.hasOwnProperty(propName)) { - var error; - try { - // This is intentionally an invariant that gets caught. It's the same - // behavior as without this statement except with a better message. - invariant( - typeof propTypes[propName] === 'function', - '%s: %s type `%s` is invalid; it must be a function, usually ' + - 'from React.PropTypes.', - componentName || 'React class', - ReactPropTypeLocationNames[location], - propName - ); - error = propTypes[propName](props, propName, componentName, location); - } catch (ex) { - error = ex; - } - if (error instanceof Error) { - // We may want to extend this logic for similar errors in - // top-level render calls, so I'm abstracting it away into - // a function to minimize refactoring in the future - var addendum = getDeclarationErrorAddendum(this); - warning( - false, - 'Failed Context Types: %s%s', - error.message, - addendum - ); - } - } - } + _checkContextTypes: function(typeSpecs, values, location) { + checkReactTypeSpec( + typeSpecs, + values, + location, + this.getName(), + null, + this._debugID + ); }, receiveComponent: function(nextElement, transaction, nextContext) { diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactStatelessComponent-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactStatelessComponent-test.js index c5b2744cebddc..56eceb67f2425 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactStatelessComponent-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactStatelessComponent-test.js @@ -178,7 +178,7 @@ describe('ReactStatelessComponent', function() { expect( console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)') ).toBe( - 'Warning: Failed propType: Invalid prop `test` of type `number` ' + + 'Warning: Failed prop type: Invalid prop `test` of type `number` ' + 'supplied to `Child`, expected `string`.\n' + ' in Child (at **)' );