Repository: colbymchenry/codegraph (2026-06-22T13:39:14Z)

Issue:
C++: class with export macro + inheritance mislabeled as kind=function in header
### Problem

A C++ class declaration in a header file that uses an **export macro** (e.g. `__attribute__((visibility("default")))`) **and** inherits from a base class is indexed as `kind=function` instead of `kind=class`. Its `qualified_name` has no `::` scope, and its line range covers the entire class body.

### Minimal Reproduction

Create `provider.h`:

```cpp
#pragma once
#include <cstdint>

#define MAPCORE_EXPORT __attribute__((visibility("default")))

class DataProvider {
public:
    virtual ~DataProvider() = default;
    virtual bool Request(void* param) = 0;
};

class MapDataLoader;
namespace mirror { class Framework; }

class MAPCORE_EXPORT LocalDataProvider : public DataProvider
{
public:
    LocalDataProvider(int32_t dataType, MapDataLoader* loader, mirror::Framework* framework);
    virtual ~LocalDataProvider();

    static void* ConvertTileBlobToData(int32_t dataType,
                                       mirror::Framework* framework,
                                       const void* blob,
                                       bool deepCopy = false);
    virtual bool Request(void* param) override;

private:
    int32_t m_dataType{0};
};
```

```bash
codegraph init
sqlite3 .codegraph/codegraph.db "SELECT kind, name, qualified_name, start_line, end_line FROM nodes WHERE name = 'LocalDataProvider'"
```

**Actual:**
```
function|LocalDataProvider|LocalDataProvider|22|39
```

**Expected:**
```
class|LocalDataProvider|LocalDataProvider|22|39
```

Note: `DataProvider` (no export macro) in the same file is correctly indexed as `class`. Removing `MAPCORE_EXPORT` from `LocalDataProvider`'s declaration makes it index correctly as `class`.

### Trigger Conditions

The mislabeling occurs when **both** conditions are met:
1. An export/visibility macro precedes the class name: `class MACRO ClassName`
2. The class inherits from a base: `: public BaseClass`

A class with only one of these (macro but no inheritance, or inheritance but no macro) is indexed correctly.

### Impact

- `codegraph impact` and `codegraph callers` may include this phantom "function" node in call-graph traversal results
- SQLite `WITH RECURSIVE` queries for blast-radius analysis pick up the node as a false caller
- Graph statistics (node kind distribution) are skewed

### Workaround

```sql
-- Exclude header-file "function" nodes without :: in qualified_name
AND NOT (kind = 'function' AND file_path LIKE '%.h' AND qualified_name NOT LIKE '%::%')
```

### Environment

- CodeGraph 1.0.1
- macOS arm64
- C++ codebase (~2500 .h/.cpp files)


Discussion:
Fixed in #948 (merged to `main`) — thanks for the clean minimal reproduction, it made the root cause easy to pin down.

Confirmed by dumping the parse tree: tree-sitter doesn't expand macros, so `class MAPCORE_EXPORT LocalDataProvider : public DataProvider` is read as an *elaborated type specifier* (`class MAPCORE_EXPORT`, the macro mistaken for the type) followed by a declarator and a body — i.e. the whole declaration becomes a `function_definition` named after the class. CodeGraph now detects that misparse structurally and **drops the phantom node**, so it no longer pollutes `codegraph callers` / `codegraph impact` / blast-radius or skews the kind counts.

One honest caveat on scope: the fix *removes* the bogus `function` rather than re-indexing `LocalDataProvider` as a `class`. The same misparse also mangles the class body (members get scattered into error/expression fragments), so the declaration isn't recoverable as a clean class without genuine macro-expansion awareness — which tree-sitter alone doesn't have. Removing the misleading node is the honest fix here; the symbol simply won't appear (rather than appearing wrong).

