Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- **Indexing a mid-size project is faster.** Below 150,000 pending references the resolver used to stay on one thread for the whole run. It now boots its worker pool part-way through once at least 20,000 references are still unresolved and they would take the main thread at least 0.4 s (an explicit `CODEGRAPH_PARALLEL_RESOLVE_MIN` turns this off). Full index of JetBrains Exposed (110,854 references) 18.5 s to 9.6 s, koel 4.9 s to 4.1 s, Alamofire 1.65 s to 1.41 s (median of 3 interleaved runs each, identical graph), at 56–132% more peak memory; a 13,900-reference project never starts the pool and is unchanged.
- **More C++ links land on the right target.** `Ptr p(0);` for `using Ptr = Widget*;` declares a pointer, so it no longer counts as constructing a `Widget`. A class nested in a function-local class is as local to that function as the class around it. A class an included `.cc` file declares is seen by the file that includes it, and of two headers declaring one class an object creation takes the one it includes. A `using Base<T>::foo;` in a nested class no longer counts as the outer class's. `NULL` is an integer zero, not a `nullptr`, when an overload is chosen. A header's unnamed namespace belongs to the files that include the header, not to every file. Two annotation-macro cases read as extraction reads them: `const int FOO_BAR = 1;` keeps its name, and `n * FOO_BAR::VALUE` stays a product.
- **Fewer wrong C++ links from overloads, aliases and specializations.** `Foo x; Take(x);` links `Take(const Foo&)`, not `Take(Foo&&)`; an `int` converted to the parameter's class still takes the `&&` one. A `constexpr Widget* p` points at a non-const `Widget`, and an element read through a const container's iterator is const, so each picks the matching `const` or non-const overload. `f(a)` on an `int a[3][4]` takes `f(int (*)[4])`, not `f(int*)`. A qualified call into a namespace a macro opens links none of the overloads that fit its arguments best when several tie. A namespace alias applies from its own line on and only inside the block that declares it, and `namespace a = ::lib;` names the global `lib`. `ROCKSDB_NAMESPACE::DB` written before `#define ROCKSDB_NAMESPACE rocksdb`, or after its `#undef`, no longer resolves through the macro; an `#undef` in an `#if 0` branch changes nothing, and an `#include` after an `#undef` that defines the macro defines it again. A specialization of `a::f` no longer removes `b::f` from a call's choices, `foo.foo<long>()` reads the template arguments after the method name, and `Put<long>()` in a class that only specializes `Put<int>` links nothing instead of a base class's `Put`. Re-index C++ projects after upgrading.
- **Two C cases no longer link a call to the wrong definition.** A `static` function in another source file is reached only through an `#include` written before the call and not inside `#if 0`. A flag counts as set for a header's flag-only definition only by a real `#define` in the call's own component: not one inside a comment or raw string, not one in a nested component, and an empty build file still marks a component boundary. Re-index C projects after upgrading.
- **Syncing a changed file in a large Python project is faster.** When a Python class has no method of the name being linked (a test class that passes `Handler.__init__` as a value, say), the resolver checks every class with that method for one that derives from it. For each class whose base is a package import (`import unittest`, then `unittest.TestCase`) it parsed the whole file again, and its cache holds only a few files, so two such references took about a second each. A plain one-line `import a.b` is now read from its line; other forms still use the parse. In CPython a one-file sync takes a median 5.5 s instead of 7.7 s on Node and 5.2 s instead of 8.1 s on Bun (4 alternating runs per build, ranges not overlapping; the phase that retries unresolved references drops from 4.6 s to 2.3 s). A full index is within the run-to-run variation, and the graph is the same on CPython and Django (no edge differs).
- **The same sync resolves each Python class's bases once.** The scan above resolved the base classes of every candidate again for each reference that needed it. The bases of a class depend only on its own header and file, so they are now kept for the rest of the run. In CPython the phase that retries unresolved references drops from a median 2.29 s to 2.08 s on Node (-9%, 4 alternating runs per build, ranges not overlapping) and a one-file sync by about 3% (5.33 s to 5.15 s on the second sync); on Bun the sync time shows no clear change. A full index is within the run-to-run variation, and no edge, node or unresolved reference differs on CPython or Django.
- **Questions to the MCP server are faster in a project with no `codegraph.json`.** While ranking results, CodeGraph checked for the project's `codegraph.json` once per result path, and with no such file each check raised and caught an error. It now asks the file system without raising, so `codegraph_explore` and the other query tools spend less CPU on most projects. Editing, adding or deleting `codegraph.json` is still picked up without a restart (#491).
Expand Down
126 changes: 126 additions & 0 deletions __tests__/c-feature-macro-variants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,129 @@ describe('a C call past a definition only a flag nothing sets compiles', () => {
expect(calls('client_version')).toEqual(['include/api.h:4']);
});
});

