Skip to content

Commit f0ccab3

Browse files
committed
Provide non-standard stack with invalid type warnings
1 parent 2bbe024 commit f0ccab3

File tree

5 files changed

+153
-0
lines changed

5 files changed

+153
-0
lines changed

.flowconfig

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PROJECT_ROOT>/examples/.*
44
<PROJECT_ROOT>/fixtures/.*
55
<PROJECT_ROOT>/build/.*
6+
<PROJECT_ROOT>/.*/node_modules/chrome-devtools-frontend/.*
67
<PROJECT_ROOT>/.*/node_modules/y18n/.*
78
<PROJECT_ROOT>/.*/__mocks__/.*
89
<PROJECT_ROOT>/.*/__tests__/.*

src/isomorphic/classic/element/ReactElementValidator.js

+12
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,17 @@ var ReactElementValidator = {
223223

224224
info += ReactComponentTreeHook.getCurrentStackAddendum();
225225

226+
var source = props !== null &&
227+
props !== undefined &&
228+
props.__source !== undefined
229+
? props.__source
230+
: null;
231+
var extraFrame = {
232+
fileName: source !== null ? source.fileName : null,
233+
lineNumber: source !== null ? source.lineNumber : null,
234+
functionName: null,
235+
};
236+
ReactComponentTreeHook.pushNonStandardWarningStack(extraFrame);
226237
warning(
227238
false,
228239
'React.createElement: type is invalid -- expected a string (for ' +
@@ -231,6 +242,7 @@ var ReactElementValidator = {
231242
type == null ? type : typeof type,
232243
info,
233244
);
245+
ReactComponentTreeHook.popNonStandardWarningStack();
234246
}
235247
}
236248

src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js

+45
Original file line numberDiff line numberDiff line change
@@ -525,4 +525,49 @@ describe('ReactElementValidator', () => {
525525
"component from the file it's defined in. Check your code at **.",
526526
);
527527
});
528+
529+
it('provides stack via non-standard console.stack for invalid types', () => {
530+
spyOn(console, 'error');
531+
532+
function Foo() {
533+
var Bad = undefined;
534+
return React.createElement(Bad);
535+
}
536+
537+
function App() {
538+
return React.createElement(Foo);
539+
}
540+
541+
try {
542+
console.stack = jest.fn();
543+
console.stackEnd = jest.fn();
544+
545+
expect(() => {
546+
ReactTestUtils.renderIntoDocument(React.createElement(App));
547+
}).toThrow(
548+
'Element type is invalid: expected a string (for built-in components) ' +
549+
'or a class/function (for composite components) but got: undefined. ' +
550+
"You likely forgot to export your component from the file it's " +
551+
'defined in. Check the render method of `Foo`.',
552+
);
553+
554+
expect(console.stack.mock.calls.length).toBe(1);
555+
expect(console.stackEnd.mock.calls.length).toBe(1);
556+
557+
var stack = console.stack.mock.calls[0][0];
558+
expect(Array.isArray(stack)).toBe(true);
559+
expect(stack.map(frame => frame.functionName)).toEqual([
560+
null,
561+
'Foo',
562+
'App',
563+
]);
564+
expect(
565+
stack.map(frame => frame.fileName && frame.fileName.slice(-8)),
566+
).toEqual([null, null, null]);
567+
expect(stack.map(frame => frame.lineNumber)).toEqual([null, null, null]);
568+
} finally {
569+
delete console.stack;
570+
delete console.stackEnd;
571+
}
572+
});
528573
});

src/isomorphic/hooks/ReactComponentTreeHook.js

+46
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ var warning = require('warning');
2020
import type {ReactElement, Source} from 'ReactElementType';
2121
import type {DebugID} from 'ReactInstanceType';
2222

23+
type StackFrame = {
24+
fileName: string | null,
25+
lineNumber: number | null,
26+
functionName: string | null,
27+
};
28+
2329
function isNative(fn) {
2430
// Based on isNative() from Lodash
2531
var funcToString = Function.prototype.toString;
@@ -402,6 +408,46 @@ var ReactComponentTreeHook = {
402408

403409
getRootIDs,
404410
getRegisteredIDs: getItemIDs,
411+
412+
pushNonStandardWarningStack(extraFrame: StackFrame | null) {
413+
if (typeof console.stack !== 'function') {
414+
return;
415+
}
416+
417+
var stack = [];
418+
if (extraFrame) {
419+
stack.push(extraFrame);
420+
}
421+
422+
var currentOwner = ReactCurrentOwner.current;
423+
var id = currentOwner && currentOwner._debugID;
424+
425+
try {
426+
while (id) {
427+
var name = ReactComponentTreeHook.getDisplayName(id);
428+
var element = ReactComponentTreeHook.getElement(id);
429+
var source = element && element._source;
430+
stack.push({
431+
fileName: source ? source.fileName : null,
432+
lineNumber: source ? source.lineNumber : null,
433+
functionName: name,
434+
});
435+
id = ReactComponentTreeHook.getParentID(id);
436+
}
437+
} catch (err) {
438+
// Internal state is messed up.
439+
// Stop building the stack (it's just a nice to have).
440+
}
441+
442+
console.stack(stack);
443+
},
444+
445+
popNonStandardWarningStack() {
446+
if (typeof console.stackEnd !== 'function') {
447+
return;
448+
}
449+
console.stackEnd();
450+
},
405451
};
406452

407453
module.exports = ReactComponentTreeHook;

src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js

+49
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,53 @@ describe('ReactJSXElementValidator', () => {
400400
' Use a static property named `defaultProps` instead.',
401401
);
402402
});
403+
404+
it('provides stack via non-standard console.stack for invalid types', () => {
405+
spyOn(console, 'error');
406+
407+
function Foo() {
408+
var Bad = undefined;
409+
return <Bad />;
410+
}
411+
412+
function App() {
413+
return <Foo />;
414+
}
415+
416+
try {
417+
console.stack = jest.fn();
418+
console.stackEnd = jest.fn();
419+
420+
expect(() => {
421+
ReactTestUtils.renderIntoDocument(<App />);
422+
}).toThrow(
423+
'Element type is invalid: expected a string (for built-in components) ' +
424+
'or a class/function (for composite components) but got: undefined. ' +
425+
"You likely forgot to export your component from the file it's " +
426+
'defined in. Check the render method of `Foo`.',
427+
);
428+
429+
expect(console.stack.mock.calls.length).toBe(1);
430+
expect(console.stackEnd.mock.calls.length).toBe(1);
431+
432+
var stack = console.stack.mock.calls[0][0];
433+
expect(Array.isArray(stack)).toBe(true);
434+
expect(stack.map(frame => frame.functionName)).toEqual([
435+
null,
436+
'Foo',
437+
'App',
438+
]);
439+
expect(
440+
stack.map(frame => frame.fileName && frame.fileName.slice(-8)),
441+
).toEqual(['-test.js', '-test.js', '-test.js']);
442+
expect(stack.map(frame => typeof frame.lineNumber)).toEqual([
443+
'number',
444+
'number',
445+
'number',
446+
]);
447+
} finally {
448+
delete console.stack;
449+
delete console.stackEnd;
450+
}
451+
});
403452
});

0 commit comments

Comments
 (0)