diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a285b6ea..54cf54d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In React, a component's JSX no longer links it to a class or component that only shares a name with one of its type arguments or with a variable of its own. A type in angle brackets, like `Document` in ` items={…} />`, `User` in `useState()` or `Entry` in a generic `(…) =>` component, is no longer read as a tag. A tag naming a variable or parameter the component sets itself, like `` after `const Content = isDropdown ? DropdownMenu.SubContent : ContextMenu.SubContent` or `` in `widgets.map((Widget) => )`, now links to nothing, unless the component declares a component of that name inside itself. Before, these linked to an unrelated class or component elsewhere in the repository: outline's document lists showed up among the callers of its `Document` model class, and its menus among the callers of the command bar's `Content`, so `codegraph_explore`, callers and impact followed renders that never happen. Re-index React projects after upgrading. - In JavaScript and TypeScript, a name imported from a package your `package.json` lists no longer links to a project symbol that only shares its name when `tsconfig.json` or `jsconfig.json` has a catch-all path alias, like `"*": ["./typings/*"]` or `"*": ["src/*", "node_modules/*"]`, or an alias that points the package at a file in `node_modules`, like `"lit/decorators": ["./node_modules/lit/decorators.js"]`. Such an alias made every package look like part of the project, so `import { Typography } from '@mui/material'` was linked to the project's own `Typography` and every lit `@property()` decorator to an unrelated class's `property` field, and `codegraph callers`, impact and `codegraph affected` listed code that never used them. An import the alias does map to a file of your project, like a `.d.ts` you keep for an untyped package or `components/Button` through `"*": ["src/*"]`, links as before. Re-index affected projects after upgrading. - `codegraph_explore`'s Flow now goes through the implementation your query names when an interface method has several that lead to the same place. Before, it took whichever one came first in the index: asking about prometheus's `Engine.execEvalStmt Queryable.Querier fanout.Querier NewMergeQuerier` traced the call through the TSDB's `DB.Querier` and left the `fanout.Querier` you named off the Flow. Any two routes of the same length are now settled this way, in favor of the one that passes through more of the symbols you named. +- In C and C++, a struct, class, union or enum defined in a variable declaration, like `struct Foo { … } foo;`, is now indexed with its methods and enum values, next to the variable. Before, at most the variable was, so the type was missing from search and `codegraph_explore`, a class deriving from it had no base, and calls to its methods linked to nothing: in redis, the Sentinel's `sentinelState` and `redis-cli`'s `config` struct were missing. A struct, union or enum with no name of its own now takes the name of the variable it declares, so redis's `static struct { … } SPT;` is found as `SPT`, and one declared inside a function is named after its variable instead of ``. The comment above the declaration becomes the type's description. Re-index C and C++ projects after upgrading. ## [1.6.2] - 2026-10-03 diff --git a/__tests__/c-cpp-declaration-types.test.ts b/__tests__/c-cpp-declaration-types.test.ts new file mode 100644 index 000000000..4ae348593 --- /dev/null +++ b/__tests__/c-cpp-declaration-types.test.ts @@ -0,0 +1,261 @@ +/** + * A class, struct, union or enum DEFINED in the type of a C or C++ variable + * declaration was never indexed: + * + * namespace n { struct Foo { int a; void f() {} } foo; } // only n::foo + * static struct { int argc; char **argv; } SPT; // nothing at all + * enum { MODE_IDLE, MODE_BUSY } current_mode; // no enumerators + * + * Both extractors handed such a declaration to the variable extractor and + * skipped its children, so the type, its methods and its enumerators never + * became nodes, a base class defined this way had nothing to resolve to, and + * calls on its methods linked to nothing. + * + * The type is now walked like one written on its own, beside the variables + * (which keep their nodes), and the comment above the declaration documents + * it. An unnamed one takes the name of the first variable its declaration + * declares, as `typedef struct { … } Name;` takes the typedef name: no code + * can name the type of `SPT`, only `SPT`. In a C++ file whose tree has errors + * the type is walked in the scopes the file's braces open (#2426), and not at + * all when the braces don't balance. 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 type { ExtractionResult, Language } 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'; + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS', 'CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT'] as const; + +/** ` ` for every symbol, file and imports aside. */ +function symbols(result: ExtractionResult): string[] { + return result.nodes + .filter((n) => n.kind !== 'file' && n.kind !== 'import') + .map((n) => `${n.kind} ${n.qualifiedName}`); +} + +/** ` > ` for every containment edge below the file. */ +function containment(result: ExtractionResult): string[] { + const byId = new Map(result.nodes.map((n) => [n.id, n])); + return result.edges + .filter((e) => e.kind === 'contains' && byId.get(e.source)?.kind !== 'file') + .map((e) => `${byId.get(e.source)?.qualifiedName} > ${byId.get(e.target)?.qualifiedName}`); +} + +describe('a type defined in a C/C++ declaration', () => { + 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)(`C++ classes and structs beside their variables: %s${label}`, (backend) => { + const source = [ + 'namespace n {', + 'struct Foo { int a; void f() {} } foo;', + 'class Bar { void g() {} } bar, *pbar;', + '}', + '', + ].join('\n'); + const result = extract(backend, 'src/decl.cpp', eol(source), 'cpp'); + expect(symbols(result)).toEqual([ + 'struct n::Foo', + 'method n::Foo::f', + 'variable n::foo', + 'class n::Bar', + 'method n::Bar::g', + 'variable n::bar', + ]); + expect(containment(result)).toEqual(['n::Foo > n::Foo::f', 'n::Bar > n::Bar::g']); + }); + + it.each(backends)(`C structs, unions and enums; an unnamed one takes its variable's name: %s${label}`, (backend) => { + const source = [ + 'struct Foo { int a; } foo;', + 'static struct { int argc; char **argv; } SPT;', + 'static const struct { const char *name; int code; } errors[] = { { "none", 0 } };', + 'struct { int x; } *first_point, second_point;', + 'union { int i; float f; } scratch = { 0 };', + 'enum { MODE_IDLE, MODE_BUSY } current_mode;', + 'typedef struct Bar { int c; } Bar;', + '', + ].join('\n'); + const result = extract(backend, 'src/decl.c', eol(source), 'c'); + expect(symbols(result)).toEqual([ + 'struct Foo', + 'struct SPT', + 'struct errors', + 'constant errors', + 'struct first_point', + 'variable first_point', + 'union scratch', + 'variable scratch', + 'enum current_mode', + 'enum_member current_mode::MODE_IDLE', + 'enum_member current_mode::MODE_BUSY', + 'struct Bar', + ]); + }); + + it.each(backends)(`a type whose declaration declares no variable stays unnamed: %s${label}`, (backend) => { + // A function's return type: naming it after the function would be wrong. + const result = extract(backend, 'src/proto.c', eol('struct { int unused; } make_unnamed(void);\n'), 'c'); + expect(symbols(result)).toEqual(['struct ']); + }); + + it.each(backends)(`the comment above the declaration documents the type: %s${label}`, (backend) => { + const source = [ + 'namespace app {', + '/** The handler registry. */', + 'struct Registry { int size() const { return 2; } } registry;', + '}', + '', + ].join('\n'); + const result = extract(backend, 'src/registry.cc', eol(source), 'cpp'); + const docs = result.nodes + .filter((n) => n.name === 'Registry' || n.name === 'registry') + .map((n) => `${n.kind} ${n.docstring}`); + expect(docs).toEqual(['struct The handler registry.', 'variable The handler registry.']); + }); + + it.each(backends)(`an unnamed type in a function body takes its variable's name too: %s${label}`, (backend) => { + const source = 'int hits(void) {\n static struct { int count; } local_stats;\n return ++local_stats.count;\n}\n'; + const result = extract(backend, 'src/stats.c', eol(source), 'c'); + expect(symbols(result)).toEqual(['function hits', 'struct hits::local_stats']); + }); + + it.each(backends)(`a file the parser misreads: C keeps the type, C++ needs balanced braces: %s${label}`, (backend) => { + // The kernel defers a file whose tree has errors to wasm; its + // error-extract hatch walks one anyway, and must agree. + process.env.CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT = '1'; + const c = extract(backend, 'src/spt.c', eol('static struct { int argc; } SPT;\nint broken( { return 1; }\n'), 'c'); + expect(symbols(c)).toContain('struct SPT'); + // A C++ file with errors is walked in the scopes its braces open, so the + // class lands in its namespace. When the braces don't balance there are + // no scopes to trust, and error recovery can close a namespace or a + // class at the wrong `}`: the variable is kept and the class not walked. + const config = 'namespace n {\nstruct Config { int retries; void apply() {} } config;\n}\n'; + const balanced = extract(backend, 'src/config.cc', eol(`${config}int broken( { return 1; }\n`), 'cpp'); + expect(symbols(balanced)).toEqual(['struct n::Config', 'method n::Config::apply', 'variable n::config']); + const unbalanced = extract(backend, 'src/config2.cc', eol(`${config}int broken() { return 1;\n`), 'cpp'); + expect(symbols(unbalanced)).toEqual(['variable n::config', 'function broken']); + }); + + it.each(backends)(`a function value in the type's body is captured once, by the type: %s${label}`, (backend) => { + const source = [ + 'void on_open() {}', + 'struct Handlers { void (*all[1])() = {&on_open}; } handlers;', + '', + ].join('\n'); + const result = extract(backend, 'src/handlers.cc', eol(source), 'cpp'); + const byId = new Map(result.nodes.map((n) => [n.id, n])); + const refs = result.unresolvedReferences + .filter((r) => r.referenceKind === 'function_ref') + .map((r) => `${byId.get(r.fromNodeId)?.name} ${r.referenceName}`); + expect(refs).toEqual(['Handlers on_open']); + }); + } +}); + +describe('an indexed C/C++ project with types defined in declarations', () => { + let root = ''; + let cg: CodeGraph | undefined; + + afterEach(() => { + cg?.destroy(); + cg = undefined; + if (root) fs.rmSync(root, { recursive: true, force: true }); + root = ''; + }); + + it('links base classes and calls to them, and finds an unnamed struct by its variable', async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-declaration-types-')); + const files: Record = { + 'src/registry.cc': [ + 'namespace app {', + 'struct Registry { int size() const { return 2; } } registry;', + 'class Base { public: virtual int run() { return 1; } } base_instance;', + 'class Worker : public Base { public: int run() override { return registry.size(); } };', + 'int tick() { Worker w; return w.run() + registry.size(); }', + '}', + '', + ].join('\n'), + 'src/setproctitle.c': [ + '/* Saved argv. */', + 'static struct { int argc; char **argv; } SPT;', + 'int spt_init(int argc) { SPT.argc = argc; return SPT.argc; }', + '', + ].join('\n'), + }; + 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 = (qualifiedName: string) => { + const found = graph.getNodesByName(qualifiedName.split('::').pop()!).filter((n) => n.qualifiedName === qualifiedName); + expect(found, qualifiedName).toHaveLength(1); + return found[0]!; + }; + const edges = (id: string, kind: 'calls' | 'extends') => + graph + .getIncomingEdgesTo([id], [kind]) + .map((e) => graph.getNode(e.source)?.qualifiedName) + .sort(); + + expect(edges(one('app::Registry::size').id, 'calls')).toEqual(['app::Worker::run', 'app::tick']); + expect(edges(one('app::Base').id, 'extends')).toEqual(['app::Worker']); + + const spt = graph.searchNodes('SPT', { limit: 5 }).map((r) => `${r.node.kind} ${r.node.name}`); + expect(spt).toContain('struct SPT'); + expect(one('SPT').docstring).toBe('Saved argv.'); + }, 60_000); +}); diff --git a/__tests__/fixtures/kernel-parity/torture.c b/__tests__/fixtures/kernel-parity/torture.c index 57e04a912..a78598051 100644 --- a/__tests__/fixtures/kernel-parity/torture.c +++ b/__tests__/fixtures/kernel-parity/torture.c @@ -168,3 +168,23 @@ typedef union { } word_t; static unsigned int hdr_raw(union packet_hdr *h) { return h->raw; } + +/* struct, union and enum definitions in a declaration's type: each is a node + beside the declaration's variables, documented by the comment above the + declaration; an unnamed one takes the name of its first variable */ +static void noop_builtin(void) {} + +/* The parser's state. */ +static struct parse_state { int depth; const char *at; } parse_state, *active_state; +static struct { int argc; char **argv; } SPT; +static const struct { const char *name; void (*fn)(void); } builtin_table[] = { + { "noop", noop_builtin }, +}; +union { int i; float f; } scratch_word = { 0 }; +enum { MODE_IDLE, MODE_BUSY } current_mode; +struct { int unused; } make_unnamed(void); + +static int declared_types_local(void) { + static struct { int hits; } local_stats; + return ++local_stats.hits + current_mode; +} diff --git a/__tests__/fixtures/kernel-parity/torture.cpp b/__tests__/fixtures/kernel-parity/torture.cpp index b6b160f7a..b2ba1ea08 100644 --- a/__tests__/fixtures/kernel-parity/torture.cpp +++ b/__tests__/fixtures/kernel-parity/torture.cpp @@ -108,6 +108,21 @@ void Widget::Tick(float Delta) { Health += Delta; } Config GlobalConfig; int build_number = 7; +// class/struct/union/enum definitions in a declaration's type — nodes beside +// the variables; an unnamed one takes its first variable's name +void on_open() {} +void on_close() {} +namespace decl_types { +/** The handler registry. */ +struct Registry { + int size() const { return 2; } + void (*handlers[2])() = {&on_open, &on_close}; +} registry; +class Bar { void g() {} } bar, *pbar; +static struct { int retries; } settings; +enum { kIdle, kBusy } state; +} // namespace decl_types + template T compute_seed(T v) { return v + 1; diff --git a/codegraph-kernel/src/ccpp/mod.rs b/codegraph-kernel/src/ccpp/mod.rs index 0772ac09f..415ca3bca 100644 --- a/codegraph-kernel/src/ccpp/mod.rs +++ b/codegraph-kernel/src/ccpp/mod.rs @@ -37,6 +37,13 @@ //! (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). +//! - a class/struct/union/enum defined in a declaration's `type` +//! (`struct Foo { … } foo;`) is walked before the variables — in C, and in +//! C++ unless the tree has errors and no brace scopes (walkDeclaredTypes); +//! an unnamed one takes the name of the first declarator identifier +//! (`static struct { … } SPT;` → `SPT`, in function bodies too), and the +//! comment above the declaration is its docstring (c/cpp +//! getDeclarationWrapper). //! - 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 @@ -369,6 +376,10 @@ pub struct Walker<'t> { brace_scopes: Option, class_scopes: NestedIntervals, class_scope_rows: HashSet, + /// walkDeclaredTypes (tree-sitter.ts): a class-like type defined in a + /// declaration is walked in C, and in C++ unless the tree has errors and + /// its braces don't balance (no brace scopes; only under the hatch). + walk_declared_types: bool, } pub fn extract(file_path: &str, source: &str, language: &str) -> Result { @@ -394,6 +405,13 @@ pub fn extract(file_path: &str, source: &str, language: &str) -> Result Result Walker<'t> { return hook; } } + if let Some(name) = self.unnamed_type_variable_name(node) { + return name; + } if let Some(name_node) = node.child_by_field_name("declarator") { let mut resolved = name_node; // Unwrap pointer/reference declarators (`int* f()`, `T& f()`). @@ -670,6 +688,21 @@ impl<'t> Walker<'t> { "".to_string() } + /// unnamedTypeVariableName (languages/c-cpp.ts): an unnamed class, struct, + /// union or enum defined in a declaration takes the name of the first + /// variable the declaration declares (`static struct { … } SPT;` → `SPT`). + fn unnamed_type_variable_name(&self, node: Node) -> Option { + if node.child_by_field_name("name").is_some() { + return None; + } + let declaration = defining_declaration(node)?; + let mut cursor = declaration.walk(); + let identifier = declaration + .children_by_field_name("declarator", &mut cursor) + .find_map(c_declarator_identifier)?; + Some(self.text(identifier).to_string()) + } + /// extractCppQualifiedMethodName (languages/c-cpp.ts:75). fn extract_cpp_qualified_method_name(&self, node: Node) -> Option { if let Some(n) = self.recover_cpp_macro_defined_name(node) { @@ -1043,21 +1076,33 @@ impl<'t> Walker<'t> { { skip_children = self.extract_type_alias(node); } else if kind == "declaration" && !self.inside_class_like() { - // In brace scopes, a class the tree reads as a declaration's type - // (glued to the tokens after it by error recovery) is still a class. - if self.brace_scopes.is_some() { - if let Some(t) = node.child_by_field_name("type") { - let class_like = matches!( - t.kind(), - "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier" - ); - if class_like && t.child_by_field_name("body").is_some() { - self.visit_node(t); + // A class, struct, union or enum defined in the declaration's type + // (`struct Foo { … } foo;`) is a definition like one written on its + // own, and the variables keep their nodes beside it — so is, in + // brace scopes, a class the tree reads as a declaration's type + // (glued to the tokens after it by error recovery). The fn-ref + // scan skips the walked type, which captured its own (the TS + // walked set). + let defined_type = node + .child_by_field_name("type") + .filter(|t| self.walk_declared_types && is_class_like_definition(*t)); + if let Some(t) = defined_type { + self.visit_node(t); + } + self.extract_variable(node); + match defined_type { + Some(t) => { + self.maybe_capture_fn_refs(node); + for i in 0..node.named_child_count() { + if let Some(c) = node.named_child(i) { + if c.id() != t.id() { + self.scan_fn_ref_subtree(c, 1); + } + } } } + None => self.scan_fn_ref_subtree(node, 0), } - self.extract_variable(node); - self.scan_fn_ref_subtree(node, 0); skip_children = true; } else if self.variant == Variant::Cpp && kind == "field_declaration" @@ -1204,13 +1249,19 @@ impl<'t> Walker<'t> { self.stack.pop(); } + /// docstringFor a class-like node: the comment above the declaration it is + /// defined in (the c/cpp getDeclarationWrapper), else above the node. + fn class_like_docstring(&self, node: Node<'t>) -> Option { + preceding_docstring(defining_declaration(node).unwrap_or(node), self.src) + } + /// extractClass for cpp class_specifier (skipBodilessClass, #1093). fn extract_class(&mut self, node: Node<'t>) { stack_guard!(); let Some(body) = node.child_by_field_name("body") else { return }; let name = self.extract_name(node); let extra = Extra { - docstring: preceding_docstring(node, self.src), + docstring: self.class_like_docstring(node), visibility: self.visibility_of(node), end: self.brace_body_end(body), ..Extra::default() @@ -1234,7 +1285,7 @@ impl<'t> Walker<'t> { let Some(body) = node.child_by_field_name("body") else { return }; let name = self.extract_name(node); let extra = Extra { - docstring: preceding_docstring(node, self.src), + docstring: self.class_like_docstring(node), visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None }, end: self.brace_body_end(body), ..Extra::default() @@ -1257,7 +1308,7 @@ impl<'t> Walker<'t> { let Some(body) = node.child_by_field_name("body") else { return }; let name = self.extract_name(node); let extra = Extra { - docstring: preceding_docstring(node, self.src), + docstring: self.class_like_docstring(node), visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None }, ..Extra::default() }; @@ -2296,7 +2347,7 @@ fn find_declarator_qualified_id(declarator: Node) -> Option { None } -/// cDeclaratorIdentifier (tree-sitter.ts:234): resolve the declared identifier +/// cDeclaratorIdentifier (languages/c-cpp.ts): resolve the declared identifier /// through init/pointer/array/parenthesized declarator wrappers; a /// function_declarator means prototype/fn-ptr — null. (The C grammar's /// parenthesized_declarator exposes no `declarator` field, so that arm always @@ -2322,6 +2373,26 @@ fn c_declarator_identifier(node: Node) -> Option { None } +/// 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() +} + +/// definingDeclaration (languages/c-cpp.ts): the `declaration` a class-like +/// definition is the type of (`struct Foo { … } foo;`). Its comment documents +/// the type, and an unnamed type takes the name of its first variable. +fn defining_declaration(node: Node) -> Option { + if !is_class_like_definition(node) { + return None; + } + let parent = node.parent()?; + let is_type = parent.child_by_field_name("type").map(|t| t.id()) == Some(node.id()); + (parent.kind() == "declaration" && is_type).then_some(parent) +} + /// isMacroMisparsedTypeDecl (languages/c-cpp.ts:261): `class MACRO Name {…}` /// misparse residue — bodyless class/struct specifier in `type` + a /// non-function_declarator declarator. diff --git a/docs/design/ccpp-kernel-port-checklist.md b/docs/design/ccpp-kernel-port-checklist.md index 839f0abf7..79201b130 100644 --- a/docs/design/ccpp-kernel-port-checklist.md +++ b/docs/design/ccpp-kernel-port-checklist.md @@ -178,7 +178,7 @@ type_identifier + ≥2 params + NO other lone-identifier param; gtest recoverMangledCppName (line 406 — only already-mangled names, `Ret (name)` idiom left alone, last token before `(`, primitive/keyword guard); stripCppTemplateArgs (line 157 — depth-counted removal of every balanced -`<…>`); cDeclaratorIdentifier (tree-sitter.ts:234 — declarator chain walk, +`<…>`); cDeclaratorIdentifier (languages/c-cpp.ts — declarator chain walk, function_declarator → null, 12-hop guard). ## tree-sitter.ts branches (anchors as of `705e501`) @@ -188,6 +188,7 @@ function_declarator → null, 12-hop guard). | 962 | cpp namespace prefix stack (#1291) | named `namespace_definition` pushes its name (C++17 `a::b` as written) onto the QN prefix while walking children; anonymous falls through bare | | — | cpp brace scopes (erroring trees only) | when the tree has errors, every visitNode takes its namespace prefix from the source's braces, and at declaration level (only class scopes above the file node) its enclosing classes: `languages/cpp-brace-scopes.ts` ↔ `ccpp/brace_scopes.rs`. A class-like node's end is its body's `}`; a class-like `type` of a namespace-level `declaration` is walked as a class. Reachable in the kernel only under `CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT=1` (every other erroring file defers) — kept parity-true there | | 2795 | C file-scope variables | only when NO function ancestor; iterate declarators; accept ONLY init_declarator / pointer_declarator / array_declarator — a BARE identifier declarator is a macro-prototype misparse, skip (loses uninit scalars by design); name via cDeclaratorIdentifier; signature `= `; kind constant/variable via isConst | +| — | class-like types defined in declarations | a `declaration` outside any body whose `type` is a class/struct/union/enum specifier WITH a body: visit the type before extracting the variables, in C always and in C++ when the tree has no errors or the file is walked in brace scopes (the row above) — not in an erroring C++ file whose braces don't balance (`walkDeclaredTypes` ↔ `walk_declared_types`); the declaration's fn-ref scan skips the walked type. An unnamed specifier whose parent declaration has it as `type` takes the first declarator's cDeclaratorIdentifier name — in function bodies too (the c/cpp `resolveName` ↔ `extract_name_raw`) — and the declaration's preceding comment is its docstring (`getDeclarationWrapper` ↔ `class_like_docstring`) | | 4313 | explicit operator calls (#1247) | callee = `function` field + ERROR-wrapped `operator_name` sibling; compact symbolic spacing (`operator *`→`operator*`, word forms keep space); receiver `->`→`.`; DROP unless receiver is `this` (bare name) or simple identifier/member chain (silent miss over wrong edge) | | ~4340 | field_expression method calls | `recv.method`/`ptr->method` → `recv.method`; SKIP_RECEIVERS {self,this,cls,super} → bare name; LITERAL receiver → emit nothing (#1230) | | 4398 | call-result receivers (#645/#608) | receiver is call_expression → `().` re-encode (c AND cpp in the gate list) | diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index c2e1f59be..f2161c6fa 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -2,6 +2,77 @@ import type { Node as SyntaxNode } from 'web-tree-sitter'; import { getChildByField, getNodeText } from '../tree-sitter-helpers'; import type { LanguageExtractor } from '../tree-sitter-types'; +/** + * Resolve the declared identifier inside a C declarator. A `declaration`'s + * `declarator` field nests the name through `init_declarator` (with value), + * `pointer_declarator`/`array_declarator`/`parenthesized_declarator` + * wrappers (each via their own `declarator` field) down to an `identifier`. + * A `function_declarator` means the declaration is a function prototype (or a + * function-pointer var) — return null so it isn't extracted as a variable. + */ +export function cDeclaratorIdentifier(node: SyntaxNode | null): SyntaxNode | null { + let cur: SyntaxNode | null = node; + let guard = 0; + while (cur && guard++ < 12) { + switch (cur.type) { + case 'identifier': + return cur; + case 'function_declarator': + return null; + case 'init_declarator': + case 'pointer_declarator': + case 'array_declarator': + case 'parenthesized_declarator': + cur = getChildByField(cur, 'declarator'); + break; + default: + return null; + } + } + return null; +} + +/** 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']); + +/** + * 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;`). + */ +export function isClassLikeDefinition(node: SyntaxNode): boolean { + return CLASS_LIKE_SPECIFIERS.has(node.type) && getChildByField(node, 'body') !== null; +} + +/** + * The `declaration` a class, struct, union or enum is defined in the type of — + * `struct Foo { … } foo;` — or null for one written on its own, in a typedef or + * in a member's type. + */ +function definingDeclaration(node: SyntaxNode): SyntaxNode | null { + if (!isClassLikeDefinition(node)) return null; + const parent = node.parent; + return parent?.type === 'declaration' && getChildByField(parent, 'type')?.id === node.id ? parent : null; +} + +/** + * The name of an unnamed class, struct, union or enum that a declaration + * defines: the first variable the declaration declares. No code can name the + * type of `static struct { … } SPT;` (only `SPT`), so the struct is indexed as + * `SPT`, the way `typedef struct { … } Name;` is indexed as `Name`. Undefined + * for a named type, and when the declaration declares no variable (a + * function's return type). Mirrored in the kernel (ccpp/mod.rs). + */ +function unnamedTypeVariableName(node: SyntaxNode, source: string): string | undefined { + if (getChildByField(node, 'name')) return undefined; + const declaration = definingDeclaration(node); + if (!declaration) return undefined; + for (const declarator of declaration.childrenForFieldName('declarator')) { + const identifier = cDeclaratorIdentifier(declarator); + if (identifier) return getNodeText(identifier, source); + } + return undefined; +} + /** * Find the function NAME's `qualified_identifier` (`Foo::bar`) inside a * declarator, skipping the `parameter_list` — a parameter with a qualified type @@ -256,7 +327,11 @@ function extractCppReturnType(node: SyntaxNode, source: string): string | undefi } export const cExtractor: LanguageExtractor = { - resolveName: recoverSingleArgMacroDefinedName, + resolveName: (node, source) => + recoverSingleArgMacroDefinedName(node, source) ?? unnamedTypeVariableName(node, source), + // A struct, union or enum defined in a declaration's type is documented by + // the comment above the declaration. + getDeclarationWrapper: (node) => definingDeclaration(node) ?? undefined, // CUDA in C-detected headers (content-gated blank; see preParseCSource). preParse: preParseCSource, // Universal net: recover a real name from any macro-mangled function name. @@ -2324,7 +2399,9 @@ export const cppExtractor: LanguageExtractor = { nameField: 'declarator', bodyField: 'body', paramsField: 'parameters', - resolveName: extractCppQualifiedMethodName, + resolveName: (node, source) => + extractCppQualifiedMethodName(node, source) ?? unnamedTypeVariableName(node, source), + getDeclarationWrapper: (node) => definingDeclaration(node) ?? undefined, getReceiverType: extractCppReceiverType, getReturnType: extractCppReturnType, // Constructors (definitions and class-body declarations) carry their diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 358b766b2..9bcf903a1 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -21,7 +21,7 @@ import { FN_REF_SPECS, captureFnRefCandidates, type FnRefSpec, type FnRefCandida import { isGeneratedFile, isMinifiedContent } from './generated-detection'; import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types'; import { EXTRACTORS } from './languages'; -import { stripCppTemplateArgs, isCppConstructorDeclaration } from './languages/c-cpp'; +import { stripCppTemplateArgs, isCppConstructorDeclaration, cDeclaratorIdentifier, isClassLikeDefinition } from './languages/c-cpp'; import { NestedIntervals, scanCppBraceScopes, type CppBraceScopes } from './languages/cpp-brace-scopes'; import { rustImplTypeName } from './languages/rust'; import { goAliasTypeNames, goEmbeddedTypeName } from './languages/go'; @@ -70,9 +70,6 @@ const REACT_COMPONENT_HOCS = new Set(['forwardRef', 'memo', 'React.forwardRef', */ const SIGNATURE_METHOD_NODE_TYPES = new Set(['method_signature']); -/** C++ specifiers that define a class-like type (or an enum) when they carry a body. */ -const CPP_CLASS_LIKE_SPECIFIERS = new Set(['class_specifier', 'struct_specifier', 'union_specifier', 'enum_specifier']); - /** Vue store collections whose object-literal members are the symbols an agent * looks for. Extracted as function nodes so `actions`/`mutations`/`getters` are * findable + readable (the foundation under any later dispatch-bridge synth). */ @@ -268,36 +265,6 @@ function csharpClassTypeName(node: SyntaxNode | null, source: string): string | } } -/** - * Resolve the declared identifier inside a C declarator. A `declaration`'s - * `declarator` field nests the name through `init_declarator` (with value), - * `pointer_declarator`/`array_declarator`/`parenthesized_declarator` - * wrappers (each via their own `declarator` field) down to an `identifier`. - * A `function_declarator` means the declaration is a function prototype (or a - * function-pointer var) — return null so it isn't extracted as a variable. - */ -function cDeclaratorIdentifier(node: SyntaxNode | null): SyntaxNode | null { - let cur: SyntaxNode | null = node; - let guard = 0; - while (cur && guard++ < 12) { - switch (cur.type) { - case 'identifier': - return cur; - case 'function_declarator': - return null; - case 'init_declarator': - case 'pointer_declarator': - case 'array_declarator': - case 'parenthesized_declarator': - cur = getChildByField(cur, 'declarator'); - break; - default: - return null; - } - } - return null; -} - /** First `simple_identifier` in `node`'s subtree (breadth-ish, first-found). * Swift's property name nests as `property_declaration → pattern → * bound_identifier → simple_identifier`; this resolves it (and the bound name of @@ -611,6 +578,13 @@ export class TreeSitterExtractor { private cppBraceScopes: CppBraceScopes | null = null; private cppClassScopes = new NestedIntervals(); private cppClassScopeIds = new Set(); + // C/C++: whether a class, struct, union or enum defined in the type of a + // declaration outside any body is walked (see visitNode). Not in a C++ file + // whose tree has errors and whose braces don't balance: error recovery can + // close a namespace or a class at the wrong `}`, or run a class past its + // own, and with no brace scopes to correct it the walked class would land + // in the wrong scope. + private walkDeclaredTypes = false; // C++ local function-pointer bindings, per enclosing symbol: // `auto kernel = &flash_fwd_kernel<…>;` recorded as callerId → kernel → // {flash_fwd_kernel}, so a later `kernel<<>>(params)` (or plain @@ -710,6 +684,9 @@ export class TreeSitterExtractor { if (this.language === 'cpp' && this.tree.rootNode.hasError) { this.cppBraceScopes = scanCppBraceScopes(this.source); } + this.walkDeclaredTypes = + this.language === 'c' || + (this.language === 'cpp' && (!this.tree.rootNode.hasError || this.cppBraceScopes !== null)); // Create file node representing the source file const fileNode: Node = { @@ -1477,14 +1454,17 @@ export class TreeSitterExtractor { this.extractor.variableTypes.includes(nodeType) && (!this.isInsideClassLikeNode() || this.isClassScopeConstantAssignment(node)) ) { - // In a file walked in brace scopes, a class the tree reads as the type - // of a declaration (`class X {…}` glued to the tokens after it by - // error recovery) is still a class. In a class body the children walk - // reached it; out of one, this branch skips that walk. - const declaredType = this.cppBraceScopes ? getChildByField(node, 'type') : null; - if (declaredType && CPP_CLASS_LIKE_SPECIFIERS.has(declaredType.type) && getChildByField(declaredType, 'body')) { - this.visitNode(declaredType); - } + // C/C++: a class, struct, union or enum defined in the declaration's + // type (`struct Foo { … } foo;`, `static struct { … } SPT;`) is a + // definition like one written on its own, and the variables keep their + // nodes beside it. So is a class the tree reads as a declaration's type + // in a file walked in brace scopes (`class X {…}` glued to the tokens + // after it by error recovery). In a class or function body the children + // walk reaches it; here, this branch skips that walk. Mirrored in the + // kernel (ccpp/mod.rs visit_node). + const declaredType = this.walkDeclaredTypes ? getChildByField(node, 'type') : null; + const definedType = declaredType && isClassLikeDefinition(declaredType) ? declaredType : null; + if (definedType) this.visitNode(definedType); this.extractVariable(node); // extractVariable doesn't walk every initializer shape (object literals // are deliberately skipped; Python/Ruby don't walk at all), so scan the @@ -1492,8 +1472,9 @@ export class TreeSitterExtractor { // { home: renderHome }`, `handlers = {"recv": target_cb}`. The scan halts // at nested function definitions (their bodies are walked — and // attributed — separately) and flush-time dedup absorbs any overlap with - // initializers extractVariable DOES walk. - this.scanFnRefSubtree(node, 0); + // initializers extractVariable DOES walk. A type walked above captured + // its own. + this.scanFnRefSubtree(node, 0, definedType ? new Set([definedType.id]) : undefined); skipChildren = true; // extractVariable handles children } // Swift properties inside a type. A stored instance property becomes a `field`