Repository navigation
fix(cpp,rust): a qualified call like Status::OK() links the class it names, not one whose name ends the same way - #2723
Merged
Conversation
…t names, not one whose name ends the same way The partial qualified-name match took any declaration whose qualified name ended with the written text, with no scope boundary. In facebook/rocksdb about 4,100 calls to `Status::OK()`, `Status::InvalidArgument(…)` and their siblings went to `IOStatus`'s methods of those names; base-class calls from an override, like `return FileSystemWrapper::NewWritableFile(…)` or leveldb's `return DB::Put(…)`, went to a namesake subclass or to the calling method itself; and ripgrep's `Path::new(…)` on std's `Path` went to its own `PrinterPath::new`. - The written name must start a scope of the candidate's: the candidate's qualified name is the name, or ends with it right after `::` or `.`. - Among those candidates, the ones in the call's own language come first. protobuf's C++ `DescriptorPool::generated_pool()` met a Ruby namesake first, which the language gate then dropped. - A C++ name qualified by a template parameter, like `CacheShard::ComputeHash` in `template <class CacheShard> class ShardedCache`, gets no edge from any strategy, except to a member of the parameter's default (`typename Parser = TcParser`). The boundary alone moved rocksdb's call to the receiver-word guess, with the same wrong target. - The C++ template-head reader names each parameter by its last word, `int N`, `detail::fixed_string S`, `const PrefetchOpts& kOpts`, and keeps its default. A `<…>` after a C++ declaration's name is a specialization's arguments, not parameters. Before, `WireFormatLite:: FieldType kKeyFieldType` made `WireFormatLite` a type parameter. Measured on main bd62d9c and re-checked on 17f1453: rocksdb 4,219 retargeted, 83 added, 30 removed; protobuf 24, 47, 40; fmt 7, 0, 21; leveldb 2, 0, 0; ripgrep 0, 0, 38. Nine Ruby, Java, C#, C, JS, Python and Go controls are byte-identical. Only C++, Rust and Ruby reach this match; the file-path suffix match is already anchored and is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 21, 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.
What was wrong
matchByQualifiedName's partial match (the fallback after the exact qualified-name lookup) took any declaration whose qualified name ended with the written text:candidate.qualifiedName.endsWith(ref.referenceName), with no scope boundary. SoStatus::OKmatchedROCKSDB_NAMESPACE::IOStatus::OK, andCacheShard::ComputeHashmatchedclock_cache::ClockCacheShard::ComputeHash, and the pick among several matches was DB order.On main, in facebook/rocksdb alone, 4,331 of 15,233 partial-match decisions were not at a boundary. About 4,100 of those were
Status::OK(),Status::InvalidArgument(…),Status::Corruption(…)and their siblings landing onIOStatus's methods (IOStatus::OKsorts first). Others were base-class calls from an override (return FileSystemWrapper::NewWritableFile(…), leveldb'sreturn DB::Put(…)inModelDB::Put) linking to a namesake subclass or to the calling method itself.The change
endsAtScopeBoundary, name-matcher.ts). A candidate's qualified name must be the written name, or end with it right after::or.. A name written from the global scope (::Foo::bar) is never the tail of a nested one: fix(cpp): a call written from the global scope links the declaration of exactly that name #2445 already resolves C/C++::names before this match, and this covers the rest (Ruby::TypeErrorand the like).DescriptorPool::generated_pool()(41 calls) andWireFormat::MakeTag(…)(5) met a Ruby or C# namesake first in DB order, and the language gate then dropped the edge, so they linked to nothing.gateTypeParameter, so it applies to every strategy).CacheShard::ComputeHash(…)insidetemplate <class CacheShard> class ShardedCachegets no edge, except to a member of the parameter's default (template <typename Parser = TcParser>keeps protobuf'sParser::ParseLoop→TcParser::ParseLoop, right for every caller that passes no parser). This part is needed because the boundary alone moved rocksdb'sCacheShard::ComputeHashto matchMethodCall's receiver-word guess (instance-method 0.7), with the same wrong target.int N = 0,detail::fixed_string S,const PrefetchOpts& kOpts,std::integral T. A<…>after a C++ declaration's name is a specialization's arguments, not parameters. Before, the reader took the first word, sotemplate <…, WireFormatLite::FieldType kKeyFieldType>declared a "parameter"WireFormatLite(the new gate would have dropped protobuf'sWireFormatLite::WriteTagToArrayandinternal::StrongReferencecalls), andconst PrefetchOpts& kOptshid the existing type-reference gate'sPrefetchOpts(one protobufinstantiatesedge now links).Which languages reach this match
A probe arm (main plus a log of every partial-match decision; its graphs are byte-identical to main's) over the 14 repos below: C++ 20,853 decisions (4,441 not at a boundary), Rust 200 (44: ripgrep, protobuf's
rust/), Ruby 59 (all at a boundary). Java, Kotlin, C#, Python, JS/TS and Go never reach it with a candidate: their qualified names join type and member with::(com.example::Foo::bar,Serilog.Core::Logger::Write), so a dotted ref likeFoo.barnever suffix-matches one. An earlier census over 66 repos (on an older main) found the same three languages.The file-path suffix match (~line 94): checked, unchanged
matchByFilePath'sendsWithonly ever compares file nodes that have exactly the ref's basename, so the suffix is already anchored unless a parent directory's name ends with the ref's first directory (mysnippets/forsnippets/…). The probe logged 5,765 of its decisions (C, C++, Objective-C, Ruby, Erlang) and none had a non-boundary candidate, so it's left alone.Validation
Arms on main bd62d9c:
main,bound(parts 1–2),fix(all four parts), plus the probe arm. They're one build with onlyname-matcher.jsandtype-parameters.jsswapped, sharing the same kernel. Every graph is diffed site by site (kind, file, line, column, ref name). Re-checked on 17f1453 (#2722, a Rust declared-type reader): rocksdb, protobuf, fmt, leveldb and sidekiq are byte-identical across the two bases on both arms, and ripgrep's 38 changed sites are the same.Retargeted, all checked:
Status::*→Status::*(about 4,100 calls);TransactionDB::Open(41) away fromOptimisticTransactionDB::Open;BlobFileMetaData::Create;DBImpl::Open. Base calls from an override now reach the base, or the method it inherits (fix(resolution): bind Class.method() to the class itself, not a class whose name contains it #1935):Customizable::IsInstanceOf,Configurable::PrepareOptions,DB::MultiGet. The only exception isCacheShard::…, which part 3 removes.TestUtil::ExpectClear×11 away fromMapTestUtil,Value::default_instance,Option::MergeImpl, fiveFieldValuePrinter::Print*self-loops →TextFormat::FieldValuePrinter). 6 wrong → wrong: calls on the unindexedFileOptions(descriptor.pb.his over the size cap) and on the dependentMessageTraits<T>are now guessed by receiver words instead of by suffix.ModelDB::Put/ModelDB::Deleteself-loops →DB::Put/DB::Delete.Added, all right: rocksdb's 83 are base-class calls that main matched to a namesake subclass declared only in a
.ccfile, which a later rule then threw away (FileSystemWrapper::*×77,SkipListFactory::kClassName,EventListener::OnSubcompaction*,FileSystem::SyncFile,PointLockManagerTest::SetUp). protobuf's 47 are the 46 own-language calls above plusPrefetchOpts.Removed, every one triaged:
Txn::*×10 (SecondaryIndexMixin<Txn> : public Txn),Traits::*×8 (GetFromBatchImpl<Traits>),CacheShard::*×6,BitFieldT::*×3,TCreateContext, gtest'sTestClass,TComparator.MessageType×15,TypeHandler×11,MessageT,ExtensionType,DescriptorProtoClass); 4 wrong (FileOptions::*/EnumValueOptions::*→pb::file::CppFileOptions,pb::enumvalue::JsonEnumValueOptions); 6 Rust, see the costs below.formatter<std::tm, Char>::format×7,sys_time,std::exception→native_formatter::format, and an ostream self-loop), 5 right by coincidence, 5 dependent onT, 1 gtestTestClass.Path::new×25 on std'sPath→PrinterPath::new;Seq::*×9 on regex-syntax'sSeq→ the file's own wrapperTSeq; 2Separator::newunderuse …::FieldContextSeparator as Separatoranduse …::FieldMatchSeparator as Separator→ContextSeparator::new), 2 right by coincidence (Separator::disabledandSeparator::newunderuse …::ContextSeparator as Separator).Known costs (right edges lost):
formatter<X>::format. The index has onefmt::formatterfor every specialization. Main's suffix match landed ondetail::native_formatter::format, which is right for nativeX(5 production calls in std.h:long long,ullong,bool×2,unsigned) only because native types' formatter inherits from it. 7 test callsformatter<int>::format(…)now reachformatter<incomplete_type>::format, a specialization defined in another test.ccfile, which is wrong. Tellingformatter<int>fromformatter<incomplete_type>needs specialization-aware lookup of a written qualifier (as fix(cpp): an explicit specialization, like protobuf'sWireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>, is named after its template #2609 did for functions), in this match and in matchMethodCall's class strategy alike. Follow-up.Key::get/insert/remove/iter_get/to_view(6).Keyisfn map_get<Key: MapKey>andtrait MapKey: … + FfiMapKey, soFfiMapKey::getwas right, but only through a suffix coincidence (Key::get_prototypestays unresolved on both arms). Follow-up: resolve a call through a trait-bounded generic to the bound trait's method.TypeHandler::*→GenericTypeHandler, rocksdbTraits::*→ one of the two localTraitsstructs) now get no edge: silent rather than one guessed instantiation.using Phaser = PeriodicCompactionPhaser;,ConfigHelper,Data) keep the same target, now through the receiver-word strategy at 0.65–0.7 instead of qualified-name 0.85 (the metadata-only rows).Tests
__tests__/qualified-name-boundary.test.ts, 10 cases. 7 fail on main: the rocksdb reproduction,Status::OKvsIOStatus::OK,DB::PutfromModelDB::Put, the Ruby namesake, RustPath::new, a function template'sHasher::Hash, the mixin'sTxn::Put. 3 guards pass on main: the parameter default, a non-type parameter's qualified type, an explicit specialization's arguments. A mutation run pins every part:Hasher::Hash,Txn::PutStatus::OK,DB::Put, RustPath::newHasher::Hash,Txn::PutParser = TcParserguardFull suite on the merged tree (this branch on 17f1453), with the kernel built and
CODEGRAPH_KERNEL_EXPECT=1,--maxWorkers=4: 670 files passed (6 skipped); 8,049 tests passed, 82 skipped, 0 failed.Cost
None measurable. rocksdb on 17f1453, interleaved main / fix / main / fix: 61 s, 59 s, 62 s, 56 s. Single runs on bd62d9c: protobuf 181 s / 162 s; the small repos are within a second. The boundary check is a string slice, and the gate runs only for resolved C++ refs whose name starts with
X::, memoized per enclosing declaration.Overlap
WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>, is named after its template #2609 (merged) wraps these candidates incppSpecializationPool; kept, now on the boundary-filtered set.🤖 Generated with Claude Code