Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fedda29
fix(cpp): a declaration with a qualifier or annotation around its nam…
colbymchenry Oct 13, 2026
bb817d4
fix(cpp): structured bindings and function wrappers give their types …
colbymchenry Oct 13, 2026
8d91806
fix(cpp): a bare call in a gtest TEST_F or TEST_P body reaches the fi…
colbymchenry Oct 13, 2026
0cea43e
fix(cpp): `->` on an absl::StatusOr reaches the class it holds; a usi…
colbymchenry Oct 13, 2026
61cb418
fix(cpp): a call or type name never links to what only another source…
colbymchenry Oct 13, 2026
a6ceb04
fix(cpp): a call to an overloaded method links to the overload its ar…
colbymchenry Oct 13, 2026
34dd3d2
fix(cpp): a nested class defined out of line is indexed under its nam…
colbymchenry Oct 13, 2026
6df4827
fix(cpp): a call through a class reaches that class's own or inherite…
colbymchenry Oct 14, 2026
754cc45
fix(cpp): an alias naming a class through a derived class leads to th…
colbymchenry Oct 14, 2026
d94a92e
fix(cpp): a gtest TEST_F or TEST_P body finds its fixture's types, an…
colbymchenry Oct 14, 2026
d4b9c2b
fix(cpp): a line starting with `*` is read as code, and block comment…
colbymchenry Oct 14, 2026
2428621
fix(cpp): a comparison or shift in a call's arguments opens no templa…
colbymchenry Oct 14, 2026
8cba7e5
fix(cpp): a nullptr argument picks the std::nullptr_t overload, and n…
colbymchenry Oct 14, 2026
e03ff67
fix(cpp): a receiver declared over more than one line has its type re…
colbymchenry Oct 14, 2026
1753472
fix(cpp): a call of a member template with explicit template argument…
colbymchenry Oct 14, 2026
a4a3c40
merge: upstream/main through 1753472a (15 C++ resolution fixes)
bompus Oct 14, 2026
dc2289f
fix(cpp): link no call or type name to what only another source file …
bompus Oct 14, 2026
1b30d19
fix(cpp): a call through a class reaches its own or inherited method …
bompus Oct 14, 2026
20bdbed
fix(cpp): read receivers through annotations, bindings and wrappers, …
bompus Oct 14, 2026
72e973a
fix(cpp): a call of a member template with explicit template argument…
bompus Oct 14, 2026
884719f
fix(cpp): a construction makes the class C++ lookup finds for the typ…
bompus Oct 14, 2026
b9510f0
fix(cpp): read a receiver declared as a pointer to a class template o…
bompus Oct 14, 2026
f92479d
fix(cpp): blank block comments and string contents before reading dec…
bompus Oct 14, 2026
7e5e1ba
test(cpp): keep the fork's no-guess result for receivers whose type c…
bompus Oct 14, 2026
b5d1647
test(golden): re-baseline for C++ constructions placed by lookup
bompus Oct 14, 2026
9d5b66d
fix(cpp): place a construction in a test tree's header only from that…
bompus Oct 14, 2026
c876f90
docs: record where the fork's C++ resolution differs from upstream's …
bompus Oct 14, 2026
125841c
merge: bring fork/consolidated ce0c73db into the upstream 1753472a re…
bompus Oct 14, 2026
666cd74
fix(cpp): answer review findings on the upstream reconcile
bompus Oct 14, 2026
89a7907
test(cpp): use a valid call in the deleted-overload fixture
bompus Oct 14, 2026
baaed0d
perf(cpp): keep the visible-file set of one C++ file at a time
bompus Oct 14, 2026
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
17 changes: 17 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.

## About this fork

