Skip to content

Commit

Permalink
TypeScript Components
Browse files Browse the repository at this point in the history
Summary:
This is the logic for parsing Native Components. The files were copied from the flow parser and updated for TypeScript specific types and differences in the shape of the AST. The logic and code path is almost identical to the flow parser.

While there is considerable duplication to the flow parser, I decided there are enough subtle differences to warrant keeping this logic separate.

Changelog:
[General][Add] - Add WIP TypeScript support for Native Component Codegen spec parsing

Reviewed By: RSNara

Differential Revision: D33080623

fbshipit-source-id: a68c8d4c4570e65a88a97dcea3cd18a6976c53c7
  • Loading branch information
charlesbdudley authored and facebook-github-bot committed Dec 20, 2021
1 parent c532fcf commit 7615bde
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 154 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,37 @@ type EventTypeAST = Object;

function buildCommandSchema(property: EventTypeAST, types: TypeDeclarationMap) {
const name = property.key.name;
const optional = property.optional;
const value = getValueFromTypes(property.value, types);
const optional = property.optional || false;
const value = getValueFromTypes(
property.typeAnnotation.typeAnnotation,
types,
);

const firstParam = value.params[0].typeAnnotation;
const firstParam = value.parameters[0].typeAnnotation;

if (
!(
firstParam.id != null &&
firstParam.id.type === 'QualifiedTypeIdentifier' &&
firstParam.id.qualification.name === 'React' &&
firstParam.id.id.name === 'ElementRef'
firstParam.typeAnnotation != null &&
firstParam.typeAnnotation.type === 'TSTypeReference' &&
firstParam.typeAnnotation.typeName.left?.name === 'React' &&
firstParam.typeAnnotation.typeName.right?.name === 'ElementRef'
)
) {
throw new Error(
`The first argument of method ${name} must be of type React.ElementRef<>`,
);
}

const params = value.params.slice(1).map(param => {
const paramName = param.name.name;
const paramValue = getValueFromTypes(param.typeAnnotation, types);
const params = value.parameters.slice(1).map(param => {
const paramName = param.name;
const paramValue = getValueFromTypes(
param.typeAnnotation.typeAnnotation,
types,
);

const type =
paramValue.type === 'GenericTypeAnnotation'
? paramValue.id.name
paramValue.type === 'TSTypeReference'
? paramValue.typeName.name
: paramValue.type;
let returnType;

Expand All @@ -56,7 +63,7 @@ function buildCommandSchema(property: EventTypeAST, types: TypeDeclarationMap) {
name: 'RootTag',
};
break;
case 'BooleanTypeAnnotation':
case 'TSBooleanKeyword':
returnType = {
type: 'BooleanTypeAnnotation',
};
Expand All @@ -76,7 +83,7 @@ function buildCommandSchema(property: EventTypeAST, types: TypeDeclarationMap) {
type: 'FloatTypeAnnotation',
};
break;
case 'StringTypeAnnotation':
case 'TSStringKeyword':
returnType = {
type: 'StringTypeAnnotation',
};
Expand Down Expand Up @@ -112,7 +119,7 @@ function getCommands(
types: TypeDeclarationMap,
): $ReadOnlyArray<NamedShape<CommandTypeAnnotation>> {
return commandTypeAST
.filter(property => property.type === 'ObjectTypeProperty')
.filter(property => property.type === 'TSPropertySignature')
.map(property => buildCommandSchema(property, types))
.filter(Boolean);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ function getPropertyType(
typeAnnotation,
): NamedShape<EventTypeAnnotation> {
const type =
typeAnnotation.type === 'GenericTypeAnnotation'
? typeAnnotation.id.name
typeAnnotation.type === 'TSTypeReference'
? typeAnnotation.typeName.name
: typeAnnotation.type;

switch (type) {
case 'BooleanTypeAnnotation':
case 'TSBooleanKeyword':
return {
name,
optional,
typeAnnotation: {
type: 'BooleanTypeAnnotation',
},
};
case 'StringTypeAnnotation':
case 'TSStringKeyword':
return {
name,
optional,
Expand Down Expand Up @@ -67,28 +67,48 @@ function getPropertyType(
type: 'FloatTypeAnnotation',
},
};
case '$ReadOnly':
case 'Readonly':
return getPropertyType(
name,
optional,
typeAnnotation.typeParameters.params[0],
);
case 'ObjectTypeAnnotation':

case 'TSTypeLiteral':
return {
name,
optional,
typeAnnotation: {
type: 'ObjectTypeAnnotation',
properties: typeAnnotation.properties.map(buildPropertiesForEvent),
properties: typeAnnotation.members.map(buildPropertiesForEvent),
},
};
case 'UnionTypeAnnotation':

case 'TSUnionType':
// Check for <T | null | void>
if (
typeAnnotation.types.some(
t => t.type === 'TSNullKeyword' || t.type === 'TSVoidKeyword',
)
) {
const optionalType = typeAnnotation.types.filter(
t => t.type !== 'TSNullKeyword' && t.type !== 'TSVoidKeyword',
)[0];

// Check for <(T | T2) | null | void>
if (optionalType.type === 'TSParenthesizedType') {
return getPropertyType(name, true, optionalType.typeAnnotation);
}

return getPropertyType(name, true, optionalType);
}

return {
name,
optional,
typeAnnotation: {
type: 'StringEnumTypeAnnotation',
options: typeAnnotation.types.map(option => option.value),
options: typeAnnotation.types.map(option => option.literal.value),
},
};
default:
Expand All @@ -103,26 +123,24 @@ function findEventArgumentsAndType(
bubblingType,
paperName,
) {
if (!typeAnnotation.id) {
if (!typeAnnotation.typeName) {
throw new Error("typeAnnotation of event doesn't have a name");
}
const name = typeAnnotation.id.name;
if (name === '$ReadOnly') {
const name = typeAnnotation.typeName.name;
if (name === 'Readonly') {
return {
argumentProps: typeAnnotation.typeParameters.params[0].properties,
argumentProps: typeAnnotation.typeParameters.params[0].members,
paperTopLevelNameDeprecated: paperName,
bubblingType,
};
} else if (name === 'BubblingEventHandler' || name === 'DirectEventHandler') {
const eventType = name === 'BubblingEventHandler' ? 'bubble' : 'direct';
const paperTopLevelNameDeprecated =
typeAnnotation.typeParameters.params.length > 1
? typeAnnotation.typeParameters.params[1].value
? typeAnnotation.typeParameters.params[1].literal.value
: null;
if (
typeAnnotation.typeParameters.params[0].type ===
'NullLiteralTypeAnnotation'
) {

if (typeAnnotation.typeParameters.params[0].type === 'TSNullKeyword') {
return {
argumentProps: [],
bubblingType: eventType,
Expand All @@ -137,7 +155,7 @@ function findEventArgumentsAndType(
);
} else if (types[name]) {
return findEventArgumentsAndType(
types[name].right,
types[name].typeAnnotation,
types,
bubblingType,
paperName,
Expand All @@ -153,12 +171,8 @@ function findEventArgumentsAndType(

function buildPropertiesForEvent(property): NamedShape<EventTypeAnnotation> {
const name = property.key.name;
const optional =
property.value.type === 'NullableTypeAnnotation' || property.optional;
let typeAnnotation =
property.value.type === 'NullableTypeAnnotation'
? property.value.typeAnnotation
: property.value;
const optional = property.optional || false;
let typeAnnotation = property.typeAnnotation.typeAnnotation;

return getPropertyType(name, optional, typeAnnotation);
}
Expand All @@ -175,18 +189,27 @@ function buildEventSchema(
property: EventTypeAST,
): ?EventTypeShape {
const name = property.key.name;
const optional =
property.optional || property.value.type === 'NullableTypeAnnotation';

let typeAnnotation =
property.value.type === 'NullableTypeAnnotation'
? property.value.typeAnnotation
: property.value;
let optional = property.optional || false;
let typeAnnotation = property.typeAnnotation.typeAnnotation;

// Check for T | null | void
if (
typeAnnotation.type === 'TSUnionType' &&
typeAnnotation.types.some(
t => t.type === 'TSNullKeyword' || t.type === 'TSVoidKeyword',
)
) {
typeAnnotation = typeAnnotation.types.filter(
t => t.type !== 'TSNullKeyword' && t.type !== 'TSVoidKeyword',
)[0];
optional = true;
}

if (
typeAnnotation.type !== 'GenericTypeAnnotation' ||
(typeAnnotation.id.name !== 'BubblingEventHandler' &&
typeAnnotation.id.name !== 'DirectEventHandler')
typeAnnotation.type !== 'TSTypeReference' ||
(typeAnnotation.typeName.name !== 'BubblingEventHandler' &&
typeAnnotation.typeName.name !== 'DirectEventHandler')
) {
return null;
}
Expand Down Expand Up @@ -228,11 +251,11 @@ function buildEventSchema(
}
}

// $FlowFixMe[unclear-type] there's no flowtype for ASTs
// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser
type EventTypeAST = Object;

type TypeMap = {
// $FlowFixMe[unclear-type] there's no flowtype for ASTs
// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser
[string]: Object,
...
};
Expand All @@ -242,7 +265,7 @@ function getEvents(
types: TypeMap,
): $ReadOnlyArray<EventTypeShape> {
return eventTypeAST
.filter(property => property.type === 'ObjectTypeProperty')
.filter(property => property.type === 'TSPropertySignature')
.map(property => buildEventSchema(types, property))
.filter(Boolean);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import type {ExtendsPropsShape} from '../../../CodegenSchema.js';
import type {TypeDeclarationMap} from '../utils.js';

function extendsForProp(prop: PropsAST, types: TypeDeclarationMap) {
if (!prop.argument) {
if (!prop.expression) {
console.log('null', prop);
}
const name = prop.argument.id.name;
const name = prop.expression.name;

if (types[name] != null) {
// This type is locally defined in the file
Expand All @@ -42,20 +42,20 @@ function removeKnownExtends(
): $ReadOnlyArray<PropsAST> {
return typeDefinition.filter(
prop =>
prop.type !== 'ObjectTypeSpreadProperty' ||
prop.type !== 'TSExpressionWithTypeArguments' ||
extendsForProp(prop, types) === null,
);
}

// $FlowFixMe[unclear-type] there's no flowtype for ASTs
// $FlowFixMe[unclear-type] TODO(T108222691): Use flow-types for @babel/parser
type PropsAST = Object;

function getExtendsProps(
typeDefinition: $ReadOnlyArray<PropsAST>,
types: TypeDeclarationMap,
): $ReadOnlyArray<ExtendsPropsShape> {
return typeDefinition
.filter(prop => prop.type === 'ObjectTypeSpreadProperty')
.filter(prop => prop.type === 'TSExpressionWithTypeArguments')
.map(prop => extendsForProp(prop, types))
.filter(Boolean);
}
Expand Down
Loading

0 comments on commit 7615bde

Please sign in to comment.