diff --git a/packages/react-native/.doxygen.config.template b/packages/react-native/.doxygen.config.template index 72db8de8be9..4f982c734bc 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 @@ -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)="" ${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/config.yml b/scripts/cxx-api/config.yml index 65ceefb0022..78a1f74a985 100644 --- a/scripts/cxx-api/config.yml +++ b/scripts/cxx-api/config.yml @@ -61,6 +61,7 @@ ReactApple: - "*/platform/macos/*" - "*/platform/android/*" definitions: + __cplusplus: 1 variants: debug: definitions: 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 00000000000..d379f7d1c9d --- /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 80290595ec9..cac0975942a 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 @@ -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)="" ${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/parser/__main__.py b/scripts/cxx-api/parser/__main__.py index 96673ad2dfe..2d7a8cfd52c 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/parser/builders.py b/scripts/cxx-api/parser/builders.py index 85d11f7ccc6..95c8dba7faf 100644 --- a/scripts/cxx-api/parser/builders.py +++ b/scripts/cxx-api/parser/builders.py @@ -33,6 +33,7 @@ Argument, extract_qualifiers, InitializerType, + normalize_pointer_spacing, parse_qualified_path, resolve_linked_text_name, ) @@ -337,6 +338,18 @@ 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: + # Normalize pointer spacing in the argsstring + normalized_argsstring = normalize_pointer_spacing(argsstring) + property_type = f"{property_type}{property_name}{normalized_argsstring}" + property_name = "" + return PropertyMember( property_name, property_type, @@ -376,15 +389,40 @@ def create_enum_scope(snapshot: Snapshot, enum_def: compound.EnumdefType) -> Non ) +def _is_category_member(member_def: compound.MemberdefType) -> bool: + """ + Check if a member comes from a category based on its definition. + + Doxygen merges category members into the base interface XML output, but the + member's definition field contains the category name in parentheses, e.g.: + "int RCTBridgeProxy(Cxx)::cxxOnlyProperty" + + We use this to filter out category members from the interface scope. + """ + definition = member_def.definition + if not definition: + return False + + # Look for pattern: ClassName(CategoryName)::memberName + # The definition contains the qualified name with category info + return bool(re.search(r"\w+\([^)]+\)::", definition)) + + def _process_objc_sections( snapshot: Snapshot, scope, section_defs: list, location_file: str, scope_type: str, + filter_category_members: bool = False, ) -> None: """ Common section processing for protocols and interfaces. + + Args: + filter_category_members: If True, skip members that come from categories. + This is used for interfaces since Doxygen incorrectly merges category + members into the base interface XML output. """ for section_def in section_defs: kind = section_def.kind @@ -399,11 +437,15 @@ def _process_objc_sections( if member_type == "attrib": for member_def in section_def.memberdef: if member_def.kind == "variable": + if filter_category_members and _is_category_member(member_def): + continue scope.add_member( get_variable_member(member_def, visibility, is_static) ) elif member_type == "func": for function_def in section_def.memberdef: + if filter_category_members and _is_category_member(function_def): + continue scope.add_member( get_function_member(function_def, visibility, is_static) ) @@ -412,6 +454,8 @@ def _process_objc_sections( if member_def.kind == "enum": create_enum_scope(snapshot, member_def) elif member_def.kind == "typedef": + if filter_category_members and _is_category_member(member_def): + continue scope.add_member(get_typedef_member(member_def, visibility)) else: print( @@ -422,6 +466,8 @@ def _process_objc_sections( elif visibility == "property": for member_def in section_def.memberdef: if member_def.kind == "property": + if filter_category_members and _is_category_member(member_def): + continue scope.add_member( get_property_member(member_def, "public", is_static) ) @@ -465,7 +511,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( @@ -474,6 +537,7 @@ def create_interface_scope( scope_def.sectiondef, scope_def.location.file, "interface", + filter_category_members=True, ) diff --git a/scripts/cxx-api/parser/member.py b/scripts/cxx-api/parser/member.py index 407fa078aec..2069aa80a84 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/parser/utils/__init__.py b/scripts/cxx-api/parser/utils/__init__.py index 25fd05b4183..248136e621f 100644 --- a/scripts/cxx-api/parser/utils/__init__.py +++ b/scripts/cxx-api/parser/utils/__init__.py @@ -18,6 +18,7 @@ extract_namespace_from_refid, InitializerType, normalize_angle_brackets, + normalize_pointer_spacing, resolve_linked_text_name, ) from .type_qualification import qualify_arguments, qualify_parsed_type, qualify_type_str diff --git a/scripts/cxx-api/parser/utils/text_resolution.py b/scripts/cxx-api/parser/utils/text_resolution.py index 497e1267a5d..8d5e7c46ad2 100644 --- a/scripts/cxx-api/parser/utils/text_resolution.py +++ b/scripts/cxx-api/parser/utils/text_resolution.py @@ -83,13 +83,46 @@ def extract_namespace_from_refid(refid: str) -> str: def normalize_angle_brackets(text: str) -> str: """Doxygen adds spaces around < and > to avoid XML ambiguity. - e.g. "NSArray< id< RCTBridgeMethod > > *" -> "NSArray> *" + e.g. "NSArray< id< RCTBridgeMethod > > *" -> "NSArray>*" """ text = re.sub(r"<\s+", "<", text) text = re.sub(r"\s+>", ">", text) return text +def normalize_pointer_spacing(text: str) -> str: + """Normalize spacing around pointer (*) and reference (&, &&) symbols. + + Doxygen outputs types with a space before * and &, e.g.: + - "NSString *" -> "NSString*" + - "int &" -> "int&" + - "T &&" -> "T&&" + + But for function arguments like "NSString *name", we want "NSString* name" + (space moved from before * to after *). + + This normalizes to have no space before the pointer/reference symbol, + while preserving the space after if there's an identifier following. + """ + # For patterns like "Type *name" -> "Type* name" (move space from before to after) + # Match: word/type character or > followed by space(s), then *, then word char + text = re.sub(r"(\w|>)\s+\*(\w)", r"\1* \2", text) + # For patterns like "Type *" at end or before comma/paren/angle/open-paren -> "Type*" + text = re.sub(r"(\w|>)\s+\*(?=\s*[,)>(]|$)", r"\1*", text) + # For patterns like "Type *" followed by another * (pointer to pointer) + text = re.sub(r"(\w|>)\s+\*(?=\*)", r"\1*", text) + + # Same for && (rvalue reference) + text = re.sub(r"(\w|>)\s+&&(\w)", r"\1&& \2", text) + text = re.sub(r"(\w|>)\s+&&(?=\s*[,)>(]|$)", r"\1&&", text) + + # Same for & (lvalue reference) - but don't match && + text = re.sub(r"(\w|>)\s+&(?!&)(\w)", r"\1& \2", text) + text = re.sub(r"(\w|>)\s+&(?!&)(?=\s*[,)>(]|$)", r"\1&", text) + + return text + + class InitializerType(Enum): NONE = (0,) ASSIGNMENT = (1,) @@ -145,7 +178,10 @@ def resolve_linked_text_name( initialier_type = InitializerType.BRACE name = name[1:-1].strip() - return (normalize_angle_brackets(name.strip()), initialier_type) + return ( + normalize_pointer_spacing(normalize_angle_brackets(name.strip())), + initialier_type, + ) def _qualify_text_with_refid(text: str, refid: str) -> str: diff --git a/scripts/cxx-api/tests/snapshots/.doxygen.config.template b/scripts/cxx-api/tests/snapshots/.doxygen.config.template index a01a146395a..00ae22eb9d2 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 @@ -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)="" +PREDEFINED = __cplusplus=1 \ + 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_handle_category_with_method/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_category_with_method/snapshot.api index a09b6d80df7..55910acb2ed 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_category_with_method/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_category_with_method/snapshot.api @@ -3,5 +3,4 @@ category RCTConvert(UIActivityIndicatorView) { } interface RCTConvert : public NSObject { - public virtual static UIActivityIndicatorViewStyle UIActivityIndicatorViewStyle:(id json); } diff --git a/scripts/cxx-api/tests/snapshots/should_handle_category_with_multiple_methods/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_category_with_multiple_methods/snapshot.api index 1c99214249a..8159b254a2c 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_category_with_multiple_methods/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_category_with_multiple_methods/snapshot.api @@ -4,6 +4,4 @@ category RCTConvert(Transform) { } interface RCTConvert : public NSObject { - public virtual static CATransform3D CATransform3D:(id json); - public virtual static RCTTransformOrigin RCTTransformOrigin:(id json); } diff --git a/scripts/cxx-api/tests/snapshots/should_handle_class_public_method_arguments/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_class_public_method_arguments/snapshot.api index f0d525d1c1c..264305555fc 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_class_public_method_arguments/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_class_public_method_arguments/snapshot.api @@ -10,7 +10,7 @@ class test::Clss { public void fn7(std::vector>> v); public void fn8(std::map> m); public void fn9(int(*)(int, int) callback); - public void fn10(void(*)(const char *, size_t) handler); + public void fn10(void(*)(const char*, size_t) handler); public void fn11(int(*(*fp)(int))(double)); public void fn12(int x = 5, std::string s = "default"); public void fn13(std::function f = nullptr); 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 00000000000..e10fec618c7 --- /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 00000000000..dc261f9b508 --- /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 00000000000..f66dd58f6c0 --- /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 00000000000..02d7e217d22 --- /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 diff --git a/scripts/cxx-api/tests/snapshots/should_handle_concept/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_concept/snapshot.api index 1b3a474c3a0..e3b332471d7 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_concept/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_concept/snapshot.api @@ -1,4 +1,4 @@ template -concept test::CSSParserSink = requires(CSSSyntaxParser &parser) { +concept test::CSSParserSink = requires(CSSSyntaxParser& parser) { { T::consume(parser) } -> std::convertible_to; }; diff --git a/scripts/cxx-api/tests/snapshots/should_handle_concept_indent/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_concept_indent/snapshot.api index 8ed0e695f88..28740457b61 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_concept_indent/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_concept_indent/snapshot.api @@ -1,9 +1,9 @@ template -concept test::CSSUniqueComponentValueVisitors = CSSSyntaxVisitorReturn && (CSSComponentValueVisitor && ...) && - ((CSSFunctionVisitor ? 1 : 0) + ... + 0) <= 1 && - ((CSSPreservedTokenVisitor ? 1 : 0) + ... + 0) <= 1 && +concept test::CSSUniqueComponentValueVisitors = CSSSyntaxVisitorReturn&& (CSSComponentValueVisitor && ...) && + ((CSSFunctionVisitor ? 1 : 0) + ... + 0) <= 1&& + ((CSSPreservedTokenVisitor ? 1 : 0) + ... + 0) <= 1&& ((CSSSimpleBlockVisitor ? 1 : 0) + ... + 0) <= 1; template -concept test::CSSParserSink = requires(CSSSyntaxParser &parser) { +concept test::CSSParserSink = requires(CSSSyntaxParser& parser) { { T::consume(parser) } -> std::convertible_to; }; 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 00000000000..3e8df8d033e --- /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 00000000000..b3d47328979 --- /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 diff --git a/scripts/cxx-api/tests/snapshots/should_handle_function_pointer_variable/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_function_pointer_variable/snapshot.api index 9ad3ff78f6b..2ca7bdec336 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_function_pointer_variable/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_function_pointer_variable/snapshot.api @@ -1,5 +1,5 @@ struct test::FunctionPointers { public int(*withReturn)(double, float); - public void *(*returnsPointer)(const char *); public void(*simple)(int); + public void*(*returnsPointer)(const char *); } diff --git a/scripts/cxx-api/tests/snapshots/should_handle_ifdef_cplusplus/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_ifdef_cplusplus/snapshot.api new file mode 100644 index 00000000000..2ce3fc4be46 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_ifdef_cplusplus/snapshot.api @@ -0,0 +1,6 @@ +category RCTBridgeProxy(Cxx) { + public @property (assign) int cxxOnlyProperty; +} + +interface RCTBridgeProxy : public NSObject { +} diff --git a/scripts/cxx-api/tests/snapshots/should_handle_ifdef_cplusplus/test.h b/scripts/cxx-api/tests/snapshots/should_handle_ifdef_cplusplus/test.h new file mode 100644 index 00000000000..fe2a5d4afcc --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_ifdef_cplusplus/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. + */ + +@interface RCTBridgeProxy : NSObject + +@end + +@interface RCTBridgeProxy (Cxx) + +#ifdef __cplusplus +@property (nonatomic, readwrite) int cxxOnlyProperty; +#endif + +@end 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 00000000000..4293bf02625 --- /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 00000000000..e9fc1c8b965 --- /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 diff --git a/scripts/cxx-api/tests/snapshots/should_handle_interface_with_method/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_interface_with_method/snapshot.api index 09d8acf7014..df124af7b02 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_interface_with_method/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_interface_with_method/snapshot.api @@ -1,5 +1,5 @@ interface RCTInterfaceWithMethod { - public virtual NSString * getTitle(); + public virtual NSString* getTitle(); public virtual void doSomething(); - public virtual void processValue:withName:(int value, NSString * name); + public virtual void processValue:withName:(int value, NSString* name); } diff --git a/scripts/cxx-api/tests/snapshots/should_handle_interface_with_property/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_interface_with_property/snapshot.api index b5649f46485..a478845b700 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_interface_with_property/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_interface_with_property/snapshot.api @@ -1,5 +1,5 @@ interface RCTInterfaceWithProperty { public @property (assign) BOOL enabled; - public @property (copy) NSString * title; + public @property (copy) NSString* title; public @property (weak) id delegate; } diff --git a/scripts/cxx-api/tests/snapshots/should_handle_method_with_attribute/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_method_with_attribute/snapshot.api index bf63be61569..522f4d92cdd 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_method_with_attribute/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_method_with_attribute/snapshot.api @@ -1,4 +1,4 @@ interface RCTTestInterface { - public virtual NSArray * interfaceDeprecatedMethod:(id param); - public virtual void normalMethod:(NSString * name); + public virtual NSArray* interfaceDeprecatedMethod:(id param); + public virtual void normalMethod:(NSString* name); } diff --git a/scripts/cxx-api/tests/snapshots/should_handle_multiple_categories/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_multiple_categories/snapshot.api index 32cebd4b82f..b1e280967e5 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_multiple_categories/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_multiple_categories/snapshot.api @@ -1,12 +1,10 @@ category RCTConvert(CategoryOne) { - public virtual static NSString * methodOne:(id json); + public virtual static NSString* methodOne:(id json); } category RCTConvert(CategoryTwo) { - public virtual static NSString * methodTwo:(id json); + public virtual static NSString* methodTwo:(id json); } interface RCTConvert : public NSObject { - public virtual static NSString * methodOne:(id json); - public virtual static NSString * methodTwo:(id json); } diff --git a/scripts/cxx-api/tests/snapshots/should_handle_protocol_with_method/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_protocol_with_method/snapshot.api index 3cd0361bbc0..3ca06a48508 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_protocol_with_method/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_protocol_with_method/snapshot.api @@ -1,5 +1,5 @@ protocol RCTProtocolWithMethod { - public virtual NSString * getTitle(); + public virtual NSString* getTitle(); public virtual void doSomething(); - public virtual void processValue:withName:(int value, NSString * name); + public virtual void processValue:withName:(int value, NSString* name); } diff --git a/scripts/cxx-api/tests/snapshots/should_handle_protocol_with_property/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_protocol_with_property/snapshot.api index c2322a45e0b..1b95f53fce4 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_protocol_with_property/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_protocol_with_property/snapshot.api @@ -1,5 +1,5 @@ protocol RCTProtocolWithProperty { public @property (assign) BOOL enabled; - public @property (copy) NSString * title; + public @property (copy) NSString* title; public @property (weak) id delegate; } diff --git a/scripts/cxx-api/tests/snapshots/should_handle_pure_virtual_trailing_return/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_pure_virtual_trailing_return/snapshot.api index ea4b88b13cd..d814c71a0dd 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_pure_virtual_trailing_return/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_pure_virtual_trailing_return/snapshot.api @@ -1,5 +1,5 @@ class test::Base { - public virtual const char * getName() = 0; + public virtual const char* getName() = 0; public virtual std::vector getMethods() = 0; } diff --git a/scripts/cxx-api/tests/snapshots/should_handle_unnamed_template_param_with_default_value/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_unnamed_template_param_with_default_value/snapshot.api index 2e9194009db..50a277c93f2 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_unnamed_template_param_with_default_value/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_unnamed_template_param_with_default_value/snapshot.api @@ -12,5 +12,5 @@ class test::Symbol { struct test::Value { template ::value || std::is_base_of::value || std::is_base_of::value || std::is_base_of::value>> - public Value(T && other); + public Value(T&& other); } 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 00000000000..e5a54251d3c --- /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 00000000000..3ffcc2f8474 --- /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/snapshots/should_not_requalify_globally_qualified_type/snapshot.api b/scripts/cxx-api/tests/snapshots/should_not_requalify_globally_qualified_type/snapshot.api index af18855b4a5..88badbbccb0 100644 --- a/scripts/cxx-api/tests/snapshots/should_not_requalify_globally_qualified_type/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_not_requalify_globally_qualified_type/snapshot.api @@ -1,7 +1,7 @@ struct other::Consumer { public ::test::inner::MyType create(); - public void process(const ::test::inner::MyType & param); - public void processPtr(::test::inner::MyType * param); + public void process(const ::test::inner::MyType& param); + public void processPtr(::test::inner::MyType* param); } diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_nested_template_type_with_underscore/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_nested_template_type_with_underscore/snapshot.api index 616760305e9..1f67e496a5b 100644 --- a/scripts/cxx-api/tests/snapshots/should_qualify_nested_template_type_with_underscore/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_qualify_nested_template_type_with_underscore/snapshot.api @@ -1,5 +1,5 @@ template -facebook::react::detail::is_dynamic::type & facebook::react::jsArgAsDynamic(T && args, size_t n); +facebook::react::detail::is_dynamic::type& facebook::react::jsArgAsDynamic(T&& args, size_t n); template diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_ptr_const_args/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_ptr_const_args/snapshot.api index a75e6cc67a1..ec31650e946 100644 --- a/scripts/cxx-api/tests/snapshots/should_qualify_ptr_const_args/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_qualify_ptr_const_args/snapshot.api @@ -1 +1 @@ -void test::doSomething(const Node *const node, int x); +void test::doSomething(const Node* const node, int x); 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 00000000000..e42d0b90b9d --- /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 00000000000..d03ef29db63 --- /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 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 00000000000..2aa3bf50881 --- /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 c78687904d4..4b21fcd728f 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"