This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`df3ce972`](https://github.com/colbymchenry/codegraph/commit/df3ce972) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests.
This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains upstream `main` through [`1753472a`](https://github.com/colbymchenry/codegraph/commit/1753472a) (after v1.6.2) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests.

The fork publishes no releases. The install scripts, npm package, badges and `codegraph upgrade` further down this page install **upstream's** releases. To run the fork, build it from source (below).

Expand Down Expand Up @@ -154,6 +154,7 @@ Call resolution the fork adds:
- C++ `->` calls on a `std::unique_ptr`, `std::shared_ptr` or `std::optional` reach the type it holds, looked up through the caller's enclosing classes and their bases, its namespaces, `using` directives and aliases. A held type the project does not declare leaves the call unresolved, as a plain pointer to one does; upstream guesses a callee from the receiver's name there. A partial class template specialization is indexed as an anonymous class, so a call to a member only it declares stays unresolved.
- C++ calls on an `auto` local, on a chained call and on a data member of the caller's class (one a base class declares, or a gtest `TEST_F` / `TEST_P` fixture's) take the type its declaration writes, as upstream's do. Where that type cannot be read (a template parameter's, a `std::shared_ptr<T>` of one, a trait of one, what a function template returns), the call stays unlinked, as does a local of a type the file does not declare; upstream guesses a callee from the receiver's name at 0.7. A chain of up to four calls is read from its declarations, where upstream reads its first inner call; a longer one is looked up by the last name, as before.
- C++ namespace aliases and declarations in macro-opened namespaces are looked up only in the caller's include closure; upstream pools them across all files.
- C++ object creation sees a header under a test directory, such as nlohmann/json's old-release copy in `tests/abi/`, only from inside that directory's tree or through an `#include`; upstream sees every header.
- C# namespace `using` directives and `using` aliases apply only inside their enclosing namespace, not to sibling namespaces in the same file (`using static` is still file-wide, as upstream).
- A Java field declared with a qualified type (`outside.Repository`) keeps its qualifier, so calls through it never resolve to an unrelated project class with the same simple name.
- Python `super().method()` resolves along the class's C3 method resolution order, starting after the class itself. Upstream resolves it like `self.method()` and drops the link only when it lands on the calling method.
Expand Down Expand Up @@ -188,7 +189,7 @@ Call resolution the fork adds:

### Measured results

Upstream `main` at `34dd3d2f` against the fork at `f045e5f7`, each run on Node.js 24.21.0 and on Bun 1.4.3. The fork contains upstream through `df3ce972`; upstream's seven later commits are C++ fixes, which touch these corpora only through CPython's C files. Measured 2026-10-13 on a 16-vCPU WSL2 host over seven corpora, with the arms in mirrored order; index figures are the median of two runs and sync figures the median of four. Upstream refuses to start on Bun, which reports itself as Node 26, because upstream blocks Node 25 and newer. With `CODEGRAPH_ALLOW_UNSAFE_NODE=1` it runs; those numbers, the method and the graph sizes are in [`docs/benchmarks/fork-vs-upstream-node-bun-2026-10-13.md`](docs/benchmarks/fork-vs-upstream-node-bun-2026-10-13.md), which also compares this run with the 2026-10-12 one. The scripts that produced them are in [`scripts/benchmarks/runtime/`](scripts/benchmarks/runtime/).
Upstream `main` at `34dd3d2f` against the fork at `f045e5f7`, each run on Node.js 24.21.0 and on Bun 1.4.3. The fork at `f045e5f7` contained upstream through `df3ce972`; upstream's seven later commits are C++ fixes, which touch these corpora only through CPython's C files. Measured 2026-10-13 on a 16-vCPU WSL2 host over seven corpora, with the arms in mirrored order; index figures are the median of two runs and sync figures the median of four. Upstream refuses to start on Bun, which reports itself as Node 26, because upstream blocks Node 25 and newer. With `CODEGRAPH_ALLOW_UNSAFE_NODE=1` it runs; those numbers, the method and the graph sizes are in [`docs/benchmarks/fork-vs-upstream-node-bun-2026-10-13.md`](docs/benchmarks/fork-vs-upstream-node-bun-2026-10-13.md), which also compares this run with the 2026-10-12 one. The scripts that produced them are in [`scripts/benchmarks/runtime/`](scripts/benchmarks/runtime/).

