From f0ccab38a549a772cb9587ed49eb67fe8acab3c9 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 | 1 + .../classic/element/ReactElementValidator.js | 12 +++++ .../__tests__/ReactElementValidator-test.js | 45 +++++++++++++++++ .../hooks/ReactComponentTreeHook.js | 46 +++++++++++++++++ .../ReactJSXElementValidator-test.js | 49 +++++++++++++++++++ 5 files changed, 153 insertions(+) diff --git a/.flowconfig b/.flowconfig index 3a962f8881555..059e1b7e95b56 100644 --- a/.flowconfig +++ b/.flowconfig @@ -3,6 +3,7 @@ /examples/.* /fixtures/.* /build/.* +/.*/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..97b69a31ab99c 100644 --- a/src/isomorphic/classic/element/ReactElementValidator.js +++ b/src/isomorphic/classic/element/ReactElementValidator.js @@ -223,6 +223,17 @@ var ReactElementValidator = { info += ReactComponentTreeHook.getCurrentStackAddendum(); + var source = props !== null && + props !== undefined && + props.__source !== undefined + ? props.__source + : null; + var extraFrame = { + fileName: source !== null ? source.fileName : null, + lineNumber: source !== null ? source.lineNumber : null, + functionName: null, + }; + ReactComponentTreeHook.pushNonStandardWarningStack(extraFrame); warning( false, 'React.createElement: type is invalid -- expected a string (for ' + @@ -231,6 +242,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..a9eb95a9555f6 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([ + null, + 'Foo', + 'App', + ]); + 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..00478909c4399 100644 --- a/src/isomorphic/hooks/ReactComponentTreeHook.js +++ b/src/isomorphic/hooks/ReactComponentTreeHook.js @@ -20,6 +20,12 @@ var warning = require('warning'); import type {ReactElement, Source} from 'ReactElementType'; import type {DebugID} from 'ReactInstanceType'; +type StackFrame = { + fileName: string | null, + lineNumber: number | null, + functionName: string | null, +}; + function isNative(fn) { // Based on isNative() from Lodash var funcToString = Function.prototype.toString; @@ -402,6 +408,46 @@ var ReactComponentTreeHook = { getRootIDs, getRegisteredIDs: getItemIDs, + + pushNonStandardWarningStack(extraFrame: StackFrame | null) { + if (typeof console.stack !== 'function') { + return; + } + + var stack = []; + if (extraFrame) { + stack.push(extraFrame); + } + + var currentOwner = ReactCurrentOwner.current; + var id = currentOwner && currentOwner._debugID; + + try { + while (id) { + var name = ReactComponentTreeHook.getDisplayName(id); + var element = ReactComponentTreeHook.getElement(id); + var source = element && element._source; + stack.push({ + fileName: source ? source.fileName : null, + lineNumber: source ? source.lineNumber : null, + functionName: name, + }); + id = 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..59fe78d5ce100 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([ + null, + 'Foo', + 'App', + ]); + 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; + } + }); });