Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Rust, a type alias, struct, enum or function declared without `pub` now links only from its own module and the modules inside it, and a name a `use` brings in from one of your workspace's crates, like `use tokio_test::task::spawn` or `use crate::error::Error`, links only to an item of that crate or of a crate it depends on. Before, a file's `use crate::Result` could link its `Result<…>` types to another module's private `type Result`, as in dioxus's CLI, and a call to an imported function could link to another crate's function of the same name, such as tokio's test `spawn(…)` calls to one in its examples, or to nothing because a private function of that name was in the way. Re-index Rust projects after upgrading.
- In Rust, a name written on its own, like an `Err(…)` call or a `Response` return type, or through a module, like `io::Error` or `use crate::de::Error`, no longer links to a `type` or `const` declared inside an `impl`, such as a `FromStr` impl's `type Err` or a `Service` impl's `type Response`: only a path like `Self::Err` names those, and they are now listed under their type, as `ValueHint::Err`. Before, serde's and ripgrep's `Ok(…)` and `Err(…)` calls linked to a serializer's or parser's `type Ok` or `type Err`, axum's handlers' `Response` to a `Service` impl's, and ripgrep's `impl Captures for RegexCaptures` to another impl's `type Captures` instead of the `Captures` trait. Re-index Rust projects after upgrading.
- In C++, every name a declaration lists now has the declaration's type, like `strings_found` in google/breakpad's `ByteBuffer symbols_found, strings_found;` or `data_` in a class's `Table *index_, *data_;`, so calls on it link to that class's methods. Before, only the first name had its type read, and a call on another name linked to nothing or to the class of an earlier variable with the same name.
- In C++, a call on a parameter, a local lambda or functor, a lambda's parameter or a range-`for` variable no longer links to a function or method elsewhere that only shares its name, like protocolbuffers/protobuf's code generators calling `format(…)` on their `Formatter& format` parameter, which linked to the conformance suite's `TestResult::format`, or facebook/rocksdb's tests calling a local `auto key = [](int i) {…};`. C++ finds the local first, so that is what the call calls.

## [1.6.2] - 2026-10-03

