Skip to content

fix(cpp): a -> call on a smart pointer or optional reaches the type it holds - #2440

Merged
colbymchenry merged 11 commits into
mainfrom
claude/eloquent-wozniak-cc7d7d
Oct 10, 2026
Merged

colbymchenry merged 11 commits into
mainfrom
claude/eloquent-wozniak-cc7d7d

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

A C++ call made with -> on a receiver declared as a std::unique_ptr<T>, std::shared_ptr<T> or std::optional<T> now links to T's method, or to a method of a class T derives from.

Before, receiver inference read the declared type as unique_ptr, found no method on it, and the call fell through to Strategy 3's guess by method name and receiver words:

  • facebook/rocksdb: std::unique_ptr<Iterator> iter; iter->Valid() reached ArenaWrappedDBIter::Valid. std::shared_ptr<FileSystem> fs; fs->GetFileSize(…) reached the test double test::StringFS::GetFileSize. std::unique_ptr<Transaction> writer; writer->Put(…) reached SstFileWriter::Put.
  • protocolbuffers/protobuf: std::unique_ptr<FieldGeneratorBase> generator; generator->GenerateMembers(…) reached MapFieldGenerator::GenerateMembers. The Kotlin generator's std::unique_ptr<FileGenerator> reached java::FileGenerator.

This is the follow-up #2413 left open. It uses that PR's CppReceiverDeclaration (raw, callers, shadowed) and cppMemberOperator. It applies only when all of these hold:

  • the declaration is the caller's own and not shadowed;
  • it is not an alias and not a pointer;
  • the call is ->;
  • the holder is std::-qualified, as isCppLibraryType reads it.

Why the held type is looked up as C++ does, not by its last name

The plan was resolveMethodOnType on T's last segment. Measured on rocksdb, that sends about 3,800 calls to the wrong class with a 0.9 typed edge (2,998 retargets, 611 additions, 154 confidence bumps):

  • Iterator names 22 classes in rocksdb: MemTableRep::Iterator, SkipList::Iterator, InlineSkipList::Iterator and gtest's generator Iterators among them.
  • The Iterator these calls mean declares none of Valid / Next / Seek / key; it inherits them from IteratorBase.
  • So getMethodMatches('Iterator', 'Valid') finds MemTableRep::Iterator::Valid, and the supertype walk never runs.

So T goes through #2407's C++ lookup, now shared as cppClassWritten, from where the call is written:

  • the calling function first (a class local to it);
  • its class and that class's bases;
  • the enclosing namespaces;
  • the file's using declarations, directives and namespace aliases;
  • aliases followed;
  • failing all that, the only class of that name the file can see (that edge gets 0.85).

The method is then taken from that class, or from its bases through the class's own extends edges. A new node-keyed getSupertypeNodes reads those edges; the name-keyed getSupertypes unions every same-named class's bases.

Each shape, decided by measurement

