From cb96485f0d97ab0263272884ad05447fbd7963ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Thu, 5 Mar 2026 02:24:50 -0800 Subject: [PATCH 1/5] Fix parsing of Objective-C interface generic inheritance (#55775) Summary: Doxygen incorrectly parses Objective-C interface declarations with protocol conformance. For example: ```objc interface RCTAppearance : RCTEventEmitter ``` Doxygen splits this into **two separate base classes** in the XML: ```xml RCTEventEmitter <RCTBridgeModule> ``` This caused the parser to output: ``` interface RCTAppearance : public RCTEventEmitter, public { ``` Instead of the expected: ``` interface RCTAppearance : public RCTEventEmitter { ``` The fix detects when a "base class" name starts and ends with `<...>` (indicating it's a protocol conformance) and combines it with the preceding actual base class name. Also for multiple generics like: ``` interface RCTAlertManager : NSObject end ``` The output should be ``` interface RCTAlertManager : public NSObject end ``` Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D94351731 --- scripts/cxx-api/parser/builders.py | 19 ++++++++++++++++++- .../snapshot.api | 3 +++ .../test.h | 16 ++++++++++++++++ .../snapshot.api | 2 ++ .../test.h | 16 ++++++++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_class_with_generic_inheritance/snapshot.api create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_class_with_generic_inheritance/test.h create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_class_with_multiple_protocol_conformances/snapshot.api create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_class_with_multiple_protocol_conformances/test.h diff --git a/scripts/cxx-api/parser/builders.py b/scripts/cxx-api/parser/builders.py index 85d11f7ccc63..3361c25a35d7 100644 --- a/scripts/cxx-api/parser/builders.py +++ b/scripts/cxx-api/parser/builders.py @@ -465,7 +465,24 @@ def create_interface_scope( interface_scope = snapshot.create_interface(interface_name) base_classes = get_base_classes(scope_def, base_class=InterfaceScopeKind.Base) - interface_scope.kind.add_base(base_classes) + + # Doxygen incorrectly splits "Foo " into separate base classes: + # "Foo", "", "". Combine them back into "Foo ". + combined_bases = [] + for base in base_classes: + if base.name.startswith("<") and base.name.endswith(">") and combined_bases: + prev_name = combined_bases[-1].name + protocol = base.name[1:-1] # Strip < and > + if "<" in prev_name and prev_name.endswith(">"): + # Previous base already has protocols, merge inside the brackets + combined_bases[-1].name = f"{prev_name[:-1]}, {protocol}>" + else: + # First protocol for this base class + combined_bases[-1].name = f"{prev_name} <{protocol}>" + else: + combined_bases.append(base) + + interface_scope.kind.add_base(combined_bases) interface_scope.location = scope_def.location.file _process_objc_sections( diff --git a/scripts/cxx-api/tests/snapshots/should_handle_class_with_generic_inheritance/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_class_with_generic_inheritance/snapshot.api new file mode 100644 index 000000000000..e10fec618c71 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_class_with_generic_inheritance/snapshot.api @@ -0,0 +1,3 @@ +interface RCTAppearance : public RCTEventEmitter { + public virtual instancetype init(); +} diff --git a/scripts/cxx-api/tests/snapshots/should_handle_class_with_generic_inheritance/test.h b/scripts/cxx-api/tests/snapshots/should_handle_class_with_generic_inheritance/test.h new file mode 100644 index 000000000000..dc261f9b5083 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_class_with_generic_inheritance/test.h @@ -0,0 +1,16 @@ +/* + * 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. + */ + +#pragma once + +namespace test { + +@interface RCTAppearance : RCTEventEmitter +- (instancetype)init; +@end + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_handle_class_with_multiple_protocol_conformances/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_class_with_multiple_protocol_conformances/snapshot.api new file mode 100644 index 000000000000..f66dd58f6c0e --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_class_with_multiple_protocol_conformances/snapshot.api @@ -0,0 +1,2 @@ +interface RCTAlertManager : public NSObject { +} diff --git a/scripts/cxx-api/tests/snapshots/should_handle_class_with_multiple_protocol_conformances/test.h b/scripts/cxx-api/tests/snapshots/should_handle_class_with_multiple_protocol_conformances/test.h new file mode 100644 index 000000000000..02d7e217d22b --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_class_with_multiple_protocol_conformances/test.h @@ -0,0 +1,16 @@ +/* + * 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. + */ + +#pragma once + +namespace test { + +@interface RCTAlertManager : NSObject + +@end + +} // namespace test From 9c87471d70eccff1ccdf25e7430072c6653cf0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Thu, 5 Mar 2026 02:24:50 -0800 Subject: [PATCH 2/5] Fix parsing of Objective-C block properties (#55776) Summary: Doxygen splits block property types across `` and `` elements. For example: ```objc property (nonatomic, copy) void (^eventInterceptor)(NSString *eventName, NSDictionary *event, NSNumber *reactTag); ``` Produces XML like: ```xml void(^ eventInterceptor )(NSString *eventName, NSDictionary *event, NSNumber *reactTag) ``` This caused the parser to output incomplete types. The fix detects when the property type ends with `(^` and combines it with the property name and argsstring: ``` property (copy) void(^eventInterceptor)(NSString *eventName, NSDictionary *event, NSNumber *reactTag); ``` Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D94366205 --- scripts/cxx-api/parser/builders.py | 10 ++++++++++ scripts/cxx-api/parser/member.py | 6 +++++- .../snapshot.api | 5 +++++ .../test.h | 15 +++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_interface_with_block_property/snapshot.api create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_interface_with_block_property/test.h diff --git a/scripts/cxx-api/parser/builders.py b/scripts/cxx-api/parser/builders.py index 3361c25a35d7..4dbd7466596f 100644 --- a/scripts/cxx-api/parser/builders.py +++ b/scripts/cxx-api/parser/builders.py @@ -337,6 +337,16 @@ def get_property_member( is_readable = getattr(member_def, "readable", "no") == "yes" is_writable = getattr(member_def, "writable", "no") == "yes" + # Handle block properties: Doxygen splits the block type across and + # = "void(^" + # = ")(NSString *eventName, NSDictionary *event, NSNumber *reactTag)" + # We need to combine them: "void(^eventInterceptor)(NSString *, NSDictionary *, NSNumber *)" + if property_type.endswith("(^"): + argsstring = member_def.get_argsstring() + if argsstring: + property_type = f"{property_type}{property_name}{argsstring}" + property_name = "" + return PropertyMember( property_name, property_type, diff --git a/scripts/cxx-api/parser/member.py b/scripts/cxx-api/parser/member.py index 407fa078aecf..2069aa80a84e 100644 --- a/scripts/cxx-api/parser/member.py +++ b/scripts/cxx-api/parser/member.py @@ -404,7 +404,11 @@ def to_string( if self.is_static: result += "static " - result += f"@property {attrs_str}{self.type} {name};" + # For block properties, name is embedded in the type (e.g., "void(^eventInterceptor)(args)") + if name: + result += f"@property {attrs_str}{self.type} {name};" + else: + result += f"@property {attrs_str}{self.type};" return result diff --git a/scripts/cxx-api/tests/snapshots/should_handle_interface_with_block_property/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_interface_with_block_property/snapshot.api new file mode 100644 index 000000000000..01e1b9d3f9cc --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_interface_with_block_property/snapshot.api @@ -0,0 +1,5 @@ +interface RCTInterfaceWithBlockProperty : public NSObject { + public @property (copy) NSString *(^blockWithReturn)(int value); + public @property (copy) void(^eventInterceptor)(NSString *eventName, NSDictionary *event, NSNumber *reactTag); + public @property (copy) void(^simpleBlock)(void); +} diff --git a/scripts/cxx-api/tests/snapshots/should_handle_interface_with_block_property/test.h b/scripts/cxx-api/tests/snapshots/should_handle_interface_with_block_property/test.h new file mode 100644 index 000000000000..e9fc1c8b9658 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_interface_with_block_property/test.h @@ -0,0 +1,15 @@ +/* + * 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. + */ + +@interface RCTInterfaceWithBlockProperty : NSObject + +@property (nonatomic, copy, nullable) void (^eventInterceptor) + (NSString *eventName, NSDictionary *event, NSNumber *reactTag); +@property (nonatomic, copy) void (^simpleBlock)(void); +@property (nonatomic, copy) NSString * (^blockWithReturn)(int value); + +@end From 6a53369bf817cb1234179e0444b79ee0d522bc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Thu, 5 Mar 2026 02:24:50 -0800 Subject: [PATCH 3/5] Remove `__deprecated_msg` from the snapshot (#55780) Summary: Set predefined `__deprecated_msg` to empty string in the `.doxygen.config.template` as doxygen has problem with parsing and produces malformed xml. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D94517576 --- .../react-native/.doxygen.config.template | 2 +- .../manual_test/.doxygen.config.template | 2 +- .../tests/snapshots/.doxygen.config.template | 2 +- .../should_handle_deprecated_msg/snapshot.api | 16 +++++++++ .../should_handle_deprecated_msg/test.h | 34 +++++++++++++++++++ 5 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_deprecated_msg/snapshot.api create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_deprecated_msg/test.h diff --git a/packages/react-native/.doxygen.config.template b/packages/react-native/.doxygen.config.template index 72db8de8be90..cd4047ae375f 100644 --- a/packages/react-native/.doxygen.config.template +++ b/packages/react-native/.doxygen.config.template @@ -2443,7 +2443,7 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = FOLLY_PACK_PUSH="" FOLLY_PACK_POP="" FOLLY_PACK_ATTR="" __attribute__(x)="" ${PREDEFINED} +PREDEFINED = FOLLY_PACK_PUSH="" FOLLY_PACK_POP="" FOLLY_PACK_ATTR="" __attribute__(x)="" __deprecated_msg(x)="" ${PREDEFINED} # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/scripts/cxx-api/manual_test/.doxygen.config.template b/scripts/cxx-api/manual_test/.doxygen.config.template index 80290595ec93..6f762bb4b57d 100644 --- a/scripts/cxx-api/manual_test/.doxygen.config.template +++ b/scripts/cxx-api/manual_test/.doxygen.config.template @@ -2442,7 +2442,7 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = FOLLY_PACK_PUSH="" FOLLY_PACK_POP="" FOLLY_PACK_ATTR="" __attribute__(x)="" ${PREDEFINED} +PREDEFINED = FOLLY_PACK_PUSH="" FOLLY_PACK_POP="" FOLLY_PACK_ATTR="" __attribute__(x)="" __deprecated_msg(x)="" ${PREDEFINED} # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/scripts/cxx-api/tests/snapshots/.doxygen.config.template b/scripts/cxx-api/tests/snapshots/.doxygen.config.template index a01a146395a5..13cbc7b57375 100644 --- a/scripts/cxx-api/tests/snapshots/.doxygen.config.template +++ b/scripts/cxx-api/tests/snapshots/.doxygen.config.template @@ -2442,7 +2442,7 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = FOLLY_PACK_PUSH="" FOLLY_PACK_POP="" FOLLY_PACK_ATTR="" __attribute__(x)="" +PREDEFINED = FOLLY_PACK_PUSH="" FOLLY_PACK_POP="" FOLLY_PACK_ATTR="" __attribute__(x)="" __deprecated_msg(x)="" # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/scripts/cxx-api/tests/snapshots/should_handle_deprecated_msg/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_deprecated_msg/snapshot.api new file mode 100644 index 000000000000..65e0334c1d09 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_deprecated_msg/snapshot.api @@ -0,0 +1,16 @@ +interface RCTDeprecatedInterface { + public virtual void customBubblingEventTypes(); + public virtual void legacyTitle(); + public virtual void normalMethod(); + public virtual void oldMethod(); +} + +interface RCTTestInterface { + public virtual NSArray * deprecatedMethod:(id param); + public virtual void normalMethod:(NSString * name); +} + +protocol RCTDeprecatedProtocol { + public virtual void deprecatedProtocolMethod(); + public virtual void normalProtocolMethod(); +} diff --git a/scripts/cxx-api/tests/snapshots/should_handle_deprecated_msg/test.h b/scripts/cxx-api/tests/snapshots/should_handle_deprecated_msg/test.h new file mode 100644 index 000000000000..b3d47328979e --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_deprecated_msg/test.h @@ -0,0 +1,34 @@ +/* + * 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. + */ + +@interface RCTTestInterface + +- (void)normalMethod:(NSString *)name; + +- (NSArray *)deprecatedMethod:(id)param __deprecated_msg("Use newMethod instead."); + +@end + +@interface RCTDeprecatedInterface + +- (void)normalMethod; + +- (void)oldMethod __deprecated_msg("Use newMethod instead."); + +- (void)customBubblingEventTypes __deprecated_msg("Use RCTBubblingEventBlock props instead."); + +- (void)legacyTitle __deprecated_msg("This API will be removed along with the legacy architecture."); + +@end + +@protocol RCTDeprecatedProtocol + +- (void)normalProtocolMethod; + +- (void)deprecatedProtocolMethod __deprecated_msg("Protocol method is deprecated."); + +@end From 64f8274da26708a5dbf5cc04255c4e43d54d3c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Thu, 5 Mar 2026 02:24:50 -0800 Subject: [PATCH 4/5] Set more predefined macros in the doxygen config. (#55900) Summary: Doxygen incorrectly parses macros like `NS_DESIGNATED_INITIALIZER`, `RCT_EXTERN`, `RCT_DEPRECATED`, `RCT_EXTERN_MODULE`, `API_AVAILABLE`. Setting them them as predefined empty string solves the issue. Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D94534964 --- .../react-native/.doxygen.config.template | 16 ++++++++- .../manual_test/.doxygen.config.template | 15 +++++++- .../tests/snapshots/.doxygen.config.template | 14 +++++++- .../should_strip_objc_macros/snapshot.api | 14 ++++++++ .../snapshots/should_strip_objc_macros/test.h | 36 +++++++++++++++++++ 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 scripts/cxx-api/tests/snapshots/should_strip_objc_macros/snapshot.api create mode 100644 scripts/cxx-api/tests/snapshots/should_strip_objc_macros/test.h diff --git a/packages/react-native/.doxygen.config.template b/packages/react-native/.doxygen.config.template index cd4047ae375f..d7f5cd197aa9 100644 --- a/packages/react-native/.doxygen.config.template +++ b/packages/react-native/.doxygen.config.template @@ -2443,7 +2443,21 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = FOLLY_PACK_PUSH="" FOLLY_PACK_POP="" FOLLY_PACK_ATTR="" __attribute__(x)="" __deprecated_msg(x)="" ${PREDEFINED} +PREDEFINED = FOLLY_PACK_PUSH="" \ + FOLLY_PACK_POP="" \ + FOLLY_PACK_ATTR="" \ + __attribute__(x)="" \ + __deprecated_msg(x)="" \ + NS_REQUIRES_SUPER="" \ + NS_UNAVAILABLE="" \ + CF_RETURNS_NOT_RETAINED="" \ + NS_DESIGNATED_INITIALIZER="" \ + NS_DESIGNATED_INITIALIZER="" \ + RCT_EXTERN="" \ + RCT_DEPRECATED="" \ + RCT_EXTERN_MODULE="" \ + API_AVAILABLE(x)="" \ + ${PREDEFINED} # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/scripts/cxx-api/manual_test/.doxygen.config.template b/scripts/cxx-api/manual_test/.doxygen.config.template index 6f762bb4b57d..15952235cae3 100644 --- a/scripts/cxx-api/manual_test/.doxygen.config.template +++ b/scripts/cxx-api/manual_test/.doxygen.config.template @@ -2442,7 +2442,20 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = FOLLY_PACK_PUSH="" FOLLY_PACK_POP="" FOLLY_PACK_ATTR="" __attribute__(x)="" __deprecated_msg(x)="" ${PREDEFINED} +PREDEFINED = FOLLY_PACK_PUSH="" \ + FOLLY_PACK_POP="" \ + FOLLY_PACK_ATTR="" \ + __attribute__(x)="" \ + __deprecated_msg(x)="" \ + NS_REQUIRES_SUPER="" \ + NS_UNAVAILABLE="" \ + CF_RETURNS_NOT_RETAINED="" \ + NS_DESIGNATED_INITIALIZER="" \ + RCT_EXTERN="" \ + RCT_DEPRECATED="" \ + RCT_EXTERN_MODULE="" \ + API_AVAILABLE(x)="" \ + ${PREDEFINED} # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/scripts/cxx-api/tests/snapshots/.doxygen.config.template b/scripts/cxx-api/tests/snapshots/.doxygen.config.template index 13cbc7b57375..50d418c394f7 100644 --- a/scripts/cxx-api/tests/snapshots/.doxygen.config.template +++ b/scripts/cxx-api/tests/snapshots/.doxygen.config.template @@ -2442,7 +2442,19 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = FOLLY_PACK_PUSH="" FOLLY_PACK_POP="" FOLLY_PACK_ATTR="" __attribute__(x)="" __deprecated_msg(x)="" +PREDEFINED = FOLLY_PACK_PUSH="" \ + FOLLY_PACK_POP="" \ + FOLLY_PACK_ATTR="" \ + __attribute__(x)="" \ + __deprecated_msg(x)="" \ + NS_REQUIRES_SUPER="" \ + NS_UNAVAILABLE="" \ + CF_RETURNS_NOT_RETAINED="" \ + NS_DESIGNATED_INITIALIZER="" \ + RCT_EXTERN="" \ + RCT_DEPRECATED="" \ + RCT_EXTERN_MODULE="" \ + API_AVAILABLE(x)="" # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/scripts/cxx-api/tests/snapshots/should_strip_objc_macros/snapshot.api b/scripts/cxx-api/tests/snapshots/should_strip_objc_macros/snapshot.api new file mode 100644 index 000000000000..b0c18447476a --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_strip_objc_macros/snapshot.api @@ -0,0 +1,14 @@ +interface RCTTestMacros { + public @property (strong, readonly) dispatch_queue_t methodQueue; + public @property (weak, readonly) id bridge; + public virtual instancetype initWithDelegate:options:(id delegate, NSDictionary * options); + public virtual instancetype initWithName:(NSString * name); + public virtual static UIUserInterfaceStyle userInterfaceStyle(); + public virtual void deprecatedMethod(); +} + +protocol RCTTestProtocol { + public @property (assign, readonly) NSString * name; + public virtual void normalMethod(); + public virtual void requiredMethod(); +} diff --git a/scripts/cxx-api/tests/snapshots/should_strip_objc_macros/test.h b/scripts/cxx-api/tests/snapshots/should_strip_objc_macros/test.h new file mode 100644 index 000000000000..d03ef29db63b --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_strip_objc_macros/test.h @@ -0,0 +1,36 @@ +/* + * 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. + */ + +@interface RCTTestMacros + +- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDelegate:(id)delegate options:(NSDictionary *)options NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, strong, readonly) dispatch_queue_t methodQueue RCT_DEPRECATED; + +@property (nonatomic, weak, readonly) id bridge RCT_DEPRECATED; + +- (void)deprecatedMethod RCT_DEPRECATED; + ++ (UIUserInterfaceStyle)userInterfaceStyle API_AVAILABLE(ios(12)); + +@end + +RCT_EXTERN void RCTExternFunction(const char *input, NSString **output); + +RCT_EXTERN NSString *RCTParseType(const char **input); + +@protocol RCTTestProtocol + +- (void)normalMethod; + +- (void)requiredMethod NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, readonly) NSString *name RCT_DEPRECATED; + +@end From 1af6f3fbcdd3cabf6d969700caaf01c40f5de6e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Thu, 5 Mar 2026 02:24:50 -0800 Subject: [PATCH 5/5] Add doxygen input filter to strip block comments (#55888) Summary: Adds a doxygen input filter (`input_filters/doxygen_strip_comments.py`) that strips block comments from source files before doxygen parses them. This prevents doxygen from incorrectly parsing Objective-C code examples (like `interface`, `protocol`) within documentation comments as actual code declarations. For example, a doc comment containing: ``` /** * Example: * interface RCT_EXTERN_MODULE(MyModule, NSObject) * end */ ``` Was being parsed by doxygen as an actual interface declaration, resulting in malformed output like `interface RCT_EXTERN_MODULE {}` in the API snapshot. The filter preserves line numbers by replacing comment content with the equivalent number of newlines, ensuring error messages remain accurate. Changelog: [Internal] Reviewed By: cortinico Differential Revision: D94538938 --- .../react-native/.doxygen.config.template | 2 +- .../input_filters/doxygen_strip_comments.py | 60 ++++++++++++++++ .../manual_test/.doxygen.config.template | 2 +- scripts/cxx-api/parser/__main__.py | 22 ++++-- .../tests/snapshots/.doxygen.config.template | 2 +- .../snapshot.api | 3 + .../test.h | 18 +++++ scripts/cxx-api/tests/test_input_filters.py | 71 +++++++++++++++++++ scripts/cxx-api/tests/test_snapshots.py | 31 ++++++-- 9 files changed, 197 insertions(+), 14 deletions(-) create mode 100755 scripts/cxx-api/input_filters/doxygen_strip_comments.py create mode 100644 scripts/cxx-api/tests/snapshots/should_not_parse_interface_from_doc_comment/snapshot.api create mode 100644 scripts/cxx-api/tests/snapshots/should_not_parse_interface_from_doc_comment/test.h create mode 100644 scripts/cxx-api/tests/test_input_filters.py diff --git a/packages/react-native/.doxygen.config.template b/packages/react-native/.doxygen.config.template index d7f5cd197aa9..4f982c734bc2 100644 --- a/packages/react-native/.doxygen.config.template +++ b/packages/react-native/.doxygen.config.template @@ -1123,7 +1123,7 @@ IMAGE_PATH = # need to set EXTENSION_MAPPING for the extension otherwise the files are not # properly processed by Doxygen. -INPUT_FILTER = +INPUT_FILTER = ${DOXYGEN_INPUT_FILTER} # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # basis. Doxygen will compare the file name with each pattern and apply the diff --git a/scripts/cxx-api/input_filters/doxygen_strip_comments.py b/scripts/cxx-api/input_filters/doxygen_strip_comments.py new file mode 100755 index 000000000000..d379f7d1c9d2 --- /dev/null +++ b/scripts/cxx-api/input_filters/doxygen_strip_comments.py @@ -0,0 +1,60 @@ +#!/usr/bin/env fbpython +# 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. + +""" +Doxygen input filter to strip block comments from source files. + +This prevents Doxygen from incorrectly parsing code examples within +documentation comments as actual code declarations (e.g., @interface, +@protocol examples in doc comments being parsed as real interfaces). + +Usage in doxygen config: + INPUT_FILTER = "python3 /path/to/doxygen_strip_comments.py" +""" + +import re +import sys + + +def strip_block_comments(content: str) -> str: + """ + Remove all block comments (/* ... */ and /** ... */) from content. + Preserves line count by replacing comment content with newlines. + """ + + def replace_with_newlines(match: re.Match) -> str: + # Count newlines in original comment to preserve line numbers + newline_count = match.group().count("\n") + return "\n" * newline_count + + # Pattern to match block comments (non-greedy) + comment_pattern = re.compile(r"/\*[\s\S]*?\*/") + + return comment_pattern.sub(replace_with_newlines, content) + + +def main(): + if len(sys.argv) < 2: + print("Usage: doxygen_strip_comments.py ", file=sys.stderr) + sys.exit(1) + + filename = sys.argv[1] + + try: + with open(filename, "r", encoding="utf-8", errors="replace") as f: + content = f.read() + + filtered = strip_block_comments(content) + print(filtered, end="") + except Exception as e: + # On error, output original content to not break the build + print(f"Warning: Filter error for {filename}: {e}", file=sys.stderr) + with open(filename, "r", encoding="utf-8", errors="replace") as f: + print(f.read(), end="") + + +if __name__ == "__main__": + main() diff --git a/scripts/cxx-api/manual_test/.doxygen.config.template b/scripts/cxx-api/manual_test/.doxygen.config.template index 15952235cae3..cac0975942a6 100644 --- a/scripts/cxx-api/manual_test/.doxygen.config.template +++ b/scripts/cxx-api/manual_test/.doxygen.config.template @@ -1122,7 +1122,7 @@ IMAGE_PATH = # need to set EXTENSION_MAPPING for the extension otherwise the files are not # properly processed by Doxygen. -INPUT_FILTER = +INPUT_FILTER = ${DOXYGEN_INPUT_FILTER} # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # basis. Doxygen will compare the file name with each pattern and apply the diff --git a/scripts/cxx-api/parser/__main__.py b/scripts/cxx-api/parser/__main__.py index 96673ad2dfe5..2d7a8cfd52c0 100644 --- a/scripts/cxx-api/parser/__main__.py +++ b/scripts/cxx-api/parser/__main__.py @@ -33,6 +33,7 @@ def build_doxygen_config( include_directories: list[str] = None, exclude_patterns: list[str] = None, definitions: dict[str, str | int] = None, + input_filter: str = None, ) -> None: if include_directories is None: include_directories = [] @@ -53,6 +54,8 @@ def build_doxygen_config( ] ) + input_filter_str = input_filter if input_filter else "" + # read the template file with open(os.path.join(directory, ".doxygen.config.template")) as f: template = f.read() @@ -62,6 +65,7 @@ def build_doxygen_config( template.replace("${INPUTS}", include_directories_str) .replace("${EXCLUDE_PATTERNS}", exclude_patterns_str) .replace("${PREDEFINED}", definitions_str) + .replace("${DOXYGEN_INPUT_FILTER}", input_filter_str) ) # write the config file @@ -77,6 +81,7 @@ def build_snapshot_for_view( definitions: dict[str, str | int], output_dir: str, verbose: bool = True, + input_filter: str = None, ) -> None: if verbose: print(f"Generating API view: {api_view}") @@ -87,6 +92,7 @@ def build_snapshot_for_view( include_directories=include_directories, exclude_patterns=exclude_patterns, definitions=definitions, + input_filter=input_filter, ) # If there is already a doxygen output directory, delete it @@ -188,6 +194,17 @@ def main(): if verbose and args.codegen_path: print(f"Codegen output path: {os.path.abspath(args.codegen_path)}") + input_filter_path = os.path.join( + get_react_native_dir(), + "scripts", + "cxx-api", + "input_filters", + "doxygen_strip_comments.py", + ) + input_filter = None + if os.path.exists(input_filter_path): + input_filter = f"python3 {input_filter_path}" + # Parse config file config_path = os.path.join( get_react_native_dir(), "scripts", "cxx-api", "config.yml" @@ -219,6 +236,7 @@ def build_snapshots(output_dir: str, verbose: bool) -> None: definitions={}, output_dir=output_dir, verbose=verbose, + input_filter=input_filter, ) if verbose: @@ -245,7 +263,3 @@ def build_snapshots(output_dir: str, verbose: bool) -> None: ) ) build_snapshots(output_dir, verbose=True) - - -if __name__ == "__main__": - main() diff --git a/scripts/cxx-api/tests/snapshots/.doxygen.config.template b/scripts/cxx-api/tests/snapshots/.doxygen.config.template index 50d418c394f7..f5f568360014 100644 --- a/scripts/cxx-api/tests/snapshots/.doxygen.config.template +++ b/scripts/cxx-api/tests/snapshots/.doxygen.config.template @@ -1122,7 +1122,7 @@ IMAGE_PATH = # need to set EXTENSION_MAPPING for the extension otherwise the files are not # properly processed by Doxygen. -INPUT_FILTER = +INPUT_FILTER = ${DOXYGEN_INPUT_FILTER} # The FILTER_PATTERNS tag can be used to specify filters on a per file pattern # basis. Doxygen will compare the file name with each pattern and apply the diff --git a/scripts/cxx-api/tests/snapshots/should_not_parse_interface_from_doc_comment/snapshot.api b/scripts/cxx-api/tests/snapshots/should_not_parse_interface_from_doc_comment/snapshot.api new file mode 100644 index 000000000000..e5a54251d3c4 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_not_parse_interface_from_doc_comment/snapshot.api @@ -0,0 +1,3 @@ +interface RCTRealInterface { + public virtual void realMethod(); +} diff --git a/scripts/cxx-api/tests/snapshots/should_not_parse_interface_from_doc_comment/test.h b/scripts/cxx-api/tests/snapshots/should_not_parse_interface_from_doc_comment/test.h new file mode 100644 index 000000000000..3ffcc2f84748 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_not_parse_interface_from_doc_comment/test.h @@ -0,0 +1,18 @@ +/* + * 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. + */ + +/** + * Example of how to use this module: + * + * @interface RCT_EXTERN_MODULE(MyModule, NSObject) + * @end + */ +@interface RCTRealInterface + +- (void)realMethod; + +@end diff --git a/scripts/cxx-api/tests/test_input_filters.py b/scripts/cxx-api/tests/test_input_filters.py new file mode 100644 index 000000000000..2aa3bf50881d --- /dev/null +++ b/scripts/cxx-api/tests/test_input_filters.py @@ -0,0 +1,71 @@ +# 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 + +import unittest + +from ..input_filters.doxygen_strip_comments import strip_block_comments + + +class TestDoxygenStripComments(unittest.TestCase): + def test_strips_single_line_block_comment(self): + content = "/* comment */ code" + result = strip_block_comments(content) + self.assertEqual(result, " code") + + def test_strips_multiline_block_comment(self): + content = """/** + * Doc comment + * with multiple lines + */ +@interface RealInterface +@end""" + result = strip_block_comments(content) + # Should preserve 4 newlines (one for each line in the comment) + self.assertEqual( + result, + """\n\n\n +@interface RealInterface +@end""", + ) + + def test_preserves_code_outside_comments(self): + content = """@interface MyClass +- (void)method; +@end""" + result = strip_block_comments(content) + self.assertEqual(result, content) + + def test_strips_comment_with_objc_keywords(self): + """This is the main use case - stripping comments that contain @interface etc.""" + content = """/** + * Example: + * @interface RCT_EXTERN_MODULE(MyModule, NSObject) + * @end + */ +@interface RealInterface +@end""" + result = strip_block_comments(content) + self.assertNotIn("RCT_EXTERN_MODULE", result) + self.assertIn("@interface RealInterface", result) + + def test_handles_multiple_comments(self): + content = """/* first */ code /* second */ more""" + result = strip_block_comments(content) + self.assertEqual(result, " code more") + + def test_handles_empty_content(self): + result = strip_block_comments("") + self.assertEqual(result, "") + + def test_handles_no_comments(self): + content = "just code without comments" + result = strip_block_comments(content) + self.assertEqual(result, content) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/cxx-api/tests/test_snapshots.py b/scripts/cxx-api/tests/test_snapshots.py index c78687904d44..4b21fcd728f7 100644 --- a/scripts/cxx-api/tests/test_snapshots.py +++ b/scripts/cxx-api/tests/test_snapshots.py @@ -49,18 +49,21 @@ def _assert_text_equal_with_diff( tc.fail(diff) -def _get_doxygen_bin() -> str: - return os.environ.get("DOXYGEN_BIN", "doxygen") - - -def _generate_doxygen_api(case_dir_path: str, doxygen_config_path: str) -> None: +def _generate_doxygen_api( + case_dir_path: str, doxygen_config_path: str, filter_script_path: str | None = None +) -> None: """Run doxygen to generate XML API documentation.""" - doxygen_bin = _get_doxygen_bin() + env = os.environ.copy() + if filter_script_path: + env["DOXYGEN_INPUT_FILTER"] = f"python3 {filter_script_path}" + + doxygen_bin = env.get("DOXYGEN_BIN", "doxygen") result = subprocess.run( [doxygen_bin, doxygen_config_path], cwd=case_dir_path, capture_output=True, text=True, + env=env, ) if result.returncode != 0: raise RuntimeError(f"Doxygen failed: {result.stderr}") @@ -101,8 +104,22 @@ def _test(self: unittest.TestCase) -> None: case_dir_path = tests_root_path / case_dir.name doxygen_config_path = tests_root_path / ".doxygen.config.template" + # Find the filter script in the package resources + pkg_root = ir.files(__package__ if __package__ else "__main__") + filter_script = ( + pkg_root.parent / "input_filters" / "doxygen_strip_comments.py" + ) + + # Get real filesystem path for filter script if it exists + filter_script_path = None + if filter_script.is_file(): + with ir.as_file(filter_script) as fs_path: + filter_script_path = str(fs_path) + # Run doxygen to generate the XML - _generate_doxygen_api(str(case_dir_path), str(doxygen_config_path)) + _generate_doxygen_api( + str(case_dir_path), str(doxygen_config_path), filter_script_path + ) # Parse the generated XML xml_dir = case_dir_path / "api" / "xml"