From 2810294260c9cdfd44daf69b509442213f207f65 Mon Sep 17 00:00:00 2001 From: Caleb Meredith Date: Wed, 25 Jul 2018 09:44:55 -0700 Subject: [PATCH] Fix React branch serialization issues (#2318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: After adding classes to my fuzzer (which does not use first render mode) I found #2316. This PR fixes #2316 and a related issue I found while working on that. (Each fix is in a separate commit.) The second issue I found is scarier since the compiler passes but we get invalid output. In the following example I observed an invariant violation: ```js const React = require("react"); __evaluatePureFunction(() => { function Tau(props) { return React.createElement( "div", null, React.createElement(Epsilon, { a: props.z, }), React.createElement(Zeta, { p: props.h, }) ); } class Epsilon extends React.Component { constructor(props) { super(props); this.state = {}; } render() { return React.createElement(Zeta, { p: this.props.a }); } } function Zeta(props) { return props.p ? null : React.createElement("foobar", null); } __optimizeReactComponentTree(Tau); module.exports = Tau; }); ``` ``` === serialized but not visited values === visited but not serialized values undefined, hash: 792057514635681 referenced by 1 scopes =>_resolveAbstractConditionalValue alternate(#75)=>ReactAdditionalFunctionEffects(#80) Invariant Violation: serialized 26 of 27 This is likely a bug in Prepack, not your code. Feel free to open an issue on GitHub. at invariant (/Users/calebmer/prepack/src/invariant.js:18:15) at ResidualHeapSerializer.serialize (/Users/calebmer/prepack/src/serializer/ResidualHeapSerializer.js:2465:17) at statistics.referenceCounts.measure (/Users/calebmer/prepack/src/serializer/serializer.js:259:15) at PerformanceTracker.measure (/Users/calebmer/prepack/src/statistics.js:100:14) at ast (/Users/calebmer/prepack/src/serializer/serializer.js:238:38) at statistics.total.measure (/Users/calebmer/prepack/src/serializer/serializer.js:169:17) at PerformanceTracker.measure (/Users/calebmer/prepack/src/statistics.js:100:14) at Serializer.init (/Users/calebmer/prepack/src/serializer/serializer.js:136:35) at prepackSources (/Users/calebmer/prepack/src/prepack-standalone.js:66:33) at compileSource (/Users/calebmer/prepack/scripts/debug-fb-www.js:92:18) ``` Somehow we were visiting `undefined`, but clearly we weren’t serializing it given the source code. Here’s what was happening: - We’d visit an additional function calling `withCleanEquivalenceSet`. - The additional function would enqueue an action which visited ``. - Later, we’d execute the `` visiting action outside our `withCleanEquivalenceSet` with our default equivalence set. - The same thing happens with our new root from our `Epsilon` class. - Except now some effects have been applied that set the `type` for our `` React element in our React equivalence set to `undefined`. Since when we created `` we modified its `type` property. (Recorded in `modifiedProperties`.) - Now our `` in `` and our `` in `` share the _exact same_ `` thanks to our equivalence set. - But `` has a `type` of `undefined` thanks to the effects we applied. We should be creating a new `` since we are in a new optimized function. - ***Boom!*** We visit `undefined`, but don’t serialize it since the same effects aren’t applied when we serialize. This test case caught an important flaw in our visiting logic, but it only manifested as an invariant under these very specific conditions. Which is a little scary. In a large example, like our internal bundle, we would of course serialize some `undefined` value but we would have still visited `undefined` instead of the proper type, _and_ we may consider two elements to be equivalent when we shouldn’t since their components may render independently. This issue (I presume) can also affect we bail out on since they create new trees inside the root tree. While debugging and fixing this issue, I found another with incorrect/suboptimal output that passes Prepack and passes eslint. Given the following input: ```js require("react"); __evaluatePureFunction(() => { const React = require("react"); function Tau(props) { return React.createElement( "a", null, React.createElement("b", null), React.createElement(Epsilon, null), React.createElement("c", null) ); } class Epsilon extends React.Component { constructor(props) { super(props); this.state = {}; } render() { return React.createElement("d", null); } } __optimizeReactComponentTree(Tau); module.exports = { Tau, Epsilon }; }); ``` We get this output: ```js (function () { var _$0 = require("react").Component; var _3 = function (props, context) { _6 === void 0 && $f_0(); _9 === void 0 && $f_1(); var _8 = <_B />; var _4 = {_6}{_8}{_9}; return _4; }; var _B = class extends _$0 { constructor(props) { super(props); this.state = {}; } render() { return _E; // <--------------------------- Incorrect. `_B` may be rendered outside of `_3`. } }; var $f_0 = function () { _6 = ; _E = ; }; var _6; var _E; var $f_1 = function () { _9 = ; }; var _9; var _0 = { Tau: _3, Epsilon: _B }; module.exports = _0; })(); ``` This happened because the React serializer’s `_lazilyHoistedNodes` logic was implemented in a way that doesn’t play well with the interleaving almost random order of the serializer invocation of React lazily hoisted nodes logic. Pull Request resolved: https://github.com/facebook/prepack/pull/2318 Differential Revision: D8992253 Pulled By: calebmer fbshipit-source-id: 4a75e5768ffb7887c3a8afa2a0f3f59e7eac266d --- src/serializer/ResidualHeapSerializer.js | 2 +- src/serializer/ResidualHeapVisitor.js | 24 +- .../ResidualReactElementSerializer.js | 37 +-- src/serializer/ResidualReactElementVisitor.js | 34 +++ test/react/ClassComponents-test.js | 8 + .../complex-class-proper-hoisting.js | 43 +++ .../complex-class-with-equivalent-node.js | 51 ++++ .../ClassComponents-test.js.snap | 272 ++++++++++++++++++ 8 files changed, 448 insertions(+), 23 deletions(-) create mode 100644 test/react/ClassComponents/complex-class-proper-hoisting.js create mode 100644 test/react/ClassComponents/complex-class-with-equivalent-node.js diff --git a/src/serializer/ResidualHeapSerializer.js b/src/serializer/ResidualHeapSerializer.js index b02b061ee..9c7305341 100644 --- a/src/serializer/ResidualHeapSerializer.js +++ b/src/serializer/ResidualHeapSerializer.js @@ -2300,7 +2300,7 @@ export class ResidualHeapSerializer { invariant(effectsGenerator === generator); effectsGenerator.serialize(this._getContext()); this.realm.withEffectsAppliedInGlobalEnv(() => { - const lazyHoistedReactNodes = this.residualReactElementSerializer.serializeLazyHoistedNodes(); + const lazyHoistedReactNodes = this.residualReactElementSerializer.serializeLazyHoistedNodes(functionValue); this.mainBody.entries.push(...lazyHoistedReactNodes); return null; }, additionalEffects.effects); diff --git a/src/serializer/ResidualHeapVisitor.js b/src/serializer/ResidualHeapVisitor.js index a94d3976a..696a0d4d6 100644 --- a/src/serializer/ResidualHeapVisitor.js +++ b/src/serializer/ResidualHeapVisitor.js @@ -66,6 +66,7 @@ import { ResidualReactElementVisitor } from "./ResidualReactElementVisitor.js"; import { GeneratorDAG } from "./GeneratorDAG.js"; export type Scope = FunctionValue | Generator; + type BindingState = {| capturedBindings: Set, capturingFunctions: Set, @@ -254,6 +255,14 @@ export class ResidualHeapVisitor { // Queues up an action to be later processed in some arbitrary scope. _enqueueWithUnrelatedScope(scope: Scope, action: () => void | boolean): void { + // If we are in a zone with a non-default equivalence set (we are wrapped in a `withCleanEquivalenceSet` call) then + // we need to save our equivalence set so that we may load it before running our action. + if (this.residualReactElementVisitor.defaultEquivalenceSet === false) { + const save = this.residualReactElementVisitor.saveEquivalenceSet(); + const originalAction = action; + action = () => this.residualReactElementVisitor.loadEquivalenceSet(save, originalAction); + } + this.delayedActions.push({ scope, action }); } @@ -1114,10 +1123,7 @@ export class ResidualHeapVisitor { this.referencedDeclaredValues.set(value, this._getAdditionalFunctionOfScope()); }, recordDelayedEntry: (generator, entry: GeneratorEntry) => { - this.delayedActions.push({ - scope: generator, - action: () => entry.visit(callbacks, generator), - }); + this._enqueueWithUnrelatedScope(generator, () => entry.visit(callbacks, generator)); }, visitModifiedProperty: (binding: PropertyBinding) => { let fixpoint_rerun = () => { @@ -1224,7 +1230,7 @@ export class ResidualHeapVisitor { // Set Visitor state // Allows us to emit function declarations etc. inside of this additional // function instead of adding them at global scope - this.residualReactElementVisitor.withCleanEquivalenceSet(() => { + let visitor = () => { invariant(funcInstance !== undefined); invariant(functionInfo !== undefined); let additionalFunctionInfo = { @@ -1238,7 +1244,13 @@ export class ResidualHeapVisitor { let effectsGenerator = additionalEffects.generator; this.generatorDAG.add(functionValue, effectsGenerator); this.visitGenerator(effectsGenerator, additionalFunctionInfo); - }); + }; + + if (this.realm.react.enabled) { + this.residualReactElementVisitor.withCleanEquivalenceSet(visitor); + } else { + visitor(); + } } visitRoots(): void { diff --git a/src/serializer/ResidualReactElementSerializer.js b/src/serializer/ResidualReactElementSerializer.js index 964f25ebc..0140e037c 100644 --- a/src/serializer/ResidualReactElementSerializer.js +++ b/src/serializer/ResidualReactElementSerializer.js @@ -14,7 +14,7 @@ import { ResidualHeapSerializer } from "./ResidualHeapSerializer.js"; import { canHoistReactElement } from "../react/hoisting.js"; import * as t from "@babel/types"; import type { BabelNode, BabelNodeExpression } from "@babel/types"; -import { AbstractValue, AbstractObjectValue, ObjectValue, SymbolValue, Value } from "../values/index.js"; +import { AbstractValue, AbstractObjectValue, ObjectValue, SymbolValue, FunctionValue, Value } from "../values/index.js"; import { convertExpressionToJSXIdentifier, convertKeyValueToJSXAttribute } from "../react/jsx.js"; import { Logger } from "../utils/logger.js"; import invariant from "../invariant.js"; @@ -52,14 +52,14 @@ export class ResidualReactElementSerializer { this.residualHeapSerializer = residualHeapSerializer; this.logger = residualHeapSerializer.logger; this.reactOutput = realm.react.output || "create-element"; - this._lazilyHoistedNodes = undefined; + this._lazilyHoistedNodes = new Map(); } realm: Realm; logger: Logger; reactOutput: ReactOutputTypes; residualHeapSerializer: ResidualHeapSerializer; - _lazilyHoistedNodes: void | LazilyHoistedNodes; + _lazilyHoistedNodes: Map; _createReactElement(value: ObjectValue): ReactElement { return { attributes: [], children: [], declared: false, type: undefined, value }; @@ -82,13 +82,17 @@ export class ResidualReactElementSerializer { ): void { // if the currentHoistedReactElements is not defined, we create it an emit the function call // this should only occur once per additional function - if (this._lazilyHoistedNodes === undefined) { + const optimizedFunction = this.residualHeapSerializer.tryGetOptimizedFunctionRoot(reactElement); + invariant(optimizedFunction); + let lazilyHoistedNodes = this._lazilyHoistedNodes.get(optimizedFunction); + if (lazilyHoistedNodes === undefined) { let funcId = t.identifier(this.residualHeapSerializer.functionNameGenerator.generate()); - this._lazilyHoistedNodes = { + lazilyHoistedNodes = { id: funcId, createElementIdentifier: hoistedCreateElementIdentifier, nodes: [], }; + this._lazilyHoistedNodes.set(optimizedFunction, lazilyHoistedNodes); let statement = t.expressionStatement( t.logicalExpression( "&&", @@ -97,14 +101,11 @@ export class ResidualReactElementSerializer { t.callExpression(funcId, originalCreateElementIdentifier ? [originalCreateElementIdentifier] : []) ) ); - let optimizedFunction = this.residualHeapSerializer.tryGetOptimizedFunctionRoot(reactElement); this.residualHeapSerializer.getPrelude(optimizedFunction).push(statement); } // we then push the reactElement and its id into our list of elements to process after // the current additional function has serialzied - invariant(this._lazilyHoistedNodes !== undefined); - invariant(Array.isArray(this._lazilyHoistedNodes.nodes)); - this._lazilyHoistedNodes.nodes.push({ id, astNode: reactElementAst }); + lazilyHoistedNodes.nodes.push({ id, astNode: reactElementAst }); } _getReactLibraryValue(): AbstractObjectValue | ObjectValue { @@ -155,15 +156,18 @@ export class ResidualReactElementSerializer { originalCreateElementIdentifier = this.residualHeapSerializer.serializeValue(createElement); if (shouldHoist) { - // if we haven't created a _lazilyHoistedNodes before, then this is the first time + const optimizedFunction = this.residualHeapSerializer.tryGetOptimizedFunctionRoot(value); + invariant(optimizedFunction); + const lazilyHoistedNodes = this._lazilyHoistedNodes.get(optimizedFunction); + // if we haven't created a lazilyHoistedNodes before, then this is the first time // so we only create the hoisted identifier once - if (this._lazilyHoistedNodes === undefined) { + if (lazilyHoistedNodes === undefined) { // create a new unique instance hoistedCreateElementIdentifier = t.identifier( this.residualHeapSerializer.intrinsicNameGenerator.generate() ); } else { - hoistedCreateElementIdentifier = this._lazilyHoistedNodes.createElementIdentifier; + hoistedCreateElementIdentifier = lazilyHoistedNodes.createElementIdentifier; } } @@ -438,10 +442,11 @@ export class ResidualReactElementSerializer { return reactElementChild; } - serializeLazyHoistedNodes(): Array { + serializeLazyHoistedNodes(optimizedFunction: FunctionValue): Array { const entries = []; - if (this._lazilyHoistedNodes !== undefined) { - let { id, nodes, createElementIdentifier } = this._lazilyHoistedNodes; + const lazilyHoistedNodes = this._lazilyHoistedNodes.get(optimizedFunction); + if (lazilyHoistedNodes !== undefined) { + let { id, nodes, createElementIdentifier } = lazilyHoistedNodes; // create a function that initializes all the hoisted nodes let func = t.functionExpression( null, @@ -454,7 +459,7 @@ export class ResidualReactElementSerializer { // output all the empty variable declarations that will hold the nodes lazily entries.push(...nodes.map(node => t.variableDeclaration("var", [t.variableDeclarator(node.id)]))); // reset the _lazilyHoistedNodes so other additional functions work - this._lazilyHoistedNodes = undefined; + this._lazilyHoistedNodes.delete(optimizedFunction); } return entries; } diff --git a/src/serializer/ResidualReactElementVisitor.js b/src/serializer/ResidualReactElementVisitor.js index 07cac48e8..75695e200 100644 --- a/src/serializer/ResidualReactElementVisitor.js +++ b/src/serializer/ResidualReactElementVisitor.js @@ -36,11 +36,18 @@ import { ReactPropsSet } from "../react/ReactPropsSet.js"; import type { ReactOutputTypes } from "../options.js"; import { Get } from "../methods/index.js"; +export opaque type ReactEquivalenceSetSave = {| + +reactEquivalenceSet: ReactEquivalenceSet, + +reactElementEquivalenceSet: ReactElementSet, + +reactPropsEquivalenceSet: ReactPropsSet, +|}; + export class ResidualReactElementVisitor { constructor(realm: Realm, residualHeapVisitor: ResidualHeapVisitor) { this.realm = realm; this.residualHeapVisitor = residualHeapVisitor; this.reactOutput = realm.react.output || "create-element"; + this.defaultEquivalenceSet = true; this.reactEquivalenceSet = new ReactEquivalenceSet(realm, this); this.reactElementEquivalenceSet = new ReactElementSet(realm, this.reactEquivalenceSet); this.reactPropsEquivalenceSet = new ReactPropsSet(realm, this.reactEquivalenceSet); @@ -49,6 +56,7 @@ export class ResidualReactElementVisitor { realm: Realm; residualHeapVisitor: ResidualHeapVisitor; reactOutput: ReactOutputTypes; + defaultEquivalenceSet: boolean; reactEquivalenceSet: ReactEquivalenceSet; reactElementEquivalenceSet: ReactElementSet; reactPropsEquivalenceSet: ReactPropsSet; @@ -138,19 +146,45 @@ export class ResidualReactElementVisitor { } withCleanEquivalenceSet(func: () => void): void { + let defaultEquivalenceSet = this.defaultEquivalenceSet; let reactEquivalenceSet = this.reactEquivalenceSet; let reactElementEquivalenceSet = this.reactElementEquivalenceSet; let reactPropsEquivalenceSet = this.reactPropsEquivalenceSet; + this.defaultEquivalenceSet = false; this.reactEquivalenceSet = new ReactEquivalenceSet(this.realm, this); this.reactElementEquivalenceSet = new ReactElementSet(this.realm, this.reactEquivalenceSet); this.reactPropsEquivalenceSet = new ReactPropsSet(this.realm, this.reactEquivalenceSet); func(); // Cleanup + this.defaultEquivalenceSet = defaultEquivalenceSet; this.reactEquivalenceSet = reactEquivalenceSet; this.reactElementEquivalenceSet = reactElementEquivalenceSet; this.reactPropsEquivalenceSet = reactPropsEquivalenceSet; } + saveEquivalenceSet(): ReactEquivalenceSetSave { + const { reactEquivalenceSet, reactElementEquivalenceSet, reactPropsEquivalenceSet } = this; + return { reactEquivalenceSet, reactElementEquivalenceSet, reactPropsEquivalenceSet }; + } + + loadEquivalenceSet(save: ReactEquivalenceSetSave, func: () => T): T { + const defaultEquivalenceSet = this.defaultEquivalenceSet; + const reactEquivalenceSet = this.reactEquivalenceSet; + const reactElementEquivalenceSet = this.reactElementEquivalenceSet; + const reactPropsEquivalenceSet = this.reactPropsEquivalenceSet; + this.defaultEquivalenceSet = false; + this.reactEquivalenceSet = save.reactEquivalenceSet; + this.reactElementEquivalenceSet = save.reactElementEquivalenceSet; + this.reactPropsEquivalenceSet = save.reactPropsEquivalenceSet; + const result = func(); + // Cleanup + this.defaultEquivalenceSet = defaultEquivalenceSet; + this.reactEquivalenceSet = reactEquivalenceSet; + this.reactElementEquivalenceSet = reactElementEquivalenceSet; + this.reactPropsEquivalenceSet = reactPropsEquivalenceSet; + return result; + } + wasTemporalAliasDeclaredInCurrentScope(temporalAlias: AbstractObjectValue): boolean { let scope = this.residualHeapVisitor.scope; if (scope instanceof FunctionValue) { diff --git a/test/react/ClassComponents-test.js b/test/react/ClassComponents-test.js index 2a0cb1e9a..f20445ec6 100644 --- a/test/react/ClassComponents-test.js +++ b/test/react/ClassComponents-test.js @@ -66,3 +66,11 @@ it("Complex class components folding into functional root component #4", () => { it("Complex class components folding into functional root component #5", () => { runTest(__dirname + "/ClassComponents/complex-class-into-functional-root5.js"); }); + +it("Complex class component rendering equivalent node to functional root component", () => { + runTest(__dirname + "/ClassComponents/complex-class-with-equivalent-node.js"); +}); + +it("Complex class component hoists nodes independently of functional root component", () => { + runTest(__dirname + "/ClassComponents/complex-class-proper-hoisting.js"); +}); diff --git a/test/react/ClassComponents/complex-class-proper-hoisting.js b/test/react/ClassComponents/complex-class-proper-hoisting.js new file mode 100644 index 000000000..6f4563699 --- /dev/null +++ b/test/react/ClassComponents/complex-class-proper-hoisting.js @@ -0,0 +1,43 @@ +const React = require("react"); + +function Tau(props) { + return React.createElement( + "a", + null, + React.createElement("b", null), + React.createElement(Epsilon, null), + React.createElement("c", null) + ); +} + +class Epsilon extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + return React.createElement(React.Fragment, null, React.createElement("d", null), React.createElement("b", null)); + } +} + +if (this.__optimizeReactComponentTree) __optimizeReactComponentTree(Tau); + +Tau.getTrials = renderer => { + const trials = []; + + renderer.update(); + trials.push(["render Epsilon", renderer.toJSON()]); + + renderer.update(); + trials.push(["render Tau", renderer.toJSON()]); + + const a = Tau().props.children[0]; + const b = Epsilon.prototype.render.call(undefined).props.children[1]; + if (a.type !== "b" || b.type !== "b") throw new Error("Expected s"); + trials.push(["different React elements", JSON.stringify(a !== b)]); + + return trials; +}; + +module.exports = Tau; diff --git a/test/react/ClassComponents/complex-class-with-equivalent-node.js b/test/react/ClassComponents/complex-class-with-equivalent-node.js new file mode 100644 index 000000000..0b7874483 --- /dev/null +++ b/test/react/ClassComponents/complex-class-with-equivalent-node.js @@ -0,0 +1,51 @@ +const React = require("react"); + +function Tau(props) { + return React.createElement( + "div", + null, + React.createElement(Epsilon, { + a: props.z, + }), + React.createElement(Zeta, { + p: props.h, + }) + ); +} + +class Epsilon extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + return React.createElement(Zeta, { p: this.props.a }); + } +} + +function Zeta(props) { + return props.p ? null : React.createElement("foobar", null); +} + +if (this.__optimizeReactComponentTree) __optimizeReactComponentTree(Tau); + +Tau.getTrials = function(renderer, Root) { + const trials = []; + + renderer.update(); + trials.push(["render 1", renderer.toJSON()]); + + renderer.update(); + trials.push(["render 2", renderer.toJSON()]); + + renderer.update(); + trials.push(["render 3", renderer.toJSON()]); + + renderer.update(); + trials.push(["render 4", renderer.toJSON()]); + + return trials; +}; + +module.exports = Tau; diff --git a/test/react/__snapshots__/ClassComponents-test.js.snap b/test/react/__snapshots__/ClassComponents-test.js.snap index 40ba42a81..c0b3f5a99 100644 --- a/test/react/__snapshots__/ClassComponents-test.js.snap +++ b/test/react/__snapshots__/ClassComponents-test.js.snap @@ -68,6 +68,278 @@ ReactStatistics { } `; +exports[`Complex class component hoists nodes independently of functional root component: (JSX => JSX) 1`] = ` +ReactStatistics { + "componentsEvaluated": 4, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "React.Fragment", + "status": "NORMAL", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component hoists nodes independently of functional root component: (JSX => createElement) 1`] = ` +ReactStatistics { + "componentsEvaluated": 4, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "React.Fragment", + "status": "NORMAL", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component hoists nodes independently of functional root component: (createElement => JSX) 1`] = ` +ReactStatistics { + "componentsEvaluated": 4, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "React.Fragment", + "status": "NORMAL", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component hoists nodes independently of functional root component: (createElement => createElement) 1`] = ` +ReactStatistics { + "componentsEvaluated": 4, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "React.Fragment", + "status": "NORMAL", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 0, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component rendering equivalent node to functional root component: (JSX => JSX) 1`] = ` +ReactStatistics { + "componentsEvaluated": 5, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 2, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component rendering equivalent node to functional root component: (JSX => createElement) 1`] = ` +ReactStatistics { + "componentsEvaluated": 5, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 2, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component rendering equivalent node to functional root component: (createElement => JSX) 1`] = ` +ReactStatistics { + "componentsEvaluated": 5, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 2, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + +exports[`Complex class component rendering equivalent node to functional root component: (createElement => createElement) 1`] = ` +ReactStatistics { + "componentsEvaluated": 5, + "evaluatedRootNodes": Array [ + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Epsilon", + "status": "NEW_TREE", + }, + Object { + "children": Array [], + "message": "", + "name": "Zeta", + "status": "INLINED", + }, + ], + "message": "", + "name": "Tau", + "status": "ROOT", + }, + ], + "inlinedComponents": 2, + "optimizedNestedClosures": 0, + "optimizedTrees": 2, +} +`; + exports[`Complex class components folding into functional root component #2: (JSX => JSX) 1`] = ` ReactStatistics { "componentsEvaluated": 4,