Skip to content

Commit d83bf73

Browse files
coadometa-codesync[bot]
authored andcommitted
Add input filter for stripping methods marked as NS_UNAVAILABLE (#55973)
Summary: Pull Request resolved: #55973 Adds input filter for stripping methods marked as `NS_UNAVAILABLE` which shouldn't be part of the public API. Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D95552298 fbshipit-source-id: 2e5b33f047bfda259733b92b187ce86d9967cf93
1 parent c0ae52b commit d83bf73

10 files changed

Lines changed: 234 additions & 91 deletions

File tree

scripts/cxx-api/input_filters/doxygen_strip_comments.py

Lines changed: 0 additions & 83 deletions
This file was deleted.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env fbpython
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
#
4+
# This source code is licensed under the MIT license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import os
8+
import sys
9+
10+
sys.path.insert(0, os.path.dirname(__file__))
11+
12+
from strip_block_comments import strip_block_comments
13+
from strip_deprecated_msg import strip_deprecated_msg
14+
from strip_ns_unavailable import strip_ns_unavailable
15+
16+
17+
def main():
18+
if len(sys.argv) < 2:
19+
print("Usage: main.py <filename>", file=sys.stderr)
20+
sys.exit(1)
21+
22+
filename = sys.argv[1]
23+
24+
try:
25+
with open(filename, "r", encoding="utf-8", errors="replace") as f:
26+
content = f.read()
27+
28+
filtered = strip_block_comments(content)
29+
filtered = strip_deprecated_msg(filtered)
30+
filtered = strip_ns_unavailable(filtered)
31+
print(filtered, end="")
32+
except Exception as e:
33+
# On error, output original content to not break the build
34+
print(f"Warning: Filter error for {filename}: {e}", file=sys.stderr)
35+
with open(filename, "r", encoding="utf-8", errors="replace") as f:
36+
print(f.read(), end="")
37+
38+
39+
if __name__ == "__main__":
40+
main()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env fbpython
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
#
4+
# This source code is licensed under the MIT license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import re
8+
9+
10+
def strip_block_comments(content: str) -> str:
11+
"""
12+
Remove all block comments (/* ... */ and /** ... */) from content.
13+
Preserves line count by replacing comment content with newlines.
14+
"""
15+
16+
def replace_with_newlines(match: re.Match) -> str:
17+
# Count newlines in original comment to preserve line numbers
18+
newline_count = match.group().count("\n")
19+
return "\n" * newline_count
20+
21+
# Pattern to match block comments (non-greedy)
22+
comment_pattern = re.compile(r"/\*[\s\S]*?\*/")
23+
24+
return comment_pattern.sub(replace_with_newlines, content)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env fbpython
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
#
4+
# This source code is licensed under the MIT license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import re
8+
9+
10+
def strip_deprecated_msg(content: str) -> str:
11+
"""
12+
Remove __deprecated_msg(...) macros and standalone __deprecated annotations
13+
from content.
14+
15+
These macros cause Doxygen to produce malformed XML output when they appear
16+
before @interface declarations, creating __pad0__ artifacts and missing
17+
members. Standalone __deprecated on method declarations causes the annotation
18+
to be parsed as a parameter name. Since the macros are stripped, deprecation
19+
info won't appear in the API snapshot output.
20+
"""
21+
# Pattern to match __deprecated_msg("...") with any content inside quotes
22+
pattern = re.compile(r'__deprecated_msg\s*\(\s*"[^"]*"\s*\)\s*')
23+
content = pattern.sub("", content)
24+
25+
# Pattern to match standalone __deprecated (not followed by _msg or other suffix)
26+
standalone_pattern = re.compile(r"\b__deprecated\b(?!_)\s*")
27+
content = standalone_pattern.sub("", content)
28+
29+
return content
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env fbpython
2+
# Copyright (c) Meta Platforms, Inc. and affiliates.
3+
#
4+
# This source code is licensed under the MIT license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import re
8+
9+
10+
def strip_ns_unavailable(content: str) -> str:
11+
"""
12+
Remove method and property declarations marked NS_UNAVAILABLE from content.
13+
14+
NS_UNAVAILABLE marks methods/properties that are explicitly not part of the
15+
public API — they exist only to produce compile errors when called. Including
16+
them in the API snapshot is misleading, so we strip the entire declaration.
17+
Preserves line count by replacing matched declarations with newlines.
18+
"""
19+
20+
def replace_with_newlines(match: re.Match) -> str:
21+
return "\n" * match.group().count("\n")
22+
23+
# Match ObjC method (-/+) or @property declarations ending with NS_UNAVAILABLE;
24+
# [^;]*? is non-greedy and cannot cross past a prior declaration's semicolon,
25+
# but [^;] *does* match newlines, so multi-line declarations are handled.
26+
pattern = re.compile(
27+
r"^[ \t]*(?:[-+]|@property\b)[^;]*?NS_UNAVAILABLE\s*;[ \t]*$",
28+
re.MULTILINE,
29+
)
30+
return pattern.sub(replace_with_newlines, content)

scripts/cxx-api/parser/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def main():
202202
"scripts",
203203
"cxx-api",
204204
"input_filters",
205-
"doxygen_strip_comments.py",
205+
"main.py",
206206
)
207207

208208
input_filter = None

scripts/cxx-api/tests/snapshots/should_strip_objc_macros/snapshot.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ interface RCTTestMacros {
55
public virtual instancetype initWithName:(NSString* name);
66
public virtual static UIUserInterfaceStyle userInterfaceStyle();
77
public virtual void deprecatedMethod();
8+
public virtual void start();
89
}
910

