feat: Add C++ macro misparse handling and structural node extraction in function bodies

Addresses C++ macros like NLOHMANN_JSON_NAMESPACE_BEGIN that cause tree-sitter to misparse namespace blocks as function_definitions. Adds isMisparsedFunction hook to filter macro artifacts while still visiting their bodies to extract legitimate class/struct/enum definitions hidden inside the misparsed "function" scope.
This commit is contained in:
Colby McHenry
2026-04-06 23:24:26 -05:00
parent 6f34be38aa
commit 237fb3b206
4 changed files with 79 additions and 7 deletions
+9
View File
@@ -88,6 +88,15 @@ export const cppExtractor: LanguageExtractor = {
}
return undefined;
},
isMisparsedFunction: (name) => {
// C++ macros like NLOHMANN_JSON_NAMESPACE_BEGIN cause tree-sitter to misparse
// namespace blocks as function_definitions (e.g. name = "namespace detail").
// Also filter C++ keywords that tree-sitter occasionally misinterprets as
// function/method names (e.g. switch statements inside macro-confused scopes).
if (name.startsWith('namespace')) return true;
const cppKeywords = ['switch', 'if', 'for', 'while', 'do', 'case', 'return'];
return cppKeywords.includes(name);
},
extractImport: (node, source) => {
const importText = source.substring(node.startIndex, node.endIndex).trim();
// C++ includes: #include <iostream>, #include "myheader.h"
+9
View File
@@ -183,4 +183,13 @@ export interface LanguageExtractor {
* or undefined to keep it as a type alias.
*/
resolveTypeAliasKind?: (node: SyntaxNode, source: string) => NodeKind | undefined;
/**
* Check if a function/method name is a misparse artifact that should be skipped.
* Used by C/C++ where macros (e.g. NLOHMANN_JSON_NAMESPACE_BEGIN) cause tree-sitter
* to misparse namespace blocks as function_definitions. When this returns true,
* the function node is NOT created, but the body is still visited for calls and
* structural nodes (classes, structs, enums).
*/
isMisparsedFunction?: (name: string, node: SyntaxNode) => boolean;
}
+58 -6
View File
@@ -455,6 +455,17 @@ export class TreeSitterExtractor {
}
if (name === '<anonymous>') return; // Skip anonymous functions
// Check for misparse artifacts (e.g. C++ macros causing "namespace detail" functions)
// Skip the node but still visit the body for calls and structural nodes
if (this.extractor.isMisparsedFunction?.(name, node)) {
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
if (body) {
this.visitFunctionBody(body, '');
}
return;
}
const docstring = getPrecedingDocstring(node, this.source);
const signature = this.extractor.getSignature?.(node, this.source);
const visibility = this.extractor.getVisibility?.(node);
@@ -542,6 +553,17 @@ export class TreeSitterExtractor {
}
const name = extractName(node, this.source, this.extractor);
// Check for misparse artifacts (e.g. C++ "switch" inside macro-confused class body)
if (this.extractor.isMisparsedFunction?.(name, node)) {
const body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
if (body) {
this.visitFunctionBody(body, '');
}
return;
}
const docstring = getPrecedingDocstring(node, this.source);
const signature = this.extractor.getSignature?.(node, this.source);
const visibility = this.extractor.getVisibility?.(node);
@@ -1272,26 +1294,56 @@ export class TreeSitterExtractor {
}
/**
* Visit function body and extract calls
* Visit function body and extract calls (and structural nodes).
*
* In addition to call expressions, this also detects class/struct/enum
* definitions inside function bodies. This handles two cases:
* 1. Local class/struct/enum definitions (valid in C++, Java, etc.)
* 2. C++ macro misparsing — macros like NLOHMANN_JSON_NAMESPACE_BEGIN cause
* tree-sitter to interpret the namespace block as a function_definition,
* hiding real class/struct/enum nodes inside the "function body".
*/
private visitFunctionBody(body: SyntaxNode, _functionId: string): void {
if (!this.extractor) return;
// Recursively find all call expressions
const visitForCalls = (node: SyntaxNode): void => {
if (this.extractor!.callTypes.includes(node.type)) {
const visitForCallsAndStructure = (node: SyntaxNode): void => {
const nodeType = node.type;
if (this.extractor!.callTypes.includes(nodeType)) {
this.extractCall(node);
}
// Extract structural nodes found inside function bodies.
// Each extract method visits its own children, so we return after extracting.
if (this.extractor!.classTypes.includes(nodeType)) {
const classification = this.extractor!.classifyClassNode?.(node) ?? 'class';
if (classification === 'struct') this.extractStruct(node);
else if (classification === 'enum') this.extractEnum(node);
else this.extractClass(node);
return;
}
if (this.extractor!.structTypes.includes(nodeType)) {
this.extractStruct(node);
return;
}
if (this.extractor!.enumTypes.includes(nodeType)) {
this.extractEnum(node);
return;
}
if (this.extractor!.interfaceTypes.includes(nodeType)) {
this.extractInterface(node);
return;
}
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (child) {
visitForCalls(child);
visitForCallsAndStructure(child);
}
}
};
visitForCalls(body);
visitForCallsAndStructure(body);
}
/**