Fix interface support in turbo module TypeScript codegen (component only) (#34778)

Summary:
Interface was supported in component, but it only allows interfaces in limited cases.

In this change, I extended interface support to all places where object literal type is supported.

I also refactor the code so that properties and events are able to share the same implementation.

In order not to mess up the diff, I noticed that implementations are repeated in processing array properties and non-array properties. But I leave it without refactoring. I will do it in future PRs.

I also commented potential problems I found in the code.

## Changelog

[General] [Changed] - Fix interface support in turbo module TypeScript codegen (component only)

Pull Request resolved: https://github.com/facebook/react-native/pull/34778

Test Plan: `yarn jest react-native-codegen` passed

Reviewed By: cortinico

Differential Revision: D39809230

Pulled By: cipolleschi

fbshipit-source-id: cfb51ce915249b5abceafee1c08b7e5762d03519
This commit is contained in:
Zihan Chen (MSFT) 2022-09-27 06:59:57 -07:00 коммит произвёл Facebook GitHub Bot
Родитель e78a495900
Коммит 8dc6bec719
6 изменённых файлов: 483 добавлений и 63 удалений

Просмотреть файл

@ -17,7 +17,11 @@ const flowSnaps = require('../../../../src/parsers/flow/components/__tests__/__s
const flowExtraCases = [];
const tsFixtures = require('../../typescript/components/__test_fixtures__/fixtures.js');
const tsSnaps = require('../../../../src/parsers/typescript/components/__tests__/__snapshots__/typescript-component-parser-test.js.snap');
const tsExtraCases = ['ARRAY2_PROP_TYPES_NO_EVENTS', 'ARRAY2_STATE_TYPES'];
const tsExtraCases = [
'ARRAY2_PROP_TYPES_NO_EVENTS',
'ARRAY2_STATE_TYPES',
'PROPS_AND_EVENTS_WITH_INTERFACES',
];
const ignoredCases = ['ARRAY_PROP_TYPES_NO_EVENTS', 'ARRAY_STATE_TYPES'];
compareSnaps(

Просмотреть файл

@ -1095,6 +1095,49 @@ export default codegenNativeComponent<ModuleProps>(
) as NativeType;
`;
const PROPS_AND_EVENTS_WITH_INTERFACES = `
import type {
BubblingEventHandler,
DirectEventHandler,
Int32,
} from 'CodegenTypes';
import type {ViewProps} from 'ViewPropTypes';
import type {HostComponent} from 'react-native';
const codegenNativeComponent = require('codegenNativeComponent');
export interface Base1 {
readonly x: string;
}
export interface Base2 {
readonly y: Int32;
}
export interface Derived extends Base1, Base2 {
readonly z: boolean;
}
export interface ModuleProps extends ViewProps {
// Props
ordinary_prop: Derived;
readonly_prop: Readonly<Derived>;
ordinary_array_prop?: readonly Derived[];
readonly_array_prop?: readonly Readonly<Derived>[];
ordinary_nested_array_prop?: readonly Derived[][];
readonly_nested_array_prop?: readonly Readonly<Derived>[][];
// Events
onDirect: DirectEventHandler<Derived>;
onBubbling: BubblingEventHandler<Readonly<Derived>>;
}
export default codegenNativeComponent<ModuleProps>('Module', {
interfaceOnly: true,
paperComponentName: 'RCTModule',
}) as HostComponent<ModuleProps>;
`;
// === STATE ===
const ALL_STATE_TYPES = `
/**
@ -1630,6 +1673,7 @@ module.exports = {
COMMANDS_DEFINED_WITH_ALL_TYPES,
PROPS_AS_EXTERNAL_TYPES,
COMMANDS_WITH_EXTERNAL_TYPES,
PROPS_AND_EVENTS_WITH_INTERFACES,
ALL_STATE_TYPES,
ARRAY_STATE_TYPES,
ARRAY2_STATE_TYPES,

Просмотреть файл

@ -12640,6 +12640,317 @@ exports[`RN Codegen TypeScript Parser can generate fixture PROPS_AND_EVENTS_TYPE
}"
`;
exports[`RN Codegen TypeScript Parser can generate fixture PROPS_AND_EVENTS_WITH_INTERFACES 1`] = `
"{
'modules': {
'Module': {
'type': 'Component',
'components': {
'Module': {
'interfaceOnly': true,
'paperComponentName': 'RCTModule',
'extendsProps': [
{
'type': 'ReactNativeBuiltInType',
'knownTypeName': 'ReactNativeCoreViewProps'
}
],
'events': [
{
'name': 'onDirect',
'optional': false,
'bubblingType': 'direct',
'typeAnnotation': {
'type': 'EventTypeAnnotation',
'argument': {
'type': 'ObjectTypeAnnotation',
'properties': [
{
'name': 'x',
'optional': false,
'typeAnnotation': {
'type': 'StringTypeAnnotation'
}
},
{
'name': 'y',
'optional': false,
'typeAnnotation': {
'type': 'Int32TypeAnnotation'
}
},
{
'name': 'z',
'optional': false,
'typeAnnotation': {
'type': 'BooleanTypeAnnotation'
}
}
]
}
}
},
{
'name': 'onBubbling',
'optional': false,
'bubblingType': 'bubble',
'typeAnnotation': {
'type': 'EventTypeAnnotation',
'argument': {
'type': 'ObjectTypeAnnotation',
'properties': [
{
'name': 'x',
'optional': false,
'typeAnnotation': {
'type': 'StringTypeAnnotation'
}
},
{
'name': 'y',
'optional': false,
'typeAnnotation': {
'type': 'Int32TypeAnnotation'
}
},
{
'name': 'z',
'optional': false,
'typeAnnotation': {
'type': 'BooleanTypeAnnotation'
}
}
]
}
}
}
],
'props': [
{
'name': 'ordinary_prop',
'optional': false,
'typeAnnotation': {
'type': 'ObjectTypeAnnotation',
'properties': [
{
'name': 'x',
'optional': false,
'typeAnnotation': {
'type': 'StringTypeAnnotation',
'default': null
}
},
{
'name': 'y',
'optional': false,
'typeAnnotation': {
'type': 'Int32TypeAnnotation',
'default': 0
}
},
{
'name': 'z',
'optional': false,
'typeAnnotation': {
'type': 'BooleanTypeAnnotation',
'default': false
}
}
]
}
},
{
'name': 'readonly_prop',
'optional': false,
'typeAnnotation': {
'type': 'ObjectTypeAnnotation',
'properties': [
{
'name': 'x',
'optional': false,
'typeAnnotation': {
'type': 'StringTypeAnnotation',
'default': null
}
},
{
'name': 'y',
'optional': false,
'typeAnnotation': {
'type': 'Int32TypeAnnotation',
'default': 0
}
},
{
'name': 'z',
'optional': false,
'typeAnnotation': {
'type': 'BooleanTypeAnnotation',
'default': false
}
}
]
}
},
{
'name': 'ordinary_array_prop',
'optional': true,
'typeAnnotation': {
'type': 'ArrayTypeAnnotation',
'elementType': {
'type': 'ObjectTypeAnnotation',
'properties': [
{
'name': 'x',
'optional': false,
'typeAnnotation': {
'type': 'StringTypeAnnotation',
'default': null
}
},
{
'name': 'y',
'optional': false,
'typeAnnotation': {
'type': 'Int32TypeAnnotation',
'default': 0
}
},
{
'name': 'z',
'optional': false,
'typeAnnotation': {
'type': 'BooleanTypeAnnotation',
'default': false
}
}
]
}
}
},
{
'name': 'readonly_array_prop',
'optional': true,
'typeAnnotation': {
'type': 'ArrayTypeAnnotation',
'elementType': {
'type': 'ObjectTypeAnnotation',
'properties': [
{
'name': 'x',
'optional': false,
'typeAnnotation': {
'type': 'StringTypeAnnotation',
'default': null
}
},
{
'name': 'y',
'optional': false,
'typeAnnotation': {
'type': 'Int32TypeAnnotation',
'default': 0
}
},
{
'name': 'z',
'optional': false,
'typeAnnotation': {
'type': 'BooleanTypeAnnotation',
'default': false
}
}
]
}
}
},
{
'name': 'ordinary_nested_array_prop',
'optional': true,
'typeAnnotation': {
'type': 'ArrayTypeAnnotation',
'elementType': {
'type': 'ArrayTypeAnnotation',
'elementType': {
'type': 'ObjectTypeAnnotation',
'properties': [
{
'name': 'x',
'optional': false,
'typeAnnotation': {
'type': 'StringTypeAnnotation',
'default': null
}
},
{
'name': 'y',
'optional': false,
'typeAnnotation': {
'type': 'Int32TypeAnnotation',
'default': 0
}
},
{
'name': 'z',
'optional': false,
'typeAnnotation': {
'type': 'BooleanTypeAnnotation',
'default': false
}
}
]
}
}
}
},
{
'name': 'readonly_nested_array_prop',
'optional': true,
'typeAnnotation': {
'type': 'ArrayTypeAnnotation',
'elementType': {
'type': 'ArrayTypeAnnotation',
'elementType': {
'type': 'ObjectTypeAnnotation',
'properties': [
{
'name': 'x',
'optional': false,
'typeAnnotation': {
'type': 'StringTypeAnnotation',
'default': null
}
},
{
'name': 'y',
'optional': false,
'typeAnnotation': {
'type': 'Int32TypeAnnotation',
'default': 0
}
},
{
'name': 'z',
'optional': false,
'typeAnnotation': {
'type': 'BooleanTypeAnnotation',
'default': false
}
}
]
}
}
}
}
],
'commands': []
}
}
}
}
}"
`;
exports[`RN Codegen TypeScript Parser can generate fixture PROPS_AS_EXTERNAL_TYPES 1`] = `
"{
'modules': {

Просмотреть файл

@ -45,38 +45,29 @@ function getProperties(
}
function getTypeAnnotationForObjectAsArrayElement<T>(
objectType: $FlowFixMe,
types: TypeDeclarationMap,
buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape<T>,
): $FlowFixMe {
return {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(
objectType.typeParameters.params[0].members ||
objectType.typeParameters.params,
types,
)
.map(prop => buildSchema(prop, types))
.filter(Boolean),
};
}
function getTypeAnnotationForArrayOfArrayOfObject<T>(
name: string,
typeAnnotation: $FlowFixMe,
types: TypeDeclarationMap,
buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape<T>,
): $FlowFixMe {
// We need to go yet another level deeper to resolve
// types that may be defined in a type alias
const nestedObjectType = getValueFromTypes(typeAnnotation, types);
// for array of array of a type
// such type must be an object literal
const elementType = getTypeAnnotationForArray(
name,
typeAnnotation,
null,
types,
buildSchema,
);
if (elementType.type !== 'ObjectTypeAnnotation') {
throw new Error(
`Only array of array of object is supported for "${name}".`,
);
}
return {
type: 'ArrayTypeAnnotation',
elementType: getTypeAnnotationForObjectAsArrayElement(
nestedObjectType,
types,
buildSchema,
),
elementType,
};
}
@ -121,7 +112,8 @@ function getTypeAnnotationForArray<T>(
// Covers: T[]
if (typeAnnotation.type === 'TSArrayType') {
return getTypeAnnotationForArrayOfArrayOfObject(
return getTypeAnnotationForObjectAsArrayElement(
name,
typeAnnotation.elementType,
types,
buildSchema,
@ -133,8 +125,10 @@ function getTypeAnnotationForArray<T>(
const objectType = getValueFromTypes(extractedTypeAnnotation, types);
if (objectType.typeName.name === 'Readonly') {
return getTypeAnnotationForObjectAsArrayElement(
objectType,
return getTypeAnnotationForArray(
name,
objectType.typeParameters.params[0],
defaultValue,
types,
buildSchema,
);
@ -142,7 +136,8 @@ function getTypeAnnotationForArray<T>(
// Covers: ReadonlyArray<T>
if (objectType.typeName.name === 'ReadonlyArray') {
return getTypeAnnotationForArrayOfArrayOfObject(
return getTypeAnnotationForObjectAsArrayElement(
name,
objectType.typeParameters.params[0],
types,
buildSchema,
@ -158,6 +153,19 @@ function getTypeAnnotationForArray<T>(
extractedTypeAnnotation.type;
switch (type) {
case 'TSTypeLiteral':
case 'TSInterfaceDeclaration': {
const rawProperties =
type === 'TSInterfaceDeclaration'
? [typeAnnotation]
: typeAnnotation.members;
return {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(rawProperties, types)
.map(prop => buildSchema(prop, types))
.filter(Boolean),
};
}
case 'TSNumberKeyword':
return {
type: 'FloatTypeAnnotation',
@ -336,27 +344,28 @@ function getTypeAnnotation<T>(
};
}
// Covers: Readonly<T>, Readonly<{ ... }>, Readonly<T | U ...>
if (
(typeAnnotation.type === 'TSTypeReference' ||
typeAnnotation.type === 'TSTypeLiteral') &&
typeAnnotation.typeName?.name === 'Readonly'
typeAnnotation.type === 'TSTypeReference' &&
typeAnnotation.typeName?.name === 'Readonly' &&
typeAnnotation.typeParameters.type === 'TSTypeParameterInstantiation'
) {
const rawProperties =
typeAnnotation.typeParameters.params[0].members ||
(typeAnnotation.typeParameters.params[0].types &&
typeAnnotation.typeParameters.params[0].types[0].members) ||
typeAnnotation.typeParameters.params;
const flattenedProperties = flattenProperties(rawProperties, types);
const properties = flattenedProperties
.map(prop => buildSchema(prop, types))
.filter(Boolean);
return {
type: 'ObjectTypeAnnotation',
properties,
};
// TODO:
// the original implementation assume Readonly<TSUnionType>
// to be Readonly<{ ... } | null | undefined>
// without actually verifying it
let elementType = typeAnnotation.typeParameters.params[0];
if (elementType.type === 'TSUnionType') {
elementType = elementType.types[0];
}
return getTypeAnnotation(
name,
elementType,
defaultValue,
withNullDefault,
types,
buildSchema,
);
}
const type =
@ -366,6 +375,22 @@ function getTypeAnnotation<T>(
: typeAnnotation.type;
switch (type) {
case 'TSTypeLiteral':
case 'TSInterfaceDeclaration': {
const rawProperties =
type === 'TSInterfaceDeclaration'
? [typeAnnotation]
: typeAnnotation.members;
const flattenedProperties = flattenProperties(rawProperties, types);
const properties = flattenedProperties
.map(prop => buildSchema(prop, types))
.filter(Boolean);
return {
type: 'ObjectTypeAnnotation',
properties,
};
}
case 'ImageSource':
return {
type: 'ReservedPropTypeAnnotation',
@ -662,11 +687,22 @@ function flattenProperties(
getProperties(property.typeName.name, types),
types,
);
} else if (property.type === 'TSExpressionWithTypeArguments') {
} else if (
property.type === 'TSExpressionWithTypeArguments' ||
property.type === 'TSInterfaceHeritage'
) {
return flattenProperties(
getProperties(property.expression.name, types),
types,
);
} else if (property.type === 'TSTypeLiteral') {
return flattenProperties(property.members, types);
} else if (property.type === 'TSInterfaceDeclaration') {
return flattenProperties(getProperties(property.id.name, types), types);
} else {
throw new Error(
`${property.type} is not a supported object literal type.`,
);
}
})
.filter(Boolean)

Просмотреть файл

@ -15,6 +15,7 @@ import type {
NamedShape,
EventTypeAnnotation,
} from '../../../CodegenSchema.js';
const {flattenProperties} = require('./componentsUtils');
function getPropertyType(
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
@ -126,16 +127,33 @@ function findEventArgumentsAndType(
bubblingType: void | 'direct' | 'bubble',
paperName: ?$FlowFixMe,
) {
if (typeAnnotation.type === 'TSInterfaceDeclaration') {
return {
argumentProps: flattenProperties([typeAnnotation], types),
paperTopLevelNameDeprecated: paperName,
bubblingType,
};
}
if (typeAnnotation.type === 'TSTypeLiteral') {
return {
argumentProps: typeAnnotation.members,
paperTopLevelNameDeprecated: paperName,
bubblingType,
};
}
if (!typeAnnotation.typeName) {
throw new Error("typeAnnotation of event doesn't have a name");
}
const name = typeAnnotation.typeName.name;
if (name === 'Readonly') {
return {
argumentProps: typeAnnotation.typeParameters.params[0].members,
paperTopLevelNameDeprecated: paperName,
return findEventArgumentsAndType(
typeAnnotation.typeParameters.params[0],
types,
bubblingType,
};
paperName,
);
} else if (name === 'BubblingEventHandler' || name === 'DirectEventHandler') {
const eventType = name === 'BubblingEventHandler' ? 'bubble' : 'direct';
const paperTopLevelNameDeprecated =
@ -160,8 +178,12 @@ function findEventArgumentsAndType(
);
}
} else if (types[name]) {
let elementType = types[name];
if (elementType.type === 'TSTypeAliasDeclaration') {
elementType = elementType.typeAnnotation;
}
return findEventArgumentsAndType(
types[name].typeAnnotation,
elementType,
types,
bubblingType,
paperName,

Просмотреть файл

@ -125,15 +125,18 @@ function resolveTypeAnnotation(
}
function getValueFromTypes(value: ASTNode, types: TypeDeclarationMap): ASTNode {
if (value.type === 'TSTypeReference' && types[value.typeName.name]) {
return getValueFromTypes(types[value.typeName.name], types);
switch (value.type) {
case 'TSTypeReference':
if (types[value.typeName.name]) {
return getValueFromTypes(types[value.typeName.name], types);
} else {
return value;
}
case 'TSTypeAliasDeclaration':
return getValueFromTypes(value.typeAnnotation, types);
default:
return value;
}
if (value.type === 'TSTypeAliasDeclaration') {
return value.typeAnnotation;
}
return value;
}
export type ParserErrorCapturer = <T>(fn: () => T) => ?T;