/**
* Where a flag counts as set: an empty build file still bounds a component, a
* `#define` in a comment or a raw string sets nothing, and a nested component's
* `#define` does not set the flag for the component around it.
*/
describe('a C flag set only in a comment, a nested component or beyond an empty build file', () => {
const FILES: Record<string, string> = {
'lib/CMakeLists.txt': lines(
'add_library(lib other.c)',
),
'lib/other.c': lines(
'#define USE_FAST 1',
'int other(void) { return 0; }',
),
'lib/sub/CMakeLists.txt': '',
'lib/sub/f.h': lines(
'#ifdef USE_FAST',
'static inline int f(void) { return 1; }',
'#else',
'int f(void);',
'#endif',
),
'lib/sub/f.c': lines(
'int f(void) { return 0; }',
),
'lib/sub/use.c': lines(
'#include "f.h"',
'int g(void) { return f(); }',
),
'doc/CMakeLists.txt': lines(
'add_library(doc f.c)',
),
'doc/notes.c': lines(
'/*',
'#define USE_NOTE 1',
'*/',
'int notes(void) { return 0; }',
),
'doc/raw.cc': lines(
'const char* text = R"(',
'#define USE_RAW 1',
')";',
),
'doc/f.h': lines(
'#ifdef USE_NOTE',
'static inline int nt(void) { return 1; }',
'#else',
'int nt(void);',
'#endif',
'#ifdef USE_RAW',
'static inline int rw(void) { return 1; }',
'#else',
'int rw(void);',
'#endif',
),
'doc/f.c': lines(
'int nt(void) { return 0; }',
'int rw(void) { return 0; }',
),
'doc/use.c': lines(
'#include "f.h"',
'int note_use(void) { return nt(); }',
'int raw_use(void) { return rw(); }',
),
'outer/CMakeLists.txt': lines(
'add_library(outer f.c)',
),
'outer/plugin/CMakeLists.txt': lines(
'add_library(plugin p.c)',
),
'outer/plugin/p.c': lines(
'#define USE_PLUGIN 1',
'int p(void) { return 0; }',
),
'outer/f.h': lines(
'#ifdef USE_PLUGIN',
'static inline int nf(void) { return 1; }',
'#else',
'int nf(void);',
'#endif',
),
'outer/f.c': lines(
'int nf(void) { return 0; }',
),
'outer/use.c': lines(
'#include "f.h"',
'int ng(void) { return nf(); }',
),
};

let root: string;
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-c-feature-components-'));
for (const [file, text] of Object.entries(FILES)) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), text);
}
cg = await CodeGraph.init(root, { index: true });
}, 120_000);

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** What a function or method calls, as `qualifiedName@file:line`. */
const callees = (caller: string): string[] => {
const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller);
expect(fn, caller).toBeDefined();
return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort();
};

it('an empty build file bounds a component', () => {
expect(callees('g')).toEqual(['f@lib/sub/f.c:1']);
});
it('a `#define` in a comment or a raw string defines nothing', () => {
expect(callees('note_use')).toEqual(['nt@doc/f.c:1']);
expect(callees('raw_use')).toEqual(['rw@doc/f.c:2']);
});
it("a nested component's `#define` is not the outer component's", () => {
expect(callees('ng')).toEqual(['nf@outer/f.c:1']);
});
});
59 changes: 59 additions & 0 deletions __tests__/c-static-call-rescue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,62 @@ describe("a C call reaches the function its file sees, not another file's static
expect(cg.getFileDependencies('src/sort.c')).not.toContain('deps/jemalloc/test/include/test/math.h');
});
});

/**
* A `static` function another source file defines is visible through an
* `#include` of that file only where the include is compiled and written before
* the call: not under `#if 0`, and not after the call.
*/
describe("a C call sees another source file's static only through an include before it", () => {
const FILES: Record<string, string> = {
'deps/dict.c': lines(
'static unsigned dictHash(const char* k) { return 0; }',
),
'inactive.c': lines(
'#if 0',
'#include "deps/dict.c"',
'#endif',
'unsigned h1(void) { return dictHash("a"); }',
),
'later.c': lines(
'unsigned h2(void) { return dictHash("a"); }',
'#include "deps/dict.c"',
),
'active.c': lines(
'#include "deps/dict.c"',
'unsigned h3(void) { return dictHash("a"); }',
),
};

let root: string;
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-c-static-include-order-'));
for (const [file, text] of Object.entries(FILES)) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), text);
}
cg = await CodeGraph.init(root, { index: true });
}, 120_000);

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** What a function or method calls, as `qualifiedName@file:line`. */
const callees = (caller: string): string[] => {
const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller);
expect(fn, caller).toBeDefined();
return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort();
};

