Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,
};
217 changes: 217 additions & 0 deletions packages/react-native-codegen/src/parsers/components/events-commons.js
Original file line number Diff line number Diff line change
@@ -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<EventTypeAnnotation> {
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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<EventTypeAST>,
types: TypeDeclarationMap,
Expand Down
Loading
Loading