From e403f30bca3af5b067bd7008bd6c37caf7d226c2 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Sat, 13 May 2017 00:01:34 +0100 Subject: [PATCH] Provide non-standard stack with invalid type warnings --- .flowconfig | 2 + .../classic/element/ReactElementValidator.js | 7 +++ .../__tests__/ReactElementValidator-test.js | 45 ++++++++++++++++ .../hooks/ReactComponentTreeHook.js | 51 +++++++++++++++++++ .../ReactJSXElementValidator-test.js | 49 ++++++++++++++++++ 5 files changed, 154 insertions(+) diff --git a/.flowconfig b/.flowconfig index 3a962f8881555..6b1984b129844 100644 --- a/.flowconfig +++ b/.flowconfig @@ -3,6 +3,8 @@ /examples/.* /fixtures/.* /build/.* +/node_modules/chrome-devtools-frontend/.* +/.*/node_modules/chrome-devtools-frontend/.* /.*/node_modules/y18n/.* /.*/__mocks__/.* /.*/__tests__/.* diff --git a/src/isomorphic/classic/element/ReactElementValidator.js b/src/isomorphic/classic/element/ReactElementValidator.js index b840fc5885e33..f7ee32194eab9 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -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 ' + @@ -231,6 +237,7 @@ var ReactElementValidator = { type == null ? type : typeof type, info, ); + ReactComponentTreeHook.popNonStandardWarningStack(); } } diff --git a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js index fc43eacc526a7..80c0b0252de76 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js @@ -525,4 +525,49 @@ describe('ReactElementValidator', () => { "component from the file it's defined in. Check your code at **.", ); }); + + it('provides stack via non-standard console.stack for invalid types', () => { + spyOn(console, 'error'); + + function Foo() { + var Bad = undefined; + return React.createElement(Bad); + } + + function App() { + return React.createElement(Foo); + } + + try { + console.stack = jest.fn(); + console.stackEnd = 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.stack.mock.calls.length).toBe(1); + expect(console.stackEnd.mock.calls.length).toBe(1); + + var stack = console.stack.mock.calls[0][0]; + expect(Array.isArray(stack)).toBe(true); + expect(stack.map(frame => frame.functionName)).toEqual([ + 'Foo', + 'App', + null, + ]); + expect( + stack.map(frame => frame.fileName && frame.fileName.slice(-8)), + ).toEqual([null, null, null]); + expect(stack.map(frame => frame.lineNumber)).toEqual([null, null, null]); + } finally { + delete console.stack; + delete console.stackEnd; + } + }); }); diff --git a/src/isomorphic/hooks/ReactComponentTreeHook.js b/src/isomorphic/hooks/ReactComponentTreeHook.js index b29488151911e..a935fa71e675b 100644 --- a/src/isomorphic/hooks/ReactComponentTreeHook.js +++ b/src/isomorphic/hooks/ReactComponentTreeHook.js @@ -402,6 +402,57 @@ var ReactComponentTreeHook = { getRootIDs, getRegisteredIDs: getItemIDs, + + pushNonStandardWarningStack( + isCreatingElement: boolean, + currentSource: ?Source, + ) { + if (typeof console.stack !== 'function') { + return; + } + + var stack = []; + var currentOwner = ReactCurrentOwner.current; + var id = currentOwner && currentOwner._debugID; + + try { + if (isCreatingElement) { + stack.push({ + fileName: currentSource ? currentSource.fileName : null, + lineNumber: currentSource ? currentSource.lineNumber : null, + functionName: id ? ReactComponentTreeHook.getDisplayName(id) : null, + }); + } + + while (id) { + var element = ReactComponentTreeHook.getElement(id); + var ownerID = ReactComponentTreeHook.getOwnerID(id); + var ownerName = ownerID + ? ReactComponentTreeHook.getDisplayName(ownerID) + : null; + var source = element && element._source; + stack.push({ + fileName: source ? source.fileName : null, + lineNumber: source ? source.lineNumber : null, + functionName: ownerName, + }); + // Owner stack is more useful for visual representation + id = ownerID || ReactComponentTreeHook.getParentID(id); + } + } catch (err) { + // Internal state is messed up. + // Stop building the stack (it's just a nice to have). + } + + console.stack(stack); + }, + + popNonStandardWarningStack() { + if (typeof console.stackEnd !== 'function') { + return; + } + console.stackEnd(); + }, }; module.exports = ReactComponentTreeHook; diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index b4f2697271ebc..2f8b8816b3eb7 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -400,4 +400,53 @@ describe('ReactJSXElementValidator', () => { ' Use a static property named `defaultProps` instead.', ); }); + + it('provides stack via non-standard console.stack for invalid types', () => { + spyOn(console, 'error'); + + function Foo() { + var Bad = undefined; + return ; + } + + function App() { + return ; + } + + try { + console.stack = jest.fn(); + console.stackEnd = jest.fn(); + + expect(() => { + ReactTestUtils.renderIntoDocument(); + }).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.stack.mock.calls.length).toBe(1); + expect(console.stackEnd.mock.calls.length).toBe(1); + + var stack = console.stack.mock.calls[0][0]; + expect(Array.isArray(stack)).toBe(true); + expect(stack.map(frame => frame.functionName)).toEqual([ + 'Foo', + 'App', + null, + ]); + expect( + stack.map(frame => frame.fileName && frame.fileName.slice(-8)), + ).toEqual(['-test.js', '-test.js', '-test.js']); + expect(stack.map(frame => typeof frame.lineNumber)).toEqual([ + 'number', + 'number', + 'number', + ]); + } finally { + delete console.stack; + delete console.stackEnd; + } + }); });