it('not through an include under `#if 0` or after the call', () => {
expect(callees('h1')).toEqual([]);
expect(callees('h2')).toEqual([]);
});
it('through an include before the call', () => {
expect(callees('h3')).toEqual(['dictHash@deps/dict.c:1']);
});
});
57 changes: 57 additions & 0 deletions __tests__/cpp-free-function-overloads.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,60 @@ describe('a C++ call picks the free-function overload its arguments fit', () =>
expect(calls('WarnCode')).toEqual(['lg::Warn@log.cc:4']);
});
});

/**
* Overloads the argument's type fits equally well are a tie even when another
* overload fits it worse: `f(short)` beside `f(long)`, `f(int)` and `f(char)`
* is not `f(long)` because it is declared first. The overloads sit in a
* namespace a macro opens, where only the qualified-name step can place the
* call; in a namespace written out, the name step picks among overloads the
* arguments fit alike by where they are, as above.
*/
describe('a C++ call whose best-fitting overloads tie', () => {
const FILES: Record<string, string> = {
'real.h': lines(
'#define REAL_BEGIN namespace real {',
'#define REAL_END }',
'REAL_BEGIN',
'void f(long v) {}',
'void f(int v) {}',
'void f(char v) {}',
'REAL_END',
),
'use.cc': lines(
'#include "real.h"',
'namespace r = real;',
'void Direct() { short s = 1; real::f(s); }',
'void ThroughAlias() { short s = 1; r::f(s); }',
),
};

let root: string;
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-overload-top-tie-'));
for (const [file, text] of Object.entries(FILES)) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), text);
}
cg = await CodeGraph.init(root, { index: true });
}, 120_000);

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** What a function or method calls, as `qualifiedName@file:line`. */
const callees = (caller: string): string[] => {
const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller);
expect(fn, caller).toBeDefined();
return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort();
};

it('links none of the tied overloads', () => {
expect(callees('Direct')).toEqual([]);
expect(callees('ThroughAlias')).toEqual([]);
});
});
67 changes: 66 additions & 1 deletion __tests__/cpp-global-alias-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* `arena_.Fuse(other.arena_)` fell back to the type's last name, landing on
* the calling method `hpb::Arena::Fuse`.
*/
import { describe, it, expect, afterEach } from 'vitest';
import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
Expand Down Expand Up @@ -214,3 +214,68 @@ describe('other C++ names written from the global scope', () => {
}
});
});

/**
* A namespace alias applies where C++ declares it: from its declaration on, and
* only inside the function that declares it. An alias of `::lib` names the global
* `lib`, not a `lib` nested in the namespace around the code.
*/
describe('a C++ namespace alias applies where it is declared', () => {
const FILES: Record<string, string> = {
'lib.h': lines(
'namespace lib { struct Type { void Run() {} }; }',
),
'a.cc': lines(
'#include "lib.h"',
'void Setup() {',
' namespace x = lib;',
'}',
'void UseLocal(x::Type t) { t.Run(); }',
'void UseBefore(y::Type t) { t.Run(); }',
'namespace y = lib;',
'namespace z = lib;',
'void UseAfter(z::Type t) { t.Run(); }',
),
'scope.h': lines(
'namespace glob { struct Type { void Run() {} }; }',
'namespace scope {',
'namespace glob { struct Type { void Run() {} }; }',
'namespace a = ::glob;',
'void Use(a::Type t) { t.Run(); }',
'}',
),
};

let root: string;
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-alias-scope-'));
for (const [file, text] of Object.entries(FILES)) {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), text);
}
cg = await CodeGraph.init(root, { index: true });
}, 120_000);

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** What a function or method calls, as `qualifiedName@file:line`. */
const callees = (caller: string): string[] => {
const fn = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n) => n.qualifiedName === caller);
expect(fn, caller).toBeDefined();
return cg.getCallees(fn!.id).filter((c) => c.edge.kind === 'calls').map((c) => `${c.node.qualifiedName}@${c.node.filePath}:${c.node.startLine}`).sort();
};

it('not outside the function that declares it, nor before its declaration', () => {
expect(callees('UseLocal')).toEqual([]);
expect(callees('UseBefore')).toEqual([]);
expect(callees('UseAfter')).toEqual(['lib::Type::Run@lib.h:1']);
});
it('an alias of `::glob` names the global namespace', () => {
expect(callees('scope::Use')).toEqual(['glob::Type::Run@scope.h:1']);
});
});
Loading