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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Fixes

- **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.
- **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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +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++ 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. A class declared in an included `.cc` file is the includer's too, and of two headers declaring one class the creation takes the one in its include closure.
- 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
275 changes: 275 additions & 0 deletions __tests__/cpp-resolver-edge-cases.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
/**
* Edge cases of the C++ resolver found reviewing the upstream reconcile (#497):
*
* - `Ptr p(0)` for `using Ptr = Widget*;` declares a pointer; it constructs no `Widget`.
* - A class nested in a function-local class is as local to the function as that class.
* - A class an included `.cc` file declares is the includer's.
* - A `using Base<T>::foo;` in a nested class is not the outer class's.
* - A header's unnamed namespace is the includer's, not every file's.
* - Of two headers declaring one class, an object creation takes the one it includes.
* - `NULL` is an integer zero; a string literal converts to `const void*` ahead of a class.
* - Annotation blanking leaves `const int FOO_BAR = 1;` and `n * FOO_BAR::VALUE` alone.
*/
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 { blankCppDeclaratorAttributeMacros, blankCppPointerAnnotationMacros } from '../src/extraction/languages/c-cpp';

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-edge-'));
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 edges of `kind` reach at `line`, as `target:its first line @confidence`. */
function reached(cg: CodeGraph, qualifiedName: string, kind: 'calls' | 'instantiates', line?: number): string[] {
const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!;
return cg.getOutgoingEdgesFrom([from.id], [kind])
.filter((e) => line === undefined || e.line === line)
.map((e) => {
const t = cg.getNode(e.target)!;
return `${t.qualifiedName}:${t.startLine} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`;
})
.sort();
}

describe('C++ object creation', () => {
it('constructs no class for a pointer alias of it', async () => {
const cg = await indexed({
'widget.h': lines(
'class Widget {',
' public:',
' Widget(int n) {}',
'};',
'using Ptr = Widget*;',
'void Run() {',
' Ptr p(0);',
' Widget w(1);',
'}',
),
});
try {
expect(reached(cg, 'Run', 'instantiates', 7)).toEqual([]);
expect(reached(cg, 'Run', 'instantiates', 8)).toEqual(['Widget:1 @0.9']);
} finally {
cg.close();
}
});

it('constructs no class for a chain of pointer aliases (a global-qualified one too), and the class for an alias with a comment', async () => {
const cg = await indexed({
'widget.h': lines(
'class Widget {',
' public:',
' Widget(int n) {}',
'};',
'using Ptr = Widget*;',
'using Ptr2 = Ptr;',
'using Global = ::Ptr;',
'using Plain = Widget /* an object * not a pointer */;',
'void Run() {',
' Ptr2 q(0);',
' Plain w(1);',
' Global g(0);',
'}',
),
});
try {
expect(reached(cg, 'Run', 'instantiates', 10)).toEqual([]);
expect(reached(cg, 'Run', 'instantiates', 11)).toEqual(['Widget:1 @0.9']);
expect(reached(cg, 'Run', 'instantiates', 12)).toEqual([]);
} finally {
cg.close();
}
});

it("does not see a class nested in a function's local class from another function", async () => {
const cg = await indexed({
'a_test.cc': lines(
'namespace ns {',
'struct Local { struct Inner { Inner(int n) {} }; };',
'TEST_F(A, B) {',
' struct Local { struct Inner { Inner(int n) {} }; };',
' Local::Inner own(1);',
'}',
'TEST_F(A, C) {',
' Local::Inner other(1);',
'}',
'} // namespace ns',
),
});
try {
const made = cg.getNodesByKind('function').filter((n) => n.name === 'TEST_F').sort((a, b) => a.startLine - b.startLine)
.map((fn) => cg.getOutgoingEdgesFrom([fn.id], ['instantiates']).map((e) => `${fn.startLine} -> ${cg.getNode(e.target)?.startLine}`).sort());
// (Each `Inner` is recorded by the name before its first `::`, `Local`.)
expect(made).toEqual([['3 -> 4'], ['7 -> 2']]);
} finally {
cg.close();
}
});

it('constructs a class an included source file declares', async () => {
const cg = await indexed({
'engine.cc': lines('class Engine {', ' public:', ' Engine(int n) {}', '};'),
'engine_test.cc': lines('#include "engine.cc"', 'void Run() {', ' Engine e(1);', '}'),
});
try {
expect(reached(cg, 'Run', 'instantiates')).toEqual(['Engine:1 @0.9']);
} finally {
cg.close();
}
});

it('takes, of two headers declaring one class, the one it includes', async () => {
const widget = (n: number) => lines('namespace ns {', 'class Widget {', ' public:', ` Widget(int n) {} // ${n}`, '};', '} // namespace ns');
const cg = await indexed({
'one/widget.h': widget(1),
'two/widget.h': widget(2),
'one/use.cc': lines('#include "two/widget.h"', 'void Run() {', ' ns::Widget w(1);', '}'),
});
try {
const run = [...cg.getNodesByKind('function')].find((n) => n.qualifiedName === 'Run')!;
const files = cg.getOutgoingEdgesFrom([run.id], ['instantiates']).map((e) => cg.getNode(e.target)?.filePath);
expect(files).toEqual(['two/widget.h']);
} finally {
cg.close();
}
});
});

