Add intersection types in react-native-codegen for TypeScript (#35247)

Summary:
Refer to "Support intersection of object types, `{...a, ...b, ...}` in Flow is equivalent to `a & b & {...}` in TypeScript, the order and the form is not important." in https://github.com/facebook/react-native/issues/34872

In this change I also extract the common part from `getTypeAnnotation` and `getTypeAnnotationForArray` into `getCommonTypeAnnotation`. Most of the differences are about `default` in the schema. After a schema is generated from `getCommonTypeAnnotation` successfully, `getTypeAnnotation` will fill `default` if necessary, while `getTypeAnnotationForArray` does nothing.

## Changelog

[General] [Changed] - Add intersection types in react-native-codegen for TypeScript

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

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

Reviewed By: cipolleschi

Differential Revision: D41105744

Pulled By: lunaleaps

fbshipit-source-id: cd250a9594a54596a20ae26e57a1c801e2047703
This commit is contained in:
Zihan Chen (MSFT) 2022-11-10 15:28:17 -08:00 коммит произвёл Facebook GitHub Bot
Родитель 63a4539e4d
Коммит 813fd04118
5 изменённых файлов: 267 добавлений и 164 удалений

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

@ -21,7 +21,7 @@ const tsExtraCases = [
'ARRAY2_PROP_TYPES_NO_EVENTS',
'PROPS_AND_EVENTS_WITH_INTERFACES',
];
const ignoredCases = ['ARRAY_PROP_TYPES_NO_EVENTS'];
const ignoredCases = [];
compareSnaps(
flowFixtures,

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

@ -452,6 +452,13 @@ export interface ModuleProps extends ViewProps {
array_of_array_of_object_required_in_file: ReadonlyArray<
ReadonlyArray<ObjectType>
>;
// Nested array of array of object types (with spread)
array_of_array_of_object_required_with_spread: ReadonlyArray<
ReadonlyArray<
Readonly<ObjectType & {}>,
>,
>,
}
export default codegenNativeComponent<ModuleProps>(
@ -584,6 +591,9 @@ export interface ModuleProps extends ViewProps {
// Nested array of array of object types (in file)
array_of_array_of_object_required_in_file: readonly ObjectType[][];
// Nested array of array of object types (with spread)
array_of_array_of_object_required_with_spread: readonly Readonly<ObjectType & {}>[][];
}
export default codegenNativeComponent<ModuleProps>(

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

@ -1221,6 +1221,29 @@ exports[`RN Codegen TypeScript Parser can generate fixture ARRAY_PROP_TYPES_NO_E
}
}
}
},
{
'name': 'array_of_array_of_object_required_with_spread',
'optional': false,
'typeAnnotation': {
'type': 'ArrayTypeAnnotation',
'elementType': {
'type': 'ArrayTypeAnnotation',
'elementType': {
'type': 'ObjectTypeAnnotation',
'properties': [
{
'name': 'prop',
'optional': false,
'typeAnnotation': {
'type': 'StringTypeAnnotation',
'default': null
}
}
]
}
}
}
}
],
'commands': []
@ -1905,6 +1928,29 @@ exports[`RN Codegen TypeScript Parser can generate fixture ARRAY2_PROP_TYPES_NO_
}
}
}
},
{
'name': 'array_of_array_of_object_required_with_spread',
'optional': false,
'typeAnnotation': {
'type': 'ArrayTypeAnnotation',
'elementType': {
'type': 'ArrayTypeAnnotation',
'elementType': {
'type': 'ObjectTypeAnnotation',
'properties': [
{
'name': 'prop',
'optional': false,
'typeAnnotation': {
'type': 'StringTypeAnnotation',
'default': null
}
}
]
}
}
}
}
],
'commands': []

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

