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
9 changes: 9 additions & 0 deletions scripts/cxx-api/parser/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
parse_type_with_argstrings,
qualify_arguments,
qualify_parsed_type,
qualify_template_args_only,
qualify_type_str,
)

Expand Down Expand Up @@ -142,6 +143,10 @@ def member_kind(self) -> MemberKind:
def close(self, scope: Scope):
self._fp_arguments = qualify_arguments(self._fp_arguments, scope)
self._parsed_type = qualify_parsed_type(self._parsed_type, scope)
# Qualify template arguments in variable name for explicit specializations
# e.g., "default_value<MyType>" -> "default_value<ns::MyType>"
if "<" in self.name:
self.name = qualify_template_args_only(self.name, scope)

def _is_function_pointer(self) -> bool:
"""Check if this variable is a function pointer type."""
Expand Down Expand Up @@ -237,6 +242,10 @@ def member_kind(self) -> MemberKind:
def close(self, scope: Scope):
self.type = qualify_type_str(self.type, scope)
self.arguments = qualify_arguments(self.arguments, scope)
# Qualify template arguments in function name for explicit specializations
# e.g., "convert<MyType>" -> "convert<ns::MyType>"
if "<" in self.name:
self.name = qualify_template_args_only(self.name, scope)

def to_string(
self,
Expand Down
54 changes: 48 additions & 6 deletions scripts/cxx-api/parser/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from .member import FriendMember, Member, MemberKind
from .template import Template, TemplateList
from .utils import parse_qualified_path
from .utils import parse_qualified_path, qualify_template_args_only, qualify_type_str


# Pre-create natsort key function for efficiency
Expand All @@ -28,6 +28,10 @@ def __init__(self, name) -> None:
def to_string(self, scope: Scope) -> str:
pass

def close(self, scope: Scope) -> None:
"""Called when the scope is closed. Override to perform cleanup."""
pass

def print_scope(self, scope: Scope) -> None:
print(self.to_string(scope))

Expand Down Expand Up @@ -72,6 +76,11 @@ def add_template(self, template: Template | [Template]) -> None:
else:
self.template_list.add(template)

def close(self, scope: Scope) -> None:
"""Qualify base class names and their template arguments."""
for base in self.base_classes:
base.name = qualify_type_str(base.name, scope)

def to_string(self, scope: Scope) -> str:
result = ""

Expand Down Expand Up @@ -174,6 +183,11 @@ def add_base(self, base: ProtocolScopeKind.Base | [ProtocolScopeKind.Base]) -> N
else:
self.base_classes.append(base)

def close(self, scope: Scope) -> None:
"""Qualify base class names and their template arguments."""
for base in self.base_classes:
base.name = qualify_type_str(base.name, scope)

def to_string(self, scope: Scope) -> str:
result = ""

Expand Down Expand Up @@ -225,6 +239,11 @@ def add_base(
else:
self.base_classes.append(base)

def close(self, scope: Scope) -> None:
"""Qualify base class names and their template arguments."""
for base in self.base_classes:
base.name = qualify_type_str(base.name, scope)

def to_string(self, scope: Scope) -> str:
result = ""

Expand Down Expand Up @@ -297,13 +316,17 @@ def __init__(self, kind: ScopeKindT, name: str | None = None) -> None:

def get_qualified_name(self) -> str:
"""
Get the qualified name of the scope.
Get the qualified name of the scope, with template arguments qualified.
"""
path = []
current_scope = self
while current_scope is not None:
if current_scope.name is not None:
path.append(current_scope.name)
# Qualify template arguments in the scope name if it has any
name = current_scope.name
if "<" in name and current_scope.parent_scope is not None:
name = qualify_template_args_only(name, current_scope.parent_scope)
path.append(name)
current_scope = current_scope.parent_scope
path.reverse()
return "::".join(path)
Expand All @@ -328,10 +351,27 @@ def qualify_name(self, name: str | None) -> str | None:

current_scope = self
# Walk up to find a scope that contains the first path segment
# Check both inner_scopes AND members (for type aliases, etc.)
base_first = self._get_base_name(path[0])
while (
current_scope is not None and base_first not in current_scope.inner_scopes
):
while current_scope is not None:
# Check if it's an inner scope
if base_first in current_scope.inner_scopes:
break

# Skip self-qualification if name matches current scope's name
if (
current_scope.name
and self._get_base_name(current_scope.name) == base_first
):
current_scope = current_scope.parent_scope
continue

# Check if it's a member (type alias, variable, etc.)
for m in current_scope._members:
if m.name == base_first and not isinstance(m, FriendMember):
prefix = current_scope.get_qualified_name()
return f"{prefix}::{name}" if prefix else name

current_scope = current_scope.parent_scope

if current_scope is None:
Expand Down Expand Up @@ -404,6 +444,8 @@ def close(self) -> None:
for member in self.get_members():
member.close(self)

self.kind.close(self)

for _, inner_scope in self.inner_scopes.items():
inner_scope.close()

Expand Down
8 changes: 7 additions & 1 deletion scripts/cxx-api/parser/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
normalize_pointer_spacing,
resolve_linked_text_name,
)
from .type_qualification import qualify_arguments, qualify_parsed_type, qualify_type_str
from .type_qualification import (
qualify_arguments,
qualify_parsed_type,
qualify_template_args_only,
qualify_type_str,
)

__all__ = [
"Argument",
Expand All @@ -38,6 +43,7 @@
"parse_type_with_argstrings",
"qualify_arguments",
"qualify_parsed_type",
"qualify_template_args_only",
"qualify_type_str",
"resolve_linked_text_name",
]
7 changes: 7 additions & 0 deletions scripts/cxx-api/parser/utils/argument_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ def _parse_single_argument(arg: str) -> Argument:
)
):
return (qualifiers, base_arg, None, default_value)
# C++ parameter names cannot contain "::". Doxygen may
# incorrectly cross-reference a parameter name to a member
# variable, producing a qualified path (e.g. "bool
# ns::Class::isActive" instead of "bool isActive"). Strip
# the namespace prefix to recover the original name.
if "::" in potential_name:
potential_name = potential_name.rsplit("::", 1)[-1]
return (qualifiers, prefix, potential_name, default_value)
else:
return (qualifiers, base_arg, None, default_value)
Expand Down
95 changes: 81 additions & 14 deletions scripts/cxx-api/parser/utils/type_qualification.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,44 @@ def qualify_arguments(arguments: list[Argument], scope: Scope) -> list[Argument]

