Skip to content

Commit 42be745

Browse files
sbaiahmed1meta-codesync[bot]
authored andcommitted
fix(codegen): infinite loop on type aliases that shadow CodegenTypes member names (#57986)
Summary: Fixes #57956. A spec containing a type alias whose name matches the aliased `CodegenTypes` member hangs codegen forever: ```js import type {CodegenTypes} from 'react-native'; type Double = CodegenTypes.Double; ``` `getResolvedTypeAnnotation` resolves `Double` to the alias's RHS (`CodegenTypes.Double`), but `getTypeAnnotationName` strips the namespace qualifier before the local-alias lookup — so the RHS resolves straight back to the same alias, forever. This hangs the Node process of anything driving the parser, most visibly ESLint via `react-native/eslint-plugin-specs`. Contrary to the issue's initial isolation, **both** the Flow and the TypeScript parsers hang (verified with a subprocess-timeout harness against both). Two guards in each parser's resolution loop: 1. A qualified name (`CodegenTypes.Double` / `TSQualifiedName`) names a namespace member, never a local alias — stop local resolution there. Downstream translation already handles qualified names, so the alias now resolves to the correct primitive (`DoubleTypeAnnotation`). 2. Track resolved alias names, so genuinely cyclic aliases (`type A = B; type B = A;`) stop resolving and fail through the existing `UnsupportedGenericParserError` (`Unrecognized generic type 'A'`) instead of hanging. ## Changelog: [GENERAL] [FIXED] - Codegen no longer hangs on type aliases that shadow CodegenTypes member names Pull Request resolved: #57986 Test Plan: - New `NAMESPACED_NATIVE_MODULE_WITH_LOCAL_TYPE_ALIASES` fixture in both the Flow and TypeScript module snapshot suites — before the fix these hang the test runner; after it they produce the correct schema (aliased `Double`/`Float` resolve to `DoubleTypeAnnotation`/`FloatTypeAnnotation`, snapshots included, cross-parser consistency suite passes). - `jest packages/react-native-codegen/src/parsers`: 13 suites, 1965 tests, 186 snapshots — all passing. - `jest packages/eslint-plugin-specs`: passing. - Verified `type A = B; type B = A;` now throws `UnsupportedGenericParserError` instead of hanging. Reviewed By: javache Differential Revision: D116386584 Pulled By: christophpurrer fbshipit-source-id: edbf04c8531d4dc35655318d6245918080ae55b7
1 parent 7844386 commit 42be745

6 files changed

Lines changed: 191 additions & 2 deletions

File tree

packages/react-native-codegen/src/parsers/flow/modules/__test_fixtures__/fixtures.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,34 @@ export default TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule');
946946
947947
`;
948948

949+
const NAMESPACED_NATIVE_MODULE_WITH_LOCAL_TYPE_ALIASES = `
950+
/**
951+
* Copyright (c) Meta Platforms, Inc. and affiliates.
952+
*
953+
* This source code is licensed under the MIT license found in the
954+
* LICENSE file in the root directory of this source tree.
955+
*
956+
* @flow strict-local
957+
* @format
958+
*/
959+
960+
'use strict';
961+
962+
import type {TurboModule} from '../RCTExport';
963+
import * as TurboModuleRegistry from '../TurboModuleRegistry';
964+
import type {CodegenTypes} from 'react-native';
965+
966+
type Double = CodegenTypes.Double;
967+
type MyFloat = CodegenTypes.Float;
968+
969+
export interface Spec extends TurboModule {
970+
+getDouble: (arg: Double) => Double;
971+
+getFloat: (arg: MyFloat) => MyFloat;
972+
}
973+
974+
export default TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule');
975+
`;
976+
949977
const NAMESPACED_NATIVE_MODULE_WITH_EVENT_EMITTERS = `
950978
/**
951979
* Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -1046,4 +1074,5 @@ module.exports = {
10461074
NAMESPACED_NATIVE_MODULE_WITH_FLOAT_AND_INT32,
10471075
NAMESPACED_NATIVE_MODULE_WITH_UNSAFE_OBJECT,
10481076
NAMESPACED_NATIVE_MODULE_WITH_EVENT_EMITTERS,
1077+
NAMESPACED_NATIVE_MODULE_WITH_LOCAL_TYPE_ALIASES,
10491078
};

packages/react-native-codegen/src/parsers/flow/modules/__tests__/__snapshots__/module-parser-snapshot-test.js.snap

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,62 @@ exports[`RN Codegen Flow Parser can generate fixture NAMESPACED_NATIVE_MODULE_WI
845845
}"
846846
`;
847847

848+
exports[`RN Codegen Flow Parser can generate fixture NAMESPACED_NATIVE_MODULE_WITH_LOCAL_TYPE_ALIASES 1`] = `
849+
"{
850+
'modules': {
851+
'NativeSampleTurboModule': {
852+
'type': 'NativeModule',
853+
'aliasMap': {},
854+
'enumMap': {},
855+
'spec': {
856+
'eventEmitters': [],
857+
'methods': [
858+
{
859+
'name': 'getDouble',
860+
'optional': false,
861+
'typeAnnotation': {
862+
'type': 'FunctionTypeAnnotation',
863+
'returnTypeAnnotation': {
864+
'type': 'DoubleTypeAnnotation'
865+
},
866+
'params': [
867+
{
868+
'name': 'arg',
869+
'optional': false,
870+
'typeAnnotation': {
871+
'type': 'DoubleTypeAnnotation'
872+
}
873+
}
874+
]
875+
}
876+
},
877+
{
878+
'name': 'getFloat',
879+
'optional': false,
880+
'typeAnnotation': {
881+
'type': 'FunctionTypeAnnotation',
882+
'returnTypeAnnotation': {
883+
'type': 'FloatTypeAnnotation'
884+
},
885+
'params': [
886+
{
887+
'name': 'arg',
888+
'optional': false,
889+
'typeAnnotation': {
890+
'type': 'FloatTypeAnnotation'
891+
}
892+
}
893+
]
894+
}
895+
}
896+
]
897+
},
898+
'moduleName': 'SampleTurboModule'
899+
}
900+
}
901+
}"
902+
`;
903+
848904
exports[`RN Codegen Flow Parser can generate fixture NAMESPACED_NATIVE_MODULE_WITH_UNSAFE_OBJECT 1`] = `
849905
"{
850906
'modules': {

packages/react-native-codegen/src/parsers/flow/parser.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ class FlowParser implements Parser {
418418
let typeResolutionStatus: TypeResolutionStatus = {
419419
successful: false,
420420
};
421+
const resolvedTypeAliases = new Set<string>();
421422

422423
for (;;) {
423424
if (node.type === 'NullableTypeAnnotation') {
@@ -430,11 +431,21 @@ class FlowParser implements Parser {
430431
break;
431432
}
432433

434+
// A qualified name (e.g. CodegenTypes.Double) refers to a namespace
435+
// member, not to the local type alias of the same unqualified name.
436+
if (node.id.type === 'QualifiedTypeIdentifier') {
437+
break;
438+
}
439+
433440
const typeAnnotationName = this.getTypeAnnotationName(node);
434441
const resolvedTypeAnnotation = types[typeAnnotationName];
435-
if (resolvedTypeAnnotation == null) {
442+
if (
443+
resolvedTypeAnnotation == null ||
444+
resolvedTypeAliases.has(typeAnnotationName)
445+
) {
436446
break;
437447
}
448+
resolvedTypeAliases.add(typeAnnotationName);
438449
const {typeAnnotation: typeAnnotationNode, typeResolutionStatus: status} =
439450
handleGenericTypeAnnotation(node, resolvedTypeAnnotation, this);
440451
typeResolutionStatus = status;

packages/react-native-codegen/src/parsers/typescript/modules/__test_fixtures__/fixtures.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,31 @@ export interface Spec extends TurboModule {
959959
export default TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule');
960960
`;
961961

962+
const NAMESPACED_NATIVE_MODULE_WITH_LOCAL_TYPE_ALIASES = `
963+
/**
964+
* Copyright (c) Meta Platforms, Inc. and affiliates.
965+
*
966+
* This source code is licensed under the MIT license found in the
967+
* LICENSE file in the root directory of this source tree.
968+
*
969+
* @format
970+
*/
971+
972+
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
973+
import type {CodegenTypes} from 'react-native';
974+
import * as TurboModuleRegistry from 'react-native/Libraries/TurboModule/TurboModuleRegistry';
975+
976+
type Double = CodegenTypes.Double;
977+
type MyFloat = CodegenTypes.Float;
978+
979+
export interface Spec extends TurboModule {
980+
readonly getDouble: (arg: Double) => Double;
981+
readonly getFloat: (arg: MyFloat) => MyFloat;
982+
}
983+
984+
export default TurboModuleRegistry.getEnforcing<Spec>('SampleTurboModule');
985+
`;
986+
962987
const NAMESPACED_NATIVE_MODULE_WITH_EVENT_EMITTERS = `
963988
/**
964989
* Copyright (c) Meta Platforms, Inc. and affiliates.
@@ -1057,4 +1082,5 @@ module.exports = {
10571082
NAMESPACED_NATIVE_MODULE_WITH_FLOAT_AND_INT32,
10581083
NAMESPACED_NATIVE_MODULE_WITH_UNSAFE_OBJECT,
10591084
NAMESPACED_NATIVE_MODULE_WITH_EVENT_EMITTERS,
1085+
NAMESPACED_NATIVE_MODULE_WITH_LOCAL_TYPE_ALIASES,
10601086
};

packages/react-native-codegen/src/parsers/typescript/modules/__tests__/__snapshots__/typescript-module-parser-snapshot-test.js.snap

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,62 @@ exports[`RN Codegen TypeScript Parser can generate fixture NAMESPACED_NATIVE_MOD
843843
}"
844844
`;
845845

846+
exports[`RN Codegen TypeScript Parser can generate fixture NAMESPACED_NATIVE_MODULE_WITH_LOCAL_TYPE_ALIASES 1`] = `
847+
"{
848+
'modules': {
849+
'NativeSampleTurboModule': {
850+
'type': 'NativeModule',
851+
'aliasMap': {},
852+
'enumMap': {},
853+
'spec': {
854+
'eventEmitters': [],
855+
'methods': [
856+
{
857+
'name': 'getDouble',
858+
'optional': false,
859+
'typeAnnotation': {
860+
'type': 'FunctionTypeAnnotation',
861+
'returnTypeAnnotation': {
862+
'type': 'DoubleTypeAnnotation'
863+
},
864+
'params': [
865+
{
866+
'name': 'arg',
867+
'optional': false,
868+
'typeAnnotation': {
869+
'type': 'DoubleTypeAnnotation'
870+
}
871+
}
872+
]
873+
}
874+
},
875+
{
876+
'name': 'getFloat',
877+
'optional': false,
878+
'typeAnnotation': {
879+
'type': 'FunctionTypeAnnotation',
880+
'returnTypeAnnotation': {
881+
'type': 'FloatTypeAnnotation'
882+
},
883+
'params': [
884+
{
885+
'name': 'arg',
886+
'optional': false,
887+
'typeAnnotation': {
888+
'type': 'FloatTypeAnnotation'
889+
}
890+
}
891+
]
892+
}
893+
}
894+
]
895+
},
896+
'moduleName': 'SampleTurboModule'
897+
}
898+
}
899+
}"
900+
`;
901+
846902
exports[`RN Codegen TypeScript Parser can generate fixture NAMESPACED_NATIVE_MODULE_WITH_UNSAFE_OBJECT 1`] = `
847903
"{
848904
'modules': {

packages/react-native-codegen/src/parsers/typescript/parser.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ class TypeScriptParser implements Parser {
455455
let typeResolutionStatus: TypeResolutionStatus = {
456456
successful: false,
457457
};
458+
const resolvedTypeAliases = new Set<string>();
458459

459460
for (;;) {
460461
const topLevelType = parseTopLevelType(node, parser);
@@ -465,11 +466,21 @@ class TypeScriptParser implements Parser {
465466
break;
466467
}
467468

469+
// A qualified name (e.g. CodegenTypes.Double) refers to a namespace
470+
// member, not to the local type alias of the same unqualified name.
471+
if (node.typeName.type === 'TSQualifiedName') {
472+
break;
473+
}
474+
468475
const typeAnnotationName = this.getTypeAnnotationName(node);
469476
const resolvedTypeAnnotation = types[typeAnnotationName];
470-
if (resolvedTypeAnnotation == null) {
477+
if (
478+
resolvedTypeAnnotation == null ||
479+
resolvedTypeAliases.has(typeAnnotationName)
480+
) {
471481
break;
472482
}
483+
resolvedTypeAliases.add(typeAnnotationName);
473484

474485
const {typeAnnotation: typeAnnotationNode, typeResolutionStatus: status} =
475486
handleGenericTypeAnnotation(node, resolvedTypeAnnotation, this);

0 commit comments

Comments
 (0)