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 index b4cc776cd055..615af32a1967 100644 --- a/scripts/cxx-api/parser/scope.py +++ b/scripts/cxx-api/parser/scope.py @@ -396,8 +396,13 @@ def qualify_name(self, name: str | None) -> str | None: 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, assume following segments exist in the scope + # 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() 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;