Skip to content

Commit 1620298

Browse files
coadometa-codesync[bot]
authored andcommitted
Add support for parsing protocols (#55766)
Summary: Pull Request resolved: #55766 This diff adds support for parsing Objective-C protocols in the C++ API snapshot parser. The angle bracket normalization is also added to ensure better snapshot structure. Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D93859440 fbshipit-source-id: 21c4a47e33df0c61e738e07c5b56cfccd56af1a4
1 parent f81f73e commit 1620298

File tree

21 files changed

+326
-26
lines changed

21 files changed

+326
-26
lines changed

scripts/cxx-api/parser/main.py

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
EnumMember,
1818
FriendMember,
1919
FunctionMember,
20+
PropertyMember,
2021
TypedefMember,
2122
VariableMember,
2223
)
23-
from .scope import StructLikeScopeKind
24+
from .scope import ProtocolScopeKind, StructLikeScopeKind
2425
from .snapshot import Snapshot
2526
from .template import Template
2627
from .utils import Argument, extract_qualifiers, parse_qualified_path
@@ -119,6 +120,15 @@ def _qualify_text_with_refid(text: str, refid: str) -> str:
119120
return prepend + "::" + text
120121

121122

123+
def normalize_angle_brackets(text: str) -> str:
124+
"""Doxygen adds spaces around < and > to avoid XML ambiguity.
125+
e.g. "NSArray< id< RCTBridgeMethod > > *" -> "NSArray<id<RCTBridgeMethod>> *"
126+
"""
127+
text = re.sub(r"<\s+", "<", text)
128+
text = re.sub(r"\s+>", ">", text)
129+
return text
130+
131+
122132
def extract_namespace_from_refid(refid: str) -> str:
123133
"""Extract the namespace prefix from a doxygen refid.
124134
e.g. 'namespacefacebook_1_1yoga_1a...' -> 'facebook::yoga'
@@ -202,12 +212,13 @@ def resolve_linked_text_name(
202212
initialier_type = InitializerType.BRACE
203213
name = name[1:-1].strip()
204214

205-
return (name.strip(), initialier_type)
215+
return (normalize_angle_brackets(name.strip()), initialier_type)
206216

207217

208218
def get_base_classes(
209219
compound_object: compound.CompounddefType,
210-
) -> [StructLikeScopeKind.Base]:
220+
base_class=StructLikeScopeKind.Base,
221+
) -> list:
211222
"""
212223
Get the base classes of a compound object.
213224
"""
@@ -229,7 +240,7 @@ def get_base_classes(
229240
continue
230241

231242
base_classes.append(
232-
StructLikeScopeKind.Base(
243+
base_class(
233244
base_name,
234245
base_prot,
235246
base_virt == "virtual",
@@ -484,6 +495,31 @@ def get_concept_member(
484495
return concept
485496

486497

498+
def get_property_member(
499+
member_def: compound.MemberdefType,
500+
visibility: str,
501+
is_static: bool = False,
502+
) -> PropertyMember:
503+
"""
504+
Get the property member from a member definition.
505+
"""
506+
property_name = member_def.get_name()
507+
property_type = resolve_linked_text_name(member_def.get_type())[0].strip()
508+
accessor = member_def.accessor if hasattr(member_def, "accessor") else None
509+
is_readable = getattr(member_def, "readable", "no") == "yes"
510+
is_writable = getattr(member_def, "writable", "no") == "yes"
511+
512+
return PropertyMember(
513+
property_name,
514+
property_type,
515+
visibility,
516+
is_static,
517+
accessor,
518+
is_readable,
519+
is_writable,
520+
)
521+
522+
487523
def create_enum_scope(snapshot: Snapshot, enum_def: compound.EnumdefType):
488524
"""
489525
Create an enum scope in the snapshot.
@@ -507,6 +543,71 @@ def create_enum_scope(snapshot: Snapshot, enum_def: compound.EnumdefType):
507543
)
508544

509545

