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/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ReactCommon:
exclude_patterns:
- "*/test_utils/*"
- "*/jni/*"
- "*/platform/cxx/*"
- "*/platform/cxx/*" # Should this be included in ReactCommon?
- "*/platform/windows/*"
- "*/platform/macos/*"
- "*/platform/ios/*"
Expand Down
3 changes: 1 addition & 2 deletions scripts/cxx-api/parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# LICENSE file in the root directory of this source tree.

from .main import build_snapshot
from .member import FunctionMember, FunctionModifiers
from .path_utils import get_repo_root

__all__ = ["build_snapshot", "FunctionMember", "FunctionModifiers", "get_repo_root"]
__all__ = ["build_snapshot", "get_repo_root"]
159 changes: 142 additions & 17 deletions scripts/cxx-api/parser/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

import os
import re
from pprint import pprint

from doxmlparser import compound, index
Expand All @@ -20,10 +21,14 @@
from .scope import StructLikeScopeKind
from .snapshot import Snapshot
from .template import Template
from .utils import parse_qualified_path
from .utils import Argument, extract_qualifiers, parse_qualified_path


def resolve_ref_text_name(type_def: compound.refTextType) -> str:
"""
Resolve the full text content of a refTextType, including all text
fragments and ref elements.
"""
if hasattr(type_def, "content_") and type_def.content_:
name = ""
for part in type_def.content_:
Expand All @@ -44,26 +49,74 @@ def resolve_ref_text_name(type_def: compound.refTextType) -> str:
return type_def.get_valueOf_()


def resolve_linked_text_name(type_def: compound.linkedTextType) -> str:
def extract_namespace_from_refid(refid: str) -> str:
"""Extract the namespace prefix from a doxygen refid.
e.g. 'namespacefacebook_1_1yoga_1a...' -> 'facebook::yoga'
"""
for prefix in ("namespace", "struct", "class", "union"):
if refid.startswith(prefix):
compound_part = refid[len(prefix) :]
idx = compound_part.find("_1a")
if idx != -1:
compound_part = compound_part[:idx]
return compound_part.replace("_1_1", "::")
return ""


def resolve_linked_text_name(type_def: compound.linkedTextType) -> (str, bool):
"""
Resolve the full text content of a linkedTextType, including all text
fragments and ref elements.
"""
name = ""
in_string = False

for part in type_def.content_:
if part.category == 1: # MixedContainer.CategoryText
in_string = part.value.count('"') % 2 != in_string
name += part.value
elif part.category == 3: # MixedContainer.CategoryComplex (ref element)
# For ref elements, get the text content
# For ref elements, get the text content and fully qualify using refid
text = ""
if hasattr(part.value, "get_valueOf_"):
name += part.value.get_valueOf_()
text = part.value.get_valueOf_()
elif hasattr(part.value, "valueOf_"):
name += part.value.valueOf_
text = part.value.valueOf_
else:
name += str(part.value)

# literal initializers keep "=" sign
text = str(part.value)

# Don't resolve refs inside string literals - doxygen may
# incorrectly treat symbols in strings as references
refid = getattr(part.value, "refid", None)
if refid and not in_string:
ns = extract_namespace_from_refid(refid)
if ns and not text.startswith(ns):
# The text may already start with a trailing portion of
# the namespace. For example ns="facebook::react::HighResDuration"
# and text="HighResDuration::zero". We need to find the
# longest suffix of ns that is a prefix of text (on a "::"
# boundary) and only prepend the missing part.
ns_parts = ns.split("::")
prepend = ns
for i in range(1, len(ns_parts)):
suffix = "::".join(ns_parts[i:])
if text.startswith(suffix + "::") or text == suffix:
prepend = "::".join(ns_parts[:i])
break
text = prepend + "::" + text

name += text

is_brace_initializer = False
if name.startswith("="):
# Detect assignment initializers: = value
name = name[1:]
elif name.startswith("{") and name.endswith("}"):
# Detect brace initializers: {value}
is_brace_initializer = True
name = name[1:-1].strip()

return name.strip()
return (name.strip(), is_brace_initializer)


