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
83 changes: 0 additions & 83 deletions scripts/cxx-api/input_filters/doxygen_strip_comments.py

This file was deleted.

40 changes: 40 additions & 0 deletions scripts/cxx-api/input_filters/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/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.

import os
import sys

sys.path.insert(0, os.path.dirname(__file__))

from strip_block_comments import strip_block_comments
from strip_deprecated_msg import strip_deprecated_msg
from strip_ns_unavailable import strip_ns_unavailable


def main():
if len(sys.argv) < 2:
print("Usage: main.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)
filtered = strip_deprecated_msg(filtered)
filtered = strip_ns_unavailable(filtered)
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()
24 changes: 24 additions & 0 deletions scripts/cxx-api/input_filters/strip_block_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/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.

import re


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)
29 changes: 29 additions & 0 deletions scripts/cxx-api/input_filters/strip_deprecated_msg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/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.

import re


def strip_deprecated_msg(content: str) -> str:
"""
Remove __deprecated_msg(...) macros and standalone __deprecated annotations
from content.

These macros cause Doxygen to produce malformed XML output when they appear
before @interface declarations, creating __pad0__ artifacts and missing
members. Standalone __deprecated on method declarations causes the annotation
to be parsed as a parameter name. Since the macros are stripped, deprecation
info won't appear in the API snapshot output.
"""
# Pattern to match __deprecated_msg("...") with any content inside quotes
pattern = re.compile(r'__deprecated_msg\s*\(\s*"[^"]*"\s*\)\s*')
content = pattern.sub("", content)

# Pattern to match standalone __deprecated (not followed by _msg or other suffix)
standalone_pattern = re.compile(r"\b__deprecated\b(?!_)\s*")
content = standalone_pattern.sub("", content)

return content
30 changes: 30 additions & 0 deletions scripts/cxx-api/input_filters/strip_ns_unavailable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/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.

import re


def strip_ns_unavailable(content: str) -> str:
"""
Remove method and property declarations marked NS_UNAVAILABLE from content.

NS_UNAVAILABLE marks methods/properties that are explicitly not part of the
public API — they exist only to produce compile errors when called. Including
them in the API snapshot is misleading, so we strip the entire declaration.
Preserves line count by replacing matched declarations with newlines.
"""

def replace_with_newlines(match: re.Match) -> str:
return "\n" * match.group().count("\n")

# Match ObjC method (-/+) or @property declarations ending with NS_UNAVAILABLE;
# [^;]*? is non-greedy and cannot cross past a prior declaration's semicolon,
# but [^;] *does* match newlines, so multi-line declarations are handled.
pattern = re.compile(
r"^[ \t]*(?:[-+]|@property\b)[^;]*?NS_UNAVAILABLE\s*;[ \t]*$",
re.MULTILINE,
)
return pattern.sub(replace_with_newlines, content)
2 changes: 1 addition & 1 deletion scripts/cxx-api/parser/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def main():
"scripts",
"cxx-api",
"input_filters",
"doxygen_strip_comments.py",
"main.py",
)

input_filter = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface RCTTestMacros {
public virtual instancetype initWithName:(NSString* name);
public virtual static UIUserInterfaceStyle userInterfaceStyle();
public virtual void deprecatedMethod();
public virtual void start();
}

