fix(extraction): index C++ pure virtual methods as nodes (#1727) (#1758)

Pure-virtual declarations (`virtual int read(int key) = 0;`) parse as
field_declaration, not function_definition, so they minted no method node —
calls through an abstract base and cpp-override synthesis had nothing to
attach to. Mirror Java interface methods: mint the node (TS + kernel), mark
isAbstract, and cover with extraction/e2e/parity fixtures.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 02:04:12 -05:00
committed by GitHub
co-authored by Colby McHenry
parent 8df9ecac9d
commit 2f1a99d34c
7 changed files with 240 additions and 6 deletions
+55 -1
View File
@@ -52,6 +52,10 @@
//! per-caller targets (insertion-ordered, branch reassignments accumulate);
//! a later bare `k(args)` emits one `calls` ref PER target and suppresses
//! the local name. Template args stripped like base-class refs (#1043).
//! - pure-virtual methods (#1727): cpp in-class `virtual T f(...) = 0;` is a
//! `field_declaration` (not `function_definition`); mint a method node so
//! abstract-base calls and cpp-override synthesis have a target. Mirrors
//! TS `methodTypes` + `classifyMethodNode` / `isAbstract`.
//! - stack construction (#1035): cpp `declaration` with class-like named
//! `type` and an init_declarator whose value is argument_list /
//! initializer_list → `instantiates` (most-vexing-parse excluded).
@@ -66,7 +70,7 @@
use crate::buffers::{
build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow,
RefRow, StrRef, Tables, FLAG_IS_EXPORTED, FUNCTION_REF_CODE, NONE, NONE_STR,
RefRow, StrRef, Tables, FLAG_IS_ABSTRACT, FLAG_IS_EXPORTED, FUNCTION_REF_CODE, NONE, NONE_STR,
};
use crate::docstring::preceding_docstring;
use crate::ids;
@@ -289,6 +293,7 @@ struct Extra {
signature: Option<String>,
visibility: Option<u8>,
is_exported: Option<bool>,
is_abstract: Option<bool>,
return_type: Option<String>,
qualified_name: Option<String>,
}
@@ -508,6 +513,9 @@ impl<'t> Walker<'t> {
if let Some(v) = extra.is_exported {
flags.set(FLAG_IS_EXPORTED, v);
}
if let Some(v) = extra.is_abstract {
flags.set(FLAG_IS_ABSTRACT, v);
}
let name_ref = self.arena.put(name);
let qn_ref = self.arena.put(&qualified);
let id_ref = self.arena.put(&id);
@@ -740,6 +748,36 @@ impl<'t> Walker<'t> {
.any(|c| c.kind() == "type_qualifier" && self.text(c) == "const")
}
/// `#1727`: C++ pure-virtual method declaration (`virtual int read(int key) = 0;`).
/// tree-sitter-cpp shapes these as `field_declaration` whose declarator unwraps
/// to a `function_declarator`, with the pure-virtual `= 0` as a DIRECT
/// `number_literal` "0" child (default-arg `= 0` lives inside
/// `parameter_declaration` and must not match).
fn is_cpp_pure_virtual_method_decl(&self, node: Node<'_>) -> bool {
if node.kind() != "field_declaration" {
return false;
}
let Some(mut declarator) = node.child_by_field_name("declarator") else {
return false;
};
while matches!(declarator.kind(), "pointer_declarator" | "reference_declarator") {
let inner = declarator
.child_by_field_name("declarator")
.or_else(|| declarator.named_child(0));
let Some(inner) = inner else {
return false;
};
declarator = inner;
}
if declarator.kind() != "function_declarator" {
return false;
}
(0..node.named_child_count())
.filter_map(|i| node.named_child(i))
.any(|c| c.kind() == "number_literal" && self.text(c) == "0")
}
/// cppExtractor.isMisparsedFunction (languages/c-cpp.ts:811). cpp only.
fn is_misparsed_function(&self, name: &str, node: Node) -> bool {
if self.variant != Variant::Cpp {
@@ -830,6 +868,17 @@ impl<'t> Walker<'t> {
self.extract_variable(node);
self.scan_fn_ref_subtree(node, 0);
skip_children = true;
} else if self.variant == Variant::Cpp
&& kind == "field_declaration"
&& self.inside_class_like()
&& self.is_cpp_pure_virtual_method_decl(node)
{
// Pure-virtual methods have no `function_definition` body — mint the
// method node so calls through the abstract base and cpp-override
// synthesis have a target (#1727). Non-pure field_declarations fall
// through to the children walk (data members / prototypes).
self.extract_method(node);
skip_children = true;
} else if kind == "preproc_include" {
self.extract_import(node);
} else if kind == "call_expression" {
@@ -914,6 +963,11 @@ impl<'t> Walker<'t> {
let extra = Extra {
docstring: preceding_docstring(node, self.src),
visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None },
is_abstract: if self.variant == Variant::Cpp && self.is_cpp_pure_virtual_method_decl(node) {
Some(true)
} else {
None
},
return_type: self.return_type_of(node),
qualified_name: receiver_type
.as_ref()