fix(cpp): a receiver declared over more than one line has its type read (#2555)

Receiver inference read a declaration a line at a time, so a receiver whose
type began on a line above (protobuf's `const
google::protobuf::internal::ExtensionIdentifier<` over `FeatureSet,
TypeTraitsT, field_type, is_packed>& extension`) or sat alone on the line above
its name (rocksdb's `autovector<…>` over `cf_sv_pairs;`) declared nothing it
could read, and the call fell to name matching. Such a line is now read with
the lines it began on: joined until the template brackets pair, or with the
type line above, as class members already were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-14 07:33:54 +00:00
committed by GitHub
parent 8cba7e5370
commit e03ff6730a
4 changed files with 223 additions and 5 deletions
+1
View File
@@ -114,6 +114,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a declaration continued on a line that starts with `*`, like facebook/rocksdb's `const auto& el =` over `*reinterpret_cast<std::shared_ptr<EventListener>*>(jhandle);`, is now read, so calls such as `el->OnFlushCompleted(…)` link to `EventListener`'s methods; such a line used to be skipped as part of a comment. Code commented out in a `/* … */` block, and the text of a string literal, like protocolbuffers/protobuf's `[[deprecated("Please use SpaceUsedLong() instead")]]`, are no longer read as declarations, which had sent a call to the wrong method or to none. Re-index C++ projects after upgrading.
- 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.
- 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.
@@ -195,11 +195,12 @@ describe('a C++ receiver declared with nested template arguments', () => {
}
});
it('the end of a declaration begun on the line above names no type, not even its template argument\'s', async () => {
it('the end of a declaration begun on the line above is read with that line, not as its template argument\'s type', async () => {
// rocksdb's blob_file_reader.h breaks `autovector<std::pair<…,` and
// `std::unique_ptr<BlobContents>>>& blob_reqs` over two lines. The second
// line alone, read as a type, is a `CachableEntry` here: the receiver's
// type is the `autovector` on the line above.
// type is the `autovector` on the line above, which the two lines read
// together declare.
const cg = await indexed({
...ROCKSDB,
'table/block_based/cachable_entry.h': [
@@ -228,7 +229,7 @@ describe('a C++ receiver declared with nested template arguments', () => {
].join('\n'),
});
try {
expect(calls(cg, 'rocksdb::MultiGetBlob')).toEqual([]);
expect(resolvedCalls(cg, 'rocksdb::MultiGetBlob')).toEqual(['rocksdb::autovector::size instance-method@0.9']);
} finally {
cg.close();
}
+164
View File
@@ -0,0 +1,164 @@
/**
* A C++ receiver whose declaration is written over more than one line: its
* type begun on a line above, or alone on the line above its name. Receiver
* inference read a declaration a line at a time, so the line naming the
* receiver declared nothing it could read, and the call fell to name matching:
* protocolbuffers/protobuf's `extension.number()` on a `const
* google::protobuf::internal::ExtensionIdentifier<` / `FeatureSet,
* TypeTraitsT, field_type, is_packed>& extension` reached hpb's
* `ExtensionIdentifier::number`. The lines are now read together.
*/
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-two-line-'));
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 or method's calls reach, as `qualifiedName resolvedBy@confidence`. */
function calls(cg: CodeGraph, qualifiedName: 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}`;
})
.sort();
}
const TABLES = lines(
'#include <memory>',
'#include <string>',
'template <typename K, typename V>',
'class Table {',
' public:',
' int size() const { return 0; }',
'};',
'class Index {',
' public:',
' int size() const { return 0; }',
'};',
);
describe('C++ receivers declared over more than one line', () => {
it("reads a type begun on a line above: protobuf's ExtensionIdentifier parameter", async () => {
const cg = await indexed({
'src/google/protobuf/extension_set.h': lines(
'#include <cstdint>',
'namespace google {',
'namespace protobuf {',
'namespace internal {',
'template <typename ExtendeeType, typename TypeTraitsType, uint8_t field_type, bool is_packed>',
'class ExtensionIdentifier {',
' public:',
' int number() const { return 0; }',
'};',
'} // namespace internal',
'} // namespace protobuf',
'} // namespace google',
),
'hpb/extension.h': lines(
'namespace hpb {',
'namespace internal {',
'template <typename T>',
'class ExtensionIdentifier {',
' public:',
' int number() const { return 0; }',
'};',
'} // namespace internal',
'} // namespace hpb',
),
'src/google/protobuf/descriptor.h': lines(
'#include "src/google/protobuf/extension_set.h"',
'namespace google {',
'namespace protobuf {',
'class FeatureSet {};',
'class DescriptorPool {',
' public:',
' template <typename TypeTraitsT, uint8_t field_type, bool is_packed>',
' bool ResolvesFeaturesFor(',
' const google::protobuf::internal::ExtensionIdentifier<',
' FeatureSet, TypeTraitsT, field_type, is_packed>& extension) const {',
' return extension.number() > 0;',
' }',
'};',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
expect(calls(cg, 'google::protobuf::DescriptorPool::ResolvesFeaturesFor'))
.toEqual(['google::protobuf::internal::ExtensionIdentifier::number instance-method@0.9']);
} finally {
cg.close();
}
});
it('reads a type whose template arguments run over several lines above', async () => {
const cg = await indexed({
'tables.h': TABLES,
'visit.cc': lines(
'#include "tables.h"',
'int Visit(',
' const Table<',
' std::string,',
' std::shared_ptr<const Index>>& table) {',
' return table.size();',
'}',
),
});
try {
expect(calls(cg, 'Visit')).toEqual(['Table::size instance-method@0.9']);
} finally {
cg.close();
}
});
it('reads a type written alone on the line above the name, and no expression continued there', async () => {
const cg = await indexed({
'tables.h': TABLES,
'build.cc': lines(
'#include "tables.h"',
'struct Meters {',
' int value() const { return 0; }',
'};',
'Meters operator*(int k, const Meters& m);',
'int Build() {',
' Table<std::string, std::shared_ptr<const Index>>',
' table;',
' Table<int, int>',
' *table_ptr = nullptr;',
' return table.size() + table_ptr->size();',
'}',
'int Scaled(int factor, const Meters& length) {',
' Meters total = factor',
' * length;',
' return length.value() + total.value();',
'}',
),
});
try {
expect(calls(cg, 'Build')).toEqual(['Table::size instance-method@0.9', 'Table::size instance-method@0.9']);
// `* length;` continues `Meters total = factor`: `length` is the parameter.
expect(calls(cg, 'Scaled')).toEqual(['Meters::value instance-method@0.9', 'Meters::value instance-method@0.9']);
} finally {
cg.close();
}
});
});
+54 -2
View File
@@ -9092,6 +9092,50 @@ function cppEndsDeclarationBegunAbove(line: string, escapedReceiver: string): bo
return end !== null && cppTypeBeganAbove(end[1]!);
}
/**
* The type a declaration of the receiver on the code `line` (lines[index])
* declares when its type is written on lines above, read with them:
* - the end of a type begun above, its text before the name closing a `<`
* an earlier line opened — protobuf's `const
* google::protobuf::internal::ExtensionIdentifier<` above `FeatureSet,
* TypeTraitsT, field_type, is_packed>& extension) const {`. Lines are
* joined, up to six, until the brackets pair;
* - a type alone on the line above, the name starting this one — rocksdb's
* `std::unordered_map<std::string, std::shared_ptr<const TableProperties>>`
* above `props;`, as cppDataMemberIn reads a member.
* Null when the line needs no line above, or the lines read together declare
* no type there.
*/
function cppDeclarationBegunAbove(
lines: readonly string[],
index: number,
line: string,
receiverPattern: RegExp,
declaratorRegex: RegExp,
rawLines: ReturnType<typeof cppNonCodeLines>,
): string | null {
const at = line.search(receiverPattern);
if (at < 0 || index === 0) return null;
const codeAt = (j: number): string => cppCodeOf(cppLiteralsBlanked(lines[j]!, j + 1, rawLines)) ?? '';
const head = line.slice(0, at);
if (/^\s*[*&]*\s*$/.test(head)) {
const above = codeAt(index - 1);
return CPP_TYPE_LINE.test(above) ? matchCppDeclarator(`${above} ${line}`, declaratorRegex)?.[1] ?? null : null;
}
// (Only such a line can need the lines above: every other line naming the
// receiver is spared joining them.)
if (!cppTypeBeganAbove(head)) return null;
let text = line;
let before = head;
for (let j = index - 1; j >= Math.max(0, index - 6); j--) {
const above = codeAt(j);
text = `${above} ${text}`;
before = `${above} ${before}`;
if (!cppTypeBeganAbove(before)) return matchCppDeclarator(text, declaratorRegex)?.[1] ?? null;
}
return null;
}
/**
* The last name of a declared C++ type: `const std::vector<std::pair<int,
* Foo>>&` → `vector`, `ns::Table<int, Box<int>>` → `Table`. Null when the text
@@ -11091,8 +11135,16 @@ function inferCppReceiverType(
return cppDeclaredType(element.text, elementType, true, ref, context, found);
}
}
// The end of a declaration begun on an earlier line, maybe the
// receiver's: one found further up may be another variable.
// A declaration begun on an earlier line, read with the lines it began on.
const begun = cppDeclarationBegunAbove(lines, i, line, receiverPattern, declaratorRegex, rawLines);
const begunType = begun && normalizeCppTypeName(begun);
if (begun && begunType && begunType !== 'auto') {
const inCallerScope = isCppCallersDeclaration(ref.filePath, i + 1, ref, context);
noteCppDeclaration(found, begun, inCallerScope);
return cppDeclaredType(begun, begunType, inCallerScope, ref, context, found);
}
// One those lines don't make readable is still maybe the receiver's:
// one found further up may be another variable.
if (cppEndsDeclarationBegunAbove(line, escapedReceiver)) found.shadowed = true;
// `for (auto& [cfh, iter] : cfh_iter_pairs_)`, `auto [status, iter] = Open();`.
const bound = !found.shadowed ? cppStructuredBindingType(line, lines, i, receiverName, callLineIndex, ref, context, rawLines) : null;