Skip to content

Commit

Permalink
Show component stack in PropTypes warnings
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiebits committed May 14, 2016
1 parent 3cc733a commit 397a15a
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 27 deletions.
20 changes: 13 additions & 7 deletions src/isomorphic/classic/element/ReactElementValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@

'use strict';

var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool');
var ReactElement = require('ReactElement');
var ReactPropTypeLocations = require('ReactPropTypeLocations');
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactPropTypeLocations = require('ReactPropTypeLocations');

var canDefineProperty = require('canDefineProperty');
var getIteratorFn = require('getIteratorFn');
Expand Down Expand Up @@ -171,13 +172,14 @@ 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 {object} props
* @param {string} location e.g. "prop", "context", "child context"
* @private
*/
function checkPropTypes(componentName, propTypes, props, location) {
function checkPropTypes(element, componentName, propTypes, location) {
var props = element.props;
for (var propName in propTypes) {
if (propTypes.hasOwnProperty(propName)) {
var error;
Expand Down Expand Up @@ -216,8 +218,12 @@ function checkPropTypes(componentName, propTypes, props, location) {
// same error.
loggedTypeFailures[error.message] = true;

var addendum = getDeclarationErrorAddendum();
warning(false, 'Failed propType: %s%s', error.message, addendum);
warning(
false,
'Failed propType: %s%s',
error.message,
ReactComponentTreeDevtool.getCurrentStackAddendum(element)
);
}
}
}
Expand All @@ -237,9 +243,9 @@ function validatePropTypes(element) {
var name = componentClass.displayName || componentClass.name;
if (componentClass.propTypes) {
checkPropTypes(
element,
name,
componentClass.propTypes,
element.props,
ReactPropTypeLocations.prop
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,10 @@ describe('ReactElementClone', function() {
expect(console.error.argsForCall[0][0]).toBe(
'Warning: Failed propType: ' +
'Invalid prop `color` of type `number` supplied to `Component`, ' +
'expected `string`. Check the render method of `Parent`.'
'expected `string`.\n' +
' in Component (created by GrandParent)\n' +
' in Parent (created by GrandParent)\n' +
' in GrandParent'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ describe('ReactElementValidator', function() {
expect(console.error.argsForCall[0][0]).toBe(
'Warning: Failed propType: ' +
'Invalid prop `color` of type `number` supplied to `MyComp`, ' +
'expected `string`. Check the render method of `ParentComp`.'
'expected `string`.\n' +
' in MyComp (created by ParentComp)\n' +
' in ParentComp'
);
});

Expand Down Expand Up @@ -318,7 +320,8 @@ describe('ReactElementValidator', function() {
expect(console.error.calls.length).toBe(1);
expect(console.error.argsForCall[0][0]).toBe(
'Warning: Failed propType: ' +
'Required prop `prop` was not specified in `Component`.'
'Required prop `prop` was not specified in `Component`.\n' +
' in Component'
);
});

Expand All @@ -342,7 +345,8 @@ describe('ReactElementValidator', function() {
expect(console.error.calls.length).toBe(1);
expect(console.error.argsForCall[0][0]).toBe(
'Warning: Failed propType: ' +
'Required prop `prop` was not specified in `Component`.'
'Required prop `prop` was not specified in `Component`.\n' +
' in Component'
);
});

Expand All @@ -368,13 +372,15 @@ describe('ReactElementValidator', function() {
expect(console.error.calls.length).toBe(2);
expect(console.error.argsForCall[0][0]).toBe(
'Warning: Failed propType: ' +
'Required prop `prop` was not specified in `Component`.'
'Required prop `prop` was not specified in `Component`.\n' +
' in Component'
);

expect(console.error.argsForCall[1][0]).toBe(
'Warning: Failed propType: ' +
'Invalid prop `prop` of type `number` supplied to ' +
'`Component`, expected `string`.'
'`Component`, expected `string`.\n' +
' in Component'
);

ReactTestUtils.renderIntoDocument(
Expand Down
7 changes: 5 additions & 2 deletions src/isomorphic/classic/types/__tests__/ReactPropTypes-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -891,8 +891,11 @@ describe('ReactPropTypes', function() {
var instance = <Component num={6} />;
instance = ReactTestUtils.renderIntoDocument(instance);
expect(console.error.argsForCall.length).toBe(1);
expect(console.error.argsForCall[0][0]).toBe(
'Warning: Failed propType: num must be 5!'
expect(
console.error.argsForCall[0][0].replace(/\(at .+?:\d+\)/g, '(at **)')
).toBe(
'Warning: Failed propType: num must be 5!\n' +
' in Component (at **)'
);
});

Expand Down
92 changes: 92 additions & 0 deletions src/isomorphic/devtools/ReactComponentTreeDevtool.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,33 @@
'use strict';

var invariant = require('invariant');
var warning = require('warning');

var tree = {};
var unmountedIDs = {};
var rootIDs = {};

var processingStack = [];

function startProcessing(id) {
processingStack.push(id);
}

function stopProcessing(id) {
// This should just be a single .pop() in most cases, but maybe not if there
// was an exception or similar.
do {
if (!processingStack.length) {
warning(
false,
'ReactComponentTreeDevtool: stopProcessing(%s) called, no match found.',
id
);
return;
}
} while (processingStack.pop() !== id);
}

function updateTree(id, update) {
if (!tree[id]) {
tree[id] = {
Expand Down Expand Up @@ -96,15 +118,18 @@ var ReactComponentTreeDevtool = {

onBeforeMountComponent(id, element) {
updateTree(id, item => item.element = element);
startProcessing(id);
},

onBeforeUpdateComponent(id, element) {
updateTree(id, item => item.element = element);
startProcessing(id);
},

onMountComponent(id) {
updateTree(id, item => item.isMounted = true);
delete unmountedIDs[id];
stopProcessing(id);
},

onMountRootComponent(id) {
Expand All @@ -113,6 +138,7 @@ var ReactComponentTreeDevtool = {

onUpdateComponent(id) {
updateTree(id, item => item.updateCount++);
stopProcessing(id);
},

onUnmountComponent(id) {
Expand All @@ -138,6 +164,67 @@ var ReactComponentTreeDevtool = {
return item ? item.isMounted : false;
},

getCurrentStackAddendum(topElement) {
function describeComponentFrame(name, source, ownerName) {
return '\n in ' + name + (
source ?
' (at ' + source.fileName.replace(/^.*[\\\/]/, '') + ':' +
source.lineNumber + ')' :
ownerName ?
' (created by ' + ownerName + ')' :
''
);
}

function describeID(id) {
var name = ReactComponentTreeDevtool.getDisplayName(id);
var element = ReactComponentTreeDevtool.getElement(id);
if (!element) {
// TODO: This check shouldn't be necessary, but in the case that a mount
// gets aborted due to an exception, processingStack has leftover
// frames. In contrast, if we clear frames in purgeUnmountedComponents
// then we get confused if someone starts a new root during a render.
// Both of these are silly (and cause visible warnings or errors) but
// are regrettably barely supported.
return '';
}
var ownerID = ReactComponentTreeDevtool.getOwnerID(id);
var ownerName;
if (ownerID) {
ownerName = ReactComponentTreeDevtool.getDisplayName(ownerID);
}
return describeComponentFrame(name, element._source, ownerName);
}

var info = '';
if (topElement) {
var type = topElement.type;
var name = typeof type === 'function' ?
type.displayName || type.name :
type;
var owner = topElement._owner;
info += describeComponentFrame(
name || 'Unknown',
topElement._source,
owner && owner.getName()
);
}

var ii = processingStack.length;
if (ii) {
var id;
while (ii-- > 0) {
id = processingStack[ii];
info += describeID(id);
}
while ((id = ReactComponentTreeDevtool.getParentID(id))) {
info += describeID(id);
}
}

return info;
},

getChildIDs(id) {
var item = tree[id];
return item ? item.childIDs : [];
Expand All @@ -148,6 +235,11 @@ var ReactComponentTreeDevtool = {
return item ? item.displayName : 'Unknown';
},

getElement(id) {
var item = tree[id];
return item ? item.element : null;
},

getOwnerID(id) {
var item = tree[id];
return item ? item.ownerID : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1741,4 +1741,78 @@ describe('ReactComponentTreeDevtool', () => {
expect(getRootDisplayNames()).toEqual([]);
expect(getRegisteredDisplayNames()).toEqual([]);
});

it('creates stack addenda', () => {
function getAddendum(element) {
var addendum = ReactComponentTreeDevtool.getCurrentStackAddendum(element);
return addendum.replace(/\(at .+?:\d+\)/g, '(at **)');
}

var Anon = React.createClass({displayName: null, render: () => null});
var Orange = React.createClass({render: () => null});

expect(getAddendum()).toBe(
''
);
expect(getAddendum(<div />)).toBe(
'\n in div (at **)'
);
expect(getAddendum(<Anon />)).toBe(
'\n in Unknown (at **)'
);
expect(getAddendum(<Orange />)).toBe(
'\n in Orange (at **)'
);
expect(getAddendum(React.createElement(Orange))).toBe(
'\n in Orange'
);

var renders = 0;
var rOwnedByQ;

function Q() {
return (rOwnedByQ = React.createElement(R));
}
function R() {
return <div><S /></div>;
}
class S extends React.Component {
componentDidMount() {
// Check that the parent path is still fetched when only S itself is on
// the stack.
this.forceUpdate();
}
render() {
expect(getAddendum()).toBe(
'\n in S (at **)' +
'\n in div (at **)' +
'\n in R (created by Q)' +
'\n in Q (at **)'
);
expect(getAddendum(<span />)).toBe(
'\n in span (at **)' +
'\n in S (at **)' +
'\n in div (at **)' +
'\n in R (created by Q)' +
'\n in Q (at **)'
);
expect(getAddendum(React.createElement('span'))).toBe(
'\n in span (created by S)' +
'\n in S (at **)' +
'\n in div (at **)' +
'\n in R (created by Q)' +
'\n in Q (at **)'
);
renders++;
return null;
}
}
ReactDOM.render(<Q />, document.createElement('div'));
expect(renders).toBe(2);

// Make sure owner is fetched for the top element too.
expect(getAddendum(rOwnedByQ)).toBe(
'\n in R (created by Q)'
);
});
});
Loading

0 comments on commit 397a15a

Please sign in to comment.