Rewrite ObjC++ module generator

Summary:
## Misc. Improvements

* We now have 95%+ flow coverage in all generator files. Henceforth, we can make changes to these files with more confidence, and trust flow to catch more errors. This should also improve the DevX of working on these files.
* Better templates: Instead of doing string replace with RegExps, we instead use functions and leverage JS template literals to generate our code. A few benefits: (1) data dependencies of templates are clearly visible, and statically checked by flow, (2) the templates are more readable in VSCode.
* Merged the GenerateModuleHObjCpp.js and GenerateModuleMm.js generators. They can share a lot of logic, so it's not a good idea to keep them separate.
* The ObjC++ module generator no longer generates “dead” structs (i.e structs that aren’t used by type-safety infra). In fact, it explicitly only supports the types in our Wiki. (I know this wasn’t the case with the legacy codegen, because we were generating native code for enums in the legacy codegen). This is a mixed bag. The test to verify correctness will be more difficult to write. However, keeping structs in the codegen needlessly complicates the parsers + generators, and creates technical debt for us to clean up later.

## Abstractions
- **StructCollector:** As we serialize NativeModule methods, when we detect an ObjectTypeAnnotation in the return type of `getConstants()` or inside a method param, we must create a Struct JS object for it. When we detect a type-alias (also in the same locations), we must look up that type-alias and create a Struct from its RHS. A Struct is basically an ObjectTypeAnnotation with a context (i.e: used in getConstants() vs as a method param), that cannot contain other ObjectTypeAnnotations.
- **serializeMethod.js** Given a NativeModule method type annotation, output the protocol method, JS return type, selector, a record of which params were structs, and which structs. Basically, this is all the information necessary to generate the declaration and implementation codegen for a partiular NativeModule method.
- **serializeStruct/*.js**: After creating all these Structs, we need to loop over all of them, and tranform them into ObjC++ code.
  - **serializeStruct.js**: Depending on the struct context, calls either `serializeRegularStruct.js` or `serializeConstantsStruct.js`. Both of these files have the same layout/abstractions. They look very similar.
- **serializeModule.js:** Outputs RCTCxxConvert categories for transforming `NSDictionary *` into C++ structs. Outputs ObjCTurboModule subclass.

## Algorithm
```
for spec in NativeModuleSpecs
  structCollector = new StructCollector
  resolveAlias = (aliasName) => nullthrows(spec.aliases[aliasName])

  methodDatas = []
  for method in methods(spec)
    methodData.push(serializeMethod(method, structCollector, resolveAlias))
  end

  structs = structCollector.getStructs()

  output generateImplCodegen(methodDatas, structs)
  output generateHeaderCodegen(methodDatas, structs)
end
```

Changelog: [Internal]

Reviewed By: hramos

Differential Revision: D23633940

fbshipit-source-id: 7c29f458b65434f4865ef1993061b0f0dc7d04ce
This commit is contained in:
Ramanpreet Nara authored and Facebook GitHub Bot committed 2020-09-29 14:39:40 -07:00
1 parent c300193853
commit c267b8de72
25 files changed
+2335 -3115

No files matched your search

@@ -0,0 +1,413 @@
/**
* Copyright (c) Facebook, Inc. and its 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 {
NativeModuleMethodParamSchema,
NativeModuleReturnTypeAnnotation,
NativeModulePropertySchema,
} from '../../../CodegenSchema';
import type {AliasResolver} from '../Utils';
const invariant = require('invariant');
const {StructCollector} = require('./StructCollector');
const {capitalize, getNamespacedStructName} = require('./Utils');
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'
| 'PromiseKind'
| 'ObjectKind'
| 'ArrayKind'
| 'NumberKind'
| 'StringKind';
export type MethodSerializationOutput = $ReadOnly<{|
methodName: string,
protocolMethod: string,
selector: string,
structParamRecords: $ReadOnlyArray<StructParameterRecord>,
returnJSType: ReturnJSType,
|}>;
function serializeMethod(
moduleName: string,
property: NativeModulePropertySchema,
structCollector: StructCollector,
resolveAlias: AliasResolver,
): $ReadOnlyArray<MethodSerializationOutput> {
const {
name: methodName,
typeAnnotation: {params, returnTypeAnnotation},
} = property;
if (methodName === 'getConstants') {
return serializeConstantsProtocolMethods(
moduleName,
property,
structCollector,
resolveAlias,
);
}
const methodParams: Array<{|paramName: string, objCType: string|}> = [];
const structParamRecords: Array<StructParameterRecord> = [];
params.forEach((param, index) => {
const structName = `Spec${capitalize(methodName)}${capitalize(param.name)}`;
const {objCType, isStruct} = getParamObjCType(
moduleName,
methodName,
param,
structName,
structCollector,
resolveAlias,
);
methodParams.push({paramName: param.name, objCType});
if (isStruct) {
structParamRecords.push({paramIndex: index, structName});
}
});
if (returnTypeAnnotation.type === 'PromiseTypeAnnotation') {
methodParams.push(
{paramName: 'resolve', objCType: 'RCTPromiseResolveBlock'},
{paramName: 'reject', objCType: 'RCTPromiseRejectBlock'},
);
}
/**
* Build Protocol Method
**/
const returnObjCType = getReturnObjCType(methodName, 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
*/
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,
},
];
}
function getParamObjCType(
moduleName: string,
methodName: string,
param: NativeModuleMethodParamSchema,
structName: string,
structCollector: StructCollector,
resolveAlias: AliasResolver,
): $ReadOnly<{|objCType: string, isStruct: boolean|}> {
const {name: paramName, typeAnnotation} = param;
const notRequired = param.optional || typeAnnotation.nullable;
function wrapIntoNullableIfNeeded(generatedType: string) {
return typeAnnotation.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 = structCollector.process(
structName,
'REGULAR',
resolveAlias,
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(moduleName, structTypeAnnotation.name) + ' &',
);
}
case 'ReservedFunctionValueTypeAnnotation':
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 '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,
typeAnnotation: NativeModuleReturnTypeAnnotation,
) {
function wrapIntoNullableIfNeeded(generatedType: string) {
return typeAnnotation.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 'ReservedFunctionValueTypeAnnotation':
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 'GenericObjectTypeAnnotation':
return wrapIntoNullableIfNeeded('NSDictionary *');
default:
(typeAnnotation.type: empty);
throw new Error(
`Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`,
);
}
}
function getReturnJSType(
methodName: string,
typeAnnotation: NativeModuleReturnTypeAnnotation,
): ReturnJSType {
switch (typeAnnotation.type) {
case 'VoidTypeAnnotation':
return 'VoidKind';
case 'PromiseTypeAnnotation':
return 'PromiseKind';
case 'ObjectTypeAnnotation':
return 'ObjectKind';
case 'TypeAliasTypeAnnotation':
return 'ObjectKind';
case 'ArrayTypeAnnotation':
return 'ArrayKind';
case 'ReservedFunctionValueTypeAnnotation':
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 'NumberKind';
case 'GenericObjectTypeAnnotation':
return 'ObjectKind';
default:
(typeAnnotation.type: empty);
throw new Error(
`Unsupported return type for ${methodName}. Found: ${typeAnnotation.type}`,
);
}
}
function serializeConstantsProtocolMethods(
moduleName: string,
property: NativeModulePropertySchema,
structCollector: StructCollector,
resolveAlias: AliasResolver,
): $ReadOnlyArray<MethodSerializationOutput> {
if (property.typeAnnotation.params.length !== 0) {
throw new Error(
`${moduleName}.getConstants() may only accept 0 arguments.`,
);
}
const {returnTypeAnnotation} = property.typeAnnotation;
if (returnTypeAnnotation.type !== 'ObjectTypeAnnotation') {
throw new Error(
`${moduleName}.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::Native${moduleName}::Constants::Builder>`;
return ['constantsToExport', 'getConstants'].map<MethodSerializationOutput>(
methodName => {
const protocolMethod = ProtocolMethodTemplate({
methodName,
returnObjCType,
params: '',
});
return {
methodName: 'getConstants',
protocolMethod,
returnJSType: 'ObjectKind',
selector: 'getConstants',
structParamRecords: [],
};
},
);
}
module.exports = {
serializeMethod,
};