describe('C++ calls', () => {
it("does not take a nested class's using-declaration for the outer class's", async () => {
const cg = await indexed({
'outer.h': lines(
'template <typename T>',
'class Base {',
' public:',
' void foo() {}',
'};',
'void foo() {}',
'template <typename T>',
'class Outer : public Base<T> {',
' public:',
' struct Nested : Base<T> {',
' using Base<T>::foo;',
' };',
' void run() {',
' foo();',
' }',
'};',
),
});
try {
// (The free function, at the confidence of a name a dependent base might also give.)
expect(reached(cg, 'Outer::run', 'calls', 14).map((r) => r.split(' ')[0])).toEqual(['foo:6']);
} finally {
cg.close();
}
});

it("reads a class's using-declaration when its first line also opens a namespace", async () => {
const cg = await indexed({
'outer.h': lines(
'template <typename T>',
'class Base {',
' public:',
' void foo() {}',
'};',
'void foo() {}',
'namespace ns { template <typename T> class Outer : public Base<T> {',
' public:',
' using Base<T>::foo;',
' void run() {',
' foo();',
' }',
'}; }',
),
});
try {
expect(reached(cg, 'ns::Outer::run', 'calls', 11).map((r) => r.split(' ')[0])).toEqual(['Base::foo:4']);
} finally {
cg.close();
}
});

it('takes NULL for an integer zero, not for a nullptr', async () => {
const cg = await indexed({
'reset.h': lines(
'#include <cstddef>',
'class Holder {',
' public:',
' void Reset(std::nullptr_t) {}',
' void Reset(int n) {}',
' void Run() { Reset(NULL); }',
'};',
),
});
try {
expect(reached(cg, 'Holder::Run', 'calls', 6)).toEqual(['Holder::Reset:5 @0.9']);
} finally {
cg.close();
}
});

it("takes a string literal for a `const void*` ahead of a std::string it converts to", async () => {
const cg = await indexed({
'take.h': lines(
'void Take(const std::string& text) {}',
'void Take(const void* data) {}',
'void Run() { Take("x"); }',
),
});
try {
expect(reached(cg, 'Run', 'calls', 3).map((r) => r.split(' ')[0])).toEqual(['Take:2']);
} finally {
cg.close();
}
});

it("is not what a header's unnamed namespace declares, named by a file that does not include it", async () => {
const cg = await indexed({
'conformance/matchers.h': lines('namespace conformance {', 'namespace {', 'int RawPayload(int response) { return response; }', '} // namespace', '} // namespace conformance'),
'conformance/matchers_test.cc': lines('namespace conformance {', 'int Describe() {', ' return RawPayload(1);', '}', '} // namespace conformance'),
'conformance/uses_matchers.cc': lines('#include "matchers.h"', 'namespace conformance {', 'int Check() {', ' return RawPayload(1);', '}', '} // namespace conformance'),
});
try {
const filesOf = (name: string) => {
const from = cg.getNodesByKind('function').find((n) => n.qualifiedName === name)!;
return cg.getOutgoingEdgesFrom([from.id], ['calls']).map((e) => cg.getNode(e.target)?.filePath);
};
expect(filesOf('conformance::Describe')).toEqual([]);
expect(filesOf('conformance::Check')).toEqual(['conformance/matchers.h']);
} finally {
cg.close();
}
});
});

describe('C++ annotation blanking', () => {
it('leaves a constant named in capitals alone', () => {
expect(blankCppDeclaratorAttributeMacros('const int FOO_BAR = 1;')).toBe('const int FOO_BAR = 1;');
expect(blankCppDeclaratorAttributeMacros('static constexpr unsigned MAX_LEN = 4;')).toBe('static constexpr unsigned MAX_LEN = 4;');
});

it('blanks a macro after a name that follows a const-qualified type', () => {
expect(blankCppDeclaratorAttributeMacros('int const value_ GUARDED_BY(mu_);')).toBe('int const value_ ' + ' '.repeat('GUARDED_BY(mu_)'.length) + ';');
expect(blankCppDeclaratorAttributeMacros('Foo const volatile value_ GUARDED_BY(mu_);')).toBe('Foo const volatile value_ ' + ' '.repeat('GUARDED_BY(mu_)'.length) + ';');
});

it('leaves a qualified name after a product alone', () => {
expect(blankCppPointerAnnotationMacros('consume(n * FOO_BAR::VALUE);')).toBe('consume(n * FOO_BAR::VALUE);');
});
});
3 changes: 3 additions & 0 deletions codegraph-kernel/src/resolve/cpp_aliases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ pub(super) fn type_segments(raw: &str) -> Option<Vec<String>> {
(!parts.is_empty() && parts.iter().all(|p| re!(r"^[A-Za-z_]\w*$").is_match(p))).then_some(parts)
}

/// Whether `raw` is a pointer or a reference to something (`Widget*`, `const Widget&`).
pub(super) fn indirect_type(raw: &str) -> bool { pointer_type(raw) || reference_type(raw) }
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

fn pointer_type(raw: &str) -> bool { indirection_type(raw, '*') }

fn reference_type(raw: &str) -> bool { indirection_type(raw, '&') }
Expand Down
Loading
Loading