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
2 changes: 1 addition & 1 deletion scripts/cxx-api/manual_test/.doxygen.config.template
Original file line number Diff line number Diff line change
Expand Up @@ -2442,7 +2442,7 @@ INCLUDE_FILE_PATTERNS =
# recursively expanded use the := operator instead of the = operator.
# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.

PREDEFINED =
PREDEFINED = FOLLY_PACK_PUSH="" FOLLY_PACK_POP="" FOLLY_PACK_ATTR="" __attribute__(x)="" ${PREDEFINED}

# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
# tag can be used to specify a list of macro names that should be expanded. The
Expand Down
185 changes: 179 additions & 6 deletions scripts/cxx-api/parser/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import os
import re
from enum import Enum
from pprint import pprint

from doxmlparser import compound, index

Expand All @@ -17,10 +16,11 @@
EnumMember,
FriendMember,
FunctionMember,
PropertyMember,
TypedefMember,
VariableMember,
)
from .scope import StructLikeScopeKind
from .scope import InterfaceScopeKind, ProtocolScopeKind, StructLikeScopeKind
from .snapshot import Snapshot
from .template import Template
from .utils import Argument, extract_qualifiers, parse_qualified_path
Expand Down Expand Up @@ -119,6 +119,15 @@ def _qualify_text_with_refid(text: str, refid: str) -> str:
return prepend + "::" + text


def normalize_angle_brackets(text: str) -> str:
"""Doxygen adds spaces around < and > to avoid XML ambiguity.
e.g. "NSArray< id< RCTBridgeMethod > > *" -> "NSArray<id<RCTBridgeMethod>> *"
"""
text = re.sub(r"<\s+", "<", text)
text = re.sub(r"\s+>", ">", text)
return text


def extract_namespace_from_refid(refid: str) -> str:
"""Extract the namespace prefix from a doxygen refid.
e.g. 'namespacefacebook_1_1yoga_1a...' -> 'facebook::yoga'
Expand Down Expand Up @@ -202,12 +211,13 @@ def resolve_linked_text_name(
initialier_type = InitializerType.BRACE
name = name[1:-1].strip()

return (name.strip(), initialier_type)
return (normalize_angle_brackets(name.strip()), initialier_type)


def get_base_classes(
compound_object: compound.CompounddefType,
) -> [StructLikeScopeKind.Base]:
base_class=StructLikeScopeKind.Base,
) -> list:
"""
Get the base classes of a compound object.
"""
Expand All @@ -229,7 +239,7 @@ def get_base_classes(
continue

base_classes.append(
StructLikeScopeKind.Base(
base_class(
base_name,
base_prot,
base_virt == "virtual",
Expand Down Expand Up @@ -484,6 +494,31 @@ def get_concept_member(
return concept


def get_property_member(
member_def: compound.MemberdefType,
visibility: str,
is_static: bool = False,
) -> PropertyMember:
"""
Get the property member from a member definition.
"""
property_name = member_def.get_name()
property_type = resolve_linked_text_name(member_def.get_type())[0].strip()
accessor = member_def.accessor if hasattr(member_def, "accessor") else None
is_readable = getattr(member_def, "readable", "no") == "yes"
is_writable = getattr(member_def, "writable", "no") == "yes"

return PropertyMember(
property_name,
property_type,
visibility,
is_static,
accessor,
is_readable,
is_writable,
)


def create_enum_scope(snapshot: Snapshot, enum_def: compound.EnumdefType):
"""
Create an enum scope in the snapshot.
Expand All @@ -507,6 +542,131 @@ def create_enum_scope(snapshot: Snapshot, enum_def: compound.EnumdefType):
)


def create_protocol_scope(snapshot: Snapshot, scope_def: compound.CompounddefType):
"""
Create a protocol scope in the snapshot.
"""
# Doxygen appends "-p" to ObjC protocol compound names
protocol_name = scope_def.compoundname
if protocol_name.endswith("-p"):
protocol_name = protocol_name[:-2]

protocol_scope = snapshot.create_protocol(protocol_name)
base_classes = get_base_classes(scope_def, base_class=ProtocolScopeKind.Base)
for base in base_classes:
base.name = base.name.strip("<>")
protocol_scope.kind.add_base(base_classes)
protocol_scope.location = scope_def.location.file

for section_def in scope_def.sectiondef:
kind = section_def.kind
parts = kind.split("-")
visibility = parts[0]
is_static = "static" in parts
member_type = parts[-1]

