mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 03:49:59 +08:00
The extractor records a call on a member chain (`a.b.m()`, `a->b->m()`,
`this->p_.m()`) by its bare method name, so it went to whichever project
method shared the name. A census of the cgstd kit found most such calls
linked by name alone, and most of those links wrong. Above all, they were
on members of standard-library types, whose methods (`size`, `reset`,
`push_back`, `get`, `empty`) a large codebase has namesakes of.
matchCppOutsideMemberChainCall reads the chain where the call is written
(cppMemberChainReceiver) and types it with the existing cppMemberObject:
- a data member is typed by its declared type in its class
(cppDataMemberType);
- `->` goes through a pointer, smart pointer, optional or iterator.
It gives no edge when the object the method is called on is a library type
(isCppLibraryType) or a type under a namespace the project declares nothing
in (isCppOutsideQualifiedType). These are the call-chain resolver's rules.
Unlike a call chain, the operator is read as written, so `.` on a
`std::vector<T>` or a `std::unique_ptr<T>` is that type's own member,
whatever `T` is.
Anything else, a member of a project class included, goes to the name
strategies as before.
Kit A/B against main a2570695 (only removals; nothing added or retargeted):
| repo | calls removed |
|---|---|
| rocksdb | 2,001 |
| protobuf | 234 |
| leveldb | 15 |
| fmt | 12 |
| redis | 0 (identical) |
Every removed link was a bare-name guess. Of the 2,235 removed sites in
rocksdb and protobuf, a judge reading the members' declarations across each
repo rated:
- 2,044 as wrong links (a library type's namesake, or an unrelated class);
- 165 unknown;
- 10 not counted as chains by its pattern (after a cast, like
`(int)data->entries.size()`);
- 16 right. These turned out to be name collisions on the judge's side,
checked in the source (`std::deque logs_`, `std::shared_ptr decompressor`,
`std::vector ids`).
24 random removals were also read by hand, and all were correct.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
5.1 KiB
TypeScript
151 lines
5.1 KiB
TypeScript
/**
|
|
* A C++ call on a data member reached through a member chain, like rocksdb's
|
|
* `options.cf_paths.emplace_back(…)` on a `std::vector<DbPath>` or
|
|
* `props->rep.db_id.size()` on a `std::string`, calls the library type's own
|
|
* method, none of the project's. The extractor records such a call by its
|
|
* bare name, so it went to any project method of that name: rocksdb's
|
|
* `autovector::emplace_back`, `BlobContents::size`.
|
|
*/
|
|
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 on a member chain of a type from outside the project', () => {
|
|
let root: string;
|
|
let cg: CodeGraph;
|
|
|
|
beforeAll(async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-member-chain-'));
|
|
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 project's namesakes of library methods, which a call matched by its bare name lands on.
|
|
write('src/types.h', lines(
|
|
'namespace rocksdb {',
|
|
'template <class T, int kSize = 8>',
|
|
'class autovector {',
|
|
' public:',
|
|
' void emplace_back(T v) {}',
|
|
' void clear() {}',
|
|
' int size() const { return 0; }',
|
|
'};',
|
|
'class BlobContents {',
|
|
' public:',
|
|
' int size() const { return 0; }',
|
|
'};',
|
|
'} // namespace rocksdb',
|
|
'namespace proto {',
|
|
'class Option {',
|
|
' public:',
|
|
' bool has_value() const { return false; }',
|
|
'};',
|
|
'} // namespace proto',
|
|
'namespace util {',
|
|
'class Status {',
|
|
' public:',
|
|
' bool ok() const { return true; }',
|
|
'};',
|
|
'} // namespace util',
|
|
'class FilePtr {',
|
|
' public:',
|
|
' void reset() {}',
|
|
'};',
|
|
'class Engine {',
|
|
' public:',
|
|
' void Run() {}',
|
|
'};',
|
|
));
|
|
write('src/use.cc', lines(
|
|
'#include <memory>',
|
|
'#include <optional>',
|
|
'#include <string>',
|
|
'#include <vector>',
|
|
'#include "absl/status/status.h"',
|
|
'#include "src/types.h"',
|
|
'struct Options {',
|
|
' std::vector<int> cf_paths;',
|
|
' std::string name;',
|
|
'};',
|
|
'struct Rep {',
|
|
' std::string db_id;',
|
|
'};',
|
|
'struct Props {',
|
|
' Rep rep;',
|
|
'};',
|
|
'struct State {',
|
|
' std::optional<int> child_pid;',
|
|
' std::shared_ptr<Engine> engine;',
|
|
' Engine plain;',
|
|
' absl::Status status;',
|
|
'};',
|
|
'void Library(Options& options) { options.cf_paths.emplace_back(1); options.name.size(); }',
|
|
'void TwoHops(Props* props) { props->rep.db_id.size(); }',
|
|
'void Optional(State* state) { state->child_pid.has_value(); }',
|
|
'void HolderOwn(State& state) { state.engine.reset(); }',
|
|
'void OutsideQualified(State& state) { state.status.ok(); }',
|
|
'void HolderArrow(State& state) { state.engine->Run(); }',
|
|
'void ProjectMember(State& state) { state.plain.Run(); }',
|
|
'class Cache {',
|
|
' public:',
|
|
' void Touch() { this->keys_.clear(); this->engine_->Run(); }',
|
|
' private:',
|
|
' std::vector<int> keys_;',
|
|
' Engine* engine_;',
|
|
'};',
|
|
'template <class T>',
|
|
'struct Box {',
|
|
' std::vector<T> items;',
|
|
' std::unique_ptr<T> owner;',
|
|
'};',
|
|
'template <class T>',
|
|
'void Drain(Box<T>& box) { box.items.clear(); box.owner.reset(); }',
|
|
));
|
|
cg = await CodeGraph.init(root, { index: true });
|
|
}, 120_000);
|
|
|
|
afterAll(() => {
|
|
cg?.close();
|
|
if (root) fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
/** `calls` callees of the function or method `name`, as `qualifiedName`. */
|
|
const calls = (name: string): string[] => {
|
|
const caller = [...cg.getNodesByKind('function'), ...cg.getNodesByKind('method')].find((n: Node) => n.name === name);
|
|
expect(caller, name).toBeDefined();
|
|
return cg.getCallees(caller!.id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.qualifiedName).sort();
|
|
};
|
|
|
|
it("a library container's or string's method reached through a member", () => {
|
|
expect(calls('Library')).toEqual([]);
|
|
expect(calls('TwoHops')).toEqual([]);
|
|
});
|
|
|
|
it("an optional's method, and a smart pointer's own through `.`", () => {
|
|
expect(calls('Optional')).toEqual([]);
|
|
expect(calls('HolderOwn')).toEqual([]);
|
|
});
|
|
|
|
it('a type under a namespace the project declares nothing in', () => {
|
|
expect(calls('OutsideQualified')).toEqual([]);
|
|
});
|
|
|
|
it("`->` on a smart pointer, and a member of a project class's type, still call the project's", () => {
|
|
expect(calls('HolderArrow')).toEqual(['Engine::Run']);
|
|
expect(calls('ProjectMember')).toEqual(['Engine::Run']);
|
|
});
|
|
|
|
it('a chain through `this`', () => {
|
|
expect(calls('Touch')).toEqual(['Engine::Run']);
|
|
});
|
|
|
|
it("a library template of a template parameter's type is still the library's", () => {
|
|
expect(calls('Drain')).toEqual([]);
|
|
});
|
|
});
|