protocol RCTTestProtocol {
Expand Down
12 changes: 12 additions & 0 deletions scripts/cxx-api/tests/snapshots/should_strip_objc_macros/test.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,24 @@

- (instancetype)initWithDelegate:(id)delegate options:(NSDictionary *)options NS_DESIGNATED_INITIALIZER;

- (instancetype)init NS_UNAVAILABLE;

+ (instancetype)new NS_UNAVAILABLE;

- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;

@property (nonatomic, strong, readonly) dispatch_queue_t methodQueue RCT_DEPRECATED;

@property (nonatomic, weak, readonly) id bridge RCT_DEPRECATED;

@property (nonatomic, copy, nullable) NSString *text NS_UNAVAILABLE;

- (void)deprecatedMethod RCT_DEPRECATED;

+ (UIUserInterfaceStyle)userInterfaceStyle API_AVAILABLE(ios(12));

- (void)start;

@end

RCT_EXTERN void RCTExternFunction(const char *input, NSString **output);
Expand All @@ -33,4 +43,6 @@ RCT_EXTERN NSString *RCTParseType(const char **input);

@property (nonatomic, readonly) NSString *name RCT_DEPRECATED;

- (void)setSelectedTextRange:(nullable UITextRange *)selectedTextRange NS_UNAVAILABLE;

@end
100 changes: 96 additions & 4 deletions scripts/cxx-api/tests/test_input_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@

import unittest

from ..input_filters.doxygen_strip_comments import (
strip_block_comments,
strip_deprecated_msg,
)
from ..input_filters.strip_block_comments import strip_block_comments
from ..input_filters.strip_deprecated_msg import strip_deprecated_msg
from ..input_filters.strip_ns_unavailable import strip_ns_unavailable


class TestDoxygenStripComments(unittest.TestCase):
Expand Down Expand Up @@ -116,5 +115,98 @@ def test_strips_deprecated_before_interface(self):
self.assertEqual(result, "@interface RCTSurface : NSObject")


class TestStripNSUnavailable(unittest.TestCase):
def test_strips_single_line_init(self):
content = "- (instancetype)init NS_UNAVAILABLE;"
result = strip_ns_unavailable(content)
self.assertEqual(result, "")

def test_strips_single_line_new(self):
content = "+ (instancetype)new NS_UNAVAILABLE;"
result = strip_ns_unavailable(content)
self.assertEqual(result, "")

def test_strips_init_with_frame(self):
content = "- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;"
result = strip_ns_unavailable(content)
self.assertEqual(result, "")

def test_strips_property(self):
content = "@property (nonatomic, copy, nullable) NSString *text NS_UNAVAILABLE;"
result = strip_ns_unavailable(content)
self.assertEqual(result, "")

def test_strips_method_with_params(self):
content = (
"- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index"
" NS_UNAVAILABLE;"
)
result = strip_ns_unavailable(content)
self.assertEqual(result, "")

def test_strips_multiline_declaration(self):
content = (
"- (instancetype)initWithSurface:(id<RCTSurfaceProtocol>)surface\n"
" sizeMeasureMode:(RCTSurfaceSizeMeasureMode)"
"sizeMeasureMode NS_UNAVAILABLE;"
)
result = strip_ns_unavailable(content)
# Should preserve line count (2 lines -> 1 newline)
self.assertEqual(result, "\n")

def test_preserves_normal_methods(self):
content = "- (instancetype)initWithBridge:(RCTBridge *)bridge;"
result = strip_ns_unavailable(content)
self.assertEqual(result, content)

def test_preserves_normal_properties(self):
content = "@property (nonatomic, strong) NSString *name;"
result = strip_ns_unavailable(content)
self.assertEqual(result, content)

def test_preserves_designated_initializer(self):
content = (
"- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;"
)
result = strip_ns_unavailable(content)
self.assertEqual(result, content)

def test_does_not_strip_across_semicolons(self):
content = (
"- (void)normalMethod;\n"
"- (instancetype)init NS_UNAVAILABLE;\n"
"- (void)anotherMethod;"
)
result = strip_ns_unavailable(content)
self.assertEqual(result, "- (void)normalMethod;\n\n- (void)anotherMethod;")

def test_strips_multiple_unavailable_methods(self):
content = (
"- (instancetype)init NS_UNAVAILABLE;\n+ (instancetype)new NS_UNAVAILABLE;"
)
result = strip_ns_unavailable(content)
self.assertEqual(result, "\n")

def test_handles_empty_content(self):
result = strip_ns_unavailable("")
self.assertEqual(result, "")

def test_preserves_line_count(self):
content = (
"@interface RCTHost : NSObject\n"
"- (instancetype)init NS_UNAVAILABLE;\n"
"+ (instancetype)new NS_UNAVAILABLE;\n"
"- (void)start;\n"
"@end"
)
result = strip_ns_unavailable(content)
self.assertEqual(result.count("\n"), content.count("\n"))

def test_handles_leading_whitespace(self):
content = " - (instancetype)init NS_UNAVAILABLE;"
result = strip_ns_unavailable(content)
self.assertEqual(result, "")


if __name__ == "__main__":
unittest.main()
4 changes: 1 addition & 3 deletions scripts/cxx-api/tests/test_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,7 @@ def _test(self: unittest.TestCase) -> None:

# 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"
)
filter_script = pkg_root.parent / "input_filters" / "main.py"

# Get real filesystem path for filter script if it exists
# IMPORTANT: Keep the context manager active while Doxygen runs,
Expand Down
Loading