if visibility == "private":
pass
elif visibility in ("public", "protected"):
if member_type == "attrib":
for member_def in section_def.memberdef:
if member_def.kind == "variable":
protocol_scope.add_member(
get_variable_member(member_def, visibility, is_static)
)
elif member_type == "func":
for function_def in section_def.memberdef:
protocol_scope.add_member(
get_function_member(function_def, visibility, is_static)
)
elif member_type == "type":
for member_def in section_def.memberdef:
if member_def.kind == "enum":
create_enum_scope(snapshot, member_def)
elif member_def.kind == "typedef":
protocol_scope.add_member(
get_typedef_member(member_def, visibility)
)
else:
print(
f"Unknown section member kind: {member_def.kind} in {scope_def.location.file}"
)
else:
print(
f"Unknown protocol section kind: {kind} in {scope_def.location.file}"
)
elif visibility == "property":
for member_def in section_def.memberdef:
if member_def.kind == "property":
protocol_scope.add_member(
get_property_member(member_def, "public", is_static)
)
else:
print(
f"Unknown protocol visibility: {visibility} in {scope_def.location.file}"
)


def create_interface_scope(snapshot: Snapshot, scope_def: compound.CompounddefType):
"""
Create an interface scope in the snapshot (Objective-C @interface).
"""
interface_name = scope_def.compoundname

interface_scope = snapshot.create_interface(interface_name)
base_classes = get_base_classes(scope_def, base_class=InterfaceScopeKind.Base)
interface_scope.kind.add_base(base_classes)
interface_scope.location = scope_def.location.file

for section_def in scope_def.sectiondef:
kind = section_def.kind
parts = kind.split("-")
visibility = parts[0]
is_static = "static" in parts
member_type = parts[-1]

if visibility == "private":
pass
elif visibility in ("public", "protected"):
if member_type == "attrib":
for member_def in section_def.memberdef:
if member_def.kind == "variable":
interface_scope.add_member(
get_variable_member(member_def, visibility, is_static)
)
elif member_type == "func":
for function_def in section_def.memberdef:
interface_scope.add_member(
get_function_member(function_def, visibility, is_static)
)
elif member_type == "type":
for member_def in section_def.memberdef:
if member_def.kind == "enum":
create_enum_scope(snapshot, member_def)
elif member_def.kind == "typedef":
interface_scope.add_member(
get_typedef_member(member_def, visibility)
)
else:
print(
f"Unknown section member kind: {member_def.kind} in {scope_def.location.file}"
)
else:
print(
f"Unknown interface section kind: {kind} in {scope_def.location.file}"
)
elif visibility == "property":
for member_def in section_def.memberdef:
if member_def.kind == "property":
interface_scope.add_member(
get_property_member(member_def, "public", is_static)
)
else:
print(
f"Unknown interface visibility: {visibility} in {scope_def.location.file}"
)


def build_snapshot(xml_dir: str) -> Snapshot:
"""
Reads the Doxygen XML output and builds a snapshot of the C++ API.
Expand All @@ -527,12 +687,23 @@ def build_snapshot(xml_dir: str) -> Snapshot:
doxygen_object = compound.parse(detail_file, silence=True)

for compound_object in doxygen_object.compounddef:
# Check if this is an Objective-C interface by looking at the compound id
# Doxygen reports ObjC interfaces as kind="class" but with id starting with "interface"
is_objc_interface = (
compound_object.kind == "class"
and compound_object.id.startswith("interface")
)

# classes and structs are represented by the same scope with a different kind
if (
compound_object.kind == "class"
or compound_object.kind == "struct"
or compound_object.kind == "union"
):
# Handle Objective-C interfaces separately
if is_objc_interface:
create_interface_scope(snapshot, compound_object)
continue
class_scope = (
snapshot.create_struct_like(
compound_object.compoundname, StructLikeScopeKind.Type.CLASS
Expand Down Expand Up @@ -679,7 +850,9 @@ def build_snapshot(xml_dir: str) -> Snapshot:
# Contains deprecation info
pass
elif compound_object.kind == "protocol":
print(f"Protocol not supported: {compound_object.compoundname}")
create_protocol_scope(snapshot, compound_object)
elif compound_object.kind == "interface":
create_interface_scope(snapshot, compound_object)
else:
print(f"Unknown compound kind: {compound_object.kind}")

Expand Down
50 changes: 50 additions & 0 deletions scripts/cxx-api/parser/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,56 @@ def to_string(
return result


class PropertyMember(Member):
def __init__(
self,
name: str,
type: str,
visibility: str,
is_static: bool,
accessor: str | None,
is_readable: bool,
is_writable: bool,
) -> None:
super().__init__(name, visibility)
self.type: str = type
self.is_static: bool = is_static
self.accessor: str | None = accessor
self.is_readable: bool = is_readable
self.is_writable: bool = is_writable

@property
def member_kind(self) -> MemberKind:
return MemberKind.VARIABLE

def to_string(
self,
indent: int = 0,
qualification: str | None = None,
hide_visibility: bool = False,
) -> str:
name = self._get_qualified_name(qualification)
result = " " * indent

if not hide_visibility:
result += self.visibility + " "

attributes = []
if self.accessor:
attributes.append(self.accessor)
if not self.is_writable and self.is_readable:
attributes.append("readonly")

attrs_str = f"({', '.join(attributes)}) " if attributes else ""

if self.is_static:
result += "static "

result += f"@property {attrs_str}{self.type} {name};"

return result


class ConceptMember(Member):
def __init__(
self,
Expand Down
Loading