/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict * @format */ import type {HermesNode} from './HermesAST'; import HermesASTAdapter from './HermesASTAdapter'; declare var BigInt: ?(value: $FlowFixMe) => mixed; function createSyntaxError(node: HermesNode, err: string): SyntaxError { const syntaxError = new SyntaxError(err); // $FlowExpectedError[prop-missing] syntaxError.loc = { line: node.loc.start.line, column: node.loc.start.column, }; return syntaxError; } export default class HermesToBabelAdapter extends HermesASTAdapter { fixSourceLocation(node: HermesNode): void { const loc = node.loc; if (loc == null) { return; } node.loc = { source: this.sourceFilename ?? null, start: loc.start, end: loc.end, }; node.start = loc.rangeStart; node.end = loc.rangeEnd; } mapNode(node: HermesNode): HermesNode { this.fixSourceLocation(node); switch (node.type) { case 'Program': return this.mapProgram(node); case 'BlockStatement': return this.mapNodeWithDirectives(node); case 'Empty': return this.mapEmpty(node); case 'Identifier': return this.mapIdentifier(node); case 'TemplateElement': return this.mapTemplateElement(node); case 'GenericTypeAnnotation': return this.mapGenericTypeAnnotation(node); case 'SymbolTypeAnnotation': return this.mapSymbolTypeAnnotation(node); case 'Property': return this.mapProperty(node); case 'MethodDefinition': return this.mapMethodDefinition(node); case 'ImportDeclaration': return this.mapImportDeclaration(node); case 'ImportSpecifier': return this.mapImportSpecifier(node); case 'ExportDefaultDeclaration': return this.mapExportDefaultDeclaration(node); case 'ExportNamedDeclaration': return this.mapExportNamedDeclaration(node); case 'ExportNamespaceSpecifier': return this.mapExportNamespaceSpecifier(node); case 'ExportAllDeclaration': return this.mapExportAllDeclaration(node); case 'RestElement': return this.mapRestElement(node); case 'ImportExpression': return this.mapImportExpression(node); case 'JSXStringLiteral': return this.mapJSXStringLiteral(node); case 'PrivateName': return this.mapPrivateName(node); case 'ClassPrivateProperty': return this.mapPrivateProperty(node); case 'FunctionDeclaration': case 'FunctionExpression': return this.mapFunction(node); case 'IndexedAccessType': case 'OptionalIndexedAccessType': case 'KeyofTypeAnnotation': case 'ConditionalType': case 'InferType': case 'TupleTypeLabeledElement': case 'TupleTypeSpreadElement': case 'ObjectTypeMappedTypeProperty': case 'ComponentTypeAnnotation': return this.mapUnsupportedTypeAnnotation(node); case 'BigIntLiteral': return this.mapBigIntLiteral(node); case 'BigIntLiteralTypeAnnotation': return this.mapBigIntLiteralTypeAnnotation(node); case 'BigIntTypeAnnotation': return this.mapBigIntTypeAnnotation(node); case 'TypeofTypeAnnotation': return this.mapTypeofTypeAnnotation(node); case 'QualifiedTypeofIdentifier': return this.mapQualifiedTypeofIdentifier(node); case 'DeclareVariable': return this.mapDeclareVariable(node); case 'DeclareEnum': return this.mapDeclareEnum(node); case 'DeclareComponent': return this.mapDeclareComponent(node); case 'JSXElement': return this.mapJSXElement(node); case 'ComponentDeclaration': return this.mapComponentDeclaration(node); default: return this.mapNodeDefault(node); } } mapProgram(node: HermesNode): HermesNode { // Visit child nodes and convert to directives const {comments, ...program} = this.mapNodeWithDirectives(node); program.sourceType = this.getSourceType(); // Adjust start loc to beginning of file program.loc.start = {line: 1, column: 0}; program.start = 0; // Adjust end loc to include last comment if program ends with a comment if (comments.length > 0) { const lastComment = comments[comments.length - 1]; if (lastComment.end > program.end) { program.loc.end = lastComment.loc.end; program.end = lastComment.end; } } // Rename root node to File node and move Program node under program property return { type: 'File', loc: program.loc, start: program.start, end: program.end, program, comments, }; } mapNodeWithDirectives(node: HermesNode): HermesNode { const directives = []; for (const child of node.body) { if (child.type === 'ExpressionStatement' && child.directive != null) { // Visit directive children const directiveChild = this.mapNode(child); // Modify string literal node to be DirectiveLiteral node directiveChild.expression.type = 'DirectiveLiteral'; // Construct Directive node with DirectiveLiteral value directives.push({ type: 'Directive', loc: directiveChild.loc, start: directiveChild.start, end: directiveChild.end, value: directiveChild.expression, }); } else { // Once we have found the first non-directive node we know there cannot be any more directives break; } } // Move directives from body to new directives array node.directives = directives; if (directives.length !== 0) { node.body = node.body.slice(directives.length); } // Visit expression statement children const body = node.body; for (let i = 0; i < body.length; i++) { const child = body[i]; if (child != null) { body[i] = this.mapNode(child); } } return node; } mapIdentifier(node: HermesNode): HermesNode { node.loc.identifierName = node.name; return this.mapNodeDefault(node); } mapTemplateElement(node: HermesNode): HermesNode { // Adjust start loc to exclude "`" at beginning of template literal if this is the first quasi, // otherwise exclude "}" from previous expression. const startCharsToExclude = 1; // Adjust end loc to exclude "`" at end of template literal if this is the last quasi, // otherwise exclude "${" from next expression. const endCharsToExclude = node.tail ? 1 : 2; return { type: 'TemplateElement', loc: { start: { line: node.loc.start.line, column: node.loc.start.column + startCharsToExclude, }, end: { line: node.loc.end.line, column: node.loc.end.column - endCharsToExclude, }, }, start: node.start + startCharsToExclude, end: node.end - endCharsToExclude, tail: node.tail, value: { cooked: node.cooked, raw: node.raw, }, }; } mapGenericTypeAnnotation(node: HermesNode): HermesNode { // Convert simple `this` generic type to ThisTypeAnnotation if ( node.typeParameters == null && node.id.type === 'Identifier' && node.id.name === 'this' ) { return { type: 'ThisTypeAnnotation', loc: node.loc, start: node.start, end: node.end, }; } return this.mapNodeDefault(node); } mapSymbolTypeAnnotation(node: HermesNode): HermesNode { return { type: 'GenericTypeAnnotation', loc: node.loc, start: node.start, end: node.end, id: { type: 'Identifier', loc: node.loc, start: node.start, end: node.end, name: 'symbol', }, typeParameters: null, }; } mapProperty(node: HermesNode): HermesNode { const key = this.mapNode(node.key); const value = this.mapNode(node.value); // Convert methods, getters, and setters to ObjectMethod nodes if (node.method || node.kind !== 'init') { // Properties under the FunctionExpression value that should be moved // to the ObjectMethod node itself. const { id, params, body, async, generator, returnType, typeParameters, predicate, } = value; const newNode: HermesNode = { type: 'ObjectMethod', loc: node.loc, start: node.start, end: node.end, // Non getter or setter methods have `kind = method` kind: node.kind === 'init' ? 'method' : node.kind, method: node.kind === 'init' ? true : false, computed: node.computed, key, id, params, body, async, generator, returnType, typeParameters, predicate, }; if (node.kind !== 'init') { // babel emits an empty variance property on accessors for some reason newNode.variance = null; } return newNode; } else { // Non-method property nodes should be renamed to ObjectProperty node.type = 'ObjectProperty'; return node; } } mapMethodDefinition(node: HermesNode): HermesNode { const key = this.mapNode(node.key); const value = this.mapNode(node.value); // Properties under the FunctionExpression value that should be moved // to the ClassMethod node itself. const { id, params, body, async, generator, returnType, typeParameters, predicate, } = value; return { type: 'ClassMethod', loc: node.loc, start: node.start, end: node.end, kind: node.kind, computed: node.computed, static: node.static, key, id, params, body, async, generator, returnType, typeParameters, predicate, }; } mapRestElement(node: HermesNode): HermesNode { const restElement = this.mapNodeDefault(node); // Hermes puts type annotations on rest elements on the argument node, // but Babel expects type annotations on the rest element node itself. const annotation = restElement.argument.typeAnnotation; if (annotation != null) { restElement.typeAnnotation = annotation; restElement.argument.typeAnnotation = null; // Unfortunately there's no way for us to recover the end location of // the argument for the general case if (restElement.argument.type === 'Identifier') { restElement.argument.end = restElement.argument.start + restElement.argument.name.length; restElement.argument.loc.end = { ...restElement.argument.loc.start, column: restElement.argument.loc.start.column + restElement.argument.name.length, }; } } return restElement; } mapImportExpression(node: HermesNode): HermesNode { // Babel expects ImportExpression to be structued as a regular // CallExpression where the callee is an Import node. return { type: 'CallExpression', loc: node.loc, start: node.start, end: node.end, callee: { type: 'Import', loc: { ...node.loc, end: { ...node.loc.start, column: node.loc.start.column + 'import'.length, }, }, start: node.start, end: node.start + 'import'.length, }, arguments: [this.mapNode(node.source)], }; } mapJSXStringLiteral(node: HermesNode): HermesNode { // Babel expects StringLiterals in JSX, // but Hermes uses JSXStringLiteral to attach the raw value without // having to internally attach it to every single string literal. return { type: 'StringLiteral', loc: node.loc, start: node.start, end: node.end, value: node.value, }; } mapFunction(node: HermesNode): HermesNode { // Remove the first parameter if it is a this-type annotation, // which is not recognized by Babel. if (node.params.length !== 0 && node.params[0].name === 'this') { node.params.shift(); } return this.mapNodeDefault(node); } /** * If Babel (the version we target) does not support a type annotation we * parse, we need to return some other valid type annotation in its place. */ mapUnsupportedTypeAnnotation(node: HermesNode): HermesNode { return { type: 'AnyTypeAnnotation', loc: node.loc, start: node.start, end: node.end, }; } mapBigIntLiteral(node: HermesNode): HermesNode { node.value = this.getBigIntLiteralValue(node.bigint).value; return node; } mapBigIntLiteralTypeAnnotation(node: HermesNode): HermesNode { node.value = this.getBigIntLiteralValue(node.raw).value; return node; } /** * Babel does not parse the bigint keyword type as the keyword node. * So we need to down-level the AST to a plain GenericTypeAnnotation */ mapBigIntTypeAnnotation(node: HermesNode): HermesNode { return { type: 'GenericTypeAnnotation', id: { type: 'Identifier', name: 'bigint', loc: node.loc, start: node.start, end: node.end, }, typeParameters: null, loc: node.loc, start: node.start, end: node.end, }; } mapPrivateProperty(nodeUnprocessed: HermesNode): HermesNode { const node = this.mapNodeDefault(nodeUnprocessed); node.key = { type: 'PrivateName', id: { ...node.key, // babel doesn't include the hash in the identifier start: node.key.start + 1, loc: { ...node.key.loc, start: { ...node.key.loc.start, column: node.key.loc.start.column + 1, }, }, }, start: node.key.start, end: node.key.end, loc: node.key.loc, }; return node; } mapPrivateName(node: HermesNode): HermesNode { // babel doesn't include the hash in the identifier node.id.start += 1; node.id.loc.start.column += 1; return node; } mapExportNamespaceSpecifier(nodeUnprocessed: HermesNode): HermesNode { const node = this.mapNodeDefault(nodeUnprocessed); // the hermes AST emits the location as the location of the entire export // but babel emits the location as *just* the "* as id" bit // the end will always align with the end of the identifier (ezpz) // but the start will align with the "*" token - which we can't recover from just the AST // so we just fudge the start location a bit to get it "good enough" // it will be wrong if the AST is anything like "export * as x from 'y'"... but oh well node.start = node.start + 'export '.length; node.loc.start.column = node.loc.start.column + 'export '.length; node.end = node.exported.end; node.loc.end = { column: node.exported.loc.end.column, line: node.exported.loc.end.line, }; return node; } mapTypeofTypeAnnotation(nodeUnprocessed: HermesNode): HermesNode { nodeUnprocessed.argument = { type: 'GenericTypeAnnotation', id: nodeUnprocessed.argument, typeParameters: null, loc: nodeUnprocessed.argument.loc, }; return this.mapNodeDefault(nodeUnprocessed); } mapQualifiedTypeofIdentifier(nodeUnprocessed: HermesNode): HermesNode { nodeUnprocessed.type = 'QualifiedTypeIdentifier'; return this.mapNodeDefault(nodeUnprocessed); } mapDeclareVariable(nodeUnprocessed: HermesNode): HermesNode { delete nodeUnprocessed.kind; return this.mapNodeDefault(nodeUnprocessed); } mapDeclareEnum(nodeUnprocessed: HermesNode): HermesNode { nodeUnprocessed.id.typeAnnotation = this.mapUnsupportedTypeAnnotation( nodeUnprocessed.body, ); delete nodeUnprocessed.body; nodeUnprocessed.type = 'DeclareVariable'; return this.mapDeclareVariable(nodeUnprocessed); } mapDeclareComponent(nodeUnprocessed: HermesNode): HermesNode { nodeUnprocessed.id.typeAnnotation = this.mapUnsupportedTypeAnnotation(nodeUnprocessed); delete nodeUnprocessed.params; delete nodeUnprocessed.rest; delete nodeUnprocessed.typeParameters; delete nodeUnprocessed.rendersType; nodeUnprocessed.type = 'DeclareVariable'; return this.mapDeclareVariable(nodeUnprocessed); } mapJSXElement(nodeUnprocessed: HermesNode): HermesNode { delete nodeUnprocessed.openingElement.typeArguments; return this.mapNodeDefault(nodeUnprocessed); } mapComponentDeclaration(nodeUnprocessed: HermesNode): HermesNode { let rendersType = nodeUnprocessed.rendersType; if (rendersType == null) { // Create empty loc for return type annotation nodes const createRendersTypeLoc = () => ({ loc: { start: {...nodeUnprocessed.body.loc.end}, end: {...nodeUnprocessed.body.loc.end}, rangeStart: nodeUnprocessed.body.loc.rangeStart, rangeEnd: nodeUnprocessed.body.loc.rangeEnd, }, }); rendersType = { type: 'GenericTypeAnnotation', id: { type: 'QualifiedTypeIdentifier', qualification: { type: 'Identifier', name: 'React', ...createRendersTypeLoc(), }, id: { type: 'Identifier', name: 'Node', ...createRendersTypeLoc(), }, }, typeParameters: null, ...createRendersTypeLoc(), }; } function getParamName(paramName: HermesNode): string { switch (paramName.type) { case 'Identifier': return paramName.name; case 'StringLiteral': return paramName.value; default: throw createSyntaxError( paramName, `Unknown Component parameter name type of "${paramName.type}"`, ); } } const properties = nodeUnprocessed.params.map(param => { switch (param.type) { case 'RestElement': { delete param.typeAnnotation; return param; } case 'ComponentParameter': { if (getParamName(param.name) === 'ref') { throw createSyntaxError( param, 'Component parameters named "ref" are currently not supported', ); } if (param.name.type === 'Identifier') { delete param.name.typeAnnotation; } if (param.local.type === 'AssignmentPattern') { delete param.local.left.typeAnnotation; delete param.local.left.optional; } else { delete param.local.typeAnnotation; delete param.local.optional; } return { type: 'ObjectProperty', key: param.name, value: param.local, method: false, shorthand: param.shorthand, computed: false, loc: param.loc, start: param.start, end: param.end, }; } default: { throw createSyntaxError( param, `Unknown Component parameter type of "${param.type}"`, ); } } }); const paramsLoc = (() => { if (properties.length === 0) { // No props, approximate range via existing nodes. const startLoc = nodeUnprocessed.typeParameters != null ? nodeUnprocessed.typeParameters.loc : nodeUnprocessed.id.loc; return { start: startLoc.end, end: rendersType.loc.start, startRange: startLoc.endRange, endRange: rendersType.loc.startRange, }; } return { start: properties[0].loc.start, end: properties[properties.length - 1].loc.end, startRange: properties[0].loc.startRange, endRange: properties[properties.length - 1].loc.endRange, }; })(); // Create empty loc for type annotation nodes const createParamsTypeLoc = () => ({ loc: { start: {...paramsLoc.end}, end: {...paramsLoc.end}, startRange: paramsLoc.endRange, endRange: paramsLoc.endRange, }, }); const params = [ { type: 'ObjectPattern', properties, typeAnnotation: { type: 'TypeAnnotation', typeAnnotation: { type: 'GenericTypeAnnotation', id: { type: 'Identifier', name: '$ReadOnly', ...createParamsTypeLoc(), }, typeParameters: { type: 'TypeParameterInstantiation', params: [ { type: 'ObjectTypeAnnotation', callProperties: [], properties: [], indexers: [], internalSlots: [], exact: false, inexact: true, ...createParamsTypeLoc(), }, ], ...createParamsTypeLoc(), }, ...createParamsTypeLoc(), }, ...createParamsTypeLoc(), }, loc: paramsLoc, }, ]; const functionComponent = { type: 'FunctionDeclaration', id: nodeUnprocessed.id, typeParameters: nodeUnprocessed.typeParameters, params, returnType: rendersType, body: nodeUnprocessed.body, async: false, generator: false, predicate: null, loc: nodeUnprocessed.loc, }; return this.mapNodeDefault(functionComponent); } }