546+
def create_protocol_scope(snapshot: Snapshot, scope_def: compound.CompounddefType):
547+
"""
548+
Create a protocol scope in the snapshot.
549+
"""
550+
# Doxygen appends "-p" to ObjC protocol compound names
551+
protocol_name = scope_def.compoundname
552+
if protocol_name.endswith("-p"):
553+
protocol_name = protocol_name[:-2]
554+
555+
protocol_scope = snapshot.create_protocol(protocol_name)
556+
base_classes = get_base_classes(scope_def, base_class=ProtocolScopeKind.Base)
557+
for base in base_classes:
558+
base.name = base.name.strip("<>")
559+
protocol_scope.kind.add_base(base_classes)
560+
protocol_scope.location = scope_def.location.file
561+
562+
for section_def in scope_def.sectiondef:
563+
kind = section_def.kind
564+
parts = kind.split("-")
565+
visibility = parts[0]
566+
is_static = "static" in parts
567+
member_type = parts[-1]
568+
569+
if visibility == "private":
570+
pass
571+
elif visibility in ("public", "protected"):
572+
if member_type == "attrib":
573+
for member_def in section_def.memberdef:
574+
if member_def.kind == "variable":
575+
protocol_scope.add_member(
576+
get_variable_member(member_def, visibility, is_static)
577+
)
578+
elif member_type == "func":
579+
for function_def in section_def.memberdef:
580+
protocol_scope.add_member(
581+
get_function_member(function_def, visibility, is_static)
582+
)
583+
elif member_type == "type":
584+
for member_def in section_def.memberdef:
585+
if member_def.kind == "enum":
586+
create_enum_scope(snapshot, member_def)
587+
elif member_def.kind == "typedef":
588+
protocol_scope.add_member(
589+
get_typedef_member(member_def, visibility)
590+
)
591+
else:
592+
print(
593+
f"Unknown section member kind: {member_def.kind} in {scope_def.location.file}"
594+
)
595+
else:
596+
print(
597+
f"Unknown protocol section kind: {kind} in {scope_def.location.file}"
598+
)
599+
elif visibility == "property":
600+
for member_def in section_def.memberdef:
601+
if member_def.kind == "property":
602+
protocol_scope.add_member(
603+
get_property_member(member_def, "public", is_static)
604+
)
605+
else:
606+
print(
607+
f"Unknown protocol visibility: {visibility} in {scope_def.location.file}"
608+
)
609+
610+
510611
def build_snapshot(xml_dir: str) -> Snapshot:
511612
"""
512613
Reads the Doxygen XML output and builds a snapshot of the C++ API.
@@ -679,7 +780,7 @@ def build_snapshot(xml_dir: str) -> Snapshot:
679780
# Contains deprecation info
680781
pass
681782
elif compound_object.kind == "protocol":
682-
print(f"Protocol not supported: {compound_object.compoundname}")
783+
create_protocol_scope(snapshot, compound_object)
683784
else:
684785
print(f"Unknown compound kind: {compound_object.kind}")
685786

scripts/cxx-api/parser/member.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,56 @@ def to_string(
381381
return result
382382

383383

384+
class PropertyMember(Member):
385+
def __init__(
386+
self,
387+
name: str,
388+
type: str,
389+
visibility: str,
390+
is_static: bool,
391+
accessor: str | None,
392+
is_readable: bool,
393+
is_writable: bool,
394+
) -> None:
395+
super().__init__(name, visibility)
396+
self.type: str = type
397+
self.is_static: bool = is_static
398+
self.accessor: str | None = accessor
399+
self.is_readable: bool = is_readable
400+
self.is_writable: bool = is_writable
401+
402+
@property
403+
def member_kind(self) -> MemberKind:
404+
return MemberKind.VARIABLE
405+
406+
def to_string(
407+
self,
408+
indent: int = 0,
409+
qualification: str | None = None,
410+
hide_visibility: bool = False,
411+
) -> str:
412+
name = self._get_qualified_name(qualification)
413+
result = " " * indent
414+
415+
if not hide_visibility:
416+
result += self.visibility + " "
417+
418+
attributes = []
419+
if self.accessor:
420+
attributes.append(self.accessor)
421+
if not self.is_writable and self.is_readable:
422+
attributes.append("readonly")
423+
424+
attrs_str = f"({', '.join(attributes)}) " if attributes else ""
425+
426+
if self.is_static:
427+
result += "static "
428+
429+
result += f"@property {attrs_str}{self.type} {name};"
430+
431+
return result
432+
433+
384434
class ConceptMember(Member):
385435
def __init__(
386436
self,

scripts/cxx-api/parser/scope.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,55 @@ def to_string(self, scope: Scope) -> str:
153153
return result
154154

155155

156+
class ProtocolScopeKind(ScopeKind):
157+
class Base:
158+
def __init__(
159+
self, name: str, protection: str, virtual: bool, refid: str
160+
) -> None:
161+
self.name: str = name
162+
self.protection: str = protection
163+
self.virtual: bool = virtual
164+
self.refid: str = refid
165+
166+
def __init__(self) -> None:
167+
super().__init__("protocol")
168+
self.base_classes: [ProtocolScopeKind.Base] = []
169+
170+
def add_base(self, base: ProtocolScopeKind.Base | [ProtocolScopeKind.Base]) -> None:
171+
if isinstance(base, list):
172+
for b in base:
173+
self.base_classes.append(b)
174+
else:
175+
self.base_classes.append(base)
176+
177+
def to_string(self, scope: Scope) -> str:
178+
result = ""
179+
180+
bases = []
181+
for base in self.base_classes:
182+
base_text = [base.protection]
183+
if base.virtual:
184+
base_text.append("virtual")
185+
base_text.append(base.name)
186+
bases.append(" ".join(base_text))
187+
188+
inheritance_string = " : " + ", ".join(bases) if bases else ""
189+
190+
result += f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{"
191+
192+
stringified_members = []
193+
for member in scope.get_members():
194+
stringified_members.append(member.to_string(2))
195+
stringified_members = natsorted(stringified_members)
196+
result += ("\n" if len(stringified_members) > 0 else "") + "\n".join(
197+
stringified_members
198+
)
199+
200+
result += "\n}"
201+
202+
return result
203+
204+
156205
class TemporaryScopeKind(ScopeKind):
157206
def __init__(self) -> None:
158207
super().__init__("temporary")

scripts/cxx-api/parser/snapshot.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .scope import (
99
EnumScopeKind,
1010
NamespaceScopeKind,
11+
ProtocolScopeKind,
1112
Scope,
1213
StructLikeScopeKind,
1314
TemporaryScopeKind,
@@ -84,6 +85,30 @@ def create_or_get_namespace(self, qualified_name: str) -> Scope[NamespaceScopeKi
8485
current_scope.inner_scopes[namespace_name] = new_scope
8586
return new_scope
8687

88+
def create_protocol(self, qualified_name: str) -> Scope[ProtocolScopeKind]:
89+
"""
90+
Create a protocol in the snapshot.
91+
"""
92+
path = parse_qualified_path(qualified_name)
93+
scope_path = path[0:-1]
94+
scope_name = path[-1]
95+
current_scope = self.ensure_scope(scope_path)
96+
97+
if scope_name in current_scope.inner_scopes:
98+
scope = current_scope.inner_scopes[scope_name]
99+
if scope.kind.name == "temporary":
100+
scope.kind = ProtocolScopeKind()
101+
else:
102+
raise RuntimeError(
103+
f"Identifier {scope_name} already exists in scope {current_scope.name}"
104+
)
105+
return scope
106+
else:
107+
new_scope = Scope(ProtocolScopeKind(), scope_name)
108+
new_scope.parent_scope = current_scope
109+
current_scope.inner_scopes[scope_name] = new_scope
110+
return new_scope
111+
87112
def create_enum(self, qualified_name: str) -> Scope[EnumScopeKind]:
88113
"""
89114
Create an enum in the snapshot.

