mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
c29caf2ae8
`auto it = logs_.begin(); it->IsSyncing()` had no type for `it`: a call on a library container (`std::deque<LogWriterNumber>`) returned nothing the auto-local reader could use, so `->` calls on it were matched by the method's name. A standard or abseil container's `begin`/`end`/`find`/ `lower_bound`/`rbegin`/… now types as `<container>::iterator`, and a standard search (`std::lower_bound`, `std::find_if`, `std::next`, …) as the iterator it is given first; #2576's cppArrowTarget then reaches the container's element. The element is looked up where the container's type is written (the `auto` local's declared site, a data member's class), not where the call is: an element nested in another class (`vs->obsolete_files_.begin()`) finds that class's type, not a namesake visible at the call. A holder's type stays looked up at the call: from a pimpl member's header the lookup finds a namesake (protobuf's other `DescriptorIndex`) or nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
249 lines
7.4 KiB
TypeScript
249 lines
7.4 KiB
TypeScript
/**
|
|
* A `->` call on a C++ `auto` local that a library container's member or a
|
|
* standard search hands an iterator, like rocksdb's `auto it =
|
|
* logs_.begin(); it->IsSyncing()`, calls a member of the container's
|
|
* element, as on an iterator declared with its type. The local had no type,
|
|
* so the call was matched by the method's name 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 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-auto-iterator-'));
|
|
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`. */
|
|
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)
|
|
.sort();
|
|
}
|
|
|
|
const lines = (...l: string[]): string => [...l, ''].join('\n');
|
|
|
|
describe('a `->` call on a C++ `auto` local holding a library container\'s iterator', () => {
|
|
it('reproduction: rocksdb\'s `auto it = logs_.begin(); it->IsSyncing()`', async () => {
|
|
const cg = await indexed({
|
|
'db/log_writer.h': lines(
|
|
'class Writer {',
|
|
' public:',
|
|
' bool IsSyncing() const { return false; }',
|
|
'};',
|
|
),
|
|
'db/db_impl.h': lines(
|
|
'#include <deque>',
|
|
'class DBImpl {',
|
|
' public:',
|
|
' struct LogWriterNumber {',
|
|
' bool IsSyncing() const { return true; }',
|
|
' void ReleaseWriter() {}',
|
|
' };',
|
|
' void MarkLogsSynced();',
|
|
' private:',
|
|
' std::deque<LogWriterNumber> logs_;',
|
|
'};',
|
|
),
|
|
'db/db_impl.cc': lines(
|
|
'#include "db/db_impl.h"',
|
|
'#include "db/log_writer.h"',
|
|
'void DBImpl::MarkLogsSynced() {',
|
|
' auto it = logs_.begin();',
|
|
' if (it->IsSyncing()) it->ReleaseWriter();',
|
|
'}',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'DBImpl::MarkLogsSynced')).toEqual(['DBImpl::LogWriterNumber::IsSyncing', 'DBImpl::LogWriterNumber::ReleaseWriter']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('from a standard search, and a reverse iterator', async () => {
|
|
const cg = await indexed({
|
|
'util/entries.h': lines(
|
|
'class Entry {',
|
|
' public:',
|
|
' const char* name() const { return nullptr; }',
|
|
'};',
|
|
'class Column {',
|
|
' public:',
|
|
' const char* name() const { return nullptr; }',
|
|
'};',
|
|
),
|
|
'util/lookup.cc': lines(
|
|
'#include <algorithm>',
|
|
'#include <vector>',
|
|
'#include "util/entries.h"',
|
|
'const char* Find(const std::vector<Entry>& entries, const Entry& key) {',
|
|
' auto it = std::lower_bound(entries.begin(), entries.end(), key);',
|
|
' return it->name();',
|
|
'}',
|
|
'const char* Last(std::vector<Entry>& entries) {',
|
|
' auto it = entries.rbegin();',
|
|
' return it->name();',
|
|
'}',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'Find')).toEqual(['Entry::name']);
|
|
expect(calls(cg, 'Last')).toEqual(['Entry::name']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('the element is looked up where the container\'s type is written', async () => {
|
|
const cg = await indexed({
|
|
'db/version_set.h': lines(
|
|
'#include <vector>',
|
|
'struct ObsoleteFile {',
|
|
' void DeleteFile() {}',
|
|
'};',
|
|
'class VersionSet {',
|
|
' public:',
|
|
' struct ObsoleteFile {',
|
|
' void DeleteFile() {}',
|
|
' };',
|
|
' std::vector<ObsoleteFile> obsolete_files_;',
|
|
'};',
|
|
),
|
|
'db/purge.cc': lines(
|
|
'#include "db/version_set.h"',
|
|
'void Purge(VersionSet* vs) {',
|
|
' auto it = vs->obsolete_files_.begin();',
|
|
' it->DeleteFile();',
|
|
'}',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'Purge')).toEqual(['VersionSet::ObsoleteFile::DeleteFile']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('a smart pointer member\'s type is still looked up where the call is: a pimpl defined in the source file', async () => {
|
|
const cg = await indexed({
|
|
'db/database.h': lines(
|
|
'#include <memory>',
|
|
'class SimpleDatabase {',
|
|
' public:',
|
|
' class DescriptorIndex {',
|
|
' public:',
|
|
' bool AddFile() { return false; }',
|
|
' };',
|
|
'};',
|
|
'class EncodedDatabase {',
|
|
' public:',
|
|
' bool Add();',
|
|
' private:',
|
|
' class DescriptorIndex;',
|
|
' std::unique_ptr<DescriptorIndex> index_;',
|
|
'};',
|
|
),
|
|
'db/database.cc': lines(
|
|
'#include "db/database.h"',
|
|
'class EncodedDatabase::DescriptorIndex {',
|
|
' public:',
|
|
' bool AddFile() { return true; }',
|
|
'};',
|
|
'bool EncodedDatabase::Add() {',
|
|
' return index_->AddFile();',
|
|
'}',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'EncodedDatabase::Add')).toEqual(['EncodedDatabase::DescriptorIndex::AddFile']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('an element of a library type has no project method to call', async () => {
|
|
const cg = await indexed({
|
|
'util/library.h': lines(
|
|
'class ObjectLibrary {',
|
|
' public:',
|
|
' ObjectLibrary* get() { return this; }',
|
|
'};',
|
|
),
|
|
'util/registry.cc': lines(
|
|
'#include <memory>',
|
|
'#include <vector>',
|
|
'#include "util/library.h"',
|
|
'class Registry {',
|
|
' public:',
|
|
' bool Newest() const {',
|
|
' auto iter = libraries_.crbegin();',
|
|
' return iter->get() != nullptr;',
|
|
' }',
|
|
' private:',
|
|
' std::vector<std::shared_ptr<ObjectLibrary>> libraries_;',
|
|
'};',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'Registry::Newest')).toEqual([]);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('a project class named like a library container keeps what its own `begin()` returns', async () => {
|
|
const cg = await indexed({
|
|
'util/span.h': lines(
|
|
'class Foo {',
|
|
' public:',
|
|
' void Bar() {}',
|
|
'};',
|
|
'class Baz {',
|
|
' public:',
|
|
' void Bar() {}',
|
|
'};',
|
|
'namespace util {',
|
|
'class span {',
|
|
' public:',
|
|
' Foo* begin() { return nullptr; }',
|
|
'};',
|
|
'} // namespace util',
|
|
),
|
|
'util/use.cc': lines(
|
|
'#include "util/span.h"',
|
|
'void Use(util::span s) {',
|
|
' auto it = s.begin();',
|
|
' it->Bar();',
|
|
'}',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'Use')).toEqual(['Foo::Bar', 'util::span::begin']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
});
|