mirror of
https://github.com/react/react-native.git
synced 2026-09-28 13:23:09 +08:00
Summary:
Fixes C++ API snapshot generation failures for nested enums and macOS/Linux output drift due to platform-dependent behavior, in the CXX API generator.
### Problems
1. Duplicate enums: Doxygen parses codegen `EventEmitters.h` twice (direct codegen input + again via `.mm` includes). The parser raised `RuntimeError: Identifier OnOrientationChangeOrientation already exists in scope ModalHostViewEventEmitter` on `ReactApple*` views.
2. Inconsistent behaviour on MacOS vs Linux:
- for codegen component aliases (`ConcreteComponentDescriptor`, `ConcreteViewShadowNode`), macOS Doxygen emits hybrid XML definitions (`typedef Type Name = Type`) while Linux CI emits `using Name = Type`. The parser keyed off `definition.startswith("typedef")`, so identical source produced different `.api` output per platform.
- `CASE_SENSE_NAMES = SYSTEM` follows the host OS default (case-insensitive on macOS, case-sensitive on Linux), causing inconsistent symbol resolution.
### Resolution
- `.doxygen.config.template` files: set `CASE_SENSE_NAMES = YES` for deterministic name matching across macOS and Linux.
- `snapshot.py`: `create_enum()` returns an existing enum scope instead of raising when the enum is already registered.
- `builders.py`: `create_enum_scope()` to skip enums that already exist; `get_typedef_member()` to normalize Doxygen’s `typedef ... = ...` form to `using` so the output format is unified.
## Changelog:
[INTERNAL] [FIXED] - Fix C++ API snapshot generation crash on duplicate codegen enums
[INTERNAL] [FIXED] - Fix platform-dependent inconsistent CXX API generator output
Pull Request resolved: https://github.com/react/react-native/pull/57536
Test Plan:
- [x] `yarn cxx-api-build` completes on macOS (tested locally)
- [x] `yarn cxx-api-validate` passes (tested locally on MacOS & on the Linux CI)
- [x] `validate-cxx-api-snapshots` passes on Linux (tested on CI)
Reviewed By: j-piasecki
Differential Revision: D112793182
Pulled By: coado
fbshipit-source-id: ada818451fb1207d965c1bed4fb5ce23ee2fe00f
228 lines
8.4 KiB
Python
228 lines
8.4 KiB
Python
# 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.
|
|
|
|
from __future__ import annotations
|
|
|
|
from .scope import (
|
|
CategoryScopeKind,
|
|
EnumScopeKind,
|
|
InterfaceScopeKind,
|
|
NamespaceScopeKind,
|
|
ProtocolScopeKind,
|
|
Scope,
|
|
StructLikeScopeKind,
|
|
TemporaryScopeKind,
|
|
)
|
|
from .utils import parse_qualified_path, split_specialization
|
|
|
|
|
|
class Snapshot:
|
|
def __init__(self) -> None:
|
|
self.root_scope: Scope = Scope(NamespaceScopeKind())
|
|
|
|
def ensure_scope(self, scope_path: list[str]) -> Scope:
|
|
"""
|
|
Ensure that a scope exists in the snapshot, creating it if necessary.
|
|
"""
|
|
current_scope = self.root_scope
|
|
for name in scope_path:
|
|
if name not in current_scope.inner_scopes:
|
|
new_scope = Scope(TemporaryScopeKind(), name)
|
|
new_scope.parent_scope = current_scope
|
|
current_scope.inner_scopes[name] = new_scope
|
|
current_scope = current_scope.inner_scopes[name]
|
|
return current_scope
|
|
|
|
def create_struct_like(
|
|
self, qualified_name: str, type: StructLikeScopeKind.Type
|
|
) -> Scope[StructLikeScopeKind]:
|
|
"""
|
|
Create a class in the snapshot.
|
|
"""
|
|
path = parse_qualified_path(qualified_name)
|
|
scope_path = path[0:-1]
|
|
scope_name = path[-1]
|
|
current_scope = self.ensure_scope(scope_path)
|
|
|
|
base_name, specialization_args = split_specialization(scope_name)
|
|
|
|
# Use the full name (including specialization) as the dict key so that
|
|
# base templates and their specializations are distinct entries.
|
|
scope_key = scope_name
|
|
|
|
if scope_key in current_scope.inner_scopes:
|
|
scope = current_scope.inner_scopes[scope_key]
|
|
if scope.kind.name == "temporary":
|
|
scope.kind = StructLikeScopeKind(type, specialization_args)
|
|
scope.name = base_name
|
|
else:
|
|
raise RuntimeError(
|
|
f"Identifier {scope_key} already exists in scope {current_scope.name}"
|
|
)
|
|
return scope
|
|
else:
|
|
new_scope = Scope(StructLikeScopeKind(type, specialization_args), base_name)
|
|
new_scope.parent_scope = current_scope
|
|
current_scope.inner_scopes[scope_key] = new_scope
|
|
return new_scope
|
|
|
|
def create_or_get_namespace(self, qualified_name: str) -> Scope[NamespaceScopeKind]:
|
|
"""
|
|
Create a namespace in the snapshot.
|
|
"""
|
|
path = parse_qualified_path(qualified_name)
|
|
scope_path = path[0:-1]
|
|
namespace_name = path[-1] if len(path) > 0 else None
|
|
current_scope = self.ensure_scope(scope_path)
|
|
|
|
if namespace_name in current_scope.inner_scopes:
|
|
scope = current_scope.inner_scopes[namespace_name]
|
|
if scope.kind.name == "temporary":
|
|
scope.kind = NamespaceScopeKind()
|
|
elif scope.kind.name == "namespace":
|
|
return scope
|
|
else:
|
|
raise RuntimeError(
|
|
f"Identifier {namespace_name} already exists in scope {current_scope.name} with kind {scope.kind.name}"
|
|
)
|
|
return scope
|
|
else:
|
|
new_scope = Scope(NamespaceScopeKind(), namespace_name)
|
|
new_scope.parent_scope = current_scope
|
|
current_scope.inner_scopes[namespace_name] = new_scope
|
|
return new_scope
|
|
|
|
def create_protocol(self, qualified_name: str) -> Scope[ProtocolScopeKind]:
|
|
"""
|
|
Create a protocol in the snapshot.
|
|
In Objective-C, a protocol and interface can share the same name,
|
|
so protocols are stored with a '-p' suffix to differentiate them.
|
|
"""
|
|
path = parse_qualified_path(qualified_name)
|
|
scope_path = path[0:-1]
|
|
scope_name = path[-1]
|
|
# Use '-p' suffix to allow protocols to coexist with interfaces of the same name
|
|
scope_key = f"{scope_name}-p"
|
|
current_scope = self.ensure_scope(scope_path)
|
|
|
|
if scope_key in current_scope.inner_scopes:
|
|
scope = current_scope.inner_scopes[scope_key]
|
|
if scope.kind.name == "temporary":
|
|
scope.kind = ProtocolScopeKind()
|
|
else:
|
|
raise RuntimeError(
|
|
f"Identifier {scope_name} already exists in scope {current_scope.name}"
|
|
)
|
|
return scope
|
|
else:
|
|
new_scope = Scope(ProtocolScopeKind(), scope_name)
|
|
new_scope.parent_scope = current_scope
|
|
current_scope.inner_scopes[scope_key] = new_scope
|
|
return new_scope
|
|
|
|
def create_interface(self, qualified_name: str) -> Scope[InterfaceScopeKind]:
|
|
"""
|
|
Create an interface in the snapshot.
|
|
"""
|
|
path = parse_qualified_path(qualified_name)
|
|
scope_path = path[0:-1]
|
|
scope_name = path[-1]
|
|
current_scope = self.ensure_scope(scope_path)
|
|
|
|
if scope_name in current_scope.inner_scopes:
|
|
scope = current_scope.inner_scopes[scope_name]
|
|
if scope.kind.name == "temporary":
|
|
scope.kind = InterfaceScopeKind()
|
|
else:
|
|
raise RuntimeError(
|
|
f"Identifier {scope_name} already exists in scope {current_scope.name}"
|
|
)
|
|
return scope
|
|
else:
|
|
new_scope = Scope(InterfaceScopeKind(), scope_name)
|
|
new_scope.parent_scope = current_scope
|
|
current_scope.inner_scopes[scope_name] = new_scope
|
|
return new_scope
|
|
|
|
def create_category(
|
|
self, class_name: str, category_name: str
|
|
) -> Scope[CategoryScopeKind]:
|
|
"""
|
|
Create a category in the snapshot.
|
|
Categories are stored with a unique key: "ClassName(CategoryName)"
|
|
"""
|
|
scope_key = f"{class_name}({category_name})"
|
|
current_scope = self.root_scope
|
|
|
|
if scope_key in current_scope.inner_scopes:
|
|
scope = current_scope.inner_scopes[scope_key]
|
|
if scope.kind.name == "temporary":
|
|
scope.kind = CategoryScopeKind(class_name, category_name)
|
|
else:
|
|
raise RuntimeError(
|
|
f"Identifier {scope_key} already exists in scope {current_scope.name}"
|
|
)
|
|
return scope
|
|
else:
|
|
new_scope = Scope(CategoryScopeKind(class_name, category_name), scope_key)
|
|
new_scope.parent_scope = current_scope
|
|
current_scope.inner_scopes[scope_key] = new_scope
|
|
return new_scope
|
|
|
|
def create_enum(self, qualified_name: str) -> Scope[EnumScopeKind]:
|
|
"""
|
|
Create an enum in the snapshot.
|
|
"""
|
|
path = parse_qualified_path(qualified_name)
|
|
scope_path = path[0:-1]
|
|
enum_name = path[-1]
|
|
current_scope = self.ensure_scope(scope_path)
|
|
|
|
if enum_name in current_scope.inner_scopes:
|
|
scope = current_scope.inner_scopes[enum_name]
|
|
if scope.kind.name == "temporary":
|
|
scope.kind = EnumScopeKind()
|
|
elif scope.kind.name == "enum":
|
|
return scope
|
|
else:
|
|
raise RuntimeError(
|
|
f"Identifier {enum_name} already exists in scope {current_scope.name}"
|
|
)
|
|
return scope
|
|
else:
|
|
new_scope = Scope(EnumScopeKind(), enum_name)
|
|
new_scope.parent_scope = current_scope
|
|
current_scope.inner_scopes[enum_name] = new_scope
|
|
return new_scope
|
|
|
|
def _ensure_scope_is_defined(self, scope: Scope) -> None:
|
|
"""
|
|
Ensure that a scope is defined in the snapshot.
|
|
"""
|
|
if scope.kind.name == "temporary":
|
|
raise RuntimeError(f"Scope {scope.name} is not defined in the snapshot")
|
|
|
|
for inner_scope in scope.inner_scopes.values():
|
|
self._ensure_scope_is_defined(inner_scope)
|
|
|
|
def finish(self) -> None:
|
|
"""
|
|
Finish the snapshot by setting the kind of all temporary scopes.
|
|
"""
|
|
self._ensure_scope_is_defined(self.root_scope)
|
|
self.root_scope.close()
|
|
|
|
def to_string(self) -> str:
|
|
"""
|
|
Get the string representation of the snapshot.
|
|
"""
|
|
return self.root_scope.to_string()
|
|
|
|
def print(self) -> None:
|
|
"""
|
|
Print the snapshot.
|
|
"""
|
|
self.root_scope.print()
|