fix(c,cpp): a pointer, array or function member is not a supertype (#2394)

extractInheritance carried Go's struct-embedding check (a field_declaration
with no direct field_identifier names an embedded type) into every grammar
with a field_declaration_list. tree-sitter-c/cpp, and Objective-C's C
structs, nest a member's field_identifier inside its pointer/array/
function/reference declarator, so `SharedState* shared;`, `jv elements[];`
and `virtual Status Put(...) = 0;` all became `extends` edges to the
member's type: false supertypes in the type hierarchy, blast radius and
codegraph_explore, and false cpp-override call edges built on them
(google/leveldb: 246 of 302 extends edges).

Gate the branch to Go in the TS extractor, drop the mirrored arm from the
C/C++ kernel port (and the "kept for parity" quirk note), and drop the
inert copy from the Rust-language port. C++ base_class_clause, Objective-C
superclasses and Go embedding are unchanged.

Validated on leveldb, jq, llm.c and redis: every removed extends edge is a
member site, no base-clause edge is lost; gin, SDWebImage and ripgrep
dumps are byte-identical; kernel/wasm parity sweeps show 0 diffs.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-06 23:27:58 +00:00
committed by GitHub
parent 0a670dc8cc
commit ac1f430d3e
7 changed files with 292 additions and 45 deletions
+1
View File
@@ -63,6 +63,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Dart, a call written through an import prefix, like `http.get(…)` after `import 'package:http/http.dart' as http;`, or `utils.Listener()` and `kit.Box.create()` through a prefix of your own, now links to what the library imported with that prefix exports, following its `export` chains, `show` and `hide`, and to nothing when that library is a package outside your project. Before, the prefix was taken for a variable, so `http.get(…)` could link to an unrelated `Http` class's `get` method, and calls made through a prefix into your own libraries were missing from `callers` and impact. Re-index Dart projects after upgrading.
- `codegraph sync` and the file watcher now link an import to the file it names when that file is added after the importing file was indexed, or comes back after being deleted: `import 'package:app/b.dart';` in Dart, `import './side'` in JavaScript or TypeScript, `#include "net/socket.h"` in C and C++, `require 'inc/db.php'` in PHP, `import pkg.mod` in Python, a Lua `require`, or a C# `using` of a namespace declared later. Before, the import stayed unlinked until the importing file itself changed or the project was re-indexed, so `codegraph affected` and the files listed as using the new file missed it. Re-index after upgrading.
- JSON files in a `templates` or `sections` folder are now read as Shopify Liquid only inside a Shopify theme, where the folder that contains `templates` or `sections` also has `layout/theme.liquid` or `config/settings_schema.json`. Before, all such JSON was, so a .NET project template's `template.json`, a schematic or a CMS's content showed up in the index as an empty Liquid file, and one that lists `sections` could be linked to an unrelated Liquid file of the same name. Shopify themes, including one kept in a subfolder of a repository, are indexed as before, and the next sync removes JSON that an earlier version indexed this way.
- In C, C++ and Objective-C, a struct or class member declared as a pointer, reference, array or function, like `SharedState* shared;`, `jv elements[];` or `virtual Status Put(…) = 0;`, no longer makes the struct look like it inherits from the member's type. Before, `codegraph_explore` and impact listed those types as base classes, every struct holding a pointer to a type as one of its subclasses, and a method of that struct as an override of the type's method with the same name. Real base classes like `class Derived : public Base`, Objective-C superclasses and Go's embedded fields are unchanged. Re-index C, C++ and Objective-C projects after upgrading.
## [1.6.2] - 2026-10-03
+273
View File
@@ -0,0 +1,273 @@
/**
* A C or C++ struct/class member whose declarator is a pointer, reference,
* array or function was recorded as an `extends` edge to the member's type:
*
* struct ThreadState { SharedState* shared; }; // ThreadState extends SharedState
* typedef struct { int length; jv elements[]; } jvp_array; // jvp_array extends jv
* class DB { virtual Status Put(const Slice& k) = 0; }; // DB extends Status
*
* Both extractors carried Go's struct-embedding check (`type DB struct { *Head }`
* — a `field_declaration` with no `field_identifier` names an embedded type)
* and ran it for every grammar with a `field_declaration_list`. tree-sitter-c
* and -cpp, and the C structs of Objective-C, nest the member's name inside
* its declarator, so every such member looked embedded and its type showed up
* as a supertype: in the type hierarchy, the "extends" chips, blast radius
* and codegraph_explore. google/leveldb had 246 of its 302 `extends` edges
* from members.
*
* Embedding is Go's alone, so the check now runs for Go only. C++ base
* classes and Objective-C superclasses come from their own clauses and are
* untouched. Runs against the native kernel (when built) and the wasm
* extractor, which must agree.
*/
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import CodeGraph from '../src/index';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
import { buildTypeHierarchy } from '../src/graph/type-hierarchy';
import type { ExtractionResult, Language, Node } from '../src/types';
const KERNEL_PATH = path.join(
__dirname,
'..',
'codegraph-kernel',
'prebuilds',
`${process.platform}-${process.arch}`,
'codegraph-kernel.node'
);
const kernelAvailable = fs.existsSync(KERNEL_PATH) || process.env.CODEGRAPH_KERNEL_EXPECT === '1';
/** jqlang/jq's shapes: flexible and fixed arrays, pointers, function pointers in a union. */
const C_SOURCE = `typedef struct { int refcnt; } jv;
typedef struct jq_state jq_state;
typedef struct { int length; jv elements[]; } jvp_array;
struct jv_parser {
jv* stack;
jv next[2];
jv value;
int stackpos;
};
union cfunction {
jv (*a1)(jq_state *, jv);
jv (*a2)(jq_state *, jv, jv);
};
struct node {
struct node *next;
const jv *const *table;
};
`;
/** google/leveldb's shapes, plus base classes that must survive, one on a later line. */
const CPP_SOURCE = `class Cache {};
class DB {};
class Status {};
class Slice {};
struct SharedState { int total; };
struct ThreadState {
SharedState* shared;
const Cache& cache;
Cache shards[4];
Status (*hook)(int);
};
class Benchmark {
public:
Status Open(int k);
virtual Status Put(const Slice& key) = 0;
Slice operator[](int n) const;
private:
DB* db_;
Cache* cache_;
};
class Base {};
namespace ns { template <typename T> class Tpl {}; }
class Derived : public Base, private ns::Tpl<int> {
Base* parent;
};
struct Plain : Base { Base* next; };
class Wrapped
: public Base {
Base* other;
};
`;
/** Go embedding is real inheritance-by-composition and keeps its edges. */
const GO_SOURCE = `package store
type Engine struct{}
type Reader interface{ Read() }
type Store struct {
*Engine
Reader
name string
engine *Engine
}
`;
/** Objective-C reuses the C struct grammar; its superclass comes from @interface. */
const OBJC_SOURCE = `#import <Foundation/Foundation.h>
typedef struct { int width; } Size;
struct Frame {
Size *size;
Size corners[4];
Size origin;
};
@interface Image : NSObject <NSCopying> {
Size *cached;
}
@end
@implementation Image
@end
`;
const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;
/** `<source> <kind> <name>` for every extends/implements ref, sorted. */
function supertypeRefs(result: ExtractionResult): string[] {
const byId = new Map(result.nodes.map((n) => [n.id, n]));
return result.unresolvedReferences
.filter((r) => r.referenceKind === 'extends' || r.referenceKind === 'implements')
.map((r) => `${byId.get(r.fromNodeId)?.name ?? r.fromNodeId} ${r.referenceKind} ${r.referenceName}`)
.sort();
}
describe('a C/C++ member is not a supertype of its struct', () => {
let savedEnv: Record<string, string | undefined> = {};
beforeAll(async () => {
await initGrammars();
await loadGrammarsForLanguages(['c', 'cpp', 'go', 'objc']);
});
beforeEach(() => {
savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
resetKernelForTests();
});
afterEach(() => {
for (const k of ENV_KEYS) {
if (savedEnv[k] === undefined) delete process.env[k];
else process.env[k] = savedEnv[k];
}
resetKernelForTests();
});
function extract(backend: 'kernel' | 'wasm', file: string, source: string, language: Language): ExtractionResult {
if (backend === 'wasm') {
process.env.CODEGRAPH_KERNEL = '0';
return extractFromSource(file, source, language);
}
delete process.env.CODEGRAPH_KERNEL;
process.env.CODEGRAPH_KERNEL_LANGS = 'all';
const result = tryKernelExtract(file, source, language);
expect(result, `kernel extraction of ${file}`).not.toBeNull();
return result!;
}
const backends = kernelAvailable ? (['kernel', 'wasm'] as const) : (['wasm'] as const);
for (const crlf of [false, true]) {
const eol = (s: string) => (crlf ? s.replace(/\n/g, '\r\n') : s);
const label = crlf ? ' (CRLF)' : '';
it.each(backends)(`C pointer, array and function-pointer members: %s${label}`, (backend) => {
const result = extract(backend, 'src/jv.c', eol(C_SOURCE), 'c');
expect(result.nodes.map((n) => n.name)).toEqual(
expect.arrayContaining(['jvp_array', 'jv_parser', 'cfunction', 'node'])
);
expect(supertypeRefs(result)).toEqual([]);
});
it.each(backends)(`C++ members, methods and operators; base classes kept: %s${label}`, (backend) => {
const result = extract(backend, 'benchmarks/db_bench.cc', eol(CPP_SOURCE), 'cpp');
expect(supertypeRefs(result)).toEqual([
'Derived extends Base',
'Derived extends ns::Tpl',
'Plain extends Base',
'Wrapped extends Base',
]);
});
it.each(backends)(`Go embedded fields still extend: %s${label}`, (backend) => {
const result = extract(backend, 'store/store.go', eol(GO_SOURCE), 'go');
expect(supertypeRefs(result)).toEqual(['Store extends Engine', 'Store extends Reader']);
});
// Objective-C has no kernel walker; the wasm extractor is its only path.
it(`Objective-C C-struct members; @interface superclass kept: wasm${label}`, () => {
const result = extractFromSource('Image.m', eol(OBJC_SOURCE), 'objc');
expect(supertypeRefs(result)).toEqual(['Image extends NSObject', 'Image implements NSCopying']);
});
}
});
describe('the type hierarchy of an indexed C/C++ project', () => {
let root = '';
let cg: CodeGraph | undefined;
afterEach(() => {
cg?.destroy();
cg = undefined;
if (root) fs.rmSync(root, { recursive: true, force: true });
root = '';
});
it('holds only declared bases and Go embeddings', async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-member-supertypes-'));
const files: Record<string, string> = {
'include/db.h': 'class Cache {};\nclass DB {\n public:\n virtual ~DB();\n};\n',
'benchmarks/db_bench.cc': [
'#include "db.h"',
'struct SharedState { int total; };',
'struct ThreadState {',
' SharedState* shared;',
'};',
'class Benchmark {',
' Cache* cache_;',
' DB* db_;',
'};',
'class MemDB : public DB {',
' Cache* cache_;',
'};',
'',
].join('\n'),
'src/jv.c': C_SOURCE,
'store/store.go': GO_SOURCE,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
const graph = cg;
const one = (name: string): Node => {
const found = graph.getNodesByName(name).filter((n) => n.kind !== 'file' && n.kind !== 'import');
expect(found, name).toHaveLength(1);
return found[0]!;
};
const ids = ['ThreadState', 'Benchmark', 'MemDB', 'jvp_array', 'jv_parser', 'cfunction', 'node', 'Store'].map(
(name) => one(name).id
);
const supertypes = graph
.getOutgoingEdgesFrom(ids, ['extends', 'implements'])
.map((e) => `${graph.getNode(e.source)?.name} ${e.kind} ${graph.getNode(e.target)?.name}`)
.sort();
// An embedded interface resolves as `implements`.
expect(supertypes).toEqual(['MemDB extends DB', 'Store extends Engine', 'Store implements Reader']);
// What the viewer's Type hierarchy and the "extends" chips are built from.
expect(buildTypeHierarchy(graph, one('ThreadState'))).toBeNull();
expect(buildTypeHierarchy(graph, one('SharedState'))).toBeNull();
expect(buildTypeHierarchy(graph, one('jv'))).toBeNull();
expect(buildTypeHierarchy(graph, one('DB'))?.descendants.map((d) => d.node.name)).toEqual(['MemDB']);
expect(buildTypeHierarchy(graph, one('Cache'))).toBeNull();
expect(buildTypeHierarchy(graph, one('Engine'))?.descendants.map((d) => d.node.name)).toEqual(['Store']);
}, 60_000);
});
+4 -21
View File
@@ -33,11 +33,6 @@
//! (loses uninit scalars by design); cpp declarations instead take the TS
//! GENERIC fallback (direct identifier children only → `int x;` extracts,
//! `int x = 5;` does not — bug-for-bug).
//! - inheritance quirk: extractInheritance recurses into
//! field_declaration_list, where a field_declaration with no DIRECT
//! field_identifier child (pointer/array/method members) but a direct
//! type_identifier emits an `extends` ref to that type (the Go-embedding
//! branch matching c/cpp shapes). Kept: the parity gate pins today's graph.
//! - static-member/value-read pass (cpp only): `field_expression` is in
//! MEMBER_ACCESS_TYPES (listed for Scala, same node kind in cpp), so
//! `Capitalized.member` / `Capitalized->member` VALUE reads emit
@@ -1866,8 +1861,10 @@ impl<'t> Walker<'t> {
// --- inheritance ---------------------------------------------------------
/// extractInheritance — the branches whose node kinds occur in the c/cpp
/// grammars: base_class_clause (#1043), the field_declaration Go-embedding
/// shape, and the field_declaration_list recursion that reaches it.
/// grammars: base_class_clause (#1043) and the field_declaration_list
/// recursion. A member `field_declaration` is never a supertype: the TS
/// Go-embedding check is gated to Go, since c/cpp members nest their
/// field_identifier inside a pointer/array/function declarator.
fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
stack_guard!();
let extends_kind = edge_kind_index("extends").unwrap();
@@ -1886,20 +1883,6 @@ impl<'t> Walker<'t> {
}
}
}
"field_declaration" => {
let has_field_identifier = (0..child.named_child_count())
.filter_map(|j| child.named_child(j))
.any(|c| c.kind() == "field_identifier");
if !has_field_identifier {
let type_id = (0..child.named_child_count())
.filter_map(|j| child.named_child(j))
.find(|c| c.kind() == "type_identifier");
if let Some(type_id) = type_id {
let name = self.text(type_id).to_string();
self.push_ref_at(class_row, &name, extends_kind, type_id);
}
}
}
"field_declaration_list" | "class_heritage" => {
self.extract_inheritance(child, class_row);
}
+3 -17
View File
@@ -1061,9 +1061,9 @@ impl<'t> Walker<'t> {
/// extractInheritance — the rust-reachable cases: trait_bounds
/// (supertraits; a scoped `fmt::Debug` bound matches NO case and is
/// dropped), the Go embedding check on field_declaration (inert in rust —
/// every field has a field_identifier), and the field_declaration_list
/// recursion that reaches it.
/// dropped) and the field_declaration_list recursion. The TS Go-embedding
/// check on field_declaration is gated to Go (and was inert here anyway —
/// every rust field has a field_identifier).
fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
stack_guard!();
let extends_kind = edge_kind_index("extends").unwrap();
@@ -1102,20 +1102,6 @@ impl<'t> Walker<'t> {
}
}
}
"field_declaration" => {
let has_field_identifier = (0..child.named_child_count())
.filter_map(|j| child.named_child(j))
.any(|c| c.kind() == "field_identifier");
if !has_field_identifier {
let type_id = (0..child.named_child_count())
.filter_map(|j| child.named_child(j))
.find(|c| c.kind() == "type_identifier");
if let Some(type_id) = type_id {
let name = self.text(type_id).to_string();
self.push_ref_at(class_row, &name, extends_kind, type_id);
}
}
}
"field_declaration_list" | "class_heritage" => {
self.extract_inheritance(child, class_row);
}
+1 -1
View File
@@ -194,7 +194,7 @@ function_declarator → null, 12-hop guard).
| 4545 | local fn-ptr call fan-out (#932-adjacent) | bare-identifier callee found in cppLocalFnPtrs[callerId] → emit one `calls` ref PER recorded target, suppress the local name |
| 5173 | stack construction (#1035) | cpp `declaration` where `type` ∈ {type_identifier, template_type, qualified_identifier} and any init_declarator has `value` ∈ {argument_list, initializer_list} → extractInstantiation |
| 5183 | fn-ptr binding recording | inside a body: `declaration>init_declarator` (identifier declarator) or `assignment_expression` (identifier left); value must be pointer_expression whose child(0) is `&`; target ∈ {identifier, template_function, qualified_identifier}, template-stripped; per-callerId map of per-local Sets (branch reassignments accumulate) |
| 5408 | base_class_clause → extends (#1043) | per base: type_identifier / qualified_identifier / template_type, stripCppTemplateArgs'd; access-specifier keywords skipped |
| 5408 | base_class_clause → extends (#1043) | per base: type_identifier / qualified_identifier / template_type, stripCppTemplateArgs'd; access-specifier keywords skipped. The only c/cpp supertype source: the Go struct-embedding branch (`field_declaration` with no direct `field_identifier`) runs for Go only, so a pointer/array/reference/function member (`Cache* cache_;`) is never an `extends` |
| 4740 | static member refs — **cpp only** (c not in STATIC_MEMBER_LANGS, line 345) | `Foo::BAR` value reads → `references` edge; VERIFY the MEMBER_ACCESS_TYPES shapes for qualified_identifier + the call-callee skip during the port |
| — | value-reference edges | **c: YES** (VALUE_REF_LANGS line 401), **cpp: NO**. Port the value-ref machinery for C only (crib go.rs / tsjs — shadow prune, scope stack, MAX_VALUE_REF_NODES cap, CODEGRAPH_VALUE_REFS=0 kill) |
| — | fn-ref capture (#756) | function-ref.ts:376: `c: cFamilySpec()`, `cpp: cFamilySpec({ addressOfOnly: true })`; note line ~582: `&Cls::m` exemption from the bare-ids-are-free-functions rule — read cFamilySpec fully when porting |
@@ -317,10 +317,10 @@ trait_item — supertraits `trait Sub: Super + Display`). Per bound child:
Each yields an `extends` ref from the trait node at the bound's position.
Struct/enum extraction also calls extractInheritance; rust struct_item children
include `field_declaration_list` → the 5652 recursion descends, but rust
`field_declaration` always carries a `field_identifier` name so the Go
struct-embedding branch (5496) never fires. Verify with the torture fixture
anyway.
include `field_declaration_list` → the 5652 recursion descends and finds
nothing: the Go struct-embedding branch (5496) runs for Go only (it was inert
here anyway — rust `field_declaration` always carries a `field_identifier`
name), so rustlang.rs has no `field_declaration` arm.
### impl Trait for Type — extractRustImplItem (5690)
+6 -2
View File
@@ -7001,8 +7001,12 @@ export class TreeSitterExtractor {
}
// Go struct embedding: field_declaration without field_identifier
// e.g. `type DB struct { *Head; Queryable }` — no field name means embedded type
if (child.type === 'field_declaration') {
// e.g. `type DB struct { *Head; Queryable }` — no field name means embedded type.
// Go only: C, C++ and Objective-C members are field_declarations too, but
// nest their name inside the declarator (`Cache* cache_;`, `jv elems[];`,
// `Status Get(int);`), so this test read every such member's type as a
// supertype. Their supertypes come from base_class_clause / superclass.
if (this.language === 'go' && child.type === 'field_declaration') {
const hasFieldIdentifier = child.namedChildren.some((c: SyntaxNode) => c.type === 'field_identifier');
if (!hasFieldIdentifier) {
const typeId = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier');