From 2eccd59d7c735df3c29fc7ca342555890eb7055b Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Mon, 14 Nov 2022 19:11:26 -0800 Subject: [PATCH] react-native-code-gen Add Union Type support for Java/ObjC TurboModules (#35312) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/35312 Changelog: [General][Added] react-native-code-gen Add Union Type support for Java/ObjC TurboModules Reviewed By: javache Differential Revision: D41216605 fbshipit-source-id: d411abed66c694d855ede40725667cbc7067538f --- .../modules/GenerateModuleJavaSpec.js | 56 +++++++++++++---- .../modules/GenerateModuleJniCpp.js | 54 ++++++++++++---- .../GenerateModuleObjCpp/serializeMethod.js | 38 ++++++++--- .../modules/__test_fixtures__/fixtures.js | 30 +++++++++ .../module-parser-snapshot-test.js.snap | 63 +++++++++++++++++++ .../src/parsers/flow/modules/index.js | 21 ++----- .../modules/__test_fixtures__/fixtures.js | 27 ++++++++ ...script-module-parser-snapshot-test.js.snap | 63 +++++++++++++++++++ .../src/parsers/typescript/modules/index.js | 27 +++++--- 9 files changed, 323 insertions(+), 56 deletions(-) diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js index 8ef4278dbc9954..bb1ee1311ae2af 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJavaSpec.js @@ -148,6 +148,20 @@ function translateFunctionParamToJavaType( default: throw new Error(createErrorMessage(realTypeAnnotation.type)); } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapNullable('double', 'Double'); + case 'ObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.ReadableMap'); + return wrapNullable('ReadableMap'); + case 'StringTypeAnnotation': + return wrapNullable('String'); + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } case 'ObjectTypeAnnotation': imports.add('com.facebook.react.bridge.ReadableMap'); return wrapNullable('ReadableMap'); @@ -162,10 +176,7 @@ function translateFunctionParamToJavaType( imports.add('com.facebook.react.bridge.Callback'); return wrapNullable('Callback'); default: - (realTypeAnnotation.type: - | 'EnumDeclaration' - | 'MixedTypeAnnotation' - | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); throw new Error(createErrorMessage(realTypeAnnotation.type)); } } @@ -229,6 +240,20 @@ function translateFunctionReturnTypeToJavaType( default: throw new Error(createErrorMessage(realTypeAnnotation.type)); } + case 'UnionTypeAnnotation': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapNullable('double', 'Double'); + case 'ObjectTypeAnnotation': + imports.add('com.facebook.react.bridge.WritableMap'); + return wrapNullable('WritableMap'); + case 'StringTypeAnnotation': + return wrapNullable('String'); + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } case 'ObjectTypeAnnotation': imports.add('com.facebook.react.bridge.WritableMap'); return wrapNullable('WritableMap'); @@ -239,10 +264,7 @@ function translateFunctionReturnTypeToJavaType( imports.add('com.facebook.react.bridge.WritableArray'); return wrapNullable('WritableArray'); default: - (realTypeAnnotation.type: - | 'EnumDeclaration' - | 'MixedTypeAnnotation' - | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); throw new Error(createErrorMessage(realTypeAnnotation.type)); } } @@ -294,6 +316,19 @@ function getFalsyReturnStatementFromReturnType( default: throw new Error(createErrorMessage(realTypeAnnotation.type)); } + case 'UnionTypeAnnotation': + switch (realTypeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return nullable ? 'return null;' : 'return 0;'; + case 'ObjectTypeAnnotation': + return 'return null;'; + case 'StringTypeAnnotation': + return nullable ? 'return null;' : 'return "";'; + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } case 'StringTypeAnnotation': return nullable ? 'return null;' : 'return "";'; case 'ObjectTypeAnnotation': @@ -303,10 +338,7 @@ function getFalsyReturnStatementFromReturnType( case 'ArrayTypeAnnotation': return 'return null;'; default: - (realTypeAnnotation.type: - | 'EnumDeclaration' - | 'MixedTypeAnnotation' - | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); 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 891f4fa6103790..490d6cd630eba2 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleJniCpp.js @@ -165,6 +165,19 @@ function translateReturnTypeToKind( `Unknown enum prop type for returning value, found: ${realTypeAnnotation.type}"`, ); } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + default: + throw new Error( + `Unsupported union member returning value, found: ${realTypeAnnotation.memberType}"`, + ); + } case 'NumberTypeAnnotation': return 'NumberKind'; case 'DoubleTypeAnnotation': @@ -182,10 +195,7 @@ function translateReturnTypeToKind( case 'ArrayTypeAnnotation': return 'ArrayKind'; default: - (realTypeAnnotation.type: - | 'EnumDeclaration' - | 'MixedTypeAnnotation' - | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); throw new Error( `Unknown prop type for returning value, found: ${realTypeAnnotation.type}"`, ); @@ -234,6 +244,19 @@ function translateParamTypeToJniType( `Unknown enum prop type for method arg, found: ${realTypeAnnotation.type}"`, ); } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return !isRequired ? 'Ljava/lang/Double;' : 'D'; + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/ReadableMap;'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + default: + throw new Error( + `Unsupported union prop value, found: ${realTypeAnnotation.memberType}"`, + ); + } case 'NumberTypeAnnotation': return !isRequired ? 'Ljava/lang/Double;' : 'D'; case 'DoubleTypeAnnotation': @@ -251,10 +274,7 @@ function translateParamTypeToJniType( case 'FunctionTypeAnnotation': return 'Lcom/facebook/react/bridge/Callback;'; default: - (realTypeAnnotation.type: - | 'EnumDeclaration' - | 'MixedTypeAnnotation' - | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); throw new Error( `Unknown prop type for method arg, found: ${realTypeAnnotation.type}"`, ); @@ -300,6 +320,19 @@ function translateReturnTypeToJniType( `Unknown enum prop type for method return type, found: ${realTypeAnnotation.type}"`, ); } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return nullable ? 'Ljava/lang/Double;' : 'D'; + case 'ObjectTypeAnnotation': + return 'Lcom/facebook/react/bridge/WritableMap;'; + case 'StringTypeAnnotation': + return 'Ljava/lang/String;'; + default: + throw new Error( + `Unsupported union member type, found: ${realTypeAnnotation.memberType}"`, + ); + } case 'NumberTypeAnnotation': return nullable ? 'Ljava/lang/Double;' : 'D'; case 'DoubleTypeAnnotation': @@ -317,10 +350,7 @@ function translateReturnTypeToJniType( case 'ArrayTypeAnnotation': return 'Lcom/facebook/react/bridge/WritableArray;'; default: - (realTypeAnnotation.type: - | 'EnumDeclaration' - | 'MixedTypeAnnotation' - | 'UnionTypeAnnotation'); + (realTypeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); throw new Error( `Unknown prop type for method return type, found: ${realTypeAnnotation.type}"`, ); 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 d0864ca5ea8ee2..2894de5ccf262d 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleObjCpp/serializeMethod.js @@ -357,13 +357,25 @@ function getReturnObjCType( `Unsupported enum return type for ${methodName}. Found: ${typeAnnotation.type}`, ); } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return wrapIntoNullableIfNeeded('NSNumber *'); + case 'ObjectTypeAnnotation': + return wrapIntoNullableIfNeeded('NSDictionary *'); + case 'StringTypeAnnotation': + // TODO: Can NSString * returns not be _Nullable? + // In the legacy codegen, we don't surround NSSTring * with _Nullable + return wrapIntoNullableIfNeeded('NSString *'); + default: + throw new Error( + `Unsupported union return type for ${methodName}, found: ${typeAnnotation.memberType}"`, + ); + } case 'GenericObjectTypeAnnotation': return wrapIntoNullableIfNeeded('NSDictionary *'); default: - (typeAnnotation.type: - | 'EnumDeclaration' - | 'MixedTypeAnnotation' - | 'UnionTypeAnnotation'); + (typeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); throw new Error( `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, ); @@ -413,11 +425,21 @@ function getReturnJSType( `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, ); } + case 'UnionTypeAnnotation': + switch (typeAnnotation.memberType) { + case 'NumberTypeAnnotation': + return 'NumberKind'; + case 'ObjectTypeAnnotation': + return 'ObjectKind'; + case 'StringTypeAnnotation': + return 'StringKind'; + default: + throw new Error( + `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, + ); + } default: - (typeAnnotation.type: - | 'EnumDeclaration' - | 'MixedTypeAnnotation' - | 'UnionTypeAnnotation'); + (typeAnnotation.type: 'EnumDeclaration' | 'MixedTypeAnnotation'); throw new Error( `Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`, ); 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 151bb480daabfe..3b42068a003ec2 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 @@ -529,6 +529,35 @@ export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULE_WITH_UNION = ` +/** + * 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 {TurboModule} from '../RCTExport'; +import * as TurboModuleRegistry from '../TurboModuleRegistry'; + +export type ChooseInt = 1 | 2 | 3; +export type ChooseFloat = 1.44 | 2.88 | 5.76; +export type ChooseObject = {} | {low: string}; +export type ChooseString = 'One' | 'Two' | 'Three'; + +export interface Spec extends TurboModule { + +getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + const ANDROID_ONLY_NATIVE_MODULE = ` /** * Copyright (c) Meta Platforms, Inc. and affiliates. @@ -671,6 +700,7 @@ module.exports = { NATIVE_MODULE_WITH_ARRAY_WITH_ALIAS, NATIVE_MODULE_WITH_BASIC_PARAM_TYPES, NATIVE_MODULE_WITH_CALLBACK, + NATIVE_MODULE_WITH_UNION, EMPTY_NATIVE_MODULE, ANDROID_ONLY_NATIVE_MODULE, IOS_ONLY_NATIVE_MODULE, 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 8f308ace82dca7..32222a962a2213 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 @@ -1628,6 +1628,69 @@ exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_SIMPLE_O }" `; +exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_UNION 1`] = ` +"{ + 'modules': { + 'NativeSampleTurboModule': { + 'type': 'NativeModule', + 'aliases': {}, + 'spec': { + 'properties': [ + { + 'name': 'getUnion', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'UnionTypeAnnotation', + 'memberType': 'ObjectTypeAnnotation' + }, + 'params': [ + { + 'name': 'chooseInt', + 'optional': false, + 'typeAnnotation': { + 'type': 'UnionTypeAnnotation', + 'memberType': 'NumberTypeAnnotation' + } + }, + { + 'name': 'chooseFloat', + 'optional': false, + 'typeAnnotation': { + 'type': 'UnionTypeAnnotation', + 'memberType': 'NumberTypeAnnotation' + } + }, + { + 'name': 'chooseObject', + 'optional': false, + 'typeAnnotation': { + 'type': 'UnionTypeAnnotation', + 'memberType': 'ObjectTypeAnnotation' + } + }, + { + 'name': 'chooseString', + 'optional': false, + 'typeAnnotation': { + 'type': 'UnionTypeAnnotation', + 'memberType': 'StringTypeAnnotation' + } + } + ] + } + } + ] + }, + 'moduleNames': [ + 'SampleTurboModule' + ] + } + } +}" +`; + exports[`RN Codegen Flow Parser can generate fixture NATIVE_MODULE_WITH_UNSAFE_OBJECT 1`] = ` "{ 'modules': { 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 bc6e28e6573cfd..2755cc5396d7af 100644 --- a/packages/react-native-codegen/src/parsers/flow/modules/index.js +++ b/packages/react-native-codegen/src/parsers/flow/modules/index.js @@ -15,8 +15,6 @@ import type { NativeModuleAliasMap, NativeModuleArrayTypeAnnotation, NativeModuleBaseTypeAnnotation, - NativeModuleFunctionTypeAnnotation, - NativeModuleParamTypeAnnotation, NativeModulePropertyShape, NativeModuleSchema, Nullable, @@ -55,15 +53,12 @@ const { } = require('../../parsers-primitives'); const { - UnnamedFunctionParamParserError, UnsupportedArrayElementTypeAnnotationParserError, UnsupportedTypeAnnotationParserError, - UnsupportedObjectPropertyTypeAnnotationParserError, IncorrectModuleRegistryCallArgumentTypeParserError, } = require('../../errors.js'); const { - throwIfUnsupportedFunctionReturnTypeAnnotationParserError, throwIfModuleInterfaceNotFound, throwIfModuleInterfaceIsMisnamed, throwIfUnusedModuleInterfaceParserError, @@ -73,7 +68,6 @@ const { throwIfUntypedModule, throwIfModuleTypeIsUnsupported, throwIfMoreThanOneModuleInterfaceParserError, - throwIfUnsupportedFunctionParamTypeAnnotationParserError, } = require('../../error-utils'); const {FlowParser} = require('../parser.js'); @@ -309,15 +303,12 @@ function translateTypeAnnotation( ); } case 'UnionTypeAnnotation': { - if (cxxOnly) { - return emitUnionTypeAnnotation( - nullable, - hasteModuleName, - typeAnnotation, - parser, - ); - } - // Fallthrough + return emitUnionTypeAnnotation( + nullable, + hasteModuleName, + typeAnnotation, + parser, + ); } case 'MixedTypeAnnotation': { if (cxxOnly) { 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 4c2f48f5d2fecc..6e71b23714936d 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 @@ -555,6 +555,32 @@ export interface Spec extends TurboModule { export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); `; +const NATIVE_MODULE_WITH_UNION = ` +/** + * 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. + * + * @format + */ + +import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport'; +import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry'; + +export type ChooseInt = 1 | 2 | 3; +export type ChooseFloat = 1.44 | 2.88 | 5.76; +export type ChooseObject = {} | {low: string}; +export type ChooseString = 'One' | 'Two' | 'Three'; + +export interface Spec extends TurboModule { + readonly getUnion: (chooseInt: ChooseInt, chooseFloat: ChooseFloat, chooseObject: ChooseObject, chooseString: ChooseString) => ChooseObject; +} + +export default TurboModuleRegistry.getEnforcing('SampleTurboModule'); + +`; + const ANDROID_ONLY_NATIVE_MODULE = ` /** * Copyright (c) Meta Platforms, Inc. and affiliates. @@ -693,6 +719,7 @@ module.exports = { NATIVE_MODULE_WITH_ARRAY2_WITH_ALIAS, NATIVE_MODULE_WITH_BASIC_PARAM_TYPES, NATIVE_MODULE_WITH_CALLBACK, + NATIVE_MODULE_WITH_UNION, EMPTY_NATIVE_MODULE, ANDROID_ONLY_NATIVE_MODULE, IOS_ONLY_NATIVE_MODULE, 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 185a836a066752..c787dcbcbb0d7e 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 @@ -1835,6 +1835,69 @@ exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_SI }" `; +exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_UNION 1`] = ` +"{ + 'modules': { + 'NativeSampleTurboModule': { + 'type': 'NativeModule', + 'aliases': {}, + 'spec': { + 'properties': [ + { + 'name': 'getUnion', + 'optional': false, + 'typeAnnotation': { + 'type': 'FunctionTypeAnnotation', + 'returnTypeAnnotation': { + 'type': 'UnionTypeAnnotation', + 'memberType': 'ObjectTypeAnnotation' + }, + 'params': [ + { + 'name': 'chooseInt', + 'optional': false, + 'typeAnnotation': { + 'type': 'UnionTypeAnnotation', + 'memberType': 'NumberTypeAnnotation' + } + }, + { + 'name': 'chooseFloat', + 'optional': false, + 'typeAnnotation': { + 'type': 'UnionTypeAnnotation', + 'memberType': 'NumberTypeAnnotation' + } + }, + { + 'name': 'chooseObject', + 'optional': false, + 'typeAnnotation': { + 'type': 'UnionTypeAnnotation', + 'memberType': 'ObjectTypeAnnotation' + } + }, + { + 'name': 'chooseString', + 'optional': false, + 'typeAnnotation': { + 'type': 'UnionTypeAnnotation', + 'memberType': 'StringTypeAnnotation' + } + } + ] + } + } + ] + }, + 'moduleNames': [ + 'SampleTurboModule' + ] + } + } +}" +`; + exports[`RN Codegen TypeScript Parser can generate fixture NATIVE_MODULE_WITH_UNSAFE_OBJECT 1`] = ` "{ 'modules': { 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 5df5fcbfb0a8d9..2d811c02e21e7a 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/index.js @@ -140,6 +140,18 @@ function translateArrayTypeAnnotation( ); } + // TODO: Added as a work-around for now until TupleTypeAnnotation are fully supported in both flow and TS + // Right now they are partially treated as UnionTypeAnnotation + if (elementType.type === 'UnionTypeAnnotation') { + throw new UnsupportedArrayElementTypeAnnotationParserError( + hasteModuleName, + tsElementType, + tsArrayType, + 'UnionTypeAnnotation', + language, + ); + } + const finalTypeAnnotation: NativeModuleArrayTypeAnnotation< Nullable, > = { @@ -314,15 +326,12 @@ function translateTypeAnnotation( ); } case 'TSUnionType': { - if (cxxOnly) { - return emitUnionTypeAnnotation( - nullable, - hasteModuleName, - typeAnnotation, - parser, - ); - } - // Fallthrough + return emitUnionTypeAnnotation( + nullable, + hasteModuleName, + typeAnnotation, + parser, + ); } case 'TSUnknownKeyword': { if (cxxOnly) {