Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions packages/react-native/.doxygen.config.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions scripts/cxx-api/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ ReactApple:
- "*/platform/macos/*"
- "*/platform/android/*"
definitions:
__cplusplus: 1
variants:
debug:
definitions:
Expand Down
60 changes: 60 additions & 0 deletions scripts/cxx-api/input_filters/doxygen_strip_comments.py
Original file line number Diff line number Diff line change
@@ -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 <filename>", 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()
17 changes: 15 additions & 2 deletions scripts/cxx-api/manual_test/.doxygen.config.template
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
22 changes: 18 additions & 4 deletions scripts/cxx-api/parser/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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}")
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand All @@ -245,7 +263,3 @@ def build_snapshots(output_dir: str, verbose: bool) -> None:
)
)
build_snapshots(output_dir, verbose=True)


if __name__ == "__main__":
main()
66 changes: 65 additions & 1 deletion scripts/cxx-api/parser/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
Argument,
extract_qualifiers,
InitializerType,
normalize_pointer_spacing,
parse_qualified_path,
resolve_linked_text_name,
)
Expand Down Expand Up @@ -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 <type> and <argsstring>
# <type> = "void(^"
# <argsstring> = ")(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,
Expand Down Expand Up @@ -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
Expand All @@ -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)
)
Expand All @@ -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(
Expand All @@ -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)
)
Expand Down Expand Up @@ -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 <Protocol1, Protocol2>" into separate base classes:
# "Foo", "<Protocol1>", "<Protocol2>". Combine them back into "Foo <Protocol1, Protocol2>".
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(
Expand All @@ -474,6 +537,7 @@ def create_interface_scope(
scope_def.sectiondef,
scope_def.location.file,
"interface",
filter_category_members=True,
)


Expand Down
6 changes: 5 additions & 1 deletion scripts/cxx-api/parser/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions scripts/cxx-api/parser/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading