Skip to content

Commit c03dc4f

Browse files
coadofacebook-github-bot
authored andcommitted
Add support for parsing protocols
Summary: 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
1 parent 8475dcc commit c03dc4f

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
@@ -16,15 +16,25 @@
1616
ConceptMember,
1717
EnumMember,
1818
FunctionMember,
19+
PropertyMember,
1920
TypedefMember,
2021
VariableMember,
2122
)
22-
from .scope import StructLikeScopeKind
23+
from .scope import ProtocolScopeKind, StructLikeScopeKind
2324
from .snapshot import Snapshot
2425
from .template import Template
2526
from .utils import Argument, extract_qualifiers, parse_qualified_path
2627

2728

29+
def normalize_angle_brackets(text: str) -> str:
30+
"""Doxygen adds spaces around < and > to avoid XML ambiguity.
31+
e.g. "NSArray< id< RCTBridgeMethod > > *" -> "NSArray<id<RCTBridgeMethod>> *"
32+
"""
33+
text = re.sub(r"<\s+", "<", text)
34+
text = re.sub(r"\s+>", ">", text)
35+
return text
36+
37+
2838
def extract_namespace_from_refid(refid: str) -> str:
2939
"""Extract the namespace prefix from a doxygen refid.
3040
e.g. 'namespacefacebook_1_1yoga_1a...' -> 'facebook::yoga'
@@ -118,12 +128,13 @@ def resolve_linked_text_name(
118128
initialier_type = InitializerType.BRACE
119129
name = name[1:-1].strip()
120130

121-
return (name.strip(), initialier_type)
131+
return (normalize_angle_brackets(name.strip()), initialier_type)
122132

123133

124134
def get_base_classes(
125135
compound_object: compound.CompounddefType,
126-
) -> [StructLikeScopeKind.Base]:
136+
base_class=StructLikeScopeKind.Base,
137+
) -> list:
127138
"""
128139
Get the base classes of a compound object.
129140
"""
@@ -145,7 +156,7 @@ def get_base_classes(
145156
continue
146157

147158
base_classes.append(
148-
StructLikeScopeKind.Base(
159+
base_class(
149160
base_name,
150161
base_prot,
151162
base_virt == "virtual",
@@ -392,6 +403,31 @@ def get_concept_member(
392403
return concept
393404

394405

406+
def get_property_member(
407+
member_def: compound.MemberdefType,
408+
visibility: str,
409+
is_static: bool = False,
410+
) -> PropertyMember:
411+
"""
412+
Get the property member from a member definition.
413+
"""
414+
property_name = member_def.get_name()
415+
property_type = resolve_linked_text_name(member_def.get_type())[0].strip()
416+
accessor = member_def.accessor if hasattr(member_def, "accessor") else None
417+
is_readable = getattr(member_def, "readable", "no") == "yes"
418+
is_writable = getattr(member_def, "writable", "no") == "yes"
419+
420+
return PropertyMember(
421+
property_name,
422+
property_type,
423+
visibility,
424+
is_static,
425+
accessor,
426+
is_readable,
427+
is_writable,
428+
)
429+
430+
395431
def create_enum_scope(snapshot: Snapshot, enum_def: compound.EnumdefType):
396432
"""
397433
Create an enum scope in the snapshot.
@@ -415,6 +451,71 @@ def create_enum_scope(snapshot: Snapshot, enum_def: compound.EnumdefType):
415451
)
416452

417453