![Fork against upstream CodeGraph on n8n and the command line: the fork indexes n8n 1.2 times faster, syncs an edited file 3.0 times faster, answers a warm question 3.3 times faster and uses 32% less memory while indexing. The fork is better in 60 of 76 comparisons, about the same in 4 and worse in 12.](assets/fork-vs-upstream-performance.svg?v=2)

Expand Down
217 changes: 217 additions & 0 deletions __tests__/cpp-absl-holder-receiver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* A C++ `->` call on an `absl::StatusOr`, an `absl::optional` or an
* `absl::NoDestructor` calls a member of the type it holds, as one on a
* `std::unique_ptr` or `std::optional` does; and a bare call in a class
* template finds a member its dependent base declares when the class brings
* the name in with a using-declaration.
*
* In protocolbuffers/protobuf, `absl::StatusOr<FeatureResolver> resolver =
* FeatureResolver::Create(…); resolver->MergeFeatures(…)` was guessed by the
* method's name: the declared type's last name, `StatusOr`, is no class of
* the project's. In facebook/rocksdb's typed_cache.h, `using
* BasicTypedCacheHelperFns<TValue>::DownCastValue;` makes a bare
* `DownCastValue(v)` the base's, which C++ doesn't look in otherwise: the
* base depends on the class's template parameters.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';

const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});

async function indexed(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-absl-held-'));
roots.push(root);
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);
}
return CodeGraph.init(root, { index: true });
}

function callable(cg: CodeGraph, qualifiedName: string): Node {
const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName);
if (!node) throw new Error(`no function or method ${qualifiedName}`);
return node;
}

