/** * 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 */ 'use strict'; import type { BooleanTypeAnnotation, DoubleTypeAnnotation, EventTypeAnnotation, FloatTypeAnnotation, Int32TypeAnnotation, NamedShape, NativeModuleAliasMap, NativeModuleBaseTypeAnnotation, NativeModuleEnumDeclaration, NativeModuleEnumMap, NativeModuleFunctionTypeAnnotation, NativeModuleGenericObjectTypeAnnotation, NativeModuleMixedTypeAnnotation, NativeModuleNumberTypeAnnotation, NativeModuleObjectTypeAnnotation, NativeModulePromiseTypeAnnotation, NativeModuleTypeAliasTypeAnnotation, NativeModuleTypeAnnotation, NativeModuleUnionTypeAnnotation, Nullable, NumberLiteralTypeAnnotation, ObjectTypeAnnotation, ReservedTypeAnnotation, StringLiteralTypeAnnotation, StringLiteralUnionTypeAnnotation, StringTypeAnnotation, VoidTypeAnnotation, } from '../CodegenSchema'; import type {Parser} from './parser'; import type { ParserErrorCapturer, TypeDeclarationMap, TypeResolutionStatus, } from './utils'; const { throwIfArrayElementTypeAnnotationIsUnsupported, throwIfPartialNotAnnotatingTypeParameter, throwIfPartialWithMoreParameter, } = require('./error-utils'); const { ParserError, UnsupportedTypeAnnotationParserError, UnsupportedUnionTypeAnnotationParserError, } = require('./errors'); const { assertGenericTypeAnnotationHasExactlyOneTypeParameter, translateFunctionTypeAnnotation, unwrapNullable, wrapNullable, } = require('./parsers-commons'); const {nullGuard} = require('./parsers-utils'); const {isModuleRegistryCall} = require('./utils'); function emitBoolean(nullable: boolean): Nullable { return wrapNullable(nullable, { type: 'BooleanTypeAnnotation', }); } function emitInt32(nullable: boolean): Nullable { return wrapNullable(nullable, { type: 'Int32TypeAnnotation', }); } function emitInt32Prop( name: string, optional: boolean, ): NamedShape { return { name, optional, typeAnnotation: { type: 'Int32TypeAnnotation', }, }; } function emitNumber( nullable: boolean, ): Nullable { return wrapNullable(nullable, { type: 'NumberTypeAnnotation', }); } function emitRootTag(nullable: boolean): Nullable { return wrapNullable(nullable, { type: 'ReservedTypeAnnotation', name: 'RootTag', }); } function emitDouble(nullable: boolean): Nullable { return wrapNullable(nullable, { type: 'DoubleTypeAnnotation', }); } function emitDoubleProp( name: string, optional: boolean, ): NamedShape { return { name, optional, typeAnnotation: { type: 'DoubleTypeAnnotation', }, }; } function emitVoid(nullable: boolean): Nullable { return wrapNullable(nullable, { type: 'VoidTypeAnnotation', }); } function emitStringish(nullable: boolean): Nullable { return wrapNullable(nullable, { type: 'StringTypeAnnotation', }); } function emitFunction( nullable: boolean, hasteModuleName: string, typeAnnotation: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, translateTypeAnnotation: $FlowFixMe, parser: Parser, ): Nullable { const translateFunctionTypeAnnotationValue: NativeModuleFunctionTypeAnnotation = translateFunctionTypeAnnotation( hasteModuleName, typeAnnotation, types, aliasMap, enumMap, tryParse, cxxOnly, translateTypeAnnotation, parser, ); return wrapNullable(nullable, translateFunctionTypeAnnotationValue); } function emitMixed( nullable: boolean, ): Nullable { return wrapNullable(nullable, { type: 'MixedTypeAnnotation', }); } function emitNumberLiteral( nullable: boolean, value: number, ): Nullable { return wrapNullable(nullable, { type: 'NumberLiteralTypeAnnotation', value, }); } function emitString(nullable: boolean): Nullable { return wrapNullable(nullable, { type: 'StringTypeAnnotation', }); } function emitStringLiteral( nullable: boolean, value: string, ): Nullable { return wrapNullable(nullable, { type: 'StringLiteralTypeAnnotation', value, }); } function emitStringProp( name: string, optional: boolean, ): NamedShape { return { name, optional, typeAnnotation: { type: 'StringTypeAnnotation', }, }; } function typeAliasResolution( typeResolution: TypeResolutionStatus, objectTypeAnnotation: ObjectTypeAnnotation< Nullable, >, aliasMap: {...NativeModuleAliasMap}, nullable: boolean, ): | Nullable | Nullable>> { if (!typeResolution.successful) { return wrapNullable(nullable, objectTypeAnnotation); } /** * All aliases RHS are required. */ aliasMap[typeResolution.name] = objectTypeAnnotation; /** * Nullability of type aliases is transitive. * * Consider this case: * * type Animal = ?{ * name: string, * }; * * type B = Animal * * export interface Spec extends TurboModule { * +greet: (animal: B) => void; * } * * In this case, we follow B to Animal, and then Animal to ?{name: string}. * * We: * 1. Replace `+greet: (animal: B) => void;` with `+greet: (animal: ?Animal) => void;`, * 2. Pretend that Animal = {name: string}. * * Why do we do this? * 1. In ObjC, we need to generate a struct called Animal, not B. * 2. This design is simpler than managing nullability within both the type alias usage, and the type alias RHS. * 3. What does it mean for a C++ struct, which is what this type alias RHS will generate, to be nullable? ¯\_(ツ)_/¯ * Nullability is a concept that only makes sense when talking about instances (i.e: usages) of the C++ structs. * Hence, it's better to manage nullability within the actual TypeAliasTypeAnnotation nodes, and not the * associated ObjectTypeAnnotations. */ return wrapNullable(nullable, { type: 'TypeAliasTypeAnnotation', name: typeResolution.name, }); } function typeEnumResolution( typeAnnotation: $FlowFixMe, typeResolution: TypeResolutionStatus, nullable: boolean, hasteModuleName: string, enumMap: {...NativeModuleEnumMap}, parser: Parser, ): Nullable { if (!typeResolution.successful || typeResolution.type !== 'enum') { throw new UnsupportedTypeAnnotationParserError( hasteModuleName, typeAnnotation, parser.language(), ); } const enumName = typeResolution.name; const enumMemberType = parser.parseEnumMembersType(typeAnnotation); try { parser.validateEnumMembersSupported(typeAnnotation, enumMemberType); } catch (e) { if (e instanceof Error) { throw new ParserError( hasteModuleName, typeAnnotation, `Failed parsing the enum ${enumName} in ${hasteModuleName} with the error: ${e.message}`, ); } else { throw e; } } const enumMembers = parser.parseEnumMembers(typeAnnotation); enumMap[enumName] = { name: enumName, type: 'EnumDeclarationWithMembers', memberType: enumMemberType, members: enumMembers, }; return wrapNullable(nullable, { name: enumName, type: 'EnumDeclaration', memberType: enumMemberType, }); } function emitPromise( hasteModuleName: string, typeAnnotation: $FlowFixMe, parser: Parser, nullable: boolean, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, translateTypeAnnotation: $FlowFixMe, ): Nullable { assertGenericTypeAnnotationHasExactlyOneTypeParameter( hasteModuleName, typeAnnotation, parser, ); const elementType = typeAnnotation.typeParameters.params[0]; if ( elementType.type === 'ExistsTypeAnnotation' || elementType.type === 'EmptyTypeAnnotation' ) { return wrapNullable(nullable, { type: 'PromiseTypeAnnotation', elementType: { type: 'VoidTypeAnnotation', }, }); } else { try { return wrapNullable(nullable, { type: 'PromiseTypeAnnotation', elementType: translateTypeAnnotation( hasteModuleName, typeAnnotation.typeParameters.params[0], types, aliasMap, enumMap, tryParse, cxxOnly, parser, ), }); } catch { return wrapNullable(nullable, { type: 'PromiseTypeAnnotation', elementType: { type: 'VoidTypeAnnotation', }, }); } } } function emitGenericObject( nullable: boolean, ): Nullable { return wrapNullable(nullable, { type: 'GenericObjectTypeAnnotation', }); } function emitDictionary( nullable: boolean, valueType: Nullable, ): Nullable { return wrapNullable(nullable, { type: 'GenericObjectTypeAnnotation', dictionaryValueType: valueType, }); } function emitObject( nullable: boolean, properties: Array<$FlowFixMe>, ): Nullable { return wrapNullable(nullable, { type: 'ObjectTypeAnnotation', properties, }); } function emitFloat(nullable: boolean): Nullable { return wrapNullable(nullable, { type: 'FloatTypeAnnotation', }); } function emitFloatProp( name: string, optional: boolean, ): NamedShape { return { name, optional, typeAnnotation: { type: 'FloatTypeAnnotation', }, }; } function emitUnion( nullable: boolean, hasteModuleName: string, typeAnnotation: $FlowFixMe, parser: Parser, ): Nullable< NativeModuleUnionTypeAnnotation | StringLiteralUnionTypeAnnotation, > { // Get all the literals by type // Verify they are all the same // If string, persist as StringLiteralUnionType // If number, persist as NumberTypeAnnotation (TODO: Number literal) const unionTypes = parser.remapUnionTypeAnnotationMemberNames( typeAnnotation.types, ); // Only support unionTypes of the same kind if (unionTypes.length > 1) { throw new UnsupportedUnionTypeAnnotationParserError( hasteModuleName, typeAnnotation, unionTypes, ); } if (unionTypes[0] === 'StringTypeAnnotation') { // Reprocess as a string literal union return emitStringLiteralUnion( nullable, hasteModuleName, typeAnnotation, parser, ); } return wrapNullable(nullable, { type: 'UnionTypeAnnotation', memberType: unionTypes[0], }); } function emitStringLiteralUnion( nullable: boolean, hasteModuleName: string, typeAnnotation: $FlowFixMe, parser: Parser, ): Nullable { const stringLiterals = parser.getStringLiteralUnionTypeAnnotationStringLiterals( typeAnnotation.types, ); return wrapNullable(nullable, { type: 'StringLiteralUnionTypeAnnotation', types: stringLiterals.map(stringLiteral => ({ type: 'StringLiteralTypeAnnotation', value: stringLiteral, })), }); } function translateArrayTypeAnnotation( hasteModuleName: string, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, enumMap: {...NativeModuleEnumMap}, cxxOnly: boolean, arrayType: 'Array' | 'ReadonlyArray', elementType: $FlowFixMe, nullable: boolean, translateTypeAnnotation: $FlowFixMe, parser: Parser, ): Nullable { try { /** * TODO(T72031674): Migrate all our NativeModule specs to not use * invalid Array ElementTypes. Then, make the elementType a required * parameter. */ const [_elementType, isElementTypeNullable] = unwrapNullable<$FlowFixMe>( translateTypeAnnotation( hasteModuleName, elementType, types, aliasMap, enumMap, /** * TODO(T72031674): Ensure that all ParsingErrors that are thrown * while parsing the array element don't get captured and collected. * Why? If we detect any parsing error while parsing the element, * we should default it to null down the line, here. This is * the correct behaviour until we migrate all our NativeModule specs * to be parseable. */ nullGuard, cxxOnly, parser, ), ); throwIfArrayElementTypeAnnotationIsUnsupported( hasteModuleName, elementType, arrayType, _elementType.type, ); return wrapNullable(nullable, { type: 'ArrayTypeAnnotation', // $FlowFixMe[incompatible-call] elementType: wrapNullable(isElementTypeNullable, _elementType), }); } catch (ex) { return wrapNullable(nullable, { type: 'ArrayTypeAnnotation', elementType: { type: 'AnyTypeAnnotation', }, }); } } function emitArrayType( hasteModuleName: string, typeAnnotation: $FlowFixMe, parser: Parser, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, enumMap: {...NativeModuleEnumMap}, cxxOnly: boolean, nullable: boolean, translateTypeAnnotation: $FlowFixMe, ): Nullable { assertGenericTypeAnnotationHasExactlyOneTypeParameter( hasteModuleName, typeAnnotation, parser, ); return translateArrayTypeAnnotation( hasteModuleName, types, aliasMap, enumMap, cxxOnly, typeAnnotation.type, typeAnnotation.typeParameters.params[0], nullable, translateTypeAnnotation, parser, ); } function Visitor(infoMap: {isComponent: boolean, isModule: boolean}): { [type: string]: (node: $FlowFixMe) => void, } { return { CallExpression(node: $FlowFixMe) { if ( node.callee.type === 'Identifier' && node.callee.name === 'codegenNativeComponent' ) { infoMap.isComponent = true; } if (isModuleRegistryCall(node)) { infoMap.isModule = true; } }, InterfaceExtends(node: $FlowFixMe) { if (node.id.name === 'TurboModule') { infoMap.isModule = true; } }, TSInterfaceDeclaration(node: $FlowFixMe) { if ( Array.isArray(node.extends) && node.extends.some( extension => extension.expression.name === 'TurboModule', ) ) { infoMap.isModule = true; } }, }; } function emitPartial( nullable: boolean, hasteModuleName: string, typeAnnotation: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, parser: Parser, ): Nullable { throwIfPartialWithMoreParameter(typeAnnotation); throwIfPartialNotAnnotatingTypeParameter(typeAnnotation, types, parser); const annotatedElement = parser.extractAnnotatedElement( typeAnnotation, types, ); const annotatedElementProperties = parser.getAnnotatedElementProperties(annotatedElement); const partialProperties = parser.computePartialProperties( annotatedElementProperties, hasteModuleName, types, aliasMap, enumMap, tryParse, cxxOnly, ); return emitObject(nullable, partialProperties); } function emitCommonTypes( hasteModuleName: string, types: TypeDeclarationMap, typeAnnotation: $FlowFixMe, aliasMap: {...NativeModuleAliasMap}, enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, nullable: boolean, parser: Parser, ): $FlowFixMe { const typeMap = { Stringish: emitStringish, Int32: emitInt32, Double: emitDouble, Float: emitFloat, UnsafeObject: emitGenericObject, Object: emitGenericObject, $Partial: emitPartial, Partial: emitPartial, BooleanTypeAnnotation: emitBoolean, NumberTypeAnnotation: emitNumber, VoidTypeAnnotation: emitVoid, StringTypeAnnotation: emitString, MixedTypeAnnotation: cxxOnly ? emitMixed : emitGenericObject, }; const typeAnnotationName = parser.convertKeywordToTypeAnnotation( typeAnnotation.type, ); // $FlowFixMe[invalid-computed-prop] const simpleEmitter = typeMap[typeAnnotationName]; if (simpleEmitter) { return simpleEmitter(nullable); } const genericTypeAnnotationName = parser.getTypeAnnotationName(typeAnnotation); // $FlowFixMe[invalid-computed-prop] const emitter = typeMap[genericTypeAnnotationName]; if (!emitter) { return null; } return emitter( nullable, hasteModuleName, typeAnnotation, types, aliasMap, enumMap, tryParse, cxxOnly, parser, ); } function emitBoolProp( name: string, optional: boolean, ): NamedShape { return { name, optional, typeAnnotation: { type: 'BooleanTypeAnnotation', }, }; } function emitMixedProp( name: string, optional: boolean, ): NamedShape { return { name, optional, typeAnnotation: { type: 'MixedTypeAnnotation', }, }; } function emitObjectProp( name: string, optional: boolean, parser: Parser, typeAnnotation: $FlowFixMe, extractArrayElementType: ( typeAnnotation: $FlowFixMe, name: string, parser: Parser, ) => EventTypeAnnotation, ): NamedShape { return { name, optional, typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), }; } function emitUnionProp( name: string, optional: boolean, parser: Parser, typeAnnotation: $FlowFixMe, ): NamedShape { return { name, optional, typeAnnotation: { type: 'StringEnumTypeAnnotation', options: typeAnnotation.types.map(option => parser.getLiteralValue(option), ), }, }; } module.exports = { emitArrayType, emitBoolean, emitBoolProp, emitDouble, emitDoubleProp, emitFloat, emitFloatProp, emitFunction, emitInt32, emitInt32Prop, emitMixedProp, emitNumber, emitNumberLiteral, emitGenericObject, emitDictionary, emitObject, emitPromise, emitRootTag, emitVoid, emitString, emitStringish, emitStringProp, emitStringLiteral, emitMixed, emitUnion, emitPartial, emitCommonTypes, typeAliasResolution, typeEnumResolution, translateArrayTypeAnnotation, Visitor, emitObjectProp, emitUnionProp, };