diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0e4a0b..1a8377da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/__tests__/c-cpp-member-supertypes.test.ts b/__tests__/c-cpp-member-supertypes.test.ts new file mode 100644 index 00000000..4321ded4 --- /dev/null +++ b/__tests__/c-cpp-member-supertypes.test.ts @@ -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 class Tpl {}; } +class Derived : public Base, private ns::Tpl { + 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 +typedef struct { int width; } Size; +struct Frame { + Size *size; + Size corners[4]; + Size origin; +}; +@interface Image : NSObject { + Size *cached; +} +@end +@implementation Image +@end +`; + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const; + +/** ` ` 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 = {}; + + 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 = { + '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); +}); diff --git a/codegraph-kernel/src/ccpp/mod.rs b/codegraph-kernel/src/ccpp/mod.rs index ef546e72..d92bc331 100644 --- a/codegraph-kernel/src/ccpp/mod.rs +++ b/codegraph-kernel/src/ccpp/mod.rs @@ -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); } diff --git a/codegraph-kernel/src/rustlang.rs b/codegraph-kernel/src/rustlang.rs index 64253f04..64b8923d 100644 --- a/codegraph-kernel/src/rustlang.rs +++ b/codegraph-kernel/src/rustlang.rs @@ -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); } diff --git a/docs/design/ccpp-kernel-port-checklist.md b/docs/design/ccpp-kernel-port-checklist.md index 3fe0562b..148bad52 100644 --- a/docs/design/ccpp-kernel-port-checklist.md +++ b/docs/design/ccpp-kernel-port-checklist.md @@ -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 | diff --git a/docs/design/rust-lang-kernel-port-checklist.md b/docs/design/rust-lang-kernel-port-checklist.md index 605013a9..e086eb08 100644 --- a/docs/design/rust-lang-kernel-port-checklist.md +++ b/docs/design/rust-lang-kernel-port-checklist.md @@ -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) diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 29debc3a..9ea465ca 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -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');