diff --git a/scripts/cxx-api/parser/__init__.py b/scripts/cxx-api/parser/__init__.py index 5a3940f6558e..9dade47a0352 100644 --- a/scripts/cxx-api/parser/__init__.py +++ b/scripts/cxx-api/parser/__init__.py @@ -4,7 +4,6 @@ # LICENSE file in the root directory of this source tree. from .main import build_snapshot -from .member import FunctionMember, FunctionModifiers from .path_utils import get_repo_root -__all__ = ["build_snapshot", "FunctionMember", "FunctionModifiers", "get_repo_root"] +__all__ = ["build_snapshot", "get_repo_root"] diff --git a/scripts/cxx-api/parser/main.py b/scripts/cxx-api/parser/main.py index 40fce52d7d66..1674484cd4b8 100644 --- a/scripts/cxx-api/parser/main.py +++ b/scripts/cxx-api/parser/main.py @@ -6,6 +6,7 @@ from __future__ import annotations import os +import re from pprint import pprint from doxmlparser import compound, index @@ -20,7 +21,7 @@ from .scope import StructLikeScopeKind from .snapshot import Snapshot from .template import Template -from .utils import parse_qualified_path +from .utils import Argument, extract_qualifiers, parse_qualified_path def resolve_ref_text_name(type_def: compound.refTextType) -> str: @@ -206,6 +207,35 @@ def get_variable_member( ) +def get_doxygen_params( + function_def: compound.MemberdefType, +) -> list[tuple[str | None, str, str | None, str | None]] | None: + """ + Extract structured parameter information from doxygen elements. + + Returns a list of Argument tuples (qualifiers, type, name, default_value), + or None if no elements are available. + """ + params = function_def.param + if not params: + return None + + arguments: list[Argument] = [] + for param in params: + param_type = ( + resolve_ref_text_name(param.get_type()).strip() if param.get_type() else "" + ) + param_name = param.declname or param.defname or None + param_default = ( + resolve_ref_text_name(param.defval).strip() if param.defval else None + ) + + qualifiers, core_type = extract_qualifiers(param_type) + arguments.append((qualifiers, core_type, param_name, param_default)) + + return arguments + + def get_function_member( function_def: compound.MemberdefType, visibility: str, @@ -217,10 +247,15 @@ def get_function_member( function_name = function_def.get_name() function_type = resolve_ref_text_name(function_def.get_type()) function_arg_string = function_def.get_argsstring() - function_virtual = ( - function_def.get_virt() == "virtual" - or function_def.get_virt() == "pure-virtual" - ) + is_pure_virtual = function_def.get_virt() == "pure-virtual" + function_virtual = function_def.get_virt() == "virtual" or is_pure_virtual + + # Doxygen incorrectly merges "=0" into the return type for pure-virtual + # functions using trailing return types (e.g. "auto f() -> T = 0"). + # Strip the trailing "=0" from the type string. + function_type = re.sub(r"\s*=\s*0\s*$", "", function_type) + + doxygen_params = get_doxygen_params(function_def) function = FunctionMember( function_name, @@ -228,7 +263,9 @@ def get_function_member( visibility, function_arg_string, function_virtual, + is_pure_virtual, is_static, + doxygen_params, ) function.add_template(get_template_params(function_def)) diff --git a/scripts/cxx-api/parser/member.py b/scripts/cxx-api/parser/member.py index 7bfddf8062e5..d6930ba55e0f 100644 --- a/scripts/cxx-api/parser/member.py +++ b/scripts/cxx-api/parser/member.py @@ -5,12 +5,21 @@ from __future__ import annotations -import re from abc import ABC, abstractmethod -from dataclasses import dataclass 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_type_str, +) if TYPE_CHECKING: from .scope import Scope @@ -90,10 +99,14 @@ def __init__( self.is_mutable: bool = is_mutable 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) def close(self, scope: Scope): - # TODO: handle unqualified references - pass + 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 variable is a function pointer type.""" @@ -125,15 +138,17 @@ def to_string( result += "mutable " 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 self.type: - result += f"{self.type}{name}{self.argstring}" + if "(*" in qualified_type: + result += f"{qualified_type}{name})({formatted_args})" else: - result += f"{self.type} (*{name}{self.argstring}" + result += f"{qualified_type} (*{name})({formatted_args})" else: - result += f"{self.type} {name}" + result += f"{format_parsed_type(self._parsed_type)} {name}" if self.value is not None and (self.is_const or self.is_constexpr): result += f" = {self.value}" @@ -143,32 +158,6 @@ def to_string( return result -@dataclass -class FunctionModifiers: - """Parsed function modifiers that appear after the parameter list.""" - - is_const: bool = False - is_override: bool = False - is_final: bool = False - is_noexcept: bool = False - noexcept_expr: str | None = None - is_pure_virtual: bool = False - is_default: bool = False - is_delete: bool = False - - -# Pattern for function pointer / pointer to member / reference to array: -# Matches: (*name), (Class::*name), (&name), (*name)[N] -# Group 1: the identifier name -_FUNC_PTR_PATTERN = re.compile( - r"\(\s*" # Opening paren - r"(?:[a-zA-Z_][a-zA-Z0-9_]*\s*::\s*)?" # Optional Class:: - r"[*&]\s*" # * or & - r"([a-zA-Z_][a-zA-Z0-9_]*)" # Capture: identifier name - r"\s*\)" # Closing paren -) - - class FunctionMember(Member): def __init__( self, @@ -177,20 +166,31 @@ def __init__( visibility: str, arg_string: str, is_virtual: bool, + is_pure_virtual: bool, is_static: bool, + doxygen_params: list[Argument] | None = None, ) -> None: super().__init__(name, visibility) self.type: str = type self.is_virtual: bool = is_virtual self.is_static: bool = is_static - self.arguments, self.modifiers = FunctionMember.parse_arg_string(arg_string) + 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 def close(self, scope: Scope): - # TODO: handle unqualified references - pass + self.type = qualify_type_str(self.type, scope) + self.arguments = qualify_arguments(self.arguments, scope) def to_string( self, @@ -218,19 +218,7 @@ def to_string( if self.type: result += f"{self.type} " - result += f"{name}(" - - for i, (arg_type, arg_name, arg_default) in enumerate(self.arguments): - if arg_name: - result += f"{arg_type} {arg_name}" - else: - result += arg_type - if arg_default: - result += f" = {arg_default}" - if i < len(self.arguments) - 1: - result += ", " - - result += ")" + result += f"{name}({format_arguments(self.arguments)})" if self.modifiers.is_const: result += " const" @@ -257,328 +245,28 @@ def to_string( result += ";" return result - @staticmethod - def _find_matching_paren(s: str, start: int = 0) -> int: - """Find the index of the closing parenthesis matching the opening one at start. - - Handles nested parentheses and angle brackets (for templates). - Returns -1 if no matching parenthesis is found. - """ - if start >= len(s) or s[start] != "(": - return -1 - - depth = 0 - angle_depth = 0 - i = start - - while i < len(s): - c = s[i] - if c == "<": - angle_depth += 1 - elif c == ">": - angle_depth -= 1 - elif c == "(": - depth += 1 - elif c == ")": - depth -= 1 - if depth == 0: - return i - i += 1 - - return -1 - - @staticmethod - def _find_matching_angle(s: str, start: int = 0) -> int: - """Find the index of the closing angle bracket matching the opening one at start. - - Handles nested angle brackets and parentheses. - Returns -1 if no matching bracket is found. - """ - if start >= len(s) or s[start] != "<": - return -1 - - depth = 0 - paren_depth = 0 - i = start - - while i < len(s): - c = s[i] - if c == "(": - paren_depth += 1 - elif c == ")": - paren_depth -= 1 - elif c == "<" and paren_depth == 0: - depth += 1 - elif c == ">" and paren_depth == 0: - depth -= 1 - if depth == 0: - return i - i += 1 - - return -1 - - @staticmethod - def _split_arguments(args_str: str) -> list[str]: - """Split arguments by comma, respecting nested structures. - - Handles: - - Nested parentheses: std::function - - Nested angle brackets: std::map - - Brace initializers: std::vector v = {1, 2, 3} - """ - result = [] - current = [] - paren_depth = 0 - angle_depth = 0 - brace_depth = 0 - - for c in args_str: - if c == "<": - angle_depth += 1 - elif c == ">": - angle_depth = max(0, angle_depth - 1) - elif c == "(": - paren_depth += 1 - elif c == ")": - paren_depth = max(0, paren_depth - 1) - elif c == "{": - brace_depth += 1 - elif c == "}": - brace_depth = max(0, brace_depth - 1) - elif ( - c == "," and paren_depth == 0 and angle_depth == 0 and brace_depth == 0 - ): - result.append("".join(current).strip()) - current = [] - continue - current.append(c) - - if current: - result.append("".join(current).strip()) - - return [arg for arg in result if arg] - - # C++ reserved keywords that serve as type qualifiers/specifiers. - # When ALL tokens in the prefix (everything before the last token) are - # from this set, the last token must be part of the type, not a name. - # Primitive type names (int, bool, char, etc.) are intentionally excluded - # to avoid ambiguity with common named params like "int x" or "bool flag". - _CPP_TYPE_QUALIFIERS = frozenset( - { - "const", - "volatile", - "mutable", - "unsigned", - "signed", - "long", - "short", - } - ) - - @staticmethod - def _prefix_is_all_qualifiers(prefix: str) -> bool: - """Check if all tokens in the prefix are type qualifiers/specifiers. - - When the prefix consists entirely of reserved qualifiers (e.g., "const", - "const unsigned"), the following token cannot be a parameter name — it - must be completing the type. - """ - return all(t in FunctionMember._CPP_TYPE_QUALIFIERS for t in prefix.split()) - - @staticmethod - def _looks_like_type_token(token: str) -> bool: - """Check if a token ends with type-related punctuation. - - Returns True if the token ends with pointer, reference, template closer, - or array bracket — indicating it's part of a type, not a name. - """ - return ( - token.endswith("*") - or token.endswith("&") - or token.endswith(">") - or token.endswith("]") - ) - - @staticmethod - def _parse_single_argument(arg: str) -> tuple[str, str, str | None]: - """Parse a single C++ argument into (type, name, default_value). - - Handles complex cases: - - Regular: "int x" -> ("int", "x", None) - - With default: "int x = 5" -> ("int", "x", "5") - - Function pointer: "int (*callback)(int, int)" -> ("int (*)(int, int)", "callback", None) - - Pointer to member: "void (Class::*method)()" -> ("void (Class::*)()", "method", None) - - Reference to array: "int (&arr)[10]" -> ("int (&)[10]", "arr", None) - - No name (void): "void" -> ("void", "", None) - - Unnamed multi-token: "const bool" -> ("const bool", "", None) - """ - arg = arg.strip() - if not arg: - return ("", "", None) - - default_value: str | None = None - base_arg = arg - - # Handle default values: split on '=' but respect nested structures - eq_pos = FunctionMember._find_default_value_start(arg) - if eq_pos != -1: - base_arg = arg[:eq_pos].strip() - default_value = arg[eq_pos + 1 :].strip() - - # Try to match function pointer / pointer to member / reference to array pattern - match = _FUNC_PTR_PATTERN.search(base_arg) - if match: - name = match.group(1) - # Reconstruct type by removing the name from the pattern - type_str = base_arg[: match.start(1)] + base_arg[match.end(1) :] - return (type_str.strip(), name, default_value) - - # Regular case: last token is the name - parts = base_arg.rsplit(None, 1) - if len(parts) == 2: - prefix, potential_name = parts - if FunctionMember._looks_like_type_token( - potential_name - ) or FunctionMember._prefix_is_all_qualifiers(prefix): - return (base_arg, "", default_value) - return (prefix, potential_name, default_value) - else: - return (base_arg, "", default_value) - - @staticmethod - def _find_default_value_start(arg: str) -> int: - """Find the position of '=' that starts a default value. - - Returns -1 if no default value found. - Ignores '=' inside nested structures like templates or lambdas. - """ - paren_depth = 0 - angle_depth = 0 - brace_depth = 0 - - i = 0 - while i < len(arg): - c = arg[i] - if c == "<": - angle_depth += 1 - elif c == ">": - angle_depth = max(0, angle_depth - 1) - elif c == "(": - paren_depth += 1 - elif c == ")": - paren_depth = max(0, paren_depth - 1) - elif c == "{": - brace_depth += 1 - elif c == "}": - brace_depth = max(0, brace_depth - 1) - elif ( - c == "=" and paren_depth == 0 and angle_depth == 0 and brace_depth == 0 - ): - return i - i += 1 - - return -1 - - @staticmethod - def _parse_modifiers(modifiers_str: str) -> FunctionModifiers: - """Parse function modifiers after the parameter list. - - Handles: const, override, final, noexcept, noexcept(expr), = 0, = default, = delete - """ - result = FunctionModifiers() - s = modifiers_str.strip() - - # Handle = 0, = default, = delete - eq_match = re.search(r"=\s*(0|default|delete)\s*$", s) - if eq_match: - value = eq_match.group(1) - if value == "0": - result.is_pure_virtual = True - elif value == "default": - result.is_default = True - elif value == "delete": - result.is_delete = True - s = s[: eq_match.start()].strip() - - # Handle noexcept with optional expression - noexcept_match = re.search(r"\bnoexcept\b", s) - if noexcept_match: - result.is_noexcept = True - # Check for noexcept(expr) - rest = s[noexcept_match.end() :] - rest_stripped = rest.lstrip() - if rest_stripped.startswith("("): - paren_end = FunctionMember._find_matching_paren(rest_stripped, 0) - if paren_end != -1: - result.noexcept_expr = rest_stripped[1:paren_end].strip() - # Remove noexcept and its expression from the string for further parsing - s = s[: noexcept_match.start()] + s[noexcept_match.end() :] - if result.noexcept_expr: - # Also remove the (expr) part - s = re.sub(r"\([^)]*\)", "", s, count=1) - - # Parse remaining tokens - tokens = s.split() - for token in tokens: - if token == "const": - result.is_const = True - elif token == "override": - result.is_override = True - elif token == "final": - result.is_final = True - - return result - - @staticmethod - def parse_arg_string( - arg_string: str, - ) -> tuple[list[tuple[str, str, str | None]], FunctionModifiers]: - """Parse a C++ function argument string. - - Args: - arg_string: String in format "(type1 arg1, type2 arg2 = default) [modifiers]" - - Returns: - Tuple of (arguments, modifiers) where: - - arguments: list of (type, name, default_value) tuples - - modifiers: FunctionModifiers dataclass with parsed modifier flags - """ - arg_string = arg_string.strip() - - if not arg_string.startswith("("): - return ([], FunctionModifiers()) - - close_paren = FunctionMember._find_matching_paren(arg_string, 0) - if close_paren == -1: - return ([], FunctionModifiers()) - - args_content = arg_string[1:close_paren].strip() - modifiers_str = arg_string[close_paren + 1 :] - - arguments: list[tuple[str, str, str | None]] = [] - if args_content: - for arg in FunctionMember._split_arguments(args_content): - parsed = FunctionMember._parse_single_argument(arg) - if parsed[0] or parsed[1]: - arguments.append(parsed) - - modifiers = FunctionMember._parse_modifiers(modifiers_str) - - return (arguments, modifiers) - class TypedefMember(Member): def __init__( self, name: str, type: str, argstring: str | None, visibility: str, keyword: str ) -> None: super().__init__(name, visibility) - self.type: str = type 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 + def close(self, scope: Scope): - # TODO: handle unqualified references - pass + 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.""" @@ -602,14 +290,16 @@ def to_string( result += self.keyword if self.keyword == "using": - result += f" {name} = {self.type};" + 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 self.type: - result += f" {self.type}{name}{self.argstring};" + if "(*" in qualified_type: + result += f" {qualified_type}{name})({formatted_args});" else: - result += f" {self.type}(*{name}{self.argstring};" + result += f" {qualified_type}(*{name})({formatted_args});" else: result += f" {self.type} {name};" diff --git a/scripts/cxx-api/parser/scope.py b/scripts/cxx-api/parser/scope.py index c68803c9230a..a6086094f3ea 100644 --- a/scripts/cxx-api/parser/scope.py +++ b/scripts/cxx-api/parser/scope.py @@ -178,6 +178,64 @@ def _get_base_name(self, name: str) -> str: angle_idx = name.find("<") return name[:angle_idx] if angle_idx != -1 else name + def qualify_name(self, name: str | None) -> str | None: + """ + Qualify a name with the relevant scope if possible. + Handles template arguments by stripping them for lookup but preserving + them in the output. + """ + if not name: + return None + + path = parse_qualified_path(name) + if not path: + return None + + current_scope = self + # Walk up to find a scope that contains the first path segment + base_first = self._get_base_name(path[0]) + while ( + current_scope is not None and base_first not in current_scope.inner_scopes + ): + current_scope = current_scope.parent_scope + + if current_scope is None: + return None + + # Remember the scope where we found the first segment — its qualified + # name is the prefix that must precede the matched path segments. + anchor_scope = current_scope + + # Walk down through the path, tracking matched segments with original template args + matched_segments: list[str] = [] + for i, path_segment in enumerate(path): + base_name = self._get_base_name(path_segment) + if base_name in current_scope.inner_scopes: + matched_segments.append(path_segment) + current_scope = current_scope.inner_scopes[base_name] + elif any(m.name == base_name for m in current_scope._members): + # Found as a member, assume following segments exist in the scope + prefix = "::".join(matched_segments) + suffix = "::".join(path[i:]) + anchor_prefix = anchor_scope.get_qualified_name() + if prefix: + if anchor_prefix: + return f"{anchor_prefix}::{prefix}::{suffix}" + return f"{prefix}::{suffix}" + else: + if anchor_prefix: + return f"{anchor_prefix}::{suffix}" + return suffix + else: + return None + + # Return qualified name with preserved template arguments + prefix = anchor_scope.get_qualified_name() + if prefix: + return f"{prefix}::{'::'.join(matched_segments)}" + else: + return "::".join(matched_segments) + def add_member(self, member: Member | None) -> None: """ Add a member to the scope. diff --git a/scripts/cxx-api/parser/utils/__init__.py b/scripts/cxx-api/parser/utils/__init__.py new file mode 100644 index 000000000000..b711a41f4e2b --- /dev/null +++ b/scripts/cxx-api/parser/utils/__init__.py @@ -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. + +from .argument_parsing import ( + Argument, + extract_qualifiers, + format_arguments, + format_parsed_type, + FunctionModifiers, + parse_arg_string, + parse_function_pointer_argstring, + parse_type_with_argstrings, +) +from .qualified_path import parse_qualified_path +from .type_qualification import qualify_arguments, qualify_parsed_type, qualify_type_str + +__all__ = [ + "Argument", + "extract_qualifiers", + "format_arguments", + "format_parsed_type", + "FunctionModifiers", + "parse_arg_string", + "parse_function_pointer_argstring", + "parse_qualified_path", + "parse_type_with_argstrings", + "qualify_arguments", + "qualify_parsed_type", + "qualify_type_str", +] diff --git a/scripts/cxx-api/parser/utils/argument_parsing.py b/scripts/cxx-api/parser/utils/argument_parsing.py new file mode 100644 index 000000000000..2e0d0bb47851 --- /dev/null +++ b/scripts/cxx-api/parser/utils/argument_parsing.py @@ -0,0 +1,531 @@ +# 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 + +import re +from dataclasses import dataclass + +# Type alias for a parsed argument tuple: +# (qualifiers, type, name, default_value) +# - qualifiers: leading CV-qualifiers like "const", "const volatile", or None +# - type: the core type without leading CV-qualifiers +# - name: the parameter name, or None if unnamedą +# - default_value: the default value expression, or None +Argument = tuple[str | None, str, str | None, str | None] + + +@dataclass +class FunctionModifiers: + """Parsed function modifiers that appear after the parameter list.""" + + is_const: bool = False + is_override: bool = False + is_final: bool = False + is_noexcept: bool = False + noexcept_expr: str | None = None + is_pure_virtual: bool = False + is_default: bool = False + is_delete: bool = False + + +# Pattern for function pointer / pointer to member / reference to array: +# Matches: (*name), (Class::*name), (&name), (*name)[N] +# Group 1: the identifier name +_FUNC_PTR_PATTERN = re.compile( + r"\(\s*" # Opening paren + r"(?:[a-zA-Z_][a-zA-Z0-9_]*\s*::\s*)?" # Optional Class:: + r"[*&]\s*" # * or & + r"([a-zA-Z_][a-zA-Z0-9_]*)" # Capture: identifier name + r"\s*\)" # Closing paren +) + +# CV-qualifiers that should be extracted into the qualifiers field. +# These precede a type but are not part of the type name itself, so +# they must be separated to allow unambiguous type qualification later. +_CV_QUALIFIERS = frozenset({"const", "volatile", "mutable"}) + +# Type specifiers that ARE part of the type name (e.g. "unsigned long"). +# They are kept in the type field, but are still used by +# _prefix_is_all_qualifiers to detect patterns like "unsigned long" +# where the last token completes the type rather than naming a parameter. +_TYPE_SPECIFIERS = frozenset({"unsigned", "signed", "long", "short"}) + +# Union of CV-qualifiers and type specifiers. Used by +# _prefix_is_all_qualifiers to decide whether a prefix consists entirely +# of qualifier/specifier keywords. +_CPP_TYPE_QUALIFIERS = _CV_QUALIFIERS | _TYPE_SPECIFIERS + + +def _find_matching_bracket( + s: str, + start: int, + open_char: str, + close_char: str, + ignore_inside: str | None = None, +) -> int: + """Find the index of the closing bracket matching the opening one at start. + + Args: + s: The string to search. + start: The index of the opening bracket. + open_char: The opening bracket character (e.g., '(' or '<'). + close_char: The closing bracket character (e.g., ')' or '>'). + ignore_inside: Optional bracket type inside which to ignore matches. + If '(', ignores matches inside parentheses. If '<', ignores inside angles. + + Returns: + The index of the matching closing bracket, or -1 if not found. + """ + if start >= len(s) or s[start] != open_char: + return -1 + + depth = 0 + ignore_depth = 0 + ignore_open = ignore_close = "" + if ignore_inside == "(": + ignore_open, ignore_close = "(", ")" + elif ignore_inside == "<": + ignore_open, ignore_close = "<", ">" + + for i in range(start, len(s)): + c = s[i] + if ignore_open and c == ignore_open: + ignore_depth += 1 + elif ignore_close and c == ignore_close: + ignore_depth -= 1 + elif c == open_char and ignore_depth == 0: + depth += 1 + elif c == close_char and ignore_depth == 0: + depth -= 1 + if depth == 0: + return i + + return -1 + + +def _find_matching_paren(s: str, start: int = 0) -> int: + """Find the index of the closing parenthesis matching the opening one at start.""" + return _find_matching_bracket(s, start, "(", ")") + + +def _find_matching_angle(s: str, start: int = 0) -> int: + """Find the index of the closing angle bracket matching the opening one at start.""" + return _find_matching_bracket(s, start, "<", ">", ignore_inside="(") + + +def _iter_at_depth_zero(s: str): + """Iterate over string, yielding (index, char, at_depth_zero) tuples. + + Tracks nested parentheses, angle brackets, and braces. The at_depth_zero + flag is True when all nesting depths are zero AFTER processing the current + character (so brackets themselves are never at depth zero). + """ + paren_depth = angle_depth = brace_depth = 0 + + for i, c in enumerate(s): + if c == "<": + angle_depth += 1 + elif c == ">": + angle_depth = max(0, angle_depth - 1) + elif c == "(": + paren_depth += 1 + elif c == ")": + paren_depth = max(0, paren_depth - 1) + elif c == "{": + brace_depth += 1 + elif c == "}": + brace_depth = max(0, brace_depth - 1) + + yield i, c, (paren_depth == 0 and angle_depth == 0 and brace_depth == 0) + + +def _split_arguments(args_str: str) -> list[str]: + """Split arguments by comma, respecting nested structures. + + Handles: + - Nested parentheses: std::function + - Nested angle brackets: std::map + - Brace initializers: std::vector v = {1, 2, 3} + """ + result = [] + current: list[str] = [] + + for _, c, at_zero in _iter_at_depth_zero(args_str): + if c == "," and at_zero: + result.append("".join(current).strip()) + current = [] + else: + current.append(c) + + if current: + result.append("".join(current).strip()) + + return [arg for arg in result if arg] + + +def _prefix_is_all_qualifiers(prefix: str) -> bool: + """Check if all tokens in the prefix are type qualifiers/specifiers. + + When the prefix consists entirely of reserved qualifiers (e.g., "const", + "const unsigned"), the following token cannot be a parameter name — it + must be completing the type. + """ + return all(t in _CPP_TYPE_QUALIFIERS for t in prefix.split()) + + +def _looks_like_type_token(token: str) -> bool: + """Check if a token ends with type-related punctuation. + + Returns True if the token ends with pointer, reference, template closer, + or array bracket — indicating it's part of a type, not a name. + """ + return ( + token.endswith("*") + or token.endswith("&") + or token.endswith(">") + or token.endswith("]") + ) + + +def _find_default_value_start(arg: str) -> int: + """Find the position of '=' that starts a default value. + + Returns -1 if no default value found. + Ignores '=' inside nested structures like templates or lambdas. + """ + for i, c, at_zero in _iter_at_depth_zero(arg): + if c == "=" and at_zero: + return i + return -1 + + +def extract_qualifiers(type_str: str) -> tuple[str | None, str]: + """Extract leading CV-qualifiers (const, volatile, mutable) from a type string. + + Returns ``(qualifiers, core_type)`` where *qualifiers* is ``None`` when + no leading CV-qualifiers are found or when extracting them would leave + the type empty. + + Examples:: + + "const std::string&" → ("const", "std::string&") + "const volatile int*" → ("const volatile", "int*") + "unsigned long" → (None, "unsigned long") + "const unsigned long" → ("const", "unsigned long") + "int" → (None, "int") + "mutable std::mutex" → ("mutable", "std::mutex") + """ + if not type_str: + return (None, "") + + tokens = type_str.split() + qual_count = 0 + for token in tokens: + if token in _CV_QUALIFIERS: + qual_count += 1 + else: + break + + # No leading qualifiers, or ALL tokens are qualifiers (e.g. just "const") + # — keep them in the type to avoid producing an empty type string. + if qual_count == 0 or qual_count >= len(tokens): + return (None, type_str) + + return (" ".join(tokens[:qual_count]), " ".join(tokens[qual_count:])) + + +def _parse_single_argument(arg: str) -> Argument: + """Parse a single C++ argument into ``(qualifiers, type, name, default_value)``. + + Leading CV-qualifiers (``const``, ``volatile``, ``mutable``) are extracted + into the *qualifiers* field so the *type* field contains only the core + type reference that may later need namespace qualification. + + Handles complex cases:: + + "int x" → (None, "int", "x", None) + "const std::string& s" → ("const", "std::string&", "s", None) + "int x = 5" → (None, "int", "x", "5") + "int (*callback)(int, int)" → (None, "int (*)(int, int)", "callback", None) + "void (Class::*method)()" → (None, "void (Class::*)()", "method", None) + "int (&arr)[10]" → (None, "int (&)[10]", "arr", None) + "void" → (None, "void", None, None) + "const unsigned long" → ("const", "unsigned long", None, None) + """ + arg = arg.strip() + if not arg: + return (None, "", None, None) + + default_value: str | None = None + base_arg = arg + + # Handle default values: split on '=' but respect nested structures + eq_pos = _find_default_value_start(arg) + if eq_pos != -1: + base_arg = arg[:eq_pos].strip() + default_value = arg[eq_pos + 1 :].strip() + + # Extract leading CV-qualifiers before further parsing + qualifiers, base_arg = extract_qualifiers(base_arg) + + # Try to match function pointer / pointer to member / reference to array pattern + match = _FUNC_PTR_PATTERN.search(base_arg) + if match: + name = match.group(1) + # Reconstruct type by removing the name from the pattern + type_str = base_arg[: match.start(1)] + base_arg[match.end(1) :] + return (qualifiers, type_str.strip(), name, default_value) + + # Regular case: last token is the name + parts = base_arg.rsplit(None, 1) + if len(parts) == 2: + prefix, potential_name = parts + # The last token is NOT a name when: + # - it looks like a type token (ends with *, &, >, ]), OR + # - it is a CV-qualifier (trailing const/volatile), OR + # - the prefix is all qualifiers/specifiers AND the last token is + # also a type specifier (e.g. "unsigned long" — "long" completes + # the type rather than naming a parameter). + if ( + _looks_like_type_token(potential_name) + or potential_name in _CV_QUALIFIERS + or ( + _prefix_is_all_qualifiers(prefix) and potential_name in _TYPE_SPECIFIERS + ) + ): + return (qualifiers, base_arg, None, default_value) + return (qualifiers, prefix, potential_name, default_value) + else: + return (qualifiers, base_arg, None, default_value) + + +def _parse_modifiers(modifiers_str: str) -> FunctionModifiers: + """Parse function modifiers after the parameter list. + + Handles: const, override, final, noexcept, noexcept(expr), = 0, = default, = delete + """ + result = FunctionModifiers() + s = modifiers_str.strip() + + # Handle = 0, = default, = delete + eq_match = re.search(r"=\s*(0|default|delete)\s*$", s) + if eq_match: + value = eq_match.group(1) + if value == "0": + result.is_pure_virtual = True + elif value == "default": + result.is_default = True + elif value == "delete": + result.is_delete = True + s = s[: eq_match.start()].strip() + + # Handle noexcept with optional expression + noexcept_match = re.search(r"\bnoexcept\b", s) + if noexcept_match: + result.is_noexcept = True + # Check for noexcept(expr) + rest = s[noexcept_match.end() :] + rest_stripped = rest.lstrip() + if rest_stripped.startswith("("): + paren_end = _find_matching_paren(rest_stripped, 0) + if paren_end != -1: + result.noexcept_expr = rest_stripped[1:paren_end].strip() + # Remove noexcept and its expression from the string for further parsing + s = s[: noexcept_match.start()] + s[noexcept_match.end() :] + if result.noexcept_expr: + # Also remove the (expr) part + s = re.sub(r"\([^)]*\)", "", s, count=1) + + # Parse remaining tokens + tokens = s.split() + for token in tokens: + if token == "const": + result.is_const = True + elif token == "override": + result.is_override = True + elif token == "final": + result.is_final = True + + return result + + +def parse_arg_string( + arg_string: str, +) -> tuple[list[Argument], FunctionModifiers]: + """Parse a C++ function argument string. + + Args: + arg_string: String in format "(type1 arg1, type2 arg2 = default) [modifiers]" + + Returns: + Tuple of (arguments, modifiers) where: + - arguments: list of (qualifiers, type, name, default_value) tuples + - modifiers: FunctionModifiers dataclass with parsed modifier flags + """ + arg_string = arg_string.strip() + + if not arg_string.startswith("("): + return ([], FunctionModifiers()) + + close_paren = _find_matching_paren(arg_string, 0) + if close_paren == -1: + return ([], FunctionModifiers()) + + args_content = arg_string[1:close_paren].strip() + modifiers_str = arg_string[close_paren + 1 :] + + arguments: list[Argument] = [] + if args_content: + for arg in _split_arguments(args_content): + parsed = _parse_single_argument(arg) + if parsed[0] or parsed[1]: + arguments.append(parsed) + + modifiers = _parse_modifiers(modifiers_str) + + return (arguments, modifiers) + + +def format_arguments(arguments: list[Argument]) -> str: + """Format a list of parsed arguments into a comma-separated string. + + Args: + arguments: list of (qualifiers, type, name, default_value) tuples as + returned by parse_arg_string or _parse_single_argument. + + Returns: + Formatted argument string, e.g. "const int x, float y = 0.0". + """ + parts = [] + for qualifiers, arg_type, arg_name, arg_default in arguments: + part = "" + if qualifiers: + part += f"{qualifiers} " + part += arg_type + if arg_name: + part += f" {arg_name}" + if arg_default: + part += f" = {arg_default}" + parts.append(part) + return ", ".join(parts) + + +def parse_function_pointer_argstring( + argstring: str, +) -> list[Argument]: + """Parse a function pointer argstring of the form ')(args...)'. + + Doxygen represents function pointer arguments in the argstring field + starting with ')(' — the ')' closes the declarator and '(' opens + the parameter list. + + Args: + argstring: Raw argstring from doxygen, e.g. ")(int x, float y)". + + Returns: + List of (qualifiers, type, name, default_value) tuples for each parameter. + """ + if not argstring or not argstring.startswith(")("): + return [] + # Remove leading ')' to get '(args...)' + inner = argstring[1:] + arguments, _ = parse_arg_string(inner) + return arguments + + +def parse_type_with_argstrings( + type_str: str, +) -> list[str | list[Argument]]: + """Parse a type string, extracting inline function argument lists. + + Doxygen sometimes embeds raw function signatures in type strings + (e.g. for 'using' typedefs). This function splits the type into + segments: plain-text fragments and parsed argument lists. + + Each segment is either: + - A ``str``: literal text (e.g. ``"void"``, ``"(*)"``). + - A ``list[Argument]``: a parsed argument list, + where each tuple is ``(qualifiers, type, name, default_value)``. + + Handles cases like: + - ``void(int x, float y)`` + → ``["void", [(None, "int", "x", None), (None, "float", "y", None)]]`` + - ``std::function`` + → ``["std::function"]`` + - ``void(*)(int x, float y)`` + → ``["void(*)", [(None, "int", "x", None), (None, "float", "y", None)]]`` + - ``int`` + → ``["int"]`` + """ + if not type_str: + return [type_str] if type_str is not None else [] + + segments: list[str | list[Argument]] = [] + i = 0 + current_text: list[str] = [] + + while i < len(type_str): + if type_str[i] == "(": + close = _find_matching_paren(type_str, i) + if close == -1: + current_text.append(type_str[i]) + i += 1 + continue + + inner = type_str[i + 1 : close] + stripped = inner.strip() + + # Check if this is a declarator like (*) or (&) — don't parse those + if stripped in ("*", "&") or re.match( + r"^[a-zA-Z_][a-zA-Z0-9_]*\s*::\s*[*&]$", stripped + ): + current_text.append(type_str[i : close + 1]) + i = close + 1 + continue + + # Try to parse as a function argument list + args: list[Argument] = [] + if stripped: + for arg in _split_arguments(stripped): + parsed = _parse_single_argument(arg) + if parsed[0] or parsed[1]: + args.append(parsed) + + if args: + # Flush accumulated text as a plain segment + if current_text: + segments.append("".join(current_text)) + current_text = [] + segments.append(args) + else: + current_text.append(type_str[i : close + 1]) + i = close + 1 + else: + current_text.append(type_str[i]) + i += 1 + + if current_text: + segments.append("".join(current_text)) + + return segments + + +def format_parsed_type( + segments: list[str | list[Argument]], +) -> str: + """Format the structured output of :func:`parse_type_with_argstrings` back + into a type string. + + Each segment is either a plain string (emitted as-is) or a parsed + argument list (formatted as ``(qualifiers type name = default, ...)``). + """ + parts: list[str] = [] + for seg in segments: + if isinstance(seg, str): + parts.append(seg) + else: + parts.append(f"({format_arguments(seg)})") + return "".join(parts) diff --git a/scripts/cxx-api/parser/utils.py b/scripts/cxx-api/parser/utils/qualified_path.py similarity index 98% rename from scripts/cxx-api/parser/utils.py rename to scripts/cxx-api/parser/utils/qualified_path.py index d69920cdaf73..1010ca6a6cdb 100644 --- a/scripts/cxx-api/parser/utils.py +++ b/scripts/cxx-api/parser/utils/qualified_path.py @@ -3,6 +3,8 @@ # 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 + def parse_qualified_path(path: str) -> list[str]: """ diff --git a/scripts/cxx-api/parser/utils/type_qualification.py b/scripts/cxx-api/parser/utils/type_qualification.py new file mode 100644 index 000000000000..115de82b5b34 --- /dev/null +++ b/scripts/cxx-api/parser/utils/type_qualification.py @@ -0,0 +1,148 @@ +# 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 .argument_parsing import _find_matching_angle, _split_arguments, Argument + +if TYPE_CHECKING: + from .scope import Scope + + +def qualify_arguments(arguments: list[Argument], scope: Scope) -> list[Argument]: + """Qualify type and default-value references in a list of arguments.""" + result: list[Argument] = [] + for qualifiers, arg_type, name, default in arguments: + qualified_type = qualify_type_str(arg_type, scope) + qualified_default = scope.qualify_name(default) + if qualified_default is not None: + default = qualified_default + + result.append((qualifiers, qualified_type, name, default)) + return result + + +def qualify_type_str(type_str: str, scope: Scope) -> str: + """Qualify a type string, handling trailing decorators (*, &, &&, etc.).""" + if not type_str: + return type_str + + # Handle template arguments first: qualify types inside angle brackets + angle_start = type_str.find("<") + if angle_start != -1: + angle_end = _find_matching_angle(type_str, angle_start) + if angle_end != -1: + prefix = type_str[:angle_start] + template_args = type_str[angle_start + 1 : angle_end] + suffix = type_str[angle_end + 1 :] + + # Qualify the prefix (outer type before the template) + qualified_prefix = scope.qualify_name(prefix) or prefix + + # Split template arguments and qualify each one + args = _split_arguments(template_args) + qualified_args = [qualify_type_str(arg.strip(), scope) for arg in args] + qualified_template = "< " + ", ".join(qualified_args) + " >" + + # Recursively qualify the suffix (handles nested templates, pointers, etc.) + qualified_suffix = qualify_type_str(suffix, scope) if suffix else "" + + return qualified_prefix + qualified_template + qualified_suffix + + # Try qualifying the entire string (handles simple cases without templates) + qualified = scope.qualify_name(type_str) + if qualified is not None: + return qualified + + # Handle function pointer/reference declarator prefix: "Type(*" or "Type(&" + for declarator in ("(*", "(&"): + if type_str.endswith(declarator): + prefix = type_str[: -len(declarator)].rstrip() + if prefix: + q = qualify_type_str(prefix, scope) + if q != prefix: + return q + declarator + return type_str + + # Strip trailing pointer/reference/const decorators and try again. + # Loop so that mixed sequences like "Type *const" or "Type const*" are + # fully peeled regardless of order. + stripped = type_str.rstrip() + suffix = "" + changed = True + while changed: + changed = False + while stripped and stripped[-1] in ("*", "&"): + suffix = stripped[-1] + suffix + stripped = stripped[:-1].rstrip() + changed = True + # Handle trailing "const" (possibly preceded by space or pointer) + if stripped.endswith(" const") or stripped.endswith("*const"): + is_after_ptr = stripped.endswith("*const") + suffix = "const " + suffix + stripped = stripped[: -len("const")].rstrip() + if is_after_ptr: + # Re-enter loop to peel the '*' that preceded "const" + pass + changed = True + + if suffix and stripped: + qualified = scope.qualify_name(stripped) + if qualified is not None: + suffix = suffix.rstrip() + if "const" in suffix: + return qualified + " " + suffix + return qualified + suffix + + return type_str + + +def _qualify_text_before_args(text: str, scope: Scope) -> str: + """Qualify the trailing return-type in a text segment that precedes an arg list. + + When ``parse_type_with_argstrings`` splits ``"std::function< Result(Param)"`` + the text segment ``"std::function< Result"`` contains a return type at the + end that must be qualified independently. + """ + # Try qualifying the whole text first (covers simple "Result" etc.) + qualified = qualify_type_str(text, scope) + if qualified != text: + return qualified + + # Try qualifying just the last whitespace-delimited token + stripped = text.rstrip() + last_space = stripped.rfind(" ") + if last_space == -1: + return text + + prefix = stripped[: last_space + 1] + last_token = stripped[last_space + 1 :] + + qualified_token = scope.qualify_name(last_token) + if qualified_token is not None: + return prefix + qualified_token + + return text + + +def qualify_parsed_type( + segments: list[str | list[Argument]], scope: Scope +) -> list[str | list[Argument]]: + """Qualify references inside parsed-type segments.""" + result: list[str | list[Argument]] = [] + for i, seg in enumerate(segments): + if isinstance(seg, list): + result.append(qualify_arguments(seg, scope)) + else: + # If the next segment is a parsed argument list, the end of this + # text likely contains a return type that needs qualification. + next_is_args = i + 1 < len(segments) and isinstance(segments[i + 1], list) + if next_is_args: + result.append(_qualify_text_before_args(seg, scope)) + else: + result.append(qualify_type_str(seg, scope)) + return result diff --git a/scripts/cxx-api/tests/snapshots/should_handle_class_public_method_arguments/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_class_public_method_arguments/snapshot.api index a963a16ebda3..1204a64ce870 100644 --- a/scripts/cxx-api/tests/snapshots/should_handle_class_public_method_arguments/snapshot.api +++ b/scripts/cxx-api/tests/snapshots/should_handle_class_public_method_arguments/snapshot.api @@ -2,18 +2,18 @@ class test::Clss { public virtual void fn16() = 0; public void fn0(int32_t arg1); public void fn1(int32_t arg1, int32_t arg2); - public void fn2(std::function< int32_t(int32_t)> arg1); - public void fn3(std::function< int32_t(int32_t)> arg1, std::function< int32_t(int32_t, int32_t)> arg2); + public void fn2(std::function< int32_t(int32_t) > arg1); + public void fn3(std::function< int32_t(int32_t) > arg1, std::function< int32_t(int32_t, int32_t) > arg2); public void fn4(std::map< std::string, int > m); public void fn5(std::unordered_map< K, std::vector< V > > m); public void fn6(std::tuple< int, float, std::string > t); public void fn7(std::vector< std::vector< std::pair< int, int > > > v); - public void fn8(std::map< K, std::function< void(A, B)> > m); + public void fn8(std::map< K, std::function< void(A, B) > > m); public void fn9(int(*)(int, int) callback); public void fn10(void(*)(const char *, size_t) handler); - public void fn11(int(*(*)(int))(double) fp); + public void fn11(int(*(*fp)(int))(double)); public void fn12(int x = 5, std::string s = "default"); - public void fn13(std::function< void()> f = nullptr); + public void fn13(std::function< void() > f = nullptr); public void fn14(std::vector< int > v = {1, 2, 3}); public void fn15() noexcept; public void fn17() = default; diff --git a/scripts/cxx-api/tests/snapshots/should_handle_pure_virtual_trailing_return/snapshot.api b/scripts/cxx-api/tests/snapshots/should_handle_pure_virtual_trailing_return/snapshot.api new file mode 100644 index 000000000000..e2c51844a8fa --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_pure_virtual_trailing_return/snapshot.api @@ -0,0 +1,7 @@ +class test::Base { + public virtual const char * getName() = 0; + public virtual std::vector< test::Method > getMethods() = 0; +} + +struct test::Method { +} diff --git a/scripts/cxx-api/tests/snapshots/should_handle_pure_virtual_trailing_return/test.h b/scripts/cxx-api/tests/snapshots/should_handle_pure_virtual_trailing_return/test.h new file mode 100644 index 000000000000..b2e9e2382da8 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_handle_pure_virtual_trailing_return/test.h @@ -0,0 +1,22 @@ +/* + * 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 + +#include + +namespace test { + +struct Method {}; + +class Base { + public: + virtual auto getMethods() -> std::vector = 0; + virtual auto getName() -> const char * = 0; +}; + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_free_declarations/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_free_declarations/snapshot.api new file mode 100644 index 000000000000..f65f0cac022b --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_free_declarations/snapshot.api @@ -0,0 +1,6 @@ +struct test::Param { +} + +typedef void(*test::FreeFnPtr)(test::Param); +using test::FreeCallback = void(test::Param); +void test::freeFunction(test::Param p); diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_free_declarations/test.h b/scripts/cxx-api/tests/snapshots/should_qualify_free_declarations/test.h new file mode 100644 index 000000000000..30ddd5fef0c3 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_free_declarations/test.h @@ -0,0 +1,18 @@ +/* + * 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 Param {}; + +void freeFunction(Param p); +typedef void (*FreeFnPtr)(Param); +using FreeCallback = void(Param); + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_function_args/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_function_args/snapshot.api new file mode 100644 index 000000000000..63d0bd88b917 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_function_args/snapshot.api @@ -0,0 +1,8 @@ +struct test::Container { + public void doConst(const test::Param& p); + public void doMultiple(test::Param a, test::Param b, int x); + public void doSomething(test::Param p); +} + +struct test::Param { +} diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_function_args/test.h b/scripts/cxx-api/tests/snapshots/should_qualify_function_args/test.h new file mode 100644 index 000000000000..96b6acc6abb8 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_function_args/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 { + +struct Param {}; + +struct Container { + void doSomething(Param p); + void doConst(const Param &p); + void doMultiple(Param a, Param b, int x); +}; + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_partially_qualified_args/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_partially_qualified_args/snapshot.api new file mode 100644 index 000000000000..1ebe9e74ffb9 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_partially_qualified_args/snapshot.api @@ -0,0 +1,8 @@ +facebook::yoga::Result facebook::react::getResult(facebook::yoga::Node node, int flags); +void facebook::react::processNode(facebook::yoga::Node node); + +struct facebook::yoga::Node { +} + +struct facebook::yoga::Result { +} diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_partially_qualified_args/test.h b/scripts/cxx-api/tests/snapshots/should_qualify_partially_qualified_args/test.h new file mode 100644 index 000000000000..6ffd778bca6c --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_partially_qualified_args/test.h @@ -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. + */ + +#pragma once + +namespace facebook { + +namespace yoga { + +struct Node {}; + +struct Result {}; + +} // namespace yoga + +namespace react { + +void processNode(yoga::Node node); +yoga::Result getResult(yoga::Node node, int flags); + +} // namespace react + +} // namespace facebook diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_ptr_const_args/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_ptr_const_args/snapshot.api new file mode 100644 index 000000000000..a75e6cc67a16 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_ptr_const_args/snapshot.api @@ -0,0 +1 @@ +void test::doSomething(const Node *const node, int x); diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_ptr_const_args/test.h b/scripts/cxx-api/tests/snapshots/should_qualify_ptr_const_args/test.h new file mode 100644 index 000000000000..47211e25d134 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_ptr_const_args/test.h @@ -0,0 +1,16 @@ +/* + * 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 Node; + +void doSomething(const Node *const node, int x); + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_typedef_function_pointer/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_typedef_function_pointer/snapshot.api new file mode 100644 index 000000000000..c41c8f2abba8 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_typedef_function_pointer/snapshot.api @@ -0,0 +1,6 @@ +struct test::Container { + public typedef void(*FnPtr)(test::Param, int); +} + +struct test::Param { +} diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_typedef_function_pointer/test.h b/scripts/cxx-api/tests/snapshots/should_qualify_typedef_function_pointer/test.h new file mode 100644 index 000000000000..06af2a21afba --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_typedef_function_pointer/test.h @@ -0,0 +1,18 @@ +/* + * 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 Param {}; + +struct Container { + typedef void (*FnPtr)(Param, int); +}; + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_using_typedef_inline_args/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_using_typedef_inline_args/snapshot.api new file mode 100644 index 000000000000..056c4c0561bf --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_using_typedef_inline_args/snapshot.api @@ -0,0 +1,10 @@ +struct test::Container { + public using Callback = void(test::Param); + public using Processor = test::Result(test::Param, int); +} + +struct test::Param { +} + +struct test::Result { +} diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_using_typedef_inline_args/test.h b/scripts/cxx-api/tests/snapshots/should_qualify_using_typedef_inline_args/test.h new file mode 100644 index 000000000000..c8baaf8b95af --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_using_typedef_inline_args/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 { + +struct Param {}; +struct Result {}; + +struct Container { + using Callback = void(Param); + using Processor = Result(Param, int); +}; + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_variable_function_pointer/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_variable_function_pointer/snapshot.api new file mode 100644 index 000000000000..88dbaa0273da --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_variable_function_pointer/snapshot.api @@ -0,0 +1,6 @@ +struct test::Container { + public void(*handler)(test::Param, int); +} + +struct test::Param { +} diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_variable_function_pointer/test.h b/scripts/cxx-api/tests/snapshots/should_qualify_variable_function_pointer/test.h new file mode 100644 index 000000000000..188271dcf760 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_variable_function_pointer/test.h @@ -0,0 +1,18 @@ +/* + * 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 Param {}; + +struct Container { + void (*handler)(Param, int); +}; + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_variable_inline_function_type/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_variable_inline_function_type/snapshot.api new file mode 100644 index 000000000000..977576a2f088 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_variable_inline_function_type/snapshot.api @@ -0,0 +1,13 @@ +struct test::Container { + public std::function< test::Result(test::Param, int)> processor; + public std::function< void(const test::Param&, test::Result)> multiRef; + public std::function< void(test::Param)> simpleCallback; + public test::Result(*fnPtrReturn)(const test::Param&); + public void(*fnPtr)(test::Param, int); +} + +struct test::Param { +} + +struct test::Result { +} diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_variable_inline_function_type/test.h b/scripts/cxx-api/tests/snapshots/should_qualify_variable_inline_function_type/test.h new file mode 100644 index 000000000000..1c726e151ae4 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_variable_inline_function_type/test.h @@ -0,0 +1,25 @@ +/* + * 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 + +#include + +namespace test { + +struct Param {}; +struct Result {}; + +struct Container { + std::function simpleCallback; + std::function processor; + std::function multiRef; + void (*fnPtr)(Param, int); + Result (*fnPtrReturn)(const Param &); +}; + +} // namespace test diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_variable_type/snapshot.api b/scripts/cxx-api/tests/snapshots/should_qualify_variable_type/snapshot.api new file mode 100644 index 000000000000..76fb590bba71 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_variable_type/snapshot.api @@ -0,0 +1,7 @@ +struct test::Container { + public const test::Param constField; + public test::Param field; +} + +struct test::Param { +} diff --git a/scripts/cxx-api/tests/snapshots/should_qualify_variable_type/test.h b/scripts/cxx-api/tests/snapshots/should_qualify_variable_type/test.h new file mode 100644 index 000000000000..ff0868054a75 --- /dev/null +++ b/scripts/cxx-api/tests/snapshots/should_qualify_variable_type/test.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +namespace test { + +struct Param {}; + +struct Container { + Param field; + const Param constField; +}; + +} // namespace test diff --git a/scripts/cxx-api/tests/test_member.py b/scripts/cxx-api/tests/test_parse_arg_string.py similarity index 60% rename from scripts/cxx-api/tests/test_member.py rename to scripts/cxx-api/tests/test_parse_arg_string.py index 8f1a4e848cc0..ae99fb034ab0 100644 --- a/scripts/cxx-api/tests/test_member.py +++ b/scripts/cxx-api/tests/test_parse_arg_string.py @@ -3,11 +3,11 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. -"""Unit tests for FunctionMember.parse_arg_string()""" +"""Unit tests for parse_arg_string()""" import unittest -from ..parser import FunctionMember, FunctionModifiers +from ..parser.utils.argument_parsing import parse_arg_string class TestParseArgString(unittest.TestCase): @@ -19,24 +19,23 @@ class TestParseArgString(unittest.TestCase): def test_empty_args(self): """Empty parentheses""" - args, mods = FunctionMember.parse_arg_string("()") + args, mods = parse_arg_string("()") self.assertEqual(args, []) - self.assertEqual(mods, FunctionModifiers()) def test_single_simple_arg(self): """Single argument: int x""" - args, mods = FunctionMember.parse_arg_string("(int x)") - self.assertEqual(args, [("int", "x", None)]) + args, mods = parse_arg_string("(int x)") + self.assertEqual(args, [(None, "int", "x", None)]) def test_multiple_simple_args(self): """Multiple arguments: int x, float y""" - args, mods = FunctionMember.parse_arg_string("(int x, float y)") - self.assertEqual(args, [("int", "x", None), ("float", "y", None)]) + args, mods = parse_arg_string("(int x, float y)") + self.assertEqual(args, [(None, "int", "x", None), (None, "float", "y", None)]) def test_void_arg(self): """void with no name""" - args, mods = FunctionMember.parse_arg_string("(void)") - self.assertEqual(args, [("void", "", None)]) + args, mods = parse_arg_string("(void)") + self.assertEqual(args, [(None, "void", None, None)]) # ========================================================================= # Modifiers: const, override, final @@ -44,42 +43,42 @@ def test_void_arg(self): def test_const_modifier(self): """() const""" - args, mods = FunctionMember.parse_arg_string("() const") + args, mods = parse_arg_string("() const") self.assertTrue(mods.is_const) self.assertFalse(mods.is_override) def test_override_modifier(self): """() override""" - args, mods = FunctionMember.parse_arg_string("() override") + args, mods = parse_arg_string("() override") self.assertTrue(mods.is_override) self.assertFalse(mods.is_const) def test_const_override(self): """() const override""" - args, mods = FunctionMember.parse_arg_string("() const override") + args, mods = parse_arg_string("() const override") self.assertTrue(mods.is_const) self.assertTrue(mods.is_override) def test_override_const(self): """() override const - reversed order""" - args, mods = FunctionMember.parse_arg_string("() override const") + args, mods = parse_arg_string("() override const") self.assertTrue(mods.is_const) self.assertTrue(mods.is_override) def test_final_modifier(self): """() final""" - args, mods = FunctionMember.parse_arg_string("() final") + args, mods = parse_arg_string("() final") self.assertTrue(mods.is_final) def test_override_final(self): """() override final""" - args, mods = FunctionMember.parse_arg_string("() override final") + args, mods = parse_arg_string("() override final") self.assertTrue(mods.is_override) self.assertTrue(mods.is_final) def test_const_override_final(self): """() const override final""" - args, mods = FunctionMember.parse_arg_string("() const override final") + args, mods = parse_arg_string("() const override final") self.assertTrue(mods.is_const) self.assertTrue(mods.is_override) self.assertTrue(mods.is_final) @@ -90,31 +89,31 @@ def test_const_override_final(self): def test_noexcept(self): """() noexcept""" - args, mods = FunctionMember.parse_arg_string("() noexcept") + args, mods = parse_arg_string("() noexcept") self.assertTrue(mods.is_noexcept) self.assertIsNone(mods.noexcept_expr) def test_noexcept_true(self): """() noexcept(true)""" - args, mods = FunctionMember.parse_arg_string("() noexcept(true)") + args, mods = parse_arg_string("() noexcept(true)") self.assertTrue(mods.is_noexcept) self.assertEqual(mods.noexcept_expr, "true") def test_noexcept_false(self): """() noexcept(false)""" - args, mods = FunctionMember.parse_arg_string("() noexcept(false)") + args, mods = parse_arg_string("() noexcept(false)") self.assertTrue(mods.is_noexcept) self.assertEqual(mods.noexcept_expr, "false") def test_noexcept_expr(self): """() noexcept(noexcept(other()))""" - args, mods = FunctionMember.parse_arg_string("() noexcept(noexcept(other()))") + args, mods = parse_arg_string("() noexcept(noexcept(other()))") self.assertTrue(mods.is_noexcept) self.assertEqual(mods.noexcept_expr, "noexcept(other())") def test_const_noexcept(self): """() const noexcept""" - args, mods = FunctionMember.parse_arg_string("() const noexcept") + args, mods = parse_arg_string("() const noexcept") self.assertTrue(mods.is_const) self.assertTrue(mods.is_noexcept) @@ -124,28 +123,28 @@ def test_const_noexcept(self): def test_pure_virtual(self): """() = 0""" - args, mods = FunctionMember.parse_arg_string("() = 0") + args, mods = parse_arg_string("() = 0") self.assertTrue(mods.is_pure_virtual) def test_default(self): """() = default""" - args, mods = FunctionMember.parse_arg_string("() = default") + args, mods = parse_arg_string("() = default") self.assertTrue(mods.is_default) def test_delete(self): """() = delete""" - args, mods = FunctionMember.parse_arg_string("() = delete") + args, mods = parse_arg_string("() = delete") self.assertTrue(mods.is_delete) def test_const_pure_virtual(self): """() const = 0""" - args, mods = FunctionMember.parse_arg_string("() const = 0") + args, mods = parse_arg_string("() const = 0") self.assertTrue(mods.is_const) self.assertTrue(mods.is_pure_virtual) def test_noexcept_default(self): """() noexcept = default""" - args, mods = FunctionMember.parse_arg_string("() noexcept = default") + args, mods = parse_arg_string("() noexcept = default") self.assertTrue(mods.is_noexcept) self.assertTrue(mods.is_default) @@ -155,31 +154,31 @@ def test_noexcept_default(self): def test_std_map(self): """std::map with comma inside template""" - args, mods = FunctionMember.parse_arg_string("(std::map m)") - self.assertEqual(args, [("std::map", "m", None)]) + args, mods = parse_arg_string("(std::map m)") + self.assertEqual(args, [(None, "std::map", "m", None)]) def test_std_unordered_map_nested(self): """std::unordered_map>""" - args, mods = FunctionMember.parse_arg_string( - "(std::unordered_map> m)" + args, mods = parse_arg_string("(std::unordered_map> m)") + self.assertEqual( + args, [(None, "std::unordered_map>", "m", None)] ) - self.assertEqual(args, [("std::unordered_map>", "m", None)]) def test_std_tuple(self): """std::tuple""" - args, mods = FunctionMember.parse_arg_string( - "(std::tuple t)" + args, mods = parse_arg_string("(std::tuple t)") + self.assertEqual( + args, [(None, "std::tuple", "t", None)] ) - self.assertEqual(args, [("std::tuple", "t", None)]) def test_deeply_nested_templates(self): """std::vector>>""" - args, mods = FunctionMember.parse_arg_string( + args, mods = parse_arg_string( "(std::vector>> v)" ) self.assertEqual( args, - [("std::vector>>", "v", None)], + [(None, "std::vector>>", "v", None)], ) # ========================================================================= @@ -188,23 +187,24 @@ def test_deeply_nested_templates(self): def test_std_function_simple(self): """std::function""" - args, mods = FunctionMember.parse_arg_string("(std::function f)") - self.assertEqual(args, [("std::function", "f", None)]) + args, mods = parse_arg_string("(std::function f)") + self.assertEqual(args, [(None, "std::function", "f", None)]) def test_std_function_with_args(self): """std::function""" - args, mods = FunctionMember.parse_arg_string("(std::function f)") - self.assertEqual(args, [("std::function", "f", None)]) + args, mods = parse_arg_string("(std::function f)") + self.assertEqual(args, [(None, "std::function", "f", None)]) def test_std_function_complex(self): """std::function""" - args, mods = FunctionMember.parse_arg_string( + args, mods = parse_arg_string( "(std::function callback)" ) self.assertEqual( args, [ ( + None, "std::function", "callback", None, @@ -214,23 +214,23 @@ def test_std_function_complex(self): def test_multiple_std_function_args(self): """Multiple std::function args""" - args, mods = FunctionMember.parse_arg_string( + args, mods = parse_arg_string( "(std::function f, std::function g)" ) self.assertEqual( args, [ - ("std::function", "f", None), - ("std::function", "g", None), + (None, "std::function", "f", None), + (None, "std::function", "g", None), ], ) def test_map_with_function_value(self): """std::map>""" - args, mods = FunctionMember.parse_arg_string( - "(std::map> m)" + args, mods = parse_arg_string("(std::map> m)") + self.assertEqual( + args, [(None, "std::map>", "m", None)] ) - self.assertEqual(args, [("std::map>", "m", None)]) # ========================================================================= # Function pointers @@ -238,20 +238,20 @@ def test_map_with_function_value(self): def test_function_pointer_simple(self): """int (*callback)(int, int)""" - args, mods = FunctionMember.parse_arg_string("(int (*callback)(int, int))") - self.assertEqual(args, [("int (*)(int, int)", "callback", None)]) + args, mods = parse_arg_string("(int (*callback)(int, int))") + self.assertEqual(args, [(None, "int (*)(int, int)", "callback", None)]) def test_function_pointer_void(self): """void (*handler)(const char*, size_t)""" - args, mods = FunctionMember.parse_arg_string( - "(void (*handler)(const char*, size_t))" + args, mods = parse_arg_string("(void (*handler)(const char*, size_t))") + self.assertEqual( + args, [(None, "void (*)(const char*, size_t)", "handler", None)] ) - self.assertEqual(args, [("void (*)(const char*, size_t)", "handler", None)]) def test_function_pointer_no_args(self): """void (*fn)()""" - args, mods = FunctionMember.parse_arg_string("(void (*fn)())") - self.assertEqual(args, [("void (*)()", "fn", None)]) + args, mods = parse_arg_string("(void (*fn)())") + self.assertEqual(args, [(None, "void (*)()", "fn", None)]) # ========================================================================= # Pointer to member function @@ -259,16 +259,14 @@ def test_function_pointer_no_args(self): def test_pointer_to_member(self): """void (Class::*method)(int, int)""" - args, mods = FunctionMember.parse_arg_string( - "(void (Class::*method)(int, int))" - ) - self.assertEqual(args, [("void (Class::*)(int, int)", "method", None)]) + args, mods = parse_arg_string("(void (Class::*method)(int, int))") + self.assertEqual(args, [(None, "void (Class::*)(int, int)", "method", None)]) def test_pointer_to_member_const(self): """int (Foo::*getter)() const - note: const after () is part of member fn signature""" - args, mods = FunctionMember.parse_arg_string("(int (Foo::*getter)() const)") + args, mods = parse_arg_string("(int (Foo::*getter)() const)") # The "const" here is part of the argument type, not a method modifier - self.assertEqual(args, [("int (Foo::*)() const", "getter", None)]) + self.assertEqual(args, [(None, "int (Foo::*)() const", "getter", None)]) # ========================================================================= # Reference to array / pointer to array @@ -276,13 +274,13 @@ def test_pointer_to_member_const(self): def test_reference_to_array(self): """int (&arr)[10]""" - args, mods = FunctionMember.parse_arg_string("(int (&arr)[10])") - self.assertEqual(args, [("int (&)[10]", "arr", None)]) + args, mods = parse_arg_string("(int (&arr)[10])") + self.assertEqual(args, [(None, "int (&)[10]", "arr", None)]) def test_pointer_to_array(self): """int (*arr)[10]""" - args, mods = FunctionMember.parse_arg_string("(int (*arr)[10])") - self.assertEqual(args, [("int (*)[10]", "arr", None)]) + args, mods = parse_arg_string("(int (*arr)[10])") + self.assertEqual(args, [(None, "int (*)[10]", "arr", None)]) # ========================================================================= # Default arguments @@ -290,40 +288,36 @@ def test_pointer_to_array(self): def test_default_int(self): """int x = 5""" - args, mods = FunctionMember.parse_arg_string("(int x = 5)") - self.assertEqual(args, [("int", "x", "5")]) + args, mods = parse_arg_string("(int x = 5)") + self.assertEqual(args, [(None, "int", "x", "5")]) def test_default_string(self): """std::string s = "default\" """ - args, mods = FunctionMember.parse_arg_string('(std::string s = "default")') - self.assertEqual(args, [("std::string", "s", '"default"')]) + args, mods = parse_arg_string('(std::string s = "default")') + self.assertEqual(args, [(None, "std::string", "s", '"default"')]) def test_default_nullptr(self): """std::function f = nullptr""" - args, mods = FunctionMember.parse_arg_string( - "(std::function f = nullptr)" - ) - self.assertEqual(args, [("std::function", "f", "nullptr")]) + args, mods = parse_arg_string("(std::function f = nullptr)") + self.assertEqual(args, [(None, "std::function", "f", "nullptr")]) def test_default_brace_initializer(self): """std::vector v = {1, 2, 3}""" - args, mods = FunctionMember.parse_arg_string("(std::vector v = {1, 2, 3})") - self.assertEqual(args, [("std::vector", "v", "{1, 2, 3}")]) + args, mods = parse_arg_string("(std::vector v = {1, 2, 3})") + self.assertEqual(args, [(None, "std::vector", "v", "{1, 2, 3}")]) def test_multiple_defaults(self): """int x = 5, std::string s = "test\" """ - args, mods = FunctionMember.parse_arg_string( - '(int x = 5, std::string s = "test")' - ) + args, mods = parse_arg_string('(int x = 5, std::string s = "test")') self.assertEqual( args, - [("int", "x", "5"), ("std::string", "s", '"test"')], + [(None, "int", "x", "5"), (None, "std::string", "s", '"test"')], ) def test_default_template_with_comma(self): """std::map m = {}""" - args, mods = FunctionMember.parse_arg_string("(std::map m = {})") - self.assertEqual(args, [("std::map", "m", "{}")]) + args, mods = parse_arg_string("(std::map m = {})") + self.assertEqual(args, [(None, "std::map", "m", "{}")]) # ========================================================================= # Complex CV-qualifiers @@ -331,20 +325,18 @@ def test_default_template_with_comma(self): def test_const_ref(self): """const std::string& s""" - args, mods = FunctionMember.parse_arg_string("(const std::string& s)") - self.assertEqual(args, [("const std::string&", "s", None)]) + args, mods = parse_arg_string("(const std::string& s)") + self.assertEqual(args, [("const", "std::string&", "s", None)]) def test_const_ptr_const_ref(self): """const int* const& ptr""" - args, mods = FunctionMember.parse_arg_string("(const int* const& ptr)") - self.assertEqual(args, [("const int* const&", "ptr", None)]) + args, mods = parse_arg_string("(const int* const& ptr)") + self.assertEqual(args, [("const", "int* const&", "ptr", None)]) def test_shared_ptr_const(self): """const std::shared_ptr& p""" - args, mods = FunctionMember.parse_arg_string( - "(const std::shared_ptr& p)" - ) - self.assertEqual(args, [("const std::shared_ptr&", "p", None)]) + args, mods = parse_arg_string("(const std::shared_ptr& p)") + self.assertEqual(args, [("const", "std::shared_ptr&", "p", None)]) # ========================================================================= # Mixed complex cases @@ -352,14 +344,14 @@ def test_shared_ptr_const(self): def test_mixed_args_with_modifiers(self): """Multiple complex args with const override""" - args, mods = FunctionMember.parse_arg_string( + args, mods = parse_arg_string( "(std::map m, std::function f) const override" ) self.assertEqual( args, [ - ("std::map", "m", None), - ("std::function", "f", None), + (None, "std::map", "m", None), + (None, "std::function", "f", None), ], ) self.assertTrue(mods.is_const) @@ -367,15 +359,15 @@ def test_mixed_args_with_modifiers(self): def test_full_complex_signature(self): """Complex signature with everything""" - args, mods = FunctionMember.parse_arg_string( + args, mods = parse_arg_string( "(const std::vector& v, " "std::function f = nullptr) const noexcept override" ) self.assertEqual( args, [ - ("const std::vector&", "v", None), - ("std::function", "f", "nullptr"), + ("const", "std::vector&", "v", None), + (None, "std::function", "f", "nullptr"), ], ) self.assertTrue(mods.is_const)