Files
codegraph/__tests__/cpp-implicit-this-calls.test.ts
Colby Mchenry fa56256c49 fix(cpp): a call a method makes with no receiver links to its class's own or inherited method (#2479)
A C++ call written with no receiver, or through `this->`, inside a method
was matched by its name alone, so another class's method of that name won
by file proximity: protobuf's generated `Api::operator=` calling
`InternalSwap(&from)` reached `Any::InternalSwap`, `Any::InternalSwap`'s
inherited `GetArena()` reached `Arena::InternalHelper::GetArena`, and
googletest's `~linked_ptr() { depart(); }` reached `linked_ptr_internal::depart`.

matchMethodCall now resolves C++'s implicit `this` the way it resolves a
typed receiver: the calling method's class, then the classes it derives
from (its own base edges, through #2440's cppMethodOf), then the classes it
is nested in, with the overload the arguments fit. The call's shape is read
at its column (the extractor drops `this->` and some receivers it can't
spell). A free function, a parameter or local of the name, a dependent base
reached by a bare call, a namesake class in another translation unit and a
member a macro may declare all leave the call to the name strategies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 08:22:50 +00:00

525 lines
16 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* A C++ call written with no receiver, or on `this`, inside a member function
* is a call on the function's object: C++ looks the name up in the function's
* class, then in the classes it derives from, then in the classes it is nested
* in, and only then at namespace scope. The resolver matched it by name alone,
* so a same-named member of another class won by file proximity:
* - protobuf's generated `Api::operator=` calling `InternalSwap(&from)` went to
* `Any::InternalSwap`, in every generated message class;
* - `Any::InternalSwap` calling the `GetArena()` it inherits from `MessageLite`
* went to `Arena::InternalHelper::GetArena`;
* - googletest's `~linked_ptr() { depart(); }` (vendored in rocksdb) went to
* `linked_ptr_internal::depart`, declared just above it.
* Nothing changes for a call the lookup can't place: a free function's, one
* through a receiver the extractor dropped, one naming a parameter, or one
* into a base that depends on a template parameter.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
function writeProject(prefix: string, files: Record<string, string>): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
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 root;
}
/** The edges carrying a call's name out of the function or method `qualifiedName` in `file`. */
function callEdgesFrom(cg: CodeGraph, file: string, qualifiedName: string) {
const from = cg.getNodesInFile(file).filter((n) => n.qualifiedName === qualifiedName && (n.kind === 'function' || n.kind === 'method'));
expect(from.length).toBeGreaterThan(0);
return cg.getOutgoingEdgesFrom(from.map((n) => n.id)).flatMap((e) => {
const metadata = e.metadata as { refName?: string; resolvedBy?: string } | undefined;
return metadata?.refName ? [{ refName: metadata.refName, resolvedBy: metadata.resolvedBy, target: cg.getNode(e.target)!.qualifiedName }] : [];
});
}
/** `name -> target qualified name` for each call (or construction) the named function or method in `file` makes. */
function callsFrom(cg: CodeGraph, file: string, qualifiedName: string): string[] {
return callEdgesFrom(cg, file, qualifiedName).map((e) => `${e.refName} -> ${e.target}`).sort();
}
/** Whether C++ name lookup from the calling function (not a guess by name) placed the call `refName`. */
function placedByLookup(cg: CodeGraph, file: string, qualifiedName: string, refName: string): boolean {
return callEdgesFrom(cg, file, qualifiedName).some((e) => e.refName === refName && e.resolvedBy === 'instance-method');
}
const MESSAGE_LITE = `#pragma once
namespace pb {
class Arena {
public:
class InternalHelper {
public:
static Arena* GetArena(const void* object) { return nullptr; }
};
};
class MessageLite {
public:
Arena* GetArena() const { return arena_; }
void Clear() {}
private:
Arena* arena_ = nullptr;
};
class Message : public MessageLite {};
} // namespace pb
`;
// Two generated message classes, each defining InternalSwap out of line;
// Any's comes first in the index.
const ANY_H = `#pragma once
#include "pb/message.h"
namespace pb {
class Any final : public Message {
public:
Any& operator=(Any&& from) noexcept {
InternalSwap(&from);
return *this;
}
void InternalSwap(Any* other);
};
} // namespace pb
`;
const API_H = `#pragma once
#include "pb/message.h"
namespace pb {
class Api final : public Message {
public:
Api& operator=(Api&& from) noexcept {
InternalSwap(&from);
return *this;
}
void Swap(Api* other) {
if (other == this) return;
this->InternalSwap(other);
}
void InternalSwap(Api* other);
};
} // namespace pb
`;
describe('C++ calls on the calling function’s own object', () => {
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = writeProject('cg-cpp-this-', {
'src/pb/message.h': MESSAGE_LITE,
'src/pb/any.pb.h': ANY_H,
'src/pb/any.pb.cc': `#include "pb/any.pb.h"
namespace pb {
void Any::InternalSwap(Any* other) {
auto* arena = GetArena();
(void)arena;
}
} // namespace pb
`,
'src/pb/api.pb.h': API_H,
'src/pb/api.pb.cc': `#include "pb/api.pb.h"
namespace pb {
void Api::InternalSwap(Api* other) {
(*this).Clear();
}
} // namespace pb
`,
'third_party/gtest/linked_ptr.h': `#pragma once
namespace testing {
namespace internal {
class linked_ptr_internal {
public:
bool depart() { return next_ == this; }
private:
mutable linked_ptr_internal const* next_;
};
template <typename T>
class linked_ptr {
public:
~linked_ptr() { depart(); }
void reset(T* ptr = nullptr) {
depart();
value_ = ptr;
}
T* get() const { return value_; }
T* operator->() const { return value_; }
T& operator*() const { return *value_; }
bool operator==(T* p) const { return value_ == p; }
bool operator!=(T* p) const { return value_ != p; }
template <typename U>
bool operator==(linked_ptr<U> const& ptr) const {
return value_ == ptr.get();
}
template <typename U>
bool operator!=(linked_ptr<U> const& ptr) const {
return value_ != ptr.get();
}
private:
template <typename U>
friend class linked_ptr;
// Takes ownership of p, which must be the only linked_ptr to it:
// the link starts out as a one-element ring.
void capture(T* p) {
value_ = p;
}
// Joins the ring another linked_ptr belongs to, sharing what it points to.
template <typename U>
void copy(linked_ptr<U> const* ptr) {
value_ = ptr->get();
}
// Leaves the ring; the last one out deletes the object.
void depart() {
if (link_.depart()) delete value_;
}
T* value_;
linked_ptr_internal link_;
};
} // namespace internal
} // namespace testing
`,
});
cg = await CodeGraph.init(root, { index: true });
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
it('links a bare call to the calling class’s own method, not another class’s namesake', () => {
expect(callsFrom(cg, 'src/pb/api.pb.h', 'pb::Api::operator=')).toEqual(['InternalSwap -> pb::Api::InternalSwap']);
expect(callsFrom(cg, 'src/pb/any.pb.h', 'pb::Any::operator=')).toEqual(['InternalSwap -> pb::Any::InternalSwap']);
});
it('links a call on `this->` or `(*this).` the same way', () => {
expect(callsFrom(cg, 'src/pb/api.pb.h', 'pb::Api::Swap')).toEqual(['InternalSwap -> pb::Api::InternalSwap']);
expect(callsFrom(cg, 'src/pb/api.pb.cc', 'pb::Api::InternalSwap')).toEqual(['Clear -> pb::MessageLite::Clear']);
});
it('links a method the class inherits, through its own base classes', () => {
expect(callsFrom(cg, 'src/pb/any.pb.cc', 'pb::Any::InternalSwap')).toEqual(['GetArena -> pb::MessageLite::GetArena']);
});
it('prefers the class’s own method over a namesake declared nearer the call', () => {
expect(callsFrom(cg, 'third_party/gtest/linked_ptr.h', 'testing::internal::linked_ptr::~linked_ptr'))
.toEqual(['depart -> testing::internal::linked_ptr::depart']);
expect(callsFrom(cg, 'third_party/gtest/linked_ptr.h', 'testing::internal::linked_ptr::reset'))
.toEqual(['depart -> testing::internal::linked_ptr::depart']);
});
});
describe('C++ name lookup from a member function', () => {
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = writeProject('cg-cpp-this-scopes-', {
'src/db/db.h': `#pragma once
#include <functional>
namespace db {
class Stats {
public:
static int Count() { return 1; }
};
class Table {
public:
static int Count();
void Flush();
void Compact();
void Reset() {}
void Close() {}
void OnDone(std::function<void()> Close) { Close(); }
class Builder {
public:
int Size() const { return Count(); }
};
};
struct Item {
void Clear() {}
};
} // namespace db
`,
'src/db/table.cc': `#include "db/db.h"
namespace db {
static void Reset() {}
static void Trim() {}
class Job {
public:
void Compact() {}
};
void Table::Flush() {
Reset();
Trim();
auto finish = [this]() { Compact(); };
finish();
}
void Rebuild() {
Trim();
Reset();
}
} // namespace db
`,
'src/db/compact.cc': `#include "db/db.h"
namespace db {
int Table::Count() { return 0; }
void Table::Compact() {
Item items[2];
items[0].Clear();
}
} // namespace db
`,
});
cg = await CodeGraph.init(root, { index: true });
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
it('takes the class’s member over a file-static function of its name, and the function when the class has none', () => {
const calls = callsFrom(cg, 'src/db/table.cc', 'db::Table::Flush');
expect(calls).toContain('Reset -> db::Table::Reset');
expect(calls).toContain('Trim -> db::Trim');
});
it('links a bare call in a lambda to the member function’s class', () => {
expect(callsFrom(cg, 'src/db/table.cc', 'db::Table::Flush')).toContain('Compact -> db::Table::Compact');
});
it('links a nested class’s bare call to the enclosing class’s static member', () => {
expect(callsFrom(cg, 'src/db/db.h', 'db::Table::Builder::Size')).toEqual(['Count -> db::Table::Count']);
});
it('leaves a free function, static or not, to find what its namespace declares', () => {
expect(callsFrom(cg, 'src/db/table.cc', 'db::Rebuild')).toEqual(['Reset -> db::Reset', 'Trim -> db::Trim']);
});
it('never takes a receiver the extractor dropped for the calling object', () => {
expect(callsFrom(cg, 'src/db/compact.cc', 'db::Table::Compact')).toEqual(['Clear -> db::Item::Clear']);
});
it('does not take a parameter named like a member for the member', () => {
expect(placedByLookup(cg, 'src/db/db.h', 'db::Table::OnDone', 'Close')).toBe(false);
});
});
describe('C++ overloads a call’s arguments pick', () => {
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = writeProject('cg-cpp-this-overloads-', {
'include/opts/configurable.h': `#pragma once
#include <string>
namespace opts {
class Configurable {
public:
template <typename T>
void RegisterOptions(T* opt_ptr, const int* opt_map) {
RegisterOptions(T::kClassName(), opt_ptr, opt_map);
}
void RegisterOptions(const std::string& name, void* opt_ptr, const int* opt_map);
void Add(int key) { Add(key, 1); }
void Add(int key, int count);
};
} // namespace opts
`,
'src/opts/configurable.cc': `#include "opts/configurable.h"
namespace opts {
void Configurable::RegisterOptions(const std::string& name, void* opt_ptr, const int* opt_map) {}
void Configurable::Add(int key, int count) {}
} // namespace opts
`,
'test/simple.cc': `#include "opts/configurable.h"
namespace opts {
class SimpleConfigurable : public Configurable {
public:
SimpleConfigurable() {
RegisterOptions(std::string("Simple") + "Unique", &unique_, &info_);
}
private:
int unique_ = 0;
int info_ = 0;
};
} // namespace opts
`,
});
cg = await CodeGraph.init(root, { index: true });
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
it('takes the inherited overload the arguments fit', () => {
// Both overloads are `opts::Configurable::RegisterOptions`: the three-parameter one is defined in the .cc.
const threeParameters = cg.getNodesInFile('src/opts/configurable.cc').find((n) => n.name === 'RegisterOptions');
const ctor = cg.getNodesInFile('test/simple.cc').find((n) => n.qualifiedName === 'opts::SimpleConfigurable::SimpleConfigurable');
const edge = cg.getOutgoingEdges(ctor!.id).find((e) => (e.metadata as { refName?: string } | undefined)?.refName === 'RegisterOptions');
expect(edge?.target).toBe(threeParameters?.id);
});
it('weighs a class’s out-of-line overloads with its inline ones', () => {
const add = cg.getNodesInFile('include/opts/configurable.h').find((n) => n.name === 'Add');
const outOfLine = cg.getNodesInFile('src/opts/configurable.cc').find((n) => n.name === 'Add');
const edge = cg.getOutgoingEdges(add!.id).find((e) => (e.metadata as { refName?: string } | undefined)?.refName === 'Add');
expect(edge?.target).toBe(outOfLine?.id);
});
});
describe('C++ classes the index can only see part of', () => {
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = writeProject('cg-cpp-this-partial-', {
// Two translation units each declare a `FileState` of their own.
'helpers/memenv.cc': `namespace kv {
namespace {
class FileState {
public:
void Truncate() {}
~FileState() { Truncate(); }
};
} // namespace
} // namespace kv
`,
'db/fault_injection_test.cc': `namespace kv {
namespace {
int Truncate(const char* name, long length) { return 0; }
struct FileState {
const char* filename_;
long pos_;
int DropUnsyncedData() const;
};
} // namespace
int FileState::DropUnsyncedData() const {
return Truncate(filename_, pos_);
}
} // namespace kv
`,
'src/reflection.cc': `#define LOCAL_VAR_ACCESSOR(type, name) \\
type Get##name() const { return value; }
namespace pb {
class Message {};
class Reflection {
public:
Message* GetMessage() const { return nullptr; }
void SwapField() const;
};
void Reflection::SwapField() const {
struct LocalVarWrapper {
LOCAL_VAR_ACCESSOR(Message*, Message);
Message* UnsafeGetMessage() const { return GetMessage(); }
Message* value;
};
}
} // namespace pb
`,
});
cg = await CodeGraph.init(root, { index: true });
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
it('never lends one translation unit’s class members to another’s class of the same name', () => {
expect(callsFrom(cg, 'helpers/memenv.cc', 'kv::FileState::~FileState')).toEqual(['Truncate -> kv::FileState::Truncate']);
expect(placedByLookup(cg, 'db/fault_injection_test.cc', 'kv::FileState::DropUnsyncedData', 'Truncate')).toBe(false);
expect(callsFrom(cg, 'db/fault_injection_test.cc', 'kv::FileState::DropUnsyncedData')).not.toContain('Truncate -> kv::FileState::Truncate');
});
it('stops at a class whose macro may declare the name, rather than take an outer class’s', () => {
const wrapper = cg.getNodesInFile('src/reflection.cc').find((n) => n.name === 'UnsafeGetMessage');
expect(wrapper).toBeDefined();
expect(placedByLookup(cg, 'src/reflection.cc', wrapper!.qualifiedName, 'GetMessage')).toBe(false);
});
});
describe('C++ name lookup in a class template', () => {
let root = '';
let cg: CodeGraph;
beforeAll(async () => {
root = writeProject('cg-cpp-this-template-', {
'src/util/list.h': `#pragma once
namespace util {
template <typename T>
class ListBase {
public:
void Grow(int n) {}
void Shrink() {}
};
inline void Grow(int n) {}
template <typename T>
class List : public ListBase<T> {
public:
void Push() {
Grow(1);
this->Shrink();
}
};
class IntList : public ListBase<int> {
public:
void Push() { Grow(1); }
};
} // namespace util
`,
});
cg = await CodeGraph.init(root, { index: true });
}, 60_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
it('looks a bare name up in a base that depends on a template parameter only when written `this->`', () => {
expect(callsFrom(cg, 'src/util/list.h', 'util::List::Push')).toContain('Shrink -> util::ListBase::Shrink');
expect(placedByLookup(cg, 'src/util/list.h', 'util::List::Push', 'Shrink')).toBe(true);
// `ListBase<T>` is not searched for a bare `Grow(1)`: C++ finds the namespace's `util::Grow`.
expect(placedByLookup(cg, 'src/util/list.h', 'util::List::Push', 'Grow')).toBe(false);
});
it('looks it up in a base with no template parameter of the class in it', () => {
expect(callsFrom(cg, 'src/util/list.h', 'util::IntList::Push')).toEqual(['Grow -> util::ListBase::Grow']);
});
});