Also worth noting: the misparse is triggered by the **leading macro alone** — `class MACRO Name { … }` misparses even without a base class — so the fix covers that case too (and the `struct` variant), not just the macro+inheritance combination. Ships in the next release.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33cc12a6c..87d364466 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - A long-running MCP server now recovers when your index is deleted and rebuilt at the same path. If `.codegraph/` was removed and recreated while the server held it open — most easily by recreating a git worktree at the same path, or `rm`-ing `.codegraph/` and running `codegraph init` again — the server kept reading the old, deleted database file and served a frozen snapshot: renamed or removed symbols still showed as live, new ones were missing, and `codegraph sync` couldn't refresh it — only restarting the server fixed it. The server now detects that the database file was swapped out from under it and reopens the live one in place, so results stay correct without a restart. (On Linux and macOS; Windows doesn't allow deleting an open file, so it isn't affected.) (#925)
 - The MCP server now opens and auto-syncs a project that lives inside a folder an enclosing git repository ignores. Before, if the directory you indexed sat within a larger repo that gitignored it, the shared MCP daemon failed to open the project — its log repeated `Failed to open project … path should be a` `path.relative()` `d string, but got "./"` — so the file watcher never started and the index silently went stale until you ran `codegraph sync` by hand (setting `CODEGRAPH_NO_DAEMON=1` was the only workaround). The daemon now opens the project and starts watching as expected. Most visible with Codex on Windows, but the cause wasn't platform-specific. (#936)
 - A git worktree of a submodule is no longer indexed as a duplicate copy of that submodule's code. CodeGraph already skips ordinary worktrees (a second working view of a repo it indexes), but a worktree created *from a submodule* — common in monorepos that check submodules out into worktrees for parallel feature work — was mistaken for a genuine embedded repo and swept in, duplicating every symbol it shared with the real submodule checkout (one report had ~28% of its index as duplicates, inflating both query results and the database). These submodule worktrees are now recognized and skipped, while the submodule's own checkout stays indexed as distinct code. Thanks @charlesxu2026-ship-it. (#945)
+- A C++ class or struct annotated with an export/visibility macro — `class MYLIB_EXPORT Foo : public Bar { … }`, the common DLL-export / visibility pattern in headers — is no longer mis-indexed as a function spanning the whole class body. Not knowing the macro is a macro, the parser read it as a type and the whole declaration as a function, so the class surfaced as a phantom `function` that showed up as a false caller in `codegraph callers`, `codegraph impact`, and blast-radius analysis, and skewed symbol counts. CodeGraph now recognizes this misparse and drops the bogus node. Thanks @spwlyzx. (#946)
 
 
 ## [1.0.1] - 2026-06-13
diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts
index 10012ea83..0d21522c4 100644
--- a/src/extraction/languages/c-cpp.ts
+++ b/src/extraction/languages/c-cpp.ts
@@ -148,6 +148,40 @@ export const cExtractor: LanguageExtractor = {
   },
 };
 
+/**
+ * Detect tree-sitter's misparse of a macro-annotated class/struct, e.g.
+ * `class MACRO Name { … }` or `class MACRO Name : public Base { … }` (#946).
+ * Not knowing `MACRO` is a macro, tree-sitter reads `class MACRO` as an
+ * *elaborated type specifier* (a bodyless `class_specifier`/`struct_specifier`
+ * whose "type name" is the macro) and the rest as a function: `Name` becomes the
+ * declarator and the `{ … }` a function body — so the whole declaration surfaces
+ * as a `function_definition` named after the class, with a line range spanning
+ * the entire class body. (A base clause, when present, additionally lands in an
+ * `ERROR` node, but it isn't required — the leading macro alone triggers this.)
+ *
+ * Two structural signals pin it down with no risk to genuine code:
+ *  - the `type` field is a *bodyless* class/struct specifier — an elaborated
+ *    type, not a real inline-defined return type like
+ *    `struct P { int x; } makeP() { … }` (which carries a field list); and
+ *  - the declarator is not a `function_declarator` — a real function definition
+ *    always has one, which also leaves the legal-but-rare `class Foo f() { … }`
+ *    (an elaborated return type on a genuine function) alone.
+ *
+ * The class body is mangled by the same misparse and is unrecoverable, so —
+ * matching how macro-prefixed C prototypes are handled — we drop the spurious
+ * node rather than mint a misleading whole-body `function` that pollutes
+ * callers/impact and skews kind statistics.
+ */
+function isMacroMisparsedTypeDecl(node: SyntaxNode): boolean {
+  const typeNode = getChildByField(node, 'type');
+  if (!typeNode) return false;
+  if (typeNode.type !== 'class_specifier' && typeNode.type !== 'struct_specifier') return false;
+  if (typeNode.namedChildren.some((c: SyntaxNode) => c.type === 'field_declaration_list')) return false;
+  const declarator = getChildByField(node, 'declarator');
+  if (declarator && declarator.type === 'function_declarator') return false;
+  return true;
+}
+
 export const cppExtractor: LanguageExtractor = {
   functionTypes: ['function_definition'],
   classTypes: ['class_specifier'],
@@ -192,14 +226,17 @@ export const cppExtractor: LanguageExtractor = {
     }
     return undefined;
   },
-  isMisparsedFunction: (name) => {
+  isMisparsedFunction: (name, node) => {
     // 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);
+    if (cppKeywords.includes(name)) return true;
+    // `class MACRO Name : public Base { … }` misparses to a function_definition
+    // named after the class — drop that phantom (#946).
+    return isMacroMisparsedTypeDecl(node);
   },
   extractImport: (node, source) => {
     const importText = source.substring(node.startIndex, node.endIndex).trim();


Test patch:
diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts
index df825f529..b2cf85d9b 100644
--- a/__tests__/extraction.test.ts
+++ b/__tests__/extraction.test.ts
@@ -2582,6 +2582,63 @@ std::unique_ptr<Widget> makeWidget() { return nullptr; }
     });
   });
 