/** `calls` callees of a function or method, as `qualifiedName @confidence`. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(callable(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => `${r.node.qualifiedName} @${(r.edge.metadata as { confidence?: number } | undefined)?.confidence}`)
.sort();
}

const lines = (...text: string[]): string => [...text, ''].join('\n');

// Two classes with a method of each name, so a guess by the name can miss.
const RESOLVERS = lines(
'namespace google {',
'namespace protobuf {',
'class FeatureResolver {',
' public:',
' int MergeFeatures(int parent, int child) const { return parent; }',
'};',
'class FeatureSetResolver {',
' public:',
' int MergeFeatures(int parent, int child) const { return child; }',
'};',
'} // namespace protobuf',
'} // namespace google',
);

describe('C++ absl holders and using-declared members of dependent bases', () => {
it("is the held class's method for `->` on an absl::StatusOr, absl::optional or absl::NoDestructor", async () => {
const cg = await indexed({
'src/feature_resolver.h': RESOLVERS,
'src/feature_resolver_test.cc': lines(
'#include "src/feature_resolver.h"',
'namespace google {',
'namespace protobuf {',
'int Merge(absl::StatusOr<FeatureResolver> resolver) {',
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
' return resolver->MergeFeatures(1, 2);',
'}',
'int MergeOptional(absl::optional<FeatureResolver> resolver) {',
' return resolver->MergeFeatures(1, 2);',
'}',
'int MergeStatic() {',
' static absl::NoDestructor<FeatureResolver> resolver;',
' return resolver->MergeFeatures(1, 2);',
'}',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
for (const caller of ['Merge', 'MergeOptional', 'MergeStatic']) {
expect(calls(cg, `google::protobuf::${caller}`)).toEqual(['google::protobuf::FeatureResolver::MergeFeatures @0.9']);
}
} finally {
cg.close();
}
});

it("is not a project's own StatusOr, nor what a StatusOr of a pointer holds", async () => {
const cg = await indexed({
'src/feature_resolver.h': RESOLVERS,
// protobuf's hpb declares a `StatusOr` of its own.
'hpb/status.h': lines(
'namespace absl {',
'template <typename T>',
'class StatusOr {',
' public:',
' T value() const;',
'};',
'} // namespace absl',
),
'src/use.cc': lines(
'#include "src/feature_resolver.h"',
'#include "hpb/status.h"',
'namespace google {',
'namespace protobuf {',
'int Merge(absl::StatusOr<FeatureResolver> resolver) {',
' return resolver->MergeFeatures(1, 2);',
'}',
'int MergeThroughPointer(absl::StatusOr<FeatureResolver*> resolver) {',
' return resolver->MergeFeatures(1, 2);',
'}',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
expect(calls(cg, 'google::protobuf::Merge')).toEqual([]);
expect(calls(cg, 'google::protobuf::MergeThroughPointer')).toEqual([]);
} finally {
cg.close();
}
});

it('is the member of the base the using-declaration names, of two dependent bases', async () => {
const cg = await indexed({
'util/runner.h': lines(
'namespace rocksdb {',
'template <class T>',
'class Second {',
' public:',
' void Run() {}',
'};',
'template <class T>',
'class First {',
' public:',
' void Run() {}',
'};',
'template <class T>',
'class Both : public Second<T>, public First<T> {',
' public:',
' using First<T>::Run;',
' void Go() { Run(); }',
'};',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::Both::Go')).toEqual(['rocksdb::First::Run @0.9']);
} finally {
cg.close();
}
});

it('is the dependent base member a using-declaration brings in, for a bare call and a data member', async () => {
const cg = await indexed({
// (The base in a header of its own, where the derived class's file doesn't write its member.)
'cache/typed_cache_base.h': lines(
'namespace rocksdb {',
'class Cache {',
' public:',
' int Insert(int key) { return key; }',
'};',
'class Other {',
' public:',
' int Insert(int key) { return 0; }',
'};',
'template <class TValue>',
'class BasicTypedCacheHelperFns {',
' public:',
' static TValue* DownCastValue(void* v) { return nullptr; }',
' protected:',
' Cache* cache_;',
'};',
'} // namespace rocksdb',
),
'cache/typed_cache.h': lines(
'#include "cache/typed_cache_base.h"',
'namespace rocksdb {',
'inline int* DownCastValue(void* v) { return nullptr; }',
'template <class TValue, class TCreateContext>',
'class FullTypedCacheHelperFns : public BasicTypedCacheHelperFns<TValue> {',
' public:',
' using BasicTypedCacheHelperFns<TValue>::DownCastValue;',
' using BasicTypedCacheHelperFns<TValue>::cache_;',
' static void Delete(void* v) {',
' DownCastValue(v);',
' }',
' int Store(int key) {',
' return cache_->Insert(key);',
' }',
'};',
'} // namespace rocksdb',
),
});
try {
// (Without the using-declarations C++ finds the namespace's `DownCastValue`, as before.)
expect(calls(cg, 'rocksdb::FullTypedCacheHelperFns::Delete')).toEqual(['rocksdb::BasicTypedCacheHelperFns::DownCastValue @0.9']);
expect(calls(cg, 'rocksdb::FullTypedCacheHelperFns::Store')).toEqual(['rocksdb::Cache::Insert @0.9']);
} finally {
cg.close();
}
});
});
151 changes: 151 additions & 0 deletions __tests__/cpp-alias-inherited-member.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* A C++ alias can name a class one of a class's bases declares:
* protocolbuffers/protobuf's conformance suites write `using
* ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;`,
* a class their base `ConformanceTestSuite` declares. Following the alias
* looked the class up only by the qualified name as spelled, which no class
* has, so constructions through the alias stopped at the alias and calls on
* such a variable lost their class. The aliased type is now looked up the way
* a declared type is, a class's members through its bases too.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});

async function indexed(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-alias-base-'));
roots.push(root);
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);
}
return CodeGraph.init(root, { index: true });
}

const lines = (...text: string[]): string => [...text, ''].join('\n');

/** What a function's or method's `kind` edges reach, as `qualifiedName @confidence`. */
function targets(cg: CodeGraph, qualifiedName: string, kind: 'calls' | 'instantiates'): string[] {
const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!;
return cg.getOutgoingEdgesFrom([from.id], [kind])
.map((e) => `${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`)
.sort();
}

const NS = 'google::protobuf::conformance';
const SUITE = lines(
'namespace google {',
'namespace protobuf {',
'namespace conformance {',
'class ConformanceTestSuite {',
' public:',
' class ConformanceRequestSetting {',
' public:',
' ConformanceRequestSetting(int level, const char* name) {}',
' const char* GetTestName() const { return ""; }',
' };',
'};',
'} // namespace conformance',
'} // namespace protobuf',
'} // namespace google',
);

describe('C++ aliases of a class a base declares', () => {
it('reach the class through an alias spelled through the derived class', async () => {
const cg = await indexed({
'conformance/conformance_test.h': SUITE,
'conformance/binary_json_conformance_suite.h': lines(
'#include "conformance/conformance_test.h"',
'namespace google {',
'namespace protobuf {',
'namespace conformance {',
'class BinaryAndJsonConformanceSuite : public ConformanceTestSuite {};',
'template <typename MessageType>',
'class BinaryAndJsonConformanceSuiteImpl {',
' public:',
' void RunTest();',
' private:',
' using ConformanceRequestSetting =',
' BinaryAndJsonConformanceSuite::ConformanceRequestSetting;',
'};',
'} // namespace conformance',
'} // namespace protobuf',
'} // namespace google',
),
'conformance/binary_json_conformance_suite.cc': lines(
'#include "conformance/binary_json_conformance_suite.h"',
'namespace google {',
'namespace protobuf {',
'namespace conformance {',
'template <typename MessageType>',
'void BinaryAndJsonConformanceSuiteImpl<MessageType>::RunTest() {',
' ConformanceRequestSetting setting(1, "x");',
' setting.GetTestName();',
'}',
'// An alias the function declares itself is not indexed: read from the function.',
'void RunLocal() {',
' using ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;',
' ConformanceRequestSetting local(3, "w");',
' local.GetTestName();',
'}',
'} // namespace conformance',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
const run = `${NS}::BinaryAndJsonConformanceSuiteImpl::RunTest`;
expect(targets(cg, run, 'instantiates')).toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting @0.9`]);
expect(targets(cg, run, 'calls').filter((t) => t.includes('GetTestName')))
.toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting::GetTestName @0.9`]);
expect(targets(cg, `${NS}::RunLocal`, 'instantiates')).toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting @0.9`]);
expect(targets(cg, `${NS}::RunLocal`, 'calls').filter((t) => t.includes('GetTestName')))
.toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting::GetTestName @0.9`]);
} finally {
cg.close();
}
});

it("reach the class through an alias a derived class writes with the base's name alone", async () => {
const cg = await indexed({
'conformance/conformance_test.h': SUITE,
'conformance/text_format_suite.h': lines(
'#include <string>',
'#include "conformance/conformance_test.h"',
'namespace google {',
'namespace protobuf {',
'namespace conformance {',
'class TextFormatSuite : public ConformanceTestSuite {',
' public:',
' using Setting = ConformanceRequestSetting;',
' using Name = std::string;',
' void Run() {',
' Setting setting(2, "y");',
' setting.GetTestName();',
' Name name("z");',
' }',
'};',
'} // namespace conformance',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
const run = `${NS}::TextFormatSuite::Run`;
// (An alias of a type from outside the project still makes the alias.)
expect(targets(cg, run, 'instantiates'))
.toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting @0.9`, `${NS}::TextFormatSuite::Name @0.9`]);
expect(targets(cg, run, 'calls').filter((t) => t.includes('GetTestName')))
.toEqual([`${NS}::ConformanceTestSuite::ConformanceRequestSetting::GetTestName @0.9`]);
} finally {
cg.close();
}
});
});
Loading