/
githubmr
/
facebook-react-native
Обзор
Документация
Войти
/
githubmr
/
facebook-react-native
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-native-codegen/src/parsers/flow/modules/__tests__/module-parser-e2e-test.js
1 255 строк
50 KB
Sam Zhou
Use the prettier config at the root for react-native-codegen (#52746)
22 июл 2025, 05:45
22 июл 2025, 05:45
2c3a00b
Код
Авторство
О чём код?
/** * 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 */ import type { NativeModuleBaseTypeAnnotation, NativeModuleParamTypeAnnotation, NativeModuleReturnTypeAnnotation, NativeModuleSchema, } from '../../../../CodegenSchema'; const { MissingTypeParameterGenericParserError, UnnamedFunctionParamParserError, UnsupportedGenericParserError, UnsupportedTypeAnnotationParserError, } = require('../../../errors'); const {unwrapNullable} = require('../../../parsers-commons'); const {FlowParser} = require('../../parser'); const invariant = require('invariant'); const flowParser = new FlowParser(); type PrimitiveTypeAnnotationType = | 'StringTypeAnnotation' | 'NumberTypeAnnotation' | 'Int32TypeAnnotation' | 'DoubleTypeAnnotation' | 'FloatTypeAnnotation' | 'BooleanTypeAnnotation'; const PRIMITIVES: $ReadOnlyArray<[string, PrimitiveTypeAnnotationType]> = [ ['string', 'StringTypeAnnotation'], ['number', 'NumberTypeAnnotation'], ['Int32', 'Int32TypeAnnotation'], ['Double', 'DoubleTypeAnnotation'], ['Float', 'FloatTypeAnnotation'], ['boolean', 'BooleanTypeAnnotation'], ]; const RESERVED_FUNCTION_VALUE_TYPE_NAME: $ReadOnlyArray<'RootTag'> = [ 'RootTag', ]; const MODULE_NAME = 'NativeFoo'; const TYPE_ALIAS_DECLARATIONS = ` type Animal = { name: string, }; type AnimalPointer = Animal; `; function expectAnimalTypeAliasToExist(module: NativeModuleSchema) { const animalAlias = module.aliasMap.Animal; expect(animalAlias).not.toBe(null); invariant(animalAlias != null, ''); expect(animalAlias.type).toBe('ObjectTypeAnnotation'); expect(animalAlias.properties.length).toBe(1); expect(animalAlias.properties[0].name).toBe('name'); expect(animalAlias.properties[0].optional).toBe(false); const [typeAnnotation, nullable] = unwrapNullable( animalAlias.properties[0].typeAnnotation, ); expect(typeAnnotation.type).toBe('StringTypeAnnotation'); expect(nullable).toBe(false); } describe('Flow Module Parser', () => { describe('Parameter Parsing', () => { it("should fail parsing when a method has an parameter of type 'any'", () => { const parser = () => parseModule(` import type {TurboModule} from 'RCTExport'; import * as TurboModuleRegistry from 'TurboModuleRegistry'; export interface Spec extends TurboModule { useArg(arg: any): void; } export default TurboModuleRegistry.get<Spec>('Foo'); `); expect(parser).toThrow(UnsupportedTypeAnnotationParserError); }); it('should fail parsing when a function param type is unamed', () => { const parser = () => parseModule(` import type {TurboModule} from 'RCTExport'; import * as TurboModuleRegistry from 'TurboModuleRegistry'; export interface Spec extends TurboModule { useArg(boolean): void; } export default TurboModuleRegistry.get<Spec>('Foo'); `); expect(parser).toThrow(UnnamedFunctionParamParserError); }); [ {nullable: false, optional: false}, {nullable: false, optional: true}, {nullable: true, optional: false}, {nullable: true, optional: true}, ].forEach(({nullable, optional}) => { const PARAM_TYPE_DESCRIPTION = nullable && optional ? 'a nullable and optional' : nullable ? 'a nullable' : optional ? 'an optional' : 'a required'; function annotateArg(paramName: string, paramType: string) { if (nullable && optional) { return `${paramName}?: ?${paramType}`; } if (nullable) { return `${paramName}: ?${paramType}`; } if (optional) { return `${paramName}?: ${paramType}`; } return `${paramName}: ${paramType}`; } function parseParamType( paramName: string, paramType: string, ): [NativeModuleParamTypeAnnotation, NativeModuleSchema] { const module = parseModule(` import type {TurboModule} from 'RCTExport'; import * as TurboModuleRegistry from 'TurboModuleRegistry'; ${TYPE_ALIAS_DECLARATIONS} export interface Spec extends TurboModule { useArg(${annotateArg(paramName, paramType)}): void; } export default TurboModuleRegistry.get<Spec>('Foo'); `); expect(module.spec.methods[0]).not.toBe(null); const param = unwrapNullable(module.spec.methods[0].typeAnnotation)[0] .params[0]; expect(param).not.toBe(null); expect(param.name).toBe(paramName); expect(param.optional).toBe(optional); const [paramTypeAnnotation, isParamTypeAnnotationNullable] = unwrapNullable(param.typeAnnotation); expect(isParamTypeAnnotationNullable).toBe(nullable); return [paramTypeAnnotation, module]; } describe( (nullable && optional ? 'Nullable and Optional' : nullable ? 'Nullable' : optional ? 'Optional' : 'Required') + ' Parameter', () => { it(`should not parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter of type 'Function'`, () => { expect(() => parseParamType('arg', 'Function')).toThrow( UnsupportedGenericParserError, ); }); describe('Primitive types', () => { PRIMITIVES.forEach(([FLOW_TYPE, PARSED_TYPE_NAME]) => { it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} primitive parameter of type '${FLOW_TYPE}'`, () => { const [paramTypeAnnotation] = parseParamType('arg', FLOW_TYPE); expect(paramTypeAnnotation.type).toBe(PARSED_TYPE_NAME); }); }); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter of type 'Object'`, () => { const [paramTypeAnnotation] = parseParamType('arg', 'Object'); expect(paramTypeAnnotation.type).toBe( 'GenericObjectTypeAnnotation', ); }); describe('Reserved Types', () => { RESERVED_FUNCTION_VALUE_TYPE_NAME.forEach(FLOW_TYPE => { it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter of reserved type '${FLOW_TYPE}'`, () => { const [paramTypeAnnotation] = parseParamType('arg', FLOW_TYPE); expect(paramTypeAnnotation.type).toBe('ReservedTypeAnnotation'); invariant( paramTypeAnnotation.type === 'ReservedTypeAnnotation', 'Param must be a Reserved type', ); expect(paramTypeAnnotation.name).toBe(FLOW_TYPE); }); }); }); describe('Array Types', () => { it(`should not parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter of type 'Array'`, () => { expect(() => parseParamType('arg', 'Array')).toThrow( MissingTypeParameterGenericParserError, ); }); function parseParamArrayElementType( paramName: string, paramType: string, ): [NativeModuleBaseTypeAnnotation, NativeModuleSchema] { const [paramTypeAnnotation, module] = parseParamType( paramName, `Array<${paramType}>`, ); expect(paramTypeAnnotation.type).toBe('ArrayTypeAnnotation'); invariant(paramTypeAnnotation.type === 'ArrayTypeAnnotation', ''); expect(paramTypeAnnotation.elementType.type).not.toEqual( 'AnyTypeAnnotation', ); invariant( paramTypeAnnotation.elementType.type !== 'AnyTypeAnnotation', '', ); const [elementType, isElementTypeNullable] = unwrapNullable<NativeModuleBaseTypeAnnotation>( paramTypeAnnotation.elementType, ); expect(isElementTypeNullable).toBe(false); return [elementType, module]; } // TODO: Do we support nullable element types? describe('Primitive Element Types', () => { PRIMITIVES.forEach(([FLOW_TYPE, PARSED_TYPE_NAME]) => { it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter of type 'Array<${FLOW_TYPE}>'`, () => { const [elementType] = parseParamArrayElementType( 'arg', FLOW_TYPE, ); expect(elementType.type).toBe(PARSED_TYPE_NAME); }); }); }); describe('Reserved Element Types', () => { RESERVED_FUNCTION_VALUE_TYPE_NAME.forEach(FLOW_TYPE => { it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter of type 'Array<${FLOW_TYPE}>'`, () => { const [elementType] = parseParamArrayElementType( 'arg', FLOW_TYPE, ); expect(elementType.type).toBe('ReservedTypeAnnotation'); invariant(elementType.type === 'ReservedTypeAnnotation', ''); expect(elementType.name).toBe(FLOW_TYPE); }); }); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter of type 'Array<Object>'`, () => { const [elementType] = parseParamArrayElementType('arg', 'Object'); expect(elementType.type).toBe('GenericObjectTypeAnnotation'); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of some array of an alias`, () => { const [elementType, module] = parseParamArrayElementType( 'arg', 'Animal', ); expect(elementType.type).toBe('TypeAliasTypeAnnotation'); invariant(elementType.type === 'TypeAliasTypeAnnotation', ''); expect(elementType.name).toBe('Animal'); expectAnimalTypeAliasToExist(module); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter of type 'Array<{foo: ?string}>'`, () => { const [elementType] = parseParamArrayElementType( 'arg', '{foo: ?string}', ); expect(elementType).not.toBe(null); expect(elementType.type).toBe('ObjectTypeAnnotation'); invariant(elementType.type === 'ObjectTypeAnnotation', ''); const {properties} = elementType; invariant(properties != null, ''); expect(properties).not.toBe(null); expect(properties[0]).not.toBe(null); expect(properties[0].name).toBe('foo'); expect(properties[0].typeAnnotation).not.toBe(null); const [typeAnnotation, isPropertyNullable] = unwrapNullable( properties[0].typeAnnotation, ); expect(typeAnnotation.type).toBe('StringTypeAnnotation'); expect(isPropertyNullable).toBe(true); expect(properties[0].optional).toBe(false); }); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of some type alias`, () => { const [paramTypeAnnotation, module] = parseParamType( 'arg', 'Animal', ); expect(paramTypeAnnotation.type).toBe('TypeAliasTypeAnnotation'); invariant( paramTypeAnnotation.type === 'TypeAliasTypeAnnotation', '', ); expect(paramTypeAnnotation.name).toBe('Animal'); expectAnimalTypeAliasToExist(module); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of some type alias that points to another type alias`, () => { const [paramTypeAnnotation, module] = parseParamType( 'arg', 'AnimalPointer', ); expect(paramTypeAnnotation.type).toBe('TypeAliasTypeAnnotation'); invariant( paramTypeAnnotation.type === 'TypeAliasTypeAnnotation', '', ); expect(paramTypeAnnotation.name).toBe('Animal'); expectAnimalTypeAliasToExist(module); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of some type alias that points to another nullable type alias`, () => { const module = parseModule(` import type {TurboModule} from 'RCTExport'; import * as TurboModuleRegistry from 'TurboModuleRegistry'; type Animal = ?{ name: string, }; type AnimalPointer = Animal; export interface Spec extends TurboModule { useArg(${annotateArg('arg', 'AnimalPointer')}): void; } export default TurboModuleRegistry.get<Spec>('Foo'); `); expect(module.spec.methods[0]).not.toBe(null); const param = unwrapNullable( module.spec.methods[0].typeAnnotation, )[0].params[0]; expect(param.name).toBe('arg'); expect(param.optional).toBe(optional); // The TypeAliasAnnotation is called Animal, and is nullable const [paramTypeAnnotation, isParamTypeAnnotationNullable] = unwrapNullable(param.typeAnnotation); expect(paramTypeAnnotation.type).toBe('TypeAliasTypeAnnotation'); invariant( paramTypeAnnotation.type === 'TypeAliasTypeAnnotation', '', ); expect(paramTypeAnnotation.name).toBe('Animal'); expect(isParamTypeAnnotationNullable).toBe(true); // The Animal type alias RHS is valid, and non-null expectAnimalTypeAliasToExist(module); }); [ {nullable: false, optional: false}, {nullable: false, optional: true}, {nullable: true, optional: false}, {nullable: true, optional: true}, ].forEach(({nullable: isPropNullable, optional: isPropOptional}) => { const PROP_TYPE_DESCRIPTION = isPropNullable && isPropOptional ? 'a nullable and optional' : isPropNullable ? 'a nullable' : isPropOptional ? 'an optional' : 'a required'; function annotateProp(propName: string, propType: string) { if (isPropNullable && isPropOptional) { return `${propName}?: ?${propType}`; } if (isPropNullable) { return `${propName}: ?${propType}`; } if (isPropOptional) { return `${propName}?: ${propType}`; } return `${propName}: ${propType}`; } function parseParamTypeObjectLiteralProp( propName: string, propType: string, ): [ $ReadOnly<{ name: string, optional: boolean, typeAnnotation: NativeModuleBaseTypeAnnotation, }>, NativeModuleSchema, ] { const [paramTypeAnnotation, module] = parseParamType( 'arg', `{${annotateProp(propName, propType)}}`, ); expect(paramTypeAnnotation.type).toBe('ObjectTypeAnnotation'); invariant( paramTypeAnnotation.type === 'ObjectTypeAnnotation', '', ); const {properties} = paramTypeAnnotation; expect(properties).not.toBe(null); invariant(properties != null, ''); expect(properties.length).toBe(1); expect(properties[0].name).toBe(propName); expect(properties[0].optional).toBe(isPropOptional); const [propertyTypeAnnotation, isPropertyTypeAnnotationNullable] = unwrapNullable(properties[0].typeAnnotation); expect(propertyTypeAnnotation).not.toBe(null); expect(isPropertyTypeAnnotationNullable).toBe(isPropNullable); return [ { ...properties[0], typeAnnotation: propertyTypeAnnotation, }, module, ]; } describe( (isPropNullable && isPropOptional ? 'Nullable and Optional' : isPropNullable ? 'Nullable' : isPropOptional ? 'Optional' : 'Required') + ' Property', () => { describe('Props with Primitive Types', () => { PRIMITIVES.forEach(([FLOW_TYPE, PARSED_TYPE_NAME]) => { it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of primitive type '${FLOW_TYPE}'`, () => { const [prop] = parseParamTypeObjectLiteralProp( 'prop', FLOW_TYPE, ); expect(prop.typeAnnotation.type).toBe(PARSED_TYPE_NAME); }); }); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Object'`, () => { const [prop] = parseParamTypeObjectLiteralProp( 'prop', 'Object', ); expect(prop.typeAnnotation.type).toBe( 'GenericObjectTypeAnnotation', ); }); describe('Props with Reserved Types', () => { RESERVED_FUNCTION_VALUE_TYPE_NAME.forEach(FLOW_TYPE => { it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of reserved type '${FLOW_TYPE}'`, () => { const [prop] = parseParamTypeObjectLiteralProp( 'prop', FLOW_TYPE, ); expect(prop.typeAnnotation.type).toBe( 'ReservedTypeAnnotation', ); invariant( prop.typeAnnotation.type === 'ReservedTypeAnnotation', '', ); expect(prop.typeAnnotation.name).toBe(FLOW_TYPE); }); }); }); describe('Props with Array Types', () => { it(`should not parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Array`, () => { expect(() => parseParamTypeObjectLiteralProp('prop', 'Array'), ).toThrow(MissingTypeParameterGenericParserError); }); function parseArrayElementType( propName: string, arrayElementType: string, ): [NativeModuleBaseTypeAnnotation, NativeModuleSchema] { const [property, module] = parseParamTypeObjectLiteralProp( 'propName', `Array<${arrayElementType}>`, ); expect(property.typeAnnotation.type).toBe( 'ArrayTypeAnnotation', ); invariant( property.typeAnnotation.type === 'ArrayTypeAnnotation', '', ); const {elementType: nullableElementType} = property.typeAnnotation; expect(nullableElementType.type).not.toEqual( 'AnyTypeAnnotation', ); invariant( nullableElementType.type !== 'AnyTypeAnnotation', '', ); const [elementType, isElementTypeNullable] = unwrapNullable<NativeModuleBaseTypeAnnotation>( nullableElementType, ); expect(isElementTypeNullable).toBe(false); return [elementType, module]; } PRIMITIVES.forEach(([FLOW_TYPE, PARSED_TYPE_NAME]) => { it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Array<${FLOW_TYPE}>'`, () => { const [elementType] = parseArrayElementType( 'prop', FLOW_TYPE, ); expect(elementType.type).toBe(PARSED_TYPE_NAME); }); }); RESERVED_FUNCTION_VALUE_TYPE_NAME.forEach(FLOW_TYPE => { it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Array<${FLOW_TYPE}>'`, () => { const [elementType] = parseArrayElementType( 'prop', FLOW_TYPE, ); expect(elementType.type).toBe('ReservedTypeAnnotation'); invariant( elementType.type === 'ReservedTypeAnnotation', '', ); expect(elementType.name).toBe(FLOW_TYPE); }); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Array<Object>'`, () => { const [elementType] = parseArrayElementType( 'prop', 'Object', ); expect(elementType.type).toBe( 'GenericObjectTypeAnnotation', ); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type of some array of an alias`, () => { const [elementType, module] = parseArrayElementType( 'prop', 'Animal', ); expect(elementType.type).toBe('TypeAliasTypeAnnotation'); invariant( elementType.type === 'TypeAliasTypeAnnotation', '', ); expect(elementType.name).toBe('Animal'); expectAnimalTypeAliasToExist(module); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of 'Array<{foo: ?string}>'`, () => { const [elementType] = parseArrayElementType( 'prop', '{foo: ?string}', ); expect(elementType.type).toBe('ObjectTypeAnnotation'); invariant(elementType.type === 'ObjectTypeAnnotation', ''); const {properties} = elementType; expect(properties).not.toBe(null); invariant(properties != null, ''); expect(properties[0]).not.toBe(null); expect(properties[0].name).toBe('foo'); expect(properties[0].typeAnnotation).not.toBe(null); const [ propertyTypeAnnotation, isPropertyTypeAnnotationNullable, ] = unwrapNullable(properties[0].typeAnnotation); expect(propertyTypeAnnotation.type).toBe( 'StringTypeAnnotation', ); expect(isPropertyTypeAnnotationNullable).toBe(true); expect(properties[0].optional).toBe(false); }); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type '{foo: ?string}'`, () => { const [property] = parseParamTypeObjectLiteralProp( 'prop', '{foo: ?string}', ); expect(property.typeAnnotation.type).toBe( 'ObjectTypeAnnotation', ); invariant( property.typeAnnotation.type === 'ObjectTypeAnnotation', '', ); const {properties} = property.typeAnnotation; expect(properties).not.toBe(null); invariant(properties != null, ''); expect(properties[0]).not.toBe(null); expect(properties[0].name).toBe('foo'); const [ propertyTypeAnnotation, isPropertyTypeAnnotationNullable, ] = unwrapNullable(properties[0].typeAnnotation); expect(propertyTypeAnnotation.type).toBe( 'StringTypeAnnotation', ); expect(isPropertyTypeAnnotationNullable).toBe(true); expect(properties[0].optional).toBe(false); }); it(`should parse methods that have ${PARAM_TYPE_DESCRIPTION} parameter type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of some type alias`, () => { const [property, module] = parseParamTypeObjectLiteralProp( 'prop', 'Animal', ); expect(property.typeAnnotation.type).toBe( 'TypeAliasTypeAnnotation', ); invariant( property.typeAnnotation.type === 'TypeAliasTypeAnnotation', '', ); expect(property.typeAnnotation.name).toBe('Animal'); expectAnimalTypeAliasToExist(module); }); }, ); }); }, ); }); }); describe('Return Parsing', () => { it('should parse methods that have a return type of void', () => { const module = parseModule(` import type {TurboModule} from 'RCTExport'; import * as TurboModuleRegistry from 'TurboModuleRegistry'; export interface Spec extends TurboModule { useArg(): void; } export default TurboModuleRegistry.get<Spec>('Foo'); `); expect(module.spec.methods[0]).not.toBe(null); const [functionTypeAnnotation, isFunctionTypeAnnotationNullable] = unwrapNullable(module.spec.methods[0].typeAnnotation); expect(isFunctionTypeAnnotationNullable).toBe(false); const [returnTypeAnnotation, isReturnTypeAnnotationNullable] = unwrapNullable(functionTypeAnnotation.returnTypeAnnotation); expect(returnTypeAnnotation.type).toBe('VoidTypeAnnotation'); expect(isReturnTypeAnnotationNullable).toBe(false); }); [true, false].forEach(IS_RETURN_TYPE_NULLABLE => { const RETURN_TYPE_DESCRIPTION = IS_RETURN_TYPE_NULLABLE ? 'a nullable' : 'a non-nullable'; const annotateRet = (retType: string) => IS_RETURN_TYPE_NULLABLE ? `?${retType}` : retType; function parseReturnType( flowType: string, ): [NativeModuleReturnTypeAnnotation, NativeModuleSchema] { const module = parseModule(` import type {TurboModule} from 'RCTExport'; import * as TurboModuleRegistry from 'TurboModuleRegistry'; ${TYPE_ALIAS_DECLARATIONS} export interface Spec extends TurboModule { useArg(): ${annotateRet(flowType)}; } export default TurboModuleRegistry.get<Spec>('Foo'); `); expect(module.spec.methods[0]).not.toBe(null); const [functionTypeAnnotation, isFunctionTypeAnnotationNullable] = unwrapNullable(module.spec.methods[0].typeAnnotation); expect(isFunctionTypeAnnotationNullable).toBe(false); const [returnTypeAnnotation, isReturnTypeAnnotationNullable] = unwrapNullable(functionTypeAnnotation.returnTypeAnnotation); expect(isReturnTypeAnnotationNullable).toBe(IS_RETURN_TYPE_NULLABLE); return [returnTypeAnnotation, module]; } describe( IS_RETURN_TYPE_NULLABLE ? 'Nullable Returns' : 'Non-Nullable Returns', () => { ['Promise<void>', 'Promise<{}>', 'Promise<*>'].forEach( promiseFlowType => { it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return of type '${promiseFlowType}'`, () => { const [returnTypeAnnotation] = parseReturnType(promiseFlowType); expect(returnTypeAnnotation.type).toBe('PromiseTypeAnnotation'); }); }, ); describe('Primitive Types', () => { PRIMITIVES.forEach(([FLOW_TYPE, PARSED_TYPE_NAME]) => { it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} primitive return of type '${FLOW_TYPE}'`, () => { const [returnTypeAnnotation] = parseReturnType(FLOW_TYPE); expect(returnTypeAnnotation.type).toBe(PARSED_TYPE_NAME); }); }); }); describe('Reserved Types', () => { RESERVED_FUNCTION_VALUE_TYPE_NAME.forEach(FLOW_TYPE => { it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} reserved return of type '${FLOW_TYPE}'`, () => { const [returnTypeAnnotation] = parseReturnType(FLOW_TYPE); expect(returnTypeAnnotation.type).toBe( 'ReservedTypeAnnotation', ); invariant( returnTypeAnnotation.type === 'ReservedTypeAnnotation', '', ); expect(returnTypeAnnotation.name).toBe(FLOW_TYPE); }); }); }); describe('Array Types', () => { it(`should not parse methods that have ${RETURN_TYPE_DESCRIPTION} return of type 'Array'`, () => { expect(() => parseReturnType('Array')).toThrow( MissingTypeParameterGenericParserError, ); }); function parseArrayElementReturnType( flowType: string, ): [NativeModuleBaseTypeAnnotation, NativeModuleSchema] { const [returnTypeAnnotation, module] = parseReturnType( 'Array' + (flowType != null ? `<${flowType}>` : ''), ); expect(returnTypeAnnotation.type).toBe('ArrayTypeAnnotation'); invariant( returnTypeAnnotation.type === 'ArrayTypeAnnotation', '', ); const arrayTypeAnnotation = returnTypeAnnotation; const {elementType} = arrayTypeAnnotation; expect(elementType.type).not.toEqual('AnyTypeAnnotation'); invariant(elementType.type !== 'AnyTypeAnnotation', ''); const [elementTypeAnnotation, isElementTypeAnnotation] = unwrapNullable<NativeModuleBaseTypeAnnotation>(elementType); expect(isElementTypeAnnotation).toBe(false); return [elementTypeAnnotation, module]; } // TODO: Do we support nullable element types? describe('Primitive Element Types', () => { PRIMITIVES.forEach(([FLOW_TYPE, PARSED_TYPE_NAME]) => { it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return of type 'Array<${FLOW_TYPE}>'`, () => { const [elementType] = parseArrayElementReturnType(FLOW_TYPE); expect(elementType.type).toBe(PARSED_TYPE_NAME); }); }); }); describe('Reserved Element Types', () => { RESERVED_FUNCTION_VALUE_TYPE_NAME.forEach(FLOW_TYPE => { it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return of type 'Array<${FLOW_TYPE}>'`, () => { const [elementType] = parseArrayElementReturnType(FLOW_TYPE); expect(elementType.type).toBe('ReservedTypeAnnotation'); invariant(elementType.type === 'ReservedTypeAnnotation', ''); expect(elementType.name).toBe(FLOW_TYPE); }); }); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return of type 'Array<Object>'`, () => { const [elementType] = parseArrayElementReturnType('Object'); expect(elementType.type).toBe('GenericObjectTypeAnnotation'); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of some array of an alias`, () => { const [elementType, module] = parseArrayElementReturnType('Animal'); expect(elementType.type).toBe('TypeAliasTypeAnnotation'); invariant(elementType.type === 'TypeAliasTypeAnnotation', ''); expect(elementType.name).toBe('Animal'); expectAnimalTypeAliasToExist(module); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return of type 'Array<{foo: ?string}>'`, () => { const [elementType] = parseArrayElementReturnType('{foo: ?string}'); expect(elementType.type).toBe('ObjectTypeAnnotation'); invariant(elementType.type === 'ObjectTypeAnnotation', ''); const {properties} = elementType; expect(properties).not.toBe(null); invariant(properties != null, ''); expect(properties[0]).not.toBe(null); expect(properties[0].name).toBe('foo'); expect(properties[0].typeAnnotation).not.toBe(null); const [propertyTypeAnnotation, isPropertyTypeAnnotationNullable] = unwrapNullable(properties[0].typeAnnotation); expect(propertyTypeAnnotation.type).toBe('StringTypeAnnotation'); expect(isPropertyTypeAnnotationNullable).toBe(true); expect(properties[0].optional).toBe(false); }); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of some type alias`, () => { const [returnTypeAnnotation, module] = parseReturnType('Animal'); expect(returnTypeAnnotation.type).toBe('TypeAliasTypeAnnotation'); invariant( returnTypeAnnotation.type === 'TypeAliasTypeAnnotation', '', ); expect(returnTypeAnnotation.name).toBe('Animal'); expectAnimalTypeAliasToExist(module); }); it(`should not parse methods that have ${RETURN_TYPE_DESCRIPTION} return of type 'Function'`, () => { expect(() => parseReturnType('Function')).toThrow( UnsupportedGenericParserError, ); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return of type 'Object'`, () => { const [returnTypeAnnotation] = parseReturnType('Object'); expect(returnTypeAnnotation.type).toBe( 'GenericObjectTypeAnnotation', ); }); describe('Object Literals Types', () => { // TODO: Inexact vs exact object literals? it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an empty object literal`, () => { const [returnTypeAnnotation] = parseReturnType('{}'); expect(returnTypeAnnotation.type).toBe('ObjectTypeAnnotation'); invariant( returnTypeAnnotation.type === 'ObjectTypeAnnotation', '', ); // Validate properties of object literal expect(returnTypeAnnotation.properties).not.toBe(null); expect(returnTypeAnnotation.properties?.length).toBe(0); }); [ {nullable: false, optional: false}, {nullable: false, optional: true}, {nullable: true, optional: false}, {nullable: true, optional: true}, ].forEach(({nullable, optional}) => { const PROP_TYPE_DESCRIPTION = nullable && optional ? 'a nullable and optional' : nullable ? 'a nullable' : optional ? 'an optional' : 'a required'; function annotateProp(propName: string, propType: string) { if (nullable && optional) { return `${propName}?: ?${propType}`; } if (nullable) { return `${propName}: ?${propType}`; } if (optional) { return `${propName}?: ${propType}`; } return `${propName}: ${propType}`; } function parseObjectLiteralReturnTypeProp( propName: string, propType: string, ): [ $ReadOnly<{ name: string, optional: boolean, typeAnnotation: NativeModuleBaseTypeAnnotation, }>, NativeModuleSchema, ] { const [returnTypeAnnotation, module] = parseReturnType( `{${annotateProp(propName, propType)}}`, ); expect(returnTypeAnnotation.type).toBe('ObjectTypeAnnotation'); invariant( returnTypeAnnotation.type === 'ObjectTypeAnnotation', '', ); const properties = returnTypeAnnotation.properties; expect(properties).not.toBe(null); invariant(properties != null, ''); expect(properties.length).toBe(1); // Validate property const property = properties[0]; expect(property.name).toBe(propName); expect(property.optional).toBe(optional); const [ propertyTypeAnnotation, isPropertyTypeAnnotationNullable, ] = unwrapNullable(property.typeAnnotation); expect(propertyTypeAnnotation).not.toBe(null); expect(isPropertyTypeAnnotationNullable).toBe(nullable); return [ { ...property, typeAnnotation: propertyTypeAnnotation, }, module, ]; } describe( (nullable && optional ? 'Nullable and Optional' : nullable ? 'Nullable' : optional ? 'Optional' : 'Required') + ' Property', () => { /** * TODO: Fill out props in promise */ describe('Props with Primitive Types', () => { PRIMITIVES.forEach(([FLOW_TYPE, PARSED_TYPE_NAME]) => { it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of primitive type '${FLOW_TYPE}'`, () => { const [property] = parseObjectLiteralReturnTypeProp( 'prop', FLOW_TYPE, ); expect(property.typeAnnotation.type).toBe( PARSED_TYPE_NAME, ); }); }); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Object'`, () => { const [property] = parseObjectLiteralReturnTypeProp( 'prop', 'Object', ); expect(property.typeAnnotation.type).toBe( 'GenericObjectTypeAnnotation', ); }); describe('Props with Reserved Types', () => { RESERVED_FUNCTION_VALUE_TYPE_NAME.forEach(FLOW_TYPE => { it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of reserved type '${FLOW_TYPE}'`, () => { const [property] = parseObjectLiteralReturnTypeProp( 'prop', FLOW_TYPE, ); expect(property.typeAnnotation.type).toBe( 'ReservedTypeAnnotation', ); invariant( property.typeAnnotation.type === 'ReservedTypeAnnotation', '', ); expect(property.typeAnnotation.name).toBe(FLOW_TYPE); }); }); }); describe('Props with Array Types', () => { it(`should not parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Array`, () => { expect(() => parseObjectLiteralReturnTypeProp('prop', 'Array'), ).toThrow(MissingTypeParameterGenericParserError); }); function parseArrayElementType( propName: string, arrayElementType: string, ): [NativeModuleBaseTypeAnnotation, NativeModuleSchema] { const [property, module] = parseObjectLiteralReturnTypeProp( propName, `Array<${arrayElementType}>`, ); expect(property.name).toBe(propName); expect(property.typeAnnotation.type).toBe( 'ArrayTypeAnnotation', ); invariant( property.typeAnnotation.type === 'ArrayTypeAnnotation', '', ); const {elementType: nullableElementType} = property.typeAnnotation; expect(nullableElementType.type).not.toEqual( 'AnyTypeAnnotation', ); invariant( nullableElementType.type !== 'AnyTypeAnnotation', '', ); const [elementType, isElementTypeNullable] = unwrapNullable<NativeModuleBaseTypeAnnotation>( nullableElementType, ); expect(isElementTypeNullable).toBe(false); return [elementType, module]; } PRIMITIVES.forEach(([FLOW_TYPE, PARSED_TYPE_NAME]) => { it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Array<${FLOW_TYPE}>'`, () => { const [elementType] = parseArrayElementType( 'prop', FLOW_TYPE, ); expect(elementType.type).toBe(PARSED_TYPE_NAME); }); }); RESERVED_FUNCTION_VALUE_TYPE_NAME.forEach(FLOW_TYPE => { it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Array<${FLOW_TYPE}>'`, () => { const [elementType] = parseArrayElementType( 'prop', FLOW_TYPE, ); expect(elementType.type).toBe('ReservedTypeAnnotation'); invariant( elementType.type === 'ReservedTypeAnnotation', '', ); expect(elementType.name).toBe(FLOW_TYPE); }); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Array<Object>'`, () => { const [elementType] = parseArrayElementType( 'prop', 'Object', ); expect(elementType).not.toBe(null); expect(elementType.type).toBe( 'GenericObjectTypeAnnotation', ); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type of some array of an aliase`, () => { const [elementType, module] = parseArrayElementType( 'prop', 'Animal', ); expect(elementType.type).toBe('TypeAliasTypeAnnotation'); invariant( elementType.type === 'TypeAliasTypeAnnotation', '', ); expect(elementType.name).toBe('Animal'); expectAnimalTypeAliasToExist(module); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of type 'Array<{foo: ?string}>'`, () => { const [elementType] = parseArrayElementType( 'prop', '{foo: ?string}', ); expect(elementType.type).toBe('ObjectTypeAnnotation'); invariant( elementType.type === 'ObjectTypeAnnotation', '', ); const {properties} = elementType; invariant(properties != null, ''); expect(properties).not.toBe(null); expect(properties[0]).not.toBe(null); expect(properties[0].name).toBe('foo'); expect(properties[0].optional).toBe(false); const [ propertyTypeAnnotation, isPropertyTypeAnnotationNullable, ] = unwrapNullable(properties[0].typeAnnotation); expect(propertyTypeAnnotation.type).toBe( 'StringTypeAnnotation', ); expect(isPropertyTypeAnnotationNullable).toBe(true); }); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of '{foo: ?string}'`, () => { const [property] = parseObjectLiteralReturnTypeProp( 'prop', '{foo: ?string}', ); expect(property.typeAnnotation.type).toBe( 'ObjectTypeAnnotation', ); invariant( property.typeAnnotation.type === 'ObjectTypeAnnotation', '', ); const {properties} = property.typeAnnotation; expect(properties).not.toBe(null); invariant(properties != null, ''); expect(properties[0]).not.toBe(null); expect(properties[0].name).toBe('foo'); expect(properties[0].optional).toBe(false); const [ propertyTypeAnnotation, isPropertyTypeAnnotationNullable, ] = unwrapNullable(properties[0].typeAnnotation); expect(propertyTypeAnnotation.type).toBe( 'StringTypeAnnotation', ); expect(isPropertyTypeAnnotationNullable).toBe(true); }); it(`should parse methods that have ${RETURN_TYPE_DESCRIPTION} return type of an object literal with ${PROP_TYPE_DESCRIPTION} prop of some type alias`, () => { const [property, module] = parseObjectLiteralReturnTypeProp( 'prop', 'Animal', ); expect(property.typeAnnotation.type).toBe( 'TypeAliasTypeAnnotation', ); invariant( property.typeAnnotation.type === 'TypeAliasTypeAnnotation', '', ); expect(property.typeAnnotation.name).toBe('Animal'); expectAnimalTypeAliasToExist(module); }); }, ); }); }); }, ); }); }); }); function parseModule(source: string) { const schema = flowParser.parseString(source, `${MODULE_NAME}.js`); const module = schema.modules.NativeFoo; invariant( module.type === 'NativeModule', "'nativeModules' in Spec NativeFoo shouldn't be null", ); return module; }