Skip to content

Commit

Permalink
Provide non-standard stack with invalid type warnings (facebook#9679)
Browse files Browse the repository at this point in the history
* Provide non-standard stack with invalid type warnings

* Include parent stack but mark owner chain as pertinent

* Just parent stack is enough for my needs

Because to avoid noise it is enough to collapse too close frames in the UI.

* functionName => name

* Hide behind a feature flag
  • Loading branch information
gaearon authored and flarnie committed Jun 7, 2017
1 parent f7f2bc7 commit 2044e1b
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<PROJECT_ROOT>/examples/.*
<PROJECT_ROOT>/fixtures/.*
<PROJECT_ROOT>/build/.*
<PROJECT_ROOT>/node_modules/chrome-devtools-frontend/.*
<PROJECT_ROOT>/.*/node_modules/chrome-devtools-frontend/.*
<PROJECT_ROOT>/.*/node_modules/y18n/.*
<PROJECT_ROOT>/.*/__mocks__/.*
<PROJECT_ROOT>/.*/__tests__/.*
Expand Down
7 changes: 7 additions & 0 deletions src/isomorphic/classic/element/ReactElementValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ var ReactElementValidator = {

info += ReactComponentTreeHook.getCurrentStackAddendum();

var currentSource = props !== null &&
props !== undefined &&
props.__source !== undefined
? props.__source
: null;
ReactComponentTreeHook.pushNonStandardWarningStack(true, currentSource);
warning(
false,
'React.createElement: type is invalid -- expected a string (for ' +
Expand All @@ -231,6 +237,7 @@ var ReactElementValidator = {
type == null ? type : typeof type,
info,
);
ReactComponentTreeHook.popNonStandardWarningStack();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -525,4 +525,55 @@ describe('ReactElementValidator', () => {
"component from the file it's defined in. Check your code at **.",
);
});

it('provides stack via non-standard console.reactStack for invalid types', () => {
spyOn(console, 'error');

function Foo() {
var Bad = undefined;
return React.createElement(Bad);
}

function App() {
return React.createElement('div', null, React.createElement(Foo));
}

try {
console.reactStack = jest.fn();
console.reactStackEnd = jest.fn();

expect(() => {
ReactTestUtils.renderIntoDocument(React.createElement(App));
}).toThrow(
'Element type is invalid: expected a string (for built-in components) ' +
'or a class/function (for composite components) but got: undefined. ' +
"You likely forgot to export your component from the file it's " +
'defined in. Check the render method of `Foo`.',
);

expect(console.reactStack.mock.calls.length).toBe(1);
expect(console.reactStackEnd.mock.calls.length).toBe(1);

var stack = console.reactStack.mock.calls[0][0];
expect(Array.isArray(stack)).toBe(true);
expect(stack.map(frame => frame.name)).toEqual([
'Foo', // <Bad> is inside Foo
'App', // <Foo> is inside App
'App', // <div> is inside App
null, // <App> is outside a component
]);
expect(
stack.map(frame => frame.fileName && frame.fileName.slice(-8)),
).toEqual([null, null, null, null]);
expect(stack.map(frame => frame.lineNumber)).toEqual([
null,
null,
null,
null,
]);
} finally {
delete console.reactStack;
delete console.reactStackEnd;
}
});
});
51 changes: 51 additions & 0 deletions src/isomorphic/hooks/ReactComponentTreeHook.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,57 @@ var ReactComponentTreeHook = {

getRootIDs,
getRegisteredIDs: getItemIDs,

pushNonStandardWarningStack(
isCreatingElement: boolean,
currentSource: ?Source,
) {
if (typeof console.reactStack !== 'function') {
return;
}

var stack = [];
var currentOwner = ReactCurrentOwner.current;
var id = currentOwner && currentOwner._debugID;

try {
if (isCreatingElement) {
stack.push({
name: id ? ReactComponentTreeHook.getDisplayName(id) : null,
fileName: currentSource ? currentSource.fileName : null,
lineNumber: currentSource ? currentSource.lineNumber : null,
});
}

while (id) {
var element = ReactComponentTreeHook.getElement(id);
var parentID = ReactComponentTreeHook.getParentID(id);
var ownerID = ReactComponentTreeHook.getOwnerID(id);
var ownerName = ownerID
? ReactComponentTreeHook.getDisplayName(ownerID)
: null;
var source = element && element._source;
stack.push({
name: ownerName,
fileName: source ? source.fileName : null,
lineNumber: source ? source.lineNumber : null,
});
id = parentID;
}
} catch (err) {
// Internal state is messed up.
// Stop building the stack (it's just a nice to have).
}

console.reactStack(stack);
},

popNonStandardWarningStack() {
if (typeof console.reactStackEnd !== 'function') {
return;
}
console.reactStackEnd();
},
};

module.exports = ReactComponentTreeHook;
Original file line number Diff line number Diff line change
Expand Up @@ -400,4 +400,55 @@ describe('ReactJSXElementValidator', () => {
' Use a static property named `defaultProps` instead.',
);
});

it('provides stack via non-standard console.reactStack for invalid types', () => {
spyOn(console, 'error');

function Foo() {
var Bad = undefined;
return <Bad />;
}

function App() {
return <div><Foo /></div>;
}

try {
console.reactStack = jest.fn();
console.reactStackEnd = jest.fn();

expect(() => {
ReactTestUtils.renderIntoDocument(<App />);
}).toThrow(
'Element type is invalid: expected a string (for built-in components) ' +
'or a class/function (for composite components) but got: undefined. ' +
"You likely forgot to export your component from the file it's " +
'defined in. Check the render method of `Foo`.',
);

expect(console.reactStack.mock.calls.length).toBe(1);
expect(console.reactStackEnd.mock.calls.length).toBe(1);

var stack = console.reactStack.mock.calls[0][0];
expect(Array.isArray(stack)).toBe(true);
expect(stack.map(frame => frame.name)).toEqual([
'Foo', // <Bad> is inside Foo
'App', // <Foo> is inside App
'App', // <div> is inside App
null, // <App> is outside a component
]);
expect(
stack.map(frame => frame.fileName && frame.fileName.slice(-8)),
).toEqual(['-test.js', '-test.js', '-test.js', '-test.js']);
expect(stack.map(frame => typeof frame.lineNumber)).toEqual([
'number',
'number',
'number',
'number',
]);
} finally {
delete console.reactStack;
delete console.reactStackEnd;
}
});
});

0 comments on commit 2044e1b

Please sign in to comment.