held type outcome rocksdb protobuf fmt
a project class with the method, its own or inherited typed edge at 0.9, or 0.85 through the unique-name fallback 7,680 retargeted · 2,500 added · 3,509 same target, now 0.9 62 · 54 · 86 1 · 3 · 1
a template parameter (std::unique_ptr<IterType>) no edge 25 removed 0 0
a library type (std::deque<…>, std::vector<…>), or an alias of one (using VectorWalPtr = std::vector<…>) no edge 9 removed 0 0
a project class that has no such method, nor do its bases no edge; a class template falls through, since a specialization may declare it (as in #2399) no such call had an edge before 0 0
a type the project doesn't declare, or an alias the lookup can't follow unchanged 38 guesses kept 12 guesses kept 0
  • Types the project doesn't declare keep their guesses. In the probe, those guesses were about half right:

    • right: generated unittest::TestAllTypes → Message::SpaceUsedLong; std::unique_ptr<Rep> rep_ → methods of an out-of-line struct BlockBasedTableBuilder::Rep, which the index names <anonymous>;
    • wrong: ZeroCopyInputStream → a test stream, while PROTOBUF_EXPORT hid the class. fix(cpp): a class keeps its members past attribute macros the parser can't read #2449 now indexes it, so those 24 calls resolve through this PR to ZeroCopyInputStream / ZeroCopyOutputStream.
  • An alias the lookup can't follow keeps its guess. rocksdb's port::Thread is std::thread in port_posix.h and WindowsThread in port_win.h. Only an alias of a library type or of a template parameter is silenced.

  • Iterators: unchanged. No receiver in the five repos is declared as std::…<T>::iterator and called with ->. All 31 -> calls on an iterator go through aliases:

    • gmock's ExpectationSet::const_iterator;
    • rocksdb's ColumnFamilySet::iterator, which is a project class.

    Reading the element type through an alias needs the alias's template arguments, which cpp-type-aliases.ts doesn't keep. Left as a follow-up.

  • -> on a raw pointer to a smart pointer: unchanged. std::unique_ptr<T>* p; p->reset() calls the holder's own member, and fix(cpp): a receiver declared as a std or other outside type with a lowercase name calls that type's own member #2413's gate already silences it. fix(cpp): a receiver declared as a std or other outside type with a lowercase name calls that type's own member #2413's gate log has 549 such calls in rocksdb and 1 in protobuf. All are reset / get / release / swap / has_value / value / emplace. A test pins it.

Validation

Setup.

What didn't change.

  • leveldb and redis (the C control): dumps byte-identical.
  • No extends, implements or synthesized edge changed in any repo, the cpp-override edges included, so the matchCppSupertype refactor changes nothing on real data.
  • Every changed site is a -> call on a caller-owned std:: holder.

Removed (rocksdb 34; protobuf and fmt none), every one read:

  • 25 template-parameter calls:
    • StressTest::TestIterateImpl's std::unique_ptr<IterType> → ArenaWrappedDBIter::*;
    • BlockBasedTable::PutDataBlockToCache's std::unique_ptr<TBlocklike> → Block::own_bytes and BlockBasedTable::ApproximateMemoryUsage.
  • 6 calls on library types:
    • std::unique_ptr<std::deque<BlockHandleInfo>> → Block::size / BlockBuilder::empty / autovector::emplace_back;
    • std::optional<std::vector<WriteBatch>> → autovector::emplace_back.
  • 3 std::unique_ptr<VectorWalPtr> calls → CompactionInputFiles::size.

Retargeted and added.

  • An independent check re-derived each target from the declaration's spelling, the caller's scopes and the extends edges. It agrees for rocksdb's 6,913 + 2,450 class sites and for every class site in fmt and protobuf; the 29 protobuf sites fix(cpp): a class keeps its members past attribute macros the parser can't read #2449 added and the 8 rocksdb range-for sites fix(cpp): a range-based for loop's variable has the type the loop declares #2439 added were read by hand.

  • The ~1,000 it can't place go through:

    • aliases (InternalIterator → InternalIteratorBase, LogFile → WalFile, MergerMaxIterHeap → BinaryHeap, UserDefinedIndexBuilder → IndexFactoryBuilder);
    • using namespace ROCKSDB_NAMESPACE; (std::unique_ptr<DB> in tools and examples);
    • or the unique-name fallback (a fixture's nested TestItem, CountingFileReadListener).

    I grouped them by declaration and target and read each group.

  • Read by hand:

    • all of protobuf's 62 retargets and 54 additions;
    • all 5 fmt changes;
    • in rocksdb: 40 random retargets, 25 random additions, 38 retargets and 37 additions each with a different held type, and every held type resolved through a base class (36).

    All were right. The old targets were implementations, test doubles or namesakes:

    • ModelDB::CompactRange for a std::unique_ptr<DB>;
    • CuckooTableReader::NewIterator for a std::unique_ptr<TableReader>;
    • ChargedCache::Lookup for a std::shared_ptr<SecondaryCache>;
    • BlobFileBuilder::Finish;
    • KeyGenerator::Next for a std::unique_ptr<LockTracker::KeyIterator>.
  • DynamicMessageFactory: on main ed199e60, protobuf's std::unique_ptr<DynamicMessageFactory> reached the base virtual MessageFactory::GetPrototype, because the index stored the class's own override under a name PROTOBUF_NONNULL mangled. fix(cpp): a class keeps its members past attribute macros the parser can't read #2449 fixed the name, and the call now reaches DynamicMessageFactory::GetPrototype itself.

Cost. Process CPU time (all threads) for a full index, interleaved runs on the earlier base:

  • rocksdb: main 165.8 / 164.2 / 164.4 s, this branch 163.6 / 159.7 / 156.7 s;
  • protobuf: main 422.0 / 409.4 s, this branch 399.4 / 393.6 s.

No measurable cost; fewer calls reach the by-name search.

Code

  • src/resolution/name-matcher.ts: matchCppHeldTypeCall and cppMethodOf, called from matchMethodCall right after fix(cpp): a receiver declared as a std or other outside type with a lowercase name calls that type's own member #2413's gate.
  • src/resolution/cpp-supertypes.ts: matchCppSupertype's lookup is now cppClassWritten(written, site), parameterized by where the name is written: scope, file and line, template parameters, and the class it can't be. matchCppSupertype is a thin wrapper over it, with unchanged behavior.
  • src/resolution/cpp-namespaces.ts (new): cppNamespaceAliases and cppMacroNamespaceFrames, moved byte for byte out of name-matcher.ts. That way cpp-supertypes.ts no longer imports name-matcher, and name-matcher imports cpp-supertypes without a cycle.
  • src/resolution/types.ts and index.ts: getSupertypeNodes(id), memoized per supertype generation like getSupertypes.
  • src/resolution/cpp-type-aliases.ts: cppTemplateArguments.

Tests

  • New file: __tests__/cpp-smart-pointer-receiver.test.ts, 8 tests. It covers:

    • rocksdb's reproduction, with MemTableRep::Iterator and ArenaWrappedDBIter as decoys;
    • shared_ptr, optional, a parameter, and a member the class declares in its header, with decoys named after each receiver;
    • a nested class found first from inside the class that declares it;
    • an alias held type, a using namespace, and a class local to the function;
    • template parameters, library types and std aliases getting no edge;
    • a class lacking the method;
    • what stays as it was: a generated message, a class template's specialization, and -> on a std::unique_ptr<T>*.

    Against main, 6 of the 8 fail. The other 2 pin unchanged behavior.

  • All 19 C/C++ test files pass on the merged branch (216 tests), among them fix(cpp): a base class links to the class C++ name lookup finds, not a namesake #2407's cpp-base-class-lookup against the refactored lookup, and fix(cpp): a class keeps its namespaces and enclosing class past code the parser misreads #2426's and fix(c,cpp): a file named like a test is visible to the code that includes it #2421's new suites. tsc --noEmit is clean.

  • Full suite on the merged branch. This box ran at 100% CPU, with other sessions' suites and indexing alongside.

    • The parallel run passed 6,549 of 6,674 tests (82 skipped). All 43 failures were timeouts (41) or EBUSY temp-dir removals (2), in 17 load-sensitive files such as sync, wal-deferral, worktree-detection, mcp-*, git-index-currency and function-ref.
    • Rerun one at a time with --testTimeout=120000, those 17 files pass: 938 tests, 0 failed, 1 skipped.

Overlap

Follow-ups (not in this PR)

  • Main's typed path for raw pointers has the same namesake problem. Iterator* it; it->Valid() resolves by the last segment, so in rocksdb 1,364 of the 44,915 calls that path types reach MemTableRep::Iterator rather than the class C++ lookup finds. Routing that path through cppClassWritten and cppMethodOf is the fix.
  • An alias of a smart pointer gets no edge today, because fix(cpp): a receiver declared through a typedef or using alias calls the type it names #2399's rule silences a capitalized alias that names no class. In rocksdb that's about 90 -> calls: 60 through ScopedArenaPtr<InternalIterator>, an alias template of std::unique_ptr, and 28 through block_test.cc's typedef std::unique_ptr<DataBlockIter> IterPtr;. Following it needs the alias's template arguments.
  • Iterators through aliases: 31 calls (above).
  • absl::StatusOr<T> and absl::optional<T> also hand -> to T. protobuf has about 75 such calls, most unlinked today.
  • The declarator regex spans two templated parameters on one line. In f(std::shared_ptr<A> a, std::unique_ptr<B> b), b's type reads as std::shared_ptr<A> a, std::unique_ptr<B>. That's 31 -> calls in the probes, 1 of them on a smart pointer.

🤖 Generated with Claude Code

colbymchenry and others added 4 commits October 7, 2026 00:43
…owercase name calls that type's own member

A C++ call on a variable, parameter or member the calling function or its
class declares as a namespace-qualified type the project does not declare,
with a lowercase name (std::string, std::vector, absl::flat_hash_set, ...),
no longer falls through to Strategy 3's guess by the method's name. leveldb's
`std::string saved_key_; saved_key_.clear()` reached Slice::clear, protobuf's
`std::string proto; proto.append(...)` LeftoverBuffer::append.

The gate applies to `.` on a value and `->` through a raw pointer; `->` on a
smart pointer, iterator or optional still reaches the element type. It skips
declarations it cannot trust: read from outside the caller's function and
class (including a class nested in the caller's), contradicted by the call
(`.` on a pointer), shadowed by an unreadable re-declaration (`auto`,
range-for, structured binding), unqualified, with unbalanced `<>` (the tail
of a multi-line declaration), or a `_t` name (scalars, tags, traits like
std::conditional_t). A `std::` name counts as the project's only when the
project declares it in `std` (vendored googletest's testing::internal::string).

Receiver inference now blanks comments before matching declarations, so
`// ... non-null imm_` no longer types leveldb's imm_ as `null`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… it holds

A C++ call made with `->` on a receiver the calling function or its class
declares as a `std::unique_ptr<T>`, `std::shared_ptr<T>` or
`std::optional<T>` now resolves on `T`: on the class C++ name lookup finds for
it where the call is written, or on a class that class derives from. Before,
receiver inference read the type as `unique_ptr`, found no method, and the
call fell through to the guess by method name: rocksdb's
`std::unique_ptr<Iterator> iter; iter->Valid()` reached
`ArenaWrappedDBIter::Valid`.

The held type goes through #2407's lookup (`cppClassWritten`, extracted from
`matchCppSupertype`), not `resolveMethodOnType` on its last name: rocksdb has
22 classes named `Iterator`, and the one the calls mean inherits its methods
from `IteratorBase`, so a last-name lookup reaches `MemTableRep::Iterator`.
The method is taken from that class or its bases through the class's own
extends edges (`getSupertypeNodes`). A template parameter, a library type or
an alias of one gets no edge; a type the project doesn't declare, or an alias
the lookup can't follow, leaves the call as it was.

The namespace-macro helpers move from name-matcher.ts to cpp-namespaces.ts
unchanged, so name-matcher can import cpp-supertypes without a cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ak-cc7d7d

# Conflicts:
#	CHANGELOG.md
#	src/resolution/name-matcher.ts
@colbymchenry
colbymchenry merged commit c0f45c7 into main Oct 10, 2026
bompus added a commit to bompus/codegraph that referenced this pull request Oct 10, 2026
…eceivers (#430)

* fix(context): call paths label synthesized hops like explore, not as events (colbymchenry#2428)

Two surfaces label a synthesized (dynamic-dispatch) `calls` hop:
codegraph_explore's Flow and dynamic-dispatch links plus the
codegraph_node trail (`dynamic: interface → impl @file:line`), and
ContextBuilder's "## Call paths", which `codegraph context` prints
(`→[…]`). Each kept its own list. The call paths' list stopped at
callback, react-render, jsx-render, vue-handler, http-client, queue-job
and socket messages, so every other synthesizer fell through to
`event ${m.event ?? ''}`: an interface-impl hop read
`→[event  @src/pdf-exporter.ts:5]`, and so did a C++ override, a redux
thunk, a gin middleware chain or a closure collection.

The wording now lives once, in src/graph/synthesized-hop.ts
(`describeSynthesizedHop`: a summary, the detail only the call paths
print, and the wiring site), as AGENTS.md asks of a derivation two
surfaces render. ToolHandler renders `dynamic: <summary> @site`, the
call paths `<summary><detail> @site`. Explore and the node trail print
exactly what they printed before; the call paths keep their wording for
the kinds they already labelled, including the queue name, a socket's
direction and the HTTP clause, and now name the rest the way explore
does. A bus event reads `bus event` there too, and the section's
footnote no longer says hops are marked `[callback/event …]`.

The helper also holds the wording colbymchenry#2414 gives a Go method a struct gets
by embedding (`metadata.promotedInto`), so that arm moves here too. The
unused long `label` form of ToolHandler.synthEdgeNote (last read by the
removed codegraph_trace) is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sync): a route rendering a same-file lazy value follows its module, and a new component gets its JSX edges (colbymchenry#2452)

A route that renders `const Docs = lazy(() => import('./pages/Docs'))` binds
to the component that module exports, and to the declaration while the module
is missing or exports none (colbymchenry#2400's declaredComponent). The reference had
resolved, so no failed-ref retry saw it, and CG-33's rebind reached it only
when the module's component shared the declaration's name: a sync that added
the module, gave it its default export or moved that export left the route
on its old answer. A data router's `lazy` route kept a moved default export
the same way.

FrameworkResolver.lazyModules names the modules a route's answer reads,
including the files a barrel forwards it through (colbymchenry#2436); React implements it
for lazy-import references and the same-file lazy values Pattern 1 reads.
After the CG-33 rebind, sync puts back the route references whose modules it
added or changed (reopenRouteModuleReaders), in the order their edges were
written, for the orphan sweep.

The jsx-render refresh read only each changed file's own content, so a new
component without JSX of its own never got the edge from `<Team />` in an
unchanged file. A sync whose definition delta holds a JSX-renderable node now
refreshes synthesis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cpp): a range-based for loop's variable has the type the loop declares (colbymchenry#2439)

C++ receiver inference read a declaration only when the declared name was
followed by `;`, `=`, `,`, `)`, `[`, `{` or `(`, so a range-based for's
`Type name :` was never read. In protocolbuffers/protobuf's conformance
runner, `suite->SetVerbose(…)`, `suite->RunSuite(…)` and
`suite->GetFailureListFlagName()` inside `for (ConformanceTestSuite *suite :
suites)` fell through to Strategy 3's guess by the receiver's name, and in
RocksDB `log->file()` on a `log::Writer` loop variable went to
`BlobLogWriter::file`.

A line the declarator regex doesn't match is now read as a range-based for
when its `for (` header declares the receiver before a single `:` (not
`::`), and the loop's variable is taken only when the call is inside the
loop's body: past the header, up to the `}` closing a braced body or the end
of a single-statement body, with comments and literals skipped. After the
loop, or in a later function, the same name is another variable, and the
scan goes on as before. A loop over `auto` elements has nothing to read and
keeps the existing fallback. Bit-fields and the scope operator are never
read as a loop's declaration.

A loop's declaration is recorded like any other (colbymchenry#2413's
noteCppDeclaration), so the std-type gate applies to
`for (const std::string& s : …)` too, and a loop that doesn't enclose the
call still marks the receiver as shadowed. With colbymchenry#2421, the 13 conformance
runner calls reach ConformanceTestSuite's methods at 0.9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(js,ts): a default import is what `export default` declares, not the first exported function (colbymchenry#2432)

A default import resolved to the module's first exported function or
component whenever the `export default` statement wrote a declaration
(`export default function Vans()`, `export default class Store`): only a
separate `export default NAME` statement was read. A React Router 6.4+
data-router page exports its `loader` or `action` above the page
component, so `import Vans, { loader as vansLoader } from './Vans'` bound
the route `/vans` (and every call through `Vans`) to `loader`.

The resolver now reads the file's own `export default` statement from
comment-stripped code. A declaration it writes is found by position, a
binding it names by name, and an anonymous function, class or arrow
function is no node at all (the guess took an exported function beside it,
or one nested inside it). The statement also beats an exported styled/memo
component above it. Every line-initial statement is read and the first one
that names a node of the file wins, so template text that only reads like
a statement names nothing. Strings are not masked: a masker loses step on a
template whose `${…}` holds another template or a regex with a backtick,
and on outline it blanked the real statement below one. Only an expression
default (`memo(Card)`, `new Service()`), a binding the file doesn't
declare, or no statement keeps the old guess.

`require('./x').default` (colbymchenry#2433) reads the same statement first, so it
reaches the declared default too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cpp): a `->` call on a smart pointer or optional reaches the type it holds (colbymchenry#2440)

A C++ call made with `->` on a receiver the calling function or its class
declares as a `std::unique_ptr<T>`, `std::shared_ptr<T>` or
`std::optional<T>` now resolves on `T`: on the class C++ name lookup finds for
it where the call is written, or on a class that class derives from. Before,
receiver inference read the type as `unique_ptr`, found no method, and the
call fell through to the guess by method name: rocksdb's
`std::unique_ptr<Iterator> iter; iter->Valid()` reached
`ArenaWrappedDBIter::Valid`.

The held type goes through colbymchenry#2407's lookup (`cppClassWritten`, extracted from
`matchCppSupertype`), not `resolveMethodOnType` on its last name: rocksdb has
22 classes named `Iterator`, and the one the calls mean inherits its methods
from `IteratorBase`, so a last-name lookup reaches `MemTableRep::Iterator`.
The method is taken from that class or its bases through the class's own
extends edges (`getSupertypeNodes`). A template parameter, a library type or
an alias of one gets no edge; a type the project doesn't declare, or an alias
the lookup can't follow, leaves the call as it was.

The namespace-macro helpers move from name-matcher.ts to cpp-namespaces.ts
unchanged, so name-matcher can import cpp-supertypes without a cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(sync): a barrel edited to forward another page moves the routes that lazily load it

---------

Co-authored-by: Colby Mchenry <me@colbymchenry.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry deleted the claude/eloquent-wozniak-cc7d7d branch October 10, 2026 06:15
colbymchenry added a commit that referenced this pull request Oct 10, 2026
… own or inherited method (#2479)

A C++ call written with no receiver, or through `this->`, inside a method
was matched by its name alone, so another class's method of that name won
by file proximity: protobuf's generated `Api::operator=` calling
`InternalSwap(&from)` reached `Any::InternalSwap`, `Any::InternalSwap`'s
inherited `GetArena()` reached `Arena::InternalHelper::GetArena`, and
googletest's `~linked_ptr() { depart(); }` reached `linked_ptr_internal::depart`.

matchMethodCall now resolves C++'s implicit `this` the way it resolves a
typed receiver: the calling method's class, then the classes it derives
from (its own base edges, through #2440's cppMethodOf), then the classes it
is nested in, with the overload the arguments fit. The call's shape is read
at its column (the extractor drops `this->` and some receivers it can't
spell). A free function, a parameter or local of the name, a dependent base
reached by a bare call, a namesake class in another translation unit and a
member a macro may declare all leave the call to the name strategies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 11, 2026
… type of its range (#2492)

#2439 typed a range-for variable when the loop names its type. A loop
over `auto` elements names none, so receiver inference kept scanning as
for an `auto` local with an unreadable initializer: it reached an earlier
declaration of the same name, often another variable's, or fell through
to the guess by the receiver's name. facebook/rocksdb's db_impl_write.cc
loops `for (auto cfd : cfds)` over a `const autovector<ColumnFamilyData*>&`.

The loop's range is now read from its header and typed as written,
template arguments and all, since the index keeps only a return type's
last name:
- a name the calling function declares, or a data member of its class
  read from the class's own lines (`versions_` from db_impl_write.cc);
- an `auto` local's initializer;
- `*x`, `x.m`, `x->m`, `this`, and calls whose declaration writes their
  return type, free or on the object's class (own or inherited);
- aliases followed with their template arguments (cppAliasedTypeWritten);
- the element: a standard or abseil sequence container's first template
  argument, protobuf's RepeatedPtrField and rocksdb's autovector's, or what
  a project class's begin() returns, dereferenced (ColumnFamilySet's
  iterator's operator* gives ColumnFamilyData*). Nullability annotations
  are dropped.

The element goes through the same declaration record as any declared
receiver, so the typed path, #2413's gate for a standard-library element
and #2440's held-type lookup for `->` on a smart-pointer element apply
unchanged. Classes are looked up with #2440's cppClassWritten, methods
through its cppMethodOf, and matchCppHeldTypeCall now reads its held type
through the shared cppStdHeldType.

A/B on main 8a5ba3d, site-keyed: leveldb identical; fmt +1 / 3
retargeted; rocksdb -68 / +173 / 104 retargeted / 231 confidence only;
protobuf -13 / +25 / 7 / 59. Every removal and retarget was a wrong
guess; 4 of rocksdb's additions reach MemTableRep::Iterator through the
typed path's last-name lookup, as a declared `Iterator* it` does on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 11, 2026
…e class its type names, not a namesake (#2493)

Receiver typing resolved a C++ call on a variable, parameter or member by
the declared type's last name: `Iterator* iter; iter->Valid()` became
resolveMethodOnType('Iterator', 'Valid'), which takes any class named
`Iterator` that declares `Valid`. facebook/rocksdb has 22 such classes,
and the `Iterator` the code means inherits `Valid`, `Next` and `key` from
`IteratorBase`, so nested namesakes like `MemTableRep::Iterator` took the
calls (google/leveldb's went to `SkipList::Iterator`).

A declaration the calling function or its class writes, called with the
operator it takes (`->` on a pointer, `.` on a value or reference), now
goes through the same C++ lookup #2440 uses for smart pointers'
held types (cppClassWritten from the caller's scopes, then the file's
`using`s, aliases followed) and cppMethodOf (the class's own method, else
one through its own base edges):

- An alias #2399 followed from the caller (function-local typedefs
  included) is the class it names, by qualified name. An `auto` loop's
  element type (#2492) is looked up where its range's type is written.
- A class found only as the one class of that name the file can see, a
  template parameter's type, a library type, or a type nothing visible
  declares: the call goes on as before.
- A class with no such method gets no edge, unless it is a class template
  or a base on the way is one the index doesn't link (a `decltype(…)`
  base, an alias each `#if` arm declares differently), which may declare
  it: then as before.
- Declarations read from elsewhere, or past one the scan couldn't read,
  are unchanged.

cppClassWritten now tries the file's `using`s before the global scope:
protobuf's upb generator writes `using google::protobuf::Descriptor;`
inside its namespace, and the PHP extension declares a global C struct
`Descriptor` in a header.

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