From 7e35328541a6b7f23acbc8fa9bc46d509c9a5436 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sat, 31 Oct 2026 13:13:50 -0500 Subject: [PATCH] fix(c,cpp): a struct links the types its members are declared with C and C++ structs referenced nothing through their members. 1.6.2 linked many of them by accident, as false `extends` edges; PR #2394 was right to remove that label but left nothing in its place, so impact on a type missed everything that holds one. On redis `impact pause_event` went from 2 symbols to 1, and rocksdb lost 4,284 such relations, redis 697: the largest loss against 1.6.2. Extraction, wasm extractor and kernel mirrored: a class, struct or union pushes a `references` ref, on the name, for each type its members are declared with: plain, pointer, reference, array, tagged, enum, union, function pointer (return and parameters), qualified, and template arguments, so `std::vector` links `Foo`. Members mint no nodes, like Go's fields (PR #2655). Members in `#if` groups and in unnamed structs and unions are the holder's. A method's return type counts, its parameters don't: a member a macro trails (`Version* v GUARDED_BY(mu);`) parses the same way. Not read: builtins, `std::` names, the type names C defines itself, the holder's own name, `decltype` and `auto`. An alias references what it names, so a change to `redisObject` reaches what holds a `robj *`. Resolution: the C++ name lookup base classes and receivers use, widened to enums and stopping at an alias. Only type declarations are candidates, so a function or macro of that name links nothing. In C, where one type spreads over files, what the file declares or includes comes first and an alias is the class's when a file including it defines it, and a type only a source file defines is found for a header it includes. Explore: these edges take no render window and stay out of the rank walk. With them in, rocksdb's read-path query lost four named symbols to member lines of its headers. Validation against main: redis +1,968 edges, rocksdb +10,145, leveldb +440, fmt +622, jq +114, 0 lost or moved, nodes identical; gin and flask dumps byte-identical. Of 1.6.2's member sites, redis 697: 610 same target, 87 corrected; rocksdb 4,284: 4,201 same, 58 corrected, 25 rightly gone (the class itself, a template parameter). By-value members, 2,794 on rocksdb, are new coverage. Kernel parity 0 diffs on all five repos, and full-index dumps wasm vs kernel byte-identical on redis, leveldb, fmt and jq. Index cost on rocksdb, median of 3 interleaved: CPU +3.4%, peak memory +1.9%, database +2.6%, wall within noise. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/c-cpp-member-type-refs.test.ts | 803 +++++++++++++++++++ __tests__/fixtures/kernel-parity/torture.c | 47 ++ __tests__/fixtures/kernel-parity/torture.cpp | 32 + codegraph-kernel/src/ccpp/mod.rs | 195 ++++- docs/design/ccpp-kernel-port-checklist.md | 1 + src/extraction/languages/c-cpp.ts | 140 ++++ src/extraction/tree-sitter.ts | 34 +- src/mcp/tools.ts | 12 +- src/resolution/cpp-supertypes.ts | 122 ++- src/resolution/cpp-type-aliases.ts | 46 +- src/resolution/index.ts | 5 +- 12 files changed, 1418 insertions(+), 20 deletions(-) create mode 100644 __tests__/c-cpp-member-type-refs.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 96fd1d906d..80b1b1a14e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -250,6 +250,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In a project that uses Vue, an import written through an `@/` or `~/` alias, like `import { Button } from '@/components'`, now links the file it imports, as it does in any other project, so `codegraph sync` and the file watcher update the files that import a module when that module, or a barrel they import through, is added or deleted. Before, Vue support linked such an import to the first declaration or the component in that file, which sync does not follow, and an import through a `tsconfig` alias that starts like one of Nuxt's built-in modules, like `#app/where`, linked the importing file to itself. Re-index Vue projects after upgrading. - `codegraph sync` and the file watcher now move an import written without an extension, like `import { sizeOf } from './button'`, to a file added beside the one it found before when the new file comes first, such as `button.ts` next to `button.vue`. Before, the import and the names it binds stayed linked to `button.vue` until the importing file changed or the project was re-indexed. JavaScript, TypeScript, Vue, Svelte and Astro. - 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. +- In C and C++, a struct, class or union now links to the types its members are declared with, whether held by value, by pointer, in an array, through a function pointer or inside a template like `std::vector`, and a `typedef` or `using` alias links to the type it names, so impact on a type reaches every struct that holds one, like redis's `redisServer` for `pause_event`. A member's type is the one C++ name lookup finds from the class, so a builtin, a macro or a function of the same name links nothing. Re-index C and C++ projects after upgrading. - In C and C++, a function-pointer table's handler, like `.read = impl_read` in an ops struct, no longer links to a Python or Rust function that only shares its name, and such a function no longer takes the place of the C function the table names. Re-index C and C++ projects that also hold Python or Rust after upgrading. Thanks @bompus. - In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. An alias of a type written from the global scope names that global type, also inside a namespace of the same name: protocolbuffers/protobuf's `using Arena = ::upb::Arena;` in `hpb::internal::backend::upb`, so hpb's `arena_.Fuse(…)` on a `backend::Arena` now reaches `upb::Arena::Fuse` instead of the calling method itself. Re-index C++ projects after upgrading. - In C++, indexing no longer fails with "Maximum call stack size exceeded" on a call whose arguments call the same overloaded method, like darts-clone's `extras(extras(id).prev()).set_next(…)` on a class with a `const` and a non-`const` `extras(id)`. Choosing the overload read its arguments, which chose the same overload again without end, and the whole index run failed. Such a call now takes the first overload when the choice comes back to itself, as one whose argument types are unknown does. Thanks @JosefAschauer. diff --git a/__tests__/c-cpp-member-type-refs.test.ts b/__tests__/c-cpp-member-type-refs.test.ts new file mode 100644 index 0000000000..77d406567f --- /dev/null +++ b/__tests__/c-cpp-member-type-refs.test.ts @@ -0,0 +1,803 @@ +/** + * A C or C++ struct referenced nothing through its members. `struct redisServer + * { pause_event pauses[3]; struct client *current; }` had no edge to either type, + * so impact on `pause_event` missed everything that holds one. 1.6.2 linked some + * of them, by accident, as false `extends` edges (#2394 removed the label and left + * nothing in its place), and never a member held by value or named by its tag. + * + * A class, struct or union now references the types its members are declared + * with, on the name; an alias references what it names, so a change to a struct + * reaches what is declared through `typedef struct redisObject robj;`. The + * references are the holder's: members mint no nodes, and impact never climbs + * from a member to its container. A member of an unnamed struct or union is the + * holder's too, since nothing can name that type. + * + * Only a type can be named there, so resolution looks the name up as C++ name + * lookup does and considers only declarations of types: a function, a macro or + * a variable of that name is never the target, nor a namesake of another + * namespace or of a folder the file has nothing to do with. Builtins, the + * standard library and template parameters link nothing. + * + * 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 { ToolHandler } from '../src/mcp/tools'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel'; +import type { Edge, 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'; + +/** What a real C header holds. */ +const C_SOURCE = `typedef struct pause_event { int paused_actions; long end; } pause_event; +typedef struct ev ev; +struct client { int fd; }; +enum level { LOW, HIGH }; +typedef enum { RED, GREEN } color; +typedef union { int i; } value; +typedef unsigned long long mstime_t; +typedef struct redisObject robj; +typedef void (*notify_fn)(struct client *c, robj *o); +#define u32 unsigned int + +struct holder { + pause_event by_value; + pause_event *by_pointer; + pause_event by_array[3]; + pause_event **by_pointers; + const pause_event *constant; + pause_event const *also_constant; + struct client *tagged; + struct client *one, two, three[2]; + union extra *tagged_union; + value in_union; + enum level tagged_enum; + color named_enum; + void (*callback)(struct client *c, value v); + ev *(*factory)(robj **out); + int (*table[4])(mstime_t when); + mstime_t when; + robj *object; + u32 mask; + int n; + char *s; + unsigned long l; + size_t len; + uint64_t id; + void *p; + bool b; + _Bool flag; + uintmax_t widest; + int_least8_t least; + wchar_t wide; +#ifdef FEATURE + ev *guarded; +#else + ev *otherwise; +#endif + struct { ev *in_unnamed; } named_member; + union { ev *in_anonymous; int i; }; + struct inner { ev *deep; } nested; + struct fwd; + struct holder *next; + LIST_ENTRY(client) link; + ev bits : 3; +}; + +typedef struct listNode { + struct listNode *prev; + ev *value; +} listNode; + +typedef struct tagged_s { + struct tagged_s *next; + struct tagged_t *other; + listNode *head; +} tagged_t; + +typedef union { + ev *a; + struct { listNode *b; } c; +} wrapped; +`; + +/** What a real C++ header holds. */ +const CPP_SOURCE = `namespace ns { +class Foo {}; +template class Map {}; +} +class Global {}; +template +class Holder : public Global { + public: + Status Open(const Slice& key); + virtual Iterator* NewIterator(const Options& options) = 0; + const Snapshot& Snap() const; + Holder(const Holder&); + operator bool() const; + void Inline() { Hidden h; } + friend class Pal; + using Base::Base; + typedef ns::Map Table; + using Rows = std::vector; + private: + ns::Foo plain; + const ns::Foo& reference; + ns::Foo* pointer; + ::Global global; + std::string text; + std::vector items; + std::unique_ptr owner; + std::map> lookup; + ns::Map templated; + Outer::Inner* nested; + std::function done; + typename T::value_type element; + T* parameter; + Holder* next; + decltype(x) computed; + auto deduced = 1; + static Registry* instance; + mutable Mutex mu; + Version* const version GUARDED_BY(mu); + bool (*predicate)(const Slice* key); + enum class Mode : int { A, B } mode; + Mode other; + class Fwd; + class Rep; + Rep* rep; + struct Node { Payload* payload; }; + union { Left left; Right right; }; +}; +`; + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const; + +/** ` ` for every `references` ref, in the order written. */ +function typeRefs(result: ExtractionResult): string[] { + const byId = new Map(result.nodes.map((n) => [n.id, n])); + return result.unresolvedReferences + .filter((r) => r.referenceKind === 'references') + .map((r) => `${byId.get(r.fromNodeId)?.name ?? r.fromNodeId} ${r.referenceName}`) + .sort(); +} + +describe('a C or C++ type references the types it is declared with', () => { + let savedEnv: Record = {}; + + beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['c', 'cpp']); + }); + + 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)(`every shape of a C member: %s${label}`, (backend) => { + const result = extract(backend, 'server.h', eol(C_SOURCE), 'c'); + expect(typeRefs(result)).toEqual([ + // A member of an unnamed struct or union is its holder's. + 'holder ev', 'holder ev', 'holder ev', 'holder ev', 'holder ev', 'holder ev', + 'holder client', 'holder client', 'holder client', + 'holder color', + 'holder extra', + 'holder inner', + 'holder level', + 'holder mstime_t', 'holder mstime_t', + 'holder pause_event', 'holder pause_event', 'holder pause_event', 'holder pause_event', 'holder pause_event', 'holder pause_event', + 'holder robj', 'holder robj', + // What a macro names is read like any type: `u32` links only if a type is named so. + 'holder u32', + 'holder value', 'holder value', + 'inner ev', + 'listNode ev', + '(*notify_fn) client', + '(*notify_fn) robj', + 'robj redisObject', + 'tagged_t listNode', + 'wrapped ev', + 'wrapped listNode', + ].sort()); + }); + + it.each(backends)(`every shape of a C++ member: %s${label}`, (backend) => { + const result = extract(backend, 'holder.h', eol(CPP_SOURCE), 'cpp'); + // Not read: `std::` names, though their template arguments are; a + // method's parameters; a computed or deduced type; the class's own name; + // a declaration that declares no member, like `class Fwd;`. + expect(typeRefs(result)).toEqual([ + 'Holder ::Global', + 'Holder Callback', + 'Holder Foo', 'Holder Foo', + 'Holder Iterator', + 'Holder Key', + 'Holder Left', + 'Holder Mode', + 'Holder Mode', + 'Holder Mutex', + 'Holder Outer::Inner', 'Holder A', 'Holder B', + 'Holder Registry', + 'Holder Rep', + 'Holder Reply', + 'Holder Right', + 'Holder Slice', + 'Holder Snapshot', + 'Holder Status', + 'Holder T', 'Holder T', + 'Holder T::value_type', + 'Holder Value', + 'Holder Version', + 'Holder ns::Foo', 'Holder ns::Foo', 'Holder ns::Foo', 'Holder ns::Foo', + 'Holder ns::Map', + 'Node Payload', + 'Rows ns::Foo', + 'Table Foo', + 'Table ns::Map', + ].sort()); + }); + + it.each(backends)(`a reference is written on the name it reads: %s${label}`, (backend) => { + const source = eol('struct a {\n const struct b *x;\n std::vector y;\n};\n'); + const refs = extract(backend, 'pos.cc', source, 'cpp').unresolvedReferences.filter((r) => r.referenceKind === 'references'); + expect(refs.map((r) => `${r.referenceName}@${r.line}:${r.column}`)).toEqual(['b@2:16', 'ns::c@3:14']); + }); + } + + it.each(backends)('members mint no nodes: %s', (backend) => { + const result = extract(backend, 'server.h', C_SOURCE, 'c'); + expect(result.nodes.map((n) => n.name)).not.toEqual(expect.arrayContaining(['by_value'])); + expect(result.nodes.some((n) => n.kind === 'field' || n.kind === 'property')).toBe(false); + }); +}); + +describe('the types of C and C++ members in an indexed project', () => { + let root = ''; + let cg: CodeGraph | undefined; + + afterEach(() => { + cg?.destroy(); + cg = undefined; + if (root) fs.rmSync(root, { recursive: true, force: true }); + root = ''; + }); + + async function index(files: Record): Promise { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-member-types-')); + write(files); + cg = await CodeGraph.init(root, { index: true }); + return cg; + } + + function write(files: Record): void { + 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); + } + } + + const named = (name: string, kind?: string, file?: string): Node => { + const found = cg!.getNodesByName(name).filter((n) => + n.kind !== 'file' && n.kind !== 'import' && (!kind || n.kind === kind) && (!file || n.filePath === file)); + expect(found, `${kind ?? ''} ${name} ${file ?? ''}`).toHaveLength(1); + return found[0]!; + }; + + /** ` @` of what a holder references, sorted and deduplicated. */ + const referenced = (holder: Node): string[] => [...new Set( + cg!.getOutgoingEdges(holder.id) + .filter((e: Edge) => e.kind === 'references') + .map((e: Edge) => { + const target = cg!.getNode(e.target)!; + return `${target.kind} ${target.qualifiedName} @${target.filePath}`; + }) + )].sort(); + + /** What impact on a declaration reaches, by name, without the declaration. */ + const impactOf = (node: Node, depth = 2): string[] => [...new Set( + [...cg!.getImpactRadius(node.id, depth).nodes.values()].filter((n) => n.id !== node.id).map((n) => n.name) + )].sort(); + + it('the struct that holds a member is back in impact on its type', async () => { + await index({ + 'server.h': [ + 'typedef struct pause_event {', + ' int paused_actions;', + ' long end;', + '} pause_event;', + '', + 'struct client {', + ' int fd;', + '};', + '', + 'struct redisServer {', + ' pause_event client_pause_per_purpose[3];', + ' struct client *current_client;', + ' int port;', + '};', + '', + ].join('\n'), + 'server.c': [ + '#include "server.h"', + 'struct redisServer server;', + 'int isPaused(struct redisServer *s) {', + ' return s->client_pause_per_purpose[0].paused_actions;', + '}', + '', + ].join('\n'), + }); + expect(impactOf(named('pause_event'))).toEqual(['redisServer']); + expect(impactOf(named('client'))).toEqual(['redisServer']); + expect(referenced(named('redisServer'))).toEqual(['struct client @server.h', 'struct pause_event @server.h']); + // The relation is what the code says; no false supertype came back. + const holder = named('redisServer'); + expect(cg!.getOutgoingEdges(holder.id).filter((e: Edge) => e.kind === 'extends')).toEqual([]); + }); + + it('a type held by value, by pointer, in an array or through a function pointer', async () => { + await index({ + 'a.h': [ + 'typedef struct ev { int x; } ev;', + 'struct holder {', + ' ev by_value;', + ' ev *by_pointer;', + ' ev by_array[3];', + ' ev (*by_fn)(int);', + ' ev *(*by_parameter)(ev e);', + '};', + '', + ].join('\n'), + }); + const edges = cg!.getOutgoingEdges(named('holder').id).filter((e: Edge) => e.kind === 'references'); + expect(edges.map((e: Edge) => e.line)).toEqual([3, 4, 5, 6, 7, 7]); + expect(impactOf(named('ev'))).toEqual(['holder']); + }); + + it('impact goes on from the holder to what uses it', async () => { + await index({ + 'a.h': 'typedef struct ev { int x; } ev;\nstruct holder { ev *e; };\nstruct owner { struct holder h; };\n', + 'a.c': '#include "a.h"\nint use(struct owner *o) { return sizeof(*o); }\nint touch(struct holder *h) { return h->e->x; }\nvoid fresh(void) { struct holder h; (void)h; touch(&h); }\n', + }); + expect(impactOf(named('ev'), 2)).toEqual(['holder', 'owner']); + }); + + it('a type declared through a typedef reaches what it names', async () => { + await index({ + 'server.h': [ + 'typedef struct redisObject robj;', + 'typedef struct dict dict;', + 'struct redisObject { int type; };', + 'struct dict { int size; };', + 'struct db { robj *key; dict *keys; struct dict *tagged; };', + '', + ].join('\n'), + }); + expect(referenced(named('db'))).toEqual([ + 'struct dict @server.h', + 'type_alias robj @server.h', + ]); + // `dict` is the struct itself, and the alias of it stays out of the way. + expect(impactOf(named('dict', 'struct'))).toEqual(['db']); + expect(referenced(named('robj'))).toEqual(['struct redisObject @server.h']); + expect(impactOf(named('redisObject'))).toEqual(['db', 'robj']); + }); + + it('a builtin, or a macro expanding to one, links nothing', async () => { + await index({ + 'a.h': [ + '#define u32 unsigned int', + '#define counter_t long', + 'struct holder {', + ' u32 mask;', + ' counter_t hits;', + ' int n;', + ' size_t len;', + ' uint64_t id;', + ' wchar_t c;', + ' FILE *out;', + ' time_t at;', + '};', + '', + ].join('\n'), + 'b.c': '#include "a.h"\nint size_t_of(void) { return 0; }\n', + }); + expect(referenced(named('holder'))).toEqual([]); + }); + + it('a compiler shim of a standard type is no type of the project', async () => { + await index({ + 'deps/compat/C99/stdbool.h': 'typedef int _Bool;\ntypedef unsigned long long uintmax_t;\n', + 'src/a.h': 'struct holder { _Bool flag; uintmax_t widest; };\n', + }); + expect(referenced(named('holder'))).toEqual([]); + }); + + it('only a type is a member’s type: no function, variable or macro of that name', async () => { + await index({ + 'a.h': [ + 'struct holder {', + ' log *to;', + ' struct stat st;', + ' counter hits;', + ' limit cap;', + '};', + '', + ].join('\n'), + 'a.c': [ + '#include "a.h"', + '#define limit 10', + 'int stat(const char *path) { return 0; }', + 'void log(const char *m) {}', + 'int counter = 3;', + '', + ].join('\n'), + }); + expect(referenced(named('holder'))).toEqual([]); + }); + + it('a type its header declares and its source file defines is that source file’s', async () => { + await index({ + // redis: a vendored copy declares the name in a header every file can see. + 'deps/hiredis/dict.h': 'typedef struct dictEntry { void *key; } dictEntry;\nstruct vendored { dictEntry *head; };\n', + 'src/dict.h': 'typedef struct dictEntry dictEntry;\nstruct dict { dictEntry **table; };\n', + 'src/dict.c': '#include "dict.h"\nstruct dictEntry { void *key; int v; };\nstruct walker { dictEntry *at; };\n', + 'src/kvstore.c': '#include "dict.h"\nstruct kvstore { dictEntry *rehashing; };\n', + }); + const mine = 'struct dictEntry @src/dict.c'; + expect(referenced(named('dict', 'struct'))).toEqual([mine]); + expect(referenced(named('walker'))).toEqual([mine]); + expect(referenced(named('kvstore'))).toEqual([mine]); + expect(referenced(named('vendored'))).toEqual(['struct dictEntry @deps/hiredis/dict.h']); + }); + + it('a struct only another source file defines, that no header declares, is not seen', async () => { + await index({ + 'a.c': 'struct state { int a; };\nint f(void) { return 0; }\n', + 'b.c': 'struct holder { struct state *s; };\n', + }); + expect(referenced(named('holder'))).toEqual([]); + }); + + it('a nested C struct is found by its own name, from outside the struct that defines it', async () => { + await index({ + 'a.h': 'struct outer { struct inner { int x; } i; };\nstruct other { struct inner *p; };\n', + }); + expect(referenced(named('outer'))).toEqual(['struct outer::inner @a.h']); + expect(referenced(named('other'))).toEqual(['struct outer::inner @a.h']); + }); + + it('a member of an unnamed struct or union belongs to the struct that holds it', async () => { + await index({ + 'a.h': [ + 'typedef struct { int x; } ev;', + 'struct holder {', + ' union { ev *in_anonymous; int i; };', + ' struct { ev *in_unnamed; } named_member;', + '};', + '', + ].join('\n'), + }); + expect(referenced(named('holder'))).toEqual(['struct ev @a.h']); + // (Impact on a struct walks what it contains, so the unnamed union is listed with it.) + expect(impactOf(named('ev'))).toContain('holder'); + }); + + it('a C++ member names what name lookup finds, not a namesake', async () => { + await index({ + 'include/types.h': [ + 'namespace other { class Foo {}; class Slice {}; }', + 'namespace ns {', + 'class Foo {};', + 'class Slice {};', + 'template class Map {};', + 'enum class Color { Red };', + '}', + 'class Global {};', + '', + ].join('\n'), + 'src/holder.h': [ + '#include "types.h"', + 'namespace ns {', + 'class Outer {', + ' struct Nested {};', + ' enum Mode { A, B };', + ' Foo bare;', + ' ns::Foo qualified;', + ' other::Foo elsewhere;', + ' ::Global global;', + ' Nested nested;', + ' Mode mode;', + ' Color color;', + ' const Slice& reference;', + ' Map map;', + ' std::vector items;', + ' std::unique_ptr owner;', + '};', + '}', + '', + ].join('\n'), + }); + expect(referenced(named('Outer'))).toEqual([ + 'class Global @include/types.h', + 'class ns::Foo @include/types.h', + 'class ns::Map @include/types.h', + 'class ns::Slice @include/types.h', + 'class other::Foo @include/types.h', + 'class other::Slice @include/types.h', + 'enum ns::Color @include/types.h', + 'enum ns::Outer::Mode @src/holder.h', + 'struct ns::Outer::Nested @src/holder.h', + ]); + }); + + it('a using-declaration or -directive, and a namespace alias, say where a name comes from', async () => { + await index({ + 'include/types.h': [ + 'namespace db { class Options {}; class Slice {}; class string {}; }', + 'namespace internal { class Slice {}; }', + '', + ].join('\n'), + 'src/a.h': [ + '#include "types.h"', + 'using db::Slice;', + 'namespace d = db;', + 'class A { Slice slice; d::Options options; };', + '', + ].join('\n'), + 'src/b.h': [ + '#include "types.h"', + 'using namespace db;', + 'class B { Options options; };', + '', + ].join('\n'), + 'src/c.h': [ + '#include "types.h"', + '#include ', + 'using std::string;', + 'class C { string text; };', + '', + ].join('\n'), + }); + expect(referenced(named('A'))).toEqual(['class db::Options @include/types.h', 'class db::Slice @include/types.h']); + expect(referenced(named('B'))).toEqual(['class db::Options @include/types.h']); + expect(referenced(named('C'))).toEqual([]); + }); + + it('a name no lookup finds is not a namesake in a scope the class is not in', async () => { + await index({ + 'include/port.h': [ + 'namespace db { namespace port { using pthread_key_t = int; class Latch {}; } }', + 'class Outer { public: using Hash = int; };', + '', + ].join('\n'), + 'src/a.h': [ + '#include "port.h"', + 'namespace db {', + 'class A { pthread_key_t key; Hash hash; port::Latch latch; };', + '}', + '', + ].join('\n'), + }); + expect(referenced(named('A'))).toEqual(['class db::port::Latch @include/port.h']); + }); + + it('an alias template is found, though the reference carries no template arguments', async () => { + await index({ + 'include/containers.h': [ + 'namespace db {', + 'template using UnorderedMap = Other;', + 'class Key {};', + 'class Holder { UnorderedMap map; };', + '}', + '', + ].join('\n'), + }); + expect(referenced(named('Holder'))).toEqual([ + 'class db::Key @include/containers.h', + 'type_alias db::UnorderedMap @include/containers.h', + ]); + }); + + it('of several declarations of a name in the file, the nearest above is the one in force', async () => { + await index({ + 'src/a.h': [ + '#ifdef WIDE', + 'typedef long width_t;', + 'struct first { width_t w; };', + '#else', + 'typedef int width_t;', + 'struct second { width_t w; };', + '#endif', + '', + ].join('\n'), + }); + const line = (holder: string): number[] => cg!.getOutgoingEdges(named(holder).id) + .filter((e: Edge) => e.kind === 'references') + .map((e: Edge) => cg!.getNode(e.target)!.startLine); + expect(line('first')).toEqual([2]); + expect(line('second')).toEqual([5]); + }); + + it('a class local to a gtest body is seen only from that body', async () => { + await index({ + 'src/a_test.cc': [ + 'namespace db {', + 'TEST_F(Suite, First) {', + ' struct Item { int a; };', + ' struct Box { Item item; };', + '}', + 'TEST_F(Suite, Second) {', + ' struct Item { int b; };', + ' struct Crate { Item item; };', + '}', + '}', + '', + ].join('\n'), + }); + const line = (holder: string): number[] => cg!.getOutgoingEdges(named(holder).id) + .filter((e: Edge) => e.kind === 'references') + .map((e: Edge) => cg!.getNode(e.target)!.startLine); + expect(line('Box')).toEqual([3]); + expect(line('Crate')).toEqual([7]); + }); + + it('a template parameter, or a member of one, names no particular type', async () => { + await index({ + 'include/t.h': [ + 'class T {};', + 'class Element {};', + 'class value_type {};', + 'template ', + 'class Box {', + ' T item;', + ' Element* pointer;', + ' typename T::value_type inner;', + ' Element other[2];', + '};', + 'template class Tree {', + ' class Node { T value; };', + ' Node* root;', + '};', + '', + ].join('\n'), + }); + expect(referenced(named('Box'))).toEqual([]); + expect(referenced(named('Node'))).toEqual([]); + expect(referenced(named('Tree'))).toEqual(['class Tree::Node @include/t.h']); + }); + + it('a pimpl’s nested type, which only the source file defines', async () => { + await index({ + 'include/table.h': [ + 'namespace db {', + 'class Table {', + ' private:', + ' struct Rep;', + ' Rep* rep_;', + '};', + '}', + '', + ].join('\n'), + 'src/table.cc': [ + '#include "table.h"', + 'namespace db {', + 'struct Table::Rep { int n; };', + '}', + '', + ].join('\n'), + 'src/other.cc': [ + '#include "table.h"', + 'namespace other {', + 'struct Rep { int m; };', + '}', + '', + ].join('\n'), + }); + // other.cc includes the header too, but its `Rep` is in no scope the class can name. + expect(referenced(named('Table'))).toEqual(['struct db::Table::Rep @src/table.cc']); + }); + + it('a struct naming itself has no edge to itself and no unresolved row for its name', async () => { + await index({ + 'a.h': [ + 'typedef struct node { struct node *next; node_t *other; } node_t;', + 'typedef struct listNode { struct listNode *prev; } listNode;', + 'struct plain { struct plain *next; };', + '', + ].join('\n'), + }); + for (const name of ['node_t', 'listNode', 'plain']) expect(referenced(named(name))).toEqual([]); + }); + + it('explore spends no window on a member: a class’s types are not its flow', async () => { + const members = Array.from({ length: 600 }, (_, i) => ` Type${i} member_with_a_long_name_${i};`).join('\n'); + const types = Array.from({ length: 600 }, (_, i) => `class Type${i} {};`).join('\n'); + await index({ + 'src/types.h': `${types}\n`, + 'src/reader.h': [ + '#include "types.h"', + 'class Reader {', + ' public:', + ' int Get(int key);', + ' int Size() const { return count_; }', + ' private:', + ' int count_;', + members, + '};', + '', + ].join('\n'), + 'src/reader.cc': [ + '#include "reader.h"', + 'static int Lookup(int key) { return key + 1; }', + 'int Reader::Get(int key) {', + ' return Lookup(key) + Size();', + '}', + '', + ].join('\n'), + }); + expect(referenced(named('Reader'))).toHaveLength(600); + const result = await new ToolHandler(cg!).execute('codegraph_explore', { query: 'Reader::Get Size Lookup' }); + const text = result.content[0]!.text; + expect(text).toContain('return Lookup(key) + Size();'); + expect(text).toContain('int Size() const'); + expect(text).not.toContain('(references)'); + expect(text).not.toContain('member_with_a_long_name_50'); + }); + + it('a member’s type links once its file is added, and survives an edit of that file', async () => { + await index({ + 'src/holder.h': '#include "ev.h"\nstruct holder { ev *e; };\n', + }); + expect(referenced(named('holder'))).toEqual([]); + + write({ 'src/ev.h': 'typedef struct ev { int x; } ev;\n' }); + await cg!.sync(); + expect(referenced(named('holder'))).toEqual(['struct ev @src/ev.h']); + + write({ 'src/ev.h': '/* moved */\n\ntypedef struct ev { int x; int y; } ev;\n' }); + await cg!.sync(); + expect(referenced(named('holder'))).toEqual(['struct ev @src/ev.h']); + expect(impactOf(named('ev'))).toEqual(['holder']); + }); +}); diff --git a/__tests__/fixtures/kernel-parity/torture.c b/__tests__/fixtures/kernel-parity/torture.c index a785980512..7f808d333d 100644 --- a/__tests__/fixtures/kernel-parity/torture.c +++ b/__tests__/fixtures/kernel-parity/torture.c @@ -188,3 +188,50 @@ static int declared_types_local(void) { static struct { int hits; } local_stats; return ++local_stats.hits + current_mode; } + +/* members: the types each is declared with are references from its holder, or + from the alias they are named through */ +typedef struct member_s member_t; +typedef enum { PAUSE_OFF, PAUSE_ON } pause_mode; +typedef union { int i; float f; } member_value; +typedef void (*member_cb)(member_t *m, word_t w); +typedef word_t *word_ptr; + +struct member_s { + member_t *next; + struct member_s *prev; + word_t by_value; + word_t by_array[4]; + const word_t *by_pointer; + word_t **by_pointers; + struct parse_state *tagged, second, third[2]; + union packet_hdr header; + enum hdr_kind kind; + pause_mode mode; + member_value (*make)(member_t *from, const struct parse_state *state); + void *(*table[2])(word_t w); + unsigned long mask; + int bits : 3; +#ifdef MEMBER_EXTRA + word_t extra; +#elif defined(MEMBER_OTHER) + member_value other; +#else + pause_mode fallback; +#endif + union { word_t in_anonymous; int raw; }; + struct { member_value in_unnamed; } named_member; + struct member_inner { word_t deep; } nested; + enum { MEMBER_ON, MEMBER_OFF } state; + enum member_level { MEMBER_LOW, MEMBER_HIGH } level; +}; + +typedef struct self_list { + struct self_list *next; + word_t value; +} self_list; + +typedef union { + word_t a; + struct { self_list *b; } c; +} wrapped_member; diff --git a/__tests__/fixtures/kernel-parity/torture.cpp b/__tests__/fixtures/kernel-parity/torture.cpp index d4041dc23c..b33843ac6b 100644 --- a/__tests__/fixtures/kernel-parity/torture.cpp +++ b/__tests__/fixtures/kernel-parity/torture.cpp @@ -171,3 +171,35 @@ T Twice(T v) requires std::integral { return v * 2; } } // namespace clauses +// Members: the types each is declared with are references from its holder, or +// from the alias they are named through. +namespace members { +class Slice {}; +template class Map {}; +using Rows = std::vector; +typedef Map SliceMap; +template +class Holder { + public: + Slice Get(const Slice& key); + virtual Rows* Scan(const Slice& from) = 0; + const Slice& Peek() const; + private: + Slice by_value; + const Slice& by_reference; + members::Slice* qualified; + ::members::Slice* global; + std::vector items; + std::unique_ptr> owner; + Outer::Inner* nested; + typename T::value_type element; + std::function done; + void (*callback)(const Slice* key, Rows rows); + decltype(1) computed; + Holder* next; + enum class Mode : int { A, B } mode; + class Fwd; + union { Slice left; int right; }; + struct Node { SliceMap* map; }; +}; +} // namespace members diff --git a/codegraph-kernel/src/ccpp/mod.rs b/codegraph-kernel/src/ccpp/mod.rs index 15bfb4127e..699f34ede6 100644 --- a/codegraph-kernel/src/ccpp/mod.rs +++ b/codegraph-kernel/src/ccpp/mod.rs @@ -44,6 +44,10 @@ //! (`static struct { … } SPT;` → `SPT`, in function bodies too), and the //! comment above the declaration is its docstring (c/cpp //! getDeclarationWrapper). +//! - the types a class's members are declared with (cMemberTypeNames) and an +//! alias's (cAliasTypeNames) are `references` from the class or alias, +//! written on the name: fields mint no nodes. An unnamed member type's +//! members are its holder's. //! - 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 @@ -93,6 +97,14 @@ const MAX_VALUE_REF_NODES: usize = 20_000; // --- compiled regexes (JS \w/\s spelled as ASCII classes for parity) --------- +/// C_STANDARD_TYPE_NAME (languages/c-cpp.ts). +fn c_standard_type_name_re() -> &'static Regex { + static RE: OnceLock = OnceLock::new(); + RE.get_or_init(|| { + Regex::new(r"^(?:(?:::\s*)?std\s*::|(?:_Bool|_Complex|wchar_t|char(?:8|16|32)_t|u?intmax_t|u?int_(?:least|fast)\d+_t|max_align_t|nullptr_t)$)") + .unwrap() + }) +} /// recoverCppMacroDefinedName: macro-shaped parsed name. fn macro_shaped_re() -> &'static Regex { static RE: OnceLock = OnceLock::new(); @@ -1386,6 +1398,7 @@ impl<'t> Walker<'t> { let scope = Scope { row, kind: "class", name, qualified: scoped }; self.open_class_scope(&scope, body); self.extract_inheritance(node, row); + self.push_member_type_refs(node, row, &scope.name); self.stack.push(scope); for i in 0..body.named_child_count() { if let Some(c) = body.named_child(i) { @@ -1412,6 +1425,7 @@ impl<'t> Walker<'t> { let scope = Scope { row, kind, name, qualified: scoped }; self.open_class_scope(&scope, body); self.extract_inheritance(node, row); + self.push_member_type_refs(node, row, &scope.name); self.stack.push(scope); for i in 0..body.named_child_count() { if let Some(c) = body.named_child(i) { @@ -1493,13 +1507,16 @@ impl<'t> Walker<'t> { ) else { return true; }; - self.stack.push(Scope { row, kind, name, qualified: None }); let type_child = node .child_by_field_name("type") .or_else(|| self.find_child_by_kind(node, "struct_specifier")) .or_else(|| self.find_child_by_kind(node, "union_specifier")); if let Some(tc) = type_child { self.extract_inheritance(tc, row); + self.push_member_type_refs(tc, row, &name); + } + self.stack.push(Scope { row, kind, name, qualified: None }); + if let Some(tc) = type_child { let body = tc.child_by_field_name("body").unwrap_or(tc); for i in 0..body.named_child_count() { if let Some(c) = body.named_child(i) { @@ -1538,7 +1555,10 @@ impl<'t> Walker<'t> { return true; } - self.create_node("type_alias", &name, node, Extra { docstring, ..Extra::default() }); + if let Some(row) = self.create_node("type_alias", &name, node, Extra { docstring, ..Extra::default() }) { + let names = self.alias_type_names(node, &name); + self.push_type_refs(names, row); + } false } @@ -2157,6 +2177,142 @@ impl<'t> Walker<'t> { } } + // --- the types members and aliases are declared with -------------------- + + /// extractCMemberTypeRefs (tree-sitter.ts): a class, struct or union + /// references the types its members are declared with, as the class: + /// members mint no nodes, and impact never climbs from a member to its + /// container. + fn push_member_type_refs(&mut self, specifier: Node<'t>, row: u32, own_name: &str) { + if is_unnamed_member_type(specifier) { + return; + } + let names = self.member_type_names(specifier, own_name); + self.push_type_refs(names, row); + } + + fn push_type_refs(&mut self, names: Vec<(String, Node<'t>)>, row: u32) { + let references = edge_kind_index("references").unwrap(); + for (name, node) in names { + self.push_ref_at(row, &name, references, node); + } + } + + /// cMemberTypeNames (languages/c-cpp.ts): the type names a class, struct or + /// union's members are declared with, in source order, each with the node + /// it is read at. + fn member_type_names(&self, specifier: Node<'t>, own_name: &str) -> Vec<(String, Node<'t>)> { + let mut names = Vec::new(); + let mut own = HashSet::from([own_name.to_string()]); + if let Some(tag) = specifier.child_by_field_name("name") { + own.insert(strip_cpp_template_args(self.text(tag))); + } + if let Some(body) = specifier.child_by_field_name("body") { + self.collect_member_type_names(body, &own, &mut names); + } + names + } + + /// cAliasTypeNames (languages/c-cpp.ts): the type names an alias is + /// declared with. + fn alias_type_names(&self, alias: Node<'t>, own_name: &str) -> Vec<(String, Node<'t>)> { + let mut names = Vec::new(); + let own = HashSet::from([own_name.to_string()]); + if let Some(ty) = alias.child_by_field_name("type") { + self.collect_type_names(ty, &own, &mut names); + } + let mut cursor = alias.walk(); + for declarator in alias.children_by_field_name("declarator", &mut cursor) { + self.collect_declarator_type_names(declarator, &own, &mut names); + } + names + } + + fn collect_member_type_names(&self, list: Node<'t>, own: &HashSet, names: &mut Vec<(String, Node<'t>)>) { + stack_guard!(); + for child in (0..list.named_child_count()).filter_map(|i| list.named_child(i)) { + if is_member_conditional(child.kind()) { + self.collect_member_type_names(child, own, names); + continue; + } + if child.kind() != "field_declaration" { + continue; + } + let Some(ty) = child.child_by_field_name("type") else { continue }; + let mut cursor = child.walk(); + let declarators: Vec> = child.children_by_field_name("declarator", &mut cursor).collect(); + let unnamed_body = if is_class_like_specifier(ty.kind()) && ty.child_by_field_name("name").is_none() { + ty.child_by_field_name("body") + } else { + None + }; + if let Some(body) = unnamed_body { + self.collect_member_type_names(body, own, names); + } else if !declarators.is_empty() { + self.collect_type_names(ty, own, names); + } + for declarator in declarators { + if !declares_member_function(declarator) { + self.collect_declarator_type_names(declarator, own, names); + } + } + } + } + + /// collectDeclaratorTypeNames (languages/c-cpp.ts): those of a function + /// pointer's parameters. + fn collect_declarator_type_names(&self, declarator: Node<'t>, own: &HashSet, names: &mut Vec<(String, Node<'t>)>) { + stack_guard!(); + if declarator.kind() == "function_declarator" { + if let Some(parameters) = declarator.child_by_field_name("parameters") { + self.collect_type_names(parameters, own, names); + } + } + if !declarator.kind().ends_with("declarator") { + return; + } + if let Some(inner) = inner_declarator(declarator) { + self.collect_declarator_type_names(inner, own, names); + } + } + + /// collectCTypeNames (languages/c-cpp.ts). + fn collect_type_names(&self, node: Node<'t>, own: &HashSet, names: &mut Vec<(String, Node<'t>)>) { + stack_guard!(); + if matches!(node.kind(), "decltype" | "placeholder_type_specifier") { + return; + } + if matches!(node.kind(), "type_identifier" | "qualified_identifier" | "template_type") { + let name = strip_cpp_template_args(self.text(node)).trim().to_string(); + if !name.is_empty() && !own.contains(&name) && !c_standard_type_name_re().is_match(&name) { + names.push((name, node)); + } + self.collect_template_arguments(node, own, names); + return; + } + if is_class_like_specifier(node.kind()) { + if let Some(tag) = node.child_by_field_name("name") { + self.collect_type_names(tag, own, names); + return; + } + } + for c in (0..node.named_child_count()).filter_map(|i| node.named_child(i)) { + self.collect_type_names(c, own, names); + } + } + + /// collectTemplateArguments (languages/c-cpp.ts). + fn collect_template_arguments(&self, node: Node<'t>, own: &HashSet, names: &mut Vec<(String, Node<'t>)>) { + stack_guard!(); + for c in (0..node.named_child_count()).filter_map(|i| node.named_child(i)) { + match c.kind() { + "template_argument_list" => self.collect_type_names(c, own, names), + "qualified_identifier" | "template_type" => self.collect_template_arguments(c, own, names), + _ => {} + } + } + } + // --- fn-ref capture (#756, cFamilySpec) ---------------------------------- /// maybeCaptureFnRefs + captureFnRefCandidates for the cFamily dispatch: @@ -2506,12 +2662,43 @@ fn c_declarator_identifier(node: Node) -> Option { None } +/// CLASS_LIKE_SPECIFIERS (languages/c-cpp.ts). +fn is_class_like_specifier(kind: &str) -> bool { + matches!(kind, "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier") +} + /// isClassLikeDefinition (languages/c-cpp.ts): a class/struct/union/enum /// specifier that carries a body defines the type; `struct Foo *p` and the /// forward declaration `class Foo;` only name it. fn is_class_like_definition(node: Node) -> bool { - matches!(node.kind(), "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier") - && node.child_by_field_name("body").is_some() + is_class_like_specifier(node.kind()) && node.child_by_field_name("body").is_some() +} + +/// isUnnamedMemberType (languages/c-cpp.ts): its members are its holder's. +fn is_unnamed_member_type(specifier: Node) -> bool { + specifier.child_by_field_name("name").is_none() + && specifier.parent().map(|p| p.kind() == "field_declaration").unwrap_or(false) +} + +/// MEMBER_CONDITIONALS (languages/c-cpp.ts). +fn is_member_conditional(kind: &str) -> bool { + matches!(kind, "preproc_if" | "preproc_ifdef" | "preproc_else" | "preproc_elif" | "preproc_elifdef") +} + +/// innerDeclarator (languages/c-cpp.ts). +fn inner_declarator(declarator: Node) -> Option { + declarator.child_by_field_name("declarator").or_else(|| declarator.named_child(0)) +} + +/// declaresMemberFunction (languages/c-cpp.ts). +fn declares_member_function(declarator: Node) -> bool { + let mut d = Some(declarator); + while let Some(n) = d.filter(|n| matches!(n.kind(), "pointer_declarator" | "reference_declarator")) { + d = inner_declarator(n); + } + d.filter(|n| n.kind() == "function_declarator") + .map(|n| n.child_by_field_name("declarator").map(|i| i.kind()) != Some("parenthesized_declarator")) + .unwrap_or(false) } /// definingDeclaration (languages/c-cpp.ts): the `declaration` a class-like diff --git a/docs/design/ccpp-kernel-port-checklist.md b/docs/design/ccpp-kernel-port-checklist.md index 79201b130a..e296290046 100644 --- a/docs/design/ccpp-kernel-port-checklist.md +++ b/docs/design/ccpp-kernel-port-checklist.md @@ -197,6 +197,7 @@ function_declarator → null, 12-hop guard). | 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. 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` | +| — | types of members and aliases | a class, struct or union pushes one `references` ref per type name its `field_declaration`s write, from the class, on the name, with `#if` groups and the members of an unnamed member type read as its own (`cMemberTypeNames` ↔ `member_type_names`; the unnamed type pushes none, `isUnnamedMemberType`). A `field_declaration` with no declarator names nothing; a member function's return type counts, its parameters don't; a function pointer's parameters do. A `type_alias` does the same for its own `type` (`cAliasTypeNames` ↔ `alias_type_names`). Skipped: the holder's own name, `std::` names (not their template arguments), the type names C itself defines (`C_STANDARD_TYPE_NAME`), `decltype` and `auto`. A nested specifier is read by its tag, an unnamed one through its body | | 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/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index 7aeca660c7..cc7759de4f 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -46,6 +46,146 @@ export function isConstDeclaration(node: SyntaxNode): boolean { /** C and C++ specifiers that define a class-like type, or an enum, when they carry a body (C has no `class_specifier`). */ const CLASS_LIKE_SPECIFIERS = new Set(['class_specifier', 'struct_specifier', 'union_specifier', 'enum_specifier']); +/** Conditional compilation a class body can hold members in. */ +const MEMBER_CONDITIONALS = new Set(['preproc_if', 'preproc_ifdef', 'preproc_else', 'preproc_elif', 'preproc_elifdef']); + +/** A type the program computes (`decltype(x)`, `auto`), which names no declaration. */ +const COMPUTED_TYPE_SPECIFIERS = new Set(['decltype', 'placeholder_type_specifier']); + +/** + * A name of the C++ standard library, or a type C itself gives a name the + * grammars read like any other. A project's copy of one is a compiler shim + * (jemalloc's msvc_compat/C99/stdbool.h), which code elsewhere never means. + */ +const C_STANDARD_TYPE_NAME = /^(?:(?:::\s*)?std\s*::|(?:_Bool|_Complex|wchar_t|char(?:8|16|32)_t|u?intmax_t|u?int_(?:least|fast)\d+_t|max_align_t|nullptr_t)$)/; + +/** What a declarator's next part is: a field where the grammar gives one, the first named child where it doesn't. */ +function innerDeclarator(declarator: SyntaxNode): SyntaxNode | null { + return getChildByField(declarator, 'declarator') ?? declarator.namedChild(0); +} + +/** + * Whether a member declarator declares a member function (`Status Get(int);`, + * `Foo* Make();`, `Slice operator[](int);`), rather than data, which may be a + * pointer to a function (`void (*cb)(int);`), whose function declarator wraps + * a parenthesized one. + */ +function declaresMemberFunction(declarator: SyntaxNode): boolean { + let d: SyntaxNode | null = declarator; + while (d && (d.type === 'pointer_declarator' || d.type === 'reference_declarator')) d = innerDeclarator(d); + return d?.type === 'function_declarator' && getChildByField(d, 'declarator')?.type !== 'parenthesized_declarator'; +} + +/** Is this class-like specifier an unnamed member of another (`struct S { union { … } u; };`)? */ +export function isUnnamedMemberType(specifier: SyntaxNode): boolean { + return !getChildByField(specifier, 'name') && specifier.parent?.type === 'field_declaration'; +} + +/** A type name a declaration writes: the name as references carry it, and the node it is read at. */ +export interface CTypeName { + name: string; + node: SyntaxNode; +} + +/** + * The type names a C or C++ class, struct or union's members are declared + * with, in source order: `pause_event` in `pause_event by_value;`, `*p`, `a[3]` + * or `(*cb)(pause_event *)`, the `struct` or `enum` a member names by its tag + * (`struct client *c`), and whatever a template's arguments name, in + * `std::vector` too. `specifier` is the class's own, carrying the body; + * `ownName` is what the index calls it, which a typedef may decide. + * + * A name inside an unnamed member (`union { Foo a; }`, `struct { Foo a; } s;`) + * is the holder's: its members are members of the holder, and nothing can name + * the unnamed type (isUnnamedMemberType). A member function's return type + * counts, since a member a macro trails reads the same (`Version* v GUARDED_BY(mu);`), + * but not its parameters. Not read: builtins; a type of `std`, though its + * template arguments are; the class's own name, which a list's `next` writes; + * a type the program computes; a nested definition that declares no member + * (`class Rep;`, `struct Node { … };`). + * + * Mirrored in the kernel (ccpp/mod.rs). + */ +export function cMemberTypeNames(specifier: SyntaxNode, ownName: string, source: string): CTypeName[] { + const names: CTypeName[] = []; + const own = new Set([ownName]); + const tag = getChildByField(specifier, 'name'); + if (tag) own.add(stripCppTemplateArgs(getNodeText(tag, source))); + const body = getChildByField(specifier, 'body'); + if (body) collectMemberTypeNames(body, own, source, names); + return names; +} + +/** + * The type names a C or C++ alias is declared with: `redisObject` in `typedef + * struct redisObject robj;`, `Foo` in `using Foos = std::vector;`. Code + * names the alias, so this is how a change to the type reaches what holds one. + * Skipped as in cMemberTypeNames: `typedef struct dict dict;` names no other type. + */ +export function cAliasTypeNames(alias: SyntaxNode, ownName: string, source: string): CTypeName[] { + const names: CTypeName[] = []; + const own = new Set([ownName]); + const type = getChildByField(alias, 'type'); + if (type) collectCTypeNames(type, own, source, names); + for (const declarator of alias.childrenForFieldName('declarator')) { + if (declarator) collectDeclaratorTypeNames(declarator, own, source, names); + } + return names; +} + +function collectMemberTypeNames(list: SyntaxNode, own: ReadonlySet, source: string, names: CTypeName[]): void { + for (const child of list.namedChildren) { + if (MEMBER_CONDITIONALS.has(child.type)) { + collectMemberTypeNames(child, own, source, names); + continue; + } + if (child.type !== 'field_declaration') continue; + const type = getChildByField(child, 'type'); + if (!type) continue; + const declarators = child.childrenForFieldName('declarator').filter((d: SyntaxNode | null): d is SyntaxNode => !!d); + const unnamedBody = CLASS_LIKE_SPECIFIERS.has(type.type) && !getChildByField(type, 'name') + ? getChildByField(type, 'body') + : null; + if (unnamedBody) collectMemberTypeNames(unnamedBody, own, source, names); + else if (declarators.length > 0) collectCTypeNames(type, own, source, names); + for (const declarator of declarators) { + if (!declaresMemberFunction(declarator)) collectDeclaratorTypeNames(declarator, own, source, names); + } + } +} + +/** The types a declarator names, past the type written before it: a function pointer's parameters. */ +function collectDeclaratorTypeNames(declarator: SyntaxNode, own: ReadonlySet, source: string, names: CTypeName[]): void { + if (declarator.type === 'function_declarator') { + const parameters = getChildByField(declarator, 'parameters'); + if (parameters) collectCTypeNames(parameters, own, source, names); + } + if (!declarator.type.endsWith('declarator')) return; + const inner = innerDeclarator(declarator); + if (inner) collectDeclaratorTypeNames(inner, own, source, names); +} + +/** Collects the type names under `node` onto `names`, in source order. */ +function collectCTypeNames(node: SyntaxNode, own: ReadonlySet, source: string, names: CTypeName[]): void { + if (COMPUTED_TYPE_SPECIFIERS.has(node.type)) return; + if (node.type === 'type_identifier' || node.type === 'qualified_identifier' || node.type === 'template_type') { + const name = stripCppTemplateArgs(getNodeText(node, source)).trim(); + if (name && !own.has(name) && !C_STANDARD_TYPE_NAME.test(name)) names.push({ name, node }); + collectTemplateArguments(node, own, source, names); + return; + } + const tag = CLASS_LIKE_SPECIFIERS.has(node.type) ? getChildByField(node, 'name') : null; + for (const c of tag ? [tag] : node.namedChildren) collectCTypeNames(c, own, source, names); +} + +/** The types the template arguments of a written name name, wherever in the name they are. */ +function collectTemplateArguments(node: SyntaxNode, own: ReadonlySet, source: string, names: CTypeName[]): void { + for (const c of node.namedChildren) { + if (c.type === 'template_argument_list') collectCTypeNames(c, own, source, names); + else if (c.type === 'qualified_identifier' || c.type === 'template_type') collectTemplateArguments(c, own, source, names); + } +} + /** * Whether a C/C++ type specifier defines a class, struct, union or enum, as * opposed to naming one (`struct Foo *p`, the forward declaration `class Foo;`). diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index eea8fa1ace..c627661400 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -21,7 +21,7 @@ import { FN_REF_SPECS, captureFnRefCandidates, goScopedFnRefScopes, type FnRefSp import { isGeneratedFile, isMinifiedContent } from './generated-detection'; import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types'; import { EXTRACTORS } from './languages'; -import { stripCppTemplateArgs, isCppConstructorDeclaration, cDeclaratorIdentifier, isClassLikeDefinition } from './languages/c-cpp'; +import { stripCppTemplateArgs, isCppConstructorDeclaration, cAliasTypeNames, cDeclaratorIdentifier, cMemberTypeNames, isClassLikeDefinition, isUnnamedMemberType, type CTypeName } from './languages/c-cpp'; import { NestedIntervals, scanCppBraceScopes, type CppBraceScopes } from './languages/cpp-brace-scopes'; import { appendCppMacroMethods } from './languages/cpp-macro-methods'; import { rustImplItemOwner, rustImplTypeName } from './languages/rust'; @@ -2752,6 +2752,7 @@ export class TreeSitterExtractor { // Extract extends/implements this.extractInheritance(node, classNode.id); + this.extractCMemberTypeRefs(node, classNode); // C# primary-constructor parameter dependencies (`class Svc(IRepo r, …)`). this.extractCsharpPrimaryCtorParamRefs(node, classNode.id); @@ -2983,6 +2984,7 @@ export class TreeSitterExtractor { // Extract inheritance (e.g. Swift: struct HTTPMethod: RawRepresentable) this.extractInheritance(node, aggregateNode.id); + this.extractCMemberTypeRefs(node, aggregateNode); // C# primary-constructor parameter dependencies (`struct P(int x)`, and // `record struct M(decimal Amount)` which the grammar nests here). @@ -3002,6 +3004,29 @@ export class TreeSitterExtractor { } } + /** + * A C or C++ class, struct or union references the types its members are + * declared with, as the class: members mint no nodes, and impact never climbs + * from a member to its container (Go's and Dart's fields are the same, #2655, + * #2327). Mirrored in the kernel (ccpp/mod.rs). + */ + private extractCMemberTypeRefs(specifier: SyntaxNode, owner: Node): void { + if ((this.language !== 'c' && this.language !== 'cpp') || isUnnamedMemberType(specifier)) return; + this.pushCTypeRefs(cMemberTypeNames(specifier, owner.name, this.source), owner); + } + + private pushCTypeRefs(types: readonly CTypeName[], owner: Node): void { + for (const { name, node } of types) { + this.unresolvedReferences.push({ + fromNodeId: owner.id, + referenceName: name, + referenceKind: 'references', + line: node.startPosition.row + 1, + column: node.startPosition.column, + }); + } + } + /** * Extract an enum */ @@ -4471,6 +4496,7 @@ export class TreeSitterExtractor { if (typeChild) { // Extract struct embedding (e.g. Go: `type DB struct { *Head; Queryable }`) this.extractInheritance(typeChild, aggregateNode.id); + this.extractCMemberTypeRefs(typeChild, aggregateNode); // A Go struct references the types its fields are declared with, as // the struct: Go fields mint no nodes, and impact never climbs from a // member to its container (Dart's fields are the same, #2327). @@ -4551,6 +4577,12 @@ export class TreeSitterExtractor { ...(owner !== undefined ? { qualifiedName: `${owner}::${name}` } : {}), }); + // A C or C++ alias references what it names, so a change to a struct + // reaches the members declared through `typedef struct redisObject robj;`. + if (typeAliasNode && (this.language === 'c' || this.language === 'cpp')) { + this.pushCTypeRefs(cAliasTypeNames(node, name, this.source), typeAliasNode); + } + // Extract type references from the alias value (e.g., `type X = ITextModel | null`) if (typeAliasNode && this.TYPE_ANNOTATION_LANGUAGES.has(this.language)) { // The value is everything after the `=`, which is typically the last named child diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 38af873fcf..07dfdc2c15 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -4922,6 +4922,13 @@ export class ToolHandler { // place in the file's budget. const TYPE_KINDS = new Set(['class', 'struct', 'union', 'interface', 'trait', 'protocol', 'enum', 'type_alias']); const isTypeDeclaration = (id: string): boolean => TYPE_KINDS.has(subgraph.nodes.get(id)?.kind ?? ''); + // A C or C++ class's edge to the type of a member, or an alias's to what it + // names, says who holds what, not where the code goes: rocksdb's classes + // reach everything through them, and they would pull a flow's rank and its + // file budget away from the code on the flow. + const isCFamily = (node: Node | undefined): boolean => node?.language === 'c' || node?.language === 'cpp'; + const isCMemberTypeEdge = (edge: Edge): boolean => + edge.kind === 'references' && isTypeDeclaration(edge.source) && isCFamily(subgraph.nodes.get(edge.source)); const connectedToEntry = new Set(); for (const edge of subgraph.edges) { if (edge.kind === 'references' && isTypeDeclaration(edge.source) && isTypeDeclaration(edge.target)) continue; @@ -5213,7 +5220,7 @@ export class ToolHandler { const nodeRwr = this.computeGraphRelevance( // Fall back to the unfiltered seeds when EVERY seed is damped: the walk // must not lose its restart vector and return all-uniform. - [...subgraph.nodes.keys()], subgraph.edges, + [...subgraph.nodes.keys()], subgraph.edges.filter((e) => !isCMemberTypeEdge(e)), rwrSeedIds.size > 0 ? rwrSeedIds : entryNodeIds, ); // @@ -6829,6 +6836,9 @@ export class ToolHandler { const outgoing = cg.getOutgoingEdges(node.id); for (const edge of outgoing) { if (!edge.line || edge.line <= 0 || edge.kind === 'contains') continue; + // (See isCMemberTypeEdge: a window per member would take the budget + // of the code around them.) + if (edge.kind === 'references' && isCFamily(node) && TYPE_KINDS.has(node.kind)) continue; const key = `${edge.line}:${edge.target}`; if (edgeLines.has(key)) continue; edgeLines.add(key); diff --git a/src/resolution/cpp-supertypes.ts b/src/resolution/cpp-supertypes.ts index 3498315483..03f00dc597 100644 --- a/src/resolution/cpp-supertypes.ts +++ b/src/resolution/cpp-supertypes.ts @@ -48,7 +48,8 @@ * * The same lookup (cppClassWritten) finds the class a `std::unique_ptr`, * `std::shared_ptr` or `std::optional` holds, for a `->` call on it, looked - * up from the calling function (name-matcher's matchCppHeldTypeCall). + * up from the calling function (name-matcher's matchCppHeldTypeCall), and the + * type a member or an alias is declared with (matchCppDeclaredType). */ import type { Node } from '../types'; import type { ResolvedRef, ResolutionContext, UnresolvedRef } from './types'; @@ -57,14 +58,18 @@ import { cppParentScope, cppScopesWithin, cppTemplateParameters, + cppTypeNamed, cppTypeSegments, stripCppTemplateArguments, - type CppNamedClass, + type CppNamedType, } from './cpp-type-aliases'; +import { cppIncluders } from './cpp-includers'; import { cppMacroNamespaceFrames, cppNamespaceAliases } from './cpp-namespaces'; import { stripCommentsForRegex } from './strip-comments'; const CLASS_KINDS: ReadonlySet = new Set(['class', 'struct', 'union']); +/** What a member's type can name. */ +const TYPE_KINDS: ReadonlySet = new Set([...CLASS_KINDS, 'enum', 'type_alias']); /** A C or C++ source file, whose definitions belong to its own translation unit. */ const SOURCE_FILE = /\.(?:c|cc|cpp|cxx|c\+\+|cu|metal)$/i; @@ -79,6 +84,17 @@ function isCFamily(n: Node): boolean { return n.language === 'cpp' || n.language === 'c'; } +/** + * Does `ref` name the type a C or C++ class's member or an alias is declared + * with? Extraction writes no other `references` from either: a value read is + * its function's. + */ +export function isCppDeclaredTypeRef(ref: UnresolvedRef, context: ResolutionContext): boolean { + if (ref.referenceKind !== 'references' || (ref.language !== 'cpp' && ref.language !== 'c')) return false; + const holder = context.getNodeById?.(ref.fromNodeId); + return !!holder && isCFamily(holder) && (CLASS_KINDS.has(holder.kind) || holder.kind === 'type_alias'); +} + /** * The declarations code at `site` can see (visibleFrom), read for code in * the file `unit`, where a class local to a function counts only in that @@ -140,6 +156,48 @@ export function matchCppSupertype(ref: UnresolvedRef, context: ResolutionContext : { original: ref, targetNodeId: found.node.id, confidence: 0.7, resolvedBy: 'exact-match' }; } +/** + * The type a member of a C or C++ class or an alias is declared with: what C++ + * name lookup finds from inside the class, as the class's base is found. Only a + * type can be named there, so a function or macro of that name is never a + * candidate, and a list's own `next` links nothing. + */ +export function matchCppDeclaredType(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + const holder = context.getNodeById?.(ref.fromNodeId); + if (!holder) return null; + const site: CppNameSite = { + scope: holder.kind === 'type_alias' ? cppParentScope(holder.qualifiedName) : holder.qualifiedName, + file: holder.filePath, + line: ref.line, + parameters: cppTemplateParameters(holder, context), + declared: true, + }; + const found = cppClassWritten(ref.referenceName, site, ref, context); + const node = found === undefined ? definedByIncluder(ref.referenceName, site, context) : found?.node; + if (!node || node.id === holder.id) return null; + return found?.exact + ? { original: ref, targetNodeId: node.id, confidence: 0.9, resolvedBy: 'qualified-name' } + : { original: ref, targetNodeId: node.id, confidence: 0.7, resolvedBy: 'exact-match' }; +} + +/** + * The type a header names and only a source file including it defines, which + * no other file can see: `struct Rep; Rep* rep_;`, `struct dictEntry *e`. + * Several such definitions would be a guess. + */ +function definedByIncluder(written: string, site: CppNameSite, context: ResolutionContext): Node | undefined { + const segments = cppTypeSegments(written); + if (!segments) return undefined; + const path = segments.join('::'); + const spelled = new Set(cppScopesWithin(site.scope).map((scope) => (scope ? `${scope}::${path}` : path))); + const candidates = context.getNodesByName(segments[segments.length - 1]!).filter((n) => + isCFamily(n) && n.kind !== 'type_alias' && TYPE_KINDS.has(n.kind) && spelled.has(n.qualifiedName)); + if (candidates.length === 0) return undefined; + const includers = cppIncluders(site.file, context); + const defined = candidates.filter((n) => includers.has(n.filePath)); + return defined.length === 1 ? defined[0] : undefined; +} + /** Where a C or C++ type name is written, which is where its lookup starts. */ export interface CppNameSite { /** @@ -154,6 +212,11 @@ export interface CppNameSite { parameters: ReadonlySet; /** A class the name cannot be: the deriving class, in its own base clause. */ self?: Node; + /** + * Find what the name itself declares: an enum counts, and an alias is the + * answer rather than a step on the way to the class it names. + */ + declared?: boolean; /** * A class looked in right after `scope` itself, its bases included: a gtest * `TEST_F` body is a member function of a class derived from its fixture, @@ -189,9 +252,11 @@ export function cppClassWritten(written: string, site: CppNameSite, ref: Unresol const scopes = cppScopesWithin(site.scope); if (site.classScope) scopes.splice(1, 0, site.classScope); const resolved = (node: Node): CppWrittenClass => ({ node, exact: true }); + const named = (spelled: string, from: readonly string[]): CppNamedType => + (site.declared ? cppTypeNamed : cppClassNamed)(spelled, from, ref, context, visible); // The scopes around the name short of the global one, or only the global one for `::Base`. - const lexical = classNode(cppClassNamed(written, global ? [''] : scopes.slice(0, -1), ref, context, visible), site, context, visible); + const lexical = classNode(named(written, global ? [''] : scopes.slice(0, -1)), site, context, visible); if (lexical !== undefined) return lexical && resolved(lexical); // What the file writes before the name, the nearest first: each stands for @@ -202,7 +267,7 @@ export function cppClassWritten(written: string, site: CppNameSite, ref: Unresol const usings = usingsIn(site.file, context).filter((u) => u.line < site.line).reverse(); const lookUp = (spelled: string): Node | undefined => { const absolute = spelled.startsWith('::'); - return classNode(cppClassNamed(spelled, absolute ? [''] : scopes, ref, context, visible), site, context, visible) ?? undefined; + return classNode(named(spelled, absolute ? [''] : scopes), site, context, visible) ?? undefined; }; // `::_pbi::MessageGlobalsBase` under `namespace _pbi = ::google::protobuf::internal;`. if (segments.length > 1) { @@ -217,6 +282,9 @@ export function cppClassWritten(written: string, site: CppNameSite, ref: Unresol if (u.kind !== 'declaration' || u.name !== segments[0]) continue; const hit = lookUp(u.path + after); if (hit) return resolved(hit); + // `using std::string;`: the file says where the name comes from, and it + // is nothing the project declares, whatever else shares its name. + if (site.declared) return null; } // `using namespace ROCKSDB_NAMESPACE;` … `public EventListener`. for (const u of usings) { @@ -225,7 +293,7 @@ export function cppClassWritten(written: string, site: CppNameSite, ref: Unresol if (hit) return resolved(hit); } // The global scope. - const outermost = classNode(cppClassNamed(written, [''], ref, context, visible), site, context, visible); + const outermost = classNode(named(written, ['']), site, context, visible); if (outermost !== undefined) return outermost && resolved(outermost); } // `fmt::detail::buffer`, where a macro opens `namespace fmt` around `detail::buffer`. @@ -234,9 +302,13 @@ export function cppClassWritten(written: string, site: CppNameSite, ref: Unresol .filter((n) => macroQualifiedName(n, context) === path), site.file); if (hit) return resolved(hit); } - // The only class of that name the file can see, by its qualified tail. + // The only class of that name the file can see, by its qualified tail. For a + // declared type, only in C: a struct nested in another is C's as much as any, + // while a C++ name no lookup found is outside the project far more often than + // it is a class of some other scope (rocksdb's `pthread_key_t`, the system's, + // beside its `port::pthread_key_t`). const tail = namedClasses(segments, site, context, visible) - .filter((n) => n.qualifiedName === path || n.qualifiedName.endsWith(`::${path}`)); + .filter((n) => (!site.declared || n.language === 'c') && (n.qualifiedName === path || n.qualifiedName.endsWith(`::${path}`))); const own = tail.filter((n) => n.filePath === site.file); const sole = own.length > 0 ? own : tail; return sole.length === 1 ? { node: sole[0]!, exact: false } : undefined; @@ -280,9 +352,11 @@ function writtenBase(ref: UnresolvedRef, context: ResolutionContext): string { * see (`visible`; null: the name names no particular class; undefined: keep * looking). */ -function classNode(named: CppNamedClass, site: CppNameSite, context: ResolutionContext, visible: (n: Node) => boolean): Node | null | undefined { +function classNode(named: CppNamedType, site: CppNameSite, context: ResolutionContext, visible: (n: Node) => boolean): Node | null | undefined { if (named === null || named === undefined) return named; + if (site.declared) return declarationNode(named, site, context, visible); if ('alias' in named) return named.alias; + if ('enums' in named) return null; const classes = context .getNodesByQualifiedName(named.cls) .filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== site.self?.id); @@ -292,6 +366,35 @@ function classNode(named: CppNamedClass, site: CppNameSite, context: ResolutionC return pick(shown.length > 0 ? shown : classes, site.file); } +/** + * The declaration a type name found by a lookup stands for, among all that one + * qualified name declares. C spreads a type over files, so that a class in + * some other folder can be the only one a lookup sees: redis's `dictEntry` is + * `typedef struct dictEntry dictEntry;` in src/dict.h and a struct in src/dict.c, + * which only that file sees, while deps/hiredis/dict.h declares its own. What + * the file itself declares or includes comes first, then any header. A class + * beats an alias of its name, and the alias is the class's when a file that + * includes the alias defines it. + */ +function declarationNode(named: NonNullable, site: CppNameSite, context: ResolutionContext, visible: (n: Node) => boolean): Node | undefined { + const name = 'cls' in named ? named.cls : 'alias' in named ? named.alias.qualifiedName : named.enums[0]!.qualifiedName; + const all = context.getNodesByQualifiedName(name).filter((n) => isCFamily(n) && TYPE_KINDS.has(n.kind)); + if (all.length <= 1) return all[0]; + const defined = (nodes: Node[]): Node[] => nodes.filter((n) => n.kind !== 'type_alias'); + const seen = all.filter(visible); + const own = seen.filter((n) => n.filePath === site.file); + // (Several in the file: the `#if` arms of one type, or the partial + // specializations of a template, whose members index under one name. The + // nearest above is the one in force.) + const above = own.filter((n) => n.startLine <= site.line).sort((a, b) => b.startLine - a.startLine); + const included = above.length > 0 ? above.slice(0, 1) : own.length > 0 ? own : seen.filter((n) => cppIncluders(n.filePath, context).has(site.file)); + const near = included.length > 0 ? included : seen; + const found = pick(defined(near).length > 0 ? defined(near) : near, site.file); + if (found?.kind !== 'type_alias') return found; + const includers = cppIncluders(found.filePath, context); + return pick(defined(all).filter((n) => includers.has(n.filePath)), found.filePath) ?? found; +} + /** * One of several declarations of a class (separate translation units, * vendored copies): the one in `file` itself, then in its header, then the @@ -316,9 +419,10 @@ function pick(classes: Node[], file: string): Node | undefined { /** C and C++ classes named like the last segment of `segments` that the file at `site` can see. */ function namedClasses(segments: readonly string[], site: CppNameSite, context: ResolutionContext, visible: (n: Node) => boolean): Node[] { + const kinds = site.declared ? TYPE_KINDS : CLASS_KINDS; return context .getNodesByName(segments[segments.length - 1]!) - .filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== site.self?.id && visible(n)); + .filter((n) => isCFamily(n) && kinds.has(n.kind) && n.id !== site.self?.id && visible(n)); } /** A declaration's qualified name with the namespaces macros open around it: `fmt::detail::buffer` for `detail::buffer`. */ diff --git a/src/resolution/cpp-type-aliases.ts b/src/resolution/cpp-type-aliases.ts index ae0bbc3580..34b73ae569 100644 --- a/src/resolution/cpp-type-aliases.ts +++ b/src/resolution/cpp-type-aliases.ts @@ -305,7 +305,9 @@ type Declared = | { alias: Node } /** A class, struct or union, by its qualified name. */ | { cls: string } - /** An enum, namespace or other non-class type. */ + /** An enum, by its declarations. */ + | { enums: Node[] } + /** A namespace or other non-class type. */ | 'other' /** * Aliases naming different types, none of them the calling file's: rocksdb's @@ -338,6 +340,8 @@ function declaredIn( if (here.length === 0) return undefined; // `typedef struct Node Node;` declares the struct as well. if (here.some((n) => CLASS_KINDS.has(n.kind))) return { cls: qualified }; + const enums = here.filter((n) => n.kind === 'enum'); + if (enums.length > 0) return { enums }; if (here.some((n) => n.kind !== 'type_alias')) return 'other'; const alias = pickAlias(here, ref, context); return alias ? { alias } : { ambiguous: here }; @@ -664,9 +668,43 @@ export function cppClassNamed( ): CppNamedClass { const name = cppTypeName(written); if (!name) return null; - const { found, next } = lookupSegments(name, scopes, ref, context, visible); + return classFound(name, lookupSegments(name, scopes, ref, context, visible), ref, context); +} + +/** What a C or C++ type name names when it is looked up for what it declares (see cppTypeNamed). */ +export type CppNamedType = CppNamedClass | { enums: Node[] }; + +/** + * What a C or C++ type written as `written` declares, looked up as cppClassNamed + * looks it up: the class, an enum, or the alias the name ends on, not followed + * to the type it names, so an edge says what the code wrote. + */ +export function cppTypeNamed( + written: string, + scopes: readonly string[], + ref: UnresolvedRef, + context: ResolutionContext, + visible?: Visible, +): CppNamedType { + const name = cppTypeName(written); + if (!name) return null; + // (A reference carries its name without template arguments, so an alias + // template can't be told by them. A name that finds one was written with them.) + const lookup = lookupSegments({ ...name, templated: name.names.map(() => true) }, scopes, ref, context, visible); + const { found, next } = lookup; + if (found && typeof found === 'object' && ('enums' in found || ('alias' in found && next === name.names.length))) return found; + return classFound(name, lookup, ref, context); +} + +/** The class a lookup of `name` found, followed through the alias it reached. */ +function classFound( + name: TypeName, + { found, next }: { found: Declared; next: number }, + ref: UnresolvedRef, + context: ResolutionContext, +): CppNamedClass { if (found === undefined) return undefined; - if (typeof found !== 'object') return null; + if (typeof found !== 'object' || 'enums' in found) return null; if ('cls' in found) return found; if ('ambiguous' in found) { // An alias each `#if` arm declares differently: the first declaration, by @@ -708,7 +746,7 @@ export function cppAliasedTypeWritten( const name = cppTypeName(written); if (!name) return undefined; const { found, next } = lookupSegments(name, scopesFor(name, scopes), ref, context); - if (!found || typeof found !== 'object' || 'cls' in found) return undefined; + if (!found || typeof found !== 'object' || 'cls' in found || 'enums' in found) return undefined; if ('ambiguous' in found || next !== name.names.length || isAliasTemplate(found.alias, context)) return null; const text = declarationText(found.alias, context); const aliased = text ? aliasedTypeIn(text, found.alias.name) : null; diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 27286e60a2..8eae307f24 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -24,7 +24,7 @@ import { import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, isRustPathCallReachable, rustChainedCallByName, matchRustUseBoundName, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, goDotImportedSupertype, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, matchGoChainedCall, preferGoBuildTwin, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonFunctionModuleAlias, pythonAttrRefResolves, matchPythonTypedLocalCall, isImportShadowedByJsBinding, isManualMockPath, isTestSuitePath, matchNewReceiverCall, newReceiverClass, NEW_RECEIVER_SHAPE, isGuessedRecordAccessor, matchKotlinReceiverChain } from './name-matcher'; import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility'; import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor'; -import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes'; +import { isCppDeclaredTypeRef, isCppSupertypeRef, matchCppDeclaredType, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes'; import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility'; import { clearDartLibraryMemos } from './dart-libraries'; import { clearVbnetReceiverMemos, isVbMemberRead, isVbPathCall, matchVbMemberRead, matchVbPathCall } from './vbnet-receivers'; @@ -1576,6 +1576,9 @@ export class ReferenceResolver { // which a framework's heuristics or name matching would take (see // ./cpp-supertypes). if (isCppSupertypeRef(ref)) return matchCppSupertype(ref, this.context); + // So is the type a member of one or an alias is declared with, which only + // a type can be: a function or macro of that name is no candidate. + if (isCppDeclaredTypeRef(ref, this.context)) return matchCppDeclaredType(ref, this.context); // A Pascal name written without parentheses (#2514) links only to a // routine its scope reaches, or through a receiver to a method of the