fix(cpp): a smart pointer's held class lacking a method gets no edge only when its bases are known (#2530)

matchCppHeldTypeCall (#2440) returned null (no edge) for a `->` call on a
`std::unique_ptr`/`std::shared_ptr`/`std::optional` whose held class
lacks the method, unless that class is a class template. #2493 found the
gap in the same rule for raw pointers and values: a base the index can't
link may declare the method. protobuf's `class FlatAllocator : public
decltype(ApplyTypeList<FlatAllocatorImpl>(…))` has no extends edge to
`FlatAllocatorImpl`, and rocksdb's `WritePreparedTxnDB` derives from an
alias each `#if` arm declares differently.

Both paths now share one verdict, cppLacksMethod: no class template, and
cppBasesKnown (every base on the way is linked to a class, or is a library
type). Otherwise the call goes on as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-12 22:16:09 +00:00
committed by GitHub
parent 80e4b459ff
commit 85201e95b3
3 changed files with 53 additions and 12 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call on a variable, parameter or member declared as a standard-library or other outside type with a lowercase name, like `std::string`, `std::vector`, `std::set` or `absl::flat_hash_set`, no longer links to a project method that only shares the method's name. In google/leveldb, `saved_key_.clear()` on a `std::string saved_key_;` reached `Slice::clear`, and in protocolbuffers/protobuf, `proto.append(…)` on a `std::string` reached an unrelated `LeftoverBuffer::append`. The same goes for `->` through a raw pointer like `std::string* out`, while `->` through a smart pointer, iterator or optional still links to the type it holds. Comments also no longer pass for a variable's declaration, so more calls now link to the method of the type the variable is really declared with. Re-index C++ projects after upgrading.
- In C++, a class written with attribute macros the parser can't read now keeps its members. That covers several macros between `class` and the class name, like protocolbuffers/protobuf's generated `class PROTOBUF_EXPORT PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED Any final`, a macro with arguments there, like RocksDB's `struct ALIGN_AS(64U) HandleImpl`, or one before a template specialization, and macros on a member: in front of it (`PROTOBUF_FUTURE_ADD_EARLY_NODISCARD`), between a pointer and its name (`PROTOBUF_NONNULL`), after its parameters or its name (`ABSL_ATTRIBUTE_LIFETIME_BOUND`, google/leveldb's `LOCKS_EXCLUDED(mu_)` and `GUARDED_BY(mu_)`), or alone on a line above a comment ({fmt}'s `FMT_BEGIN_EXPORT`). Before, protobuf's generated message classes were missing and their members, like `GetDescriptor`, `GetReflection` and `unknown_fields`, were indexed as functions outside any class, and leveldb's annotated methods were indexed under the annotation's name, so calls to them linked to nothing. Re-index C++ projects after upgrading.
- In C++, a call on a range-based `for` loop's variable, like `suite->SetVerbose(verbose)` inside `for (ConformanceTestSuite *suite : suites)`, now links to the method of the type the loop declares. Before, the loop's declaration was never read, so the call was matched by the variable's name alone: in RocksDB, `log->file()` on a `log::Writer` went to `BlobLogWriter::file`, and a loop variable named like a member of its class was taken for that member. The variable counts only inside its loop. Re-index C++ projects after upgrading.
- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr<Iterator> iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr<FileSystem>` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr<std::deque<…>>`, the call no longer links to a project method. Re-index C++ projects after upgrading.
- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr<Iterator> iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr<FileSystem>` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr<std::deque<…>>`, or a class that has no method of that name and neither do the classes it derives from, the call no longer links to a project method; a class derived from something that can't be followed, like a `decltype(…)`, may still have it, so that call is matched as before. Re-index C++ projects after upgrading.
- In C++, a call a method makes with no receiver, or through `this->`, now links to the method C++ itself would call: one of the method's own class, else one the class inherits from a base class, else one of a class it is nested in. The same goes for a call in a lambda inside the method, and of a class's overloads the one the call's arguments fit is taken. Before, the call was matched by its name alone, so another class's method of that name could win because it was declared nearer or found first: in protocolbuffers/protobuf, every generated message's `Swap` and move assignment calling `InternalSwap(…)` reached `Any::InternalSwap`, and the `GetArena()` messages inherit from `MessageLite` reached an arena helper's `GetArena`, while googletest's `~linked_ptr() { depart(); }`, vendored in facebook/rocksdb, reached `linked_ptr_internal::depart`. Calls C++ doesn't look up in the class are matched as before: a free function's, a call to a parameter or local of that name, and a template's bare call into a base class that depends on its template parameters. Re-index C++ projects after upgrading. Thanks @danusha2345 for reporting the C++ case in #1952.
- In C++, a call on the variable of a range-based `for` loop over `auto` elements, like `cfd->Ref()` inside facebook/rocksdb's `for (auto cfd : cfds)`, now links to the method of the type the loop goes over. That type is read from what the loop iterates: a `std::vector`, `std::set`, `absl::Span` or similar container, protocolbuffers/protobuf's `RepeatedPtrField` or RocksDB's `autovector`, whether a variable, parameter or member holds it, an `auto` variable is set to it or a function or method returns it, and a project class you loop over through its `begin()`, like RocksDB's `ColumnFamilySet`. Before, the call was matched by the variable's name, or by an earlier variable of that name: in RocksDB, `blob_file.GetBlobFileNumber()` over a list of `BlobFileGarbage` reached `BlobFileAddition::GetBlobFileNumber`, and in protobuf's Objective-C generator, calls over a list of `std::unique_ptr<MessageGenerator>` reached other generators' methods. A call on an element of a standard-library type, like `file.find(…)` over a `std::vector<std::string>`, no longer links to a project method, and `->` on an element held in a `std::unique_ptr` or `std::shared_ptr` reaches the type it holds. Re-index C++ projects after upgrading.
- In C++, a call on a variable, parameter or member declared as one of your classes, like `iter->Valid()` on an `Iterator* iter` or `it.Valid()` on a `const Iterator& it`, now links to that method of the class C++ itself would find for the type where the call is written, or of a class it inherits from. Before, the method was picked by the type's last name, so a class of the same name nested elsewhere could win: in facebook/rocksdb, calls on an `Iterator` reached `MemTableRep::Iterator`, which declares `Valid` and `Next` itself while the `Iterator` the code means inherits them, google/leveldb's reached `SkipList::Iterator`, and in protocolbuffers/protobuf the Java generator's `context->options()` reached the hpb generator's `Context` and `text_parser.Parse(…)` on a `TextFormat::Parser` reached the `.proto` parser. When that class and the classes it derives from have no method of that name, the call no longer links to an unrelated class's method that shares it, as protobuf's `field->full_name()` on a `FieldDescriptor*` did. A type is also found through a `using` declaration the file writes, like protobuf's `using google::protobuf::Descriptor;`, before a class of the same name declared at global scope elsewhere in the repository; this also applies to base classes and to the type a smart pointer holds. Re-index C++ projects after upgrading.
+34 -3
View File
@@ -17,9 +17,9 @@
* `Iterator` the call means inherits `Valid` from `IteratorBase`. A template
* parameter, a library type and an alias of a type outside the project hold
* no project class, so those calls get no edge; neither does a call the held
* class has no method for (unless a class template's specialization may
* declare it). A held type the project doesn't declare — a generated message
* — leaves the call as it was.
* class has no method for (unless a class template's specialization, or a
* base the index doesn't link, may declare it). A held type the project
* doesn't declare — a generated message — leaves the call as it was.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
@@ -447,4 +447,35 @@ describe('what keeps the call as it was', () => {
cg.close();
}
});
it('a held class with a base the index does not link, which may declare the method', async () => {
// protobuf's FlatAllocator derives from a `decltype(…)` that names FlatAllocatorImpl.
const cg = await indexed({
'src/google/protobuf/descriptor.cc': lines(
'#include <memory>',
'namespace google {',
'namespace protobuf {',
'namespace internal {',
'template <typename... T>',
'class FlatAllocatorImpl {',
' public:',
' void PlanArray(int array_size) {}',
'};',
'template <typename... T>',
'FlatAllocatorImpl<T...> ApplyTypeList(int);',
'class FlatAllocator : public decltype(ApplyTypeList<char, int>(0)) {};',
'} // namespace internal',
'void PlanAllocationSize(std::unique_ptr<internal::FlatAllocator>& alloc) {',
' alloc->PlanArray(1);',
'}',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
expect(calls(cg, 'google::protobuf::PlanAllocationSize')).toEqual(['google::protobuf::internal::FlatAllocatorImpl::PlanArray']);
} finally {
cg.close();
}
});
});
+18 -8
View File
@@ -9576,8 +9576,9 @@ function cppStdHeldType(raw: string): string | null {
* - The member of the class it names, or of a class that class derives from.
* - null when no project method is called: the type is a template
* parameter's, a library type (`std::unique_ptr<std::deque<…>>`) or an
* alias of one, or a class that lacks the method (but a class template's
* specialization may declare it, so not there).
* alias of one, or a class that surely lacks the method (cppLacksMethod:
* not when a class template's specialization or a base the index doesn't
* link may declare it).
* - undefined when `raw` declares no such holder, or the type it holds is
* none the project declares (a generated message, a class the index
* doesn't name): the call goes on as before.
@@ -9604,7 +9605,7 @@ function matchCppHeldTypeCall(raw: string, methodName: string, ref: UnresolvedRe
}
const method = cppMethodOf(found.node, methodName, ref, context);
if (method) return { original: ref, targetNodeId: method.id, confidence: found.exact ? 0.9 : 0.85, resolvedBy: 'instance-method' };
return cppTemplateParameters(found.node, context).size > 0 ? undefined : null;
return cppLacksMethod(found.node, context) ? null : undefined;
}
const CPP_CLASS_KINDS: ReadonlySet<string> = new Set(['class', 'struct', 'union']);
@@ -9630,10 +9631,9 @@ function cppCallSite(caller: Node, ref: UnresolvedRef, context: ResolutionContex
* `MemTableRep::Iterator::Valid`, one of 22 classes named `Iterator`, while the
* `Iterator` the code means inherits `Valid` from `IteratorBase`.
* - The member of that class, or of a class it derives from.
* - null when that class has no such method: protobuf's descriptor accessors
* aren't indexed, and `field->full_name()` reached unrelated classes' by
* the method's name. (A class template's specialization may declare it, so
* not there.)
* - null when that class surely has no such method (cppLacksMethod):
* protobuf's descriptor accessors aren't indexed, and `field->full_name()`
* reached unrelated classes' by the method's name.
* - undefined when the declaration may be another variable's (read from
* elsewhere, past one of the receiver the scan couldn't read, or not taking
* that operator), or the lookup doesn't place the type: a library type, a
@@ -9664,7 +9664,17 @@ function matchCppDeclaredTypeCall(
if (!found?.exact || !CPP_CLASS_KINDS.has(found.node.kind)) return undefined;
const method = cppMethodOf(found.node, methodName, ref, context);
if (method) return { original: ref, targetNodeId: method.id, confidence: 0.9, resolvedBy: 'instance-method' };
return cppTemplateParameters(found.node, context).size === 0 && cppBasesKnown(found.node, context) ? null : undefined;
return cppLacksMethod(found.node, context) ? null : undefined;
}
/**
* Whether a C++ class with no method of a name, its own or one it inherits
* through its base edges, surely has none: it is no class template, whose
* specializations may declare members the template doesn't, and every base on
* the way is one the walk follows (cppBasesKnown).
*/
function cppLacksMethod(cls: Node, context: ResolutionContext): boolean {
return cppTemplateParameters(cls, context).size === 0 && cppBasesKnown(cls, context);
}
/**