454+
def create_protocol_scope(snapshot: Snapshot, scope_def: compound.CompounddefType):
455+
"""
456+
Create a protocol scope in the snapshot.
457+
"""
458+
# Doxygen appends "-p" to ObjC protocol compound names
459+
protocol_name = scope_def.compoundname
460+
if protocol_name.endswith("-p"):
461+
protocol_name = protocol_name[:-2]
462+
463+
protocol_scope = snapshot.create_protocol(protocol_name)
464+
base_classes = get_base_classes(scope_def, base_class=ProtocolScopeKind.Base)
465+
for base in base_classes:
466+
base.name = base.name.strip("<>")
467+
protocol_scope.kind.add_base(base_classes)
468+
protocol_scope.location = scope_def.location.file
469+
470+
for section_def in scope_def.sectiondef:
471+
kind = section_def.kind
472+
parts = kind.split("-")
473+
visibility = parts[0]
474+
is_static = "static" in parts
475+
member_type = parts[-1]
476+
477+
if visibility == "private":
478+
pass
479+
elif visibility in ("public", "protected"):
480+
if member_type == "attrib":
481+
for member_def in section_def.memberdef:
482+
if member_def.kind == "variable":
483+
protocol_scope.add_member(
484+
get_variable_member(member_def, visibility, is_static)
485+
)
486+
elif member_type == "func":
487+
for function_def in section_def.memberdef:
488+
protocol_scope.add_member(
489+
get_function_member(function_def, visibility, is_static)
490+
)
491+
elif member_type == "type":
492+
for member_def in section_def.memberdef:
493+
if member_def.kind == "enum":
494+
create_enum_scope(snapshot, member_def)
495+
elif member_def.kind == "typedef":
496+
protocol_scope.add_member(
497+
get_typedef_member(member_def, visibility)
498+
)
499+
else:
500+
print(
501+
f"Unknown section member kind: {member_def.kind} in {scope_def.location.file}"
502+
)
503+
else:
504+
print(
505+
f"Unknown protocol section kind: {kind} in {scope_def.location.file}"
506+
)
507+
elif visibility == "property":
508+
for member_def in section_def.memberdef:
509+
if member_def.kind == "property":
510+
protocol_scope.add_member(
511+
get_property_member(member_def, "public", is_static)
512+
)
513+
else:
514+
print(
515+
f"Unknown protocol visibility: {visibility} in {scope_def.location.file}"
516+
)
517+
518+
418519
def build_snapshot(xml_dir: str) -> Snapshot:
419520
"""
420521
Reads the Doxygen XML output and builds a snapshot of the C++ API.
@@ -567,7 +668,7 @@ def build_snapshot(xml_dir: str) -> Snapshot:
567668
# Contains deprecation info
568669
pass
569670
elif compound_object.kind == "protocol":
570-
print(f"Protocol not supported: {compound_object.compoundname}")
671+
create_protocol_scope(snapshot, compound_object)
571672
else:
572673
print(f"Unknown compound kind: {compound_object.kind}")
573674

scripts/cxx-api/parser/member.py

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

355355

356+
class PropertyMember(Member):
357+
def __init__(
358+
self,
359+
name: str,
360+
type: str,
361+
visibility: str,
362+
is_static: bool,
363+
accessor: str | None,
364+
is_readable: bool,
365+
is_writable: bool,
366+
) -> None:
367+
super().__init__(name, visibility)
368+
self.type: str = type
369+
self.is_static: bool = is_static
370+
self.accessor: str | None = accessor
371+
self.is_readable: bool = is_readable
372+
self.is_writable: bool = is_writable
373+
374+
@property
375+
def member_kind(self) -> MemberKind:
376+
return MemberKind.VARIABLE
377+
378+
def to_string(
379+
self,
380+
indent: int = 0,
381+
qualification: str | None = None,
382+
hide_visibility: bool = False,
383+
) -> str:
384+
name = self._get_qualified_name(qualification)
385+
result = " " * indent
386+
387+
if not hide_visibility:
388+
result += self.visibility + " "
389+
390+
attributes = []
391+
if self.accessor:
392+
attributes.append(self.accessor)
393+
if not self.is_writable and self.is_readable:
394+
attributes.append("readonly")
395+
396+
attrs_str = f"({', '.join(attributes)}) " if attributes else ""
397+
398+
if self.is_static:
399+
result += "static "
400+
401+
result += f"@property {attrs_str}{self.type} {name};"
402+
403+
return result
404+
405+
356406
class ConceptMember(Member):
357407
def __init__(
358408
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
@@ -46,7 +46,7 @@ def qualify_type_str(type_str: str, scope: Scope) -> str:
4646
# Split template arguments and qualify each one
4747
args = _split_arguments(template_args)
4848
qualified_args = [qualify_type_str(arg.strip(), scope) for arg in args]
49-
qualified_template = "< " + ", ".join(qualified_args) + " >"
49+
qualified_template = "<" + ", ".join(qualified_args) + ">"
5050

5151
# Recursively qualify the suffix (handles nested templates, pointers, etc.)
5252
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)