Skip to content

fix(cpp): a declaration with a qualifier or annotation around its name gives its type to calls on it - #2536

Merged
colbymchenry merged 1 commit into
mainfrom
claude/cpp-const-declarators
Oct 13, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/cpp-const-declarators

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

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 type const, which names nothing. The scan then went on to an earlier declaration of iter, or the call was guessed: iter->SeekToFirst() reached the unrelated ArenaWrappedDBIter at 0.65.
  • Slice const& key, a type written before its const, did the same.
  • io::EpsCopyOutputStream* PROTOBUF_NONNULL stream, one of protobuf's ~3,600 nullability annotations, read as a type named PROTOBUF_NONNULL, so calls on stream got 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 on imm_ were guessed.

What changed (name-matcher.ts)

  • Reading past what surrounds the name. buildDeclaratorRegex, and CPP_DECLARED_TYPE_TAIL for range-for, read past:
    • cv-qualifiers and __restrict before or after a * or & (T* const, T const&, T* const*);
    • nullability annotations after a * or &: abseil's and clang's (the words in CPP_NULLABILITY), and ALL-CAPS macros with an underscore, the rule c-cpp.ts's blankCppPointerAnnotationMacros uses for the parser;
    • an ALL-CAPS annotation macro's call after the name (GUARDED_BY(mutex_)).
  • The type without its annotations. matchCppDeclarator hands the readers the type without them (cppDeclaratorType): Arena* PROTOBUF_NULLABLE becomes Arena*.
    • cv-qualifiers stay, since normalizeCppTypeName, the pointer check, cppClassWritten and the smart-pointer helpers already read past them.
    • An ALL-CAPS word is an annotation only after a * or &, so a type's own name stays (OS_HANDLE* const h).
    • Template arguments are left as written.
  • Readers that use it:

Validation

Site-keyed edge diffs (diff-sites.mjs), main df3ce972 vs this branch, both built from the same tree, kernel included:

repo removed added retargeted metadata-only
google/leveldb 18 52 60 81
fmtlib/fmt 3 13 6 60
facebook/rocksdb 52 232 141 402
protocolbuffers/protobuf 17 316 6 26
redis/redis (C, control) 0 0 0 0

Nodes and files are identical between the arms.

Removed (90), every site read. 83 were wrong guesses whose receivers now have a known type:

  • Library types:
    • leveldb's GUARDED_BY containers had reached Slice::begin / end / clear (17): writers_.begin() on a std::deque<Writer*>, file_map_.end() on a std::map typedef.
    • rocksdb blob_files.size() had reached CompactionInputFiles::size (27).
    • rocksdb's other library-type calls (std::map, std::string, std::shared_ptr) had reached toku::omt::find, gtest's FilePath and others (22).
  • protobuf descriptor classes whose accessors the index doesn't hold (fix(cpp): a call on a declared pointer, value or reference reaches the class its type names, not a namesake #2493's policy, 17):
    • descriptor_->full_name() on const Descriptor* const descriptor_ had reached EnumValueDescriptor::full_name;
    • a test's file_descriptor->options() had reached upb::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:

  • 6 where the class is indexed <anonymous>:
    • out-of-line nested definitions: leveldb SkipList<K,C>::Node (Node* const head_), rocksdb BlockBasedTable::Rep (const Rep* const rep, 2);
    • a partial specialization: fmt's vendored gmock FunctionMocker<R(Args...)> (FunctionMocker<F>* const, 3).
  • 1 where the type is a template parameter named like a project class. rocksdb's InlineSkipList<Comparator> member Comparator const compare_ is looked up as rocksdb::Comparator, which has no decode_key. const Comparator compare_ gets the same on main.

Retargeted (213), every group read, all wrong→right. Calls on * const members and locals now reach the declared class instead of a namesake:

  • rocksdb:
    • iter->SeekToFirst() after Iterator* 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: was port::Mutex at 0.9);
    • SpecialEnv* const env → EnvWrapper::CreateDirIfMissing, DBImpl* const db_ in a local struct, and the rest.
  • leveldb:
    • Env* const env_ (34, InMemoryEnv → Env);
    • DBIter's Iterator* const iter_ (21: was DBIter's own methods);
    • const Comparator* const user_comparator_, Reporter* const reporter_.
  • protobuf: MessageFactory* const message_factory_, io::ZeroCopyOutputStream* const output_.
  • fmt's vendored gtest: UnitTestImpl* const unit_test_, ExpectationBase* const locals.

Added (613): calls on the receivers now read, and chains through them.

  • protobuf:
    • 176 on io::EpsCopyOutputStream* PROTOBUF_NONNULL stream in generated _InternalSerialize;
    • 95 on MessageLite, plus Arena* PROTOBUF_NULLABLE arena, io::Printer* PROTOBUF_NONNULL p and others;
    • 3 at 0.65 / 0.7 by the name strategies, all right, which the type misread as PROTOBUF_NONNULL / PROTOBUF_NULLABLE had blocked. Two are on const AccessorType* PROTOBUF_NONNULL accessor_, a template-dependent alias, which goes to the name strategies as such declarations do. One is on Chunk* PROTOBUF_NULLABLE head_, whose struct is defined out of line.
  • rocksdb:
    • ColumnFamilyData* const cfd = … and chains through it (versions->GetColumnFamilySet()->GetDefault()), VersionStorageInfo* const storage_info, const InternalKeyComparator* const icmp_ and more;
    • 3 are wrong: Node* const head_ in InlineSkipList reaches a Node in hash_linklist_rep.cc's anonymous namespace. Main has the same lookup bug for 29 plain Node* locals in those headers (x->Next(…)).
  • leveldb: the GUARDED_BY members (imm_, bg_error_, versions_, snapshots_) and the * const ones.

Metadata-only (569): the same target, now at 0.9.

Tests: __tests__/cpp-qualified-declarator-receiver.test.ts, 10 cases:

  • 9 fail on main:
    • Iterator* const, with the nested namesake beside it, which main reached at 0.9;
    • Slice const& / Slice const*;
    • PROTOBUF_NULLABLE / absl_nonnull parameters, locals and members;
    • GUARDED_BY members;
    • auto* const, smart-pointer and range-for variables, one with an annotation;
    • an ALL-CAPS type name;
    • . and -> on qualified declarations;
    • header globals;
    • a std::function<void()> const parameter that shadows a method.
  • 1 guards a declaration without qualifiers.
  • 11 mutations, one per guard or branch; 10 fail a test. The survivor drops cppDeclaratorType's template-argument check, which keeps an argument like Foo<&GLOBAL_VAR> as written; no code in these repos depends on it.
  • fix(cpp): a call on a data member of the caller's class reaches its type's method, wherever the class is declared #2533's member test still fails without either of its own two guards: a declarator inside parentheses, and a name an argument list follows.
  • Full suite: 6,981 passed, 0 failed, 82 skipped. fix(cpp): a call on a data member of the caller's class reaches its type's method, wherever the class is declared #2533's test that a method's parameter is no member used T* const locals 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 df3ce972 pass the 28 C/C++ test files (335 tests).

Not in this PR

  • Classes the index names <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.
  • C++ lookup finds a class in another file's anonymous namespace: hash_linklist_rep.cc's Node for inlineskiplist.h's own Node* receivers, 29 edges on main.

🤖 Generated with Claude Code

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant