/** * 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 { Nullable, NamedShape, NativeModuleAliasMap, NativeModuleBaseTypeAnnotation, NativeModuleSchema, NativeModuleTypeAnnotation, NativeModuleFunctionTypeAnnotation, NativeModuleParamTypeAnnotation, NativeModulePropertyShape, SchemaType, NativeModuleEnumMap, OptionsShape, } from '../CodegenSchema.js'; import type {Parser} from './parser'; import type {ParserType} from './errors'; import type {ParserErrorCapturer, TypeDeclarationMap} from './utils'; import type {ComponentSchemaBuilderConfig} from './schema.js'; const { getConfigType, extractNativeModuleName, createParserErrorCapturer, visit, isModuleRegistryCall, verifyPlatforms, } = require('./utils'); const { throwIfPropertyValueTypeIsUnsupported, throwIfUnsupportedFunctionParamTypeAnnotationParserError, throwIfUnsupportedFunctionReturnTypeAnnotationParserError, throwIfModuleTypeIsUnsupported, throwIfUnusedModuleInterfaceParserError, throwIfMoreThanOneModuleRegistryCalls, throwIfWrongNumberOfCallExpressionArgs, throwIfUntypedModule, throwIfIncorrectModuleRegistryCallTypeParameterParserError, throwIfIncorrectModuleRegistryCallArgument, throwIfModuleInterfaceNotFound, throwIfMoreThanOneModuleInterfaceParserError, throwIfModuleInterfaceIsMisnamed, } = require('./error-utils'); const { MissingTypeParameterGenericParserError, MoreThanOneTypeParameterGenericParserError, UnnamedFunctionParamParserError, } = require('./errors'); const invariant = require('invariant'); export type CommandOptions = $ReadOnly<{ supportedCommands: $ReadOnlyArray, }>; // $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser type OptionsAST = Object; function wrapModuleSchema( nativeModuleSchema: NativeModuleSchema, hasteModuleName: string, ): SchemaType { return { modules: { [hasteModuleName]: nativeModuleSchema, }, }; } function unwrapNullable<+T: NativeModuleTypeAnnotation>( x: Nullable, ): [T, boolean] { if (x.type === 'NullableTypeAnnotation') { return [x.typeAnnotation, true]; } return [x, false]; } function wrapNullable<+T: NativeModuleTypeAnnotation>( nullable: boolean, typeAnnotation: T, ): Nullable { if (!nullable) { return typeAnnotation; } return { type: 'NullableTypeAnnotation', typeAnnotation, }; } function assertGenericTypeAnnotationHasExactlyOneTypeParameter( moduleName: string, /** * TODO(T108222691): Use flow-types for @babel/parser */ typeAnnotation: $FlowFixMe, parser: Parser, ) { if (typeAnnotation.typeParameters == null) { throw new MissingTypeParameterGenericParserError( moduleName, typeAnnotation, parser, ); } const typeAnnotationType = parser.typeParameterInstantiation; invariant( typeAnnotation.typeParameters.type === typeAnnotationType, `assertGenericTypeAnnotationHasExactlyOneTypeParameter: Type parameters must be an AST node of type '${typeAnnotationType}'`, ); if (typeAnnotation.typeParameters.params.length !== 1) { throw new MoreThanOneTypeParameterGenericParserError( moduleName, typeAnnotation, parser, ); } } function isObjectProperty(property: $FlowFixMe, language: ParserType): boolean { switch (language) { case 'Flow': return property.type === 'ObjectTypeProperty'; case 'TypeScript': return property.type === 'TSPropertySignature'; default: return false; } } function parseObjectProperty( property: $FlowFixMe, hasteModuleName: string, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, nullable: boolean, translateTypeAnnotation: $FlowFixMe, parser: Parser, ): NamedShape> { const language = parser.language(); const name = parser.getKeyName(property, hasteModuleName); const {optional = false} = property; const languageTypeAnnotation = language === 'TypeScript' ? property.typeAnnotation.typeAnnotation : property.value; const [propertyTypeAnnotation, isPropertyNullable] = unwrapNullable<$FlowFixMe>( translateTypeAnnotation( hasteModuleName, languageTypeAnnotation, types, aliasMap, enumMap, tryParse, cxxOnly, parser, ), ); if ( propertyTypeAnnotation.type === 'FunctionTypeAnnotation' || propertyTypeAnnotation.type === 'PromiseTypeAnnotation' || propertyTypeAnnotation.type === 'VoidTypeAnnotation' ) { throwIfPropertyValueTypeIsUnsupported( hasteModuleName, languageTypeAnnotation, property.key, propertyTypeAnnotation.type, ); } return { name, optional, typeAnnotation: wrapNullable(isPropertyNullable, propertyTypeAnnotation), }; } function translateFunctionTypeAnnotation( hasteModuleName: string, // TODO(T108222691): Use flow-types for @babel/parser // TODO(T71778680): This is a FunctionTypeAnnotation. Type this. functionTypeAnnotation: $FlowFixMe, types: TypeDeclarationMap, aliasMap: {...NativeModuleAliasMap}, enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, translateTypeAnnotation: $FlowFixMe, parser: Parser, ): NativeModuleFunctionTypeAnnotation { type Param = NamedShape>; const params: Array = []; for (const param of parser.getFunctionTypeAnnotationParameters( functionTypeAnnotation, )) { const parsedParam = tryParse(() => { if (parser.getFunctionNameFromParameter(param) == null) { throw new UnnamedFunctionParamParserError(param, hasteModuleName); } const paramName = parser.getParameterName(param); const [paramTypeAnnotation, isParamTypeAnnotationNullable] = unwrapNullable<$FlowFixMe>( translateTypeAnnotation( hasteModuleName, parser.getParameterTypeAnnotation(param), types, aliasMap, enumMap, tryParse, cxxOnly, parser, ), ); if ( paramTypeAnnotation.type === 'VoidTypeAnnotation' || paramTypeAnnotation.type === 'PromiseTypeAnnotation' ) { return throwIfUnsupportedFunctionParamTypeAnnotationParserError( hasteModuleName, param.typeAnnotation, paramName, paramTypeAnnotation.type, ); } return { name: paramName, optional: Boolean(param.optional), typeAnnotation: wrapNullable( isParamTypeAnnotationNullable, paramTypeAnnotation, ), }; }); if (parsedParam != null) { params.push(parsedParam); } } const [returnTypeAnnotation, isReturnTypeAnnotationNullable] = unwrapNullable<$FlowFixMe>( translateTypeAnnotation( hasteModuleName, parser.getFunctionTypeAnnotationReturnType(functionTypeAnnotation), types, aliasMap, enumMap, tryParse, cxxOnly, parser, ), ); throwIfUnsupportedFunctionReturnTypeAnnotationParserError( hasteModuleName, functionTypeAnnotation, 'FunctionTypeAnnotation', cxxOnly, returnTypeAnnotation.type, ); return { type: 'FunctionTypeAnnotation', returnTypeAnnotation: wrapNullable( isReturnTypeAnnotationNullable, returnTypeAnnotation, ), params, }; } function buildPropertySchema( hasteModuleName: string, // TODO(T108222691): [TS] Use flow-types for @babel/parser // TODO(T71778680): [Flow] 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}, enumMap: {...NativeModuleEnumMap}, tryParse: ParserErrorCapturer, cxxOnly: boolean, resolveTypeAnnotation: $FlowFixMe, translateTypeAnnotation: $FlowFixMe, parser: Parser, ): NativeModulePropertyShape { let nullable: boolean = false; let {key, value} = property; const methodName: string = key.name; if (parser.language() === 'TypeScript') { value = property.type === 'TSMethodSignature' ? property : property.typeAnnotation; } ({nullable, typeAnnotation: value} = resolveTypeAnnotation(value, types)); throwIfModuleTypeIsUnsupported( hasteModuleName, property.value, key.name, value.type, parser, ); return { name: methodName, optional: Boolean(property.optional), typeAnnotation: wrapNullable( nullable, translateFunctionTypeAnnotation( hasteModuleName, value, types, aliasMap, enumMap, tryParse, cxxOnly, translateTypeAnnotation, parser, ), ), }; } function buildSchemaFromConfigType( configType: 'module' | 'component' | 'none', filename: ?string, ast: $FlowFixMe, wrapComponentSchema: (config: ComponentSchemaBuilderConfig) => SchemaType, buildComponentSchema: ( ast: $FlowFixMe, parser: Parser, ) => ComponentSchemaBuilderConfig, buildModuleSchema: ( hasteModuleName: string, ast: $FlowFixMe, tryParse: ParserErrorCapturer, parser: Parser, resolveTypeAnnotation: $FlowFixMe, translateTypeAnnotation: $FlowFixMe, ) => NativeModuleSchema, parser: Parser, resolveTypeAnnotation: $FlowFixMe, translateTypeAnnotation: $FlowFixMe, ): SchemaType { switch (configType) { case 'component': { return wrapComponentSchema(buildComponentSchema(ast, parser)); } case 'module': { if (filename === undefined || filename === null) { throw new Error('Filepath expected while parasing a module'); } const nativeModuleName = extractNativeModuleName(filename); const [parsingErrors, tryParse] = createParserErrorCapturer(); const schema = tryParse(() => buildModuleSchema( nativeModuleName, ast, tryParse, parser, resolveTypeAnnotation, translateTypeAnnotation, ), ); if (parsingErrors.length > 0) { /** * TODO(T77968131): We have two options: * - Throw the first error, but indicate there are more then one errors. * - Display all errors, nicely formatted. * * For the time being, we're just throw the first error. **/ throw parsingErrors[0]; } invariant( schema != null, 'When there are no parsing errors, the schema should not be null', ); return wrapModuleSchema(schema, nativeModuleName); } default: return {modules: {}}; } } function buildSchema( contents: string, filename: ?string, wrapComponentSchema: (config: ComponentSchemaBuilderConfig) => SchemaType, buildComponentSchema: ( ast: $FlowFixMe, parser: Parser, ) => ComponentSchemaBuilderConfig, buildModuleSchema: ( hasteModuleName: string, ast: $FlowFixMe, tryParse: ParserErrorCapturer, parser: Parser, resolveTypeAnnotation: $FlowFixMe, translateTypeAnnotation: $FlowFixMe, ) => NativeModuleSchema, Visitor: ({isComponent: boolean, isModule: boolean}) => { [type: string]: (node: $FlowFixMe) => void, }, parser: Parser, resolveTypeAnnotation: $FlowFixMe, translateTypeAnnotation: $FlowFixMe, ): SchemaType { // Early return for non-Spec JavaScript files if ( !contents.includes('codegenNativeComponent') && !contents.includes('TurboModule') ) { return {modules: {}}; } const ast = parser.getAst(contents); const configType = getConfigType(ast, Visitor); return buildSchemaFromConfigType( configType, filename, ast, wrapComponentSchema, buildComponentSchema, buildModuleSchema, parser, resolveTypeAnnotation, translateTypeAnnotation, ); } function createComponentConfig( foundConfig: $FlowFixMe, commandsTypeNames: $FlowFixMe, ): $FlowFixMe { return { ...foundConfig, commandTypeName: commandsTypeNames[0] == null ? null : commandsTypeNames[0].commandTypeName, commandOptionsExpression: commandsTypeNames[0] == null ? null : commandsTypeNames[0].commandOptionsExpression, }; } const parseModuleName = ( hasteModuleName: string, moduleSpec: $FlowFixMe, ast: $FlowFixMe, parser: Parser, ): string => { const callExpressions = []; visit(ast, { CallExpression(node) { if (isModuleRegistryCall(node)) { callExpressions.push(node); } }, }); throwIfUnusedModuleInterfaceParserError( hasteModuleName, moduleSpec, callExpressions, ); throwIfMoreThanOneModuleRegistryCalls( hasteModuleName, callExpressions, callExpressions.length, ); const [callExpression] = callExpressions; const typeParameters = parser.callExpressionTypeParameters(callExpression); const methodName = callExpression.callee.property.name; throwIfWrongNumberOfCallExpressionArgs( hasteModuleName, callExpression, methodName, callExpression.arguments.length, ); throwIfIncorrectModuleRegistryCallArgument( hasteModuleName, callExpression.arguments[0], methodName, ); const $moduleName = callExpression.arguments[0].value; throwIfUntypedModule( typeParameters, hasteModuleName, callExpression, methodName, $moduleName, ); throwIfIncorrectModuleRegistryCallTypeParameterParserError( hasteModuleName, typeParameters, methodName, $moduleName, parser, ); return $moduleName; }; const buildModuleSchema = ( hasteModuleName: string, /** * TODO(T71778680): Flow-type this node. */ ast: $FlowFixMe, tryParse: ParserErrorCapturer, parser: Parser, resolveTypeAnnotation: $FlowFixMe, translateTypeAnnotation: $FlowFixMe, ): NativeModuleSchema => { const language = parser.language(); const types = parser.getTypes(ast); const moduleSpecs = (Object.values(types): $ReadOnlyArray<$FlowFixMe>).filter( t => parser.isModuleInterface(t), ); throwIfModuleInterfaceNotFound( moduleSpecs.length, hasteModuleName, ast, language, ); throwIfMoreThanOneModuleInterfaceParserError( hasteModuleName, moduleSpecs, language, ); const [moduleSpec] = moduleSpecs; throwIfModuleInterfaceIsMisnamed(hasteModuleName, moduleSpec.id, language); // Parse Module Name const moduleName = parseModuleName(hasteModuleName, moduleSpec, ast, parser); // 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, moduleName, ); const properties: $ReadOnlyArray<$FlowFixMe> = language === 'Flow' ? moduleSpec.body.properties : moduleSpec.body.body; // $FlowFixMe[missing-type-arg] return properties .filter( property => property.type === 'ObjectTypeProperty' || property.type === 'TSPropertySignature' || property.type === 'TSMethodSignature', ) .map(property => { const aliasMap: {...NativeModuleAliasMap} = {}; const enumMap: {...NativeModuleEnumMap} = {}; return tryParse(() => ({ aliasMap, enumMap, propertyShape: buildPropertySchema( hasteModuleName, property, types, aliasMap, enumMap, tryParse, cxxOnly, resolveTypeAnnotation, translateTypeAnnotation, parser, ), })); }) .filter(Boolean) .reduce( ( moduleSchema: NativeModuleSchema, {aliasMap, enumMap, propertyShape}, ) => ({ type: 'NativeModule', aliasMap: {...moduleSchema.aliasMap, ...aliasMap}, enumMap: {...moduleSchema.enumMap, ...enumMap}, spec: { properties: [...moduleSchema.spec.properties, propertyShape], }, moduleName: moduleSchema.moduleName, excludedPlatforms: moduleSchema.excludedPlatforms, }), { type: 'NativeModule', aliasMap: {}, enumMap: {}, spec: {properties: []}, moduleName, excludedPlatforms: excludedPlatforms.length !== 0 ? [...excludedPlatforms] : undefined, }, ); }; /** * This function is used to find the type of a native component * provided the default exports statement from generated AST. * @param statement The statement to be parsed. * @param foundConfigs The 'mutable' array of configs that have been found. * @param parser The language parser to be used. * @returns void */ function findNativeComponentType( statement: $FlowFixMe, foundConfigs: Array<{[string]: string}>, parser: Parser, ): void { let declaration = statement.declaration; // codegenNativeComponent can be nested inside a cast // expression so we need to go one level deeper if ( declaration.type === 'TSAsExpression' || declaration.type === 'TypeCastExpression' ) { declaration = declaration.expression; } try { if (declaration.callee.name === 'codegenNativeComponent') { const typeArgumentParams = parser.getTypeArgumentParamsFromDeclaration(declaration); const funcArgumentParams = declaration.arguments; const nativeComponentType: {[string]: string} = parser.getNativeComponentType(typeArgumentParams, funcArgumentParams); if (funcArgumentParams.length > 1) { nativeComponentType.optionsExpression = funcArgumentParams[1]; } foundConfigs.push(nativeComponentType); } } catch (e) { // ignore } } function getCommandOptions( commandOptionsExpression: OptionsAST, ): ?CommandOptions { if (commandOptionsExpression == null) { return null; } let foundOptions; try { foundOptions = commandOptionsExpression.properties.reduce( (options, prop) => { options[prop.key.name] = ( (prop && prop.value && prop.value.elements) || [] ).map(element => element && element.value); return options; }, {}, ); } catch (e) { throw new Error( 'Failed to parse command options, please check that they are defined correctly', ); } return foundOptions; } function getOptions(optionsExpression: OptionsAST): ?OptionsShape { if (!optionsExpression) { return null; } let foundOptions; try { foundOptions = optionsExpression.properties.reduce((options, prop) => { if (prop.value.type === 'ArrayExpression') { options[prop.key.name] = prop.value.elements.map( element => element.value, ); } else { options[prop.key.name] = prop.value.value; } return options; }, {}); } catch (e) { throw new Error( 'Failed to parse codegen options, please check that they are defined correctly', ); } if ( foundOptions.paperComponentName && foundOptions.paperComponentNameDeprecated ) { throw new Error( 'Failed to parse codegen options, cannot use both paperComponentName and paperComponentNameDeprecated', ); } return foundOptions; } module.exports = { wrapModuleSchema, unwrapNullable, wrapNullable, assertGenericTypeAnnotationHasExactlyOneTypeParameter, isObjectProperty, parseObjectProperty, translateFunctionTypeAnnotation, buildPropertySchema, buildSchemaFromConfigType, buildSchema, createComponentConfig, parseModuleName, buildModuleSchema, findNativeComponentType, getCommandOptions, getOptions, };