diff --git a/code_review_graph/parser.py b/code_review_graph/parser.py index f681263..12427d7 100644 --- a/code_review_graph/parser.py +++ b/code_review_graph/parser.py @@ -3978,6 +3978,45 @@ def _get_name(self, node, language: str, kind: str) -> Optional[str]: result = self._get_name(child, language, kind) if result: return result + # C++: inside function_declarator, the name appears as + # qualified_identifier (Class::method), destructor_name (~Class), + # operator_name (operator==), or field_identifier. The generic + # loop below only recognizes 'identifier'/'type_identifier', + # so scoped method definitions would otherwise fall through and + # match the outer return-type type_identifier as the function name. + # Nested scopes (Outer::Inner::method) produce nested + # qualified_identifier nodes — peel until we find the leaf name. + if language == "cpp" and node.type == "function_declarator": + def _leaf_name(qi): + # Walk right-to-left: the rightmost identifier/ + # destructor_name/operator_name is the method name. + # If the rightmost child is itself a qualified_identifier + # (nested scope), recurse into it. + for sub in reversed(qi.children): + if sub.type in ( + "identifier", + "destructor_name", + "operator_name", + ): + return sub.text.decode( + "utf-8", errors="replace") + if sub.type == "qualified_identifier": + inner = _leaf_name(sub) + if inner: + return inner + return None + for child in node.children: + if child.type == "qualified_identifier": + name = _leaf_name(child) + if name: + return name + if child.type in ( + "field_identifier", + "destructor_name", + "operator_name", + ): + return child.text.decode( + "utf-8", errors="replace") # Objective-C method_definition: the method name is the first # ``identifier`` child (first part of the selector). Multi-part diff --git a/tests/test_parser.py b/tests/test_parser.py index b38ff11..9c946f0 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1209,3 +1209,44 @@ def test_elixir_top_level_dotted_call_attributes_to_file(self): and e.target.endswith("puts") ] assert len(top_level) == 1 + + def test_cpp_scoped_method_names(self, tmp_path): + """C++ scoped method definitions must extract the leaf method name, + not the return-type identifier. + + Regression: previously ``Ret Class::method()`` indexed as ``Ret`` + (return type) and ``void Class::method()`` was silently dropped + because _get_name() fell through to the generic identifier loop, + which did not recognise qualified_identifier, destructor_name, or + operator_name nodes inside function_declarator. + """ + src = b""" +void PlaybackExtension::resetStateForPool() {} +quint64 PlaybackExtension::startTimestamp() const { return 0; } +PlaybackExtension::~PlaybackExtension() {} +~PlaybackExtension() {} +bool operator==(const A& a, const B& b) { return true; } +bool MyClass::operator<(const MyClass& o) const { return true; } +void foo() {} +int SnapshotController::getHandleIndex() { return 0; } +bool PlaybackWidget::AllocateResourceStrategy::allocateExtensionResource(int i) { return true; } +void A::B::C::deep() {} +ExtensionID PlaybackExtension::ID() const { return {}; } +""" + p = tmp_path / "x.cpp" + p.write_bytes(src) + nodes, _ = self.parser.parse_file(p) + names = [n.name for n in nodes if n.kind == "Function"] + assert names == [ + "resetStateForPool", + "startTimestamp", + "~PlaybackExtension", + "~PlaybackExtension", + "operator==", + "operator<", + "foo", + "getHandleIndex", + "allocateExtensionResource", + "deep", + "ID", + ]