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
+40 -1
View File
@@ -166,6 +166,35 @@ export function stripCppTemplateArgs(name: string): string {
return out.trim();
}
/**
* Is this C++ `field_declaration` a pure-virtual method (`virtual int read(int key) = 0;`)?
* tree-sitter-cpp shapes those as a field_declaration whose declarator unwraps to a
* `function_declarator`, with the pure-virtual `= 0` as a DIRECT `number_literal` "0"
* child of the field_declaration (default-arg `= 0` lives inside parameter_declaration
* and must not match). Bodiless method prototypes (`int foo();`) and data members
* (`int x = 0;`) are excluded — prototypes usually have an out-of-line definition that
* already mints the method node; pure virtuals never do (#1727).
*/
export function isCppPureVirtualMethodDecl(node: SyntaxNode): boolean {
if (node.type !== 'field_declaration') return false;
let declarator: SyntaxNode | null = getChildByField(node, 'declarator');
if (!declarator) return false;
while (
declarator.type === 'pointer_declarator' ||
declarator.type === 'reference_declarator'
) {
const inner: SyntaxNode | null =
getChildByField(declarator, 'declarator') || declarator.namedChild(0);
if (!inner) return false;
declarator = inner;
}
if (declarator.type !== 'function_declarator') return false;
return node.namedChildren.some(
(c: SyntaxNode) => c.type === 'number_literal' && c.text === '0'
);
}
/**
* A function/method's return type lives in the `function_definition`'s `type`
* field (`Metrics& Metrics::instance()` → `Metrics`). Constructors, destructors,
@@ -1607,7 +1636,17 @@ export const cppExtractor: LanguageExtractor = {
// get picked as the blast-radius representative over — the single real
// definition, exactly as bodiless struct/enum specifiers are already skipped. (#1093)
skipBodilessClass: true,
methodTypes: ['function_definition'],
// `function_definition` covers inline / out-of-line bodies; `field_declaration`
// covers pure-virtual methods (`virtual int read(int key) = 0;`), which have no
// body and would otherwise mint no node — so calls through the abstract base and
// cpp-override synthesis had nothing to attach to (#1727). classifyMethodNode
// keeps ordinary data members / prototypes on the children-walk path.
methodTypes: ['function_definition', 'field_declaration'],
classifyMethodNode: (node) => {
if (node.type !== 'field_declaration') return 'method';
return isCppPureVirtualMethodDecl(node) ? 'method' : 'skip';
},
isAbstract: (node) => (isCppPureVirtualMethodDecl(node) ? true : undefined),
interfaceTypes: [],
structTypes: ['struct_specifier'],
// C++ unions additionally carry member functions, which extract through the
+8 -2
View File
@@ -160,6 +160,8 @@ export interface LanguageExtractor {
isAsync?: (node: SyntaxNode) => boolean;
/** Check if node is static */
isStatic?: (node: SyntaxNode) => boolean;
/** Check if a method/class is abstract (C++ pure virtual, Java abstract, …). Return true to set; undefined/false leaves the flag unset. */
isAbstract?: (node: SyntaxNode) => boolean | undefined;
/** Check if variable declaration is a constant (const vs let/var) */
isConst?: (node: SyntaxNode) => boolean;
/**
@@ -221,9 +223,13 @@ export interface LanguageExtractor {
* both callable and data members (#808): TS/JS class FIELDS
* (`public_field_definition` / `field_definition`) are methods only when
* their value is callable (`onClick = () => {}`); a plain field
* (`public fonts: Fonts;`, `count = 0`) is a property. Default: 'method'.
* (`public fonts: Fonts;`, `count = 0`) is a property. C++ also lists
* `field_declaration` in methodTypes so pure-virtual methods (`= 0`) can
* mint nodes (#1727); non-callable field_declarations return `'skip'` so
* the walker still descends (data-member initializers keep their call
* edges). Default: 'method'.
*/
classifyMethodNode?: (node: SyntaxNode) => 'method' | 'property';
classifyMethodNode?: (node: SyntaxNode) => 'method' | 'property' | 'skip';
/**
* Resolve the body node for a function/method/class when it's not a child field.
+12 -2
View File
@@ -1041,8 +1041,14 @@ export class TreeSitterExtractor {
else if (this.extractor.methodTypes.includes(nodeType)) {
// TS/JS class fields parse as a methodTypes node; only function-valued
// fields are methods — a plain field (`public fonts: Fonts;`) is a
// property (#808). classifyMethodNode is absent for other languages.
if (this.extractor.classifyMethodNode?.(node) === 'property') {
// property (#808). C++ lists `field_declaration` so pure-virtual methods
// mint nodes (#1727); non-callable ones return 'skip' and fall through to
// the children walk. classifyMethodNode is absent for other languages.
const methodClass = this.extractor.classifyMethodNode?.(node) ?? 'method';
if (methodClass === 'skip') {
// Not a method — leave skipChildren false so data-member initializers
// still contribute call/instantiation edges under the enclosing class.
} else if (methodClass === 'property') {
const propNode = this.extractProperty(node);
// Walk the initializer so its calls/instantiations attribute to the
// property (`history = createHistory()` → history calls
@@ -1798,6 +1804,9 @@ export class TreeSitterExtractor {
const visibility = this.extractor.getVisibility?.(node);
const isAsync = this.extractor.isAsync?.(node);
const isStatic = this.extractor.isStatic?.(node);
// Only persist abstract when true — a false return must not mint `isAbstract: false`
// on every ordinary method (breaks kernel↔wasm parity JSON equality).
const isAbstract = this.extractor.isAbstract?.(node) ? true : undefined;
const returnType = this.extractor.getReturnType?.(node, this.source);
const extraProps: Partial<Node> = {
docstring,
@@ -1805,6 +1814,7 @@ export class TreeSitterExtractor {
visibility,
isAsync,
isStatic,
isAbstract,
returnType,
};
if (receiverType) {