diff --git a/scripts/cxx-api/parser/__main__.py b/scripts/cxx-api/parser/__main__.py index 4e4e88089d38..bdaac3147bc0 100644 --- a/scripts/cxx-api/parser/__main__.py +++ b/scripts/cxx-api/parser/__main__.py @@ -198,6 +198,11 @@ def main(): type=str, help="Directory containing committed snapshots for comparison (used with --check)", ) + parser.add_argument( + "--view", + type=str, + help="Name of the API view to generate", + ) parser.add_argument( "--test", action="store_true", @@ -250,6 +255,9 @@ def main(): def build_snapshots(output_dir: str, verbose: bool) -> None: if not args.test: for config in snapshot_configs: + if args.view and config.snapshot_name != args.view: + continue + build_snapshot_for_view( api_view=config.snapshot_name, react_native_dir=react_native_package_dir, diff --git a/scripts/cxx-api/parser/builders.py b/scripts/cxx-api/parser/builders.py index 9beb9a93880d..f72c62f45e2b 100644 --- a/scripts/cxx-api/parser/builders.py +++ b/scripts/cxx-api/parser/builders.py @@ -249,7 +249,7 @@ def get_variable_member( if initializer_type == InitializerType.BRACE: is_brace_initializer = True - return VariableMember( + member = VariableMember( variable_name, variable_type, visibility, @@ -263,6 +263,10 @@ def get_variable_member( is_brace_initializer, ) + member.add_template(get_template_params(member_def)) + + return member + def get_doxygen_params( function_def: compound.MemberdefType, @@ -313,6 +317,21 @@ def get_doxygen_params( else: param_type += param_array + # Handle pointer-to-member-function types where the name must be + # embedded inside the declarator group. Doxygen gives: + # type = "void(ns::*)() const", name = "asFoo" + # We need to produce: + # "void(ns::*asFoo)() const" + if param_name: + m = re.search(r"\([^)]*::\*\)", param_type) + if m: + # Insert name before the closing ')' of the ptr-to-member group + insert_pos = m.end() - 1 + param_type = ( + param_type[:insert_pos] + param_name + param_type[insert_pos:] + ) + param_name = None + qualifiers, core_type = extract_qualifiers(param_type) arguments.append((qualifiers, core_type, param_name, param_default)) diff --git a/scripts/cxx-api/parser/member.py b/scripts/cxx-api/parser/member.py deleted file mode 100644 index 7681b42dc1df..000000000000 --- a/scripts/cxx-api/parser/member.py +++ /dev/null @@ -1,546 +0,0 @@ -# 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. - -from __future__ import annotations - -from abc import ABC, abstractmethod -from enum import IntEnum -from typing import TYPE_CHECKING - -from .template import Template, TemplateList -from .utils import ( - Argument, - format_arguments, - format_parsed_type, - parse_arg_string, - parse_function_pointer_argstring, - parse_type_with_argstrings, - qualify_arguments, - qualify_parsed_type, - qualify_template_args_only, - qualify_type_str, -) - -if TYPE_CHECKING: - from .scope import Scope - -STORE_INITIALIZERS_IN_SNAPSHOT = False - - -class MemberKind(IntEnum): - """ - Classification of member kinds for grouping in output. - The order here determines the output order within namespace scopes. - """ - - CONSTANT = 0 - TYPE_ALIAS = 1 - CONCEPT = 2 - FUNCTION = 3 - OPERATOR = 4 - VARIABLE = 5 - FRIEND = 6 - - -class Member(ABC): - def __init__(self, name: str, visibility: str) -> None: - self.name: str = name - self.visibility: str = visibility - self.template_list: TemplateList | None = None - - @property - @abstractmethod - def member_kind(self) -> MemberKind: - pass - - @abstractmethod - def to_string( - self, - indent: int = 0, - qualification: str | None = None, - hide_visibility: bool = False, - ) -> str: - pass - - def close(self, scope: Scope): - pass - - def _get_qualified_name(self, qualification: str | None): - return f"{qualification}::{self.name}" if qualification else self.name - - def add_template(self, template: Template | [Template]) -> None: - if template and self.template_list is None: - self.template_list = TemplateList() - - if isinstance(template, list): - for t in template: - self.template_list.add(t) - else: - self.template_list.add(template) - - -class EnumMember(Member): - def __init__(self, name: str, value: str | None) -> None: - super().__init__(name, "public") - self.value: str | None = value - - @property - def member_kind(self) -> MemberKind: - return MemberKind.CONSTANT - - def to_string( - self, - indent: int = 0, - qualification: str | None = None, - hide_visibility: bool = False, - ) -> str: - name = self._get_qualified_name(qualification) - - if not STORE_INITIALIZERS_IN_SNAPSHOT or self.value is None: - return " " * indent + f"{name}" - - return " " * indent + f"{name} = {self.value}" - - -class VariableMember(Member): - def __init__( - self, - name: str, - type: str, - visibility: str, - is_const: bool, - is_static: bool, - is_constexpr: bool, - is_mutable: bool, - value: str | None, - definition: str, - argstring: str | None = None, - is_brace_initializer: bool = False, - ) -> None: - super().__init__(name, visibility) - self.type: str = type - self.value: str | None = value - self.is_const: bool = is_const - self.is_static: bool = is_static - self.is_constexpr: bool = is_constexpr - self.is_mutable: bool = is_mutable - self.is_brace_initializer: bool = is_brace_initializer - self.definition: str = definition - self.argstring: str | None = argstring - self._fp_arguments: list[Argument] = ( - parse_function_pointer_argstring(argstring) if argstring else [] - ) - self._parsed_type: list[str | list[Argument]] = parse_type_with_argstrings(type) - - @property - def member_kind(self) -> MemberKind: - if self.is_const or self.is_constexpr: - return MemberKind.CONSTANT - return MemberKind.VARIABLE - - 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" -> "default_value" - 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.""" - return self.argstring is not None and self.argstring.startswith(")(") - - 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 + " " - - if self.is_static: - result += "static " - - if self.is_constexpr: - result += "constexpr " - - if self.is_mutable: - result += "mutable " - - if self.is_const and not self.is_constexpr: - result += "const " - - if self._is_function_pointer(): - formatted_args = format_arguments(self._fp_arguments) - qualified_type = format_parsed_type(self._parsed_type) - # Function pointer types: argstring is ")(args...)" - # If type already contains "(*", e.g. "void *(*" or "void(*", use directly - # Otherwise add "(*" to form proper function pointer syntax - if "(*" in qualified_type: - result += f"{qualified_type}{name})({formatted_args})" - else: - result += f"{qualified_type} (*{name})({formatted_args})" - else: - result += f"{format_parsed_type(self._parsed_type)} {name}" - - if STORE_INITIALIZERS_IN_SNAPSHOT and self.value is not None: - if self.is_brace_initializer: - result += f"{{{self.value}}}" - else: - result += f" = {self.value}" - - result += ";" - - return result - - -class FunctionMember(Member): - def __init__( - self, - name: str, - type: str, - visibility: str, - arg_string: str, - is_virtual: bool, - is_pure_virtual: bool, - is_static: bool, - doxygen_params: list[Argument] | None = None, - is_constexpr: bool = False, - ) -> None: - super().__init__(name, visibility) - self.type: str = type - self.is_virtual: bool = is_virtual - self.is_static: bool = is_static - self.is_constexpr: bool = is_constexpr - parsed_arguments, self.modifiers = parse_arg_string(arg_string) - self.arguments = ( - doxygen_params if doxygen_params is not None else parsed_arguments - ) - - # Doxygen signals pure-virtual via the virt attribute, but the arg string - # may not contain "= 0" (e.g. trailing return type syntax), so the - # modifiers parsed from the arg string may miss it. Propagate the flag. - if is_pure_virtual: - self.modifiers.is_pure_virtual = True - - self.is_const = self.modifiers.is_const - self.is_override = self.modifiers.is_override - - @property - def member_kind(self) -> MemberKind: - if self.name.startswith("operator"): - return MemberKind.OPERATOR - return MemberKind.FUNCTION - - 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" -> "convert" - if "<" in self.name: - self.name = qualify_template_args_only(self.name, scope) - - def to_string( - self, - indent: int = 0, - qualification: str | None = None, - hide_visibility: bool = False, - ) -> str: - name = self._get_qualified_name(qualification) - result = "" - - if self.template_list is not None: - result += " " * indent + self.template_list.to_string() + "\n" - - result += " " * indent - - if not hide_visibility: - result += self.visibility + " " - - if self.is_virtual: - result += "virtual " - - if self.is_static: - result += "static " - - if self.is_constexpr: - result += "constexpr " - - if self.type: - result += f"{self.type} " - - result += f"{name}({format_arguments(self.arguments)})" - - if self.modifiers.is_const: - result += " const" - - if self.modifiers.is_noexcept: - if self.modifiers.noexcept_expr: - result += f" noexcept({self.modifiers.noexcept_expr})" - else: - result += " noexcept" - - if self.modifiers.is_override: - result += " override" - - if self.modifiers.is_final: - result += " final" - - if self.modifiers.is_pure_virtual: - result += " = 0" - elif self.modifiers.is_default: - result += " = default" - elif self.modifiers.is_delete: - result += " = delete" - - result += ";" - return result - - -class TypedefMember(Member): - def __init__( - self, name: str, type: str, argstring: str | None, visibility: str, keyword: str - ) -> None: - super().__init__(name, visibility) - self.keyword: str = keyword - self.argstring: str | None = argstring - - # Parse function pointer argstrings (e.g. ")(int x, float y)") - self._fp_arguments: list[Argument] = ( - parse_function_pointer_argstring(argstring) if argstring else [] - ) - - # Parse inline function signatures in the type so that argument - # lists are stored as structured data, not raw strings. - self._parsed_type: list[str | list[Argument]] = parse_type_with_argstrings(type) - self.type: str = type - - @property - def member_kind(self) -> MemberKind: - return MemberKind.TYPE_ALIAS - - def close(self, scope: Scope): - self._fp_arguments = qualify_arguments(self._fp_arguments, scope) - self._parsed_type = qualify_parsed_type(self._parsed_type, scope) - - def _is_function_pointer(self) -> bool: - """Check if this typedef is a function pointer type.""" - return self.argstring is not None and self.argstring.startswith(")(") - - def get_value(self) -> str: - if self.keyword == "using": - return format_parsed_type(self._parsed_type) - elif self._is_function_pointer(): - formatted_args = format_arguments(self._fp_arguments) - qualified_type = format_parsed_type(self._parsed_type) - if "(*" in qualified_type: - return f"{qualified_type})({formatted_args})" - else: - return f"{qualified_type}(*)({formatted_args})" - else: - return self.type - - 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 self.keyword == "using" and self.template_list is not None: - result += self.template_list.to_string() + "\n" + " " * indent - - if not hide_visibility: - result += self.visibility + " " - - result += self.keyword - - if self.keyword == "using": - result += f" {name} = {format_parsed_type(self._parsed_type)};" - elif self._is_function_pointer(): - formatted_args = format_arguments(self._fp_arguments) - qualified_type = format_parsed_type(self._parsed_type) - # Function pointer typedef: "typedef return_type (*name)(args);" - # type is e.g. "void(*", argstring is ")(args...)" - if "(*" in qualified_type: - result += f" {qualified_type}{name})({formatted_args});" - else: - result += f" {qualified_type}(*{name})({formatted_args});" - else: - result += f" {self.type} {name};" - - 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 " - - # For block properties, name is embedded in the type (e.g., "void(^eventInterceptor)(args)") - if name: - result += f"@property {attrs_str}{self.type} {name};" - else: - result += f"@property {attrs_str}{self.type};" - - return result - - -class FriendMember(Member): - def __init__(self, name: str, visibility: str = "public") -> None: - super().__init__(name, visibility) - - @property - def member_kind(self) -> MemberKind: - return MemberKind.FRIEND - - 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 + " " - result += f"friend {name};" - return result - - -class ConceptMember(Member): - def __init__( - self, - name: str, - constraint: str, - ) -> None: - super().__init__(name, "public") - self.constraint: str = self._normalize_constraint(constraint) - - @property - def member_kind(self) -> MemberKind: - return MemberKind.CONCEPT - - @staticmethod - def _normalize_constraint(constraint: str) -> str: - """ - Normalize the whitespace in a concept constraint expression. - - Doxygen preserves original source indentation, which becomes - inconsistent when we flatten namespaces and use qualified names. - This method normalizes the indentation by dedenting all lines - to the minimum non-empty indentation level. - """ - if not constraint: - return constraint - - lines = constraint.split("\n") - if len(lines) <= 1: - return constraint.strip() - - # Find minimum indentation (excluding the first line and empty lines) - min_indent = float("inf") - for line in lines[1:]: - stripped = line.lstrip() - if stripped: # Skip empty lines - indent = len(line) - len(stripped) - min_indent = min(min_indent, indent) - - if min_indent == float("inf"): - min_indent = 0 - - # Dedent all lines by the minimum indentation - result_lines = [lines[0].strip()] - for line in lines[1:]: - if line.strip(): # Non-empty line - # Remove the minimum indentation to normalize - dedented = ( - line[int(min_indent) :] - if len(line) >= min_indent - else line.lstrip() - ) - result_lines.append(dedented.rstrip()) - else: - result_lines.append("") - - # Check if no line is indented - if all(not line.startswith(" ") for line in result_lines): - # Re-indent all lines but the first by 2 spaces - not_indented = result_lines - result_lines = [not_indented[0]] - for line in not_indented[1:]: - if line.strip(): # Non-empty line - result_lines.append(" " + line) - else: - result_lines.append("") - - return "\n".join(result_lines) - - def close(self, scope: Scope): - # TODO: handle unqualified references - pass - - def to_string( - self, - indent: int = 0, - qualification: str | None = None, - hide_visibility: bool = False, - ) -> str: - name = self._get_qualified_name(qualification) - result = "" - - if self.template_list is not None: - result += " " * indent + self.template_list.to_string() + "\n" - - result += " " * indent + f"concept {name} = {self.constraint};" - - return result diff --git a/scripts/cxx-api/parser/member/__init__.py b/scripts/cxx-api/parser/member/__init__.py new file mode 100644 index 000000000000..7cf0b49ba053 --- /dev/null +++ b/scripts/cxx-api/parser/member/__init__.py @@ -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. + +from .base import Member, MemberKind, STORE_INITIALIZERS_IN_SNAPSHOT +from .concept_member import ConceptMember +from .enum_member import EnumMember +from .friend_member import FriendMember +from .function_member import FunctionMember +from .property_member import PropertyMember +from .typedef_member import TypedefMember +from .variable_member import VariableMember + +__all__ = [ + "ConceptMember", + "EnumMember", + "FriendMember", + "FunctionMember", + "Member", + "MemberKind", + "PropertyMember", + "STORE_INITIALIZERS_IN_SNAPSHOT", + "TypedefMember", + "VariableMember", +] diff --git a/scripts/cxx-api/parser/member/base.py b/scripts/cxx-api/parser/member/base.py new file mode 100644 index 000000000000..47ce69906120 --- /dev/null +++ b/scripts/cxx-api/parser/member/base.py @@ -0,0 +1,69 @@ +# 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. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from enum import IntEnum +from typing import TYPE_CHECKING + +from ..template import Template, TemplateList + +if TYPE_CHECKING: + from ..scope import Scope + +STORE_INITIALIZERS_IN_SNAPSHOT = False + + +class MemberKind(IntEnum): + """ + Classification of member kinds for grouping in output. + The order here determines the output order within namespace scopes. + """ + + CONSTANT = 0 + TYPE_ALIAS = 1 + CONCEPT = 2 + FUNCTION = 3 + OPERATOR = 4 + VARIABLE = 5 + FRIEND = 6 + + +class Member(ABC): + def __init__(self, name: str, visibility: str) -> None: + self.name: str = name + self.visibility: str = visibility + self.template_list: TemplateList | None = None + + @property + @abstractmethod + def member_kind(self) -> MemberKind: + pass + + @abstractmethod + def to_string( + self, + indent: int = 0, + qualification: str | None = None, + hide_visibility: bool = False, + ) -> str: + pass + + def close(self, scope: Scope): + pass + + def _get_qualified_name(self, qualification: str | None): + return f"{qualification}::{self.name}" if qualification else self.name + + def add_template(self, template: Template | [Template]) -> None: + if template and self.template_list is None: + self.template_list = TemplateList() + + if isinstance(template, list): + for t in template: + self.template_list.add(t) + else: + self.template_list.add(template) diff --git a/scripts/cxx-api/parser/member/concept_member.py b/scripts/cxx-api/parser/member/concept_member.py new file mode 100644 index 000000000000..3e6c07a6e34f --- /dev/null +++ b/scripts/cxx-api/parser/member/concept_member.py @@ -0,0 +1,102 @@ +# 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. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import Member, MemberKind + +if TYPE_CHECKING: + from ..scope import Scope + + +class ConceptMember(Member): + def __init__( + self, + name: str, + constraint: str, + ) -> None: + super().__init__(name, "public") + self.constraint: str = self._normalize_constraint(constraint) + + @property + def member_kind(self) -> MemberKind: + return MemberKind.CONCEPT + + @staticmethod + def _normalize_constraint(constraint: str) -> str: + """ + Normalize the whitespace in a concept constraint expression. + + Doxygen preserves original source indentation, which becomes + inconsistent when we flatten namespaces and use qualified names. + This method normalizes the indentation by dedenting all lines + to the minimum non-empty indentation level. + """ + if not constraint: + return constraint + + lines = constraint.split("\n") + if len(lines) <= 1: + return constraint.strip() + + # Find minimum indentation (excluding the first line and empty lines) + min_indent = float("inf") + for line in lines[1:]: + stripped = line.lstrip() + if stripped: # Skip empty lines + indent = len(line) - len(stripped) + min_indent = min(min_indent, indent) + + if min_indent == float("inf"): + min_indent = 0 + + # Dedent all lines by the minimum indentation + result_lines = [lines[0].strip()] + for line in lines[1:]: + if line.strip(): # Non-empty line + # Remove the minimum indentation to normalize + dedented = ( + line[int(min_indent) :] + if len(line) >= min_indent + else line.lstrip() + ) + result_lines.append(dedented.rstrip()) + else: + result_lines.append("") + + # Check if no line is indented + if all(not line.startswith(" ") for line in result_lines): + # Re-indent all lines but the first by 2 spaces + not_indented = result_lines + result_lines = [not_indented[0]] + for line in not_indented[1:]: + if line.strip(): # Non-empty line + result_lines.append(" " + line) + else: + result_lines.append("") + + return "\n".join(result_lines) + + def close(self, scope: Scope): + # TODO: handle unqualified references + pass + + def to_string( + self, + indent: int = 0, + qualification: str | None = None, + hide_visibility: bool = False, + ) -> str: + name = self._get_qualified_name(qualification) + result = "" + + if self.template_list is not None: + result += " " * indent + self.template_list.to_string() + "\n" + + result += " " * indent + f"concept {name} = {self.constraint};" + + return result diff --git a/scripts/cxx-api/parser/member/enum_member.py b/scripts/cxx-api/parser/member/enum_member.py new file mode 100644 index 000000000000..a838b4221f75 --- /dev/null +++ b/scripts/cxx-api/parser/member/enum_member.py @@ -0,0 +1,31 @@ +# 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. + +from __future__ import annotations + +from .base import Member, MemberKind, STORE_INITIALIZERS_IN_SNAPSHOT + + +class EnumMember(Member): + def __init__(self, name: str, value: str | None) -> None: + super().__init__(name, "public") + self.value: str | None = value + + @property + def member_kind(self) -> MemberKind: + return MemberKind.CONSTANT + + def to_string( + self, + indent: int = 0, + qualification: str | None = None, + hide_visibility: bool = False, + ) -> str: + name = self._get_qualified_name(qualification) + + if not STORE_INITIALIZERS_IN_SNAPSHOT or self.value is None: + return " " * indent + f"{name}" + + return " " * indent + f"{name} = {self.value}" diff --git a/scripts/cxx-api/parser/member/friend_member.py b/scripts/cxx-api/parser/member/friend_member.py new file mode 100644 index 000000000000..0a00c03d7b7e --- /dev/null +++ b/scripts/cxx-api/parser/member/friend_member.py @@ -0,0 +1,30 @@ +# 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. + +from __future__ import annotations + +from .base import Member, MemberKind + + +class FriendMember(Member): + def __init__(self, name: str, visibility: str = "public") -> None: + super().__init__(name, visibility) + + @property + def member_kind(self) -> MemberKind: + return MemberKind.FRIEND + + 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 + " " + result += f"friend {name};" + return result diff --git a/scripts/cxx-api/parser/member/function_member.py b/scripts/cxx-api/parser/member/function_member.py new file mode 100644 index 000000000000..6f7c90778877 --- /dev/null +++ b/scripts/cxx-api/parser/member/function_member.py @@ -0,0 +1,124 @@ +# 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. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..utils import ( + Argument, + format_arguments, + parse_arg_string, + qualify_arguments, + qualify_template_args_only, + qualify_type_str, +) +from .base import Member, MemberKind + +if TYPE_CHECKING: + from ..scope import Scope + + +class FunctionMember(Member): + def __init__( + self, + name: str, + type: str, + visibility: str, + arg_string: str, + is_virtual: bool, + is_pure_virtual: bool, + is_static: bool, + doxygen_params: list[Argument] | None = None, + is_constexpr: bool = False, + ) -> None: + super().__init__(name, visibility) + self.type: str = type + self.is_virtual: bool = is_virtual + self.is_static: bool = is_static + self.is_constexpr: bool = is_constexpr + parsed_arguments, self.modifiers = parse_arg_string(arg_string) + self.arguments = ( + doxygen_params if doxygen_params is not None else parsed_arguments + ) + + # Doxygen signals pure-virtual via the virt attribute, but the arg string + # may not contain "= 0" (e.g. trailing return type syntax), so the + # modifiers parsed from the arg string may miss it. Propagate the flag. + if is_pure_virtual: + self.modifiers.is_pure_virtual = True + + self.is_const = self.modifiers.is_const + self.is_override = self.modifiers.is_override + + @property + def member_kind(self) -> MemberKind: + if self.name.startswith("operator"): + return MemberKind.OPERATOR + return MemberKind.FUNCTION + + 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" -> "convert" + if "<" in self.name: + self.name = qualify_template_args_only(self.name, scope) + + def to_string( + self, + indent: int = 0, + qualification: str | None = None, + hide_visibility: bool = False, + ) -> str: + name = self._get_qualified_name(qualification) + result = "" + + if self.template_list is not None: + result += " " * indent + self.template_list.to_string() + "\n" + + result += " " * indent + + if not hide_visibility: + result += self.visibility + " " + + if self.is_virtual: + result += "virtual " + + if self.is_static: + result += "static " + + if self.is_constexpr: + result += "constexpr " + + if self.type: + result += f"{self.type} " + + result += f"{name}({format_arguments(self.arguments)})" + + if self.modifiers.is_const: + result += " const" + + if self.modifiers.is_noexcept: + if self.modifiers.noexcept_expr: + result += f" noexcept({self.modifiers.noexcept_expr})" + else: + result += " noexcept" + + if self.modifiers.is_override: + result += " override" + + if self.modifiers.is_final: + result += " final" + + if self.modifiers.is_pure_virtual: + result += " = 0" + elif self.modifiers.is_default: + result += " = default" + elif self.modifiers.is_delete: + result += " = delete" + + result += ";" + return result diff --git a/scripts/cxx-api/parser/member/property_member.py b/scripts/cxx-api/parser/member/property_member.py new file mode 100644 index 000000000000..46c90e97af98 --- /dev/null +++ b/scripts/cxx-api/parser/member/property_member.py @@ -0,0 +1,62 @@ +# 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. + +from __future__ import annotations + +from .base import Member, MemberKind + + +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 " + + # For block properties, name is embedded in the type (e.g., "void(^eventInterceptor)(args)") + if name: + result += f"@property {attrs_str}{self.type} {name};" + else: + result += f"@property {attrs_str}{self.type};" + + return result diff --git a/scripts/cxx-api/parser/member/typedef_member.py b/scripts/cxx-api/parser/member/typedef_member.py new file mode 100644 index 000000000000..4e7f09b5d3a8 --- /dev/null +++ b/scripts/cxx-api/parser/member/typedef_member.py @@ -0,0 +1,99 @@ +# 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. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..utils import ( + Argument, + format_arguments, + format_parsed_type, + parse_function_pointer_argstring, + parse_type_with_argstrings, + qualify_arguments, + qualify_parsed_type, +) +from .base import Member, MemberKind + +if TYPE_CHECKING: + from ..scope import Scope + + +class TypedefMember(Member): + def __init__( + self, name: str, type: str, argstring: str | None, visibility: str, keyword: str + ) -> None: + super().__init__(name, visibility) + self.keyword: str = keyword + self.argstring: str | None = argstring + + # Parse function pointer argstrings (e.g. ")(int x, float y)") + self._fp_arguments: list[Argument] = ( + parse_function_pointer_argstring(argstring) if argstring else [] + ) + + # Parse inline function signatures in the type so that argument + # lists are stored as structured data, not raw strings. + self._parsed_type: list[str | list[Argument]] = parse_type_with_argstrings(type) + self.type: str = type + + @property + def member_kind(self) -> MemberKind: + return MemberKind.TYPE_ALIAS + + def close(self, scope: Scope): + self._fp_arguments = qualify_arguments(self._fp_arguments, scope) + self._parsed_type = qualify_parsed_type(self._parsed_type, scope) + + def _is_function_pointer(self) -> bool: + """Check if this typedef is a function pointer type.""" + return self.argstring is not None and self.argstring.startswith(")(") + + def get_value(self) -> str: + if self.keyword == "using": + return format_parsed_type(self._parsed_type) + elif self._is_function_pointer(): + formatted_args = format_arguments(self._fp_arguments) + qualified_type = format_parsed_type(self._parsed_type) + if "(*" in qualified_type: + return f"{qualified_type})({formatted_args})" + else: + return f"{qualified_type}(*)({formatted_args})" + else: + return self.type + + 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 self.keyword == "using" and self.template_list is not None: + result += self.template_list.to_string() + "\n" + " " * indent + + if not hide_visibility: + result += self.visibility + " " + + result += self.keyword + + if self.keyword == "using": + result += f" {name} = {format_parsed_type(self._parsed_type)};" + elif self._is_function_pointer(): + formatted_args = format_arguments(self._fp_arguments) + qualified_type = format_parsed_type(self._parsed_type) + # Function pointer typedef: "typedef return_type (*name)(args);" + # type is e.g. "void(*", argstring is ")(args...)" + if "(*" in qualified_type: + result += f" {qualified_type}{name})({formatted_args});" + else: + result += f" {qualified_type}(*{name})({formatted_args});" + else: + result += f" {self.type} {name};" + + return result diff --git a/scripts/cxx-api/parser/member/variable_member.py b/scripts/cxx-api/parser/member/variable_member.py new file mode 100644 index 000000000000..69c18509caa7 --- /dev/null +++ b/scripts/cxx-api/parser/member/variable_member.py @@ -0,0 +1,123 @@ +# 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. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..utils import ( + Argument, + format_arguments, + format_parsed_type, + parse_function_pointer_argstring, + parse_type_with_argstrings, + qualify_arguments, + qualify_parsed_type, + qualify_template_args_only, +) +from .base import Member, MemberKind, STORE_INITIALIZERS_IN_SNAPSHOT + +if TYPE_CHECKING: + from ..scope import Scope + + +class VariableMember(Member): + def __init__( + self, + name: str, + type: str, + visibility: str, + is_const: bool, + is_static: bool, + is_constexpr: bool, + is_mutable: bool, + value: str | None, + definition: str, + argstring: str | None = None, + is_brace_initializer: bool = False, + ) -> None: + super().__init__(name, visibility) + self.type: str = type + self.value: str | None = value + self.is_const: bool = is_const + self.is_static: bool = is_static + self.is_constexpr: bool = is_constexpr + self.is_mutable: bool = is_mutable + self.is_brace_initializer: bool = is_brace_initializer + self.definition: str = definition + self.argstring: str | None = argstring + self._fp_arguments: list[Argument] = ( + parse_function_pointer_argstring(argstring) if argstring else [] + ) + self._parsed_type: list[str | list[Argument]] = parse_type_with_argstrings(type) + + @property + def member_kind(self) -> MemberKind: + if self.is_const or self.is_constexpr: + return MemberKind.CONSTANT + return MemberKind.VARIABLE + + 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" -> "default_value" + 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.""" + return self.argstring is not None and self.argstring.startswith(")(") + + 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 self.template_list is not None: + result += self.template_list.to_string() + "\n" + " " * indent + + if not hide_visibility: + result += self.visibility + " " + + if self.is_static: + result += "static " + + if self.is_constexpr: + result += "constexpr " + + if self.is_mutable: + result += "mutable " + + if self.is_const and not self.is_constexpr: + result += "const " + + if self._is_function_pointer(): + formatted_args = format_arguments(self._fp_arguments) + qualified_type = format_parsed_type(self._parsed_type) + # Function pointer types: argstring is ")(args...)" + # If type already contains "(*", e.g. "void *(*" or "void(*", use directly + # Otherwise add "(*" to form proper function pointer syntax + if "(*" in qualified_type: + result += f"{qualified_type}{name})({formatted_args})" + else: + result += f"{qualified_type} (*{name})({formatted_args})" + else: + result += f"{format_parsed_type(self._parsed_type)} {name}" + + if STORE_INITIALIZERS_IN_SNAPSHOT and self.value is not None: + if self.is_brace_initializer: + result += f"{{{self.value}}}" + else: + result += f" = {self.value}" + + result += ";" + + return result diff --git a/scripts/cxx-api/parser/scope.py b/scripts/cxx-api/parser/scope.py deleted file mode 100644 index b4cc776cd055..000000000000 --- a/scripts/cxx-api/parser/scope.py +++ /dev/null @@ -1,530 +0,0 @@ -# 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. - -from __future__ import annotations - -from abc import ABC, abstractmethod -from enum import Enum -from typing import Generic, TypeVar - -from natsort import natsort_keygen, natsorted - -from .member import FriendMember, Member, MemberKind, TypedefMember -from .template import Template, TemplateList -from .utils import parse_qualified_path, qualify_template_args_only, qualify_type_str - - -# Pre-create natsort key function for efficiency -_natsort_key = natsort_keygen() - - -class ScopeKind(ABC): - def __init__(self, name) -> None: - self.name: str = name - - @abstractmethod - 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)) - - -class StructLikeScopeKind(ScopeKind): - class Base: - def __init__( - self, name: str, protection: str, virtual: bool, refid: str - ) -> None: - self.name: str = name - self.protection: str = protection - self.virtual: bool = virtual - self.refid: str = refid - - class Type(Enum): - CLASS = "class" - STRUCT = "struct" - UNION = "union" - - def __init__(self, type: Type) -> None: - super().__init__(type.value) - - self.base_classes: [StructLikeScopeKind.Base] = [] - self.template_list: TemplateList | None = None - - def add_base( - self, base: StructLikeScopeKind.Base | [StructLikeScopeKind.Base] - ) -> None: - if isinstance(base, list): - for b in base: - self.base_classes.append(b) - else: - self.base_classes.append(base) - - def add_template(self, template: Template | [Template]) -> None: - if template and self.template_list is None: - self.template_list = TemplateList() - - if isinstance(template, list): - for t in template: - self.template_list.add(t) - 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 = "" - - bases = [] - for base in self.base_classes: - base_text = [base.protection] - if base.virtual: - base_text.append("virtual") - base_text.append(base.name) - bases.append(" ".join(base_text)) - - inheritance_string = " : " + ", ".join(bases) if bases else "" - - if self.template_list is not None: - result += "\n" + self.template_list.to_string() + "\n" - result += f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" - - stringified_members = [] - for member in scope.get_members(): - stringified_members.append(member.to_string(2)) - stringified_members = natsorted(stringified_members) - result += ("\n" if len(stringified_members) > 0 else "") + "\n".join( - stringified_members - ) - - result += "\n}" - - return result - - -class NamespaceScopeKind(ScopeKind): - def __init__(self) -> None: - super().__init__("namespace") - - def to_string(self, scope: Scope) -> str: - qualification = scope.get_qualified_name() - - # Group members by kind - groups: dict[MemberKind, list[str]] = {kind: [] for kind in MemberKind} - - for member in scope.get_members(): - kind = member.member_kind - stringified = member.to_string(0, qualification, hide_visibility=True) - groups[kind].append(stringified) - - # Sort within each group and combine in kind order - result = [] - for kind in MemberKind: - sorted_group = natsorted(groups[kind]) - result.extend(sorted_group) - - return "\n".join(result) - - -class EnumScopeKind(ScopeKind): - def __init__(self) -> None: - super().__init__("enum") - self.type: str | None = None - - def to_string(self, scope: Scope) -> str: - result = "" - inheritance_string = f" : {self.type}" if self.type else "" - - result += ( - "\n" + f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" - ) - - stringified_members = [] - for member in scope.get_members(): - stringified_members.append(member.to_string(2) + ",") - - stringified_members = natsorted(stringified_members) - result += ("\n" if len(stringified_members) > 0 else "") + "\n".join( - stringified_members - ) - - result += "\n}" - - return result - - -class ProtocolScopeKind(ScopeKind): - class Base: - def __init__( - self, name: str, protection: str, virtual: bool, refid: str - ) -> None: - self.name: str = name - self.protection: str = protection - self.virtual: bool = virtual - self.refid: str = refid - - def __init__(self) -> None: - super().__init__("protocol") - self.base_classes: [ProtocolScopeKind.Base] = [] - - def add_base(self, base: ProtocolScopeKind.Base | [ProtocolScopeKind.Base]) -> None: - if isinstance(base, list): - for b in base: - self.base_classes.append(b) - 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 = "" - - bases = [] - for base in self.base_classes: - base_text = [base.protection] - if base.virtual: - base_text.append("virtual") - base_text.append(base.name) - bases.append(" ".join(base_text)) - - inheritance_string = " : " + ", ".join(bases) if bases else "" - - result += f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" - - stringified_members = [] - for member in scope.get_members(): - stringified_members.append(member.to_string(2)) - stringified_members = natsorted(stringified_members) - result += ("\n" if len(stringified_members) > 0 else "") + "\n".join( - stringified_members - ) - - result += "\n}" - - return result - - -class InterfaceScopeKind(ScopeKind): - class Base: - def __init__( - self, name: str, protection: str, virtual: bool, refid: str - ) -> None: - self.name: str = name - self.protection: str = protection - self.virtual: bool = virtual - self.refid: str = refid - - def __init__(self) -> None: - super().__init__("interface") - self.base_classes: [InterfaceScopeKind.Base] = [] - - def add_base( - self, base: InterfaceScopeKind.Base | [InterfaceScopeKind.Base] - ) -> None: - if isinstance(base, list): - for b in base: - self.base_classes.append(b) - 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 = "" - - bases = [] - for base in self.base_classes: - base_text = [base.protection] - if base.virtual: - base_text.append("virtual") - base_text.append(base.name) - bases.append(" ".join(base_text)) - - inheritance_string = " : " + ", ".join(bases) if bases else "" - - result += f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" - - stringified_members = [] - for member in scope.get_members(): - stringified_members.append(member.to_string(2)) - stringified_members = natsorted(stringified_members) - result += ("\n" if len(stringified_members) > 0 else "") + "\n".join( - stringified_members - ) - - result += "\n}" - - return result - - -class CategoryScopeKind(ScopeKind): - def __init__(self, class_name: str, category_name: str) -> None: - super().__init__("category") - self.class_name: str = class_name - self.category_name: str = category_name - - def to_string(self, scope: Scope) -> str: - result = f"{self.name} {self.class_name}({self.category_name}) {{" - - stringified_members = [] - for member in scope.get_members(): - stringified_members.append(member.to_string(2)) - stringified_members = natsorted(stringified_members) - result += ("\n" if len(stringified_members) > 0 else "") + "\n".join( - stringified_members - ) - - result += "\n}" - - return result - - -class TemporaryScopeKind(ScopeKind): - def __init__(self) -> None: - super().__init__("temporary") - - def to_string(self, scope: Scope) -> str: - raise RuntimeError("Temporary scope should not be printed") - - -ScopeKindT = TypeVar("ScopeKindT", bound=ScopeKind) - - -class Scope(Generic[ScopeKindT]): - def __init__(self, kind: ScopeKindT, name: str | None = None) -> None: - self.name: str | None = name - self.kind: ScopeKindT = kind - self.parent_scope: Scope | None = None - self.inner_scopes: dict[str, Scope] = {} - self.location: str | None = None - self._members: list[Member] = [] - self._private_typedefs: dict[str, TypedefMember] = {} - - def get_qualified_name(self) -> str: - """ - 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: - # 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) - - def _get_base_name(self, name: str) -> str: - """Strip template arguments from a name for scope lookup.""" - angle_idx = name.find("<") - return name[:angle_idx] if angle_idx != -1 else name - - def qualify_name(self, name: str | None) -> str | None: - """ - Qualify a name with the relevant scope if possible. - Handles template arguments by stripping them for lookup but preserving - them in the output. - """ - if not name: - return None - - path = parse_qualified_path(name) - if not path: - return 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: - # 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 - - # Check private typedefs: substitute with the expanded definition - if len(path) == 1 and base_first in current_scope._private_typedefs: - return current_scope._private_typedefs[base_first].get_value() - - current_scope = current_scope.parent_scope - - if current_scope is None: - return None - - # Remember the scope where we found the first segment — its qualified - # name is the prefix that must precede the matched path segments. - anchor_scope = current_scope - - # Walk down through the path, tracking matched segments with original template args - matched_segments: list[str] = [] - for i, path_segment in enumerate(path): - base_name = self._get_base_name(path_segment) - if base_name in current_scope.inner_scopes: - matched_segments.append(path_segment) - current_scope = current_scope.inner_scopes[base_name] - elif any( - m.name == base_name and not isinstance(m, FriendMember) - for m in current_scope._members - ): - # Found as a member, assume following segments exist in the scope - prefix = "::".join(matched_segments) - suffix = "::".join(path[i:]) - anchor_prefix = anchor_scope.get_qualified_name() - if prefix: - if anchor_prefix: - return f"{anchor_prefix}::{prefix}::{suffix}" - return f"{prefix}::{suffix}" - else: - if anchor_prefix: - return f"{anchor_prefix}::{suffix}" - return suffix - else: - # Segment not found as an inner scope or a real member of - # the current scope. When inside a struct-like scope this - # typically means Doxygen's refid-based qualification - # incorrectly placed a type under a compound that does not - # actually contain it — for example a friend declaration or - # an inherited constructor reported as a member ref. Try - # to re-qualify from the remaining unmatched segments so the - # type resolves against the broader scope hierarchy. - if isinstance(current_scope.kind, StructLikeScopeKind): - remaining = "::".join(path[i:]) - return self.qualify_name(remaining) - return None - - # Return qualified name with preserved template arguments - prefix = anchor_scope.get_qualified_name() - if prefix: - return f"{prefix}::{'::'.join(matched_segments)}" - else: - return "::".join(matched_segments) - - def add_private_typedef(self, member: TypedefMember) -> None: - """ - Store a private typedef for use during type resolution. - - Private typedefs are not included in the snapshot output, but their - definitions are substituted for references to them in public members. - """ - self._private_typedefs[member.name] = member - - def add_member(self, member: Member | None) -> None: - """ - Add a member to the scope. - """ - if member is None: - return - self._members.append(member) - - def get_members(self) -> list[Member]: - """ - Get all members of the scope. - """ - return self._members - - def close(self) -> None: - """ - Close the scope by setting the kind of all temporary scopes. - """ - for typedef in self._private_typedefs.values(): - typedef.close(self) - - for member in self.get_members(): - member.close(self) - - self.kind.close(self) - - for _, inner_scope in self.inner_scopes.items(): - inner_scope.close() - - def to_string(self) -> str: - """ - Get the string representation of the scope. - """ - # Get this scope's content (e.g., class members, free functions, ...) - this_content = self.kind.to_string(self) - - # Separate inner scopes into namespaces and non-namespaces - # Keep (scope, string) tuples to sort by scope properties - namespace_scope_items: list[tuple[Scope, str]] = [] - non_namespace_scope_items: list[tuple[Scope, str]] = [] - - for _, inner_scope in self.inner_scopes.items(): - if inner_scope.name is None: - continue - inner_str = inner_scope.to_string() - if not inner_str.strip(): - continue - - if isinstance(inner_scope.kind, NamespaceScopeKind): - namespace_scope_items.append((inner_scope, inner_str)) - else: - non_namespace_scope_items.append((inner_scope, inner_str)) - - # Sort non-namespace scopes by depth (fewer :: first) then by string - def scope_sort_key(item: tuple[Scope, str]) -> tuple: - scope, string = item - depth = scope.get_qualified_name().count("::") - return (depth, _natsort_key(string)) - - non_namespace_scope_items.sort(key=scope_sort_key) - non_namespace_scope_strings = [s for _, s in non_namespace_scope_items] - namespace_scope_strings = [s for _, s in namespace_scope_items] - - # Build result: - # 1. Free members / this scope's content first - # 2. Non-namespace inner scopes (classes, structs, enums), sorted by depth - # 3. Namespace inner scopes, each separated by "\n\n\n" (two blank lines) - - local_parts = [] - if this_content.strip(): - local_parts.append(this_content) - local_parts.extend(non_namespace_scope_strings) - - # NOTE: Don't sort local_parts together - free members should come first - local_block = "\n\n".join(local_parts) - - # Combine with namespace scopes using one more blank line for clearer separation - all_blocks = [] - if local_block.strip(): - all_blocks.append(local_block) - all_blocks.extend(natsorted(namespace_scope_strings)) - - return "\n\n\n".join(all_blocks).strip() - - def print(self): - """ - Print a scope and its contents. - """ - print(self.to_string()) diff --git a/scripts/cxx-api/parser/scope/__init__.py b/scripts/cxx-api/parser/scope/__init__.py new file mode 100644 index 000000000000..350bad569ba4 --- /dev/null +++ b/scripts/cxx-api/parser/scope/__init__.py @@ -0,0 +1,27 @@ +# 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. + +from .base_scope_kind import ScopeKind, ScopeKindT +from .category_scope_kind import CategoryScopeKind +from .enum_scope_kind import EnumScopeKind +from .interface_scope_kind import InterfaceScopeKind +from .namespace_scope_kind import NamespaceScopeKind +from .protocol_scope_kind import ProtocolScopeKind +from .scope import Scope +from .struct_like_scope_kind import StructLikeScopeKind +from .temporary_scope_kind import TemporaryScopeKind + +__all__ = [ + "CategoryScopeKind", + "EnumScopeKind", + "InterfaceScopeKind", + "NamespaceScopeKind", + "ProtocolScopeKind", + "Scope", + "ScopeKind", + "ScopeKindT", + "StructLikeScopeKind", + "TemporaryScopeKind", +] diff --git a/scripts/cxx-api/parser/scope/base_scope_kind.py b/scripts/cxx-api/parser/scope/base_scope_kind.py new file mode 100644 index 000000000000..4c3a09431162 --- /dev/null +++ b/scripts/cxx-api/parser/scope/base_scope_kind.py @@ -0,0 +1,36 @@ +# 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. + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, TypeVar + +from natsort import natsort_keygen + +if TYPE_CHECKING: + from .scope import Scope + +# Pre-create natsort key function for efficiency +_natsort_key = natsort_keygen() + + +class ScopeKind(ABC): + def __init__(self, name) -> None: + self.name: str = name + + @abstractmethod + 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)) + + +ScopeKindT = TypeVar("ScopeKindT", bound=ScopeKind) diff --git a/scripts/cxx-api/parser/scope/category_scope_kind.py b/scripts/cxx-api/parser/scope/category_scope_kind.py new file mode 100644 index 000000000000..ac62b7e9d4d1 --- /dev/null +++ b/scripts/cxx-api/parser/scope/category_scope_kind.py @@ -0,0 +1,37 @@ +# 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. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from natsort import natsorted + +from .base_scope_kind import ScopeKind + +if TYPE_CHECKING: + from .scope import Scope + + +class CategoryScopeKind(ScopeKind): + def __init__(self, class_name: str, category_name: str) -> None: + super().__init__("category") + self.class_name: str = class_name + self.category_name: str = category_name + + def to_string(self, scope: Scope) -> str: + result = f"{self.name} {self.class_name}({self.category_name}) {{" + + stringified_members = [] + for member in scope.get_members(): + stringified_members.append(member.to_string(2)) + stringified_members = natsorted(stringified_members) + result += ("\n" if len(stringified_members) > 0 else "") + "\n".join( + stringified_members + ) + + result += "\n}" + + return result diff --git a/scripts/cxx-api/parser/scope/enum_scope_kind.py b/scripts/cxx-api/parser/scope/enum_scope_kind.py new file mode 100644 index 000000000000..1e1addb0214b --- /dev/null +++ b/scripts/cxx-api/parser/scope/enum_scope_kind.py @@ -0,0 +1,42 @@ +# 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. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from natsort import natsorted + +from .base_scope_kind import ScopeKind + +if TYPE_CHECKING: + from .scope import Scope + + +class EnumScopeKind(ScopeKind): + def __init__(self) -> None: + super().__init__("enum") + self.type: str | None = None + + def to_string(self, scope: Scope) -> str: + result = "" + inheritance_string = f" : {self.type}" if self.type else "" + + result += ( + "\n" + f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" + ) + + stringified_members = [] + for member in scope.get_members(): + stringified_members.append(member.to_string(2) + ",") + + stringified_members = natsorted(stringified_members) + result += ("\n" if len(stringified_members) > 0 else "") + "\n".join( + stringified_members + ) + + result += "\n}" + + return result diff --git a/scripts/cxx-api/parser/scope/extendable.py b/scripts/cxx-api/parser/scope/extendable.py new file mode 100644 index 000000000000..4051f79c3722 --- /dev/null +++ b/scripts/cxx-api/parser/scope/extendable.py @@ -0,0 +1,38 @@ +# 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. + +from __future__ import annotations + + +class Extendable: + class Base: + def __init__( + self, name: str, protection: str, virtual: bool, refid: str + ) -> None: + self.name: str = name + self.protection: str = protection + self.virtual: bool = virtual + self.refid: str = refid + + def __init__(self) -> None: + self.base_classes = [] + + def add_base(self, base: Base | list[Base]) -> None: + if isinstance(base, list): + for b in base: + self.base_classes.append(b) + else: + self.base_classes.append(base) + + def get_inheritance_string(self) -> str: + bases = [] + for base in self.base_classes: + base_text = [base.protection] + if base.virtual: + base_text.append("virtual") + base_text.append(base.name) + bases.append(" ".join(base_text)) + + return (" : " + ", ".join(bases)) if bases else "" diff --git a/scripts/cxx-api/parser/scope/interface_scope_kind.py b/scripts/cxx-api/parser/scope/interface_scope_kind.py new file mode 100644 index 000000000000..bba180194792 --- /dev/null +++ b/scripts/cxx-api/parser/scope/interface_scope_kind.py @@ -0,0 +1,46 @@ +# 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. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from natsort import natsorted + +from ..utils import qualify_type_str +from .base_scope_kind import ScopeKind +from .extendable import Extendable + +if TYPE_CHECKING: + from .scope import Scope + + +class InterfaceScopeKind(ScopeKind, Extendable): + def __init__(self) -> None: + ScopeKind.__init__(self, "interface") + Extendable.__init__(self) + + 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 = "" + + inheritance_string = self.get_inheritance_string() + result += f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" + + stringified_members = [] + for member in scope.get_members(): + stringified_members.append(member.to_string(2)) + stringified_members = natsorted(stringified_members) + result += ("\n" if len(stringified_members) > 0 else "") + "\n".join( + stringified_members + ) + + result += "\n}" + + return result diff --git a/scripts/cxx-api/parser/scope/namespace_scope_kind.py b/scripts/cxx-api/parser/scope/namespace_scope_kind.py new file mode 100644 index 000000000000..0d7444be5473 --- /dev/null +++ b/scripts/cxx-api/parser/scope/namespace_scope_kind.py @@ -0,0 +1,40 @@ +# 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. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from natsort import natsorted + +from ..member import MemberKind +from .base_scope_kind import ScopeKind + +if TYPE_CHECKING: + from .scope import Scope + + +class NamespaceScopeKind(ScopeKind): + def __init__(self) -> None: + super().__init__("namespace") + + def to_string(self, scope: Scope) -> str: + qualification = scope.get_qualified_name() + + # Group members by kind + groups: dict[MemberKind, list[str]] = {kind: [] for kind in MemberKind} + + for member in scope.get_members(): + kind = member.member_kind + stringified = member.to_string(0, qualification, hide_visibility=True) + groups[kind].append(stringified) + + # Sort within each group and combine in kind order + result = [] + for kind in MemberKind: + sorted_group = natsorted(groups[kind]) + result.extend(sorted_group) + + return "\n".join(result) diff --git a/scripts/cxx-api/parser/scope/protocol_scope_kind.py b/scripts/cxx-api/parser/scope/protocol_scope_kind.py new file mode 100644 index 000000000000..fcbd77624c67 --- /dev/null +++ b/scripts/cxx-api/parser/scope/protocol_scope_kind.py @@ -0,0 +1,46 @@ +# 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. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from natsort import natsorted + +from ..utils import qualify_type_str +from .base_scope_kind import ScopeKind +from .extendable import Extendable + +if TYPE_CHECKING: + from .scope import Scope + + +class ProtocolScopeKind(ScopeKind, Extendable): + def __init__(self) -> None: + ScopeKind.__init__(self, "protocol") + Extendable.__init__(self) + + 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 = "" + + inheritance_string = self.get_inheritance_string() + result += f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" + + stringified_members = [] + for member in scope.get_members(): + stringified_members.append(member.to_string(2)) + stringified_members = natsorted(stringified_members) + result += ("\n" if len(stringified_members) > 0 else "") + "\n".join( + stringified_members + ) + + result += "\n}" + + return result diff --git a/scripts/cxx-api/parser/scope/scope.py b/scripts/cxx-api/parser/scope/scope.py new file mode 100644 index 000000000000..6586207ed523 --- /dev/null +++ b/scripts/cxx-api/parser/scope/scope.py @@ -0,0 +1,247 @@ +# 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. + +from __future__ import annotations + +from typing import Generic + +from natsort import natsorted + +from ..member import FriendMember, Member, TypedefMember +from ..utils import parse_qualified_path, qualify_template_args_only +from .base_scope_kind import _natsort_key, ScopeKindT +from .enum_scope_kind import EnumScopeKind +from .namespace_scope_kind import NamespaceScopeKind +from .struct_like_scope_kind import StructLikeScopeKind + + +class Scope(Generic[ScopeKindT]): + def __init__(self, kind: ScopeKindT, name: str | None = None) -> None: + self.name: str | None = name + self.kind: ScopeKindT = kind + self.parent_scope: Scope | None = None + self.inner_scopes: dict[str, Scope] = {} + self.location: str | None = None + self._members: list[Member] = [] + self._private_typedefs: dict[str, TypedefMember] = {} + + def get_qualified_name(self) -> str: + """ + 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: + # 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) + + def _get_base_name(self, name: str) -> str: + """Strip template arguments from a name for scope lookup.""" + angle_idx = name.find("<") + return name[:angle_idx] if angle_idx != -1 else name + + def qualify_name(self, name: str | None) -> str | None: + """ + Qualify a name with the relevant scope if possible. + Handles template arguments by stripping them for lookup but preserving + them in the output. + """ + if not name: + return None + + path = parse_qualified_path(name) + if not path: + return 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: + # 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 + + # Check private typedefs: substitute with the expanded definition + if len(path) == 1 and base_first in current_scope._private_typedefs: + return current_scope._private_typedefs[base_first].get_value() + + current_scope = current_scope.parent_scope + + if current_scope is None: + return None + + # Remember the scope where we found the first segment — its qualified + # name is the prefix that must precede the matched path segments. + anchor_scope = current_scope + + # Walk down through the path, tracking matched segments with original template args + matched_segments: list[str] = [] + for i, path_segment in enumerate(path): + base_name = self._get_base_name(path_segment) + if base_name in current_scope.inner_scopes: + matched_segments.append(path_segment) + current_scope = current_scope.inner_scopes[base_name] + elif any( + m.name == base_name and not isinstance(m, FriendMember) + for m in current_scope._members + ) or any( + any(m.name == base_name for m in inner._members) + for inner in current_scope.inner_scopes.values() + if isinstance(inner.kind, EnumScopeKind) + ): + # Found as a member (or as an unscoped enum value accessible + # from the parent scope), assume following segments exist + prefix = "::".join(matched_segments) + suffix = "::".join(path[i:]) + anchor_prefix = anchor_scope.get_qualified_name() + if prefix: + if anchor_prefix: + return f"{anchor_prefix}::{prefix}::{suffix}" + return f"{prefix}::{suffix}" + else: + if anchor_prefix: + return f"{anchor_prefix}::{suffix}" + return suffix + else: + # Segment not found as an inner scope or a real member of + # the current scope. When inside a struct-like scope this + # typically means Doxygen's refid-based qualification + # incorrectly placed a type under a compound that does not + # actually contain it — for example a friend declaration or + # an inherited constructor reported as a member ref. Try + # to re-qualify from the remaining unmatched segments so the + # type resolves against the broader scope hierarchy. + if isinstance(current_scope.kind, StructLikeScopeKind): + remaining = "::".join(path[i:]) + return self.qualify_name(remaining) + return None + + # Return qualified name with preserved template arguments + prefix = anchor_scope.get_qualified_name() + if prefix: + return f"{prefix}::{'::'.join(matched_segments)}" + else: + return "::".join(matched_segments) + + def add_private_typedef(self, member: TypedefMember) -> None: + """ + Store a private typedef for use during type resolution. + + Private typedefs are not included in the snapshot output, but their + definitions are substituted for references to them in public members. + """ + self._private_typedefs[member.name] = member + + def add_member(self, member: Member | None) -> None: + """ + Add a member to the scope. + """ + if member is None: + return + self._members.append(member) + + def get_members(self) -> list[Member]: + """ + Get all members of the scope. + """ + return self._members + + def close(self) -> None: + """ + Close the scope by setting the kind of all temporary scopes. + """ + for typedef in self._private_typedefs.values(): + typedef.close(self) + + for member in self.get_members(): + member.close(self) + + self.kind.close(self) + + for _, inner_scope in self.inner_scopes.items(): + inner_scope.close() + + def to_string(self) -> str: + """ + Get the string representation of the scope. + """ + # Get this scope's content (e.g., class members, free functions, ...) + this_content = self.kind.to_string(self) + + # Separate inner scopes into namespaces and non-namespaces + # Keep (scope, string) tuples to sort by scope properties + namespace_scope_items: list[tuple[Scope, str]] = [] + non_namespace_scope_items: list[tuple[Scope, str]] = [] + + for _, inner_scope in self.inner_scopes.items(): + if inner_scope.name is None: + continue + inner_str = inner_scope.to_string() + if not inner_str.strip(): + continue + + if isinstance(inner_scope.kind, NamespaceScopeKind): + namespace_scope_items.append((inner_scope, inner_str)) + else: + non_namespace_scope_items.append((inner_scope, inner_str)) + + # Sort non-namespace scopes by depth (fewer :: first) then by string + def scope_sort_key(item: tuple[Scope, str]) -> tuple: + scope, string = item + depth = scope.get_qualified_name().count("::") + return (depth, _natsort_key(string)) + + non_namespace_scope_items.sort(key=scope_sort_key) + non_namespace_scope_strings = [s for _, s in non_namespace_scope_items] + namespace_scope_strings = [s for _, s in namespace_scope_items] + + # Build result: + # 1. Free members / this scope's content first + # 2. Non-namespace inner scopes (classes, structs, enums), sorted by depth + # 3. Namespace inner scopes, each separated by "\n\n\n" (two blank lines) + + local_parts = [] + if this_content.strip(): + local_parts.append(this_content) + local_parts.extend(non_namespace_scope_strings) + + # NOTE: Don't sort local_parts together - free members should come first + local_block = "\n\n".join(local_parts) + + # Combine with namespace scopes using one more blank line for clearer separation + all_blocks = [] + if local_block.strip(): + all_blocks.append(local_block) + all_blocks.extend(natsorted(namespace_scope_strings)) + + return "\n\n\n".join(all_blocks).strip() + + def print(self): + """ + Print a scope and its contents. + """ + print(self.to_string()) diff --git a/scripts/cxx-api/parser/scope/struct_like_scope_kind.py b/scripts/cxx-api/parser/scope/struct_like_scope_kind.py new file mode 100644 index 000000000000..11c0db3e9291 --- /dev/null +++ b/scripts/cxx-api/parser/scope/struct_like_scope_kind.py @@ -0,0 +1,68 @@ +# 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. + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +from natsort import natsorted + +from ..template import Template, TemplateList +from ..utils import qualify_type_str +from .base_scope_kind import ScopeKind +from .extendable import Extendable + +if TYPE_CHECKING: + from .scope import Scope + + +class StructLikeScopeKind(ScopeKind, Extendable): + class Type(Enum): + CLASS = "class" + STRUCT = "struct" + UNION = "union" + + def __init__(self, type: Type) -> None: + ScopeKind.__init__(self, type.value) + Extendable.__init__(self) + + self.template_list: TemplateList | None = None + + def add_template(self, template: Template | [Template]) -> None: + if template and self.template_list is None: + self.template_list = TemplateList() + + if isinstance(template, list): + for t in template: + self.template_list.add(t) + 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 = "" + + if self.template_list is not None: + result += "\n" + self.template_list.to_string() + "\n" + + inheritance_string = self.get_inheritance_string() + result += f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" + + stringified_members = [] + for member in scope.get_members(): + stringified_members.append(member.to_string(2)) + stringified_members = natsorted(stringified_members) + result += ("\n" if len(stringified_members) > 0 else "") + "\n".join( + stringified_members + ) + + result += "\n}" + + return result diff --git a/scripts/cxx-api/parser/scope/temporary_scope_kind.py b/scripts/cxx-api/parser/scope/temporary_scope_kind.py new file mode 100644 index 000000000000..04e3c9953a9c --- /dev/null +++ b/scripts/cxx-api/parser/scope/temporary_scope_kind.py @@ -0,0 +1,21 @@ +# 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. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base_scope_kind import ScopeKind + +if TYPE_CHECKING: + from .scope import Scope + + +class TemporaryScopeKind(ScopeKind): + def __init__(self) -> None: + super().__init__("temporary") + + def to_string(self, scope: Scope) -> str: + raise RuntimeError("Temporary scope should not be printed") diff --git a/scripts/cxx-api/tests/snapshots/should_handle_pointer_to_member_function_param/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_pointer_to_member_function_param/snapshot.api new file mode 100644 index 000000000000..3df3cf570cee --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_pointer_to_member_function_param/snapshot.api @@ -0,0 +1,6 @@ +struct folly::dynamic { +} + + +template +R test::jsArg(const folly::dynamic& arg, R(folly::dynamic::*asFoo)() const, const T &... desc); diff --git a/scripts/cxx-api/tests/snapshots/should_handle_pointer_to_member_function_param/test.h b/scripts/cxx-api/tests/snapshots/should_handle_pointer_to_member_function_param/test.h new file mode 100644 index 000000000000..8aaea82b5a17 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_pointer_to_member_function_param/test.h @@ -0,0 +1,21 @@ +/* + * 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 folly { + +struct dynamic {}; + +} // namespace folly + +namespace test { + +template +R jsArg(const folly::dynamic &arg, R (folly::dynamic::*asFoo)() const, const T &...desc); + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_handle_template_variable/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_template_variable/snapshot.api new file mode 100644 index 000000000000..52711af8da35 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_template_variable/snapshot.api @@ -0,0 +1,7 @@ +template +const test::Strct test::Strct::VALUE; + +template +struct test::Strct { + public static const test::Strct VALUE; +} diff --git a/scripts/cxx-api/tests/snapshots/should_handle_template_variable/test.h b/scripts/cxx-api/tests/snapshots/should_handle_template_variable/test.h new file mode 100644 index 000000000000..c1caf34d39df --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_template_variable/test.h @@ -0,0 +1,20 @@ +/* + * 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 { + +template +struct Strct { + static const Strct VALUE; +}; + +template +const Strct Strct::VALUE = {}; + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_enum_value_in_template_specialization/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_enum_value_in_template_specialization/snapshot.api new file mode 100644 index 000000000000..690ad506546f --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_enum_value_in_template_specialization/snapshot.api @@ -0,0 +1,19 @@ +struct test::Event { +} + +enum test::Event::Type { + NodeAllocation, + NodeDeallocation, +} + +template +struct test::Event::TypedData { +} + +struct test::Event::TypedData { + public int config; +} + +struct test::Event::TypedData { + public int config; +} diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_enum_value_in_template_specialization/test.h b/scripts/cxx-api/tests/snapshots/should_qualify_enum_value_in_template_specialization/test.h new file mode 100644 index 000000000000..b1c42974c792 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_enum_value_in_template_specialization/test.h @@ -0,0 +1,32 @@ +/* + * 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 Event { + enum Type { + NodeAllocation, + NodeDeallocation, + }; + + template + struct TypedData {}; +}; + +template <> +struct Event::TypedData { + int config; +}; + +template <> +struct Event::TypedData { + int config; +}; + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_variable_template_specialization/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_variable_template_specialization/snapshot.api index 2d598603d29e..6248acdb5c77 100644 --- a/scripts/cxx-api/tests/snapshots/should_qualify_variable_template_specialization/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_qualify_variable_template_specialization/snapshot.api @@ -1,5 +1,7 @@ -constexpr T test::default_value; constexpr test::MyType test::default_value; +template +constexpr T test::default_value; +template T* test::null_ptr; test::MyType* test::null_ptr;