From 00b795642a6562fb52d6df12e367b84674994623 Mon Sep 17 00:00:00 2001 From: "Zihan Chen (MSFT)" <53799235+ZihanChen-MSFT@users.noreply.github.com> Date: Fri, 7 Oct 2022 03:13:38 -0700 Subject: [PATCH] Refactor in turbo module TypeScript codegen: process `(T)`, `T|U`, `T|undefined` and related stuff in a central place (#34814) Summary: `TSParenthesizedType`, `TSUnionType`, `TSNullKeyword`, `TSUndefinedKeyword`, `TSVoidKeyword` etc are repeatly processed in so many places. In this change I put them in a new file `parseTopLevelType.js`, and everyone call that file, all repeat implementation are deleted. The `parseTopLevelType` function will look into a type consisted by the above types in any possible combination (but still very easy to do), and tell you if a type is nullable, or if there is a default value, and what is the real type with all noises removed. Array types and union types are processed in component twice, for property of array, and property of nested arrays (`componentsUtils.js`). They are extracted into single functions. ## Changelog [General] [Changed] - Refactor in turbo module TypeScript codegen: process `(T)`, `T|U`, `T|undefined` and related stuff in a central place Pull Request resolved: https://github.com/facebook/react-native/pull/34814 Test Plan: `yarn jest react-native-codegen` passed Reviewed By: yungsters, sammy-SC Differential Revision: D40094373 fbshipit-source-id: f28e145bc4e7734be9036815ea425d820eadb8f0 --- .../parsers/typescript/components/commands.js | 15 +- .../typescript/components/componentsUtils.js | 605 ++++++------------ .../parsers/typescript/components/events.js | 80 +-- .../parsers/typescript/components/props.js | 3 +- .../parsers/typescript/components/states.js | 3 +- .../src/parsers/typescript/modules/index.js | 29 - .../parsers/typescript/parseTopLevelType.js | 189 ++++++ .../src/parsers/typescript/utils.js | 34 +- 8 files changed, 430 insertions(+), 528 deletions(-) create mode 100644 packages/react-native-codegen/src/parsers/typescript/parseTopLevelType.js diff --git a/packages/react-native-codegen/src/parsers/typescript/components/commands.js b/packages/react-native-codegen/src/parsers/typescript/components/commands.js index c8b56c205dd0c6..fb827d2cbc98c1 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/commands.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/commands.js @@ -15,19 +15,18 @@ import type { CommandTypeAnnotation, } from '../../../CodegenSchema.js'; import type {TypeDeclarationMap} from '../utils.js'; - -const {getValueFromTypes} = require('../utils.js'); +const {parseTopLevelType} = require('../parseTopLevelType'); type EventTypeAST = Object; function buildCommandSchema(property: EventTypeAST, types: TypeDeclarationMap) { - const name = property.key.name; - const optional = property.optional || false; - const value = getValueFromTypes( + const topLevelType = parseTopLevelType( property.typeAnnotation.typeAnnotation, types, ); - + const name = property.key.name; + const optional = property.optional || topLevelType.optional; + const value = topLevelType.type; const firstParam = value.parameters[0].typeAnnotation; if ( @@ -45,10 +44,10 @@ function buildCommandSchema(property: EventTypeAST, types: TypeDeclarationMap) { const params = value.parameters.slice(1).map(param => { const paramName = param.name; - const paramValue = getValueFromTypes( + const paramValue = parseTopLevelType( param.typeAnnotation.typeAnnotation, types, - ); + ).type; const type = paramValue.type === 'TSTypeReference' diff --git a/packages/react-native-codegen/src/parsers/typescript/components/componentsUtils.js b/packages/react-native-codegen/src/parsers/typescript/components/componentsUtils.js index e9b9888c497b27..d608a491eeca06 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/componentsUtils.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/componentsUtils.js @@ -12,7 +12,7 @@ import type {ASTNode} from '../utils'; import type {TypeDeclarationMap} from '../utils.js'; import type {NamedShape} from '../../../CodegenSchema.js'; -const {getValueFromTypes} = require('../utils.js'); +const {parseTopLevelType} = require('../parseTopLevelType'); function getProperties( typeName: string, @@ -44,105 +44,160 @@ function getProperties( } } -function getTypeAnnotationForObjectAsArrayElement( +function getUnionOfLiterals( name: string, - typeAnnotation: $FlowFixMe, + forArray: boolean, + elementTypes: $FlowFixMe[], + defaultValue: $FlowFixMe | void, types: TypeDeclarationMap, - buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape, -): $FlowFixMe { - // 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, - }; -} +) { + elementTypes.reduce((lastType, currType) => { + const lastFlattenedType = + lastType && lastType.type === 'TSLiteralType' + ? lastType.literal.type + : lastType.type; + const currFlattenedType = + currType.type === 'TSLiteralType' ? currType.literal.type : currType.type; + + if (lastFlattenedType && currFlattenedType !== lastFlattenedType) { + throw new Error(`Mixed types are not supported (see "${name}")`); + } + return currType; + }); -function getTypeAnnotationForArray( - name: string, - typeAnnotation: $FlowFixMe, - defaultValue: $FlowFixMe | null, - types: TypeDeclarationMap, - buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape, -): $FlowFixMe { - if (typeAnnotation.type === 'TSParenthesizedType') { - return getTypeAnnotationForArray( - name, - typeAnnotation.typeAnnotation, - defaultValue, - types, - buildSchema, - ); + if (defaultValue === undefined) { + throw new Error(`A default enum value is required for "${name}"`); } - const extractedTypeAnnotation = getValueFromTypes(typeAnnotation, types); - + const unionType = elementTypes[0].type; if ( - extractedTypeAnnotation.type === 'TSUnionType' && - extractedTypeAnnotation.types.some( - t => t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword', - ) + unionType === 'TSLiteralType' && + elementTypes[0].literal?.type === 'StringLiteral' + ) { + return { + type: 'StringEnumTypeAnnotation', + default: (defaultValue: string), + options: elementTypes.map(option => option.literal.value), + }; + } else if ( + unionType === 'TSLiteralType' && + elementTypes[0].literal?.type === 'NumericLiteral' ) { + if (forArray) { + throw new Error(`Arrays of int enums are not supported (see: "${name}")`); + } else { + return { + type: 'Int32EnumTypeAnnotation', + default: (defaultValue: number), + options: elementTypes.map(option => option.literal.value), + }; + } + } else { throw new Error( - 'Nested optionals such as "ReadonlyArray" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray | null | undefined"', + `Unsupported union type for "${name}", received "${ + unionType === 'TSLiteralType' + ? elementTypes[0].literal?.type + : unionType + }"`, ); } +} +function detectArrayType( + name: string, + typeAnnotation: $FlowFixMe | ASTNode, + defaultValue: $FlowFixMe | void, + types: TypeDeclarationMap, + buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape, +): $FlowFixMe { + // Covers: readonly T[] if ( - extractedTypeAnnotation.type === 'TSTypeReference' && - extractedTypeAnnotation.typeName.name === 'WithDefault' + typeAnnotation.type === 'TSTypeOperator' && + typeAnnotation.operator === 'readonly' && + typeAnnotation.typeAnnotation.type === 'TSArrayType' ) { - throw new Error( - 'Nested defaults such as "ReadonlyArray>" are not supported, please declare defaults at the top level of value definitions as in "WithDefault, false>"', - ); + return { + type: 'ArrayTypeAnnotation', + elementType: getTypeAnnotationForArray( + name, + typeAnnotation.typeAnnotation.elementType, + defaultValue, + types, + buildSchema, + ), + }; } // Covers: T[] if (typeAnnotation.type === 'TSArrayType') { - return getTypeAnnotationForObjectAsArrayElement( - name, - typeAnnotation.elementType, - types, - buildSchema, - ); - } - - if (extractedTypeAnnotation.type === 'TSTypeReference') { - // Resolve the type alias if it's not defined inline - const objectType = getValueFromTypes(extractedTypeAnnotation, types); - - if (objectType.typeName.name === 'Readonly') { - return getTypeAnnotationForArray( + return { + type: 'ArrayTypeAnnotation', + elementType: getTypeAnnotationForArray( name, - objectType.typeParameters.params[0], + typeAnnotation.elementType, defaultValue, types, buildSchema, - ); - } + ), + }; + } - // Covers: ReadonlyArray - if (objectType.typeName.name === 'ReadonlyArray') { - return getTypeAnnotationForObjectAsArrayElement( + // Covers: Array and ReadonlyArray + if ( + typeAnnotation.type === 'TSTypeReference' && + (typeAnnotation.typeName.name === 'ReadonlyArray' || + typeAnnotation.typeName.name === 'Array') + ) { + return { + type: 'ArrayTypeAnnotation', + elementType: getTypeAnnotationForArray( name, - objectType.typeParameters.params[0], + typeAnnotation.typeParameters.params[0], + defaultValue, types, buildSchema, + ), + }; + } + + return null; +} + +function getTypeAnnotationForArray( + name: string, + typeAnnotation: $FlowFixMe, + defaultValue: $FlowFixMe | void, + types: TypeDeclarationMap, + buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape, +): $FlowFixMe { + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType(typeAnnotation, types); + if (topLevelType.defaultValue !== undefined) { + throw new Error( + 'Nested optionals such as "ReadonlyArray" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray | null | undefined"', + ); + } + if (topLevelType.optional) { + throw new Error( + 'Nested optionals such as "ReadonlyArray" are not supported, please declare optionals at the top level of value definitions as in "ReadonlyArray | null | undefined"', + ); + } + + const extractedTypeAnnotation = topLevelType.type; + const arrayType = detectArrayType( + name, + extractedTypeAnnotation, + defaultValue, + types, + buildSchema, + ); + if (arrayType) { + if (arrayType.elementType.type !== 'ObjectTypeAnnotation') { + throw new Error( + `Only array of array of object is supported for "${name}".`, ); } + return arrayType; } const type = @@ -157,8 +212,11 @@ function getTypeAnnotationForArray( case 'TSInterfaceDeclaration': { const rawProperties = type === 'TSInterfaceDeclaration' - ? [typeAnnotation] - : typeAnnotation.members; + ? [extractedTypeAnnotation] + : extractedTypeAnnotation.members; + if (rawProperties === undefined) { + throw new Error(type); + } return { type: 'ObjectTypeAnnotation', properties: flattenProperties(rawProperties, types) @@ -216,52 +274,13 @@ function getTypeAnnotationForArray( type: 'StringTypeAnnotation', }; case 'TSUnionType': - typeAnnotation.types.reduce((lastType, currType) => { - const lastFlattenedType = - lastType && lastType.type === 'TSLiteralType' - ? lastType.literal.type - : lastType.type; - const currFlattenedType = - currType.type === 'TSLiteralType' - ? currType.literal.type - : currType.type; - - if (lastFlattenedType && currFlattenedType !== lastFlattenedType) { - throw new Error(`Mixed types are not supported (see "${name}")`); - } - return currType; - }); - - if (defaultValue === null) { - throw new Error(`A default enum value is required for "${name}"`); - } - - const unionType = typeAnnotation.types[0].type; - if ( - unionType === 'TSLiteralType' && - typeAnnotation.types[0].literal?.type === 'StringLiteral' - ) { - return { - type: 'StringEnumTypeAnnotation', - default: (defaultValue: string), - options: typeAnnotation.types.map(option => option.literal.value), - }; - } else if ( - unionType === 'TSLiteralType' && - typeAnnotation.types[0].literal?.type === 'NumericLiteral' - ) { - throw new Error( - `Arrays of int enums are not supported (see: "${name}")`, - ); - } else { - throw new Error( - `Unsupported union type for "${name}", received "${ - unionType === 'TSLiteralType' - ? typeAnnotation.types[0].literal?.type - : unionType - }"`, - ); - } + return getUnionOfLiterals( + name, + true, + extractedTypeAnnotation.types, + defaultValue, + types, + ); default: (type: empty); throw new Error(`Unknown prop type for "${name}": ${type}`); @@ -271,101 +290,22 @@ function getTypeAnnotationForArray( function getTypeAnnotation( name: string, annotation: $FlowFixMe | ASTNode, - defaultValue: $FlowFixMe | null, - withNullDefault: boolean, + defaultValue: $FlowFixMe | void, types: TypeDeclarationMap, buildSchema: (property: PropAST, types: TypeDeclarationMap) => ?NamedShape, ): $FlowFixMe { - const typeAnnotation = getValueFromTypes(annotation, types); - - // Covers: (T) - if (typeAnnotation.type === 'TSParenthesizedType') { - return getTypeAnnotation( - name, - typeAnnotation.typeAnnotation, - defaultValue, - withNullDefault, - types, - buildSchema, - ); - } - - // Covers: readonly T[] - if ( - typeAnnotation.type === 'TSTypeOperator' && - typeAnnotation.operator === 'readonly' && - typeAnnotation.typeAnnotation.type === 'TSArrayType' - ) { - return { - type: 'ArrayTypeAnnotation', - elementType: getTypeAnnotationForArray( - name, - typeAnnotation.typeAnnotation.elementType, - defaultValue, - types, - buildSchema, - ), - }; - } - - // Covers: ReadonlyArray - if ( - typeAnnotation.type === 'TSTypeReference' && - typeAnnotation.typeName.name === 'ReadonlyArray' - ) { - return { - type: 'ArrayTypeAnnotation', - elementType: getTypeAnnotationForArray( - name, - typeAnnotation.typeParameters.params[0], - defaultValue, - types, - buildSchema, - ), - }; - } - - // Covers: Readonly - if ( - typeAnnotation.type === 'TSTypeReference' && - typeAnnotation.typeName?.name === 'Readonly' && - typeAnnotation.typeParameters.type === 'TSTypeParameterInstantiation' && - typeAnnotation.typeParameters.params[0].type === 'TSArrayType' - ) { - return { - type: 'ArrayTypeAnnotation', - elementType: getTypeAnnotationForArray( - name, - typeAnnotation.typeParameters.params[0], - defaultValue, - types, - buildSchema, - ), - }; - } - - // Covers: Readonly, Readonly<{ ... }>, Readonly - if ( - typeAnnotation.type === 'TSTypeReference' && - typeAnnotation.typeName?.name === 'Readonly' && - typeAnnotation.typeParameters.type === 'TSTypeParameterInstantiation' - ) { - // TODO: - // the original implementation assume Readonly - // 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, - ); + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType(annotation, types); + const typeAnnotation = topLevelType.type; + const arrayType = detectArrayType( + name, + typeAnnotation, + defaultValue, + types, + buildSchema, + ); + if (arrayType) { + return arrayType; } const type = @@ -433,140 +373,67 @@ function getTypeAnnotation( case 'Float': return { type: 'FloatTypeAnnotation', - default: withNullDefault - ? (defaultValue: number | null) - : ((defaultValue ? defaultValue : 0): number), + default: ((defaultValue === null + ? null + : defaultValue + ? defaultValue + : 0): number | null), }; case 'TSBooleanKeyword': return { type: 'BooleanTypeAnnotation', - default: withNullDefault - ? (defaultValue: boolean | null) - : ((defaultValue == null ? false : defaultValue): boolean), + default: defaultValue === null ? null : !!defaultValue, }; case 'TSStringKeyword': - if (typeof defaultValue !== 'undefined') { - return { - type: 'StringTypeAnnotation', - default: (defaultValue: string | null), - }; - } - throw new Error(`A default string (or null) is required for "${name}"`); + return { + type: 'StringTypeAnnotation', + default: ((defaultValue === undefined ? null : defaultValue): + | string + | null), + }; case 'Stringish': - if (typeof defaultValue !== 'undefined') { - return { - type: 'StringTypeAnnotation', - default: (defaultValue: string | null), - }; - } - throw new Error(`A default string (or null) is required for "${name}"`); - case 'TSUnionType': - typeAnnotation.types.reduce((lastType, currType) => { - const lastFlattenedType = - lastType && lastType.type === 'TSLiteralType' - ? lastType.literal.type - : lastType.type; - const currFlattenedType = - currType.type === 'TSLiteralType' - ? currType.literal.type - : currType.type; - - if (lastFlattenedType && currFlattenedType !== lastFlattenedType) { - throw new Error(`Mixed types are not supported (see "${name}")`); - } - return currType; - }); - - if (defaultValue === null) { - throw new Error(`A default enum value is required for "${name}"`); - } - - const unionType = typeAnnotation.types[0].type; - if ( - unionType === 'TSLiteralType' && - typeAnnotation.types[0].literal?.type === 'StringLiteral' - ) { - return { - type: 'StringEnumTypeAnnotation', - default: (defaultValue: string), - options: typeAnnotation.types.map(option => option.literal.value), - }; - } else if ( - unionType === 'TSLiteralType' && - typeAnnotation.types[0].literal?.type === 'NumericLiteral' - ) { - return { - type: 'Int32EnumTypeAnnotation', - default: (defaultValue: number), - options: typeAnnotation.types.map(option => option.literal.value), - }; - } else { - throw new Error( - `Unsupported union type for "${name}", received "${ - unionType === 'TSLiteralType' - ? typeAnnotation.types[0].literal?.type - : unionType - }"`, - ); - } + 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}"`); } } -function findProp( - name: string, - typeAnnotation: $FlowFixMe, - optionalType: boolean, -) { - switch (typeAnnotation.type) { - // Check for (T) - case 'TSParenthesizedType': - return findProp(name, typeAnnotation.typeAnnotation, optionalType); - - // Check for optional type in union e.g. T | null | undefined - case 'TSUnionType': - return findProp( - name, - typeAnnotation.types.filter( - t => t.type !== 'TSNullKeyword' && t.type !== 'TSUndefinedKeyword', - )[0], - optionalType || - typeAnnotation.types.some( - t => t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword', - ), - ); - - case 'TSTypeReference': - // Check against optional type inside `WithDefault` - if (typeAnnotation.typeName.name === 'WithDefault' && optionalType) { - throw new Error( - 'WithDefault<> is optional and does not need to be marked as optional. Please remove the union of undefined and/or null', - ); - } - // Remove unwanted types - if ( - typeAnnotation.typeName.name === 'DirectEventHandler' || - typeAnnotation.typeName.name === 'BubblingEventHandler' - ) { - return null; - } - if ( - name === 'style' && - typeAnnotation.type === 'GenericTypeAnnotation' && - typeAnnotation.typeName.name === 'ViewStyleProp' - ) { - return null; - } - return {typeAnnotation, optionalType}; - default: - return {typeAnnotation, optionalType}; +function isProp(name: string, typeAnnotation: $FlowFixMe) { + if (typeAnnotation.type === 'TSTypeReference') { + // Remove unwanted types + if ( + typeAnnotation.typeName.name === 'DirectEventHandler' || + typeAnnotation.typeName.name === 'BubblingEventHandler' + ) { + return false; + } + if ( + name === 'style' && + typeAnnotation.type === 'GenericTypeAnnotation' && + typeAnnotation.typeName.name === 'ViewStyleProp' + ) { + return false; + } } + return true; } type SchemaInfo = { @@ -574,98 +441,34 @@ type SchemaInfo = { optional: boolean, typeAnnotation: $FlowFixMe, defaultValue: $FlowFixMe, - withNullDefault: boolean, }; function getSchemaInfo( property: PropAST, types: TypeDeclarationMap, ): ?SchemaInfo { - const name = property.key.name; - - const value = getValueFromTypes( + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType( property.typeAnnotation.typeAnnotation, types, ); - const foundProp = findProp(name, value, false); - if (!foundProp) { + const name = property.key.name; + if (!isProp(name, topLevelType.type)) { return null; } - let {typeAnnotation, optionalType} = foundProp; - let optional = property.optional || optionalType; - // example: Readonly<{prop: string} | null | undefined>; - if ( - value.type === 'TSTypeReference' && - typeAnnotation.typeParameters?.params[0].type === 'TSUnionType' && - typeAnnotation.typeParameters?.params[0].types.some( - element => - element.type === 'TSNullKeyword' || - element.type === 'TSUndefinedKeyword', - ) - ) { - optional = true; - } - - if ( - !property.optional && - value.type === 'TSTypeReference' && - typeAnnotation.typeName.name === 'WithDefault' - ) { + if (!property.optional && topLevelType.defaultValue !== undefined) { throw new Error( `key ${name} must be optional if used with WithDefault<> annotation`, ); } - let type = typeAnnotation.type; - let defaultValue = null; - let withNullDefault = false; - if ( - type === 'TSTypeReference' && - typeAnnotation.typeName.name === 'WithDefault' - ) { - if (typeAnnotation.typeParameters.params.length === 1) { - throw new Error( - `WithDefault requires two parameters, did you forget to provide a default value for "${name}"?`, - ); - } - - let defaultValueType = typeAnnotation.typeParameters.params[1].type; - defaultValue = typeAnnotation.typeParameters.params[1].value; - - if (defaultValueType === 'TSLiteralType') { - defaultValueType = typeAnnotation.typeParameters.params[1].literal.type; - defaultValue = typeAnnotation.typeParameters.params[1].literal.value; - if ( - defaultValueType === 'UnaryExpression' && - typeAnnotation.typeParameters.params[1].literal.argument.type === - 'NumericLiteral' && - typeAnnotation.typeParameters.params[1].literal.operator === '-' - ) { - defaultValue = - -1 * typeAnnotation.typeParameters.params[1].literal.argument.value; - } - } - - if (defaultValueType === 'TSNullKeyword') { - defaultValue = null; - withNullDefault = true; - } - - typeAnnotation = typeAnnotation.typeParameters.params[0]; - type = - typeAnnotation.type === 'TSTypeReference' - ? typeAnnotation.typeName.name - : typeAnnotation.type; - } - return { name, - optional, - typeAnnotation, - defaultValue, - withNullDefault, + optional: property.optional || topLevelType.optional, + typeAnnotation: topLevelType.type, + defaultValue: topLevelType.defaultValue, }; } diff --git a/packages/react-native-codegen/src/parsers/typescript/components/events.js b/packages/react-native-codegen/src/parsers/typescript/components/events.js index a6256242865643..4ac105455570dc 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/events.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/events.js @@ -16,19 +16,21 @@ import type { EventTypeAnnotation, } from '../../../CodegenSchema.js'; const {flattenProperties} = require('./componentsUtils'); +const {parseTopLevelType} = require('../parseTopLevelType'); +import type {TypeDeclarationMap} from '../utils.js'; function getPropertyType( /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ name, - optional: boolean, + optionalProperty: boolean, /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ - typeAnnotation, + annotation, ): NamedShape { - if (typeAnnotation.type === 'TSParenthesizedType') { - return getPropertyType(name, optional, typeAnnotation.typeAnnotation); - } + const topLevelType = parseTopLevelType(annotation); + const typeAnnotation = topLevelType.type; + const optional = optionalProperty || topLevelType.optional; const type = typeAnnotation.type === 'TSTypeReference' ? typeAnnotation.typeName.name @@ -75,13 +77,6 @@ function getPropertyType( type: 'FloatTypeAnnotation', }, }; - case 'Readonly': - return getPropertyType( - name, - optional, - typeAnnotation.typeParameters.params[0], - ); - case 'TSTypeLiteral': return { name, @@ -93,20 +88,6 @@ function getPropertyType( }; case 'TSUnionType': - // Check for - if ( - typeAnnotation.types.some( - t => t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword', - ) - ) { - const optionalType = typeAnnotation.types.filter( - t => t.type !== 'TSNullKeyword' && t.type !== 'TSUndefinedKeyword', - )[0]; - - // Check for <(T | T2) | null | undefined> - return getPropertyType(name, true, optionalType); - } - return { name, optional, @@ -123,7 +104,7 @@ function getPropertyType( function findEventArgumentsAndType( typeAnnotation: $FlowFixMe, - types: TypeMap, + types: TypeDeclarationMap, bubblingType: void | 'direct' | 'bubble', paperName: ?$FlowFixMe, ) { @@ -216,49 +197,38 @@ function getEventArgument(argumentProps, name: $FlowFixMe) { }; } -function findEvent(typeAnnotation: $FlowFixMe, optional: boolean) { +function isEvent(typeAnnotation: $FlowFixMe) { switch (typeAnnotation.type) { - // Check for T | null | undefined - case 'TSUnionType': - return findEvent( - typeAnnotation.types.filter( - t => t.type !== 'TSNullKeyword' && t.type !== 'TSUndefinedKeyword', - )[0], - optional || - typeAnnotation.types.some( - t => t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword', - ), - ); - // Check for (T) - case 'TSParenthesizedType': - return findEvent(typeAnnotation.typeAnnotation, optional); case 'TSTypeReference': if ( typeAnnotation.typeName.name !== 'BubblingEventHandler' && typeAnnotation.typeName.name !== 'DirectEventHandler' ) { - return null; + return false; } else { - return {typeAnnotation, optional}; + return true; } default: - return null; + return false; } } function buildEventSchema( - types: TypeMap, + types: TypeDeclarationMap, property: EventTypeAST, ): ?EventTypeShape { - const name = property.key.name; - const foundEvent = findEvent( + // unpack WithDefault, (T) or T|U + const topLevelType = parseTopLevelType( property.typeAnnotation.typeAnnotation, - property.optional || false, + types, ); - if (!foundEvent) { + if (!isEvent(topLevelType.type)) { return null; } - const {typeAnnotation, optional} = foundEvent; + + const name = property.key.name; + const typeAnnotation = topLevelType.type; + const optional = property.optional || topLevelType.optional; const {argumentProps, bubblingType, paperTopLevelNameDeprecated} = findEventArgumentsAndType(typeAnnotation, types); @@ -299,15 +269,9 @@ function buildEventSchema( // $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser type EventTypeAST = Object; -type TypeMap = { - // $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser - [string]: Object, - ... -}; - function getEvents( eventTypeAST: $ReadOnlyArray, - types: TypeMap, + types: TypeDeclarationMap, ): $ReadOnlyArray { return eventTypeAST .filter(property => property.type === 'TSPropertySignature') diff --git a/packages/react-native-codegen/src/parsers/typescript/components/props.js b/packages/react-native-codegen/src/parsers/typescript/components/props.js index 4ad1d288f1cc15..a612296bc9fbef 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/props.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/props.js @@ -29,7 +29,7 @@ function buildPropSchema( if (info == null) { return null; } - const {name, optional, typeAnnotation, defaultValue, withNullDefault} = info; + const {name, optional, typeAnnotation, defaultValue} = info; return { name, optional, @@ -37,7 +37,6 @@ function buildPropSchema( name, typeAnnotation, defaultValue, - withNullDefault, types, buildPropSchema, ), diff --git a/packages/react-native-codegen/src/parsers/typescript/components/states.js b/packages/react-native-codegen/src/parsers/typescript/components/states.js index 60eeda52942f95..5b1b01551c4aa2 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/states.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/states.js @@ -29,7 +29,7 @@ function buildStateSchema( if (info == null) { return null; } - const {name, optional, typeAnnotation, defaultValue, withNullDefault} = info; + const {name, optional, typeAnnotation, defaultValue} = info; return { name, optional, @@ -37,7 +37,6 @@ function buildStateSchema( name, typeAnnotation, defaultValue, - withNullDefault, types, buildStateSchema, ), diff --git a/packages/react-native-codegen/src/parsers/typescript/modules/index.js b/packages/react-native-codegen/src/parsers/typescript/modules/index.js index d6b69f924a08b9..893d6e69520fd1 100644 --- a/packages/react-native-codegen/src/parsers/typescript/modules/index.js +++ b/packages/react-native-codegen/src/parsers/typescript/modules/index.js @@ -154,16 +154,6 @@ function translateTypeAnnotation( resolveTypeAnnotation(typeScriptTypeAnnotation, types); switch (typeAnnotation.type) { - case 'TSParenthesizedType': { - return translateTypeAnnotation( - hasteModuleName, - typeAnnotation.typeAnnotation, - types, - aliasMap, - tryParse, - cxxOnly, - ); - } case 'TSArrayType': { return translateArrayTypeAnnotation( hasteModuleName, @@ -231,25 +221,6 @@ function translateTypeAnnotation( nullable, ); } - case 'Readonly': { - assertGenericTypeAnnotationHasExactlyOneTypeParameter( - hasteModuleName, - typeAnnotation, - ); - - const [paramType, isParamNullable] = unwrapNullable( - translateTypeAnnotation( - hasteModuleName, - typeAnnotation.typeParameters.params[0], - types, - aliasMap, - tryParse, - cxxOnly, - ), - ); - - return wrapNullable(nullable || isParamNullable, paramType); - } case 'Stringish': { return wrapNullable(nullable, { type: 'StringTypeAnnotation', diff --git a/packages/react-native-codegen/src/parsers/typescript/parseTopLevelType.js b/packages/react-native-codegen/src/parsers/typescript/parseTopLevelType.js new file mode 100644 index 00000000000000..b51da605f0b991 --- /dev/null +++ b/packages/react-native-codegen/src/parsers/typescript/parseTopLevelType.js @@ -0,0 +1,189 @@ +/** + * 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-local + * @format + */ + +'use strict'; + +import type {TypeDeclarationMap} from './utils.js'; + +export type LegalDefaultValues = string | number | boolean | null; + +type TopLevelTypeInternal = { + unions: Array<$FlowFixMe>, + optional: boolean, + defaultValue?: LegalDefaultValues, +}; + +export type TopLevelType = { + type: $FlowFixMe, + optional: boolean, + defaultValue?: LegalDefaultValues, +}; + +function getValueFromTypes( + value: $FlowFixMe, + types: TypeDeclarationMap, +): $FlowFixMe { + 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; + } +} + +function isNull(t: $FlowFixMe) { + return t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword'; +} + +function isNullOrVoid(t: $FlowFixMe) { + return isNull(t) || t.type === 'TSVoidKeyword'; +} + +function couldBeNumericLiteral(type: string) { + return type === 'Literal' || type === 'NumericLiteral'; +} + +function couldBeSimpleLiteral(type: string) { + return ( + couldBeNumericLiteral(type) || + type === 'StringLiteral' || + type === 'BooleanLiteral' + ); +} + +function evaluateLiteral( + literalNode: $FlowFixMe, +): string | number | boolean | null { + const valueType = literalNode.type; + if (valueType === 'TSLiteralType') { + const literal = literalNode.literal; + if (couldBeSimpleLiteral(literal.type)) { + if ( + typeof literal.value === 'string' || + typeof literal.value === 'number' || + typeof literal.value === 'boolean' + ) { + return literal.value; + } + } else if ( + literal.type === 'UnaryExpression' && + literal.operator === '-' && + couldBeNumericLiteral(literal.argument.type) && + typeof literal.argument.value === 'number' + ) { + return -literal.argument.value; + } + } else if (isNull(literalNode)) { + return null; + } + + throw new Error( + 'The default value in WithDefault must be string, number, boolean or null .', + ); +} + +function handleUnionAndParen( + type: $FlowFixMe, + result: TopLevelTypeInternal, + knownTypes?: TypeDeclarationMap, +): void { + switch (type.type) { + case 'TSParenthesizedType': { + handleUnionAndParen(type.typeAnnotation, result, knownTypes); + break; + } + case 'TSUnionType': { + // the order is important + // result.optional must be set first + for (const t of type.types) { + if (isNullOrVoid(t)) { + result.optional = true; + } + } + for (const t of type.types) { + if (!isNullOrVoid(t)) { + handleUnionAndParen(t, result, knownTypes); + } + } + break; + } + case 'TSTypeReference': + if (type.typeName.name === 'Readonly') { + handleUnionAndParen(type.typeParameters.params[0], result, knownTypes); + } else if (type.typeName.name === 'WithDefault') { + if (result.optional) { + throw new Error( + 'WithDefault<> is optional and does not need to be marked as optional. Please remove the union of undefined and/or null', + ); + } + if (type.typeParameters.params.length !== 2) { + throw new Error( + 'WithDefault requires two parameters: type and default value.', + ); + } + if (result.defaultValue !== undefined) { + throw new Error( + 'Multiple WithDefault is not allowed nested or in a union type.', + ); + } + result.optional = true; + result.defaultValue = evaluateLiteral(type.typeParameters.params[1]); + handleUnionAndParen(type.typeParameters.params[0], result, knownTypes); + } else if (!knownTypes) { + result.unions.push(type); + } else { + const resolvedType = getValueFromTypes(type, knownTypes); + if ( + resolvedType.type === 'TSTypeReference' && + resolvedType.typeName.name === type.typeName.name + ) { + result.unions.push(type); + } else { + handleUnionAndParen(resolvedType, result, knownTypes); + } + } + break; + default: + result.unions.push(type); + } +} + +function parseTopLevelType( + type: $FlowFixMe, + knownTypes?: TypeDeclarationMap, +): TopLevelType { + let result: TopLevelTypeInternal = {unions: [], optional: false}; + handleUnionAndParen(type, result, knownTypes); + if (result.unions.length === 0) { + throw new Error('Union type could not be just null or undefined.'); + } else if (result.unions.length === 1) { + return { + type: result.unions[0], + optional: result.optional, + defaultValue: result.defaultValue, + }; + } else { + return { + type: {type: 'TSUnionType', types: result.unions}, + optional: result.optional, + defaultValue: result.defaultValue, + }; + } +} + +module.exports = { + parseTopLevelType, +}; diff --git a/packages/react-native-codegen/src/parsers/typescript/utils.js b/packages/react-native-codegen/src/parsers/typescript/utils.js index 51b1b8f950858a..11fb8a4d5b3f30 100644 --- a/packages/react-native-codegen/src/parsers/typescript/utils.js +++ b/packages/react-native-codegen/src/parsers/typescript/utils.js @@ -11,6 +11,7 @@ 'use strict'; const {ParserError} = require('./errors'); +const {parseTopLevelType} = require('./parseTopLevelType'); /** * TODO(T108222691): Use flow-types for @babel/parser @@ -82,18 +83,11 @@ function resolveTypeAnnotation( }; for (;;) { - // Check for optional type in union e.g. T | null | undefined - if ( - node.type === 'TSUnionType' && - node.types.some( - t => t.type === 'TSNullKeyword' || t.type === 'TSUndefinedKeyword', - ) - ) { - node = node.types.filter( - t => t.type !== 'TSNullKeyword' && t.type !== 'TSUndefinedKeyword', - )[0]; - nullable = true; - } else if (node.type === 'TSTypeReference') { + const topLevelType = parseTopLevelType(node); + nullable = nullable || topLevelType.optional; + node = topLevelType.type; + + if (node.type === 'TSTypeReference') { typeAliasResolutionStatus = { successful: true, aliasName: node.typeName.name, @@ -124,21 +118,6 @@ function resolveTypeAnnotation( }; } -function getValueFromTypes(value: ASTNode, types: TypeDeclarationMap): ASTNode { - 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; - } -} - export type ParserErrorCapturer = (fn: () => T) => ?T; function createParserErrorCapturer(): [ @@ -231,7 +210,6 @@ function isModuleRegistryCall(node: $FlowFixMe): boolean { } module.exports = { - getValueFromTypes, resolveTypeAnnotation, createParserErrorCapturer, getTypes,