/** * An `auto` local initialized from a call of a C++ member template with * explicit template arguments has the type the template returns for them. * A member call was read only when its callee ended in the method's name, so * `reflection->GetRaw>(…)` was no call it could * read; and a return type that is the template's own parameter (`template * const Type& GetRaw(…)`) is what the arguments make it. * protocolbuffers/protobuf's `rep.size()` on such a local, in a macro whose * lines end in `\`, guessed `SooRep::size`; facebook/rocksdb's `auto bbto = * factory->GetOptions();` gave `bbto` no type. */ 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'; const roots: string[] = []; afterEach(() => { for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); }); async function indexed(files: Record): Promise { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-member-template-')); 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 }); } const lines = (...text: string[]): string => [...text, ''].join('\n'); /** What a function's calls of `method` reach, as `qualifiedName resolvedBy@confidence`. */ function calls(cg: CodeGraph, qualifiedName: string, method: string): string[] { const from = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName)!; return cg.getOutgoingEdgesFrom([from.id], ['calls']) .map((e) => { const meta = e.metadata as { confidence?: number; resolvedBy?: string } | undefined; return `${cg.getNode(e.target)?.qualifiedName} ${meta?.resolvedBy}@${meta?.confidence}`; }) .filter((t) => t.split(' ')[0]!.endsWith(`::${method}`)) .sort(); } const SHAPES = lines( 'class Widget {', ' public:', ' void Draw() {}', '};', 'class Gadget {', ' public:', ' void Draw() {}', '};', ); describe('C++ member templates called with explicit template arguments', () => { it("types protobuf's `const auto& rep =` from GetRaw>, its line spliced with `\\`", async () => { const cg = await indexed({ 'src/google/protobuf/reflection.h': lines( 'namespace google {', 'namespace protobuf {', 'class Message {};', 'class FieldDescriptor {};', 'template ', 'class RepeatedPtrField {', ' public:', ' int size() const { return 0; }', '};', 'class SooRep {', ' public:', ' int size() const { return 0; }', '};', 'class Reflection {', ' public:', ' template ', ' const Type& GetRaw(const Message& message, const FieldDescriptor* field) const;', '};', 'template ', 'const Type& Reflection::GetRaw(const Message& message, const FieldDescriptor* field) const {', ' static Type value;', ' return value;', '}', '} // namespace protobuf', '} // namespace google', ), 'src/google/protobuf/reflection_visit_fields.h': lines( '#include "src/google/protobuf/reflection.h"', 'namespace google {', 'namespace protobuf {', 'int VisitFields(const Reflection* reflection, const Message& message,', ' const FieldDescriptor* field) {', // protobuf writes this in a macro, whose lines end in `\`. ' const auto& rep = \\', ' reflection->GetRaw>(message, field); \\', ' ;', ' return rep.size();', '}', '} // namespace protobuf', '} // namespace google', ), }); try { expect(calls(cg, 'google::protobuf::VisitFields', 'size')).toEqual(['google::protobuf::RepeatedPtrField::size instance-method@0.9']); } finally { cg.close(); } }); it("types rocksdb's `auto bbto =` from GetOptions(), const and not", async () => { const cg = await indexed({ 'include/rocksdb/configurable.h': lines( 'namespace rocksdb {', 'class Configurable {', ' public:', ' template ', ' const T* GetOptions() const { return nullptr; }', ' template ', ' T* GetOptions() { return nullptr; }', '};', 'struct BlockBasedTableOptions {', ' bool IsValid() const { return true; }', '};', 'struct PlainTableOptions {', ' bool IsValid() const { return true; }', '};', '} // namespace rocksdb', ), 'db/check.cc': lines( '#include "include/rocksdb/configurable.h"', 'namespace rocksdb {', 'bool Check(Configurable* factory) {', ' auto bbto = factory->GetOptions();', ' return bbto->IsValid();', '}', '} // namespace rocksdb', ), }); try { expect(calls(cg, 'rocksdb::Check', 'IsValid')).toEqual(['rocksdb::BlockBasedTableOptions::IsValid instance-method@0.9']); } finally { cg.close(); } }); it('reads a template overload only, a defaulted parameter by its name, and no guess between overloads', async () => { const cg = await indexed({ 'shapes.h': SHAPES, 'store.cc': lines( '#include "shapes.h"', 'class Store {', ' public:', ' Widget* Find() { return nullptr; }', ' template ', ' T* Find() { return nullptr; }', ' template ', ' T* Make() { return nullptr; }', ' template ', ' A* Get() { return nullptr; }', ' template ', ' B* Get(int key) { return nullptr; }', '};', 'void Found(Store* store) {', ' auto found = store->Find();', ' found->Draw();', '}', 'void Made(Store* store) {', ' auto made = store->Make();', ' made->Draw();', '}', 'void Got(Store* store) {', ' auto got = store->Get(1);', ' got->Draw();', '}', ), }); try { // Explicit template arguments call `T* Find()`, never `Widget* Find()`. expect(calls(cg, 'Found', 'Draw')).toEqual(['Gadget::Draw instance-method@0.9']); expect(calls(cg, 'Made', 'Draw')).toEqual(['Gadget::Draw instance-method@0.9']); // The overloads return different parameters; which one the call takes // isn't read, so neither is guessed. expect(calls(cg, 'Got', 'Draw').filter((t) => t.endsWith('@0.9'))).toEqual([]); } finally { cg.close(); } }); });