Expand Down
3 changes: 2 additions & 1 deletion __tests__/cpp-listed-declarator-receiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ describe('a C++ receiver declared later in a list of names', () => {

it('a bare call on a name declared later in a list is a local\'s, as on the first name', async () => {
// C++ lookup finds the local, not a member of the method's class, so a
// bare call on either name is left to the name strategies: `failed()`
// bare call on either name calls the local and links nothing: `failed()`
// reached Runner::failed as a call on `this`, while `done()` was a local's.
const cg = await indexed({
'jobs/runner.h': lines(
Expand Down Expand Up @@ -310,6 +310,7 @@ describe('a C++ receiver declared later in a list of names', () => {
run.find((c) => c.startsWith(`jobs::Runner::${method} `))?.split('@')[1];
expect(run).not.toContain('jobs::Runner::failed @0.9');
expect(howLinked('failed')).toBe(howLinked('done'));
expect(run).toEqual([]);
} finally {
cg.close();
}
Expand Down
209 changes: 209 additions & 0 deletions __tests__/cpp-local-callable-calls.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/**
* A C++ call written by its bare name, `format(…)`, on a name the calling
* function binds itself — a parameter, a lambda's parameter, a local lambda or
* functor, a range-`for` variable — calls that local. C++ lookup finds the
* local before any function or method of that name, so no project function or
* method is what it calls.
*
* protocolbuffers/protobuf's code generators take a `const Formatter& format`
* and call `format("…")`. The call went to the one other `format` the project
* has, the conformance suite's `TestResult::format`, by name. facebook/rocksdb's
* tests call local lambdas, `auto key = [](int i) {…};` then `key(10)`, and
* those went to `ArenaWrappedDBIter::key`.
*
* A name the function declares as a function (`void helper(int);`) or a type
* still links as before, and so does a local whose block closed before the
* call, or an expression that only reads like a declaration (`!force &&
* MaybeInsertDummy(key)`).
*/
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-local-callable-'));
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, constructors aside, as `qualifiedName`, one per callee. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(callable(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.filter((r) => {
const [owner, name] = r.node.qualifiedName.split('::').slice(-2);
return owner !== name;
})
.map((r) => r.node.qualifiedName)
.sort();
}

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

/** The project's other `format`, `key`, `append`, `fn` and `Value`: what a call on the local could be mistaken for. */
const ELSEWHERE = {
'conformance/testee.h': lines(
'namespace conformance {',
'class TestResult {',
' public:',
' int format() const { return 0; }',
'};',
'class Iterator {',
' public:',
' int key() const { return 1; }',
'};',
'class LeftoverBuffer {',
' public:',
' void append(const char* s) {}',
'};',
'class Runner {',
' public:',
' void fn() {}',
'};',
'} // namespace conformance',
),
};

describe('a C++ bare call on a name the calling function binds', () => {
it('reproduction: protobuf\'s `format(…)` on a `const Formatter& format` parameter is no TestResult::format', async () => {
const cg = await indexed({
...ELSEWHERE,
'compiler/cpp/message.cc': lines(
'#include <string>',
'namespace compiler {',
'class Formatter {',
' public:',
' void operator()(const char* text) const {}',
'};',
'class MessageGenerator {',
' public:',
' void GenerateFieldNames(const Formatter& format);',
'};',
'void MessageGenerator::GenerateFieldNames(const Formatter& format) {',
' format("static const char kNames[] = {");',
' format("};");',
'}',
'} // namespace compiler',
),
});
try {
expect(calls(cg, 'compiler::MessageGenerator::GenerateFieldNames')).toEqual([]);
expect(cg.getCallers(callable(cg, 'conformance::TestResult::format').id).filter((r) => r.edge.kind === 'calls')).toEqual([]);
} finally {
cg.close();
}
});

it('a local lambda, a local functor, a lambda\'s parameter and a range-for variable', async () => {
const cg = await indexed({
...ELSEWHERE,
'db/iter_test.cc': lines(
'#include <functional>',
'#include <string>',
'#include <vector>',
'namespace db {',
'class Formatter {',
' public:',
' explicit Formatter(int indent) {}',
' void operator()(const char* text) const {}',
'};',
'int Lambda() {',
' auto key = [](int i) { return i * 2; };',
' return key(10) + key(90);',
'}',
'void Functor(int indent) {',
' Formatter format(indent);',
' format("x");',
'}',
'void LambdaParameter(const std::function<void(std::function<void(const char*)>)>& with) {',
' with([&](auto append) {',
' append("tail");',
' });',
'}',
'void Loop(const std::vector<std::function<void()>>& callbacks) {',
' for (auto& fn : callbacks) fn();',
'}',
'} // namespace db',
),
});
try {
expect(calls(cg, 'db::Lambda')).toEqual([]);
expect(calls(cg, 'db::Functor')).toEqual([]);
expect(calls(cg, 'db::LambdaParameter')).toEqual([]);
expect(calls(cg, 'db::Loop')).toEqual([]);
} finally {
cg.close();
}
});

it('a function the body declares, a local out of scope and an expression still link', async () => {
const cg = await indexed({
'cache/helpers.h': lines(
'namespace cache {',
'int helper(int n);',
'int key(int i);',
'} // namespace cache',
),
'cache/helpers.cc': lines(
'#include "cache/helpers.h"',
'namespace cache {',
'int helper(int n) { return n; }',
'int key(int i) { return i; }',
'} // namespace cache',
),
'cache/secondary_cache.cc': lines(
'#include "cache/helpers.h"',
'namespace cache {',
'int Declared() {',
' int helper(int n);',
' return helper(1);',
'}',
'int OutOfScope(bool shadow) {',
' if (shadow) {',
' auto key = [](int i) { return -i; };',
' return key(1);',
' }',
' return key(2);',
'}',
'class SecondaryCache {',
' public:',
' bool Insert(int k, bool force_insert);',
' bool MaybeInsertDummy(int k) { return k > 0; }',
'};',
'bool SecondaryCache::Insert(int k, bool force_insert) {',
' if (!force_insert && MaybeInsertDummy(k)) {',
' return false;',
' }',
' return true;',
'}',
'} // namespace cache',
),
});
try {
expect(calls(cg, 'cache::Declared')).toEqual(['cache::helper']);
expect(calls(cg, 'cache::OutOfScope')).toEqual(['cache::key']);
expect(calls(cg, 'cache::SecondaryCache::Insert')).toEqual(['cache::SecondaryCache::MaybeInsertDummy']);
} finally {
cg.close();
}
});
});
Loading