From a151674c11014ce564eaf7584995ccf420ab8c45 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 13 Mar 2026 00:07:52 -0700 Subject: [PATCH 01/10] Handle template variables in the API snapshot Summary: Changelog: [Internal] Adds support for templates in variable declaration to the c++ API snapshot generator Differential Revision: D96279463 --- scripts/cxx-api/parser/builders.py | 6 +++++- scripts/cxx-api/parser/member.py | 3 +++ .../snapshot.api | 7 +++++++ .../should_handle_template_variable/test.h | 20 +++++++++++++++++++ .../snapshot.api | 4 +++- 5 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_template_variable/snapshot.api create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_template_variable/test.h diff --git a/scripts/cxx-api/parser/builders.py b/scripts/cxx-api/parser/builders.py index 9beb9a93880d..1122433b9c14 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, diff --git a/scripts/cxx-api/parser/member.py b/scripts/cxx-api/parser/member.py index 7681b42dc1df..7c8290dad50e 100644 --- a/scripts/cxx-api/parser/member.py +++ b/scripts/cxx-api/parser/member.py @@ -162,6 +162,9 @@ def to_string( result = " " * indent + if self.template_list is not None: + result += self.template_list.to_string() + "\n" + " " * indent + if not hide_visibility: result += self.visibility + " " 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_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; From a35bcecf67f6011a1073954a75ce85405e0a4b34 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 13 Mar 2026 00:07:52 -0700 Subject: [PATCH 02/10] Fix handling of member function pointers in the API snapshot Summary: Changelog: [Internal] Fixes handling of member function pointers in the C++ Api snapshot. Differential Revision: D96279461 --- scripts/cxx-api/parser/builders.py | 15 +++++++++++++ .../snapshot.api | 6 ++++++ .../test.h | 21 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_pointer_to_member_function_param/snapshot.api create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_pointer_to_member_function_param/test.h diff --git a/scripts/cxx-api/parser/builders.py b/scripts/cxx-api/parser/builders.py index 1122433b9c14..f72c62f45e2b 100644 --- a/scripts/cxx-api/parser/builders.py +++ b/scripts/cxx-api/parser/builders.py @@ -317,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/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 From 51834febc4feea34f1cfb60108e2e2189b23d2d0 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 13 Mar 2026 00:07:52 -0700 Subject: [PATCH 03/10] Fix qualification of enum values in template specialization args Summary: Changelog: [Internal] Fixes qualification of unscoped enum values used as template specialization arguments. Differential Revision: D96279462 --- scripts/cxx-api/parser/scope.py | 7 +++- .../snapshot.api | 19 +++++++++++ .../test.h | 32 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 scripts/cxx-api/tests/snapshots/should_qualify_enum_value_in_template_specialization/snapshot.api create mode 100644 scripts/cxx-api/tests/snapshots/should_qualify_enum_value_in_template_specialization/test.h 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_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 From 16ffd31f3ac006efb71d1c2ab0ad890cb0cf658d Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 13 Mar 2026 00:07:52 -0700 Subject: [PATCH 04/10] Add option to generate a single snapshot view from CLI Summary: Changelog: [Internal] Adds `--view` argument to the snapshot generator, which allows to generate a single snapshot view instead of all of them. Differential Revision: D96280524 --- scripts/cxx-api/parser/__main__.py | 8 ++++++++ 1 file changed, 8 insertions(+) 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, From 2affee4077100190b3c556442f09f5b85cb41b4c Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 13 Mar 2026 00:07:52 -0700 Subject: [PATCH 05/10] Split member definitions into smaller files Summary: Changelog: [Internal] Splits `member.py` into multiple smaller files, each containing single member definition. Differential Revision: D96282653 --- scripts/cxx-api/parser/member.py | 549 ------------------ scripts/cxx-api/parser/member/__init__.py | 26 + scripts/cxx-api/parser/member/base.py | 69 +++ .../cxx-api/parser/member/concept_member.py | 102 ++++ scripts/cxx-api/parser/member/enum_member.py | 31 + .../cxx-api/parser/member/friend_member.py | 30 + .../cxx-api/parser/member/function_member.py | 124 ++++ .../cxx-api/parser/member/property_member.py | 62 ++ .../cxx-api/parser/member/typedef_member.py | 99 ++++ .../cxx-api/parser/member/variable_member.py | 123 ++++ 10 files changed, 666 insertions(+), 549 deletions(-) delete mode 100644 scripts/cxx-api/parser/member.py create mode 100644 scripts/cxx-api/parser/member/__init__.py create mode 100644 scripts/cxx-api/parser/member/base.py create mode 100644 scripts/cxx-api/parser/member/concept_member.py create mode 100644 scripts/cxx-api/parser/member/enum_member.py create mode 100644 scripts/cxx-api/parser/member/friend_member.py create mode 100644 scripts/cxx-api/parser/member/function_member.py create mode 100644 scripts/cxx-api/parser/member/property_member.py create mode 100644 scripts/cxx-api/parser/member/typedef_member.py create mode 100644 scripts/cxx-api/parser/member/variable_member.py diff --git a/scripts/cxx-api/parser/member.py b/scripts/cxx-api/parser/member.py deleted file mode 100644 index 7c8290dad50e..000000000000 --- a/scripts/cxx-api/parser/member.py +++ /dev/null @@ -1,549 +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 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 - - -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 From bf1046d4b25e073aa6139b71bc94f6b9de66192d Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 13 Mar 2026 00:07:52 -0700 Subject: [PATCH 06/10] Split scope definitions into smaller files Summary: Changelog: [Internal] Splits `scope.py` into multiple smaller files, each containing a separate defitnition. Differential Revision: D96284004 --- scripts/cxx-api/parser/scope/__init__.py | 27 ++ .../cxx-api/parser/scope/base_scope_kind.py | 36 +++ .../parser/scope/category_scope_kind.py | 37 +++ .../cxx-api/parser/scope/enum_scope_kind.py | 42 +++ .../parser/scope/interface_scope_kind.py | 72 +++++ .../parser/scope/namespace_scope_kind.py | 40 +++ .../parser/scope/protocol_scope_kind.py | 70 ++++ scripts/cxx-api/parser/{ => scope}/scope.py | 304 +----------------- .../parser/scope/struct_like_scope_kind.py | 93 ++++++ .../parser/scope/temporary_scope_kind.py | 21 ++ 10 files changed, 446 insertions(+), 296 deletions(-) create mode 100644 scripts/cxx-api/parser/scope/__init__.py create mode 100644 scripts/cxx-api/parser/scope/base_scope_kind.py create mode 100644 scripts/cxx-api/parser/scope/category_scope_kind.py create mode 100644 scripts/cxx-api/parser/scope/enum_scope_kind.py create mode 100644 scripts/cxx-api/parser/scope/interface_scope_kind.py create mode 100644 scripts/cxx-api/parser/scope/namespace_scope_kind.py create mode 100644 scripts/cxx-api/parser/scope/protocol_scope_kind.py rename scripts/cxx-api/parser/{ => scope}/scope.py (50%) create mode 100644 scripts/cxx-api/parser/scope/struct_like_scope_kind.py create mode 100644 scripts/cxx-api/parser/scope/temporary_scope_kind.py 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/interface_scope_kind.py b/scripts/cxx-api/parser/scope/interface_scope_kind.py new file mode 100644 index 000000000000..a39517de5b2d --- /dev/null +++ b/scripts/cxx-api/parser/scope/interface_scope_kind.py @@ -0,0 +1,72 @@ +# 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 + +if TYPE_CHECKING: + from .scope import Scope + + +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 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..0ae304fad7c7 --- /dev/null +++ b/scripts/cxx-api/parser/scope/protocol_scope_kind.py @@ -0,0 +1,70 @@ +# 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 + +if TYPE_CHECKING: + from .scope import Scope + + +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 diff --git a/scripts/cxx-api/parser/scope.py b/scripts/cxx-api/parser/scope/scope.py similarity index 50% rename from scripts/cxx-api/parser/scope.py rename to scripts/cxx-api/parser/scope/scope.py index 615af32a1967..6586207ed523 100644 --- a/scripts/cxx-api/parser/scope.py +++ b/scripts/cxx-api/parser/scope/scope.py @@ -5,304 +5,16 @@ from __future__ import annotations -from abc import ABC, abstractmethod -from enum import Enum -from typing import Generic, TypeVar +from typing import Generic -from natsort import natsort_keygen, natsorted +from natsort import 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) +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]): 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..68a7abc53179 --- /dev/null +++ b/scripts/cxx-api/parser/scope/struct_like_scope_kind.py @@ -0,0 +1,93 @@ +# 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 + +if TYPE_CHECKING: + from .scope import 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 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") From a33565a5e80de60489fb10ce74d6d596a6c866b3 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 13 Mar 2026 00:07:52 -0700 Subject: [PATCH 07/10] Move inheritance logic to a separate class Summary: Changelog: [Internal] Extracts the duplicated handling of inheritance and base classes to a separate, reusable class. Differential Revision: D96287394 --- scripts/cxx-api/parser/scope/extendable.py | 38 +++++++++++++++++++ .../parser/scope/interface_scope_kind.py | 36 +++--------------- .../parser/scope/protocol_scope_kind.py | 34 +++-------------- .../parser/scope/struct_like_scope_kind.py | 37 +++--------------- 4 files changed, 54 insertions(+), 91 deletions(-) create mode 100644 scripts/cxx-api/parser/scope/extendable.py 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 index a39517de5b2d..bba180194792 100644 --- a/scripts/cxx-api/parser/scope/interface_scope_kind.py +++ b/scripts/cxx-api/parser/scope/interface_scope_kind.py @@ -11,33 +11,16 @@ 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): - 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 InterfaceScopeKind(ScopeKind, Extendable): 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) + ScopeKind.__init__(self, "interface") + Extendable.__init__(self) def close(self, scope: Scope) -> None: """Qualify base class names and their template arguments.""" @@ -47,16 +30,7 @@ def close(self, scope: Scope) -> None: 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 "" - + inheritance_string = self.get_inheritance_string() result += f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" stringified_members = [] diff --git a/scripts/cxx-api/parser/scope/protocol_scope_kind.py b/scripts/cxx-api/parser/scope/protocol_scope_kind.py index 0ae304fad7c7..fcbd77624c67 100644 --- a/scripts/cxx-api/parser/scope/protocol_scope_kind.py +++ b/scripts/cxx-api/parser/scope/protocol_scope_kind.py @@ -11,31 +11,16 @@ 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): - 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 ProtocolScopeKind(ScopeKind, Extendable): 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) + ScopeKind.__init__(self, "protocol") + Extendable.__init__(self) def close(self, scope: Scope) -> None: """Qualify base class names and their template arguments.""" @@ -45,16 +30,7 @@ def close(self, scope: Scope) -> None: 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 "" - + inheritance_string = self.get_inheritance_string() result += f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" stringified_members = [] diff --git a/scripts/cxx-api/parser/scope/struct_like_scope_kind.py b/scripts/cxx-api/parser/scope/struct_like_scope_kind.py index 68a7abc53179..11c0db3e9291 100644 --- a/scripts/cxx-api/parser/scope/struct_like_scope_kind.py +++ b/scripts/cxx-api/parser/scope/struct_like_scope_kind.py @@ -13,41 +13,24 @@ 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): - 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 StructLikeScopeKind(ScopeKind, Extendable): class Type(Enum): CLASS = "class" STRUCT = "struct" UNION = "union" def __init__(self, type: Type) -> None: - super().__init__(type.value) + ScopeKind.__init__(self, type.value) + Extendable.__init__(self) - 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() @@ -66,18 +49,10 @@ def close(self, scope: Scope) -> None: 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" + + inheritance_string = self.get_inheritance_string() result += f"{self.name} {scope.get_qualified_name()}{inheritance_string} {{" stringified_members = [] From a85ebafc1b20671f1e13c0d66fae4067cca49057 Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 13 Mar 2026 00:07:52 -0700 Subject: [PATCH 08/10] Extract class specializations from name Summary: Changelog: [Internal] Updates the parser to extract the template specializations from name string in order to avoid repeated operations on the name string. Differential Revision: D96303732 --- scripts/cxx-api/parser/builders.py | 4 ++-- scripts/cxx-api/parser/scope/scope.py | 24 +++++++++++++++---- .../parser/scope/struct_like_scope_kind.py | 5 +++- scripts/cxx-api/parser/snapshot.py | 21 ++++++++++------ scripts/cxx-api/parser/utils/__init__.py | 3 +++ .../cxx-api/parser/utils/argument_parsing.py | 16 +++++++++++++ 6 files changed, 59 insertions(+), 14 deletions(-) diff --git a/scripts/cxx-api/parser/builders.py b/scripts/cxx-api/parser/builders.py index f72c62f45e2b..c1a749509564 100644 --- a/scripts/cxx-api/parser/builders.py +++ b/scripts/cxx-api/parser/builders.py @@ -38,6 +38,7 @@ normalize_pointer_spacing, parse_qualified_path, resolve_linked_text_name, + split_specialization, ) from .utils.argument_parsing import _find_matching_angle, _split_arguments @@ -124,8 +125,7 @@ def _fix_inherited_constructor_name( return class_unqualified_name = parse_qualified_path(compound_name)[-1] - # Strip template args for comparison - class_base_name = class_unqualified_name.split("<")[0] + class_base_name, _ = split_specialization(class_unqualified_name) if func_member.name != class_base_name: func_member.name = class_unqualified_name diff --git a/scripts/cxx-api/parser/scope/scope.py b/scripts/cxx-api/parser/scope/scope.py index 6586207ed523..b7703683da48 100644 --- a/scripts/cxx-api/parser/scope/scope.py +++ b/scripts/cxx-api/parser/scope/scope.py @@ -10,7 +10,7 @@ from natsort import natsorted from ..member import FriendMember, Member, TypedefMember -from ..utils import parse_qualified_path, qualify_template_args_only +from ..utils import parse_qualified_path, qualify_type_str from .base_scope_kind import _natsort_key, ScopeKindT from .enum_scope_kind import EnumScopeKind from .namespace_scope_kind import NamespaceScopeKind @@ -35,10 +35,14 @@ def get_qualified_name(self) -> str: 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) + if ( + isinstance(current_scope.kind, StructLikeScopeKind) + and current_scope.kind.specialization_args is not None + ): + name = ( + f"{name}<{', '.join(current_scope.kind.specialization_args)}>" + ) path.append(name) current_scope = current_scope.parent_scope path.reverse() @@ -174,6 +178,18 @@ def close(self) -> None: """ Close the scope by setting the kind of all temporary scopes. """ + # Qualify specialization args early so that members and inner scopes + # see the fully-qualified name when they call get_qualified_name(). + if ( + isinstance(self.kind, StructLikeScopeKind) + and self.kind.specialization_args is not None + and self.parent_scope is not None + ): + self.kind.specialization_args = [ + qualify_type_str(arg, self.parent_scope) + for arg in self.kind.specialization_args + ] + for typedef in self._private_typedefs.values(): typedef.close(self) diff --git a/scripts/cxx-api/parser/scope/struct_like_scope_kind.py b/scripts/cxx-api/parser/scope/struct_like_scope_kind.py index 11c0db3e9291..0a06d7e00d10 100644 --- a/scripts/cxx-api/parser/scope/struct_like_scope_kind.py +++ b/scripts/cxx-api/parser/scope/struct_like_scope_kind.py @@ -25,11 +25,14 @@ class Type(Enum): STRUCT = "struct" UNION = "union" - def __init__(self, type: Type) -> None: + def __init__( + self, type: Type, specialization_args: list[str] | None = None + ) -> None: ScopeKind.__init__(self, type.value) Extendable.__init__(self) self.template_list: TemplateList | None = None + self.specialization_args = specialization_args def add_template(self, template: Template | [Template]) -> None: if template and self.template_list is None: diff --git a/scripts/cxx-api/parser/snapshot.py b/scripts/cxx-api/parser/snapshot.py index 0ffb11df1ede..eca55a0bcb29 100644 --- a/scripts/cxx-api/parser/snapshot.py +++ b/scripts/cxx-api/parser/snapshot.py @@ -15,7 +15,7 @@ StructLikeScopeKind, TemporaryScopeKind, ) -from .utils import parse_qualified_path +from .utils import parse_qualified_path, split_specialization class Snapshot: @@ -46,19 +46,26 @@ def create_struct_like( scope_name = path[-1] current_scope = self.ensure_scope(scope_path) - if scope_name in current_scope.inner_scopes: - scope = current_scope.inner_scopes[scope_name] + base_name, specialization_args = split_specialization(scope_name) + + # Use the full name (including specialization) as the dict key so that + # base templates and their specializations are distinct entries. + scope_key = scope_name + + if scope_key in current_scope.inner_scopes: + scope = current_scope.inner_scopes[scope_key] if scope.kind.name == "temporary": - scope.kind = StructLikeScopeKind(type) + scope.kind = StructLikeScopeKind(type, specialization_args) + scope.name = base_name else: raise RuntimeError( - f"Identifier {scope_name} already exists in scope {current_scope.name}" + f"Identifier {scope_key} already exists in scope {current_scope.name}" ) return scope else: - new_scope = Scope(StructLikeScopeKind(type), scope_name) + new_scope = Scope(StructLikeScopeKind(type, specialization_args), base_name) new_scope.parent_scope = current_scope - current_scope.inner_scopes[scope_name] = new_scope + current_scope.inner_scopes[scope_key] = new_scope return new_scope def create_or_get_namespace(self, qualified_name: str) -> Scope[NamespaceScopeKind]: diff --git a/scripts/cxx-api/parser/utils/__init__.py b/scripts/cxx-api/parser/utils/__init__.py index 2a9b7536adf6..bf8858fce2bc 100644 --- a/scripts/cxx-api/parser/utils/__init__.py +++ b/scripts/cxx-api/parser/utils/__init__.py @@ -12,6 +12,7 @@ parse_arg_string, parse_function_pointer_argstring, parse_type_with_argstrings, + split_specialization, ) from .qualified_path import parse_qualified_path from .text_resolution import ( @@ -37,6 +38,7 @@ "FunctionModifiers", "InitializerType", "normalize_angle_brackets", + "normalize_pointer_spacing", "parse_arg_string", "parse_function_pointer_argstring", "parse_qualified_path", @@ -46,4 +48,5 @@ "qualify_template_args_only", "qualify_type_str", "resolve_linked_text_name", + "split_specialization", ] diff --git a/scripts/cxx-api/parser/utils/argument_parsing.py b/scripts/cxx-api/parser/utils/argument_parsing.py index 5a540d3974b2..01f4018a2fa7 100644 --- a/scripts/cxx-api/parser/utils/argument_parsing.py +++ b/scripts/cxx-api/parser/utils/argument_parsing.py @@ -166,6 +166,22 @@ def _split_arguments(args_str: str) -> list[str]: return [arg for arg in result if arg] +def split_specialization(name: str) -> tuple[str, list[str] | None]: + """Split a potentially specialized name into base name and specialization args. + + Examples: + "Foo" -> ("Foo", None) + "Foo" -> ("Foo", ["int"]) + "Foo" -> ("Foo", ["int", "float"]) + "Foo>" -> ("Foo", ["Bar"]) + """ + angle_start = name.find("<") + if angle_start == -1 or not name.endswith(">"): + return (name, None) + args = _split_arguments(name[angle_start + 1 : -1]) + return (name[:angle_start], args if args else None) + + def _prefix_is_all_qualifiers(prefix: str) -> bool: """Check if all tokens in the prefix are type qualifiers/specifiers. From 032ec565c721a6d332d24e048c5485f02a29a74d Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 13 Mar 2026 00:07:52 -0700 Subject: [PATCH 09/10] Extract member specializations from name Summary: Changelog: [Internal] Updates the C++ API snapshot generator to extract specializations from name when a member is created, instead of relying on the one stored in the name string. It also makes out-of-class definitions of members to be skipped in the snapshot as those were essentially duplicated between their declaration inside the class, and the initialization outside of it. Differential Revision: D96303740 --- scripts/cxx-api/parser/main.py | 12 ++++++++++- .../cxx-api/parser/member/function_member.py | 20 ++++++++++++------ .../cxx-api/parser/member/variable_member.py | 21 +++++++++++++------ scripts/cxx-api/parser/utils/__init__.py | 10 +++------ .../cxx-api/parser/utils/argument_parsing.py | 21 +++++++++++++++++++ .../parser/utils/type_qualification.py | 10 --------- .../should_handle_free_variable/snapshot.api | 1 + .../should_handle_free_variable/test.h | 14 +++++++++++++ .../snapshot.api | 3 --- 9 files changed, 79 insertions(+), 33 deletions(-) create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_free_variable/snapshot.api create mode 100644 scripts/cxx-api/tests/snapshots/should_handle_free_variable/test.h diff --git a/scripts/cxx-api/parser/main.py b/scripts/cxx-api/parser/main.py index c1f8b6ad1f09..ddc055bb0cce 100644 --- a/scripts/cxx-api/parser/main.py +++ b/scripts/cxx-api/parser/main.py @@ -25,7 +25,7 @@ get_variable_member, ) from .snapshot import Snapshot -from .utils import parse_qualified_path +from .utils import has_scope_resolution_outside_angles, parse_qualified_path def build_snapshot(xml_dir: str) -> Snapshot: @@ -84,12 +84,22 @@ def build_snapshot(xml_dir: str) -> Snapshot: for section_def in compound_object.sectiondef: if section_def.kind == "var": for variable_def in section_def.memberdef: + # Skip out-of-class definitions (e.g. "Strct::VALUE") + if has_scope_resolution_outside_angles( + variable_def.get_name() + ): + continue is_static = variable_def.static == "yes" namespace_scope.add_member( get_variable_member(variable_def, "public", is_static) ) elif section_def.kind == "func": for function_def in section_def.memberdef: + # Skip out-of-class definitions (e.g. "Strct::convert") + if has_scope_resolution_outside_angles( + function_def.get_name() + ): + continue function_static = function_def.static == "yes" if not function_static: diff --git a/scripts/cxx-api/parser/member/function_member.py b/scripts/cxx-api/parser/member/function_member.py index 6f7c90778877..17650c243722 100644 --- a/scripts/cxx-api/parser/member/function_member.py +++ b/scripts/cxx-api/parser/member/function_member.py @@ -12,8 +12,8 @@ format_arguments, parse_arg_string, qualify_arguments, - qualify_template_args_only, qualify_type_str, + split_specialization, ) from .base import Member, MemberKind @@ -34,7 +34,9 @@ def __init__( doxygen_params: list[Argument] | None = None, is_constexpr: bool = False, ) -> None: - super().__init__(name, visibility) + base_name, specialization_args = split_specialization(name) + super().__init__(base_name, visibility) + self.specialization_args: list[str] | None = specialization_args self.type: str = type self.is_virtual: bool = is_virtual self.is_static: bool = is_static @@ -62,10 +64,16 @@ def member_kind(self) -> MemberKind: def close(self, scope: Scope): self.type = qualify_type_str(self.type, scope) self.arguments = qualify_arguments(self.arguments, scope) - # Qualify template arguments in function name for explicit specializations - # e.g., "convert" -> "convert" - if "<" in self.name: - self.name = qualify_template_args_only(self.name, scope) + if self.specialization_args is not None: + self.specialization_args = [ + qualify_type_str(arg, scope) for arg in self.specialization_args + ] + + def _get_qualified_name(self, qualification: str | None) -> str: + name = self.name + if self.specialization_args is not None: + name = f"{name}<{', '.join(self.specialization_args)}>" + return f"{qualification}::{name}" if qualification else name def to_string( self, diff --git a/scripts/cxx-api/parser/member/variable_member.py b/scripts/cxx-api/parser/member/variable_member.py index 69c18509caa7..c69378eb835d 100644 --- a/scripts/cxx-api/parser/member/variable_member.py +++ b/scripts/cxx-api/parser/member/variable_member.py @@ -15,7 +15,8 @@ parse_type_with_argstrings, qualify_arguments, qualify_parsed_type, - qualify_template_args_only, + qualify_type_str, + split_specialization, ) from .base import Member, MemberKind, STORE_INITIALIZERS_IN_SNAPSHOT @@ -38,7 +39,9 @@ def __init__( argstring: str | None = None, is_brace_initializer: bool = False, ) -> None: - super().__init__(name, visibility) + base_name, specialization_args = split_specialization(name) + super().__init__(base_name, visibility) + self.specialization_args: list[str] | None = specialization_args self.type: str = type self.value: str | None = value self.is_const: bool = is_const @@ -62,15 +65,21 @@ def member_kind(self) -> MemberKind: def close(self, scope: Scope): self._fp_arguments = qualify_arguments(self._fp_arguments, scope) self._parsed_type = qualify_parsed_type(self._parsed_type, scope) - # Qualify template arguments in variable name for explicit specializations - # e.g., "default_value" -> "default_value" - if "<" in self.name: - self.name = qualify_template_args_only(self.name, scope) + if self.specialization_args is not None: + self.specialization_args = [ + qualify_type_str(arg, scope) for arg in self.specialization_args + ] 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 _get_qualified_name(self, qualification: str | None) -> str: + name = self.name + if self.specialization_args is not None: + name = f"{name}<{', '.join(self.specialization_args)}>" + return f"{qualification}::{name}" if qualification else name + def to_string( self, indent: int = 0, diff --git a/scripts/cxx-api/parser/utils/__init__.py b/scripts/cxx-api/parser/utils/__init__.py index bf8858fce2bc..bf2ce030127e 100644 --- a/scripts/cxx-api/parser/utils/__init__.py +++ b/scripts/cxx-api/parser/utils/__init__.py @@ -9,6 +9,7 @@ format_arguments, format_parsed_type, FunctionModifiers, + has_scope_resolution_outside_angles, parse_arg_string, parse_function_pointer_argstring, parse_type_with_argstrings, @@ -22,12 +23,7 @@ normalize_pointer_spacing, resolve_linked_text_name, ) -from .type_qualification import ( - qualify_arguments, - qualify_parsed_type, - qualify_template_args_only, - qualify_type_str, -) +from .type_qualification import qualify_arguments, qualify_parsed_type, qualify_type_str __all__ = [ "Argument", @@ -36,6 +32,7 @@ "format_arguments", "format_parsed_type", "FunctionModifiers", + "has_scope_resolution_outside_angles", "InitializerType", "normalize_angle_brackets", "normalize_pointer_spacing", @@ -45,7 +42,6 @@ "parse_type_with_argstrings", "qualify_arguments", "qualify_parsed_type", - "qualify_template_args_only", "qualify_type_str", "resolve_linked_text_name", "split_specialization", diff --git a/scripts/cxx-api/parser/utils/argument_parsing.py b/scripts/cxx-api/parser/utils/argument_parsing.py index 01f4018a2fa7..e9bcfb010eeb 100644 --- a/scripts/cxx-api/parser/utils/argument_parsing.py +++ b/scripts/cxx-api/parser/utils/argument_parsing.py @@ -116,6 +116,27 @@ def _find_matching_angle(s: str, start: int = 0) -> int: return _find_matching_bracket(s, start, "<", ">", ignore_inside="(") +def has_scope_resolution_outside_angles(name: str) -> bool: + """Check if '::' appears outside angle brackets in a name. + + Returns True for class-prefixed out-of-class definitions + (e.g. 'Strct< T >::VALUE') but False when '::' only appears inside + template arguments (e.g. 'func'). + """ + depth = 0 + i = 0 + while i < len(name): + ch = name[i] + if ch == "<": + depth += 1 + elif ch == ">": + depth -= 1 + elif ch == ":" and depth == 0 and i + 1 < len(name) and name[i + 1] == ":": + return True + i += 1 + return False + + def _iter_at_depth_zero(s: str): """Iterate over string, yielding (index, char, at_depth_zero) tuples. diff --git a/scripts/cxx-api/parser/utils/type_qualification.py b/scripts/cxx-api/parser/utils/type_qualification.py index 24b166050755..ded0978d56df 100644 --- a/scripts/cxx-api/parser/utils/type_qualification.py +++ b/scripts/cxx-api/parser/utils/type_qualification.py @@ -31,16 +31,6 @@ def qualify_type_str(type_str: str, scope: Scope) -> str: return _qualify_type_str_impl(type_str, scope, qualify_base=True) -def qualify_template_args_only(type_str: str, scope: Scope) -> str: - """Qualify only template arguments in a type string, leaving the base type unchanged. - - This is useful for class names in template specializations where the base type - is already positioned in the correct scope but the template arguments need - qualification (e.g., "MyVector< Test >" -> "MyVector< ns::Test >"). - """ - return _qualify_type_str_impl(type_str, scope, qualify_base=False) - - def _qualify_prefix_with_decorators(prefix: str, scope: Scope) -> str: """Qualify a template prefix that may have leading const/volatile qualifiers.""" stripped = prefix.lstrip() diff --git a/scripts/cxx-api/tests/snapshots/should_handle_free_variable/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_free_variable/snapshot.api new file mode 100644 index 000000000000..98c7eb48b9e5 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_free_variable/snapshot.api @@ -0,0 +1 @@ +int test::helloWorld; diff --git a/scripts/cxx-api/tests/snapshots/should_handle_free_variable/test.h b/scripts/cxx-api/tests/snapshots/should_handle_free_variable/test.h new file mode 100644 index 000000000000..1e0f3698376d --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_free_variable/test.h @@ -0,0 +1,14 @@ +/* + * 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 { + +extern int helloWorld; + +} // 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 index 52711af8da35..a24a25f6de4a 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_template_variable/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_template_variable/snapshot.api @@ -1,6 +1,3 @@ -template -const test::Strct test::Strct::VALUE; - template struct test::Strct { public static const test::Strct VALUE; From ecfe4b5f7b87ccd138091212df3695d3ca9036de Mon Sep 17 00:00:00 2001 From: Jakub Piasecki Date: Fri, 13 Mar 2026 03:11:36 -0700 Subject: [PATCH 10/10] Replace subprocess rm calls with Python stdlib Summary: Changelog: [Internal] Removes direct calls to `rm` in the snapshot generator Differential Revision: D96455685 --- scripts/cxx-api/parser/__main__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/cxx-api/parser/__main__.py b/scripts/cxx-api/parser/__main__.py index bdaac3147bc0..432633c71a4b 100644 --- a/scripts/cxx-api/parser/__main__.py +++ b/scripts/cxx-api/parser/__main__.py @@ -13,6 +13,7 @@ import argparse import os +import shutil import subprocess import sys import tempfile @@ -114,7 +115,7 @@ def build_snapshot_for_view( if os.path.exists(os.path.join(react_native_dir, "api")): if verbose: print("Deleting existing output directory") - subprocess.run(["rm", "-rf", os.path.join(react_native_dir, "api")]) + shutil.rmtree(os.path.join(react_native_dir, "api")) if verbose: print(f"Generating API view: {api_view}") @@ -163,7 +164,10 @@ def build_snapshot_for_view( # Delete the Doxygen config file if verbose: print("Deleting Doxygen config file") - subprocess.run(["rm", DOXYGEN_CONFIG_FILE], cwd=react_native_dir) + os.remove(os.path.join(react_native_dir, DOXYGEN_CONFIG_FILE)) + + if verbose: + print("Building snapshot") # build snapshot, convert to string, and save to file snapshot = build_snapshot(os.path.join(react_native_dir, "api", "xml"))