/** * 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-local * @format */ 'use strict'; import type { NamedShape, NativeModuleAliasMap, NativeModuleArrayTypeAnnotation, NativeModuleBaseTypeAnnotation, NativeModuleFunctionTypeAnnotation, NativeModuleParamTypeAnnotation, NativeModulePropertyShape, NativeModuleSchema, Nullable, } from '../../../CodegenSchema.js'; import type {ParserErrorCapturer, TypeDeclarationMap} from '../../utils'; import type {NativeModuleTypeAnnotation} from '../../../CodegenSchema.js'; const {nullGuard} = require('../../parsers-utils'); const {throwIfMoreThanOneModuleRegistryCalls} = require('../../error-utils'); const {visit, isModuleRegistryCall} = require('../../utils'); const {resolveTypeAnnotation, getTypes} = require('../utils.js'); const { unwrapNullable, wrapNullable, assertGenericTypeAnnotationHasExactlyOneTypeParameter, emitMixedTypeAnnotation, emitUnionTypeAnnotation, translateDefault, } = require('../../parsers-commons'); const { emitBoolean, emitDouble, emitFloat, emitFunction, emitNumber, emitInt32, emitObject, emitPromise, emitRootTag, emitVoid, emitString, emitStringish, typeAliasResolution, } = require('../../parsers-primitives'); const { UnnamedFunctionParamParserError, UnsupportedArrayElementTypeAnnotationParserError, UnsupportedTypeAnnotationParserError, UnsupportedObjectPropertyTypeAnnotationParserError, IncorrectModuleRegistryCallArgumentTypeParserError, } = require('../../errors.js'); const {verifyPlatforms} = require('../../utils'); const { throwIfUnsupportedFunctionReturnTypeAnnotationParserError, throwIfModuleInterfaceNotFound, throwIfModuleInterfaceIsMisnamed, throwIfPropertyValueTypeIsUnsupported, throwIfUnusedModuleInterfaceParserError, throwIfWrongNumberOfCallExpressionArgs, throwIfIncorrectModuleRegistryCallTypeParameterParserError, throwIfUntypedModule, throwIfModuleTypeIsUnsupported, throwIfMoreThanOneModuleInterfaceParserError, throwIfUnsupportedFunctionParamTypeAnnotationParserError, } = require('../../error-utils'); const {FlowParser} = require('../parser.js'); const {getKeyName} = require('../../parsers-commons'); const language = 'Flow'; const parser = new FlowParser(); function translateArrayTypeAnnotation( hasteModuleName: string, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, cxxOnly: boolean, flowArrayType: 'Array' | '$ReadOnlyArray', flowElementType: $FlowFixMe, nullable: boolean, ): 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( translateTypeAnnotation( hasteModuleName, flowElementType, types, aliasMap, /** * 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, ), ); if (elementType.type === 'VoidTypeAnnotation') { throw new UnsupportedArrayElementTypeAnnotationParserError( hasteModuleName, flowElementType, flowArrayType, 'void', language, ); } if (elementType.type === 'PromiseTypeAnnotation') { throw new UnsupportedArrayElementTypeAnnotationParserError( hasteModuleName, flowElementType, flowArrayType, 'Promise', language, ); } if (elementType.type === 'FunctionTypeAnnotation') { throw new UnsupportedArrayElementTypeAnnotationParserError( hasteModuleName, flowElementType, flowArrayType, 'FunctionTypeAnnotation', language, ); } const finalTypeAnnotation: NativeModuleArrayTypeAnnotation< Nullable, > = { type: 'ArrayTypeAnnotation', elementType: wrapNullable(isElementTypeNullable, elementType), }; return wrapNullable(nullable, finalTypeAnnotation); } catch (ex) { return wrapNullable(nullable, { type: 'ArrayTypeAnnotation', }); } } function translateTypeAnnotation( hasteModuleName: string, /** * TODO(T71778680): Flow-type this node. */ flowTypeAnnotation: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, ): Nullable { const {nullable, typeAnnotation, typeAliasResolutionStatus} = resolveTypeAnnotation(flowTypeAnnotation, types); switch (typeAnnotation.type) { case 'GenericTypeAnnotation': { switch (typeAnnotation.id.name) { case 'RootTag': { return emitRootTag(nullable); } case 'Promise': { return emitPromise( hasteModuleName, typeAnnotation, language, nullable, ); } case 'Array': case '$ReadOnlyArray': { assertGenericTypeAnnotationHasExactlyOneTypeParameter( hasteModuleName, typeAnnotation, language, ); return translateArrayTypeAnnotation( hasteModuleName, types, aliasMap, cxxOnly, typeAnnotation.type, typeAnnotation.typeParameters.params[0], nullable, ); } case '$ReadOnly': { assertGenericTypeAnnotationHasExactlyOneTypeParameter( hasteModuleName, typeAnnotation, language, ); const [paramType, isParamNullable] = unwrapNullable( translateTypeAnnotation( hasteModuleName, typeAnnotation.typeParameters.params[0], types, aliasMap, tryParse, cxxOnly, ), ); return wrapNullable(nullable || isParamNullable, paramType); } case 'Stringish': { return emitStringish(nullable); } case 'Int32': { return emitInt32(nullable); } case 'Double': { return emitDouble(nullable); } case 'Float': { return emitFloat(nullable); } case 'UnsafeObject': case 'Object': { return emitObject(nullable); } default: { return translateDefault( hasteModuleName, typeAnnotation, types, nullable, parser, ); } } } case 'ObjectTypeAnnotation': { const objectTypeAnnotation = { type: 'ObjectTypeAnnotation', // $FlowFixMe[missing-type-arg] properties: ([ ...typeAnnotation.properties, ...typeAnnotation.indexers, ]: Array<$FlowFixMe>) .map>>( property => { return tryParse(() => { if ( property.type !== 'ObjectTypeProperty' && property.type !== 'ObjectTypeIndexer' ) { throw new UnsupportedObjectPropertyTypeAnnotationParserError( hasteModuleName, property, property.type, language, ); } const {optional = false} = property; const name = getKeyName(property, hasteModuleName, language); if (property.type === 'ObjectTypeIndexer') { return { name, optional, typeAnnotation: emitObject(nullable), }; } const [propertyTypeAnnotation, isPropertyNullable] = unwrapNullable( translateTypeAnnotation( hasteModuleName, property.value, types, aliasMap, tryParse, cxxOnly, ), ); if ( propertyTypeAnnotation.type === 'FunctionTypeAnnotation' || propertyTypeAnnotation.type === 'PromiseTypeAnnotation' || propertyTypeAnnotation.type === 'VoidTypeAnnotation' ) { throwIfPropertyValueTypeIsUnsupported( hasteModuleName, property.value, property.key, propertyTypeAnnotation.type, language, ); } else { return { name, optional, typeAnnotation: wrapNullable( isPropertyNullable, propertyTypeAnnotation, ), }; } }); }, ) .filter(Boolean), }; return typeAliasResolution( typeAliasResolutionStatus, objectTypeAnnotation, aliasMap, nullable, ); } case 'BooleanTypeAnnotation': { return emitBoolean(nullable); } case 'NumberTypeAnnotation': { return emitNumber(nullable); } case 'VoidTypeAnnotation': { return emitVoid(nullable); } case 'StringTypeAnnotation': { return emitString(nullable); } case 'FunctionTypeAnnotation': { const translateFunctionTypeAnnotationValue: NativeModuleFunctionTypeAnnotation = translateFunctionTypeAnnotation( hasteModuleName, typeAnnotation, types, aliasMap, tryParse, cxxOnly, ); return emitFunction(nullable, translateFunctionTypeAnnotationValue); } case 'UnionTypeAnnotation': { if (cxxOnly) { return emitUnionTypeAnnotation( nullable, hasteModuleName, typeAnnotation, language, ); } // Fallthrough } case 'MixedTypeAnnotation': { if (cxxOnly) { return emitMixedTypeAnnotation(nullable); } // Fallthrough } default: { throw new UnsupportedTypeAnnotationParserError( hasteModuleName, typeAnnotation, language, ); } } } function translateFunctionTypeAnnotation( hasteModuleName: string, // TODO(T71778680): This is a FunctionTypeAnnotation. Type this. flowFunctionTypeAnnotation: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, ): NativeModuleFunctionTypeAnnotation { type Param = NamedShape>; const params: Array = []; for (const flowParam of (flowFunctionTypeAnnotation.params: $ReadOnlyArray<$FlowFixMe>)) { const parsedParam = tryParse(() => { if (flowParam.name == null) { throw new UnnamedFunctionParamParserError( flowParam, hasteModuleName, language, ); } const paramName = flowParam.name.name; const [paramTypeAnnotation, isParamTypeAnnotationNullable] = unwrapNullable( translateTypeAnnotation( hasteModuleName, flowParam.typeAnnotation, types, aliasMap, tryParse, cxxOnly, ), ); if ( paramTypeAnnotation.type === 'VoidTypeAnnotation' || paramTypeAnnotation.type === 'PromiseTypeAnnotation' ) { return throwIfUnsupportedFunctionParamTypeAnnotationParserError( hasteModuleName, flowParam.typeAnnotation, paramName, paramTypeAnnotation.type, ); } return { name: flowParam.name.name, optional: flowParam.optional, typeAnnotation: wrapNullable( isParamTypeAnnotationNullable, paramTypeAnnotation, ), }; }); if (parsedParam != null) { params.push(parsedParam); } } const [returnTypeAnnotation, isReturnTypeAnnotationNullable] = unwrapNullable( translateTypeAnnotation( hasteModuleName, flowFunctionTypeAnnotation.returnType, types, aliasMap, tryParse, cxxOnly, ), ); throwIfUnsupportedFunctionReturnTypeAnnotationParserError( hasteModuleName, flowFunctionTypeAnnotation, 'FunctionTypeAnnotation', language, cxxOnly, returnTypeAnnotation.type, ); return { type: 'FunctionTypeAnnotation', returnTypeAnnotation: wrapNullable( isReturnTypeAnnotationNullable, returnTypeAnnotation, ), params, }; } function buildPropertySchema( hasteModuleName: string, // TODO(T71778680): This is an ObjectTypeProperty containing either: // - a FunctionTypeAnnotation or GenericTypeAnnotation // - a NullableTypeAnnoation containing a FunctionTypeAnnotation or GenericTypeAnnotation // Flow type this node property: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, ): NativeModulePropertyShape { let nullable = false; let {key, value} = property; const methodName: string = key.name; ({nullable, typeAnnotation: value} = resolveTypeAnnotation(value, types)); throwIfModuleTypeIsUnsupported( hasteModuleName, property.value, property.key.name, value.type, language, ); return { name: methodName, optional: property.optional, typeAnnotation: wrapNullable( nullable, translateFunctionTypeAnnotation( hasteModuleName, value, types, aliasMap, tryParse, cxxOnly, ), ), }; } function isModuleInterface(node: $FlowFixMe) { return ( node.type === 'InterfaceDeclaration' && node.extends.length === 1 && node.extends[0].type === 'InterfaceExtends' && node.extends[0].id.name === 'TurboModule' ); } function buildModuleSchema( hasteModuleName: string, /** * TODO(T71778680): Flow-type this node. */ ast: $FlowFixMe, tryParse: ParserErrorCapturer, ): NativeModuleSchema { const types = getTypes(ast); const moduleSpecs = (Object.values(types): $ReadOnlyArray<$FlowFixMe>).filter( isModuleInterface, ); throwIfModuleInterfaceNotFound( moduleSpecs.length, hasteModuleName, ast, language, ); throwIfMoreThanOneModuleInterfaceParserError( hasteModuleName, moduleSpecs, language, ); const [moduleSpec] = moduleSpecs; throwIfModuleInterfaceIsMisnamed(hasteModuleName, moduleSpec.id, language); // Parse Module Names const moduleName = tryParse((): string => { const callExpressions = []; visit(ast, { CallExpression(node) { if (isModuleRegistryCall(node)) { callExpressions.push(node); } }, }); throwIfUnusedModuleInterfaceParserError( hasteModuleName, moduleSpec, callExpressions, language, ); throwIfMoreThanOneModuleRegistryCalls( hasteModuleName, callExpressions, callExpressions.length, language, ); const [callExpression] = callExpressions; const {typeArguments} = callExpression; const methodName = callExpression.callee.property.name; throwIfWrongNumberOfCallExpressionArgs( hasteModuleName, callExpression, methodName, callExpression.arguments.length, language, ); if (callExpression.arguments[0].type !== 'Literal') { const {type} = callExpression.arguments[0]; throw new IncorrectModuleRegistryCallArgumentTypeParserError( hasteModuleName, callExpression.arguments[0], methodName, type, language, ); } const $moduleName = callExpression.arguments[0].value; throwIfUntypedModule( typeArguments, hasteModuleName, callExpression, methodName, $moduleName, language, ); throwIfIncorrectModuleRegistryCallTypeParameterParserError( hasteModuleName, typeArguments, methodName, $moduleName, language, ); return $moduleName; }); const moduleNames = moduleName == null ? [] : [moduleName]; // Some module names use platform suffix to indicate platform-exclusive modules. // Eventually this should be made explicit in the Flow type itself. // Also check the hasteModuleName for platform suffix. // Note: this shape is consistent with ComponentSchema. const {cxxOnly, excludedPlatforms} = verifyPlatforms( hasteModuleName, moduleNames, ); // $FlowFixMe[missing-type-arg] return (moduleSpec.body.properties: $ReadOnlyArray<$FlowFixMe>) .filter(property => property.type === 'ObjectTypeProperty') .map(property => { const aliasMap: {...NativeModuleAliasMap} = {}; return tryParse(() => ({ aliasMap: aliasMap, propertyShape: buildPropertySchema( hasteModuleName, property, types, aliasMap, tryParse, cxxOnly, ), })); }) .filter(Boolean) .reduce( (moduleSchema: NativeModuleSchema, {aliasMap, propertyShape}) => { return { type: 'NativeModule', aliases: {...moduleSchema.aliases, ...aliasMap}, spec: { properties: [...moduleSchema.spec.properties, propertyShape], }, moduleNames: moduleSchema.moduleNames, excludedPlatforms: moduleSchema.excludedPlatforms, }; }, { type: 'NativeModule', aliases: {}, spec: {properties: []}, moduleNames: moduleNames, excludedPlatforms: excludedPlatforms.length !== 0 ? [...excludedPlatforms] : undefined, }, ); } module.exports = { buildModuleSchema, };