+  describe('C++ macro-prefixed class/struct misparse (#946)', () => {
+    // An export/visibility macro before the class name plus a base clause
+    // (`class MACRO Name : public Base { … }`) makes tree-sitter read `class
+    // MACRO` as an elaborated type and the whole declaration as a
+    // function_definition named after the class, spanning the entire body — a
+    // phantom `function` that polluted callers/impact/blast-radius. It's dropped.
+    it('does not mint a phantom function for a macro-annotated class that inherits', () => {
+      const code = `#pragma once
+#define MAPCORE_EXPORT __attribute__((visibility("default")))
+
+class DataProvider {
+public:
+    virtual bool Request(void* param) = 0;
+};
+
+class MAPCORE_EXPORT LocalDataProvider : public DataProvider
+{
+public:
+    LocalDataProvider(int dataType);
+    virtual bool Request(void* param) override;
+};
+`;
+      // A header rich in C++ (class / public: / virtual) detects as C++ — the
+      // issue's exact scenario (a `.h` file). Guard it so a detection regression
+      // can't make this test pass for the wrong reason.
+      expect(detectLanguage('provider.h', code)).toBe('cpp');
+      const result = extractFromSource('provider.h', code);
+
+      // The misparse used to surface as `function | LocalDataProvider` spanning
+      // the whole class body — a false caller in the graph. It's gone now.
+      expect(
+        result.nodes.find((n) => n.name === 'LocalDataProvider' && n.kind === 'function')
+      ).toBeUndefined();
+
+      // The sibling class without the macro is unaffected — still a class.
+      expect(result.nodes.find((n) => n.name === 'DataProvider')?.kind).toBe('class');
+    });
+
+    it('drops the struct variant too, without dropping a genuine class', () => {
+      const code = `
+#define API __declspec(dllexport)
+struct API Widget : public Base { int x; };
+class Plain : public Base { public: int y; };
+`;
+      const result = extractFromSource('widget.cpp', code);
+
+      // `struct MACRO Name : Base { … }` misparses the same way — no phantom function.
+      expect(
+        result.nodes.find((n) => n.name === 'Widget' && n.kind === 'function')
+      ).toBeUndefined();
+
+      // A normal class with a base clause and no macro must still be a class — the
+      // drop is precise, not a blanket "class with inheritance" filter.
+      expect(result.nodes.find((n) => n.name === 'Plain')?.kind).toBe('class');
+    });
+  });
+
   describe('C/C++ imports', () => {
     it('should extract system include', () => {
       const code = `#include <iostream>`;