scripts/cxx-api/parser/utils/type_qualification.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def qualify_type_str(type_str: str, scope: Scope) -> str:
5050
# Split template arguments and qualify each one
5151
args = _split_arguments(template_args)
5252
qualified_args = [qualify_type_str(arg.strip(), scope) for arg in args]
53-
qualified_template = "< " + ", ".join(qualified_args) + " >"
53+
qualified_template = "<" + ", ".join(qualified_args) + ">"
5454

5555
# Recursively qualify the suffix (handles nested templates, pointers, etc.)
5656
qualified_suffix = qualify_type_str(suffix, scope) if suffix else ""

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
struct test::Node {
22
public void setArray(int(&arr)[10]);
33
template <size_t N>
4-
public static std::vector< test::PropNameID > names(test::PropNameID(&&propertyNames)[N]);
4+
public static std::vector<test::PropNameID> names(test::PropNameID(&&propertyNames)[N]);
55
}
66

77
struct test::PropNameID {
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
struct test::Config {
2-
public std::optional< bool > expanded;
3-
public std::optional< std::string > label;
2+
public std::optional<bool> expanded;
3+
public std::optional<std::string> label;
44
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@ class test::Clss {
22
public virtual void fn16() = 0;
33
public void fn0(int32_t arg1);
44
public void fn1(int32_t arg1, int32_t arg2);
5-
public void fn2(std::function< int32_t(int32_t) > arg1);
6-
public void fn3(std::function< int32_t(int32_t) > arg1, std::function< int32_t(int32_t, int32_t) > arg2);
7-
public void fn4(std::map< std::string, int > m);
8-
public void fn5(std::unordered_map< K, std::vector< V > > m);
9-
public void fn6(std::tuple< int, float, std::string > t);
10-
public void fn7(std::vector< std::vector< std::pair< int, int > > > v);
11-
public void fn8(std::map< K, std::function< void(A, B) > > m);
5+
public void fn2(std::function<int32_t(int32_t)> arg1);
6+
public void fn3(std::function<int32_t(int32_t)> arg1, std::function<int32_t(int32_t, int32_t)> arg2);
7+
public void fn4(std::map<std::string, int> m);
8+
public void fn5(std::unordered_map<K, std::vector<V>> m);
9+
public void fn6(std::tuple<int, float, std::string> t);
10+
public void fn7(std::vector<std::vector<std::pair<int, int>>> v);
11+
public void fn8(std::map<K, std::function<void(A, B)>> m);
1212
public void fn9(int(*)(int, int) callback);
1313
public void fn10(void(*)(const char *, size_t) handler);
1414
public void fn11(int(*(*fp)(int))(double));
1515
public void fn12(int x = 5, std::string s = "default");
16-
public void fn13(std::function< void() > f = nullptr);
17-
public void fn14(std::vector< int > v = {1, 2, 3});
16+
public void fn13(std::function<void()> f = nullptr);
17+
public void fn14(std::vector<int> v = {1, 2, 3});
1818
public void fn15() noexcept;
1919
public void fn17() = default;
2020
public void fn18() = delete;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
protocol RCTEmptyProtocol {
2+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
@protocol RCTEmptyProtocol
9+
10+
@end

0 commit comments

Comments
 (0)