@ -11,7 +11,10 @@
'use strict';
import type {ASTNode} from '../utils';
import type {NamedShape} from '../../../CodegenSchema.js';
const {parseTopLevelType} = require('../parseTopLevelType');
const {
parseTopLevelType,
flattenIntersectionType,
} = require('../parseTopLevelType');
import type {TypeDeclarationMap} from '../../utils';
function getProperties(
@ -163,6 +166,102 @@ function detectArrayType<T>(
return null;
}
function buildObjectType<T>(
rawProperties: Array<$FlowFixMe>,
types: TypeDeclarationMap,
buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape<T>,
): $FlowFixMe {
const flattenedProperties = flattenProperties(rawProperties, types);
const properties = flattenedProperties
.map(prop => buildSchema(prop, types))
.filter(Boolean);
return {
type: 'ObjectTypeAnnotation',
properties,
};
}
function getCommonTypeAnnotation<T>(
name: string,
forArray: boolean,
type: string,
typeAnnotation: $FlowFixMe,
defaultValue: $FlowFixMe | void,
types: TypeDeclarationMap,
buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape<T>,
): $FlowFixMe {
switch (type) {
case 'TSTypeLiteral':
return buildObjectType(typeAnnotation.members, types, buildSchema);
case 'TSInterfaceDeclaration':
return buildObjectType([typeAnnotation], types, buildSchema);
case 'TSIntersectionType':
return buildObjectType(
flattenIntersectionType(typeAnnotation, types),
types,
buildSchema,
);
case 'ImageSource':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageSourcePrimitive',
};
case 'ImageRequest':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageRequestPrimitive',
};
case 'ColorValue':
case 'ProcessedColorValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
};
case 'PointValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'PointPrimitive',
};
case 'EdgeInsetsValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'EdgeInsetsPrimitive',
};
case 'TSUnionType':
return getUnionOfLiterals(
name,
forArray,
typeAnnotation.types,
defaultValue,
types,
);
case 'Int32':
return {
type: 'Int32TypeAnnotation',
};
case 'Double':
return {
type: 'DoubleTypeAnnotation',
};
case 'Float':
return {
type: 'FloatTypeAnnotation',
};
case 'TSBooleanKeyword':
return {
type: 'BooleanTypeAnnotation',
};
case 'Stringish':
case 'TSStringKeyword':
return {
type: 'StringTypeAnnotation',
};
default:
return undefined;
}
}
function getTypeAnnotationForArray<T>(
name: string,
typeAnnotation: $FlowFixMe,
@ -207,91 +306,57 @@ function getTypeAnnotationForArray<T>(
extractedTypeAnnotation.typeName?.name ||
extractedTypeAnnotation.type;
const common = getCommonTypeAnnotation(
name,
true,
type,
extractedTypeAnnotation,
defaultValue,
types,
buildSchema,
);
if (common) {
return common;
}
switch (type) {
case 'TSTypeLiteral':
case 'TSInterfaceDeclaration': {
const rawProperties =
type === 'TSInterfaceDeclaration'
? [extractedTypeAnnotation]
: extractedTypeAnnotation.members;
if (rawProperties === undefined) {
throw new Error(type);
}
return {
type: 'ObjectTypeAnnotation',
properties: flattenProperties(rawProperties, types)
.map(prop => buildSchema(prop, types))
.filter(Boolean),
};
}
case 'TSNumberKeyword':
return {
type: 'FloatTypeAnnotation',
};
case 'ImageSource':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageSourcePrimitive',
};
case 'ImageRequest':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageRequestPrimitive',
};
case 'ColorValue':
case 'ProcessedColorValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
};
case 'PointValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'PointPrimitive',
};
case 'EdgeInsetsValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'EdgeInsetsPrimitive',
};
case 'Stringish':
return {
type: 'StringTypeAnnotation',
};
case 'Int32':
return {
type: 'Int32TypeAnnotation',
};
case 'Double':
return {
type: 'DoubleTypeAnnotation',
};
case 'Float':
return {
type: 'FloatTypeAnnotation',
};
case 'TSBooleanKeyword':
return {
type: 'BooleanTypeAnnotation',
};
case 'TSStringKeyword':
return {
type: 'StringTypeAnnotation',
};
case 'TSUnionType':
return getUnionOfLiterals(
name,
true,
extractedTypeAnnotation.types,
defaultValue,
types,
);
default:
(type: empty);
throw new Error(`Unknown prop type for "${name}": ${type}`);
}
}
function setDefaultValue(
common: $FlowFixMe,
defaultValue: $FlowFixMe | void,
): void {
switch (common.type) {
case 'Int32TypeAnnotation':
case 'DoubleTypeAnnotation':
common.default = ((defaultValue ? defaultValue : 0): number);
break;
case 'FloatTypeAnnotation':
common.default = ((defaultValue === null
? null
: defaultValue
? defaultValue
: 0): number | null);
break;
case 'BooleanTypeAnnotation':
common.default = defaultValue === null ? null : !!defaultValue;
break;
case 'StringTypeAnnotation':
common.default = ((defaultValue === undefined ? null : defaultValue):
| string
| null);
break;
}
}
function getTypeAnnotation<T>(
name: string,
annotation: $FlowFixMe | ASTNode,
@ -319,39 +384,21 @@ function getTypeAnnotation<T>(
? typeAnnotation.typeName.name
: 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);
const common = getCommonTypeAnnotation(
name,
false,
type,
typeAnnotation,
defaultValue,
types,
buildSchema,
);
if (common) {
setDefaultValue(common, defaultValue);
return common;
}
return {
type: 'ObjectTypeAnnotation',
properties,
};
}
case 'ImageSource':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageSourcePrimitive',
};
case 'ImageRequest':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ImageRequestPrimitive',
};
case 'ColorValue':
case 'ProcessedColorValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'ColorPrimitive',
};
switch (type) {
case 'ColorArrayValue':
return {
type: 'ArrayTypeAnnotation',
@ -360,66 +407,10 @@ function getTypeAnnotation<T>(
name: 'ColorPrimitive',
},
};
case 'PointValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'PointPrimitive',
};
case 'EdgeInsetsValue':
return {
type: 'ReservedPropTypeAnnotation',
name: 'EdgeInsetsPrimitive',
};
case 'Int32':
return {
type: 'Int32TypeAnnotation',
default: ((defaultValue ? defaultValue : 0): number),
};
case 'Double':
return {
type: 'DoubleTypeAnnotation',
default: ((defaultValue ? defaultValue : 0): number),
};
case 'Float':
return {
type: 'FloatTypeAnnotation',
default: ((defaultValue === null
? null
: defaultValue
? defaultValue
: 0): number | null),
};
case 'TSBooleanKeyword':
return {
type: 'BooleanTypeAnnotation',
default: defaultValue === null ? null : !!defaultValue,
};
case 'TSStringKeyword':
return {
type: 'StringTypeAnnotation',
default: ((defaultValue === undefined ? null : defaultValue):
| string
| null),
};
case 'Stringish':
return {
type: 'StringTypeAnnotation',
default: ((defaultValue === undefined ? null : defaultValue):
| string
| null),
};
case 'TSNumberKeyword':
throw new Error(
`Cannot use "${type}" type annotation for "${name}": must use a specific numeric type like Int32, Double, or Float`,
);
case 'TSUnionType':
return getUnionOfLiterals(
name,
false,
typeAnnotation.types,
defaultValue,
types,
);
default:
(type: empty);
throw new Error(`Unknown prop type for "${name}": "${type}"`);
@ -498,6 +489,8 @@ function flattenProperties(
return flattenProperties(property.members, types);
} else if (property.type === 'TSInterfaceDeclaration') {
return flattenProperties(getProperties(property.id.name, types), types);
} else if (property.type === 'TSIntersectionType') {
return flattenProperties(property.types, types);
} else {
throw new Error(
`${property.type} is not a supported object literal type.`,

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

@ -184,6 +184,60 @@ function parseTopLevelType(
}
}
function handleIntersectionAndParen(
type: $FlowFixMe,
result: Array<$FlowFixMe>,
knownTypes?: TypeDeclarationMap,
): void {
switch (type.type) {
case 'TSParenthesizedType': {
handleIntersectionAndParen(type.typeAnnotation, result, knownTypes);
break;
}
case 'TSIntersectionType': {
for (const t of type.types) {
handleIntersectionAndParen(t, result, knownTypes);
}
break;
}
case 'TSTypeReference':
if (type.typeName.name === 'Readonly') {
handleIntersectionAndParen(
type.typeParameters.params[0],
result,
knownTypes,
);
} else if (type.typeName.name === 'WithDefault') {
throw new Error('WithDefault<> is now allowed in intersection types.');
} else if (!knownTypes) {
result.push(type);
} else {
const resolvedType = getValueFromTypes(type, knownTypes);
if (
resolvedType.type === 'TSTypeReference' &&
resolvedType.typeName.name === type.typeName.name
) {
result.push(type);
} else {
handleIntersectionAndParen(resolvedType, result, knownTypes);
}
}
break;
default:
result.push(type);
}
}
function flattenIntersectionType(
type: $FlowFixMe,
knownTypes?: TypeDeclarationMap,
): Array<$FlowFixMe> {
const result: Array<$FlowFixMe> = [];
handleIntersectionAndParen(type, result, knownTypes);
return result;
}
module.exports = {
parseTopLevelType,
flattenIntersectionType,
};