def resolve_linked_text_full(type_def: compound.linkedTextType) -> str:
Expand Down Expand Up @@ -151,8 +204,16 @@ def get_template_params(
template_type = " ".join(parts[:-1])
template_name = parts[-1]
elif len(parts) == 1:
# Just a name like "T" with no type keyword
template_name = parts[0]
# Check if this is an unnamed template parameter
# (just "typename" or "class" with or without a default value)
# In this case, we leave name as None/empty
if parts[0] in ("typename", "class"):
# Unnamed template parameter
# e.g., "typename" or "typename = std::enable_if_t<...>"
template_name = None
else:
# Just a name like "T" with no type keyword
template_name = parts[0]

template_params.append(
Template(template_type, template_name, template_value)
Expand Down Expand Up @@ -189,8 +250,11 @@ def get_variable_member(
if is_const:
variable_type = variable_type[5:].strip()

is_brace_initializer = False
if member_def.initializer is not None:
variable_value = resolve_linked_text_name(member_def.initializer)
(variable_value, is_brace_initializer) = resolve_linked_text_name(
member_def.initializer
)

return VariableMember(
variable_name,
Expand All @@ -203,9 +267,63 @@ def get_variable_member(
variable_value,
variable_definition,
variable_argstring,
is_brace_initializer,
)


def get_doxygen_params(
function_def: compound.MemberdefType,
) -> list[tuple[str | None, str, str | None, str | None]] | None:
"""
Extract structured parameter information from doxygen <param> elements.

Returns a list of Argument tuples (qualifiers, type, name, default_value),
or None if no <param> elements are available.
"""
params = function_def.param
if not params:
return None

arguments: list[Argument] = []
for param in params:
param_type = (
resolve_ref_text_name(param.get_type()).strip() if param.get_type() else ""
)
param_name = param.declname or param.defname or None
param_default = (
resolve_ref_text_name(param.defval).strip() if param.defval else None
)

# Doxygen splits array dimensions into a separate <array> element.
# For complex declarators like "PropNameID (&&propertyNames)[N]",
# doxygen gives type="PropNameID(&&)", name="propertyNames",
# array="[N]". We must reconstruct the full declarator with the
# name embedded inside the grouping parentheses:
# PropNameID(&&propertyNames)[N]
param_array = param.array
if param_array:
# Match type ending with a pointer/reference declarator group:
# e.g. "PropNameID(&&)", "int(&)", "void(*)"
m = re.search(r"\([*&]+\)\s*$", param_type)
if m and param_name:
# Insert name before the closing ')' and append array
insert_pos = m.end() - 1 # position of trailing ')'
param_type = (
param_type[:insert_pos]
+ param_name
+ param_type[insert_pos:]
+ param_array
)
param_name = None
else:
param_type += param_array

qualifiers, core_type = extract_qualifiers(param_type)
arguments.append((qualifiers, core_type, param_name, param_default))

return arguments


def get_function_member(
function_def: compound.MemberdefType,
visibility: str,
Expand All @@ -217,18 +335,25 @@ def get_function_member(
function_name = function_def.get_name()
function_type = resolve_ref_text_name(function_def.get_type())
function_arg_string = function_def.get_argsstring()
function_virtual = (
function_def.get_virt() == "virtual"
or function_def.get_virt() == "pure-virtual"
)
is_pure_virtual = function_def.get_virt() == "pure-virtual"
function_virtual = function_def.get_virt() == "virtual" or is_pure_virtual

# Doxygen incorrectly merges "=0" into the return type for pure-virtual
# functions using trailing return types (e.g. "auto f() -> T = 0").
# Strip the trailing "=0" from the type string.
function_type = re.sub(r"\s*=\s*0\s*$", "", function_type)

doxygen_params = get_doxygen_params(function_def)

function = FunctionMember(
function_name,
function_type,
visibility,
function_arg_string,
function_virtual,
is_pure_virtual,
is_static,
doxygen_params,
)

function.add_template(get_template_params(function_def))
Expand Down Expand Up @@ -301,7 +426,7 @@ def create_enum_scope(snapshot: Snapshot, enum_def: compound.EnumdefType):
value_value = None

if enum_value_def.initializer is not None:
value_value = resolve_linked_text_name(enum_value_def.initializer)
(value_value, _) = resolve_linked_text_name(enum_value_def.initializer)

scope.add_member(
EnumMember(
Expand Down
Loading
Loading