Generate pojo for Component Props

Summary:
## What This Does
- **Phase 1:** Given a component, convert its props (and all its ObjectTypeAnnotations) into this Pojo data structure under some namespace. This is a recursive operation.
- **Phase 2:** Loop over each Pojo data structure, and serialize it to Java class.

So...
- Each Component has its own namespace (i.e: its hasteModuleName) for Java Pojo objects.
- Each Component generates 1 Pojo object, for its props.
- Each Component generates 1 Pojo object for every ObjectTypeAnnotation in its props.

## Decisions
By design, [JNI can read/write to private properties on Java objects](https://stackoverflow.com/a/12208643). So, each Pojo, which represents an ObjectTypeAnnotation, contains only a private member variable for each of its properties, and a getter to retrieve the data from each of its private members.

## Todos
- Improved type-safety:
   - ReservedTypeAnnotation (e.g: PointPrimitive). These currently map to ReadableMap.
   - String enums, and Int enums don't actually generate Java enums.
- Verify if there are any compilation issues by wiring this up to the Codegen buck infra.
- To actually use the Pojos, we'll need C++/Jni codegen to transform C++ props into these Pojos. Building this out will give a more accurate assessment of the app-size cost of Pojos. However, we can do that if we deem that the app-size increase from adding just the Pojo classes is negligible enough to **not** rule out this entire approach.
- ~~Add some annotations to prevent these Pojo classes from being stripped at compile-time.~~ D26041103

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D26038189

fbshipit-source-id: c137c4ca6b043ee76adb354105aff6e0f270df86
This commit is contained in:
Ramanpreet Nara
2021-01-26 17:07:00 -08:00
committed by Facebook GitHub Bot
parent dc0b917fb5
commit 8461409e88
12 changed files with 1621 additions and 18 deletions
@@ -0,0 +1,186 @@
/**
* 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 {
ReservedPropTypeAnnotation,
NamedShape,
ObjectTypeAnnotation,
BooleanTypeAnnotation,
StringTypeAnnotation,
DoubleTypeAnnotation,
FloatTypeAnnotation,
Int32TypeAnnotation,
PropTypeAnnotation,
} from '../../../CodegenSchema';
const {capitalize} = require('../../Utils');
export type Pojo = {
name: string,
namespace: string,
properties: $ReadOnlyArray<PojoProperty>,
};
export type PojoProperty = NamedShape<PojoTypeAnnotation>;
export type PojoTypeAliasAnnotation = {
type: 'PojoTypeAliasTypeAnnotation',
name: string,
};
export type PojoTypeAnnotation =
| $ReadOnly<{
type: 'BooleanTypeAnnotation',
default: boolean | null,
}>
| $ReadOnly<{
type: 'StringTypeAnnotation',
default: string | null,
}>
| $ReadOnly<{
type: 'DoubleTypeAnnotation',
default: number,
}>
| $ReadOnly<{
type: 'FloatTypeAnnotation',
default: number | null,
}>
| $ReadOnly<{
type: 'Int32TypeAnnotation',
default: number,
}>
| $ReadOnly<{
type: 'StringEnumTypeAnnotation',
default: string,
options: $ReadOnlyArray<string>,
}>
| $ReadOnly<{
type: 'Int32EnumTypeAnnotation',
default: number,
options: $ReadOnlyArray<number>,
}>
| ReservedPropTypeAnnotation
| PojoTypeAliasAnnotation
| $ReadOnly<{
type: 'ArrayTypeAnnotation',
elementType:
| BooleanTypeAnnotation
| StringTypeAnnotation
| DoubleTypeAnnotation
| FloatTypeAnnotation
| Int32TypeAnnotation
| $ReadOnly<{
type: 'StringEnumTypeAnnotation',
default: string,
options: $ReadOnlyArray<string>,
}>
| PojoTypeAliasAnnotation
| ReservedPropTypeAnnotation
| $ReadOnly<{
type: 'ArrayTypeAnnotation',
elementType: PojoTypeAliasAnnotation,
}>,
}>;
class PojoCollector {
_pojos: Map<string, Pojo> = new Map();
process(
namespace: string,
pojoName: string,
typeAnnotation: PropTypeAnnotation,
): PojoTypeAnnotation {
switch (typeAnnotation.type) {
case 'ObjectTypeAnnotation': {
this._insertPojo(namespace, pojoName, typeAnnotation);
return {
type: 'PojoTypeAliasTypeAnnotation',
name: pojoName,
};
}
case 'ArrayTypeAnnotation': {
const arrayTypeAnnotation = typeAnnotation;
// TODO: Flow assumes elementType can be any. Fix this.
const elementType: $PropertyType<
typeof arrayTypeAnnotation,
'elementType',
> = arrayTypeAnnotation.elementType;
const pojoElementType = (() => {
switch (elementType.type) {
case 'ObjectTypeAnnotation': {
this._insertPojo(namespace, `${pojoName}Element`, elementType);
return {
type: 'PojoTypeAliasTypeAnnotation',
name: `${pojoName}Element`,
};
}
case 'ArrayTypeAnnotation': {
const {elementType: objectTypeAnnotation} = elementType;
this._insertPojo(
namespace,
`${pojoName}ElementElement`,
objectTypeAnnotation,
);
return {
type: 'ArrayTypeAnnotation',
elementType: {
type: 'PojoTypeAliasTypeAnnotation',
name: `${pojoName}ElementElement`,
},
};
}
default: {
return elementType;
}
}
})();
return {
type: 'ArrayTypeAnnotation',
elementType: pojoElementType,
};
}
default:
return typeAnnotation;
}
}
_insertPojo(
namespace: string,
pojoName: string,
objectTypeAnnotation: ObjectTypeAnnotation<PropTypeAnnotation>,
) {
const properties = objectTypeAnnotation.properties.map(property => {
const propertyPojoName = pojoName + capitalize(property.name);
return {
...property,
typeAnnotation: this.process(
namespace,
propertyPojoName,
property.typeAnnotation,
),
};
});
this._pojos.set(pojoName, {
name: pojoName,
namespace,
properties,
});
}
getAllPojos(): $ReadOnlyArray<Pojo> {
return [...this._pojos.values()];
}
}
module.exports = PojoCollector;
@@ -0,0 +1,80 @@
/**
* 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 {SchemaType} from '../../../CodegenSchema';
const PojoCollector = require('./PojoCollector');
const {capitalize} = require('../../Utils');
const {serializePojo} = require('./serializePojo');
type FilesOutput = Map<string, string>;
module.exports = {
generate(
libraryName: string,
schema: SchemaType,
packageName?: string,
): FilesOutput {
const pojoCollector = new PojoCollector();
const basePackageName = 'com.facebook.react.viewmanagers';
Object.keys(schema.modules).forEach(hasteModuleName => {
const module = schema.modules[hasteModuleName];
if (module.type !== 'Component') {
return;
}
const {components} = module;
// No components in this module
if (components == null) {
return null;
}
Object.keys(components)
.filter(componentName => {
const component = components[componentName];
return !(
component.excludedPlatforms &&
component.excludedPlatforms.includes('android')
);
})
.forEach(componentName => {
const component = components[componentName];
if (component == null) {
return;
}
const {props} = component;
pojoCollector.process(
capitalize(hasteModuleName),
`${capitalize(componentName)}Props`,
{
type: 'ObjectTypeAnnotation',
properties: props,
},
);
});
});
const pojoDir = basePackageName.split('.').join('/');
return new Map(
pojoCollector.getAllPojos().map(pojo => {
return [
`java/${pojoDir}/${pojo.namespace}/${pojo.name}.java`,
serializePojo(pojo, basePackageName),
];
}),
);
},
};
@@ -0,0 +1,285 @@
/**
* 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 {Pojo, PojoProperty, PojoTypeAnnotation} from './PojoCollector';
const {capitalize} = require('../../Utils');
type ImportCollector = ($import: string) => void;
function toJavaType(
typeAnnotation: PojoTypeAnnotation,
addImport: ImportCollector,
): string {
const importNullable = () => addImport('androidx.annotation.Nullable');
const importReadableMap = () =>
addImport('com.facebook.react.bridge.ReadableMap');
const importArrayList = () => addImport('java.util.ArrayList');
switch (typeAnnotation.type) {
/**
* Primitives
*/
case 'BooleanTypeAnnotation': {
if (typeAnnotation.default === null) {
importNullable();
return '@Nullable Boolean';
} else {
return 'boolean';
}
}
case 'StringTypeAnnotation': {
importNullable();
return '@Nullable String';
}
case 'DoubleTypeAnnotation': {
return 'double';
}
case 'FloatTypeAnnotation': {
if (typeAnnotation.default === null) {
importNullable();
return '@Nullable Float';
} else {
return 'float';
}
}
case 'Int32TypeAnnotation': {
return 'int';
}
/**
* Enums
*/
// TODO: Make StringEnumTypeAnnotation type-safe?
case 'StringEnumTypeAnnotation':
importNullable();
return '@Nullable String';
// TODO: Make Int32EnumTypeAnnotation type-safe?
case 'Int32EnumTypeAnnotation':
importNullable();
return '@Nullable Integer';
/**
* Reserved types
*/
case 'ReservedPropTypeAnnotation': {
switch (typeAnnotation.name) {
case 'ColorPrimitive':
importNullable();
return '@Nullable Integer';
// TODO: Make ImageSourcePrimitive type-safe
case 'ImageSourcePrimitive':
importNullable();
importReadableMap();
return '@Nullable ReadableMap';
// TODO: Make PointPrimitive type-safe
case 'PointPrimitive':
importNullable();
importReadableMap();
return '@Nullable ReadableMap';
// TODO: Make EdgeInsetsPrimitive type-safe
case 'EdgeInsetsPrimitive':
importNullable();
importReadableMap();
return '@Nullable ReadableMap';
default:
(typeAnnotation.name: empty);
throw new Error(
`Received unknown ReservedPropTypeAnnotation ${typeAnnotation.name}`,
);
}
}
/**
* Other Pojo objects
*/
case 'PojoTypeAliasTypeAnnotation': {
return typeAnnotation.name;
}
/**
* Arrays
*/
case 'ArrayTypeAnnotation': {
const {elementType} = typeAnnotation;
const elementTypeString = (() => {
switch (elementType.type) {
/**
* Primitives
*/
case 'BooleanTypeAnnotation': {
return 'Boolean';
}
case 'StringTypeAnnotation': {
importNullable();
return '@Nullable String';
}
case 'DoubleTypeAnnotation': {
return 'Double';
}
case 'FloatTypeAnnotation': {
return 'Float';
}
case 'Int32TypeAnnotation': {
return 'Integer';
}
/**
* Enums
*/
// TODO: Make StringEnums type-safe in Pojos
case 'StringEnumTypeAnnotation': {
importNullable();
return '@Nullable String';
}
/**
* Other Pojo objects
*/
case 'PojoTypeAliasTypeAnnotation': {
return elementType.name;
}
/**
* Reserved types
*/
case 'ReservedPropTypeAnnotation': {
switch (elementType.name) {
case 'ColorPrimitive':
importNullable();
return '@Nullable Integer';
// TODO: Make ImageSourcePrimitive type-safe
case 'ImageSourcePrimitive':
importNullable();
importReadableMap();
return '@Nullable ReadableMap';
// TODO: Make PointPrimitive type-safe
case 'PointPrimitive':
importNullable();
importReadableMap();
return '@Nullable ReadableMap';
// TODO: Make EdgeInsetsPrimitive type-safe
case 'EdgeInsetsPrimitive':
importNullable();
importReadableMap();
return '@Nullable ReadableMap';
default:
(elementType.name: empty);
throw new Error(
`Received unknown ReservedPropTypeAnnotation ${elementType.name}`,
);
}
}
// Arrays
case 'ArrayTypeAnnotation': {
const {elementType: pojoTypeAliasTypeAnnotation} = elementType;
importArrayList();
return `ArrayList<${pojoTypeAliasTypeAnnotation.name}>`;
}
default: {
(elementType.type: empty);
throw new Error(
`Unrecognized PojoTypeAnnotation Array element type annotation '${typeAnnotation.type}'`,
);
}
}
})();
importArrayList();
return `ArrayList<${elementTypeString}>`;
}
default: {
(typeAnnotation.type: empty);
throw new Error(
`Unrecognized PojoTypeAnnotation '${typeAnnotation.type}'`,
);
}
}
}
function toJavaMemberName(property: PojoProperty): string {
return `m${capitalize(property.name)}`;
}
function toJavaMemberDeclaration(
property: PojoProperty,
addImport: ImportCollector,
): string {
const type = toJavaType(property.typeAnnotation, addImport);
const memberName = toJavaMemberName(property);
return `private ${type} ${memberName};`;
}
function toJavaGetter(property: PojoProperty, addImport: ImportCollector) {
const type = toJavaType(property.typeAnnotation, addImport);
const getterName = `get${capitalize(property.name)}`;
const memberName = toJavaMemberName(property);
return `public ${type} ${getterName}() {
return ${memberName};
}`;
}
function serializePojo(pojo: Pojo, basePackageName: string): string {
const importSet: Set<string> = new Set();
const addImport = ($import: string) => {
importSet.add($import);
};
const indent = ' '.repeat(2);
const members = pojo.properties
.map(property => toJavaMemberDeclaration(property, addImport))
.map(member => `${indent}${member}`)
.join('\n');
const getters = pojo.properties
.map(property => toJavaGetter(property, addImport))
.map(getter =>
getter
.split('\n')
.map(line => `${indent}${line}`)
.join('\n'),
)
.join('\n');
const imports = [...importSet]
.map($import => `import ${$import};`)
.sort()
.join('\n');
return `/**
* 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.
*
* ${'@'}generated by codegen project: GeneratePropsJavaPojo.js
*/
package ${basePackageName}.${pojo.namespace};
${imports === '' ? '' : `\n${imports}\n`}
public class ${pojo.name} {
${members}
${getters}
}
`;
}
module.exports = {serializePojo};