Skip to content

Commit 2be2770

Browse files
coadometa-codesync[bot]
authored andcommitted
Fix issue with parsing lightweight objective-c generics (#55979)
Summary: Pull Request resolved: #55979 Fixes issue with parsing lightweight objective-c generics. Doxygen produces malformed xml output due to incorrect handling of interfaces such as: ```obj-c interface Foo<T> : NSObject end ``` The solution is to encode the interface into the form of `interface Foo__GENERICS_T_ENDGENERICS`, then produce the snapshot and finally decode it back to `interface Foo<T>`. Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D95567196 fbshipit-source-id: c8ae97f99e893ecd48ded21f24a650286d708c60
1 parent dd056d9 commit 2be2770

8 files changed

Lines changed: 173 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
# Sentinel tokens used to encode ObjC generics into the class name so they
11+
# survive Doxygen processing. The parser decodes them back into angle-bracket
12+
# syntax when building the snapshot.
13+
GENERICS_START = "__GENERICS__"
14+
GENERICS_END = "__ENDGENERICS__"
15+
GENERICS_COMMA = "__COMMA__"
16+
17+
18+
def encode_objc_interface_generics(content: str) -> str:
19+
"""
20+
Encode ObjC lightweight generic type parameters on @interface declarations
21+
into the class name so Doxygen doesn't misinterpret them as C++ templates.
22+
23+
Doxygen treats angle brackets in ``@interface Foo<T> : Base`` as template
24+
syntax, which corrupts the XML output (wrong base class, __pad0__
25+
artifacts, property attribute leaks). We encode ``<T>`` into the class
26+
name as ``__GENERICS__T__ENDGENERICS__`` so the name remains a valid C
27+
identifier. The parser later decodes it back to ``Foo<T>``.
28+
29+
Only generics *immediately* after the class name are affected. Protocol
30+
conformances like ``Base <Protocol>`` (with a space) are left untouched.
31+
"""
32+
33+
def _encode(match: re.Match) -> str:
34+
prefix = match.group(1) # "@interface ClassName"
35+
params = match.group(2) # "K, V"
36+
parts = [p.strip() for p in params.split(",")]
37+
encoded = GENERICS_COMMA.join(parts)
38+
return f"{prefix}{GENERICS_START}{encoded}{GENERICS_END}"
39+
40+
# Match @interface followed by a name immediately followed by <...> (no space).
41+
# This distinguishes generics (no space) from protocol conformance (space before <).
42+
pattern = re.compile(r"(@interface\s+\w+)<([^>]+)>")
43+
return pattern.sub(_encode, content)
44+
45+
46+
def decode_objc_generics(name: str) -> str:
47+
"""
48+
Decode an encoded class name back to its original generic syntax.
49+
50+
``Foo__GENERICS__K__COMMA__V__ENDGENERICS__`` → ``Foo<K, V>``
51+
"""
52+
match = re.match(
53+
rf"^(.+?){re.escape(GENERICS_START)}(.+?){re.escape(GENERICS_END)}$",
54+
name,
55+
)
56+
if not match:
57+
return name
58+
class_name = match.group(1)
59+
params = match.group(2).replace(GENERICS_COMMA, ", ")
60+
return f"{class_name}<{params}>"

scripts/cxx-api/input_filters/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

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

12+
from handle_objc_interface_generics import encode_objc_interface_generics
1213
from strip_block_comments import strip_block_comments
1314
from strip_deprecated_msg import strip_deprecated_msg
1415
from strip_ns_unavailable import strip_ns_unavailable
@@ -28,6 +29,7 @@ def main():
2829
filtered = strip_block_comments(content)
2930
filtered = strip_deprecated_msg(filtered)
3031
filtered = strip_ns_unavailable(filtered)
32+
filtered = encode_objc_interface_generics(filtered)
3133
print(filtered, end="")
3234
except Exception as e:
3335
# On error, output original content to not break the build

scripts/cxx-api/parser/builders.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from doxmlparser import compound
1919

20+
from ..input_filters.handle_objc_interface_generics import decode_objc_generics
2021
from .member import (
2122
ConceptMember,
2223
EnumMember,
@@ -510,6 +511,12 @@ def create_interface_scope(
510511
"""
511512
interface_name = scope_def.compoundname
512513

514+
# Decode ObjC generics that were encoded by the input filter.
515+
# The input filter encodes ``@interface Foo<T>`` as
516+
# ``@interface Foo__GENERICS__T__ENDGENERICS__`` so Doxygen can parse it.
517+
# We restore the original ``Foo<T>`` name here.
518+
interface_name = decode_objc_generics(interface_name)
519+
513520
interface_scope = snapshot.create_interface(interface_name)
514521
base_classes = get_base_classes(scope_def, base_class=InterfaceScopeKind.Base)
515522

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
interface RCTMapper<KeyT, ValueT> : public NSObject {
2+
public @property (strong) KeyT key;
3+
public @property (strong) ValueT value;
4+
public virtual void setKey:value:(KeyT key, ValueT value);
5+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
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+
8+
@interface RCTMapper<KeyT, ValueT> : NSObject
9+
10+
@property (nonatomic, strong) KeyT key;
11+
@property (nonatomic, strong) ValueT value;
12+
- (void)setKey:(KeyT)key value:(ValueT)value;
13+
14+
@end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
interface RCTGenericDelegateSplitter<DelegateT> : public NSObject {
2+
public @property (copy) void(^delegateUpdateBlock)(DelegateT _Nullable delegate);
3+
public virtual void addDelegate:(DelegateT delegate);
4+
public virtual void removeDelegate:(DelegateT delegate);
5+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
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+
8+
@interface RCTGenericDelegateSplitter<DelegateT> : NSObject
9+
10+
@property (nonatomic, copy, nullable) void (^delegateUpdateBlock)(DelegateT _Nullable delegate);
11+
- (void)addDelegate:(DelegateT)delegate;
12+
- (void)removeDelegate:(DelegateT)delegate;
13+
14+
@end

scripts/cxx-api/tests/test_input_filters.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77

88
import unittest
99

10+
from ..input_filters.handle_objc_interface_generics import (
11+
decode_objc_generics,
12+
encode_objc_interface_generics,
13+
)
1014
from ..input_filters.strip_block_comments import strip_block_comments
1115
from ..input_filters.strip_deprecated_msg import strip_deprecated_msg
1216
from ..input_filters.strip_ns_unavailable import strip_ns_unavailable
@@ -208,5 +212,67 @@ def test_handles_leading_whitespace(self):
208212
self.assertEqual(result, "")
209213

210214

215+
class TestEncodeObjcInterfaceGenerics(unittest.TestCase):
216+
def test_single_generic_param(self):
217+
content = "@interface Foo<T> : NSObject\n@end"
218+
result = encode_objc_interface_generics(content)
219+
self.assertEqual(
220+
result,
221+
"@interface Foo__GENERICS__T__ENDGENERICS__ : NSObject\n@end",
222+
)
223+
224+
def test_two_generic_params(self):
225+
content = "@interface MyMap<K, V> : NSObject\n@end"
226+
result = encode_objc_interface_generics(content)
227+
self.assertEqual(
228+
result,
229+
"@interface MyMap__GENERICS__K__COMMA__V__ENDGENERICS__ : NSObject\n@end",
230+
)
231+
232+
def test_protocol_conformance_not_encoded(self):
233+
content = "@interface Foo : NSObject <NSCopying>"
234+
result = encode_objc_interface_generics(content)
235+
self.assertEqual(result, content)
236+
237+
def test_interface_without_generics(self):
238+
content = "@interface MyClass : NSObject\n- (void)method;\n@end"
239+
result = encode_objc_interface_generics(content)
240+
self.assertEqual(result, content)
241+
242+
def test_multiple_interfaces_both_with_generics(self):
243+
content = (
244+
"@interface Foo<T> : NSObject\n@end\n\n"
245+
"@interface Bar<K, V> : NSObject\n@end"
246+
)
247+
result = encode_objc_interface_generics(content)
248+
self.assertIn("__GENERICS__T__ENDGENERICS__", result)
249+
self.assertIn("__GENERICS__K__COMMA__V__ENDGENERICS__", result)
250+
251+
252+
class TestDecodeObjcGenerics(unittest.TestCase):
253+
def test_single_generic_param(self):
254+
result = decode_objc_generics("Foo__GENERICS__T__ENDGENERICS__")
255+
self.assertEqual(result, "Foo<T>")
256+
257+
def test_two_generic_params(self):
258+
result = decode_objc_generics("MyMap__GENERICS__K__COMMA__V__ENDGENERICS__")
259+
self.assertEqual(result, "MyMap<K, V>")
260+
261+
def test_roundtrip_single_generic(self):
262+
content = "@interface Foo<T> : NSObject\n@end"
263+
encoded = encode_objc_interface_generics(content)
264+
# Extract the encoded class name (between "@interface " and " :")
265+
encoded_name = encoded.split("@interface ")[1].split(" :")[0]
266+
decoded_name = decode_objc_generics(encoded_name)
267+
self.assertEqual(decoded_name, "Foo<T>")
268+
269+
def test_roundtrip_multiple_generics(self):
270+
content = "@interface MyMap<K, V> : NSObject\n@end"
271+
encoded = encode_objc_interface_generics(content)
272+
encoded_name = encoded.split("@interface ")[1].split(" :")[0]
273+
decoded_name = decode_objc_generics(encoded_name)
274+
self.assertEqual(decoded_name, "MyMap<K, V>")
275+
276+
211277
if __name__ == "__main__":
212278
unittest.main()

0 commit comments

Comments
 (0)