1011
protocol RCTTestProtocol {

scripts/cxx-api/tests/snapshots/should_strip_objc_macros/test.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,24 @@
1111

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

14+
- (instancetype)init NS_UNAVAILABLE;
15+
16+
+ (instancetype)new NS_UNAVAILABLE;
17+
18+
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
19+
1420
@property (nonatomic, strong, readonly) dispatch_queue_t methodQueue RCT_DEPRECATED;
1521

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

24+
@property (nonatomic, copy, nullable) NSString *text NS_UNAVAILABLE;
25+
1826
- (void)deprecatedMethod RCT_DEPRECATED;
1927

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

30+
- (void)start;
31+
2232
@end
2333

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

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

46+
- (void)setSelectedTextRange:(nullable UITextRange *)selectedTextRange NS_UNAVAILABLE;
47+
3648
@end

scripts/cxx-api/tests/test_input_filters.py

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@
77

88
import unittest
99

10-
from ..input_filters.doxygen_strip_comments import (
11-
strip_block_comments,
12-
strip_deprecated_msg,
13-
)
10+
from ..input_filters.strip_block_comments import strip_block_comments
11+
from ..input_filters.strip_deprecated_msg import strip_deprecated_msg
12+
from ..input_filters.strip_ns_unavailable import strip_ns_unavailable
1413

1514

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

118117

118+
class TestStripNSUnavailable(unittest.TestCase):
119+
def test_strips_single_line_init(self):
120+
content = "- (instancetype)init NS_UNAVAILABLE;"
121+
result = strip_ns_unavailable(content)
122+
self.assertEqual(result, "")
123+
124+
def test_strips_single_line_new(self):
125+
content = "+ (instancetype)new NS_UNAVAILABLE;"
126+
result = strip_ns_unavailable(content)
127+
self.assertEqual(result, "")
128+
129+
def test_strips_init_with_frame(self):
130+
content = "- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;"
131+
result = strip_ns_unavailable(content)
132+
self.assertEqual(result, "")
133+
134+
def test_strips_property(self):
135+
content = "@property (nonatomic, copy, nullable) NSString *text NS_UNAVAILABLE;"
136+
result = strip_ns_unavailable(content)
137+
self.assertEqual(result, "")
138+
139+
def test_strips_method_with_params(self):
140+
content = (
141+
"- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index"
142+
" NS_UNAVAILABLE;"
143+
)
144+
result = strip_ns_unavailable(content)
145+
self.assertEqual(result, "")
146+
147+
def test_strips_multiline_declaration(self):
148+
content = (
149+
"- (instancetype)initWithSurface:(id<RCTSurfaceProtocol>)surface\n"
150+
" sizeMeasureMode:(RCTSurfaceSizeMeasureMode)"
151+
"sizeMeasureMode NS_UNAVAILABLE;"
152+
)
153+
result = strip_ns_unavailable(content)
154+
# Should preserve line count (2 lines -> 1 newline)
155+
self.assertEqual(result, "\n")
156+
157+
def test_preserves_normal_methods(self):
158+
content = "- (instancetype)initWithBridge:(RCTBridge *)bridge;"
159+
result = strip_ns_unavailable(content)
160+
self.assertEqual(result, content)
161+
162+
def test_preserves_normal_properties(self):
163+
content = "@property (nonatomic, strong) NSString *name;"
164+
result = strip_ns_unavailable(content)
165+
self.assertEqual(result, content)
166+
167+
def test_preserves_designated_initializer(self):
168+
content = (
169+
"- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;"
170+
)
171+
result = strip_ns_unavailable(content)
172+
self.assertEqual(result, content)
173+
174+
def test_does_not_strip_across_semicolons(self):
175+
content = (
176+
"- (void)normalMethod;\n"
177+
"- (instancetype)init NS_UNAVAILABLE;\n"
178+
"- (void)anotherMethod;"
179+
)
180+
result = strip_ns_unavailable(content)
181+
self.assertEqual(result, "- (void)normalMethod;\n\n- (void)anotherMethod;")
182+
183+
def test_strips_multiple_unavailable_methods(self):
184+
content = (
185+
"- (instancetype)init NS_UNAVAILABLE;\n+ (instancetype)new NS_UNAVAILABLE;"
186+
)
187+
result = strip_ns_unavailable(content)
188+
self.assertEqual(result, "\n")
189+
190+
def test_handles_empty_content(self):
191+
result = strip_ns_unavailable("")
192+
self.assertEqual(result, "")
193+
194+
def test_preserves_line_count(self):
195+
content = (
196+
"@interface RCTHost : NSObject\n"
197+
"- (instancetype)init NS_UNAVAILABLE;\n"
198+
"+ (instancetype)new NS_UNAVAILABLE;\n"
199+
"- (void)start;\n"
200+
"@end"
201+
)
202+
result = strip_ns_unavailable(content)
203+
self.assertEqual(result.count("\n"), content.count("\n"))
204+
205+
def test_handles_leading_whitespace(self):
206+
content = " - (instancetype)init NS_UNAVAILABLE;"
207+
result = strip_ns_unavailable(content)
208+
self.assertEqual(result, "")
209+
210+
119211
if __name__ == "__main__":
120212
unittest.main()

scripts/cxx-api/tests/test_snapshots.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,7 @@ def _test(self: unittest.TestCase) -> None:
106106

107107
# Find the filter script in the package resources
108108
pkg_root = ir.files(__package__ if __package__ else "__main__")
109-
filter_script = (
110-
pkg_root.parent / "input_filters" / "doxygen_strip_comments.py"
111-
)
109+
filter_script = pkg_root.parent / "input_filters" / "main.py"
112110

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

0 commit comments

Comments
 (0)