mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
Step 2 of member chains; #2626 was step 1. matchCppMemberChainCall (formerly
matchCppOutsideMemberChainCall) now also resolves a chain whose member is
of a project class, in the same steps matchCppUnspelledReceiverCall takes
for `it->second.Flush()`:
- the class C++ lookup finds for the member's type, where that type is
written;
- then its method, or one it inherits (cppMethodOf, with overloads picked
by argument types);
- or no edge when the class surely lacks it (cppLacksMethod).
Before, such a call went by its bare name to whichever class had a method
of that name. Inside a method it could also be taken for an implicit
`this` call on the method's own class.
A member is now read from the class node C++ lookup found (cppDataMemberIn),
not from the first class of that qualified name that declares it
(cppDataMemberType, removed). rocksdb's db_bench_tool.cc and
db_stress_shared_state.h each declare a ROCKSDB_NAMESPACE::ThreadState,
whose `rand` is a Random64 in one and a Random in the other.
Kit A/B against main 6e884d7b:
| repo | added | retargeted | same target, now typed | removed |
|---|---|---|---|---|
| rocksdb | 571 | 2,008 | 1,738 | 2 |
| protobuf | 42 | 95 | 142 | 0 |
| leveldb | 42 | 30 | 141 | 0 |
| fmt | 0 | 56 | 20 | 0 |
| redis | 0 | 0 | 0 | 0 (identical) |
The most common rocksdb retargets are DBImpl -> DB (calls on a `DB*`),
BlobContents -> Slice, BlobFileReader -> FileDescriptor and
EnvWrapper -> Env. Hand-reading random samples found one wrong
retarget (db_bench's `thread->rand.Next()`, the ThreadState namesake),
which led to the member-lookup fix above. After that fix, all 76 sites
sampled across the four repos were correct:
- 30 retargets and additions, plus both removals, in rocksdb;
- 20 in protobuf, 14 in leveldb and 10 in fmt.
The two removed calls are `task_reader->rep_->GetCoroutine(…)` on a
`SstFileReader::Rep`, which has no such method. They had linked to
`CoroDB`'s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
5.6 KiB
TypeScript
177 lines
5.6 KiB
TypeScript
/**
|
|
* A C++ call on a data member reached through a member chain, like rocksdb's
|
|
* `thread->rnd.Next()` on a `Random rnd` or `meta.smallest.user_key()` on an
|
|
* `InternalKey smallest`, calls the method of the class the member is
|
|
* declared as, or of a class it derives from. The extractor records such a
|
|
* call by its bare name, so it went to whichever class had a method of that
|
|
* name: `ArenaWrappedDBIter::Next`, `ClippingIterator::user_key`, or the
|
|
* calling method's own class.
|
|
*/
|
|
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 project class', () => {
|
|
let root: string;
|
|
let cg: CodeGraph;
|
|
|
|
beforeAll(async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-member-chain-project-'));
|
|
const write = (file: string, text: string): void => {
|
|
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
|
fs.writeFileSync(path.join(root, file), text);
|
|
};
|
|
// Each namesake comes first: a call matched by its bare name lands on it.
|
|
write('src/types.h', lines(
|
|
'class ArenaWrappedDBIter {',
|
|
' public:',
|
|
' void Next() {}',
|
|
'};',
|
|
'class Random {',
|
|
' public:',
|
|
' unsigned Next() { return 0; }',
|
|
'};',
|
|
'class ClippingIterator {',
|
|
' public:',
|
|
' int user_key() const { return 1; }',
|
|
'};',
|
|
'class InternalKey {',
|
|
' public:',
|
|
' int user_key() const { return 0; }',
|
|
'};',
|
|
'class InMemoryEnv {',
|
|
' public:',
|
|
' void NewWritableFile() {}',
|
|
'};',
|
|
'class Env {',
|
|
' public:',
|
|
' virtual void NewWritableFile() {}',
|
|
'};',
|
|
'class Other {',
|
|
' public:',
|
|
' void Shared() {}',
|
|
'};',
|
|
'class Base {',
|
|
' public:',
|
|
' void Shared() {}',
|
|
'};',
|
|
'class Derived : public Base {};',
|
|
'class Free {',
|
|
' public:',
|
|
' void Missing() {}',
|
|
'};',
|
|
'class Lacking {',
|
|
' public:',
|
|
' void Something() {}',
|
|
'};',
|
|
'class Cache {',
|
|
' public:',
|
|
' void Put(int k, int v) {}',
|
|
'};',
|
|
'class Table {',
|
|
' public:',
|
|
' void Put(int k) {}',
|
|
' void Put(int k, int v) {}',
|
|
'};',
|
|
'class Random64 {',
|
|
' public:',
|
|
' unsigned Next() { return 1; }',
|
|
'};',
|
|
));
|
|
// Two files each declare a `SharedState` of their own, with members of the same name.
|
|
write('src/a_shared.h', lines(
|
|
'#include "src/types.h"',
|
|
'struct SharedState {',
|
|
' Random rand;',
|
|
'};',
|
|
));
|
|
write('src/bench.cc', lines(
|
|
'#include "src/types.h"',
|
|
'struct SharedState {',
|
|
' Random64 rand;',
|
|
'};',
|
|
'void Bench(SharedState* shared) { shared->rand.Next(); }',
|
|
));
|
|
write('src/use.cc', lines(
|
|
'#include "src/types.h"',
|
|
'struct ThreadState {',
|
|
' Random rnd;',
|
|
'};',
|
|
'struct FileMeta {',
|
|
' InternalKey smallest;',
|
|
'};',
|
|
'struct Options {',
|
|
' Env* env;',
|
|
'};',
|
|
'struct Holder {',
|
|
' Derived d;',
|
|
' Lacking l;',
|
|
' Table table;',
|
|
'};',
|
|
'struct Outer {',
|
|
' ThreadState inner;',
|
|
'};',
|
|
'void Rand(ThreadState* thread) { thread->rnd.Next(); }',
|
|
'void Key(FileMeta& meta) { meta.smallest.user_key(); }',
|
|
'void EnvCall(Options& options) { options.env->NewWritableFile(); }',
|
|
'void TwoHops(Outer* o) { o->inner.rnd.Next(); }',
|
|
'void Inherited(Holder& h) { h.d.Shared(); }',
|
|
'void Lacks(Holder& h) { h.l.Missing(); }',
|
|
'void Overload(Holder& h) { h.table.Put(1, 2); }',
|
|
'class Worker {',
|
|
' public:',
|
|
' void Next() {}',
|
|
' void Step() { this->state_.rnd.Next(); }',
|
|
' private:',
|
|
' ThreadState state_;',
|
|
'};',
|
|
));
|
|
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` (`:line` with `withLine`). */
|
|
const calls = (name: string, withLine = false): 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) => (withLine ? `${r.node.qualifiedName}:${r.node.startLine}` : r.node.qualifiedName)).sort();
|
|
};
|
|
|
|
it("calls the method of the member's class, not a namesake's", () => {
|
|
expect(calls('Rand')).toEqual(['Random::Next']);
|
|
expect(calls('Key')).toEqual(['InternalKey::user_key']);
|
|
expect(calls('EnvCall')).toEqual(['Env::NewWritableFile']);
|
|
});
|
|
|
|
it('through two members', () => {
|
|
expect(calls('TwoHops')).toEqual(['Random::Next']);
|
|
});
|
|
|
|
it("a method the member's class inherits, and none when the class lacks it", () => {
|
|
expect(calls('Inherited')).toEqual(['Base::Shared']);
|
|
expect(calls('Lacks')).toEqual([]);
|
|
});
|
|
|
|
it("of the class's overloads, the one the arguments fit", () => {
|
|
expect(calls('Overload', true)).toEqual(['Table::Put:49']);
|
|
});
|
|
|
|
it("a chain through `this` is no call on the calling method's own class", () => {
|
|
expect(calls('Step')).toEqual(['Random::Next']);
|
|
});
|
|
|
|
it("a member of the file's own class, not of another file's class of that name", () => {
|
|
expect(calls('Bench')).toEqual(['Random64::Next']);
|
|
});
|
|
});
|