Repository navigation
fix(cpp): a declaration with a qualifier or annotation around its name gives its type to calls on it - #2536
Merged
Conversation
…e gives its type to calls on it Receiver inference reads a C++ declaration as a type followed by the name. What may stand between them was read as the type: in rocksdb's `Iterator* const iter = iter_guard.get();` the type was `const`, which names nothing, so the scan went on to an earlier function's `iter`, and in protobuf's `io::EpsCopyOutputStream* PROTOBUF_NONNULL stream` the type was a class named `PROTOBUF_NONNULL`. A name an annotation follows (leveldb's `MemTable* imm_ GUARDED_BY(mutex_);`) was not read at all. - The declarator regex (and the range-for type tail) reads past a cv- qualifier or `__restrict` before or after a `*` or `&` (`T* const`, `T const&`), the nullability annotations after one (abseil's and clang's, and the ALL-CAPS macros c-cpp.ts already blanks for the parser), and an ALL-CAPS annotation macro's call after the name. - matchCppDeclarator hands the readers the type without those annotations (cppDeclaratorType); cv-qualifiers stay, since what reads a type reads past them. An ALL-CAPS word counts as an annotation only after a `*` or `&`, so a type's own name stays (`OS_HANDLE* const h`), and template arguments are kept as written. - The call's own file, its same-named header, a class's members and a range-for's variable are read that way; the local-name check for bare calls reads past the qualifiers too. - #2533's test that a method's parameter is no member used `T* const` locals, which the scan couldn't read; it now declares no local, since one the scan reads decides first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
C++ receiver inference reads a declaration as a type followed by the name, and took whatever stood right before the name for the type:
Iterator* const iter = iter_guard.get();(facebook/rocksdb) read as the typeconst, which names nothing. The scan then went on to an earlier declaration ofiter, or the call was guessed:iter->SeekToFirst()reached the unrelatedArenaWrappedDBIterat 0.65.Slice const& key, a type written before itsconst, did the same.io::EpsCopyOutputStream* PROTOBUF_NONNULL stream, one of protobuf's ~3,600 nullability annotations, read as a type namedPROTOBUF_NONNULL, so calls onstreamgot no edge.MemTable* imm_ GUARDED_BY(mutex_);, a leveldb thread-safety annotation, wasn't read at all: the name has to be followed by;,=,,,)and so on. Calls onimm_were guessed.What changed (name-matcher.ts)
buildDeclaratorRegex, andCPP_DECLARED_TYPE_TAILfor range-for, read past:__restrictbefore or after a*or&(T* const,T const&,T* const*);*or&: abseil's and clang's (the words inCPP_NULLABILITY), and ALL-CAPS macros with an underscore, the rule c-cpp.ts'sblankCppPointerAnnotationMacrosuses for the parser;GUARDED_BY(mutex_)).matchCppDeclaratorhands the readers the type without them (cppDeclaratorType):Arena* PROTOBUF_NULLABLEbecomesArena*.normalizeCppTypeName, the pointer check,cppClassWrittenand the smart-pointer helpers already read past them.*or&, so a type's own name stays (OS_HANDLE* const h).inferCppReceiverType);cppDataMemberIn, fix(cpp): a call on a data member of the caller's class reaches its type's method, wherever the class is declared #2533);cppRangeForDeclaration);isCppLocalName), through the regex alone:std::function<void()> const Finishnow shadows a methodFinish.Validation
Site-keyed edge diffs (
diff-sites.mjs), maindf3ce972vs this branch, both built from the same tree, kernel included:Nodes and files are identical between the arms.
Removed (90), every site read. 83 were wrong guesses whose receivers now have a known type:
GUARDED_BYcontainers had reachedSlice::begin/end/clear(17):writers_.begin()on astd::deque<Writer*>,file_map_.end()on astd::maptypedef.blob_files.size()had reachedCompactionInputFiles::size(27).std::map,std::string,std::shared_ptr) had reachedtoku::omt::find, gtest'sFilePathand others (22).descriptor_->full_name()onconst Descriptor* const descriptor_had reachedEnumValueDescriptor::full_name;file_descriptor->options()had reachedupb::FileDefPtr::options.The other 7 lose a guess that happened to be right. In each, the declared type's class is one the index can't follow, and a declaration of that type without the qualifier gets no edge on main either:
<anonymous>:SkipList<K,C>::Node(Node* const head_), rocksdbBlockBasedTable::Rep(const Rep* const rep, 2);FunctionMocker<R(Args...)>(FunctionMocker<F>* const, 3).InlineSkipList<Comparator>memberComparator const compare_is looked up asrocksdb::Comparator, which has nodecode_key.const Comparator compare_gets the same on main.Retargeted (213), every group read, all wrong→right. Calls on
* constmembers and locals now reach the declared class instead of a namesake:iter->SeekToFirst()afterIterator* const iter = iter_guard.get()(30,ArenaWrappedDBIter→IteratorBase);Env* const env_(50,CompositeEnv/PosixEnv/SpecialEnv→Env);const SliceTransform* const prefix_extractor_(17);InstrumentedMutex* const mutex_(4: wasport::Mutexat 0.9);SpecialEnv* const env→EnvWrapper::CreateDirIfMissing,DBImpl* const db_in a local struct, and the rest.Env* const env_(34,InMemoryEnv→Env);DBIter'sIterator* const iter_(21: wasDBIter's own methods);const Comparator* const user_comparator_,Reporter* const reporter_.MessageFactory* const message_factory_,io::ZeroCopyOutputStream* const output_.UnitTestImpl* const unit_test_,ExpectationBase* constlocals.Added (613): calls on the receivers now read, and chains through them.
io::EpsCopyOutputStream* PROTOBUF_NONNULL streamin generated_InternalSerialize;MessageLite, plusArena* PROTOBUF_NULLABLE arena,io::Printer* PROTOBUF_NONNULL pand others;PROTOBUF_NONNULL/PROTOBUF_NULLABLEhad blocked. Two are onconst AccessorType* PROTOBUF_NONNULL accessor_, a template-dependent alias, which goes to the name strategies as such declarations do. One is onChunk* PROTOBUF_NULLABLE head_, whose struct is defined out of line.ColumnFamilyData* const cfd = …and chains through it (versions->GetColumnFamilySet()->GetDefault()),VersionStorageInfo* const storage_info,const InternalKeyComparator* const icmp_and more;Node* const head_inInlineSkipListreaches aNodeinhash_linklist_rep.cc's anonymous namespace. Main has the same lookup bug for 29 plainNode*locals in those headers (x->Next(…)).GUARDED_BYmembers (imm_,bg_error_,versions_,snapshots_) and the* constones.Metadata-only (569): the same target, now at 0.9.
Tests:
__tests__/cpp-qualified-declarator-receiver.test.ts, 10 cases:Iterator* const, with the nested namesake beside it, which main reached at 0.9;Slice const&/Slice const*;PROTOBUF_NULLABLE/absl_nonnullparameters, locals and members;GUARDED_BYmembers;auto* const, smart-pointer and range-for variables, one with an annotation;.and->on qualified declarations;std::function<void()> constparameter that shadows a method.cppDeclaratorType's template-argument check, which keeps an argument likeFoo<&GLOBAL_VAR>as written; no code in these repos depends on it.T* constlocals the scan couldn't read; it now declares none, since a local the scan reads decides first.Together with #2537
#2537, bare calls in gtest bodies reaching the fixture's methods, is independent. The two branches touch different parts of name-matcher.ts and conflict only in CHANGELOG: keep both bullets. Both applied to
df3ce972pass the 28 C/C++ test files (335 tests).Not in this PR
<anonymous>: out-of-line definitions of nested classes (struct SkipList<K,C>::Node {…},struct BlockBasedTable::Rep {…}) and partial specializations. A declared receiver of such a type gets no edge, on main too.hash_linklist_rep.cc'sNodeforinlineskiplist.h's ownNode*receivers, 29 edges on main.🤖 Generated with Claude Code