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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call with a comparison or a shift among its arguments, like protocolbuffers/protobuf's `p->Emit({{"declare", has_bit_word_index < 0 ? … : ""}, …}, "…")` or `WriteVarint(num << 3, s)`, is now read with the right number of arguments, so it links to the overload that fits rather than one taking fewer. A parameter or variable whose type has template arguments is now read even after another such parameter on the same line, like facebook/rocksdb's `const autovector<uint32_t>& cf_ids`, or with empty ones, like fmt's `parse_context<>& ctx`: calls on it link to that class's methods, and calls on a `std::vector` declared that way no longer link to a project class's method of the same name. Re-index C++ projects after upgrading.
- In C++, a call passing `nullptr` now links to the overload that takes a `std::nullptr_t`, as C++ picks it, rather than one taking a pointer, and a call no longer links to an overload declared `= delete`, which no call that compiles can reach: protocolbuffers/protobuf's `ptr.reset(nullptr)` on an `Arena::UniquePtr` reaches `reset(std::nullptr_t)`, and `field.Add("foo")` on a `RepeatedPtrField` reaches `Add(Element&&)` instead of the deleted `Add(const Element&)`. Re-index C++ projects after upgrading.
- In C++, a variable or parameter declared over more than one line, with its type begun on a line above or alone on the line above its name, now has its type read, so calls on it link to that class's methods: protocolbuffers/protobuf's `extension.number()` on a `const google::protobuf::internal::ExtensionIdentifier<` … `>& extension` parameter reaches protobuf's `ExtensionIdentifier`, not hpb's, and facebook/rocksdb's `cf_sv_pairs.size()` on an `autovector<…>` written above `cf_sv_pairs;` reaches `autovector`. Calls on a standard-library container declared that way no longer link to a project class's method of the same name. Re-index C++ projects after upgrading.
- In C++, a variable initialized from a call of a member function template with explicit template arguments now has the type that template returns for them, so calls on it link to that class's methods: protocolbuffers/protobuf's `const auto& rep = reflection->GetRaw<RepeatedPtrField<…>>(…)` is a `RepeatedPtrField`, and facebook/rocksdb's `auto bbto = factory->GetOptions<BlockBasedTableOptions>()` a `BlockBasedTableOptions`. Re-index C++ projects after upgrading.
- Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading.
- In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged.
- In C and C++, a call written from the global scope, like `::leveldb::RepairDB(…)` or `::hpb::CreateMessage(…)`, now links to the function of exactly that name, and one through a namespace alias, like protobuf's `::_pbi::…` calls, to the function in the aliased namespace. Before, these calls were dropped unless the repository also held Swift and Objective-C files, and even then a name spelled out in full, like `::google::protobuf::internal::TSanWrite(…)`, linked to nothing, while `::operator delete(…)` linked to one class's own `operator delete`. A global call never links to a function or method of the same name inside a namespace or class, such as a wrapper `open()` that calls the C library's `::open(…)`, and `::std::…` and `::testing::…` calls stay unlinked unless that library's source is part of your project. Re-index C and C++ projects after upgrading.
Expand Down
5 changes: 3 additions & 2 deletions __tests__/cpp-auto-local-receiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,9 @@ describe('what keeps the call as it was', () => {
),
});
try {
// Not the shared_ptr<MergeOperator> member's type, which has no SetName.
expect(calls(cg, 'rocksdb::Rename').filter((c) => c.includes('SetName'))).toEqual(['rocksdb::test::ChanglingMergeOperator::SetName @0.7']);
// Not the shared_ptr<MergeOperator> member's type, which has no SetName:
// the type `CheckedCast<test::ChanglingMergeOperator>()` returns.
expect(calls(cg, 'rocksdb::Rename').filter((c) => c.includes('SetName'))).toEqual(['rocksdb::test::ChanglingMergeOperator::SetName @0.9']);
} finally {
cg.close();
}
Expand Down
189 changes: 189 additions & 0 deletions __tests__/cpp-member-template-returns.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/**
* 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<RepeatedPtrField<CPPTYPE>>(…)` was no call it could
* read; and a return type that is the template's own parameter (`template
* <typename Type> 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<BlockBasedTableOptions>();` 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<string, string>): Promise<CodeGraph> {
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<RepeatedPtrField<…>>, its line spliced with `\\`", async () => {
const cg = await indexed({
'src/google/protobuf/reflection.h': lines(
'namespace google {',
'namespace protobuf {',
'class Message {};',
'class FieldDescriptor {};',
'template <typename Element>',
'class RepeatedPtrField {',
' public:',
' int size() const { return 0; }',
'};',
'class SooRep {',
' public:',
' int size() const { return 0; }',
'};',
'class Reflection {',
' public:',
' template <typename Type>',
' const Type& GetRaw(const Message& message, const FieldDescriptor* field) const;',
'};',
'template <typename Type>',
'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<RepeatedPtrField<int>>(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<BlockBasedTableOptions>(), const and not", async () => {
const cg = await indexed({
'include/rocksdb/configurable.h': lines(
'namespace rocksdb {',
'class Configurable {',
' public:',
' template <typename T>',
' const T* GetOptions() const { return nullptr; }',
' template <typename T>',
' 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<BlockBasedTableOptions>();',
' 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 <typename T>',
' T* Find() { return nullptr; }',
' template <typename T = Widget>',
' T* Make() { return nullptr; }',
' template <typename A, typename B>',
' A* Get() { return nullptr; }',
' template <typename A, typename B>',
' B* Get(int key) { return nullptr; }',
'};',
'void Found(Store* store) {',
' auto found = store->Find<Gadget>();',
' found->Draw();',
'}',
'void Made(Store* store) {',
' auto made = store->Make<Gadget>();',
' made->Draw();',
'}',
'void Got(Store* store) {',
' auto got = store->Get<Widget, Gadget>(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();
}
});
});
74 changes: 69 additions & 5 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9397,6 +9397,61 @@ function cppWrittenReturnType(fn: Node, context: ResolutionContext): string | nu
return written;
}

/**
* The names of the template parameters a C++ function template's own
* `template <…>` header declares, in order, without their defaults:
* `['Type']` for protobuf's `template <typename Type> const Type& GetRaw(…)`.
* A template's node starts at its declaration, so the header is the code
* right before it. Empty for a function that is no template.
*/
function cppOwnTemplateParameters(fn: Node, context: ResolutionContext): string[] {
const lines = context.getFileLines?.(fn.filePath) ?? context.readFile(fn.filePath)?.split(/\r?\n/);
if (!lines) return [];
const at = lines[fn.startLine - 1] ?? '';
const before = [...lines.slice(Math.max(0, fn.startLine - 4), fn.startLine - 1), at.slice(0, fn.startColumn)]
.map((line) => cppCodeOf(line) ?? '').join(' ').trimEnd();
let depth = 0;
for (let i = before.endsWith('>') ? before.length - 1 : -1; i >= 0; i--) {
if (before[i] === '>') depth++;
else if (before[i] === '<' && --depth === 0) {
if (!/\btemplate\s*$/.test(before.slice(0, i))) return [];
return splitCppTopLevel(before.slice(i + 1, -1)).map((p) => /([A-Za-z_]\w*)$/.exec(p.split('=')[0]!.trim())?.[1] ?? '');
}
}
return [];
}

