mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user