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/member.py b/scripts/cxx-api/parser/member.py index 7bfddf8062e5..13bcea436438 100644 --- a/scripts/cxx-api/parser/member.py +++ b/scripts/cxx-api/parser/member.py @@ -5,12 +5,18 @@ 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, +) if TYPE_CHECKING: from .scope import Scope @@ -90,6 +96,10 @@ 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 @@ -125,15 +135,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 +155,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, @@ -178,12 +164,16 @@ def __init__( arg_string: str, is_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 + ) self.is_const = self.modifiers.is_const self.is_override = self.modifiers.is_override @@ -218,19 +208,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,325 +235,25 @@ 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 @@ -602,14 +280,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/utils.py b/scripts/cxx-api/parser/utils.py index d69920cdaf73..e69de29bb2d1 100644 --- a/scripts/cxx-api/parser/utils.py +++ b/scripts/cxx-api/parser/utils.py @@ -1,63 +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. - - -def parse_qualified_path(path: str) -> list[str]: - """ - Parse a qualified path into a list of names. - - Handles template syntax correctly by not splitting on "::" inside - angle brackets. For example: - - "std::vector::test" -> ["std", "vector", "test"] - - "ns::Foo::Bar" -> ["ns", "Foo", "Bar"] - - Also handles edge cases: - - Comparison operators inside parentheses: "std::enable_if<(N > 0)>::type" - - Arrow operators: "decltype(ptr->member)::type" - - Bitshift operators: "std::integral_constant> 2)>::value" - """ - result = [] - current = "" - angle_depth = 0 - paren_depth = 0 - i = 0 - - while i < len(path): - char = path[i] - - if char == "(": - paren_depth += 1 - current += char - i += 1 - elif char == ")": - paren_depth -= 1 - current += char - i += 1 - elif char == "<" and paren_depth == 0: - angle_depth += 1 - current += char - i += 1 - elif char == ">" and paren_depth == 0: - # Check for arrow operator "->" which should not affect angle_depth - if i > 0 and path[i - 1] == "-": - current += char - i += 1 - else: - angle_depth -= 1 - current += char - i += 1 - elif path[i : i + 2] == "::" and angle_depth == 0 and paren_depth == 0: - if current: - result.append(current) - current = "" - i += 2 - else: - current += char - i += 1 - - if current: - result.append(current) - - return result diff --git a/scripts/cxx-api/parser/utils/__init__.py b/scripts/cxx-api/parser/utils/__init__.py new file mode 100644 index 000000000000..5f46db325571 --- /dev/null +++ b/scripts/cxx-api/parser/utils/__init__.py @@ -0,0 +1,28 @@ +# 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 + +__all__ = [ + "Argument", + "extract_qualifiers", + "format_arguments", + "format_parsed_type", + "FunctionModifiers", + "parse_arg_string", + "parse_function_pointer_argstring", + "parse_qualified_path", + "parse_type_with_argstrings", +] 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/qualified_path.py b/scripts/cxx-api/parser/utils/qualified_path.py new file mode 100644 index 000000000000..1010ca6a6cdb --- /dev/null +++ b/scripts/cxx-api/parser/utils/qualified_path.py @@ -0,0 +1,65 @@ +# 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 + + +def parse_qualified_path(path: str) -> list[str]: + """ + Parse a qualified path into a list of names. + + Handles template syntax correctly by not splitting on "::" inside + angle brackets. For example: + - "std::vector::test" -> ["std", "vector", "test"] + - "ns::Foo::Bar" -> ["ns", "Foo", "Bar"] + + Also handles edge cases: + - Comparison operators inside parentheses: "std::enable_if<(N > 0)>::type" + - Arrow operators: "decltype(ptr->member)::type" + - Bitshift operators: "std::integral_constant> 2)>::value" + """ + result = [] + current = "" + angle_depth = 0 + paren_depth = 0 + i = 0 + + while i < len(path): + char = path[i] + + if char == "(": + paren_depth += 1 + current += char + i += 1 + elif char == ")": + paren_depth -= 1 + current += char + i += 1 + elif char == "<" and paren_depth == 0: + angle_depth += 1 + current += char + i += 1 + elif char == ">" and paren_depth == 0: + # Check for arrow operator "->" which should not affect angle_depth + if i > 0 and path[i - 1] == "-": + current += char + i += 1 + else: + angle_depth -= 1 + current += char + i += 1 + elif path[i : i + 2] == "::" and angle_depth == 0 and paren_depth == 0: + if current: + result.append(current) + current = "" + i += 2 + else: + current += char + i += 1 + + if current: + result.append(current) + + return result 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)