mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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
511 lines
15 KiB
JavaScript
511 lines
15 KiB
JavaScript
/**
|
|
* 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,
|
|
NativeModuleParamTypeAnnotation,
|
|
NativeModuleReturnTypeAnnotation,
|
|
NativeModulePropertyShape,
|
|
} from '../../../CodegenSchema';
|
|
|
|
import type {AliasResolver} from '../Utils';
|
|
import type {StructCollector} from './StructCollector';
|
|
|
|
const invariant = require('invariant');
|
|
const {getNamespacedStructName} = require('./Utils');
|
|
const {capitalize} = require('../../Utils');
|
|
const {
|
|
wrapNullable,
|
|
unwrapNullable,
|
|
} = require('../../../parsers/parsers-commons');
|
|
|
|
const ProtocolMethodTemplate = ({
|
|
returnObjCType,
|
|
methodName,
|
|
params,
|
|
}: $ReadOnly<{
|
|
returnObjCType: string,
|
|
methodName: string,
|
|
params: string,
|
|
}>) => `- (${returnObjCType})${methodName}${params};`;
|
|
|
|
export type StructParameterRecord = $ReadOnly<{
|
|
paramIndex: number,
|
|
structName: string,
|
|
}>;
|
|
|
|
type ReturnJSType =
|
|
| 'VoidKind'
|
|
| 'BooleanKind'
|
|
| 'PromiseKind'
|
|
| 'ObjectKind'
|
|
| 'ArrayKind'
|
|
| 'NumberKind'
|
|
| 'StringKind';
|
|
|
|
export type MethodSerializationOutput = $ReadOnly<{
|
|
methodName: string,
|
|
protocolMethod: string,
|
|
selector: string,
|
|
structParamRecords: $ReadOnlyArray<StructParameterRecord>,
|
|
returnJSType: ReturnJSType,
|
|
argCount: number,
|
|
}>;
|
|
|
|
function serializeMethod(
|
|
hasteModuleName: string,
|
|
property: NativeModulePropertyShape,
|
|
structCollector: StructCollector,
|
|
resolveAlias: AliasResolver,
|
|
): $ReadOnlyArray<MethodSerializationOutput> {
|
|
const {name: methodName, typeAnnotation: nullableTypeAnnotation} = property;
|
|
const [propertyTypeAnnotation] = unwrapNullable(nullableTypeAnnotation);
|
|
const {params} = propertyTypeAnnotation;
|
|
|
|
if (methodName === 'getConstants') {
|
|
return serializeConstantsProtocolMethods(
|
|
hasteModuleName,
|
|
property,
|
|
structCollector,
|
|
resolveAlias,
|
|
);
|
|
}
|
|
|
|
const methodParams: Array<{paramName: string, objCType: string}> = [];
|
|
const structParamRecords: Array<StructParameterRecord> = [];
|
|
|
|
params.forEach((param, index) => {
|
|
const structName = getParamStructName(methodName, param);
|
|
const {objCType, isStruct} = getParamObjCType(
|
|
hasteModuleName,
|
|
methodName,
|
|
param,
|
|
structName,
|
|
structCollector,
|
|
resolveAlias,
|
|
);
|
|
|
|
methodParams.push({paramName: param.name, objCType});
|
|
|
|
if (isStruct) {
|
|
structParamRecords.push({paramIndex: index, structName});
|
|
}
|
|
});
|
|
|
|
// Unwrap returnTypeAnnotation, so we check if the return type is Promise
|
|
// TODO(T76719514): Disallow nullable PromiseTypeAnnotations
|
|
const [returnTypeAnnotation] = unwrapNullable(
|
|
propertyTypeAnnotation.returnTypeAnnotation,
|
|
);
|
|
|
|
if (returnTypeAnnotation.type === 'PromiseTypeAnnotation') {
|
|
methodParams.push(
|
|
{paramName: 'resolve', objCType: 'RCTPromiseResolveBlock'},
|
|
{paramName: 'reject', objCType: 'RCTPromiseRejectBlock'},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Build Protocol Method
|
|
**/
|
|
const returnObjCType = getReturnObjCType(
|
|
methodName,
|
|
propertyTypeAnnotation.returnTypeAnnotation,
|
|
);
|
|
const paddingMax = `- (${returnObjCType})${methodName}`.length;
|
|
|
|
const objCParams = methodParams.reduce(
|
|
($objCParams, {objCType, paramName}, i) => {
|
|
const rhs = `(${objCType})${paramName}`;
|
|
const padding = ' '.repeat(Math.max(0, paddingMax - paramName.length));
|
|
return i === 0
|
|
? `:${rhs}`
|
|
: `${$objCParams}\n${padding}${paramName}:${rhs}`;
|
|
},
|
|
'',
|
|
);
|
|
|
|
const protocolMethod = ProtocolMethodTemplate({
|
|
methodName,
|
|
returnObjCType,
|
|
params: objCParams,
|
|
});
|
|
|
|
/**
|
|
* Build ObjC Selector
|
|
*/
|
|
// $FlowFixMe[missing-type-arg]
|
|
const selector = methodParams
|
|
.map<string>(({paramName}) => paramName)
|
|
.reduce(($selector, paramName, i) => {
|
|
return i === 0 ? `${$selector}:` : `${$selector}${paramName}:`;
|
|
}, methodName);
|
|
|
|
/**
|
|
* Build JS Return type
|
|
*/
|
|
const returnJSType = getReturnJSType(methodName, returnTypeAnnotation);
|
|
|
|
return [
|
|
{
|
|
methodName,
|
|
protocolMethod,
|
|
selector: `@selector(${selector})`,
|
|
structParamRecords,
|
|
returnJSType,
|
|
argCount: params.length,
|
|
},
|
|
];
|
|
}
|
|
|
|
type Param = NamedShape<Nullable<NativeModuleParamTypeAnnotation>>;
|
|
|
|
function getParamStructName(methodName: string, param: Param): string {
|
|
const [typeAnnotation] = unwrapNullable(param.typeAnnotation);
|
|
if (typeAnnotation.type === 'TypeAliasTypeAnnotation') {
|
|
return typeAnnotation.name;
|
|
}
|
|
|
|
return `Spec${capitalize(methodName)}${capitalize(param.name)}`;
|
|
}
|
|
|
|
function getParamObjCType(
|
|
hasteModuleName: string,
|
|
methodName: string,
|
|
param: Param,
|
|
structName: string,
|
|
structCollector: StructCollector,
|
|
resolveAlias: AliasResolver,
|
|
): $ReadOnly<{objCType: string, isStruct: boolean}> {
|
|
const {name: paramName, typeAnnotation: nullableTypeAnnotation} = param;
|
|
const [typeAnnotation, nullable] = unwrapNullable(nullableTypeAnnotation);
|
|
const notRequired = param.optional || nullable;
|
|
|
|
function wrapIntoNullableIfNeeded(generatedType: string) {
|
|
return nullable ? `${generatedType} _Nullable` : generatedType;
|
|
}
|
|
|
|
const isStruct = (objCType: string) => ({
|
|
isStruct: true,
|
|
objCType,
|
|
});
|
|
|
|
const notStruct = (objCType: string) => ({
|
|
isStruct: false,
|
|
objCType,
|
|
});
|
|
|
|
// Handle types that can only be in parameters
|
|
switch (typeAnnotation.type) {
|
|
case 'FunctionTypeAnnotation': {
|
|
return notStruct('RCTResponseSenderBlock');
|
|
}
|
|
case 'ArrayTypeAnnotation': {
|
|
/**
|
|
* Array in params always codegen NSArray *
|
|
*
|
|
* TODO(T73933406): Support codegen for Arrays of structs and primitives
|
|
*
|
|
* For example:
|
|
* Array<number> => NSArray<NSNumber *>
|
|
* type Animal = {};
|
|
* Array<Animal> => NSArray<JS::NativeSampleTurboModule::Animal *>, etc.
|
|
*/
|
|
return notStruct(wrapIntoNullableIfNeeded('NSArray *'));
|
|
}
|
|
}
|
|
|
|
const [structTypeAnnotation] = unwrapNullable(
|
|
structCollector.process(
|
|
structName,
|
|
'REGULAR',
|
|
resolveAlias,
|
|
wrapNullable(nullable, typeAnnotation),
|
|
),
|
|
);
|
|
|
|
invariant(
|
|
structTypeAnnotation.type !== 'ArrayTypeAnnotation',
|
|
'ArrayTypeAnnotations should have been processed earlier',
|
|
);
|
|
|
|
switch (structTypeAnnotation.type) {
|
|
case 'TypeAliasTypeAnnotation': {
|
|
/**
|
|
* TODO(T73943261): Support nullable object literals and aliases?
|
|
*/
|
|
return isStruct(
|
|
getNamespacedStructName(hasteModuleName, structTypeAnnotation.name) +
|
|
' &',
|
|
);
|
|
}
|
|
case 'ReservedTypeAnnotation':
|
|
switch (structTypeAnnotation.name) {
|
|
case 'RootTag':
|
|
return notStruct(notRequired ? 'NSNumber *' : 'double');
|
|
default:
|
|
(structTypeAnnotation.name: empty);
|
|
throw new Error(
|
|
`Unsupported type for param "${paramName}" in ${methodName}. Found: ${structTypeAnnotation.type}`,
|
|
);
|
|
}
|
|
case 'StringTypeAnnotation':
|
|
return notStruct(wrapIntoNullableIfNeeded('NSString *'));
|
|
case 'NumberTypeAnnotation':
|
|
return notStruct(notRequired ? 'NSNumber *' : 'double');
|
|
case 'FloatTypeAnnotation':
|
|
return notStruct(notRequired ? 'NSNumber *' : 'double');
|
|
case 'DoubleTypeAnnotation':
|
|
return notStruct(notRequired ? 'NSNumber *' : 'double');
|
|
case 'Int32TypeAnnotation':
|
|
return notStruct(notRequired ? 'NSNumber *' : 'double');
|
|
case 'BooleanTypeAnnotation':
|
|
return notStruct(notRequired ? 'NSNumber *' : 'BOOL');
|
|
case 'EnumDeclaration':
|
|
switch (typeAnnotation.memberType) {
|
|
case 'NumberTypeAnnotation':
|
|
return notStruct(notRequired ? 'NSNumber *' : 'double');
|
|
case 'StringTypeAnnotation':
|
|
return notStruct(wrapIntoNullableIfNeeded('NSString *'));
|
|
default:
|
|
throw new Error(
|
|
`Unsupported enum type for param "${paramName}" in ${methodName}. Found: ${typeAnnotation.type}`,
|
|
);
|
|
}
|
|
case 'GenericObjectTypeAnnotation':
|
|
return notStruct(wrapIntoNullableIfNeeded('NSDictionary *'));
|
|
default:
|
|
(structTypeAnnotation.type: empty);
|
|
throw new Error(
|
|
`Unsupported type for param "${paramName}" in ${methodName}. Found: ${typeAnnotation.type}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function getReturnObjCType(
|
|
methodName: string,
|
|
nullableTypeAnnotation: Nullable<NativeModuleReturnTypeAnnotation>,
|
|
): string {
|
|
const [typeAnnotation, nullable] = unwrapNullable(nullableTypeAnnotation);
|
|
|
|
function wrapIntoNullableIfNeeded(generatedType: string) {
|
|
return nullable ? `${generatedType} _Nullable` : generatedType;
|
|
}
|
|
|
|
switch (typeAnnotation.type) {
|
|
case 'VoidTypeAnnotation':
|
|
return 'void';
|
|
case 'PromiseTypeAnnotation':
|
|
return 'void';
|
|
case 'ObjectTypeAnnotation':
|
|
return wrapIntoNullableIfNeeded('NSDictionary *');
|
|
case 'TypeAliasTypeAnnotation':
|
|
return wrapIntoNullableIfNeeded('NSDictionary *');
|
|
case 'ArrayTypeAnnotation':
|
|
if (typeAnnotation.elementType == null) {
|
|
return wrapIntoNullableIfNeeded('NSArray<id<NSObject>> *');
|
|
}
|
|
|
|
return wrapIntoNullableIfNeeded(
|
|
`NSArray<${getReturnObjCType(
|
|
methodName,
|
|
typeAnnotation.elementType,
|
|
)}> *`,
|
|
);
|
|
case 'ReservedTypeAnnotation':
|
|
switch (typeAnnotation.name) {
|
|
case 'RootTag':
|
|
return wrapIntoNullableIfNeeded('NSNumber *');
|
|
default:
|
|
(typeAnnotation.name: empty);
|
|
throw new Error(
|
|
`Unsupported return type for ${methodName}. Found: ${typeAnnotation.name}`,
|
|
);
|
|
}
|
|
case 'StringTypeAnnotation':
|
|
// TODO: Can NSString * returns not be _Nullable?
|
|
// In the legacy codegen, we don't surround NSSTring * with _Nullable
|
|
return wrapIntoNullableIfNeeded('NSString *');
|
|
case 'NumberTypeAnnotation':
|
|
return wrapIntoNullableIfNeeded('NSNumber *');
|
|
case 'FloatTypeAnnotation':
|
|
return wrapIntoNullableIfNeeded('NSNumber *');
|
|
case 'DoubleTypeAnnotation':
|
|
return wrapIntoNullableIfNeeded('NSNumber *');
|
|
case 'Int32TypeAnnotation':
|
|
return wrapIntoNullableIfNeeded('NSNumber *');
|
|
case 'BooleanTypeAnnotation':
|
|
return wrapIntoNullableIfNeeded('NSNumber *');
|
|
case 'EnumDeclaration':
|
|
switch (typeAnnotation.memberType) {
|
|
case 'NumberTypeAnnotation':
|
|
return wrapIntoNullableIfNeeded('NSNumber *');
|
|
case 'StringTypeAnnotation':
|
|
return wrapIntoNullableIfNeeded('NSString *');
|
|
default:
|
|
throw new Error(
|
|
`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');
|
|
throw new Error(
|
|
`Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function getReturnJSType(
|
|
methodName: string,
|
|
nullableTypeAnnotation: Nullable<NativeModuleReturnTypeAnnotation>,
|
|
): ReturnJSType {
|
|
const [typeAnnotation] = unwrapNullable(nullableTypeAnnotation);
|
|
switch (typeAnnotation.type) {
|
|
case 'VoidTypeAnnotation':
|
|
return 'VoidKind';
|
|
case 'PromiseTypeAnnotation':
|
|
return 'PromiseKind';
|
|
case 'ObjectTypeAnnotation':
|
|
return 'ObjectKind';
|
|
case 'TypeAliasTypeAnnotation':
|
|
return 'ObjectKind';
|
|
case 'ArrayTypeAnnotation':
|
|
return 'ArrayKind';
|
|
case 'ReservedTypeAnnotation':
|
|
return 'NumberKind';
|
|
case 'StringTypeAnnotation':
|
|
return 'StringKind';
|
|
case 'NumberTypeAnnotation':
|
|
return 'NumberKind';
|
|
case 'FloatTypeAnnotation':
|
|
return 'NumberKind';
|
|
case 'DoubleTypeAnnotation':
|
|
return 'NumberKind';
|
|
case 'Int32TypeAnnotation':
|
|
return 'NumberKind';
|
|
case 'BooleanTypeAnnotation':
|
|
return 'BooleanKind';
|
|
case 'GenericObjectTypeAnnotation':
|
|
return 'ObjectKind';
|
|
case 'EnumDeclaration':
|
|
switch (typeAnnotation.memberType) {
|
|
case 'NumberTypeAnnotation':
|
|
return 'NumberKind';
|
|
case 'StringTypeAnnotation':
|
|
return 'StringKind';
|
|
default:
|
|
throw new Error(
|
|
`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');
|
|
throw new Error(
|
|
`Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function serializeConstantsProtocolMethods(
|
|
hasteModuleName: string,
|
|
property: NativeModulePropertyShape,
|
|
structCollector: StructCollector,
|
|
resolveAlias: AliasResolver,
|
|
): $ReadOnlyArray<MethodSerializationOutput> {
|
|
const [propertyTypeAnnotation] = unwrapNullable(property.typeAnnotation);
|
|
if (propertyTypeAnnotation.params.length !== 0) {
|
|
throw new Error(
|
|
`${hasteModuleName}.getConstants() may only accept 0 arguments.`,
|
|
);
|
|
}
|
|
|
|
const {returnTypeAnnotation} = propertyTypeAnnotation;
|
|
if (returnTypeAnnotation.type !== 'ObjectTypeAnnotation') {
|
|
throw new Error(
|
|
`${hasteModuleName}.getConstants() may only return an object literal: {...}.`,
|
|
);
|
|
}
|
|
|
|
if (returnTypeAnnotation.properties.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const realTypeAnnotation = structCollector.process(
|
|
'Constants',
|
|
'CONSTANTS',
|
|
resolveAlias,
|
|
returnTypeAnnotation,
|
|
);
|
|
|
|
invariant(
|
|
realTypeAnnotation.type === 'TypeAliasTypeAnnotation',
|
|
"Unable to generate C++ struct from module's getConstants() method return type.",
|
|
);
|
|
|
|
const returnObjCType = `facebook::react::ModuleConstants<JS::${hasteModuleName}::Constants::Builder>`;
|
|
|
|
// $FlowFixMe[missing-type-arg]
|
|
return ['constantsToExport', 'getConstants'].map<MethodSerializationOutput>(
|
|
methodName => {
|
|
const protocolMethod = ProtocolMethodTemplate({
|
|
methodName,
|
|
returnObjCType,
|
|
params: '',
|
|
});
|
|
|
|
return {
|
|
methodName,
|
|
protocolMethod,
|
|
returnJSType: 'ObjectKind',
|
|
selector: `@selector(${methodName})`,
|
|
structParamRecords: [],
|
|
argCount: 0,
|
|
};
|
|
},
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
serializeMethod,
|
|
};
|