diff --git a/tools/hermes-parser/js/hermes-parser/__tests__/ComponentDeclaration-test.js b/tools/hermes-parser/js/hermes-parser/__tests__/ComponentDeclaration-test.js index 6f55b7e3ec1..67e3419b631 100644 --- a/tools/hermes-parser/js/hermes-parser/__tests__/ComponentDeclaration-test.js +++ b/tools/hermes-parser/js/hermes-parser/__tests__/ComponentDeclaration-test.js @@ -326,4 +326,71 @@ switch (thing) { `); }); }); + + describe('ref and normal params with hoisting', () => { + const code = ` +Bar; +unrelated; +someSideEffect(Foo); +unrelated; + +component Foo(foo: string, ref: Ref) {} + +Bar; +component Bar(foo: string, ref: Ref) {} + `; + + test('ESTree', async () => { + expect(await printForSnapshotESTree(code)).toBe(code.trim()); + expect(await parseForSnapshotESTree(code)).toMatchSnapshot(); + }); + + test('Babel', async () => { + expect(await parseForSnapshotBabel(code)).toMatchSnapshot(); + expect(await printForSnapshotBabel(code)).toMatchInlineSnapshot(` + "const Bar = React.forwardRef(Bar_withRef); + Bar; + unrelated; + const Foo = React.forwardRef(Foo_withRef); + someSideEffect(Foo); + unrelated; + + function Foo_withRef({ + foo + }: $ReadOnly<{...}>, ref: Ref): React.Node {} + + Bar; + + function Bar_withRef({ + foo + }: $ReadOnly<{...}>, ref: Ref): React.Node {}" + `); + }); + }); + + describe('ref and normal params with hoisting (recursive)', () => { + const code = ` +component Foo(bar: mixed = Foo, ref: any) { + return null; +} + `; + + test('ESTree', async () => { + expect(await printForSnapshotESTree(code)).toBe(code.trim()); + expect(await parseForSnapshotESTree(code)).toMatchSnapshot(); + }); + + test('Babel', async () => { + expect(await parseForSnapshotBabel(code)).toMatchSnapshot(); + expect(await printForSnapshotBabel(code)).toMatchInlineSnapshot(` + "const Foo = React.forwardRef(Foo_withRef); + + function Foo_withRef({ + bar = Foo + }: $ReadOnly<{...}>, ref: any): React.Node { + return null; + }" + `); + }); + }); }); diff --git a/tools/hermes-parser/js/hermes-parser/__tests__/__snapshots__/ComponentDeclaration-test.js.snap b/tools/hermes-parser/js/hermes-parser/__tests__/__snapshots__/ComponentDeclaration-test.js.snap index 2487b6a8cae..1d132b158d1 100644 --- a/tools/hermes-parser/js/hermes-parser/__tests__/__snapshots__/ComponentDeclaration-test.js.snap +++ b/tools/hermes-parser/js/hermes-parser/__tests__/__snapshots__/ComponentDeclaration-test.js.snap @@ -1308,6 +1308,745 @@ exports[`ComponentDeclaration ref and normal params named exported ESTree 1`] = } `; +exports[`ComponentDeclaration ref and normal params with hoisting (recursive) Babel 1`] = ` +{ + "body": [ + { + "declarations": [ + { + "id": { + "name": "Foo", + "type": "Identifier", + }, + "init": { + "arguments": [ + { + "name": "Foo_withRef", + "type": "Identifier", + }, + ], + "callee": { + "computed": false, + "object": { + "name": "React", + "type": "Identifier", + }, + "property": { + "name": "forwardRef", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "type": "CallExpression", + }, + "type": "VariableDeclarator", + }, + ], + "kind": "const", + "type": "VariableDeclaration", + }, + { + "__componentDeclaration": true, + "async": false, + "body": { + "body": [ + { + "argument": { + "type": "NullLiteral", + }, + "type": "ReturnStatement", + }, + ], + "directives": [], + "type": "BlockStatement", + }, + "generator": false, + "id": { + "name": "Foo_withRef", + "type": "Identifier", + }, + "params": [ + { + "properties": [ + { + "computed": false, + "key": { + "name": "bar", + "type": "Identifier", + }, + "method": false, + "shorthand": true, + "type": "ObjectProperty", + "value": { + "left": { + "name": "bar", + "type": "Identifier", + }, + "right": { + "name": "Foo", + "type": "Identifier", + }, + "type": "AssignmentPattern", + }, + }, + ], + "type": "ObjectPattern", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "id": { + "name": "$ReadOnly", + "type": "Identifier", + }, + "type": "GenericTypeAnnotation", + "typeParameters": { + "params": [ + { + "callProperties": [], + "exact": false, + "indexers": [], + "inexact": true, + "internalSlots": [], + "properties": [], + "type": "ObjectTypeAnnotation", + }, + ], + "type": "TypeParameterInstantiation", + }, + }, + }, + }, + { + "name": "ref", + "type": "Identifier", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "type": "AnyTypeAnnotation", + }, + }, + }, + ], + "returnType": { + "type": "TypeAnnotation", + "typeAnnotation": { + "id": { + "id": { + "name": "Node", + "type": "Identifier", + }, + "qualification": { + "name": "React", + "type": "Identifier", + }, + "type": "QualifiedTypeIdentifier", + }, + "type": "GenericTypeAnnotation", + "typeParameters": null, + }, + }, + "type": "FunctionDeclaration", + }, + ], + "type": "Program", +} +`; + +exports[`ComponentDeclaration ref and normal params with hoisting (recursive) ESTree 1`] = ` +{ + "body": [ + { + "body": { + "body": [ + { + "argument": { + "literalType": "null", + "raw": "null", + "type": "Literal", + "value": null, + }, + "type": "ReturnStatement", + }, + ], + "type": "BlockStatement", + }, + "id": { + "name": "Foo", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "params": [ + { + "local": { + "left": { + "name": "bar", + "optional": false, + "type": "Identifier", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "type": "MixedTypeAnnotation", + }, + }, + }, + "right": { + "name": "Foo", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "type": "AssignmentPattern", + }, + "name": { + "name": "bar", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "shorthand": true, + "type": "ComponentParameter", + }, + { + "local": { + "name": "ref", + "optional": false, + "type": "Identifier", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "type": "AnyTypeAnnotation", + }, + }, + }, + "name": { + "name": "ref", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "shorthand": true, + "type": "ComponentParameter", + }, + ], + "rendersType": null, + "type": "ComponentDeclaration", + "typeParameters": null, + }, + ], + "type": "Program", +} +`; + +exports[`ComponentDeclaration ref and normal params with hoisting Babel 1`] = ` +{ + "body": [ + { + "declarations": [ + { + "id": { + "name": "Bar", + "type": "Identifier", + }, + "init": { + "arguments": [ + { + "name": "Bar_withRef", + "type": "Identifier", + }, + ], + "callee": { + "computed": false, + "object": { + "name": "React", + "type": "Identifier", + }, + "property": { + "name": "forwardRef", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "type": "CallExpression", + }, + "type": "VariableDeclarator", + }, + ], + "kind": "const", + "type": "VariableDeclaration", + }, + { + "expression": { + "name": "Bar", + "type": "Identifier", + }, + "type": "ExpressionStatement", + }, + { + "expression": { + "name": "unrelated", + "type": "Identifier", + }, + "type": "ExpressionStatement", + }, + { + "declarations": [ + { + "id": { + "name": "Foo", + "type": "Identifier", + }, + "init": { + "arguments": [ + { + "name": "Foo_withRef", + "type": "Identifier", + }, + ], + "callee": { + "computed": false, + "object": { + "name": "React", + "type": "Identifier", + }, + "property": { + "name": "forwardRef", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "type": "CallExpression", + }, + "type": "VariableDeclarator", + }, + ], + "kind": "const", + "type": "VariableDeclaration", + }, + { + "expression": { + "arguments": [ + { + "name": "Foo", + "type": "Identifier", + }, + ], + "callee": { + "name": "someSideEffect", + "type": "Identifier", + }, + "type": "CallExpression", + }, + "type": "ExpressionStatement", + }, + { + "expression": { + "name": "unrelated", + "type": "Identifier", + }, + "type": "ExpressionStatement", + }, + { + "__componentDeclaration": true, + "async": false, + "body": { + "body": [], + "directives": [], + "type": "BlockStatement", + }, + "generator": false, + "id": { + "name": "Foo_withRef", + "type": "Identifier", + }, + "params": [ + { + "properties": [ + { + "computed": false, + "key": { + "name": "foo", + "type": "Identifier", + }, + "method": false, + "shorthand": true, + "type": "ObjectProperty", + "value": { + "name": "foo", + "type": "Identifier", + }, + }, + ], + "type": "ObjectPattern", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "id": { + "name": "$ReadOnly", + "type": "Identifier", + }, + "type": "GenericTypeAnnotation", + "typeParameters": { + "params": [ + { + "callProperties": [], + "exact": false, + "indexers": [], + "inexact": true, + "internalSlots": [], + "properties": [], + "type": "ObjectTypeAnnotation", + }, + ], + "type": "TypeParameterInstantiation", + }, + }, + }, + }, + { + "name": "ref", + "type": "Identifier", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "id": { + "name": "Ref", + "type": "Identifier", + }, + "type": "GenericTypeAnnotation", + "typeParameters": null, + }, + }, + }, + ], + "returnType": { + "type": "TypeAnnotation", + "typeAnnotation": { + "id": { + "id": { + "name": "Node", + "type": "Identifier", + }, + "qualification": { + "name": "React", + "type": "Identifier", + }, + "type": "QualifiedTypeIdentifier", + }, + "type": "GenericTypeAnnotation", + "typeParameters": null, + }, + }, + "type": "FunctionDeclaration", + }, + { + "expression": { + "name": "Bar", + "type": "Identifier", + }, + "type": "ExpressionStatement", + }, + { + "__componentDeclaration": true, + "async": false, + "body": { + "body": [], + "directives": [], + "type": "BlockStatement", + }, + "generator": false, + "id": { + "name": "Bar_withRef", + "type": "Identifier", + }, + "params": [ + { + "properties": [ + { + "computed": false, + "key": { + "name": "foo", + "type": "Identifier", + }, + "method": false, + "shorthand": true, + "type": "ObjectProperty", + "value": { + "name": "foo", + "type": "Identifier", + }, + }, + ], + "type": "ObjectPattern", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "id": { + "name": "$ReadOnly", + "type": "Identifier", + }, + "type": "GenericTypeAnnotation", + "typeParameters": { + "params": [ + { + "callProperties": [], + "exact": false, + "indexers": [], + "inexact": true, + "internalSlots": [], + "properties": [], + "type": "ObjectTypeAnnotation", + }, + ], + "type": "TypeParameterInstantiation", + }, + }, + }, + }, + { + "name": "ref", + "type": "Identifier", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "id": { + "name": "Ref", + "type": "Identifier", + }, + "type": "GenericTypeAnnotation", + "typeParameters": null, + }, + }, + }, + ], + "returnType": { + "type": "TypeAnnotation", + "typeAnnotation": { + "id": { + "id": { + "name": "Node", + "type": "Identifier", + }, + "qualification": { + "name": "React", + "type": "Identifier", + }, + "type": "QualifiedTypeIdentifier", + }, + "type": "GenericTypeAnnotation", + "typeParameters": null, + }, + }, + "type": "FunctionDeclaration", + }, + ], + "type": "Program", +} +`; + +exports[`ComponentDeclaration ref and normal params with hoisting ESTree 1`] = ` +{ + "body": [ + { + "directive": null, + "expression": { + "name": "Bar", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "type": "ExpressionStatement", + }, + { + "directive": null, + "expression": { + "name": "unrelated", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "type": "ExpressionStatement", + }, + { + "directive": null, + "expression": { + "arguments": [ + { + "name": "Foo", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + ], + "callee": { + "name": "someSideEffect", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "optional": false, + "type": "CallExpression", + "typeArguments": null, + }, + "type": "ExpressionStatement", + }, + { + "directive": null, + "expression": { + "name": "unrelated", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "type": "ExpressionStatement", + }, + { + "body": { + "body": [], + "type": "BlockStatement", + }, + "id": { + "name": "Foo", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "params": [ + { + "local": { + "name": "foo", + "optional": false, + "type": "Identifier", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "type": "StringTypeAnnotation", + }, + }, + }, + "name": { + "name": "foo", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "shorthand": true, + "type": "ComponentParameter", + }, + { + "local": { + "name": "ref", + "optional": false, + "type": "Identifier", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "id": { + "name": "Ref", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "type": "GenericTypeAnnotation", + "typeParameters": null, + }, + }, + }, + "name": { + "name": "ref", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "shorthand": true, + "type": "ComponentParameter", + }, + ], + "rendersType": null, + "type": "ComponentDeclaration", + "typeParameters": null, + }, + { + "directive": null, + "expression": { + "name": "Bar", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "type": "ExpressionStatement", + }, + { + "body": { + "body": [], + "type": "BlockStatement", + }, + "id": { + "name": "Bar", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "params": [ + { + "local": { + "name": "foo", + "optional": false, + "type": "Identifier", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "type": "StringTypeAnnotation", + }, + }, + }, + "name": { + "name": "foo", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "shorthand": true, + "type": "ComponentParameter", + }, + { + "local": { + "name": "ref", + "optional": false, + "type": "Identifier", + "typeAnnotation": { + "type": "TypeAnnotation", + "typeAnnotation": { + "id": { + "name": "Ref", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "type": "GenericTypeAnnotation", + "typeParameters": null, + }, + }, + }, + "name": { + "name": "ref", + "optional": false, + "type": "Identifier", + "typeAnnotation": null, + }, + "shorthand": true, + "type": "ComponentParameter", + }, + ], + "rendersType": null, + "type": "ComponentDeclaration", + "typeParameters": null, + }, + ], + "type": "Program", +} +`; + exports[`ComponentDeclaration ref and normal params within block Babel 1`] = ` { "body": [ diff --git a/tools/hermes-parser/js/hermes-parser/src/babel/TransformReactScriptForBabel.js b/tools/hermes-parser/js/hermes-parser/src/babel/TransformReactScriptForBabel.js index 308b99b2040..2fab616d51e 100644 --- a/tools/hermes-parser/js/hermes-parser/src/babel/TransformReactScriptForBabel.js +++ b/tools/hermes-parser/js/hermes-parser/src/babel/TransformReactScriptForBabel.js @@ -42,6 +42,7 @@ import type { import {SimpleTransform} from '../transform/SimpleTransform'; import {shallowCloneNode} from '../transform/astNodeMutationHelpers'; +import {SimpleTraverser} from '../traverse/SimpleTraverser'; const nodeWith = SimpleTransform.nodeWith; @@ -493,6 +494,40 @@ function mapComponentDeclaration(node: ComponentDeclaration): { return {comp, forwardRefDetails}; } +/** + * Scan a list of statements and return the position of the + * first statement that contains a reference to a given component + * or null of no references were found. + */ +function scanForFirstComponentReference( + compName: string, + bodyList: Array, +): ?number { + for (let i = 0; i < bodyList.length; i++) { + const bodyNode = bodyList[i]; + let referencePos = null; + SimpleTraverser.traverse(bodyNode, { + enter(node: ESNode) { + switch (node.type) { + case 'Identifier': { + if (node.name === compName) { + // We found a reference, record it and stop. + referencePos = i; + throw SimpleTraverser.Break; + } + } + } + }, + leave(_node: ESNode) {}, + }); + if (referencePos != null) { + return referencePos; + } + } + + return null; +} + function mapComponentDeclarationIntoList( node: ComponentDeclaration, newBody: Array, @@ -500,8 +535,21 @@ function mapComponentDeclarationIntoList( ) { const {comp, forwardRefDetails} = mapComponentDeclaration(node); if (forwardRefDetails != null) { - newBody.push(forwardRefDetails.forwardRefStatement); + // Scan for references to our component. + const referencePos = scanForFirstComponentReference( + forwardRefDetails.forwardRefCompId.name, + newBody, + ); + + // If a reference is found insert the forwardRef statement before it (to simulate function hoisting). + if (referencePos != null) { + newBody.splice(referencePos, 0, forwardRefDetails.forwardRefStatement); + } else { + newBody.push(forwardRefDetails.forwardRefStatement); + } + newBody.push(comp); + if (insertExport != null) { newBody.push(insertExport(forwardRefDetails.forwardRefCompId)); }