def qualify_type_str(type_str: str, scope: Scope) -> str:
"""Qualify a type string, handling trailing decorators (*, &, &&, etc.)."""
return _qualify_type_str_impl(type_str, scope, qualify_base=True)


def qualify_template_args_only(type_str: str, scope: Scope) -> str:
"""Qualify only template arguments in a type string, leaving the base type unchanged.

This is useful for class names in template specializations where the base type
is already positioned in the correct scope but the template arguments need
qualification (e.g., "MyVector< Test >" -> "MyVector< ns::Test >").
"""
return _qualify_type_str_impl(type_str, scope, qualify_base=False)


def _qualify_prefix_with_decorators(prefix: str, scope: Scope) -> str:
"""Qualify a template prefix that may have leading const/volatile qualifiers."""
stripped = prefix.lstrip()
decorator_prefix = ""
changed = True
while changed:
changed = False
# Handle leading const/volatile qualifiers (must have trailing space)
for qualifier in ("const ", "volatile "):
if stripped.startswith(qualifier):
decorator_prefix += qualifier
stripped = stripped[len(qualifier) :].lstrip()
changed = True
break

if decorator_prefix and stripped:
qualified_inner = scope.qualify_name(stripped)
if qualified_inner is not None:
return decorator_prefix + qualified_inner