/**
* What a call of the C++ member template whose overloads are `methods`, with
* the explicit template arguments `args` (`<…>` as written), returns when
* every overload is declared to return the same one of its own template
* parameters, perhaps const, a pointer or a reference: that argument, so
* qualified, as written at the call (`site`). protobuf's `template <typename
* Type> const Type& GetRaw(…)` called as `reflection->GetRaw<RepeatedPtrField<
* std::string>>(…)` returns a `const RepeatedPtrField<std::string>&`, and
* rocksdb's `T* GetOptions()` called as `GetOptions<BlockBasedTableOptions>()`
* a `BlockBasedTableOptions*`. Undefined otherwise: then the return type is
* read as written.
*/
function cppMemberTemplateReturn(
methods: readonly Node[],
args: string,
site: Omit<CppWrittenType, 'text'>,
context: ResolutionContext,
): CppWrittenType | undefined {
let slot: { index: number; written: string; param: string } | undefined;
for (const fn of methods) {
const written = cppWrittenReturnType(fn, context);
if (!written) continue;
const param = written.replace(/\b(?:const|volatile)\b|[*&]/g, ' ').trim();
const index = param ? cppOwnTemplateParameters(fn, context).indexOf(param) : -1;
if (index < 0 || (slot && slot.index !== index)) return undefined;
slot ??= { index, written, param };
}
const arg = slot && splitCppTopLevel(args.slice(1, -1))[slot.index];
return slot && arg ? { ...site, text: slot.written.replace(new RegExp(`\\b${slot.param}\\b`), arg) } : undefined;
}

