From b444f0e44e0d8670139acea5f14c2de32c5e2ddc Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Tue, 30 Aug 2022 00:48:06 -0700 Subject: [PATCH] react-native-code-gen Add Enum Type support for C++ TurboModules Summary: There are cases where we want to pass an enum type into a TurboModule method which is handy to restrict certain arguments to a restricted set of values, e.g. restricting quality to ```enum Quality { SD, HD, }``` Approach: - We are not generating an ```enum``` type in C++ but rather just cast type safe to the corresponding member type. - We don't support mixed enum types at this time, e.g. ```export enum StringOption { One = 'one', Two = 2, Three = 'three', };``` will not work. See: https://www.typescriptlang.org/docs/handbook/enums.html#heterogeneous-enums - We only support untyped (default to String), String, and Number enum properties This is for C++ only, Java and ObjC are not supported atm. Changelog: [General][Added] - react-native-code-gen Add Enum Type support for C++ TurboModules Reviewed By: RSNara Differential Revision: D38880963 fbshipit-source-id: f2399b29948306bc555429b6f96c43ea4c39c46e --- .../react-native-modules.js | 2 +- .../react-native-codegen/src/CodegenSchema.js | 6 +++ .../generators/modules/GenerateModuleCpp.js | 11 +++++ .../src/generators/modules/GenerateModuleH.js | 9 ++++ .../modules/GenerateModuleJavaSpec.js | 15 +++++-- .../modules/GenerateModuleJniCpp.js | 15 +++++-- .../GenerateModuleObjCpp/StructCollector.js | 2 + .../GenerateModuleObjCpp/serializeMethod.js | 10 ++++- .../modules/__test_fixtures__/fixtures.js | 36 +++++++++++++++ .../GenerateModuleCpp-test.js.snap | 4 ++ .../GenerateModuleH-test.js.snap | 9 ++++ .../src/parsers/flow/index.js | 2 +- .../modules/__test_fixtures__/fixtures.js | 22 ++++++++++ .../module-parser-snapshot-test.js.snap | 44 +++++++++++++++++++ .../src/parsers/flow/modules/errors.js | 19 ++++++++ .../src/parsers/flow/modules/index.js | 26 +++++++++++ .../src/parsers/flow/utils.js | 15 ++++++- .../modules/__test_fixtures__/fixtures.js | 22 ++++++++++ ...script-module-parser-snapshot-test.js.snap | 44 +++++++++++++++++++ .../src/parsers/typescript/modules/errors.js | 19 ++++++++ .../src/parsers/typescript/modules/index.js | 28 ++++++++++++ .../src/parsers/typescript/utils.js | 16 +++++-- 22 files changed, 361 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin-specs/react-native-modules.js b/packages/eslint-plugin-specs/react-native-modules.js index d0b5aab0ac4cea..28ff8f065a727c 100644 --- a/packages/eslint-plugin-specs/react-native-modules.js +++ b/packages/eslint-plugin-specs/react-native-modules.js @@ -134,7 +134,7 @@ function rule(context) { const [parsingErrors, tryParse] = createParserErrorCapturer(); const sourceCode = context.getSourceCode().getText(); - const ast = flowParser.parse(sourceCode); + const ast = flowParser.parse(sourceCode, {enums: true}); tryParse(() => { buildModuleSchema(hasteModuleName, ast, tryParse); diff --git a/packages/react-native-codegen/src/CodegenSchema.js b/packages/react-native-codegen/src/CodegenSchema.js index 8ffc00612548b4..3e8857ec2be0a2 100644 --- a/packages/react-native-codegen/src/CodegenSchema.js +++ b/packages/react-native-codegen/src/CodegenSchema.js @@ -282,6 +282,11 @@ export type NativeModuleBooleanTypeAnnotation = $ReadOnly<{ type: 'BooleanTypeAnnotation', }>; +export type NativeModuleEnumDeclaration = $ReadOnly<{ + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation' | 'StringTypeAnnotation', +}>; + export type NativeModuleGenericObjectTypeAnnotation = $ReadOnly<{ type: 'GenericObjectTypeAnnotation', }>; @@ -316,6 +321,7 @@ export type NativeModuleBaseTypeAnnotation = | NativeModuleDoubleTypeAnnotation | NativeModuleFloatTypeAnnotation | NativeModuleBooleanTypeAnnotation + | NativeModuleEnumDeclaration | NativeModuleGenericObjectTypeAnnotation | ReservedTypeAnnotation | NativeModuleTypeAliasTypeAnnotation diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js index d49ed9253154e2..b32b60c6f0e45a 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleCpp.js @@ -153,6 +153,17 @@ function serializeArg( return wrap(val => `${val}.asString(rt)`); case 'BooleanTypeAnnotation': return wrap(val => `${val}.asBool()`); + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrap(val => `${val}.asNumber()`); + case 'StringTypeAnnotation': + return wrap(val => `${val}.asString(rt)`); + default: + throw new Error( + `Unknown enum type for "${arg.name}, found: ${realTypeAnnotation.type}"`, + ); + } case 'NumberTypeAnnotation': return wrap(val => `${val}.asNumber()`); case 'FloatTypeAnnotation': diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js index 6e5f97a8551a39..7c80bad095e1f8 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js @@ -146,6 +146,15 @@ function translatePrimitiveJSTypeToCpp( return wrap('int'); case 'BooleanTypeAnnotation': return wrap('bool'); + case 'EnumDeclaration': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrap('double'); + case 'StringTypeAnnotation': + return wrap('jsi::String'); + default: + throw new Error(createErrorMessage(realTypeAnnotation.type)); + } case 'GenericObjectTypeAnnotation': return wrap('jsi::Object'); case 'UnionTypeAnnotation': diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js index aa833afbba30c3..32443237f3a8e2 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js @@ -156,7 +156,10 @@ function translateFunctionParamToJavaType( imports.add('com.facebook.react.bridge.Callback'); return 'Callback'; default: - (realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: + | 'EnumDeclaration' + | 'MixedTypeAnnotation' + | 'UnionTypeAnnotation'); throw new Error(createErrorMessage(realTypeAnnotation.type)); } } @@ -220,7 +223,10 @@ function translateFunctionReturnTypeToJavaType( imports.add('com.facebook.react.bridge.WritableArray'); return wrapIntoNullableIfNeeded('WritableArray'); default: - (realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: + | 'EnumDeclaration' + | 'MixedTypeAnnotation' + | 'UnionTypeAnnotation'); throw new Error(createErrorMessage(realTypeAnnotation.type)); } } @@ -272,7 +278,10 @@ function getFalsyReturnStatementFromReturnType( case 'ArrayTypeAnnotation': return 'return null;'; default: - (realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: + | 'EnumDeclaration' + | 'MixedTypeAnnotation' + | 'UnionTypeAnnotation'); throw new Error(createErrorMessage(realTypeAnnotation.type)); } } diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js index bca66c4f9925a4..aa56ed608c872b 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js @@ -171,7 +171,10 @@ function translateReturnTypeToKind( case 'ArrayTypeAnnotation': return 'ArrayKind'; default: - (realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: + | 'EnumDeclaration' + | 'MixedTypeAnnotation' + | 'UnionTypeAnnotation'); throw new Error( `Unknown prop type for returning value, found: ${realTypeAnnotation.type}"`, ); @@ -226,7 +229,10 @@ function translateParamTypeToJniType( case 'FunctionTypeAnnotation': return 'Lcom/facebook/react/bridge/Callback;'; default: - (realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: + | 'EnumDeclaration' + | 'MixedTypeAnnotation' + | 'UnionTypeAnnotation'); throw new Error( `Unknown prop type for method arg, found: ${realTypeAnnotation.type}"`, ); @@ -278,7 +284,10 @@ function translateReturnTypeToJniType( case 'ArrayTypeAnnotation': return 'Lcom/facebook/react/bridge/WritableArray;'; default: - (realTypeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: + | 'EnumDeclaration' + | 'MixedTypeAnnotation' + | 'UnionTypeAnnotation'); throw new Error( `Unknown prop type for method return type, found: ${realTypeAnnotation.type}"`, ); diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/StructCollector.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/StructCollector.js index 82e8d5bc245d18..a30fa5400ecf70 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/StructCollector.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/StructCollector.js @@ -112,6 +112,8 @@ class StructCollector { this._insertAlias(typeAnnotation.name, structContext, resolveAlias); return wrapNullable(nullable, typeAnnotation); } + case 'EnumDeclaration': + throw new Error('Enum types are unsupported in structs'); case 'MixedTypeAnnotation': throw new Error('Mixed types are unsupported in structs'); case 'UnionTypeAnnotation': diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js index d7f33cecb7afcd..ead5482a824d4b 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js @@ -338,7 +338,10 @@ function getReturnObjCType( case 'GenericObjectTypeAnnotation': return wrapIntoNullableIfNeeded('NSDictionary *'); default: - (typeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation'); + (typeAnnotation.type: + | 'EnumDeclaration' + | 'MixedTypeAnnotation' + | 'UnionTypeAnnotation'); throw new Error( `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, ); @@ -378,7 +381,10 @@ function getReturnJSType( case 'GenericObjectTypeAnnotation': return 'ObjectKind'; default: - (typeAnnotation.type: 'MixedTypeAnnotation' | 'UnionTypeAnnotation'); + (typeAnnotation.type: + | 'EnumDeclaration' + | 'MixedTypeAnnotation' + | 'UnionTypeAnnotation'); throw new Error( `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, ); diff --git a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js index 590d3e939de5ca..b9064512c97052 100644 --- a/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/generators/modules/__test_fixtures__/fixtures.js @@ -1567,6 +1567,42 @@ const CXX_ONLY_NATIVE_MODULES: SchemaType = { ], }, }, + { + name: 'getEnums', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'StringTypeAnnotation', + }, + params: [ + { + name: 'enumInt', + optional: false, + typeAnnotation: { + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }, + { + name: 'enumFloat', + optional: false, + typeAnnotation: { + type: 'EnumDeclaration', + memberType: 'NumberTypeAnnotation', + }, + }, + { + name: 'enumString', + optional: false, + typeAnnotation: { + type: 'EnumDeclaration', + memberType: 'StringTypeAnnotation', + }, + }, + ], + }, + }, { name: 'getUnion', optional: false, diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleCpp-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleCpp-test.js.snap index 741eb73ee239ed..2dd9ca5ce68405 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleCpp-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleCpp-test.js.snap @@ -115,6 +115,9 @@ static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getNullableNu auto result = static_cast(&turboModule)->getNullableNumberFromNullableAlias(rt, args[0].isNull() || args[0].isUndefined() ? std::nullopt : std::make_optional(args[0].asObject(rt))); return result ? jsi::Value(std::move(*result)) : jsi::Value::null(); } +static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getEnums(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getEnums(rt, args[0].asNumber(), args[1].asNumber(), args[2].asString(rt)); +} static jsi::Value __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getUnion(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { return static_cast(&turboModule)->getUnion(rt, args[0].asNumber(), args[1].asNumber(), args[2].asObject(rt), args[3].asString(rt)); } @@ -123,6 +126,7 @@ NativeSampleTurboModuleCxxSpecJSI::NativeSampleTurboModuleCxxSpecJSI(std::shared : TurboModule(\\"SampleTurboModuleCxx\\", jsInvoker) { methodMap_[\\"getMixed\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getMixed}; methodMap_[\\"getNullableNumberFromNullableAlias\\"] = MethodMetadata {1, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getNullableNumberFromNullableAlias}; + methodMap_[\\"getEnums\\"] = MethodMetadata {3, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getEnums}; methodMap_[\\"getUnion\\"] = MethodMetadata {4, __hostFunction_NativeSampleTurboModuleCxxSpecJSI_getUnion}; } diff --git a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap index 8cf7d8ee7f5315..461e34715e676e 100644 --- a/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap +++ b/packages/react-native-codegen/src/generators/modules/__tests__/__snapshots__/GenerateModuleH-test.js.snap @@ -209,6 +209,7 @@ protected: public: virtual jsi::Value getMixed(jsi::Runtime &rt, jsi::Value arg) = 0; virtual std::optional getNullableNumberFromNullableAlias(jsi::Runtime &rt, std::optional a) = 0; + virtual jsi::String getEnums(jsi::Runtime &rt, double enumInt, double enumFloat, jsi::String enumString) = 0; virtual jsi::Object getUnion(jsi::Runtime &rt, double chooseInt, double chooseFloat, jsi::Object chooseObject, jsi::String chooseString) = 0; }; @@ -247,6 +248,14 @@ private: return bridging::callFromJs>( rt, &T::getNullableNumberFromNullableAlias, jsInvoker_, instance_, std::move(a)); } + jsi::String getEnums(jsi::Runtime &rt, double enumInt, double enumFloat, jsi::String enumString) override { + static_assert( + bridging::getParameterCount(&T::getEnums) == 4, + \\"Expected getEnums(...) to have 4 parameters\\"); + + return bridging::callFromJs( + rt, &T::getEnums, jsInvoker_, instance_, std::move(enumInt), std::move(enumFloat), std::move(enumString)); + } jsi::Object getUnion(jsi::Runtime &rt, double chooseInt, double chooseFloat, jsi::Object chooseObject, jsi::String chooseString) override { static_assert( bridging::getParameterCount(&T::getUnion) == 5, diff --git a/packages/react-native-codegen/src/parsers/flow/index.js b/packages/react-native-codegen/src/parsers/flow/index.js index 516025223e7694..23e1a5c6aa6aa1 100644 --- a/packages/react-native-codegen/src/parsers/flow/index.js +++ b/packages/react-native-codegen/src/parsers/flow/index.js @@ -77,7 +77,7 @@ function buildSchema(contents: string, filename: ?string): SchemaType { return {modules: {}}; } - const ast = flowParser.parse(contents); + const ast = flowParser.parse(contents, {enums: true}); const configType = getConfigType(ast); switch (configType) { diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js index f9f84ee20ac15b..2d73c47301050d 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js @@ -598,9 +598,31 @@ export type ChooseFloat = 1.44 | 2.88 | 5.76; export type ChooseObject = {} | {low: string}; export type ChooseString = 'One' | 'Two' | 'Three'; +export enum Quality { + SD, + HD, +} + +export enum Resolution { + Low = 720, + High = 1080, +} + +export enum Floppy { + LowDensity = 0.72, + HighDensity = 1.44, +} + +export enum StringOptions { + One = 'one', + Two = 'two', + Three = 'three', +} + export interface Spec extends TurboModule { +getCallback: () => () => void; +getMixed: (arg: mixed) => mixed; + +getEnums: (quality: Quality, resolution?: Resolution, floppy: Floppy, stringOptions: StringOptions) => string; +getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; } diff --git a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap index abe2c393988318..bf9ab323ea9d52 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap @@ -78,6 +78,50 @@ exports[`RN Codegen Flow Parser can generate fixture CXX_ONLY_NATIVE_MODULE 1`] ] } }, + { + 'name': 'getEnums', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'StringTypeAnnotation' + }, + 'params': [ + { + 'name': 'quality', + 'optional': false, + 'typeAnnotation': { + 'type': 'EnumDeclaration', + 'memberType': 'StringTypeAnnotation' + } + }, + { + 'name': 'resolution', + 'optional': true, + 'typeAnnotation': { + 'type': 'EnumDeclaration', + 'memberType': 'NumberTypeAnnotation' + } + }, + { + 'name': 'floppy', + 'optional': false, + 'typeAnnotation': { + 'type': 'EnumDeclaration', + 'memberType': 'NumberTypeAnnotation' + } + }, + { + 'name': 'stringOptions', + 'optional': false, + 'typeAnnotation': { + 'type': 'EnumDeclaration', + 'memberType': 'StringTypeAnnotation' + } + } + ] + } + }, { 'name': 'getUnion', 'optional': false, diff --git a/packages/react-native-codegen/src/parsers/flow/modules/errors.js b/packages/react-native-codegen/src/parsers/flow/modules/errors.js index 03bc6d63be7b08..b694800198ccfe 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/errors.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/errors.js @@ -227,6 +227,24 @@ class UnsupportedFunctionReturnTypeAnnotationParserError extends ParserError { } } +/** + * Enum parsing errors + */ + +class UnsupportedEnumDeclarationParserError extends ParserError { + constructor( + hasteModuleName: string, + arrayElementTypeAST: $FlowFixMe, + memberType: string, + ) { + super( + hasteModuleName, + arrayElementTypeAST, + `Unexpected enum member type ${memberType}. Only string and number enum members are supported`, + ); + } +} + /** * Union parsing errors */ @@ -347,6 +365,7 @@ module.exports = { UnsupportedFlowTypeAnnotationParserError, UnsupportedFunctionParamTypeAnnotationParserError, UnsupportedFunctionReturnTypeAnnotationParserError, + UnsupportedEnumDeclarationParserError, UnsupportedUnionTypeAnnotationParserError, UnsupportedModulePropertyParserError, UnsupportedObjectPropertyTypeAnnotationParserError, diff --git a/packages/react-native-codegen/src/parsers/flow/modules/index.js b/packages/react-native-codegen/src/parsers/flow/modules/index.js index 528813922cf87f..4ced736958f7e6 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/index.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/index.js @@ -44,6 +44,7 @@ const { UnsupportedFlowTypeAnnotationParserError, UnsupportedFunctionParamTypeAnnotationParserError, UnsupportedFunctionReturnTypeAnnotationParserError, + UnsupportedEnumDeclarationParserError, UnsupportedUnionTypeAnnotationParserError, UnsupportedModulePropertyParserError, UnsupportedObjectPropertyTypeAnnotationParserError, @@ -214,6 +215,31 @@ function translateTypeAnnotation( }); } default: { + const maybeEumDeclaration = types[typeAnnotation.id.name]; + if ( + cxxOnly && + maybeEumDeclaration && + maybeEumDeclaration.type === 'EnumDeclaration' + ) { + const memberType = maybeEumDeclaration.body.type + .replace('EnumNumberBody', 'NumberTypeAnnotation') + .replace('EnumStringBody', 'StringTypeAnnotation'); + if ( + memberType === 'NumberTypeAnnotation' || + memberType === 'StringTypeAnnotation' + ) { + return wrapNullable(nullable, { + type: 'EnumDeclaration', + memberType: memberType, + }); + } else { + throw new UnsupportedEnumDeclarationParserError( + hasteModuleName, + typeAnnotation, + memberType, + ); + } + } throw new UnsupportedFlowGenericParserError( hasteModuleName, typeAnnotation, diff --git a/packages/react-native-codegen/src/parsers/flow/utils.js b/packages/react-native-codegen/src/parsers/flow/utils.js index 86547dc9b85589..c337570c060f88 100644 --- a/packages/react-native-codegen/src/parsers/flow/utils.js +++ b/packages/react-native-codegen/src/parsers/flow/utils.js @@ -31,9 +31,17 @@ function getTypes(ast: $FlowFixMe): TypeDeclarationMap { ) { types[node.declaration.id.name] = node.declaration; } + } else if ( + node.type === 'ExportNamedDeclaration' && + node.exportKind === 'value' && + node.declaration && + node.declaration.type === 'EnumDeclaration' + ) { + types[node.declaration.id.name] = node.declaration; } else if ( node.type === 'TypeAlias' || - node.type === 'InterfaceDeclaration' + node.type === 'InterfaceDeclaration' || + node.type === 'EnumDeclaration' ) { types[node.id.name] = node; } @@ -85,7 +93,10 @@ function resolveTypeAnnotation( aliasName: node.id.name, }; const resolvedTypeAnnotation = types[node.id.name]; - if (resolvedTypeAnnotation == null) { + if ( + resolvedTypeAnnotation == null || + resolvedTypeAnnotation.type === 'EnumDeclaration' + ) { break; } diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js index f776e26ae3ca62..0ca6c6ab0d99d0 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js @@ -608,6 +608,27 @@ const CXX_ONLY_NATIVE_MODULE = ` import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; +export enum Quality { + SD, + HD, +} + +export enum Resolution { + Low = 720, + High = 1080, +} + +export enum Floppy { + LowDensity = 0.72, + HighDensity = 1.44, +} + +export enum StringOptions { + One = 'one', + Two = 'two', + Three = 'three', +} + export type ChooseInt = 1 | 2 | 3; export type ChooseFloat = 1.44 | 2.88 | 5.76; export type ChooseObject = {} | {low: string}; @@ -616,6 +637,7 @@ export type ChooseString = 'One' | 'Two' | 'Three'; export interface Spec extends TurboModule { readonly getCallback: () => () => void; readonly getMixed: (arg: unknown) => unknown; + readonly getEnums: (quality: Quality, resolution?: Resolution, floppy: Floppy, stringOptions: StringOptions) => string; readonly getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; } diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap index e56dfc7871c8ad..03a0b11a41f1ab 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap +++ b/packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap @@ -76,6 +76,50 @@ exports[`RN Codegen TypeScript Parser can generate fixture CXX_ONLY_NATIVE_MODUL ] } }, + { + 'name': 'getEnums', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'StringTypeAnnotation' + }, + 'params': [ + { + 'name': 'quality', + 'optional': false, + 'typeAnnotation': { + 'type': 'EnumDeclaration', + 'memberType': 'StringTypeAnnotation' + } + }, + { + 'name': 'resolution', + 'optional': true, + 'typeAnnotation': { + 'type': 'EnumDeclaration', + 'memberType': 'NumberTypeAnnotation' + } + }, + { + 'name': 'floppy', + 'optional': false, + 'typeAnnotation': { + 'type': 'EnumDeclaration', + 'memberType': 'NumberTypeAnnotation' + } + }, + { + 'name': 'stringOptions', + 'optional': false, + 'typeAnnotation': { + 'type': 'EnumDeclaration', + 'memberType': 'StringTypeAnnotation' + } + } + ] + } + }, { 'name': 'getUnion', 'optional': false, diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/errors.js b/packages/react-native-codegen/src/parsers/typescript/modules/errors.js index 76064b5f0566af..60cf42b2d347cb 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/errors.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/errors.js @@ -223,6 +223,24 @@ class UnsupportedFunctionReturnTypeAnnotationParserError extends ParserError { } } +/** + * Enum parsing errors + */ + +class UnsupportedTypeScriptEnumDeclarationParserError extends ParserError { + constructor( + hasteModuleName: string, + arrayElementTypeAST: $FlowFixMe, + memberType: string, + ) { + super( + hasteModuleName, + arrayElementTypeAST, + `Unexpected enum member type ${memberType}. Only string and number enum members are supported`, + ); + } +} + /** * Union parsing errors */ @@ -343,6 +361,7 @@ module.exports = { UnsupportedTypeScriptTypeAnnotationParserError, UnsupportedFunctionParamTypeAnnotationParserError, UnsupportedFunctionReturnTypeAnnotationParserError, + UnsupportedTypeScriptEnumDeclarationParserError, UnsupportedTypeScriptUnionTypeAnnotationParserError, UnsupportedModulePropertyParserError, UnsupportedObjectPropertyTypeAnnotationParserError, diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/index.js b/packages/react-native-codegen/src/parsers/typescript/modules/index.js index 5db27cc5f9559d..2a8bf950aacaa1 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/index.js @@ -44,6 +44,7 @@ const { UnsupportedTypeScriptTypeAnnotationParserError, UnsupportedFunctionParamTypeAnnotationParserError, UnsupportedFunctionReturnTypeAnnotationParserError, + UnsupportedTypeScriptEnumDeclarationParserError, UnsupportedTypeScriptUnionTypeAnnotationParserError, UnsupportedModulePropertyParserError, UnsupportedObjectPropertyTypeAnnotationParserError, @@ -266,6 +267,33 @@ function translateTypeAnnotation( }); } default: { + const maybeEumDeclaration = types[typeAnnotation.typeName.name]; + if ( + cxxOnly && + maybeEumDeclaration && + maybeEumDeclaration.type === 'TSEnumDeclaration' + ) { + const memberType = maybeEumDeclaration.members[0].initializer + ? maybeEumDeclaration.members[0].initializer.type + .replace('NumericLiteral', 'NumberTypeAnnotation') + .replace('StringLiteral', 'StringTypeAnnotation') + : 'StringTypeAnnotation'; + if ( + memberType === 'NumberTypeAnnotation' || + memberType === 'StringTypeAnnotation' + ) { + return wrapNullable(nullable, { + type: 'EnumDeclaration', + memberType: memberType, + }); + } else { + throw new UnsupportedTypeScriptEnumDeclarationParserError( + hasteModuleName, + typeAnnotation, + memberType, + ); + } + } throw new UnsupportedTypeScriptGenericParserError( hasteModuleName, typeAnnotation, diff --git a/packages/react-native-codegen/src/parsers/typescript/utils.js b/packages/react-native-codegen/src/parsers/typescript/utils.js index 685e6b50390beb..b2666eee1808a4 100644 --- a/packages/react-native-codegen/src/parsers/typescript/utils.js +++ b/packages/react-native-codegen/src/parsers/typescript/utils.js @@ -26,13 +26,20 @@ function getTypes(ast: $FlowFixMe): TypeDeclarationMap { ) { types[node.declaration.id.name] = node.declaration; } + } else if ( + node.type === 'ExportNamedDeclaration' && + node.exportKind === 'value' && + node.declaration && + node.declaration.type === 'TSEnumDeclaration' + ) { + types[node.declaration.id.name] = node.declaration; } else if ( node.type === 'TSTypeAliasDeclaration' || - node.type === 'TSInterfaceDeclaration' + node.type === 'TSInterfaceDeclaration' || + node.type === 'TSEnumDeclaration' ) { types[node.id.name] = node; } - return types; }, {}); } @@ -92,7 +99,10 @@ function resolveTypeAnnotation( aliasName: node.typeName.name, }; const resolvedTypeAnnotation = types[node.typeName.name]; - if (resolvedTypeAnnotation == null) { + if ( + resolvedTypeAnnotation == null || + resolvedTypeAnnotation.type === 'TSEnumDeclaration' + ) { break; }