return prefix


def _qualify_type_str_impl(type_str: str, scope: Scope, qualify_base: bool) -> str:
"""Implementation of type string qualification with control over base type handling."""
if not type_str:
return type_str

Expand All @@ -44,28 +82,57 @@ def qualify_type_str(type_str: str, scope: Scope) -> str:
template_args = type_str[angle_start + 1 : angle_end]
suffix = type_str[angle_end + 1 :]

# Qualify the prefix (outer type before the template)
qualified_prefix = scope.qualify_name(prefix) or prefix
# Qualify the prefix (outer type before the template) only if requested
# Use recursive qualification to handle leading decorators like "const *Type"
if qualify_base:
# Try simple qualification first
simple_qualified = scope.qualify_name(prefix)
if simple_qualified is not None:
qualified_prefix = simple_qualified
else:
# Handle prefixes with leading decorators (const, *, &, etc.)
qualified_prefix = _qualify_prefix_with_decorators(prefix, scope)
else:
qualified_prefix = prefix

# Split template arguments and qualify each one
# Split template arguments and qualify each one (always qualify args)
args = _split_arguments(template_args)
qualified_args = [qualify_type_str(arg.strip(), scope) for arg in args]
qualified_args = [
_qualify_type_str_impl(arg.strip(), scope, qualify_base=True)
for arg in args
]
qualified_template = "<" + ", ".join(qualified_args) + ">"

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

return qualified_prefix + qualified_template + qualified_suffix

# Handle leading qualifiers (const, volatile) that prevent qualify_name
# from matching. Strip them, qualify the rest, and prepend back.
for qualifier in ("const ", "volatile "):
if type_str.startswith(qualifier):
inner = type_str[len(qualifier) :]
qualified_inner = qualify_type_str(inner, scope)
if qualified_inner != inner:
return qualifier + qualified_inner
break
# If not qualifying base types, return as-is for non-template types
if not qualify_base:
return type_str

# Handle leading const/volatile qualifiers.
# Strip leading qualifiers, qualify the rest, and prepend back.
stripped = type_str.lstrip()
prefix = ""
changed = True
while changed:
changed = False
# Handle leading const/volatile qualifiers (must have trailing space)
for qualifier in ("const ", "volatile "):
if stripped.startswith(qualifier):
prefix += qualifier
stripped = stripped[len(qualifier) :].lstrip()
changed = True
break

if prefix and stripped:
qualified_inner = _qualify_type_str_impl(stripped, scope, qualify_base=True)
if qualified_inner != stripped:
return prefix + qualified_inner

# Try qualifying the entire string (handles simple cases without templates)
qualified = scope.qualify_name(type_str)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class test::Tracer {
public bool isActive;
public uint32_t subscribe(test::Tracer::StateCallback callback);
public using StateCallback = void(*)(bool isActive);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

namespace test {

class Tracer {
public:
bool isActive;
using StateCallback = void (*)(bool isActive);
uint32_t subscribe(StateCallback callback);
};

} // namespace test
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using test::BaseAlias = test::Base;

class test::Base {
public void method();
}

class test::DerivedFromAlias : public test::Base {
public void derivedMethod();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

namespace test {

class Base {
public:
void method();
};

using BaseAlias = Base;

class DerivedFromAlias : public BaseAlias {
public:
void derivedMethod();
};

} // namespace test
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
template <typename T>
T test::convert(T value);
template <typename T>
void test::process(T* ptr);
test::MyType test::convert<test::MyType>(test::MyType value);
void test::process<test::MyType>(test::MyType* ptr);

struct test::MyType {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

namespace test {

struct MyType {};

template <typename T>
T convert(T value);

template <>
MyType convert<MyType>(MyType value);

template <typename T>
void process(T *ptr);

template <>
void process<MyType>(MyType *ptr);

} // namespace test
Loading
Loading