From 4466afc84a3dad5659a57b242bf0ea3228aff2c2 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Sat, 14 Mar 2026 21:39:58 -0700 Subject: [PATCH 1/2] Add centralized ReservedPrimitiveTypes registry and shared toSafeIdentifier helper (#56049) Summary: Reduce duplication and inconsistency across codegen generators by centralizing reserved primitive type mappings into a single `ReservedPrimitiveTypes.js` registry, making future type support and fixes a single-source change and lowering bug risk. Additionally, standardize identifier capitalization via a shared `toSafeIdentifier` helper in `Utils.js` to prevent divergent string handling across C++/Java helpers. Also removes dead TODO comments and obsolete commented-out code from `RNCodegen.js`, `GenerateModuleH.js`, and parser files. Changelog: [Internal] Differential Revision: D95711348 --- .../src/generators/RNCodegen.js | 6 - .../src/generators/ReservedPrimitiveTypes.js | 155 ++++++++++++++++++ .../src/generators/Utils.js | 13 +- .../components/ComponentsGeneratorUtils.js | 93 +++-------- .../src/generators/components/CppHelpers.js | 23 +-- .../src/generators/components/JavaHelpers.js | 44 +---- .../src/generators/modules/GenerateModuleH.js | 1 - .../src/parsers/flow/parser.js | 10 +- .../src/parsers/typescript/parser.js | 4 +- 9 files changed, 201 insertions(+), 148 deletions(-) create mode 100644 packages/react-native-codegen/src/generators/ReservedPrimitiveTypes.js diff --git a/packages/react-native-codegen/src/generators/RNCodegen.js b/packages/react-native-codegen/src/generators/RNCodegen.js index 5e439abd8f48..face426d207a 100644 --- a/packages/react-native-codegen/src/generators/RNCodegen.js +++ b/packages/react-native-codegen/src/generators/RNCodegen.js @@ -10,12 +10,6 @@ 'use strict'; -/* -TODO: - -- ViewConfigs should spread in View's valid attributes -*/ - import type {SchemaType} from '../CodegenSchema'; const schemaValidator = require('../SchemaValidator.js'); diff --git a/packages/react-native-codegen/src/generators/ReservedPrimitiveTypes.js b/packages/react-native-codegen/src/generators/ReservedPrimitiveTypes.js new file mode 100644 index 000000000000..97e521d78f52 --- /dev/null +++ b/packages/react-native-codegen/src/generators/ReservedPrimitiveTypes.js @@ -0,0 +1,155 @@ +/** + * 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 + * @format + */ + +'use strict'; + +/** + * Single source of truth for the 6 reserved primitive types used in + * React Native component props. Each type maps to its per-language + * representation (C++ type name, C++ includes, Java imports). + * + * Previously these mappings were scattered across CppHelpers.js, + * ComponentsGeneratorUtils.js, and JavaHelpers.js. + */ + +export type ReservedPrimitiveName = + | 'ColorPrimitive' + | 'EdgeInsetsPrimitive' + | 'ImageRequestPrimitive' + | 'ImageSourcePrimitive' + | 'PointPrimitive' + | 'DimensionPrimitive'; + +type CppTypeInfo = { + +typeName: string, + +localIncludes: ReadonlyArray, + +conversionIncludes: ReadonlyArray, +}; + +type JavaImportInfo = { + +interfaceImports: ReadonlyArray, + +delegateImports: ReadonlyArray, +}; + +type ReservedTypeMapping = { + +cpp: CppTypeInfo, + +java: JavaImportInfo, +}; + +const RESERVED_TYPES: {+[ReservedPrimitiveName]: ReservedTypeMapping} = { + ColorPrimitive: { + cpp: { + typeName: 'SharedColor', + localIncludes: ['#include '], + conversionIncludes: [], + }, + java: { + interfaceImports: [], + delegateImports: ['import com.facebook.react.bridge.ColorPropConverter;'], + }, + }, + ImageSourcePrimitive: { + cpp: { + typeName: 'ImageSource', + localIncludes: ['#include '], + conversionIncludes: [ + '#include ', + ], + }, + java: { + interfaceImports: ['import com.facebook.react.bridge.ReadableMap;'], + delegateImports: ['import com.facebook.react.bridge.ReadableMap;'], + }, + }, + ImageRequestPrimitive: { + cpp: { + typeName: 'ImageRequest', + localIncludes: ['#include '], + conversionIncludes: [], + }, + java: { + // ImageRequestPrimitive is not used in Java component props + interfaceImports: [], + delegateImports: [], + }, + }, + PointPrimitive: { + cpp: { + typeName: 'Point', + localIncludes: ['#include '], + conversionIncludes: [], + }, + java: { + interfaceImports: ['import com.facebook.react.bridge.ReadableMap;'], + delegateImports: ['import com.facebook.react.bridge.ReadableMap;'], + }, + }, + EdgeInsetsPrimitive: { + cpp: { + typeName: 'EdgeInsets', + localIncludes: ['#include '], + conversionIncludes: [], + }, + java: { + interfaceImports: ['import com.facebook.react.bridge.ReadableMap;'], + delegateImports: ['import com.facebook.react.bridge.ReadableMap;'], + }, + }, + DimensionPrimitive: { + cpp: { + typeName: 'YGValue', + localIncludes: [ + '#include ', + '#include ', + ], + conversionIncludes: [ + '#include ', + ], + }, + java: { + interfaceImports: ['import com.facebook.yoga.YogaValue;'], + delegateImports: [ + 'import com.facebook.react.bridge.DimensionPropConverter;', + ], + }, + }, +}; + +function getCppTypeForReservedPrimitive(name: ReservedPrimitiveName): string { + return RESERVED_TYPES[name].cpp.typeName; +} + +function getCppLocalIncludesForReservedPrimitive( + name: ReservedPrimitiveName, +): ReadonlyArray { + return RESERVED_TYPES[name].cpp.localIncludes; +} + +function getCppConversionIncludesForReservedPrimitive( + name: ReservedPrimitiveName, +): ReadonlyArray { + return RESERVED_TYPES[name].cpp.conversionIncludes; +} + +function getJavaImportsForReservedPrimitive( + name: ReservedPrimitiveName, + type: 'interface' | 'delegate', +): ReadonlyArray { + const info = RESERVED_TYPES[name].java; + return type === 'interface' ? info.interfaceImports : info.delegateImports; +} + +module.exports = { + RESERVED_TYPES, + getCppTypeForReservedPrimitive, + getCppLocalIncludesForReservedPrimitive, + getCppConversionIncludesForReservedPrimitive, + getJavaImportsForReservedPrimitive, +}; diff --git a/packages/react-native-codegen/src/generators/Utils.js b/packages/react-native-codegen/src/generators/Utils.js index 7fcd65eeeb13..b503ae805ff1 100644 --- a/packages/react-native-codegen/src/generators/Utils.js +++ b/packages/react-native-codegen/src/generators/Utils.js @@ -34,11 +34,19 @@ function toPascalCase(inString: string): string { return inString; } - return inString[0].toUpperCase() + inString.slice(1); + return capitalize(inString); +} + +function toSafeIdentifier(input: string, shouldCapitalize: boolean): string { + const parts = input.split('-'); + if (!shouldCapitalize) { + return parts.join(''); + } + return parts.map(toPascalCase).join(''); } function toSafeCppString(input: string): string { - return input.split('-').map(toPascalCase).join(''); + return toSafeIdentifier(input, true); } function getEnumName(moduleName: string, origEnumName: string): string { @@ -105,6 +113,7 @@ module.exports = { indent, parseValidUnionType, toPascalCase, + toSafeIdentifier, toSafeCppString, getEnumName, HeterogeneousUnionError, diff --git a/packages/react-native-codegen/src/generators/components/ComponentsGeneratorUtils.js b/packages/react-native-codegen/src/generators/components/ComponentsGeneratorUtils.js index 20d01db68618..2e5422b84e75 100644 --- a/packages/react-native-codegen/src/generators/components/ComponentsGeneratorUtils.js +++ b/packages/react-native-codegen/src/generators/components/ComponentsGeneratorUtils.js @@ -21,6 +21,10 @@ import type { StringTypeAnnotation, } from '../../CodegenSchema'; +const { + getCppLocalIncludesForReservedPrimitive, + getCppTypeForReservedPrimitive, +} = require('../ReservedPrimitiveTypes'); const {getEnumName} = require('../Utils'); const { generateStructName, @@ -66,23 +70,7 @@ function getNativeTypeFromAnnotation( case 'FloatTypeAnnotation': return getCppTypeForAnnotation(typeAnnotation.type); case 'ReservedPropTypeAnnotation': - switch (typeAnnotation.name) { - case 'ColorPrimitive': - return 'SharedColor'; - case 'ImageSourcePrimitive': - return 'ImageSource'; - case 'ImageRequestPrimitive': - return 'ImageRequest'; - case 'PointPrimitive': - return 'Point'; - case 'EdgeInsetsPrimitive': - return 'EdgeInsets'; - case 'DimensionPrimitive': - return 'YGValue'; - default: - (typeAnnotation.name: empty); - throw new Error('Received unknown ReservedPropTypeAnnotation'); - } + return getCppTypeForReservedPrimitive(typeAnnotation.name); case 'ArrayTypeAnnotation': { const arrayType = typeAnnotation.elementType.type; if (arrayType === 'ArrayTypeAnnotation') { @@ -175,43 +163,25 @@ function convertVariableToPointer( return value; } -const convertCtorParamToAddressType = (type: string): string => { - const typesToConvert: Set = new Set(); - typesToConvert.add('ImageSource'); +// Configuration for C++ type conversions of reserved types. +// Centralizes the knowledge of which types need special pointer/address handling. +const CTOR_PARAM_ADDRESS_TYPES: Set = new Set(['ImageSource']); +const SHARED_POINTER_TYPES: Set = new Set(['ImageRequest']); - return convertTypesToConstAddressIfNeeded(type, typesToConvert); -}; +const convertCtorParamToAddressType = (type: string): string => + convertTypesToConstAddressIfNeeded(type, CTOR_PARAM_ADDRESS_TYPES); -const convertCtorInitToSharedPointers = ( - type: string, - value: string, -): string => { - const typesToConvert: Set = new Set(); - typesToConvert.add('ImageRequest'); +const convertCtorInitToSharedPointers = (type: string, value: string): string => + convertValueToSharedPointerWithMove(type, value, SHARED_POINTER_TYPES); - return convertValueToSharedPointerWithMove(type, value, typesToConvert); -}; +const convertGettersReturnTypeToAddressType = (type: string): string => + convertTypesToConstAddressIfNeeded(type, SHARED_POINTER_TYPES); -const convertGettersReturnTypeToAddressType = (type: string): string => { - const typesToConvert: Set = new Set(); - typesToConvert.add('ImageRequest'); +const convertVarTypeToSharedPointer = (type: string): string => + convertVariableToSharedPointer(type, SHARED_POINTER_TYPES); - return convertTypesToConstAddressIfNeeded(type, typesToConvert); -}; - -const convertVarTypeToSharedPointer = (type: string): string => { - const typesToConvert: Set = new Set(); - typesToConvert.add('ImageRequest'); - - return convertVariableToSharedPointer(type, typesToConvert); -}; - -const convertVarValueToPointer = (type: string, value: string): string => { - const typesToConvert: Set = new Set(); - typesToConvert.add('ImageRequest'); - - return convertVariableToPointer(type, value, typesToConvert); -}; +const convertVarValueToPointer = (type: string, value: string): string => + convertVariableToPointer(type, value, SHARED_POINTER_TYPES); function getLocalImports( properties: ReadonlyArray>, @@ -227,29 +197,8 @@ function getLocalImports( | 'ImageRequestPrimitive' | 'DimensionPrimitive', ) { - switch (name) { - case 'ColorPrimitive': - imports.add('#include '); - return; - case 'ImageSourcePrimitive': - imports.add('#include '); - return; - case 'ImageRequestPrimitive': - imports.add('#include '); - return; - case 'PointPrimitive': - imports.add('#include '); - return; - case 'EdgeInsetsPrimitive': - imports.add('#include '); - return; - case 'DimensionPrimitive': - imports.add('#include '); - imports.add('#include '); - return; - default: - (name: empty); - throw new Error(`Invalid ReservedPropTypeAnnotation name, got ${name}`); + for (const include of getCppLocalIncludesForReservedPrimitive(name)) { + imports.add(include); } } diff --git a/packages/react-native-codegen/src/generators/components/CppHelpers.js b/packages/react-native-codegen/src/generators/components/CppHelpers.js index e09977af1058..e344284252c6 100644 --- a/packages/react-native-codegen/src/generators/components/CppHelpers.js +++ b/packages/react-native-codegen/src/generators/components/CppHelpers.js @@ -15,6 +15,9 @@ import type { PropTypeAnnotation, } from '../../CodegenSchema'; +const { + getCppConversionIncludesForReservedPrimitive, +} = require('../ReservedPrimitiveTypes'); const {getEnumName, parseValidUnionType, toSafeCppString} = require('../Utils'); function toIntEnumValueName(propName: string, value: number): string { @@ -134,24 +137,8 @@ function getImports( | 'PointPrimitive' | 'DimensionPrimitive', ) { - switch (name) { - case 'ColorPrimitive': - return; - case 'PointPrimitive': - return; - case 'EdgeInsetsPrimitive': - return; - case 'ImageRequestPrimitive': - return; - case 'ImageSourcePrimitive': - imports.add('#include '); - return; - case 'DimensionPrimitive': - imports.add('#include '); - return; - default: - (name: empty); - throw new Error(`Invalid name, got ${name}`); + for (const include of getCppConversionIncludesForReservedPrimitive(name)) { + imports.add(include); } } diff --git a/packages/react-native-codegen/src/generators/components/JavaHelpers.js b/packages/react-native-codegen/src/generators/components/JavaHelpers.js index 40f7bf9a904d..ea6aae15eb21 100644 --- a/packages/react-native-codegen/src/generators/components/JavaHelpers.js +++ b/packages/react-native-codegen/src/generators/components/JavaHelpers.js @@ -12,9 +12,10 @@ import type {ComponentShape} from '../../CodegenSchema'; -function upperCaseFirst(inString: string): string { - return inString[0].toUpperCase() + inString.slice(1); -} +const { + getJavaImportsForReservedPrimitive, +} = require('../ReservedPrimitiveTypes'); +const {toSafeIdentifier} = require('../Utils'); function getInterfaceJavaClassName(componentName: string): string { return `${componentName.replace(/^RCT/, '')}ManagerInterface`; @@ -28,13 +29,7 @@ function toSafeJavaString( input: string, shouldUpperCaseFirst?: boolean, ): string { - const parts = input.split('-'); - - if (shouldUpperCaseFirst === false) { - return parts.join(''); - } - - return parts.map(upperCaseFirst).join(''); + return toSafeIdentifier(input, shouldUpperCaseFirst !== false); } function getImports( @@ -74,33 +69,8 @@ function getImports( | 'PointPrimitive' | 'DimensionPrimitive', ) { - switch (name) { - case 'ColorPrimitive': - if (type === 'delegate') { - imports.add('import com.facebook.react.bridge.ColorPropConverter;'); - } - return; - case 'ImageSourcePrimitive': - imports.add('import com.facebook.react.bridge.ReadableMap;'); - return; - case 'PointPrimitive': - imports.add('import com.facebook.react.bridge.ReadableMap;'); - return; - case 'EdgeInsetsPrimitive': - imports.add('import com.facebook.react.bridge.ReadableMap;'); - return; - case 'DimensionPrimitive': - if (type === 'delegate') { - imports.add( - 'import com.facebook.react.bridge.DimensionPropConverter;', - ); - } else { - imports.add('import com.facebook.yoga.YogaValue;'); - } - return; - default: - (name: empty); - throw new Error(`Invalid ReservedPropTypeAnnotation name, got ${name}`); + for (const javaImport of getJavaImportsForReservedPrimitive(name, type)) { + imports.add(javaImport); } } diff --git a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js index dddf253913ac..a2e16ff06e73 100644 --- a/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js +++ b/packages/react-native-codegen/src/generators/modules/GenerateModuleH.js @@ -68,7 +68,6 @@ function serializeArg( // param?: T if (optional && !nullable) { - // throw new Error('are we hitting this case? ' + moduleName); return `count <= ${index} || ${val}.isUndefined() ? std::nullopt : std::make_optional(${expression})`; } diff --git a/packages/react-native-codegen/src/parsers/flow/parser.js b/packages/react-native-codegen/src/parsers/flow/parser.js index ac2c351c9ad3..bf3c77b2ffe7 100644 --- a/packages/react-native-codegen/src/parsers/flow/parser.js +++ b/packages/react-native-codegen/src/parsers/flow/parser.js @@ -272,15 +272,7 @@ class FlowParser implements Parser { return types[typeAnnotation.typeParameters.params[0].id.name]; } - /** - * This FlowFixMe is supposed to refer to an InterfaceDeclaration or TypeAlias - * declaration type. Unfortunately, we don't have those types, because flow-parser - * generates them, and flow-parser is not type-safe. In the future, we should find - * a way to get these types from our flow parser library. - * - * TODO(T71778680): Flow type AST Nodes - */ - + // TODO(T71778680): Flow type AST Nodes getTypes(ast: $FlowFixMe): TypeDeclarationMap { return ast.body.reduce((types, node) => { if ( diff --git a/packages/react-native-codegen/src/parsers/typescript/parser.js b/packages/react-native-codegen/src/parsers/typescript/parser.js index b3f4d603b4d3..86c9bdfade58 100644 --- a/packages/react-native-codegen/src/parsers/typescript/parser.js +++ b/packages/react-native-codegen/src/parsers/typescript/parser.js @@ -298,9 +298,7 @@ class TypeScriptParser implements Parser { return types[typeAnnotation.typeParameters.params[0].typeName.name]; } - /** - * TODO(T108222691): Use flow-types for @babel/parser - */ + // TODO(T108222691): Use flow-types for @babel/parser getTypes(ast: $FlowFixMe): TypeDeclarationMap { return ast.body.reduce((types, node) => { switch (node.type) { From 9e35fae67c4120921d298a888e66e2112767c325 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Sat, 14 Mar 2026 21:39:58 -0700 Subject: [PATCH 2/2] Unify Flow and TypeScript event and command parsing into shared modules (#56052) Summary: Extract duplicated event and command type resolution logic from Flow and TypeScript parsers into shared modules (`events-commons.js` and `commands-commons.js`), eliminating ~300 lines of near-identical code. Both parsers now delegate to the same type resolution functions, which use the parser interface (`extractTypeFromTypeAnnotation` and `convertKeywordToTypeAnnotation`) to normalize language-specific AST differences. Also fixes `extractTypeFromTypeAnnotation` in the Flow parser to correctly handle `QualifiedTypeIdentifier` (e.g. `CodegenTypes.Int32`) and adds missing `TSUnionType` mapping in the TypeScript parser. Changelog: [Internal] Differential Revision: D95728706 --- .../parsers/components/commands-commons.js | 76 ++++++ .../src/parsers/components/events-commons.js | 217 ++++++++++++++++++ .../src/parsers/flow/components/commands.js | 70 +----- .../src/parsers/flow/components/events.js | 160 +------------ .../src/parsers/flow/parser.js | 2 +- .../parsers/typescript/components/commands.js | 45 +--- .../parsers/typescript/components/events.js | 139 +---------- .../src/parsers/typescript/parser.js | 4 +- 8 files changed, 320 insertions(+), 393 deletions(-) create mode 100644 packages/react-native-codegen/src/parsers/components/commands-commons.js create mode 100644 packages/react-native-codegen/src/parsers/components/events-commons.js diff --git a/packages/react-native-codegen/src/parsers/components/commands-commons.js b/packages/react-native-codegen/src/parsers/components/commands-commons.js new file mode 100644 index 000000000000..4290d9712003 --- /dev/null +++ b/packages/react-native-codegen/src/parsers/components/commands-commons.js @@ -0,0 +1,76 @@ +/** + * 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 + * @format + */ + +'use strict'; + +import type {ComponentCommandArrayTypeAnnotation} from '../../CodegenSchema.js'; +import type {Parser} from '../parser'; + +type Allowed = ComponentCommandArrayTypeAnnotation['elementType']; + +/** + * Shared command array element type resolution for Flow and TypeScript parsers. + * + * Uses parser.extractTypeFromTypeAnnotation() to resolve generic/reference types + * and parser.convertKeywordToTypeAnnotation() to normalize language-specific keywords + * to a common set of type names. + */ +function getCommandArrayElementTypeType( + inputType: unknown, + parser: Parser, +): Allowed { + // TODO: T172453752 support more complex type annotation for array element + if (inputType == null || typeof inputType !== 'object') { + throw new Error(`Expected an object, received ${typeof inputType}`); + } + + const rawType = inputType?.type; + if (typeof rawType !== 'string') { + throw new Error('Command array element type must be a string'); + } + + // $FlowFixMe[incompatible-call] + const resolvedName = parser.extractTypeFromTypeAnnotation(inputType); + const normalizedType = parser.convertKeywordToTypeAnnotation(resolvedName); + + switch (normalizedType) { + case 'BooleanTypeAnnotation': + return { + type: 'BooleanTypeAnnotation', + }; + case 'StringTypeAnnotation': + return { + type: 'StringTypeAnnotation', + }; + case 'Int32': + return { + type: 'Int32TypeAnnotation', + }; + case 'Float': + return { + type: 'FloatTypeAnnotation', + }; + case 'Double': + return { + type: 'DoubleTypeAnnotation', + }; + default: + // Unresolvable types (aliases to objects/unions) fall back to + // MixedTypeAnnotation. Generators produce ReadableMap or + // (const NSArray *) which are untyped. + return { + type: 'MixedTypeAnnotation', + }; + } +} + +module.exports = { + getCommandArrayElementTypeType, +}; diff --git a/packages/react-native-codegen/src/parsers/components/events-commons.js b/packages/react-native-codegen/src/parsers/components/events-commons.js new file mode 100644 index 000000000000..7a06ee4652c0 --- /dev/null +++ b/packages/react-native-codegen/src/parsers/components/events-commons.js @@ -0,0 +1,217 @@ +/** + * 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 + * @format + */ + +'use strict'; + +import type {EventTypeAnnotation, NamedShape} from '../../CodegenSchema.js'; +import type {Parser} from '../parser'; + +const {buildPropertiesForEvent} = require('../parsers-commons'); +const { + emitBoolProp, + emitDoubleProp, + emitFloatProp, + emitInt32Prop, + emitMixedProp, + emitObjectProp, + emitStringProp, + emitUnionProp, +} = require('../parsers-primitives'); + +/** + * Shared event property type resolution for Flow and TypeScript parsers. + * + * Both parsers resolve type annotations to normalized names using: + * - parser.extractTypeFromTypeAnnotation() to resolve generic/reference types + * - parser.convertKeywordToTypeAnnotation() to normalize language-specific keywords + * + * Flow-specific types ($ReadOnly, $ReadOnlyArray) are handled inline since + * TypeScript's parseTopLevelType() resolves them before this function is called. + */ +// Check if a type annotation represents null, undefined, or void. +// Covers both Flow (NullLiteralTypeAnnotation, VoidTypeAnnotation) and +// TypeScript (TSNullKeyword, TSUndefinedKeyword, TSVoidKeyword) AST nodes. +function isNullOrVoidType(typeAnnotation: $FlowFixMe): boolean { + return ( + typeAnnotation.type === 'NullLiteralTypeAnnotation' || + typeAnnotation.type === 'VoidTypeAnnotation' || + typeAnnotation.type === 'TSNullKeyword' || + typeAnnotation.type === 'TSUndefinedKeyword' || + typeAnnotation.type === 'TSVoidKeyword' + ); +} + +function getPropertyType( + name: string, + optional: boolean, + typeAnnotation: $FlowFixMe, + parser: Parser, +): NamedShape { + const resolvedType = parser.extractTypeFromTypeAnnotation(typeAnnotation); + const type = parser.convertKeywordToTypeAnnotation(resolvedType); + + // Handle Flow's read-only wrappers (no-op for TS which pre-resolves these) + if (resolvedType === '$ReadOnly' || resolvedType === 'Readonly') { + return getPropertyType( + name, + optional, + typeAnnotation.typeParameters.params[0], + parser, + ); + } + + if (resolvedType === '$ReadOnlyArray' || resolvedType === 'ReadonlyArray') { + return { + name, + optional, + typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), + }; + } + + // For nullable unions (e.g. 'small' | 'large' | null | undefined), + // strip null/undefined/void types and unwrap single-type unions. + // TypeScript's parseTopLevelType handles this at the top level, but + // nested event properties also need this unwrapping. + if (type === 'UnionTypeAnnotation') { + const nonNullableTypes = typeAnnotation.types.filter( + (t: $FlowFixMe) => !isNullOrVoidType(t), + ); + if (nonNullableTypes.length < typeAnnotation.types.length) { + // Had nullable types - unwrap + if (nonNullableTypes.length === 1) { + return getPropertyType(name, true, nonNullableTypes[0], parser); + } + return emitUnionProp(name, true, parser, { + ...typeAnnotation, + types: nonNullableTypes, + }); + } + } + + switch (type) { + case 'BooleanTypeAnnotation': + return emitBoolProp(name, optional); + case 'StringTypeAnnotation': + return emitStringProp(name, optional); + case 'Int32': + return emitInt32Prop(name, optional); + case 'Double': + return emitDoubleProp(name, optional); + case 'Float': + return emitFloatProp(name, optional); + case 'ObjectTypeAnnotation': + return emitObjectProp( + name, + optional, + parser, + typeAnnotation, + extractArrayElementType, + ); + case 'UnionTypeAnnotation': + return emitUnionProp(name, optional, parser, typeAnnotation); + case 'UnsafeMixed': + return emitMixedProp(name, optional); + case 'ArrayTypeAnnotation': + return { + name, + optional, + typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), + }; + default: + throw new Error(`Unable to determine event type for "${name}": ${type}`); + } +} + +function extractArrayElementType( + typeAnnotation: $FlowFixMe, + name: string, + parser: Parser, +): EventTypeAnnotation { + const resolvedType = parser.extractTypeFromTypeAnnotation(typeAnnotation); + const type = parser.convertKeywordToTypeAnnotation(resolvedType); + + // Handle TS parenthesized types (no-op for Flow) + if (typeAnnotation.type === 'TSParenthesizedType') { + return extractArrayElementType(typeAnnotation.typeAnnotation, name, parser); + } + + // Handle Flow's read-only arrays (no-op for TS which pre-resolves these) + if (resolvedType === '$ReadOnlyArray' || resolvedType === 'ReadonlyArray') { + const genericParams = typeAnnotation.typeParameters.params; + if (genericParams.length !== 1) { + throw new Error( + `Events only supports arrays with 1 Generic type. Found ${ + genericParams.length + } types:\n${JSON.stringify(genericParams, null, 2)}`, + ); + } + return { + type: 'ArrayTypeAnnotation', + elementType: extractArrayElementType(genericParams[0], name, parser), + }; + } + + switch (type) { + case 'BooleanTypeAnnotation': + return {type: 'BooleanTypeAnnotation'}; + case 'StringTypeAnnotation': + return {type: 'StringTypeAnnotation'}; + case 'Int32': + return {type: 'Int32TypeAnnotation'}; + case 'Float': + return {type: 'FloatTypeAnnotation'}; + case 'NumberTypeAnnotation': + case 'Double': + return { + type: 'DoubleTypeAnnotation', + }; + case 'UnionTypeAnnotation': + return { + type: 'UnionTypeAnnotation', + types: typeAnnotation.types.map(option => ({ + type: 'StringLiteralTypeAnnotation', + value: parser.getLiteralValue(option), + })), + }; + case 'UnsafeMixed': + return {type: 'MixedTypeAnnotation'}; + case 'ObjectTypeAnnotation': + return { + type: 'ObjectTypeAnnotation', + properties: parser + .getObjectProperties(typeAnnotation) + .map(member => + buildPropertiesForEvent(member, parser, getPropertyType), + ), + }; + case 'ArrayTypeAnnotation': + return { + type: 'ArrayTypeAnnotation', + elementType: extractArrayElementType( + typeAnnotation.elementType, + name, + parser, + ), + }; + default: + throw new Error( + `Unrecognized ${type} for Array ${name} in events.\n${JSON.stringify( + typeAnnotation, + null, + 2, + )}`, + ); + } +} + +module.exports = { + getPropertyType, + extractArrayElementType, +}; diff --git a/packages/react-native-codegen/src/parsers/flow/components/commands.js b/packages/react-native-codegen/src/parsers/flow/components/commands.js index 7874ab8534a3..08be5863b512 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/commands.js +++ b/packages/react-native-codegen/src/parsers/flow/components/commands.js @@ -13,12 +13,14 @@ import type { CommandParamTypeAnnotation, CommandTypeAnnotation, - ComponentCommandArrayTypeAnnotation, NamedShape, } from '../../../CodegenSchema.js'; import type {Parser} from '../../parser'; import type {TypeDeclarationMap} from '../../utils'; +const { + getCommandArrayElementTypeType, +} = require('../../components/commands-commons'); const {getValueFromTypes} = require('../utils.js'); // $FlowFixMe[unclear-type] there's no flowtype for ASTs @@ -157,72 +159,6 @@ function buildCommandSchema( }; } -type Allowed = ComponentCommandArrayTypeAnnotation['elementType']; - -function getCommandArrayElementTypeType( - inputType: unknown, - parser: Parser, -): Allowed { - // TODO: T172453752 support more complex type annotation for array element - if (typeof inputType !== 'object') { - throw new Error('Expected an object'); - } - - const type = inputType?.type; - - if (inputType == null || typeof type !== 'string') { - throw new Error('Command array element type must be a string'); - } - - switch (type) { - case 'BooleanTypeAnnotation': - return { - type: 'BooleanTypeAnnotation', - }; - case 'StringTypeAnnotation': - return { - type: 'StringTypeAnnotation', - }; - case 'GenericTypeAnnotation': - const name = - typeof inputType.id === 'object' - ? parser.getTypeAnnotationName(inputType) - : null; - - if (typeof name !== 'string') { - throw new Error( - 'Expected GenericTypeAnnotation AST name to be a string', - ); - } - - switch (name) { - case 'Int32': - return { - type: 'Int32TypeAnnotation', - }; - case 'Float': - return { - type: 'FloatTypeAnnotation', - }; - case 'Double': - return { - type: 'DoubleTypeAnnotation', - }; - default: - // This is not a great solution. This generally means its a type alias to another type - // like an object or union. Ideally we'd encode that in the schema so the compat-check can - // validate those deeper objects for breaking changes and the generators can do something smarter. - // As of now, the generators just create ReadableMap or (const NSArray *) which are untyped - return { - type: 'MixedTypeAnnotation', - }; - } - - default: - throw new Error(`Unsupported array element type ${type}`); - } -} - function getCommands( commandTypeAST: ReadonlyArray, types: TypeDeclarationMap, diff --git a/packages/react-native-codegen/src/parsers/flow/components/events.js b/packages/react-native-codegen/src/parsers/flow/components/events.js index 1c6d05fe282c..191264f740d8 100644 --- a/packages/react-native-codegen/src/parsers/flow/components/events.js +++ b/packages/react-native-codegen/src/parsers/flow/components/events.js @@ -10,174 +10,24 @@ 'use strict'; -import type { - EventTypeAnnotation, - EventTypeShape, - NamedShape, -} from '../../../CodegenSchema.js'; +import type {EventTypeShape} from '../../../CodegenSchema.js'; import type {Parser} from '../../parser'; import type {EventArgumentReturnType} from '../../parsers-commons'; +const { + extractArrayElementType, + getPropertyType, +} = require('../../components/events-commons'); const { throwIfArgumentPropsAreNull, throwIfBubblingTypeIsNull, throwIfEventHasNoName, } = require('../../error-utils'); const { - buildPropertiesForEvent, emitBuildEventSchema, getEventArgument, handleEventHandler, } = require('../../parsers-commons'); -const { - emitBoolProp, - emitDoubleProp, - emitFloatProp, - emitInt32Prop, - emitMixedProp, - emitObjectProp, - emitStringProp, - emitUnionProp, -} = require('../../parsers-primitives'); - -function getPropertyType( - /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's - * LTI update could not be added via codemod */ - name: string, - optional: boolean, - typeAnnotation: $FlowFixMe, - parser: Parser, -): NamedShape { - const type = extractTypeFromTypeAnnotation(typeAnnotation, parser); - - switch (type) { - case 'BooleanTypeAnnotation': - return emitBoolProp(name, optional); - case 'StringTypeAnnotation': - return emitStringProp(name, optional); - case 'Int32': - return emitInt32Prop(name, optional); - case 'Double': - return emitDoubleProp(name, optional); - case 'Float': - return emitFloatProp(name, optional); - case '$ReadOnly': - case 'Readonly': - return getPropertyType( - name, - optional, - typeAnnotation.typeParameters.params[0], - parser, - ); - case 'ObjectTypeAnnotation': - return emitObjectProp( - name, - optional, - parser, - typeAnnotation, - extractArrayElementType, - ); - case 'UnionTypeAnnotation': - return emitUnionProp(name, optional, parser, typeAnnotation); - case 'UnsafeMixed': - return emitMixedProp(name, optional); - case 'ArrayTypeAnnotation': - case '$ReadOnlyArray': - case 'ReadonlyArray': - return { - name, - optional, - typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), - }; - default: - throw new Error(`Unable to determine event type for "${name}": ${type}`); - } -} - -function extractArrayElementType( - typeAnnotation: $FlowFixMe, - name: string, - parser: Parser, -): EventTypeAnnotation { - const type = extractTypeFromTypeAnnotation(typeAnnotation, parser); - - switch (type) { - case 'BooleanTypeAnnotation': - return {type: 'BooleanTypeAnnotation'}; - case 'StringTypeAnnotation': - return {type: 'StringTypeAnnotation'}; - case 'Int32': - return {type: 'Int32TypeAnnotation'}; - case 'Float': - return {type: 'FloatTypeAnnotation'}; - case 'NumberTypeAnnotation': - case 'Double': - return { - type: 'DoubleTypeAnnotation', - }; - case 'UnionTypeAnnotation': - return { - type: 'UnionTypeAnnotation', - types: typeAnnotation.types.map(option => ({ - type: 'StringLiteralTypeAnnotation', - value: parser.getLiteralValue(option), - })), - }; - case 'UnsafeMixed': - return {type: 'MixedTypeAnnotation'}; - case 'ObjectTypeAnnotation': - return { - type: 'ObjectTypeAnnotation', - properties: parser - .getObjectProperties(typeAnnotation) - .map(member => - buildPropertiesForEvent(member, parser, getPropertyType), - ), - }; - case 'ArrayTypeAnnotation': - return { - type: 'ArrayTypeAnnotation', - elementType: extractArrayElementType( - typeAnnotation.elementType, - name, - parser, - ), - }; - case '$ReadOnlyArray': - case 'ReadonlyArray': - const genericParams = typeAnnotation.typeParameters.params; - if (genericParams.length !== 1) { - throw new Error( - `Events only supports arrays with 1 Generic type. Found ${ - genericParams.length - } types:\n${prettify(genericParams)}`, - ); - } - return { - type: 'ArrayTypeAnnotation', - elementType: extractArrayElementType(genericParams[0], name, parser), - }; - default: - throw new Error( - `Unrecognized ${type} for Array ${name} in events.\n${prettify( - typeAnnotation, - )}`, - ); - } -} - -function prettify(jsonObject: $FlowFixMe): string { - return JSON.stringify(jsonObject, null, 2); -} - -function extractTypeFromTypeAnnotation( - typeAnnotation: $FlowFixMe, - parser: Parser, -): string { - return typeAnnotation.type === 'GenericTypeAnnotation' - ? parser.getTypeAnnotationName(typeAnnotation) - : typeAnnotation.type; -} function findEventArgumentsAndType( parser: Parser, diff --git a/packages/react-native-codegen/src/parsers/flow/parser.js b/packages/react-native-codegen/src/parsers/flow/parser.js index bf3c77b2ffe7..ed2ce48d79b4 100644 --- a/packages/react-native-codegen/src/parsers/flow/parser.js +++ b/packages/react-native-codegen/src/parsers/flow/parser.js @@ -548,7 +548,7 @@ class FlowParser implements Parser { extractTypeFromTypeAnnotation(typeAnnotation: $FlowFixMe): string { return typeAnnotation.type === 'GenericTypeAnnotation' - ? typeAnnotation.id.name + ? this.getTypeAnnotationName(typeAnnotation) : typeAnnotation.type; } 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 dc35f3304bb4..a726f0ad6536 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/commands.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/commands.js @@ -13,12 +13,14 @@ import type { CommandParamTypeAnnotation, CommandTypeAnnotation, - ComponentCommandArrayTypeAnnotation, NamedShape, } from '../../../CodegenSchema.js'; import type {Parser} from '../../parser'; import type {TypeDeclarationMap} from '../../utils'; +const { + getCommandArrayElementTypeType, +} = require('../../components/commands-commons'); const {parseTopLevelType} = require('../parseTopLevelType'); const {getPrimitiveTypeAnnotation} = require('./componentsUtils'); @@ -128,47 +130,6 @@ function buildCommandSchemaInternal( }; } -function getCommandArrayElementTypeType( - inputType: unknown, - parser: Parser, -): ComponentCommandArrayTypeAnnotation['elementType'] { - // TODO: T172453752 support more complex type annotation for array element - - if (inputType == null || typeof inputType !== 'object') { - throw new Error(`Expected an object, received ${typeof inputType}`); - } - - const type = inputType.type; - if (typeof type !== 'string') { - throw new Error('Command array element type must be a string'); - } - - // This is not a great solution. This generally means its a type alias to another type - // like an object or union. Ideally we'd encode that in the schema so the compat-check can - // validate those deeper objects for breaking changes and the generators can do something smarter. - // As of now, the generators just create ReadableMap or (const NSArray *) which are untyped - if (type === 'TSTypeReference') { - const name = - typeof inputType.typeName === 'object' - ? parser.getTypeAnnotationName(inputType) - : null; - - if (typeof name !== 'string') { - throw new Error('Expected TSTypeReference AST name to be a string'); - } - - try { - return getPrimitiveTypeAnnotation(name); - } catch (e) { - return { - type: 'MixedTypeAnnotation', - }; - } - } - - return getPrimitiveTypeAnnotation(type); -} - function buildCommandSchema( property: EventTypeAST, types: TypeDeclarationMap, 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 faa4bc11c0ae..a57864b6e90f 100644 --- a/packages/react-native-codegen/src/parsers/typescript/components/events.js +++ b/packages/react-native-codegen/src/parsers/typescript/components/events.js @@ -18,157 +18,42 @@ import type { import type {Parser} from '../../parser'; import type {TypeDeclarationMap} from '../../utils'; +const { + extractArrayElementType, + getPropertyType: getPropertyTypeCommon, +} = require('../../components/events-commons'); const { throwIfArgumentPropsAreNull, throwIfBubblingTypeIsNull, throwIfEventHasNoName, } = require('../../error-utils'); const { - buildPropertiesForEvent, emitBuildEventSchema, getEventArgument, handleEventHandler, } = require('../../parsers-commons'); -const { - emitBoolProp, - emitDoubleProp, - emitFloatProp, - emitInt32Prop, - emitMixedProp, - emitObjectProp, - emitStringProp, - emitUnionProp, -} = require('../../parsers-primitives'); const {parseTopLevelType} = require('../parseTopLevelType'); const {flattenProperties} = require('./componentsUtils'); +/** + * TypeScript wrapper around the shared getPropertyType that applies + * parseTopLevelType to unwrap Readonly, WithDefault, and nullable unions + * before delegating to the shared type resolution logic. + */ function getPropertyType( /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ - name, + name: string, optionalProperty: boolean, /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's * LTI update could not be added via codemod */ - annotation, + annotation: $FlowFixMe, parser: Parser, ): NamedShape { const topLevelType = parseTopLevelType(annotation, parser); const typeAnnotation = topLevelType.type; const optional = optionalProperty || topLevelType.optional; - const type = - typeAnnotation.type === 'TSTypeReference' - ? parser.getTypeAnnotationName(typeAnnotation) - : typeAnnotation.type; - - switch (type) { - case 'TSBooleanKeyword': - return emitBoolProp(name, optional); - case 'TSStringKeyword': - return emitStringProp(name, optional); - case 'Int32': - return emitInt32Prop(name, optional); - case 'Double': - return emitDoubleProp(name, optional); - case 'Float': - return emitFloatProp(name, optional); - case 'TSTypeLiteral': - return emitObjectProp( - name, - optional, - parser, - typeAnnotation, - extractArrayElementType, - ); - case 'TSUnionType': - return emitUnionProp(name, optional, parser, typeAnnotation); - case 'UnsafeMixed': - return emitMixedProp(name, optional); - case 'TSArrayType': - return { - name, - optional, - typeAnnotation: extractArrayElementType(typeAnnotation, name, parser), - }; - default: - throw new Error(`Unable to determine event type for "${name}": ${type}`); - } -} - -function extractArrayElementType( - typeAnnotation: $FlowFixMe, - name: string, - parser: Parser, -): EventTypeAnnotation { - const type = extractTypeFromTypeAnnotation(typeAnnotation, parser); - - switch (type) { - case 'TSParenthesizedType': - return extractArrayElementType( - typeAnnotation.typeAnnotation, - name, - parser, - ); - case 'TSBooleanKeyword': - return {type: 'BooleanTypeAnnotation'}; - case 'TSStringKeyword': - return {type: 'StringTypeAnnotation'}; - case 'Float': - return { - type: 'FloatTypeAnnotation', - }; - case 'Int32': - return { - type: 'Int32TypeAnnotation', - }; - case 'TSNumberKeyword': - case 'Double': - return { - type: 'DoubleTypeAnnotation', - }; - case 'TSUnionType': - return { - type: 'UnionTypeAnnotation', - types: typeAnnotation.types.map(option => ({ - type: 'StringLiteralTypeAnnotation', - value: parser.getLiteralValue(option), - })), - }; - case 'TSTypeLiteral': - return { - type: 'ObjectTypeAnnotation', - properties: parser - .getObjectProperties(typeAnnotation) - .map(member => - buildPropertiesForEvent(member, parser, getPropertyType), - ), - }; - case 'TSArrayType': - return { - type: 'ArrayTypeAnnotation', - elementType: extractArrayElementType( - typeAnnotation.elementType, - name, - parser, - ), - }; - default: - throw new Error( - `Unrecognized ${type} for Array ${name} in events.\n${JSON.stringify( - typeAnnotation, - null, - 2, - )}`, - ); - } -} - -function extractTypeFromTypeAnnotation( - typeAnnotation: $FlowFixMe, - parser: Parser, -): string { - return typeAnnotation.type === 'TSTypeReference' - ? parser.getTypeAnnotationName(typeAnnotation) - : typeAnnotation.type; + return getPropertyTypeCommon(name, optional, typeAnnotation, parser); } function findEventArgumentsAndType( diff --git a/packages/react-native-codegen/src/parsers/typescript/parser.js b/packages/react-native-codegen/src/parsers/typescript/parser.js index 86c9bdfade58..4478d0f4b31b 100644 --- a/packages/react-native-codegen/src/parsers/typescript/parser.js +++ b/packages/react-native-codegen/src/parsers/typescript/parser.js @@ -401,6 +401,8 @@ class TypeScriptParser implements Parser { return 'StringTypeAnnotation'; case 'TSTypeLiteral': return 'ObjectTypeAnnotation'; + case 'TSUnionType': + return 'UnionTypeAnnotation'; case 'TSUnknownKeyword': return 'MixedTypeAnnotation'; } @@ -603,7 +605,7 @@ class TypeScriptParser implements Parser { extractTypeFromTypeAnnotation(typeAnnotation: $FlowFixMe): string { return typeAnnotation.type === 'TSTypeReference' - ? typeAnnotation.typeName.name + ? this.getTypeAnnotationName(typeAnnotation) : typeAnnotation.type; }