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,