/** What one of `fns` (overloads, a declaration and its definition) returns, as written where it is declared. */
function cppReturnTypeOf(fns: readonly Node[], context: ResolutionContext): CppWrittenType | null {
for (const fn of fns) {
Expand Down Expand Up @@ -9785,12 +9840,17 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC
const returned = wrapper && cppWrapperReturnType(wrapper);
if (returned) return returned;
}
// `recv.method(…)`, `recv->method(…)`: the method of the receiver's class.
const member = /^(.+?)\s*(\.|->)\s*([A-Za-z_]\w*)$/.exec(callee);
// `recv.method(…)`, `recv->method(…)`, `recv->method<T>(…)`: the method of the receiver's class.
const member = /^(.+?)\s*(\.|->)\s*([A-Za-z_]\w*)\s*(<[\s\S]*>)?$/.exec(callee);
if (member) {
const object = cppMemberObject(member[1]!, member[2]!, at, context, depth);
const cls = object && cppWrittenClass(object, at, context);
return cls ? cppReturnTypeOf(cppMethodsOf(cls, member[3]!, at, context), context) : null;
if (!cls) return null;
const methods = cppMethodsOf(cls, member[3]!, at, context);
if (!member[4]) return cppReturnTypeOf(methods, context);
// Explicit template arguments call a template, never an overload that is none.
const templates = methods.filter((fn) => cppOwnTemplateParameters(fn, context).length > 0);
return cppMemberTemplateReturn(templates, member[4], callers, context) ?? cppReturnTypeOf(templates, context);
}
if (!/^(?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*(?:\s*<[\s\S]*>)?$/.test(callee)) return null;
const spelled = stripCppTemplateArguments(callee).replace(/\s+/g, '');
Expand Down Expand Up @@ -11354,19 +11414,23 @@ function cppAutoInitializerStatement(

/**
* The C++ statement that `text`, the rest of `lines[index]`, begins, read on
* to the `;` that ends it. Null when no `;` ends it within a few lines.
* to the `;` that ends it. Null when no `;` ends it within a few lines. A
* macro body's lines end in `\`, which splices them: protobuf's `const auto&
* rep = \` goes on with `reflection->GetRaw<…>(message, field); \`.
*/
function cppStatementFrom(
text: string,
lines: readonly string[],
index: number,
rawLines: ReturnType<typeof cppNonCodeLines>,
): string | null {
const spliced = (code: string): string => code.replace(/\\\s*$/, ' ');
text = spliced(text);
for (let next = index + 1; ; next++) {
const end = cppStatementEnd(text);
if (end >= 0) return text.slice(0, end).trim() || null;
if (next >= lines.length || next > index + 4) return null;
text += ` ${cppCodeOf(cppLiteralsBlanked(lines[next]!, next + 1, rawLines)) ?? ''}`;
text += ` ${spliced(cppCodeOf(cppLiteralsBlanked(lines[next]!, next + 1, rawLines)) ?? '')}`;
}
}

Expand Down