@samitouri / QOS-React-2 / commits / 98d410f500

Build Component Stacks from Native Stack Frames (#18561)

* Implement component stack extraction hack * Normalize errors in tests This drops the requirement to include owner to pass the test. * Special case tests * Add destructuring to force toObject which throws before the side-effects This ensures that we don't double call yieldValue or advanceTime in tests. Ideally we could use empty destructuring but ES lint doesn't like it. * Cache the result in DEV In DEV it's somewhat likely that we'll see many logs that add component stacks. This could be slow so we cache the results of previous components. * Fixture * Add Reflect to lint * Log if out of range. * Fix special case when the function call throws in V8 In V8 we need to ignore the first line. Normally we would never get there because the stacks would differ before that, but the stacks are the same if we end up throwing at the same place as the control.

Sebastian Markbåge committed Apr 10, 2020 at 13:32 UTC 98d410f5005988644d01c9ec79b7181c3dd6c847
35 files changed +594 -179
fixtures/stacks/BabelClass-compiled.js new
+25
@@ -0,0 +1,25 @@
1 +function _inheritsLoose(subClass, superClass) {
2 + subClass.prototype = Object.create(superClass.prototype);
3 + subClass.prototype.constructor = subClass;
4 + subClass.__proto__ = superClass;
5 +}
6 +
7 +// Compile this with Babel.
8 +// babel --config-file ./babel.config.json BabelClass.js --out-file BabelClass-compiled.js --source-maps
9 +let BabelClass = /*#__PURE__*/ (function(_React$Component) {
10 + _inheritsLoose(BabelClass, _React$Component);
11 +
12 + function BabelClass() {
13 + return _React$Component.apply(this, arguments) || this;
14 + }
15 +
16 + var _proto = BabelClass.prototype;
17 +
18 + _proto.render = function render() {
19 + return this.props.children;
20 + };
21 +
22 + return BabelClass;
23 +})(React.Component);
24 +
25 +//# sourceMappingURL=BabelClass-compiled.js.map
fixtures/stacks/BabelClass-compiled.js.map new
+1
@@ -0,0 +1 @@
1 +{"version":3,"sources":["BabelClass.js"],"names":[],"mappings":";;AAAA;AACA;IAEM,U;;;;;;;;;SACJ,M,GAAA,kBAAS;AACP,WAAO,KAAK,KAAL,CAAW,QAAlB;AACD,G;;;EAHsB,KAAK,CAAC,S","file":"BabelClass-compiled.js","sourcesContent":["// Compile this with Babel.\n// babel --config-file ./babel.config.json BabelClass.js --out-file BabelClass-compiled.js --source-maps\n\nclass BabelClass extends React.Component {\n render() {\n return this.props.children;\n }\n}\n"]}
\ No newline at end of file
fixtures/stacks/BabelClass.js new
+8
@@ -0,0 +1,8 @@
1 +// Compile this with Babel.
2 +// babel --config-file ./babel.config.json BabelClass.js --out-file BabelClass-compiled.js --source-maps
3 +
4 +class BabelClass extends React.Component {
5 + render() {
6 + return this.props.children;
7 + }
8 +}
fixtures/stacks/Component.js new
+20
@@ -0,0 +1,20 @@
1 +// Example
2 +
3 +const Throw = React.lazy(() => {
4 + throw new Error('Example');
5 +});
6 +
7 +const Component = React.memo(function Component({children}) {
8 + return children;
9 +});
10 +
11 +function DisplayName({children}) {
12 + return children;
13 +}
14 +DisplayName.displayName = 'Custom Name';
15 +
16 +class NativeClass extends React.Component {
17 + render() {
18 + return this.props.children;
19 + }
20 +}
fixtures/stacks/Example.js new
+59
@@ -0,0 +1,59 @@
1 +// Example
2 +
3 +const x = React.createElement;
4 +
5 +class ErrorBoundary extends React.Component {
6 + static getDerivedStateFromError(error) {
7 + return {
8 + error: error,
9 + };
10 + }
11 +
12 + componentDidCatch(error, errorInfo) {
13 + console.log(error.message, errorInfo.componentStack);
14 + this.setState({
15 + componentStack: errorInfo.componentStack,
16 + });
17 + }
18 +
19 + render() {
20 + if (this.state && this.state.error) {
21 + return x(
22 + 'div',
23 + null,
24 + x('h3', null, this.state.error.message),
25 + x('pre', null, this.state.componentStack)
26 + );
27 + }
28 + return this.props.children;
29 + }
30 +}
31 +
32 +function Example() {
33 + let state = React.useState(false);
34 + return x(
35 + ErrorBoundary,
36 + null,
37 + x(
38 + DisplayName,
39 + null,
40 + x(
41 + React.SuspenseList,
42 + null,
43 + x(
44 + NativeClass,
45 + null,
46 + x(
47 + BabelClass,
48 + null,
49 + x(
50 + React.Suspense,
51 + null,
52 + x('div', null, x(Component, null, x(Throw)))
53 + )
54 + )
55 + )
56 + )
57 + )
58 + );
59 +}
fixtures/stacks/babel.config.json new
+5
@@ -0,0 +1,5 @@
1 +{
2 + "plugins": [
3 + ["@babel/plugin-transform-classes", {"loose": true}]
4 + ]
5 +}
fixtures/stacks/index.html new
+51
@@ -0,0 +1,51 @@
1 +<!DOCTYPE html>
2 +<html>
3 + <head>
4 + <meta charset="utf-8">
5 + <title>Component Stacks</title>
6 + <style>
7 + html, body {
8 + margin: 20px;
9 + }
10 + pre {
11 + background: #eee;
12 + border: 1px solid #ccc;
13 + padding: 2px;
14 + }
15 + </style>
16 + </head>
17 + <body>
18 + <div id="container">
19 + <p>
20 + To install React, follow the instructions on
21 + <a href="https://github.com/facebook/react/">GitHub</a>.
22 + </p>
23 + <p>
24 + If you can see this, React is <strong>not</strong> working right.
25 + If you checked out the source from GitHub make sure to run <code>npm run build</code>.
26 + </p>
27 + </div>
28 + <script src="../../build/node_modules/react/umd/react.production.min.js"></script>
29 + <script src="../../build/node_modules/react-dom/umd/react-dom.production.min.js"></script>
30 + <script src="./Component.js"></script>
31 + <script src="./BabelClass-compiled.js"></script>
32 + <script src="./Example.js"></script>
33 + <script>
34 + const container = document.getElementById("container");
35 + ReactDOM.render(React.createElement(Example), container);
36 + </script>
37 + <h3>The above stack should look something like this:</h3>
38 + <pre>
39 +
40 + at Lazy
41 + at Component (/stacks/Component.js:7:50)
42 + at div
43 + at Suspense
44 + at BabelClass (/stacks/BabelClass-compiled.js:13:29)
45 + at NativeClass (/stacks/Component.js:16:1)
46 + at SuspenseList
47 + at Custom Name (/stacks/Component.js:11:23)
48 + at ErrorBoundary (/stacks/Example.js:5:1)
49 + at Example (/stacks/Example.js:33:21)</pre>
50 + </body>
51 +</html>
packages/react-devtools-shared/src/__tests__/console-test.js
+6 -1
@@ -61,7 +61,12 @@ describe('console', () => {
61 });
62
63 function normalizeCodeLocInfo(str) {
64 - return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
64 + return (
65 + str &&
66 + str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
67 + return '\n in ' + name + ' (at **)';
68 + })
69 + );
70 }
71
72 it('should not patch console methods that do not receive component stacks', () => {
packages/react-dom/src/__tests__/ReactComponent-test.js
+6 -1
@@ -16,7 +16,12 @@ let ReactTestUtils;
16
17 describe('ReactComponent', () => {
18 function normalizeCodeLocInfo(str) {
19 - return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
19 + return (
20 + str &&
21 + str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
22 + return '\n in ' + name + ' (at **)';
23 + })
24 + );
25 }
26
27 beforeEach(() => {
packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+26 -11
@@ -17,7 +17,12 @@ describe('ReactDOMComponent', () => {
17 const ReactFeatureFlags = require('shared/ReactFeatureFlags');
18
19 function normalizeCodeLocInfo(str) {
20 - return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
20 + return (
21 + str &&
22 + str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
23 + return '\n in ' + name + ' (at **)';
24 + })
25 + );
26 }
27
28 beforeEach(() => {
@@ -1719,16 +1724,26 @@ describe('ReactDOMComponent', () => {
1724 <tr />
1725 </div>,
1726 );
1722 - }).toErrorDev([
1723 - 'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1724 - '<div>.' +
1725 - '\n in tr (at **)' +
1726 - '\n in div (at **)',
1727 - 'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1728 - '<div>.' +
1729 - '\n in tr (at **)' +
1730 - '\n in div (at **)',
1731 - ]);
1727 + }).toErrorDev(
1728 + ReactFeatureFlags.enableComponentStackLocations
1729 + ? [
1730 + // This warning dedupes since they're in the same component.
1731 + 'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1732 + '<div>.' +
1733 + '\n in tr (at **)' +
1734 + '\n in div (at **)',
1735 + ]
1736 + : [
1737 + 'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1738 + '<div>.' +
1739 + '\n in tr (at **)' +
1740 + '\n in div (at **)',
1741 + 'Warning: validateDOMNesting(...): <tr> cannot appear as a child of ' +
1742 + '<div>.' +
1743 + '\n in tr (at **)' +
1744 + '\n in div (at **)',
1745 + ],
1746 + );
1747 });
1748
1749 it('warns on invalid nesting at root', () => {
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.internal.js
+2 -2
@@ -1097,7 +1097,7 @@ describe('ReactDOMServerHooks', () => {
1097
1098 it('useOpaqueIdentifier: ID is not used during hydration but is used in an update', async () => {
1099 let _setShow;
1100 - function App() {
1100 + function App({unused}) {
1101 Scheduler.unstable_yieldValue('App');
1102 const id = useOpaqueIdentifier();
1103 const [show, setShow] = useState(false);
@@ -1129,7 +1129,7 @@ describe('ReactDOMServerHooks', () => {
1129
1130 it('useOpaqueIdentifier: ID is not used during hydration but is used in an update in legacy', async () => {
1131 let _setShow;
1132 - function App() {
1132 + function App({unused}) {
1133 Scheduler.unstable_yieldValue('App');
1134 const id = useOpaqueIdentifier();
1135 const [show, setShow] = useState(false);
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+4 -4
@@ -497,7 +497,7 @@ describe('ReactErrorBoundaries', () => {
497 }
498 };
499
500 - BrokenUseEffect = props => {
500 + BrokenUseEffect = ({children}) => {
501 Scheduler.unstable_yieldValue('BrokenUseEffect render');
502
503 React.useEffect(() => {
@@ -505,10 +505,10 @@ describe('ReactErrorBoundaries', () => {
505 throw new Error('Hello');
506 });
507
508 - return props.children;
508 + return children;
509 };
510
511 - BrokenUseLayoutEffect = props => {
511 + BrokenUseLayoutEffect = ({children}) => {
512 Scheduler.unstable_yieldValue('BrokenUseLayoutEffect render');
513
514 React.useLayoutEffect(() => {
@@ -518,7 +518,7 @@ describe('ReactErrorBoundaries', () => {
518 throw new Error('Hello');
519 });
520
521 - return props.children;
521 + return children;
522 };
523
524 NoopErrorBoundary = class extends React.Component {
packages/react-dom/src/__tests__/ReactServerRendering-test.js
+6 -1
@@ -18,7 +18,12 @@ const enableSuspenseServerRenderer = require('shared/ReactFeatureFlags')
18 .enableSuspenseServerRenderer;
19
20 function normalizeCodeLocInfo(str) {
21 - return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
21 + return (
22 + str &&
23 + str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
24 + return '\n in ' + name + ' (at **)';
25 + })
26 + );
27 }
28
29 describe('ReactDOMServer', () => {
packages/react-dom/src/__tests__/ReactUpdates-test.js
+1 -1
@@ -1615,7 +1615,7 @@ describe('ReactUpdates', () => {
1615 Scheduler.unstable_clearYields();
1616 }
1617 expect(error).toContain('Warning: Maximum update depth exceeded.');
1618 - expect(stack).toContain('in NonTerminating');
1618 + expect(stack).toContain(' NonTerminating');
1619 // rethrow error to prevent going into an infinite loop when act() exits
1620 throw error;
1621 });
packages/react-native-renderer/src/__tests__/ReactNativeError-test.internal.js
+12 -16
@@ -16,7 +16,12 @@ let createReactNativeComponentClass;
16 let computeComponentStackForErrorReporting;
17
18 function normalizeCodeLocInfo(str) {
19 - return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
19 + return (
20 + str &&
21 + str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
22 + return '\n in ' + name + ' (at **)';
23 + })
24 + );
25 }
26
27 describe('ReactNativeError', () => {
@@ -74,20 +79,11 @@ describe('ReactNativeError', () => {
79 computeComponentStackForErrorReporting(reactTag),
80 );
81
77 - if (__DEV__) {
78 - expect(componentStack).toBe(
79 - '\n' +
80 - ' in View (at **)\n' +
81 - ' in FunctionComponent (at **)\n' +
82 - ' in ClassComponent (at **)',
83 - );
84 - } else {
85 - expect(componentStack).toBe(
86 - '\n' +
87 - ' in View\n' +
88 - ' in FunctionComponent\n' +
89 - ' in ClassComponent',
90 - );
91 - }
82 + expect(componentStack).toBe(
83 + '\n' +
84 + ' in View (at **)\n' +
85 + ' in FunctionComponent (at **)\n' +
86 + ' in ClassComponent (at **)',
87 + );
88 });
89 });
packages/react-reconciler/src/__tests__/ReactFragment-test.js
+18 -6
@@ -10,6 +10,7 @@
10 'use strict';
11
12 let React;
13 +let ReactFeatureFlags;
14 let ReactNoop;
15 let Scheduler;
16
@@ -18,6 +19,7 @@ describe('ReactFragment', () => {
19 jest.resetModules();
20
21 React = require('react');
22 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
23 ReactNoop = require('react-noop-renderer');
24 Scheduler = require('scheduler');
25 });
@@ -900,17 +902,27 @@ describe('ReactFragment', () => {
902 );
903
904 ReactNoop.render(<Foo condition={false} />);
903 - expect(() => expect(Scheduler).toFlushWithoutYielding()).toErrorDev(
904 - 'Each child in a list should have a unique "key" prop.',
905 - );
905 + if (ReactFeatureFlags.enableComponentStackLocations) {
906 + // The key warning gets deduped because it's in the same component.
907 + expect(Scheduler).toFlushWithoutYielding();
908 + } else {
909 + expect(() => expect(Scheduler).toFlushWithoutYielding()).toErrorDev(
910 + 'Each child in a list should have a unique "key" prop.',
911 + );
912 + }
913
914 expect(ops).toEqual(['Update Stateful']);
915 expect(ReactNoop.getChildren()).toEqual([span(), div()]);
916
917 ReactNoop.render(<Foo condition={true} />);
911 - expect(() => expect(Scheduler).toFlushWithoutYielding()).toErrorDev(
912 - 'Each child in a list should have a unique "key" prop.',
913 - );
918 + if (ReactFeatureFlags.enableComponentStackLocations) {
919 + // The key warning gets deduped because it's in the same component.
920 + expect(Scheduler).toFlushWithoutYielding();
921 + } else {
922 + expect(() => expect(Scheduler).toFlushWithoutYielding()).toErrorDev(
923 + 'Each child in a list should have a unique "key" prop.',
924 + );
925 + }
926
927 expect(ops).toEqual(['Update Stateful', 'Update Stateful']);
928 expect(ReactNoop.getChildren()).toEqual([span(), div()]);
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+48 -45
@@ -10,8 +10,8 @@
10
11 'use strict';
12
13 +let ReactFeatureFlags = require('shared/ReactFeatureFlags');
14 let PropTypes;
14 -let ReactFeatureFlags;
15 let React;
16 let ReactNoop;
17 let Scheduler;
@@ -37,7 +37,12 @@ describe('ReactIncrementalErrorHandling', () => {
37 }
38
39 function normalizeCodeLocInfo(str) {
40 - return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
40 + return (
41 + str &&
42 + str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
43 + return '\n in ' + name + ' (at **)';
44 + })
45 + );
46 }
47
48 it('recovers from errors asynchronously', () => {
@@ -57,17 +62,17 @@ describe('ReactIncrementalErrorHandling', () => {
62 }
63 }
64
60 - function ErrorMessage(props) {
65 + function ErrorMessage({error}) {
66 Scheduler.unstable_yieldValue('ErrorMessage');
62 - return <span prop={`Caught an error: ${props.error.message}`} />;
67 + return <span prop={`Caught an error: ${error.message}`} />;
68 }
69
65 - function Indirection(props) {
70 + function Indirection({children}) {
71 Scheduler.unstable_yieldValue('Indirection');
67 - return props.children || null;
72 + return children || null;
73 }
74
70 - function BadRender() {
75 + function BadRender({unused}) {
76 Scheduler.unstable_yieldValue('throw');
77 throw new Error('oops!');
78 }
@@ -151,17 +156,17 @@ describe('ReactIncrementalErrorHandling', () => {
156 }
157 }
158
154 - function ErrorMessage(props) {
159 + function ErrorMessage({error}) {
160 Scheduler.unstable_yieldValue('ErrorMessage');
156 - return <span prop={`Caught an error: ${props.error.message}`} />;
161 + return <span prop={`Caught an error: ${error.message}`} />;
162 }
163
159 - function Indirection(props) {
164 + function Indirection({children}) {
165 Scheduler.unstable_yieldValue('Indirection');
161 - return props.children || null;
166 + return children || null;
167 }
168
164 - function BadRender() {
169 + function BadRender({unused}) {
170 Scheduler.unstable_yieldValue('throw');
171 throw new Error('oops!');
172 }
@@ -341,17 +346,17 @@ describe('ReactIncrementalErrorHandling', () => {
346 });
347
348 it('retries one more time before handling error', () => {
344 - function BadRender() {
349 + function BadRender({unused}) {
350 Scheduler.unstable_yieldValue('BadRender');
351 throw new Error('oops');
352 }
353
349 - function Sibling() {
354 + function Sibling({unused}) {
355 Scheduler.unstable_yieldValue('Sibling');
356 return <span prop="Sibling" />;
357 }
358
354 - function Parent() {
359 + function Parent({unused}) {
360 Scheduler.unstable_yieldValue('Parent');
361 return (
362 <>
@@ -381,7 +386,7 @@ describe('ReactIncrementalErrorHandling', () => {
386 });
387
388 it('retries one more time if an error occurs during a render that expires midway through the tree', () => {
384 - function Oops() {
389 + function Oops({unused}) {
390 Scheduler.unstable_yieldValue('Oops');
391 throw new Error('Oops');
392 }
@@ -391,7 +396,7 @@ describe('ReactIncrementalErrorHandling', () => {
396 return text;
397 }
398
394 - function App() {
399 + function App({unused}) {
400 return (
401 <>
402 <Text text="A" />
@@ -525,7 +530,7 @@ describe('ReactIncrementalErrorHandling', () => {
530 }
531 }
532
528 - function BrokenRender(props) {
533 + function BrokenRender({unused}) {
534 Scheduler.unstable_yieldValue('BrokenRender');
535 throw new Error('Hello');
536 }
@@ -571,7 +576,7 @@ describe('ReactIncrementalErrorHandling', () => {
576 }
577 }
578
574 - function BrokenRender(props) {
579 + function BrokenRender({unused}) {
580 Scheduler.unstable_yieldValue('BrokenRender');
581 throw new Error('Hello');
582 }
@@ -618,7 +623,7 @@ describe('ReactIncrementalErrorHandling', () => {
623 }
624 }
625
621 - function BrokenRender(props) {
626 + function BrokenRender({unused}) {
627 Scheduler.unstable_yieldValue('BrokenRender');
628 throw new Error('Hello');
629 }
@@ -659,7 +664,7 @@ describe('ReactIncrementalErrorHandling', () => {
664 }
665 }
666
662 - function BrokenRender() {
667 + function BrokenRender({unused}) {
668 Scheduler.unstable_yieldValue('BrokenRender');
669 throw new Error('Hello');
670 }
@@ -698,7 +703,7 @@ describe('ReactIncrementalErrorHandling', () => {
703 }
704 }
705
701 - function BrokenRender() {
706 + function BrokenRender({unused}) {
707 Scheduler.unstable_yieldValue('BrokenRender');
708 throw new Error('Hello');
709 }
@@ -739,7 +744,7 @@ describe('ReactIncrementalErrorHandling', () => {
744 }
745 }
746
742 - function BrokenRender() {
747 + function BrokenRender({unused}) {
748 Scheduler.unstable_yieldValue('BrokenRender');
749 throw new Error('Hello');
750 }
@@ -779,7 +784,7 @@ describe('ReactIncrementalErrorHandling', () => {
784 }
785 }
786
782 - function BrokenRender() {
787 + function BrokenRender({unused}) {
788 Scheduler.unstable_yieldValue('BrokenRender');
789 throw new Error('Hello');
790 }
@@ -857,12 +862,12 @@ describe('ReactIncrementalErrorHandling', () => {
862 });
863
864 it('can schedule updates after uncaught error in render on mount', () => {
860 - function BrokenRender() {
865 + function BrokenRender({unused}) {
866 Scheduler.unstable_yieldValue('BrokenRender');
867 throw new Error('Hello');
868 }
869
865 - function Foo() {
870 + function Foo({unused}) {
871 Scheduler.unstable_yieldValue('Foo');
872 return null;
873 }
@@ -882,24 +887,24 @@ describe('ReactIncrementalErrorHandling', () => {
887 });
888
889 it('can schedule updates after uncaught error in render on update', () => {
885 - function BrokenRender(props) {
890 + function BrokenRender({shouldThrow}) {
891 Scheduler.unstable_yieldValue('BrokenRender');
887 - if (props.throw) {
892 + if (shouldThrow) {
893 throw new Error('Hello');
894 }
895 return null;
896 }
897
893 - function Foo() {
898 + function Foo({unused}) {
899 Scheduler.unstable_yieldValue('Foo');
900 return null;
901 }
902
898 - ReactNoop.render(<BrokenRender throw={false} />);
903 + ReactNoop.render(<BrokenRender shouldThrow={false} />);
904 expect(Scheduler).toFlushAndYield(['BrokenRender']);
905
906 expect(() => {
902 - ReactNoop.render(<BrokenRender throw={true} />);
907 + ReactNoop.render(<BrokenRender shouldThrow={true} />);
908 expect(Scheduler).toFlushWithoutYielding();
909 }).toThrow('Hello');
910 expect(Scheduler).toHaveYielded([
@@ -1449,13 +1454,13 @@ describe('ReactIncrementalErrorHandling', () => {
1454 }
1455 }
1456
1452 - function Indirection(props) {
1457 + function Indirection({children}) {
1458 Scheduler.unstable_yieldValue('Indirection');
1454 - return props.children;
1459 + return children;
1460 }
1461
1462 const notAnError = {nonStandardMessage: 'oops'};
1458 - function BadRender() {
1463 + function BadRender({unused}) {
1464 Scheduler.unstable_yieldValue('BadRender');
1465 throw notAnError;
1466 }
@@ -1514,17 +1519,17 @@ describe('ReactIncrementalErrorHandling', () => {
1519 }
1520 }
1521
1517 - function ErrorMessage(props) {
1522 + function ErrorMessage({error}) {
1523 Scheduler.unstable_yieldValue('ErrorMessage');
1519 - return <span prop={`Caught an error: ${props.error.message}`} />;
1524 + return <span prop={`Caught an error: ${error.message}`} />;
1525 }
1526
1522 - function BadRenderSibling(props) {
1527 + function BadRenderSibling({unused}) {
1528 Scheduler.unstable_yieldValue('BadRenderSibling');
1529 return null;
1530 }
1531
1527 - function BadRender() {
1532 + function BadRender({unused}) {
1533 Scheduler.unstable_yieldValue('throw');
1534 throw new Error('oops!');
1535 }
@@ -1562,7 +1567,7 @@ describe('ReactIncrementalErrorHandling', () => {
1567 // This test seems a bit contrived, but it's based on an actual regression
1568 // where we checked for the existence of didUpdate instead of didMount, and
1569 // didMount was not defined.
1565 - function BadRender() {
1570 + function BadRender({unused}) {
1571 Scheduler.unstable_yieldValue('throw');
1572 throw new Error('oops!');
1573 }
@@ -1633,10 +1638,8 @@ describe('ReactIncrementalErrorHandling', () => {
1638 expect(ReactNoop.getChildren()).toEqual([
1639 span(
1640 'Caught an error:\n' +
1636 - (__DEV__
1637 - ? ' in BrokenRender (at **)\n'
1638 - : ' in BrokenRender\n') +
1639 - (__DEV__ ? ' in ErrorBoundary (at **).' : ' in ErrorBoundary.'),
1641 + ' in BrokenRender (at **)\n' +
1642 + ' in ErrorBoundary (at **).',
1643 ),
1644 ]);
1645 });
@@ -1669,7 +1672,7 @@ describe('ReactIncrementalErrorHandling', () => {
1672 expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello')]);
1673 });
1674
1672 - if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
1675 + if (!ReactFeatureFlags.disableModulePatternComponents) {
1676 it('handles error thrown inside getDerivedStateFromProps of a module-style context provider', () => {
1677 function Provider() {
1678 return {
@@ -1708,7 +1711,7 @@ describe('ReactIncrementalErrorHandling', () => {
1711 it('uncaught errors should be discarded if the render is aborted', async () => {
1712 const root = ReactNoop.createRoot();
1713
1711 - function Oops() {
1714 + function Oops({unused}) {
1715 Scheduler.unstable_yieldValue('Oops');
1716 throw Error('Oops');
1717 }
packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js
+11 -11
@@ -59,9 +59,9 @@ describe('ReactIncrementalErrorLogging', () => {
59 ? expect.stringMatching(
60 new RegExp(
61 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
62 - '\\s+in ErrorThrowingComponent (.*)\n' +
63 - '\\s+in span (.*)\n' +
64 - '\\s+in div (.*)\n\n' +
62 + '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
63 + '\\s+(in|at) span(.*)\n' +
64 + '\\s+(in|at) div(.*)\n\n' +
65 'Consider adding an error boundary to your tree ' +
66 'to customize error handling behavior\\.',
67 ),
@@ -95,9 +95,9 @@ describe('ReactIncrementalErrorLogging', () => {
95 ? expect.stringMatching(
96 new RegExp(
97 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
98 - '\\s+in ErrorThrowingComponent (.*)\n' +
99 - '\\s+in span (.*)\n' +
100 - '\\s+in div (.*)\n\n' +
98 + '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
99 + '\\s+(in|at) span(.*)\n' +
100 + '\\s+(in|at) div(.*)\n\n' +
101 'Consider adding an error boundary to your tree ' +
102 'to customize error handling behavior\\.',
103 ),
@@ -134,9 +134,9 @@ describe('ReactIncrementalErrorLogging', () => {
134 ? expect.stringMatching(
135 new RegExp(
136 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
137 - '\\s+in ErrorThrowingComponent (.*)\n' +
138 - '\\s+in span (.*)\n' +
139 - '\\s+in div (.*)\n\n' +
137 + '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
138 + '\\s+(in|at) span(.*)\n' +
139 + '\\s+(in|at) div(.*)\n\n' +
140 'Consider adding an error boundary to your tree ' +
141 'to customize error handling behavior\\.',
142 ),
@@ -206,8 +206,8 @@ describe('ReactIncrementalErrorLogging', () => {
206 ? expect.stringMatching(
207 new RegExp(
208 'The above error occurred in the <Foo> component:\n' +
209 - '\\s+in Foo (.*)\n' +
210 - '\\s+in ErrorBoundary (.*)\n\n' +
209 + '\\s+(in|at) Foo (.*)\n' +
210 + '\\s+(in|at) ErrorBoundary (.*)\n\n' +
211 'React will try to recreate this component tree from scratch ' +
212 'using the error boundary you provided, ErrorBoundary.',
213 ),
packages/react/src/ReactDebugCurrentFrame.js
+7 -18
@@ -7,26 +7,20 @@
7 * @flow
8 */
9
10 -import type {ReactElement} from 'shared/ReactElementType';
11 -
12 -import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame';
13 -
10 const ReactDebugCurrentFrame = {};
11
16 -let currentlyValidatingElement = (null: null | ReactElement);
12 +let currentExtraStackFrame = (null: null | string);
13
18 -export function setCurrentlyValidatingElement(element: null | ReactElement) {
14 +export function setExtraStackFrame(stack: null | string) {
15 if (__DEV__) {
20 - currentlyValidatingElement = element;
16 + currentExtraStackFrame = stack;
17 }
18 }
19
20 if (__DEV__) {
25 - ReactDebugCurrentFrame.setCurrentlyValidatingElement = function(
26 - element: null | ReactElement,
27 - ) {
21 + ReactDebugCurrentFrame.setExtraStackFrame = function(stack: null | string) {
22 if (__DEV__) {
29 - currentlyValidatingElement = element;
23 + currentExtraStackFrame = stack;
24 }
25 };
26 // Stack implementation injected by the current renderer.
@@ -36,13 +30,8 @@ if (__DEV__) {
30 let stack = '';
31
32 // Add an extra top frame while an element is being validated
39 - if (currentlyValidatingElement) {
40 - const owner = currentlyValidatingElement._owner;
41 - stack += describeUnknownElementTypeFrameInDEV(
42 - currentlyValidatingElement.type,
43 - currentlyValidatingElement._source,
44 - owner ? owner.type : null,
45 - );
33 + if (currentExtraStackFrame) {
34 + stack += currentExtraStackFrame;
35 }
36
37 // Delegate to the injected renderer-specific implementation
packages/react/src/ReactElementValidator.js
+20 -3
@@ -31,7 +31,24 @@ import {
31 cloneElement,
32 jsxDEV,
33 } from './ReactElement';
34 -import {setCurrentlyValidatingElement} from './ReactDebugCurrentFrame';
34 +import {setExtraStackFrame} from './ReactDebugCurrentFrame';
35 +import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame';
36 +
37 +function setCurrentlyValidatingElement(element) {
38 + if (__DEV__) {
39 + if (element) {
40 + const owner = element._owner;
41 + const stack = describeUnknownElementTypeFrameInDEV(
42 + element.type,
43 + element._source,
44 + owner ? owner.type : null,
45 + );
46 + setExtraStackFrame(stack);
47 + } else {
48 + setExtraStackFrame(null);
49 + }
50 + }
51 +}
52
53 let propTypesMisspellWarningShown;
54
@@ -127,16 +144,16 @@ function validateExplicitKey(element, parentType) {
144 )}.`;
145 }
146
130 - setCurrentlyValidatingElement(element);
147 if (__DEV__) {
148 + setCurrentlyValidatingElement(element);
149 console.error(
150 'Each child in a list should have a unique "key" prop.' +
151 '%s%s See https://fb.me/react-warning-keys for more information.',
152 currentComponentErrorInfo,
153 childOwner,
154 );
155 + setCurrentlyValidatingElement(null);
156 }
139 - setCurrentlyValidatingElement(null);
157 }
158
159 /**
packages/react/src/__tests__/ReactElementClone-test.js
+2 -2
@@ -301,8 +301,8 @@ describe('ReactElementClone', () => {
301 'Warning: Failed prop type: ' +
302 'Invalid prop `color` of type `number` supplied to `Component`, ' +
303 'expected `string`.\n' +
304 - ' in Component (created by GrandParent)\n' +
305 - ' in Parent (created by GrandParent)\n' +
304 + ' in Component (at **)\n' +
305 + ' in Parent (at **)\n' +
306 ' in GrandParent',
307 );
308 });
packages/react/src/__tests__/ReactElementJSX-test.js
+2 -2
@@ -364,8 +364,8 @@ describe('ReactElement.jsx', () => {
364 ).toErrorDev(
365 'Warning: Each child in a list should have a unique "key" prop.\n\n' +
366 'Check the render method of `Parent`. See https://fb.me/react-warning-keys for more information.\n' +
367 - ' in Child (created by Parent)\n' +
368 - ' in Parent',
367 + ' in Child (at **)\n' +
368 + ' in Parent (at **)',
369 );
370 });
371
packages/react/src/__tests__/ReactElementValidator-test.internal.js
+2 -2
@@ -227,8 +227,8 @@ describe('ReactElementValidator', () => {
227 'Warning: Failed prop type: ' +
228 'Invalid prop `color` of type `number` supplied to `MyComp`, ' +
229 'expected `string`.\n' +
230 - ' in MyComp (created by ParentComp)\n' +
231 - ' in ParentComp',
230 + ' in MyComp (at **)\n' +
231 + ' in ParentComp (at **)',
232 );
233 });
234
packages/react/src/__tests__/ReactProfiler-test.internal.js
+2 -2
@@ -1041,7 +1041,7 @@ describe('Profiler', () => {
1041 it('should accumulate actual time after an error handled by componentDidCatch()', () => {
1042 const callback = jest.fn();
1043
1044 - const ThrowsError = () => {
1044 + const ThrowsError = ({unused}) => {
1045 Scheduler.unstable_advanceTime(3);
1046 throw Error('expected error');
1047 };
@@ -1120,7 +1120,7 @@ describe('Profiler', () => {
1120 it('should accumulate actual time after an error handled by getDerivedStateFromError()', () => {
1121 const callback = jest.fn();
1122
1123 - const ThrowsError = () => {
1123 + const ThrowsError = ({unused}) => {
1124 Scheduler.unstable_advanceTime(10);
1125 throw Error('expected error');
1126 };
packages/react/src/__tests__/forwardRef-test.internal.js
+2 -1
@@ -159,8 +159,9 @@ describe('forwardRef', () => {
159 }
160
161 function Wrapper(props) {
162 + const forwardedRef = props.forwardedRef;
163 Scheduler.unstable_yieldValue('Wrapper');
163 - return <BadRender {...props} ref={props.forwardedRef} />;
164 + return <BadRender {...props} ref={forwardedRef} />;
165 }
166
167 const RefForwardingComponent = React.forwardRef((props, ref) => (
packages/react/src/jsx/ReactJSXElementValidator.js
+25 -6
@@ -24,11 +24,30 @@ import {
24 import {warnAboutSpreadingKeyToJSX} from 'shared/ReactFeatureFlags';
25
26 import {jsxDEV} from './ReactJSXElement';
27 +
28 +import {describeUnknownElementTypeFrameInDEV} from 'shared/ReactComponentStackFrame';
29 +
30 import ReactSharedInternals from 'shared/ReactSharedInternals';
31
32 const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
33 const ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame;
34
35 +function setCurrentlyValidatingElement(element) {
36 + if (__DEV__) {
37 + if (element) {
38 + const owner = element._owner;
39 + const stack = describeUnknownElementTypeFrameInDEV(
40 + element.type,
41 + element._source,
42 + owner ? owner.type : null,
43 + );
44 + ReactDebugCurrentFrame.setExtraStackFrame(stack);
45 + } else {
46 + ReactDebugCurrentFrame.setExtraStackFrame(null);
47 + }
48 + }
49 +}
50 +
51 let propTypesMisspellWarningShown;
52
53 if (__DEV__) {
@@ -140,14 +159,14 @@ function validateExplicitKey(element, parentType) {
159 )}.`;
160 }
161
143 - ReactDebugCurrentFrame.setCurrentlyValidatingElement(element);
162 + setCurrentlyValidatingElement(element);
163 console.error(
164 'Each child in a list should have a unique "key" prop.' +
165 '%s%s See https://fb.me/react-warning-keys for more information.',
166 currentComponentErrorInfo,
167 childOwner,
168 );
150 - ReactDebugCurrentFrame.setCurrentlyValidatingElement(null);
169 + setCurrentlyValidatingElement(null);
170 }
171 }
172
@@ -224,9 +243,9 @@ function validatePropTypes(element) {
243 return;
244 }
245 if (propTypes) {
227 - ReactDebugCurrentFrame.setCurrentlyValidatingElement(element);
246 + setCurrentlyValidatingElement(element);
247 checkPropTypes(propTypes, element.props, 'prop', name);
229 - ReactDebugCurrentFrame.setCurrentlyValidatingElement(null);
248 + setCurrentlyValidatingElement(null);
249 } else if (type.PropTypes !== undefined && !propTypesMisspellWarningShown) {
250 propTypesMisspellWarningShown = true;
251 console.error(
@@ -252,7 +271,7 @@ function validatePropTypes(element) {
271 */
272 function validateFragmentProps(fragment) {
273 if (__DEV__) {
255 - ReactDebugCurrentFrame.setCurrentlyValidatingElement(fragment);
274 + setCurrentlyValidatingElement(fragment);
275
276 const keys = Object.keys(fragment.props);
277 for (let i = 0; i < keys.length; i++) {
@@ -271,7 +290,7 @@ function validateFragmentProps(fragment) {
290 console.error('Invalid attribute `ref` supplied to `React.Fragment`.');
291 }
292
274 - ReactDebugCurrentFrame.setCurrentlyValidatingElement(null);
293 + setCurrentlyValidatingElement(null);
294 }
295 }
296
packages/shared/ConsolePatchingDev.js
+31 -18
@@ -12,6 +12,7 @@
12 // lazily which won't cover if the log function was extracted eagerly.
13 // We could also eagerly patch the method.
14
15 +let disabledDepth = 0;
16 let prevLog;
17 let prevInfo;
18 let prevWarn;
@@ -21,28 +22,40 @@ function disabledLog() {}
22
23 export function disableLogs(): void {
24 if (__DEV__) {
24 - /* eslint-disable react-internal/no-production-logging */
25 - prevLog = console.log;
26 - prevInfo = console.info;
27 - prevWarn = console.warn;
28 - prevError = console.error;
29 - // $FlowFixMe Flow thinks console is immutable.
30 - console.log = console.info = console.warn = console.error = disabledLog;
31 - /* eslint-enable react-internal/no-production-logging */
25 + if (disabledDepth === 0) {
26 + /* eslint-disable react-internal/no-production-logging */
27 + prevLog = console.log;
28 + prevInfo = console.info;
29 + prevWarn = console.warn;
30 + prevError = console.error;
31 + // $FlowFixMe Flow thinks console is immutable.
32 + console.log = console.info = console.warn = console.error = disabledLog;
33 + /* eslint-enable react-internal/no-production-logging */
34 + }
35 + disabledDepth++;
36 }
37 }
38
39 export function reenableLogs(): void {
40 if (__DEV__) {
37 - /* eslint-disable react-internal/no-production-logging */
38 - // $FlowFixMe Flow thinks console is immutable.
39 - console.log = prevLog;
40 - // $FlowFixMe Flow thinks console is immutable.
41 - console.info = prevInfo;
42 - // $FlowFixMe Flow thinks console is immutable.
43 - console.warn = prevWarn;
44 - // $FlowFixMe Flow thinks console is immutable.
45 - console.error = prevError;
46 - /* eslint-enable react-internal/no-production-logging */
41 + disabledDepth--;
42 + if (disabledDepth === 0) {
43 + /* eslint-disable react-internal/no-production-logging */
44 + // $FlowFixMe Flow thinks console is immutable.
45 + console.log = prevLog;
46 + // $FlowFixMe Flow thinks console is immutable.
47 + console.info = prevInfo;
48 + // $FlowFixMe Flow thinks console is immutable.
49 + console.warn = prevWarn;
50 + // $FlowFixMe Flow thinks console is immutable.
51 + console.error = prevError;
52 + /* eslint-enable react-internal/no-production-logging */
53 + }
54 + if (disabledDepth < 0) {
55 + console.error(
56 + 'disabledDepth fell below zero. ' +
57 + 'This is a bug in React. Please file an issue.',
58 + );
59 + }
60 }
61 }
packages/shared/ReactComponentStackFrame.js
+168 -24
@@ -10,6 +10,8 @@
10 import type {Source} from 'shared/ReactElementType';
11 import type {LazyComponent} from 'react/src/ReactLazy';
12
13 +import {enableComponentStackLocations} from 'shared/ReactFeatureFlags';
14 +
15 import {
16 REACT_SUSPENSE_TYPE,
17 REACT_SUSPENSE_LIST_TYPE,
@@ -19,6 +21,151 @@ import {
21 REACT_LAZY_TYPE,
22 } from 'shared/ReactSymbols';
23
24 +import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
25 +
26 +import ReactSharedInternals from 'shared/ReactSharedInternals';
27 +
28 +const {ReactCurrentDispatcher} = ReactSharedInternals;
29 +
30 +let prefix;
31 +export function describeBuiltInComponentFrame(
32 + name: string,
33 + source: void | null | Source,
34 + ownerFn: void | null | Function,
35 +): string {
36 + if (enableComponentStackLocations) {
37 + if (prefix === undefined) {
38 + // Extract the VM specific prefix used by each line.
39 + const match = Error()
40 + .stack.trim()
41 + .match(/\n( *(at )?)/);
42 + prefix = (match && match[1]) || '';
43 + }
44 + // We use the prefix to ensure our stacks line up with native stack frames.
45 + return '\n' + prefix + name;
46 + } else {
47 + let ownerName = null;
48 + if (__DEV__ && ownerFn) {
49 + ownerName = ownerFn.displayName || ownerFn.name || null;
50 + }
51 + return describeComponentFrame(name, source, ownerName);
52 + }
53 +}
54 +
55 +let reentry = false;
56 +let componentFrameCache;
57 +if (__DEV__) {
58 + const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
59 + componentFrameCache = new PossiblyWeakMap();
60 +}
61 +
62 +export function describeNativeComponentFrame(
63 + fn: Function,
64 + construct: boolean,
65 +): string {
66 + // If something asked for a stack inside a fake render, it should get ignored.
67 + if (!fn || reentry) {
68 + return '';
69 + }
70 +
71 + if (__DEV__) {
72 + const frame = componentFrameCache.get(fn);
73 + if (frame !== undefined) {
74 + return frame;
75 + }
76 + }
77 +
78 + const control = Error();
79 +
80 + reentry = true;
81 + let previousDispatcher;
82 + if (__DEV__) {
83 + previousDispatcher = ReactCurrentDispatcher.current;
84 + // Set the dispatcher in DEV because this might be call in the render function
85 + // for warnings.
86 + ReactCurrentDispatcher.current = null;
87 + disableLogs();
88 + }
89 + try {
90 + // This should throw.
91 + if (construct) {
92 + // Something should be setting the props in the constructor.
93 + const Fake = function() {};
94 + // $FlowFixMe
95 + Object.defineProperty(Fake.prototype, 'props', {
96 + set: function() {
97 + // We use a throwing setter instead of frozen or non-writable props
98 + // because that won't throw in a non-strict mode function.
99 + throw Error();
100 + },
101 + });
102 + if (typeof Reflect === 'object' && Reflect.construct) {
103 + Reflect.construct(fn, [], Fake);
104 + } else {
105 + fn.call(new Fake());
106 + }
107 + } else {
108 + fn();
109 + }
110 + } catch (sample) {
111 + // This is inlined manually because closure doesn't do it for us.
112 + if (sample && typeof sample.stack === 'string') {
113 + // This extracts the first frame from the sample that isn't also in the control.
114 + // Skipping one frame that we assume is the frame that calls the two.
115 + const sampleLines = sample.stack.split('\n');
116 + const controlLines = control.stack.split('\n');
117 + let s = sampleLines.length - 1;
118 + let c = controlLines.length - 1;
119 + while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
120 + // We expect at least one stack frame to be shared.
121 + // Typically this will be the root most one. However, stack frames may be
122 + // cut off due to maximum stack limits. In this case, one maybe cut off
123 + // earlier than the other. We assume that the sample is longer or the same
124 + // and there for cut off earlier. So we should find the root most frame in
125 + // the sample somewhere in the control.
126 + c--;
127 + }
128 + for (; s >= 1 && c >= 0; s--, c--) {
129 + // Next we find the first one that isn't the same which should be the
130 + // frame that called our sample function.
131 + if (sampleLines[s] !== controlLines[c]) {
132 + // In V8, the first line is describing the message but other VMs don't.
133 + // If we're about to return the first line, and the control is also on the same
134 + // line, that's a pretty good indicator that our sample threw at same line as
135 + // the control. I.e. before we entered the sample frame. So we ignore this result.
136 + // This can happen if you passed a class to function component, or non-function.
137 + if (s !== 1 || c !== 1) {
138 + // V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
139 + const frame = '\n' + sampleLines[s - 1].replace(' at new ', ' at ');
140 + if (__DEV__) {
141 + if (typeof fn === 'function') {
142 + componentFrameCache.set(fn, frame);
143 + }
144 + }
145 + // Return the line we found.
146 + return frame;
147 + }
148 + }
149 + }
150 + }
151 + } finally {
152 + reentry = false;
153 + if (__DEV__) {
154 + ReactCurrentDispatcher.current = previousDispatcher;
155 + reenableLogs();
156 + }
157 + }
158 + // Fallback to just using the name if we couldn't make it throw.
159 + const name = fn ? fn.displayName || fn.name : '';
160 + const syntheticFrame = name ? describeBuiltInComponentFrame(name) : '';
161 + if (__DEV__) {
162 + if (typeof fn === 'function') {
163 + componentFrameCache.set(fn, syntheticFrame);
164 + }
165 + }
166 + return syntheticFrame;
167 +}
168 +
169 const BEFORE_SLASH_RE = /^(.*)[\\\/]/;
170
171 function describeComponentFrame(
@@ -49,24 +196,16 @@ function describeComponentFrame(
196 return '\n in ' + (name || 'Unknown') + sourceInfo;
197 }
198
52 -export function describeBuiltInComponentFrame(
53 - name: string,
54 - source: void | null | Source,
55 - ownerFn: void | null | Function,
56 -): string {
57 - let ownerName = null;
58 - if (__DEV__ && ownerFn) {
59 - ownerName = ownerFn.displayName || ownerFn.name || null;
60 - }
61 - return describeComponentFrame(name, source, ownerName);
62 -}
63 -
199 export function describeClassComponentFrame(
200 ctor: Function,
201 source: void | null | Source,
202 ownerFn: void | null | Function,
203 ): string {
69 - return describeFunctionComponentFrame(ctor, source, ownerFn);
204 + if (enableComponentStackLocations) {
205 + return describeNativeComponentFrame(ctor, true);
206 + } else {
207 + return describeFunctionComponentFrame(ctor, source, ownerFn);
208 + }
209 }
210
211 export function describeFunctionComponentFrame(
@@ -74,15 +213,19 @@ export function describeFunctionComponentFrame(
213 source: void | null | Source,
214 ownerFn: void | null | Function,
215 ): string {
77 - if (!fn) {
78 - return '';
79 - }
80 - const name = fn.displayName || fn.name || null;
81 - let ownerName = null;
82 - if (__DEV__ && ownerFn) {
83 - ownerName = ownerFn.displayName || ownerFn.name || null;
216 + if (enableComponentStackLocations) {
217 + return describeNativeComponentFrame(fn, false);
218 + } else {
219 + if (!fn) {
220 + return '';
221 + }
222 + const name = fn.displayName || fn.name || null;
223 + let ownerName = null;
224 + if (__DEV__ && ownerFn) {
225 + ownerName = ownerFn.displayName || ownerFn.name || null;
226 + }
227 + return describeComponentFrame(name, source, ownerName);
228 }
85 - return describeComponentFrame(name, source, ownerName);
229 }
230
231 function shouldConstruct(Component: Function) {
@@ -102,10 +245,11 @@ export function describeUnknownElementTypeFrameInDEV(
245 return '';
246 }
247 if (typeof type === 'function') {
105 - if (shouldConstruct(type)) {
106 - return describeClassComponentFrame(type, source, ownerFn);
248 + if (enableComponentStackLocations) {
249 + return describeNativeComponentFrame(type, shouldConstruct(type));
250 + } else {
251 + return describeFunctionComponentFrame(type, source, ownerFn);
252 }
108 - return describeFunctionComponentFrame(type, source, ownerFn);
253 }
254 if (typeof type === 'string') {
255 return describeBuiltInComponentFrame(type, source, ownerFn);
packages/shared/ReactFeatureFlags.js
+1 -1
@@ -101,7 +101,7 @@ export const deferPassiveEffectCleanupDuringUnmount = false;
101 // a deprecated pattern we want to get rid of in the future
102 export const warnAboutSpreadingKeyToJSX = false;
103
104 -export const enableComponentStackLocations = false;
104 +export const enableComponentStackLocations = __EXPERIMENTAL__;
105
106 // Internal-only attempt to debug a React Native issue. See D20130868.
107 export const throwEarlyForMysteriousError = false;
packages/shared/__tests__/describeComponentFrame-test.js
+6
@@ -11,6 +11,7 @@
11
12 let React;
13 let ReactDOM;
14 +const ReactFeatureFlags = require('shared/ReactFeatureFlags');
15
16 describe('Component stack trace displaying', () => {
17 beforeEach(() => {
@@ -18,6 +19,11 @@ describe('Component stack trace displaying', () => {
19 ReactDOM = require('react-dom');
20 });
21
22 + if (ReactFeatureFlags.enableComponentStackLocations) {
23 + it("empty test so Jest doesn't complain", () => {});
24 + return;
25 + }
26 +
27 it('should provide filenames in stack traces', () => {
28 class Component extends React.Component {
29 render() {
scripts/jest/matchers/toWarnDev.js
+13 -1
@@ -5,7 +5,19 @@ const util = require('util');
5 const shouldIgnoreConsoleError = require('../shouldIgnoreConsoleError');
6
7 function normalizeCodeLocInfo(str) {
8 - return str && str.replace(/at .+?:\d+/g, 'at **');
8 + if (typeof str !== 'string') {
9 + return str;
10 + }
11 + // This special case exists only for the special source location in
12 + // ReactElementValidator. That will go away if we remove source locations.
13 + str = str.replace(/Check your code at .+?:\d+/g, 'Check your code at **');
14 + // V8 format:
15 + // at Component (/path/filename.js:123:45)
16 + // React format:
17 + // in Component (at filename.js:123)
18 + return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
19 + return '\n in ' + name + ' (at **)';
20 + });
21 }
22
23 const createMatcherFor = (consoleMethod, matcherName) =>
scripts/rollup/validate/eslintrc.cjs.js
+1
@@ -14,6 +14,7 @@ module.exports = {
14 WeakMap: true,
15 WeakSet: true,
16 Uint16Array: true,
17 + Reflect: true,
18 // Vendor specific
19 MSApp: true,
20 __REACT_DEVTOOLS_GLOBAL_HOOK__: true,
scripts/rollup/validate/eslintrc.fb.js
+1
@@ -14,6 +14,7 @@ module.exports = {
14 WeakMap: true,
15 WeakSet: true,
16 Uint16Array: true,
17 + Reflect: true,
18 // Vendor specific
19 MSApp: true,
20 __REACT_DEVTOOLS_GLOBAL_HOOK__: true,
scripts/rollup/validate/eslintrc.rn.js
+1
@@ -13,6 +13,7 @@ module.exports = {
13 Proxy: true,
14 WeakMap: true,
15 WeakSet: true,
16 + Reflect: true,
17 // Vendor specific
18 MSApp: true,
19 __REACT_DEVTOOLS_GLOBAL_HOOK__: true,
scripts/rollup/validate/eslintrc.umd.js
+1
@@ -13,6 +13,7 @@ module.exports = {
13 WeakMap: true,
14 WeakSet: true,
15 Uint16Array: true,
16 + Reflect: true,
17 // Vendor specific
18 MSApp: true,
19 __REACT_DEVTOOLS_GLOBAL_HOOK__: true,