Fix React branch serialization issues (#2318)

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💯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💯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 `<foobar>`.
- Later, we’d execute the `<foobar>` 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 `<foobar>` React element in our React equivalence set to `undefined`. Since when we created `<foobar>` we modified its `type` property. (Recorded in `modifiedProperties`.)
- Now our `<Zeta>` in `<Tau>` and our `<Zeta>` in `<Epsilon>` share the _exact same_ `<foobar>` thanks to our equivalence set.
- But `<foobar>` has a `type` of `undefined` thanks to the effects we applied. We should be creating a new `<foobar>` 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 = <a>{_6}{_8}{_9}</a>;

    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 = <b />;
    _E = <d />;
  };

  var _6;

  var _E;

  var $f_1 = function () {
    _9 = <c />;
  };

  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
This commit is contained in:
Caleb Meredith 2018-07-25 09:44:55 -07:00 committed by Facebook Github Bot
parent a1bd30ddb9
commit 2810294260
8 changed files with 448 additions and 23 deletions

View File

@ -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);

View File

@ -66,6 +66,7 @@ import { ResidualReactElementVisitor } from "./ResidualReactElementVisitor.js";
import { GeneratorDAG } from "./GeneratorDAG.js";
export type Scope = FunctionValue | Generator;
type BindingState = {|
capturedBindings: Set<ResidualFunctionBinding>,
capturingFunctions: Set<FunctionValue>,
@ -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 {

View File

@ -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<FunctionValue, LazilyHoistedNodes>;
_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<BabelNodeStatement> {
serializeLazyHoistedNodes(optimizedFunction: FunctionValue): Array<BabelNodeStatement> {
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;
}

View File

@ -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<T>(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) {

View File

@ -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");
});

View File

@ -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(<Epsilon />);
trials.push(["render Epsilon", renderer.toJSON()]);
renderer.update(<Tau />);
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 <b>s");
trials.push(["different React elements", JSON.stringify(a !== b)]);
return trials;
};
module.exports = Tau;

View File

@ -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(<Root z={false} p={false} />);
trials.push(["render 1", renderer.toJSON()]);
renderer.update(<Root z={true} p={false} />);
trials.push(["render 2", renderer.toJSON()]);
renderer.update(<Root z={false} p={true} />);
trials.push(["render 3", renderer.toJSON()]);
renderer.update(<Root z={true} p={true} />);
trials.push(["render 4", renderer.toJSON()]);
return trials;
};
module.exports = Tau;

View File

@ -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,