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: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call on a variable, parameter or member declared as one of your classes, like `iter->Valid()` on an `Iterator* iter` or `it.Valid()` on a `const Iterator& it`, now links to that method of the class C++ itself would find for the type where the call is written, or of a class it inherits from. Before, the method was picked by the type's last name, so a class of the same name nested elsewhere could win: in facebook/rocksdb, calls on an `Iterator` reached `MemTableRep::Iterator`, which declares `Valid` and `Next` itself while the `Iterator` the code means inherits them, google/leveldb's reached `SkipList::Iterator`, and in protocolbuffers/protobuf the Java generator's `context->options()` reached the hpb generator's `Context` and `text_parser.Parse(…)` on a `TextFormat::Parser` reached the `.proto` parser. When that class and the classes it derives from have no method of that name, the call no longer links to an unrelated class's method that shares it, as protobuf's `field->full_name()` on a `FieldDescriptor*` did. A type is also found through a `using` declaration the file writes, like protobuf's `using google::protobuf::Descriptor;`, before a class of the same name declared at global scope elsewhere in the repository; this also applies to base classes and to the type a smart pointer holds. A type written from the global scope, like `::upb::Arena& arena`, is looked up there, so a namespace's own `upb::Arena` is not taken for it, nor is an alias of that name the calling function declares. A type written as a member of a class template, like `SkipList<Key, Comparator>::Iterator iter` in leveldb's and RocksDB's skip-list tests, is now read whole, so its calls reach the skip list's own iterator rather than the general `Iterator`, and RocksDB's skip-list memtable iterator no longer links its calls on the skip list's iterator to its own methods; a standard container's `iterator` written this way is the library's, never a class of yours named `iterator`. Re-index C++ projects after upgrading.
- In C++, a method defined by a macro of the same file is now indexed where the file invokes the macro, so calls to it link. protocolbuffers/protobuf's descriptor classes only declare most of their accessors and define them further down with macros like `PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, number, int)`, and its reflection defines `GetInt32`, `SetInt32` and the like by pasting names together in a macro. Those methods had no node at all, so calls like `field->number()`, `descriptor->full_name()` or `reflection->GetInt32(…)` linked to nothing. Calls through what such a method returns, like `field->containing_type()->full_name()`, now link too. A macro defined in another file, and a method declared but defined nowhere in the repository, are still not indexed. Re-index C++ projects after upgrading.
- In C++, an explicit specialization of a function or member function template, like protocolbuffers/protobuf's `template <> bool WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>(…)` or facebook/rocksdb's `template <> int64_t Deserialize<int64_t>(…)`, is now indexed under its template's name, so search finds it and calls link to it. Before, it kept its template arguments in its name, and one with a `::` among them was named after the last part, like `TYPE_INT32>`. A call that writes template arguments, like `Deserialize<int64_t>(src, offset)`, links to the specialization written with the same ones, and otherwise to the template itself, as fmt's `num_bits<unsigned short>()` does; a call that writes none links to a specialization only when nothing else has that name. Re-index C++ projects after upgrading.
- In C++, a call to an overloaded function outside a class now links to the overload its arguments fit, by how many it passes and the types they have where the call writes them, as a call to a method already did: protocolbuffers/protobuf's `ParseWithLimit(limit, data, result)` on an `int32_t result` reaches the overload taking an `int32_t&`, and facebook/rocksdb's `NewLRUCache(cache_opts)` the one taking an `LRUCacheOptions`. Before, the overload nearest the call was taken. Such a call is never linked to an overload only another source file can see, like another `.cc` file's `static` function: it now links to the one its own file sees, where before it often linked to nothing. A call to a name that both a function and a class have reaches the function, which hides the class, and a parameter named like the function it belongs to, like protobuf's `std::string u64(uint64_t u64)`, now has its own type. Re-index C++ projects after upgrading.
- In C++, a call to an overloaded function outside a class now links to the overload its arguments fit, by how many it passes and the types they have where the call writes them, as a call to a method already did: protocolbuffers/protobuf's `ParseWithLimit(limit, data, result)` on an `int32_t result` reaches the overload taking an `int32_t&`, and facebook/rocksdb's `NewLRUCache(cache_opts)` the one taking an `LRUCacheOptions`. Before, the overload nearest the call was taken. Such a call is never linked to an overload only another source file can see, like another `.cc` file's `static` function: it now links to the one its own file sees, where before it often linked to nothing. A call to a name that both a function and a class have reaches the function, which hides the class, and a parameter named like the function it belongs to, like protobuf's `std::string u64(uint64_t u64)`, now has its own type. Of two overloads alike but for a `const`, like protobuf's `DynamicCastMessage(const MessageLite* from)` and `DynamicCastMessage(MessageLite* from)`, a call now reaches the one C++ picks: the non-const overload for a pointer to a non-const object, or for the object itself, and the const one for a const object or a temporary, where before the overload declared first won. An argument declared with `const` in front, like `const Buffer* b`, counts as const, and an array as a pointer to its element, so facebook/rocksdb's `Add(level, file, smallest, largest, …)` on `char` arrays reaches the overload taking `const char*`. An object is no longer matched to a pointer parameter, nor a character array to a pointer of another type, so protobuf's `printer->WithVars(EnumVars(…))` reaches `WithVars(Map&& vars)`, not `WithVars(const Map* vars)`. Re-index C++ projects after upgrading.
- In C, a call now links to the function of that name its own file can see: its own, else one a header it includes defines, like a `static inline` helper, else one another source file defines without `static`. Before, the function nearest the call was taken, and when that was another source file's `static` function, which no other file can call, the call linked to nothing: in redis, the calls to `server.c`'s `mstime()` and `ustime()` from the rest of the server were lost to the `static` copies other files keep for themselves, and jemalloc's tests' calls to the `do_arena_create` their shared header defines to a neighbouring test's own. A call to a `static inline` function that several headers define reaches the copy in a header the file includes, so protocolbuffers/protobuf's upb code no longer links to the copies in its PHP extension's combined `php-upb.h`. A source file that `#include`s another, like redis's `ae.c` with its event-loop backend or hiredis's `async.c` with `dict.c`, now calls that file's `static` functions, and a header a file includes through an include directory, like jemalloc's `#include "jemalloc/internal/bit_util.h"`, counts as included. A function a header defines only for a build that turns on a flag, like lua-cjson's `fpconv_init` under `#ifdef USE_INTERNAL_FPCONV`, gives way to another source file's definition when nothing in the project's code or build files sets that flag, so redis's calls to `fpconv_init()` reach the `fpconv.c` its build compiles. Re-index C projects after upgrading.
- In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading.
- In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr<Iterator>(…)` or `auto* mutex = static_cast<InstrumentedMutex*>(arg)`, from `new` or `std::make_unique` of a class, and from what a function or method is declared to return, like the `Iterator*` of `txn->GetIterator(…)`, read to the end of an initializer that goes on to the next line. Before, such a variable took the type of an earlier variable of the same name, or none: in RocksDB, `iter->SeekToFirst()` reached `MemTableRep::Iterator` and `mutex->Lock()` reached `port::Mutex`. A chained call like `cfd->current()->storage_info()` now looks up the class the inner call is declared to return the same way, through a smart pointer too, so protocolbuffers/protobuf's Java generator no longer reaches the C# generator's `EnumGenerator`. A type from outside the project no longer links to a class of the project that shares its name: protobuf's `status.ok()` on an `absl::Status` reached `upb::Status::ok`, and fmtlib/fmt's `os.flush()` on a `std::ostream&` reached fmt's own `ostream::flush`. The same holds for a variable declared in a macro body, or typed by what a function in another file returns, and for a class the index doesn't hold whose name only another class shares: protobuf's `_status.ok()` in its `RETURN_IF_ERROR` macro reached `upb::Status::ok`, and `range.options()` on its generated `DescriptorProto::ExtensionRange` reached `Descriptor::ExtensionRange::options`. A namespace a macro stands for is the project's under either name: facebook/rocksdb declares its classes in `namespace ROCKSDB_NAMESPACE` with `#define ROCKSDB_NAMESPACE rocksdb`, so `db->Put(…)` on a `rocksdb::DB*` links to RocksDB's own `DB::Put`. Re-index C++ projects after upgrading.
Expand Down
195 changes: 195 additions & 0 deletions __tests__/cpp-overload-constness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* A C++ call whose argument fits a function's const and non-const overloads
* alike — protobuf's `DynamicCastMessage<T>(ptr.get())` beside `const T*
* DynamicCastMessage(const MessageLite*)` and `T* DynamicCastMessage(MessageLite*)`
* — reaches the one C++ picks: the non-const overload for a non-const object,
* the const one for a const object or for a value a call returns. Before, the
* overload declared first won the tie.
*/
import { describe, it, expect, beforeAll, afterAll } 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 lines = (...l: string[]): string => [...l, ''].join('\n');

describe("a C++ call picks between const and non-const overloads by its argument's constness", () => {
let root: string;
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-overload-constness-'));
const write = (file: string, text: string): void => {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), text);
};
// The const overloads come first, as in protobuf's message_lite.h.
write('src/message_lite.h', lines(
'#include <memory>',
'class MessageLite {',
' public:',
' virtual ~MessageLite() {}',
'};',
'class Message : public MessageLite {};',
'template <typename T>',
'const T* DynamicCastMessage(const MessageLite* from) { return nullptr; }',
'template <typename T>',
'T* DynamicCastMessage(MessageLite* from) { return nullptr; }',
'template <typename T>',
'const T& DynamicCastMessage(const MessageLite& from) { return *static_cast<const T*>(&from); }',
'template <typename T>',
'T& DynamicCastMessage(MessageLite& from) { return *static_cast<T*>(&from); }',
'class Buffer {};',
'class Writer {',
' public:',
' void Write(const Buffer* buffer) {}',
' void Write(Buffer* buffer) {}',
'};',
'Message MakeMessage() { return Message(); }',
// The non-const overload first.
'class Reader {',
' public:',
' void Read(Buffer* buffer) {}',
' void Read(const Buffer* buffer) {}',
'};',
// Overloads that differ by more than a `const`, as fmt's `to_string` do.
'template <typename T>',
'int to_text(T value) { return 0; }',
'template <typename T>',
'int to_text(const T& value) { return 1; }',
// An array is a pointer to its element, as fmt's `Char buf[8]`.
'template <typename OutputIt>',
'OutputIt format_hex(OutputIt out, unsigned value) { return out; }',
'template <typename Char>',
'Char* format_hex(Char* out, unsigned value) { return out; }',
// rocksdb's static_cast_with_check: the template's own `Src` takes the argument's `const`.
'template <class Dest, class Src>',
'Dest* checked_cast(Src* x) { return nullptr; }',
'template <class Dest, class Src>',
'std::shared_ptr<Dest> checked_cast(std::shared_ptr<Src>&& x) { return nullptr; }',
// fmt's to_string_view: an argument of a template parameter's type could be a pointer.
'template <typename T>',
'int view_of(const T* s) { return 0; }',
'template <typename T>',
'int view_of(const T& s) { return 1; }',
// rocksdb's Put: characters are no ColumnFamilyHandle.
'class Handle {};',
'class Slice {',
' public:',
' Slice(const char* data) {}',
'};',
'class Store {',
' public:',
' int Put(Handle* handle, const Slice& key) { return 0; }',
' int Put(const Slice& key, const Slice& value) { return 1; }',
'};',
// ...but a template's own `T*` takes them.
'template <typename T>',
'int sink(T* p) { return 0; }',
'int sink(const Slice& s) { return 1; }',
// A pointer is no characters, a `constexpr` array is const, and a long header is still a header.
'int Emit(const char* text) { return 0; }',
'int Emit(const Slice& text) { return 1; }',
'int Take(char* p) { return 0; }',
'int Take(const char* p) { return 1; }',
'template <typename T,',
' typename A,',
' typename B,',
' typename C = int>',
'int peek(T* p) { return 0; }',
'int peek(const Slice& s) { return 1; }',
));
write('src/use.cc', lines(
'#include "src/message_lite.h"',
'void NonConstHolder(std::shared_ptr<Message> ptr) { DynamicCastMessage<Message>(ptr.get()); }',
'void ConstHolder(std::shared_ptr<const Message> ptr) { DynamicCastMessage<Message>(ptr.get()); }',
'void NonConstPointer(Message* m) { DynamicCastMessage<Message>(m); }',
'void ConstPointer(const Message* m) { DynamicCastMessage<Message>(m); }',
'void NonConstReference(Message& m) { DynamicCastMessage<Message>(m); }',
'void ConstReference(const Message& m) { DynamicCastMessage<Message>(m); }',
'void Temporary() { DynamicCastMessage<Message>(MakeMessage()); }',
'void WriteNonConst(Writer& w, Buffer* b) { w.Write(b); }',
'void WriteConst(Writer& w, const Buffer* b) { w.Write(b); }',
'void WriteConstPointer(Writer& w, Buffer* const b) { w.Write(b); }',
'void ReadNonConst(Reader& r, Buffer* b) { r.Read(b); }',
'void ReadConst(Reader& r, const Buffer* b) { r.Read(b); }',
'void Show(Buffer& b) { to_text(b); }',
'template <typename Char>',
'void WriteHex(unsigned v) { Char buf[8]; format_hex(buf, v); }',
'void Merge(const Buffer& other) { checked_cast<const Buffer>(&other); }',
'template <typename S>',
'void Format(const S& fmt) { view_of(fmt); }',
// A header over several lines, as fmt's `join` writes.
'template <typename It,',
' typename Sentinel,',
' typename S,',
' typename Char = char>',
'void Join(It begin, Sentinel end, S&& sep) { view_of(sep); }',
'void Save(Store& s) {',
' char key[] = "key";',
' char value[] = "value";',
' s.Put(key, value);',
'}',
'void Drain() { char buf[4]; sink(buf); }',
'void Send(Buffer* b) { Emit(b); }',
'void Keep() {',
' constexpr char name[] = "name";',
' Take(name);',
'}',
'void Look(const Buffer* b) { peek(b); }',
));
cg = await CodeGraph.init(root, { index: true });
}, 120_000);

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

/** The `name` overloads a function calls, as `name:startLine`. */
const calls = (caller: string, name: string): string[] => {
const fn = cg.getNodesByKind('function').find((n: Node) => n.name === caller);
expect(fn, caller).toBeDefined();
return cg.getCallees(fn!.id).filter((r) => r.edge.kind === 'calls' && r.node.name === name).map((r) => `${r.node.name}:${r.node.startLine}`);
};

it('a pointer to a non-const object takes the non-const overload, to a const one the const overload', () => {
expect(calls('NonConstHolder', 'DynamicCastMessage')).toEqual(['DynamicCastMessage:10']);
expect(calls('ConstHolder', 'DynamicCastMessage')).toEqual(['DynamicCastMessage:8']);
expect(calls('NonConstPointer', 'DynamicCastMessage')).toEqual(['DynamicCastMessage:10']);
expect(calls('ConstPointer', 'DynamicCastMessage')).toEqual(['DynamicCastMessage:8']);
});

it('a non-const object takes the non-const reference, a const object or a temporary the const one', () => {
expect(calls('NonConstReference', 'DynamicCastMessage')).toEqual(['DynamicCastMessage:14']);
expect(calls('ConstReference', 'DynamicCastMessage')).toEqual(['DynamicCastMessage:12']);
expect(calls('Temporary', 'DynamicCastMessage')).toEqual(['DynamicCastMessage:12']);
});

it("a class's own overloads, whichever comes first", () => {
expect(calls('WriteNonConst', 'Write')).toEqual(['Write:19']);
expect(calls('WriteConst', 'Write')).toEqual(['Write:18']);
// A pointer's own `const` is not its object's.
expect(calls('WriteConstPointer', 'Write')).toEqual(['Write:19']);
expect(calls('ReadNonConst', 'Read')).toEqual(['Read:24']);
expect(calls('ReadConst', 'Read')).toEqual(['Read:25']);
});

it('no overload that differs by more than a `const`, and an array as a pointer', () => {
expect(calls('Show', 'to_text')).toEqual(['to_text:28']);
expect(calls('WriteHex', 'format_hex')).toEqual(['format_hex:34']);
});

it("a template's own parameter, an argument of a template parameter's type, and characters for a class pointer", () => {
expect(calls('Merge', 'checked_cast')).toEqual(['checked_cast:36']);
expect(calls('Format', 'view_of')).toEqual(['view_of:40']);
expect(calls('Join', 'view_of')).toEqual(['view_of:40']);
expect(calls('Save', 'Put')).toEqual(['Put:51']);
expect(calls('Drain', 'sink')).toEqual(['sink:54']);
expect(calls('Send', 'Emit')).toEqual(['Emit:57']);
expect(calls('Keep', 'Take')).toEqual(['Take:59']);
expect(calls('Look', 'peek')).toEqual(['peek:64']);
});
});
Loading