Skip to content

Commit

Permalink
feat: handle negative values in enums (#47452)
Browse files Browse the repository at this point in the history
Summary:
This PR adds support for negative values in enums.

Currently when we try to use an enum with negative value:

```ts
enum MyEnum {
  ZERO = 0,
  POSITIVE = 1,
  NEGATIVE = -1,
}

export interface Spec extends TurboModule {
  useArg(arg: MyEnum): void;
}

export default TurboModuleRegistry.get<Spec>('Foo');
```

It will fail:

```
Enum values can not be mixed. They all must be either blank, number, or string values.
```

This is because negative values are parsed as `UnaryExpressions` which have `-` operator in front and value as argument.

With the new approach codegen properly generates enums with negative values.

## Changelog:

[GENERAL] [ADDED] - Codegen: Support negative values in enums

Pull Request resolved: #47452

Test Plan: I've added tests to see if everything is working properly

Reviewed By: vzaidman

Differential Revision: D65887888

Pulled By: elicwhite

fbshipit-source-id: edb25f663dc58afa68c69cb84a47cfc67fc1f7e7
  • Loading branch information
okwasniewski authored and facebook-github-bot committed Nov 13, 2024
1 parent 0875912 commit 177bf4d
Show file tree
Hide file tree
Showing 8 changed files with 169 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,35 @@ export interface Spec extends TurboModule {
export default TurboModuleRegistry.getEnforcing<Spec>('MixedValuesEnumNativeModule');
`;

const NUMERIC_VALUES_ENUM_NATIVE_MODULE = `
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
import type {TurboModule} from '../RCTExport';
import * as TurboModuleRegistry from '../TurboModuleRegistry';
export enum SomeEnum {
NUM = 1,
NEGATIVE = -1,
SUBFACTORIAL = !5,
}
export interface Spec extends TurboModule {
+getEnums: (a: SomeEnum) => string;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NumericValuesEnumNativeModule');
`;

const MAP_WITH_EXTRA_KEYS_NATIVE_MODULE = `
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
Expand Down Expand Up @@ -304,5 +333,6 @@ module.exports = {
TWO_NATIVE_EXTENDING_TURBO_MODULE,
EMPTY_ENUM_NATIVE_MODULE,
MIXED_VALUES_ENUM_NATIVE_MODULE,
NUMERIC_VALUES_ENUM_NATIVE_MODULE,
MAP_WITH_EXTRA_KEYS_NATIVE_MODULE,
};
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,7 @@ export enum Quality {
}
export enum Resolution {
Corrupted = -1,
Low = 720,
High = 1080,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULES_WITH_REA
exports[`RN Codegen Flow Parser Fails with error message NATIVE_MODULES_WITH_UNNAMED_PARAMS 1`] = `"Module NativeSampleTurboModule: All function parameters must be named."`;
exports[`RN Codegen Flow Parser Fails with error message NUMERIC_VALUES_ENUM_NATIVE_MODULE 1`] = `
"Syntax error in path/NativeSampleTurboModule.js: 'true', 'false', 'string', 'number' or 'bigint' expected in enum member initializer (20:17)
SUBFACTORIAL = !5,
~~~~~~~~~~~~~~~^"
`;
exports[`RN Codegen Flow Parser Fails with error message TWO_NATIVE_EXTENDING_TURBO_MODULE 1`] = `"Module NativeSampleTurboModule: Every NativeModule spec file must declare exactly one NativeModule Flow interface. This file declares 2: 'Spec', and 'Spec2'. Please remove the extraneous Flow interface declarations."`;
exports[`RN Codegen Flow Parser Fails with error message TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT 1`] = `"Module NativeSampleTurboModule: No Flow interfaces extending TurboModule were detected in this NativeModule spec."`;
Expand Down Expand Up @@ -154,6 +160,10 @@ exports[`RN Codegen Flow Parser can generate fixture CXX_ONLY_NATIVE_MODULE 1`]
'type': 'EnumDeclarationWithMembers',
'memberType': 'NumberTypeAnnotation',
'members': [
{
'name': 'Corrupted',
'value': -1
},
{
'name': 'Low',
'value': 720
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,34 @@ export default TurboModuleRegistry.getEnforcing<Spec>(
);
`;

const NUMERIC_VALUES_ENUM_NATIVE_MODULE = `
/**
* 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.
*
* @format
*/
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
export enum SomeEnum {
NUM = 1,
NEGATIVE = -1,
SUBFACTORIAL = !5,
}
export interface Spec extends TurboModule {
readonly getEnums: (a: SomeEnum) => string;
}
export default TurboModuleRegistry.getEnforcing<Spec>(
'NumericValuesEnumNativeModule',
);
`;

const MAP_WITH_EXTRA_KEYS_NATIVE_MODULE = `
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
Expand Down Expand Up @@ -243,5 +271,6 @@ module.exports = {
TWO_NATIVE_EXTENDING_TURBO_MODULE,
EMPTY_ENUM_NATIVE_MODULE,
MIXED_VALUES_ENUM_NATIVE_MODULE,
NUMERIC_VALUES_ENUM_NATIVE_MODULE,
MAP_WITH_EXTRA_KEYS_NATIVE_MODULE,
};
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,7 @@ export enum Quality {
}
export enum Resolution {
Corrupted = -1,
Low = 720,
High = 1080,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULES_WI

exports[`RN Codegen TypeScript Parser Fails with error message NATIVE_MODULES_WITH_UNNAMED_PARAMS 1`] = `"Module NativeSampleTurboModule: All function parameters must be named."`;

exports[`RN Codegen TypeScript Parser Fails with error message NUMERIC_VALUES_ENUM_NATIVE_MODULE 1`] = `"Module NativeSampleTurboModule: Failed parsing the enum SomeEnum in NativeSampleTurboModule with the error: Enum values can not be mixed. They all must be either blank, number, or string values."`;

exports[`RN Codegen TypeScript Parser Fails with error message TWO_NATIVE_EXTENDING_TURBO_MODULE 1`] = `"Module NativeSampleTurboModule: Every NativeModule spec file must declare exactly one NativeModule TypeScript interface. This file declares 2: 'Spec', and 'Spec2'. Please remove the extraneous TypeScript interface declarations."`;

exports[`RN Codegen TypeScript Parser Fails with error message TWO_NATIVE_MODULES_EXPORTED_WITH_DEFAULT 1`] = `"Module NativeSampleTurboModule: No TypeScript interfaces extending TurboModule were detected in this NativeModule spec."`;
Expand Down Expand Up @@ -145,6 +147,10 @@ exports[`RN Codegen TypeScript Parser can generate fixture CXX_ONLY_NATIVE_MODUL
'type': 'EnumDeclarationWithMembers',
'memberType': 'NumberTypeAnnotation',
'members': [
{
'name': 'Corrupted',
'value': -1
},
{
'name': 'Low',
'value': 720
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,52 @@ describe('TypeScript Module Parser', () => {
expect(parser).toThrow(UnnamedFunctionParamParserError);
});

it('should properly parse negative enums', () => {
const parser = () =>
parseModule(`
import type {TurboModule} from 'RCTExport';
import * as TurboModuleRegistry from 'TurboModuleRegistry';
enum MyEnum {
ZERO = 0,
POSITIVE = 1,
NEGATIVE = -1,
}
export interface Spec extends TurboModule {
useArg(arg: MyEnum): void;
}
export default TurboModuleRegistry.get<Spec>('Foo');
`);

expect(parser).not.toThrow();
expect(parser().enumMap.MyEnum.members).toEqual([
{name: 'ZERO', value: 0},
{name: 'POSITIVE', value: 1},
{name: 'NEGATIVE', value: -1},
]);
});

it('should properly parse enums', () => {
const parser = () =>
parseModule(`
import type {TurboModule} from 'RCTExport';
import * as TurboModuleRegistry from 'TurboModuleRegistry';
enum MyEnum {
ZERO = 0,
POSITIVE = 1,
}
export interface Spec extends TurboModule {
useArg(arg: MyEnum): void;
}
export default TurboModuleRegistry.get<Spec>('Foo');
`);

expect(parser).not.toThrow();
expect(parser().enumMap.MyEnum.members).toEqual([
{name: 'ZERO', value: 0},
{name: 'POSITIVE', value: 1},
]);
});

[
{nullable: false, optional: false},
{nullable: false, optional: true},
Expand Down
59 changes: 46 additions & 13 deletions packages/react-native-codegen/src/parsers/typescript/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,30 @@ class TypeScriptParser implements Parser {

parseEnumMembersType(typeAnnotation: $FlowFixMe): NativeModuleEnumMemberType {
const enumInitializer = typeAnnotation.members[0]?.initializer;
const enumMembersType: ?NativeModuleEnumMemberType =
!enumInitializer || enumInitializer.type === 'StringLiteral'
? 'StringTypeAnnotation'
: enumInitializer.type === 'NumericLiteral'
? 'NumberTypeAnnotation'
: null;
const enumInitializerType = enumInitializer?.type;

let enumMembersType: ?NativeModuleEnumMemberType = null;

if (!enumInitializerType) {
return 'StringTypeAnnotation';
}

switch (enumInitializerType) {
case 'StringLiteral':
enumMembersType = 'StringTypeAnnotation';
break;
case 'NumericLiteral':
enumMembersType = 'NumberTypeAnnotation';
break;
case 'UnaryExpression':
if (enumInitializer.operator === '-') {
enumMembersType = 'NumberTypeAnnotation';
}
break;
default:
enumMembersType = null;
}

if (!enumMembersType) {
throw new Error(
'Enum values must be either blank, number, or string values.',
Expand All @@ -213,9 +231,14 @@ class TypeScriptParser implements Parser {
: null;

typeAnnotation.members.forEach(member => {
if (
(member.initializer?.type ?? 'StringLiteral') !== enumInitializerType
) {
const isNegative =
member.initializer?.type === 'UnaryExpression' &&
member.initializer?.operator === '-';
const initializerType = isNegative
? member.initializer?.argument?.type
: member.initializer?.type;

if ((initializerType ?? 'StringLiteral') !== enumInitializerType) {
throw new Error(
'Enum values can not be mixed. They all must be either blank, number, or string values.',
);
Expand All @@ -226,10 +249,20 @@ class TypeScriptParser implements Parser {
parseEnumMembers(
typeAnnotation: $FlowFixMe,
): $ReadOnlyArray<NativeModuleEnumMember> {
return typeAnnotation.members.map(member => ({
name: member.id.name,
value: member.initializer?.value ?? member.id.name,
}));
return typeAnnotation.members.map(member => {
// Handle negative values
if (member.initializer?.operator === '-') {
return {
name: member.id.name,
value: -member.initializer?.argument?.value ?? member.id.name,
};
}

return {
name: member.id.name,
value: member.initializer?.value ?? member.id.name,
};
});
}

isModuleInterface(node: $FlowFixMe): boolean {
Expand Down

0 comments on commit 177bf4d

Please sign in to comment.