Repository navigation
fix(cpp): a class keeps its members past attribute macros the parser can't read - #2449
Merged
Merged
Conversation
…can't read
tree-sitter-cpp has no preprocessor, so an attribute macro where it
expects a name or a type misparses the declaration around it. The C++
preParse already blanked one export macro before a class name, an inline
macro before a return type, and a lone macro line before a declaration.
It missed:
- several macros, or one with arguments, between `class` and the name
(protobuf's generated `class PROTOBUF_EXPORT
PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED Any final : public Message`,
rocksdb's `struct ALIGN_AS(64U) HandleImpl`), and a macro before a
partial specialization's name;
- a macro between a pointer and the declared name (`const Descriptor*
PROTOBUF_NONNULL descriptor()`), after a parameter list
(`unknown_fields() const ABSL_ATTRIBUTE_LIFETIME_BOUND {`, leveldb's
`LOCKS_EXCLUDED(mu_) {`), after a declared name (`int count_
GUARDED_BY(mu_);`), or opening a declaration
(`PROTOBUF_FUTURE_ADD_EARLY_NODISCARD absl::string_view name() const`);
- a lone macro line with a comment under it ({fmt}'s
`FMT_BEGIN_EXPORT` above `// A generic formatting context ...`).
Every generated protobuf message class misparsed, so its members were
indexed as namespace-level functions; leveldb's annotated methods became
phantoms named after the annotation.
The new passes match on the code alone (comments and directives as
spaces, literals as placeholders), blank only the macro tokens, and keep
every offset. They run in the hoisted preParse, so files that now parse
clean go through the native kernel, at parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s fixtures misparsing The new passes each masked the file again. A pass that blanks nothing now hands the next the same string, and one that blanks hands it the mask blanked the same way, so a file is masked once. The declarator pass also looks for the macro first instead of trying every word. The preParse output is byte-identical on every C/C++ file of leveldb, fmt, protobuf, rocksdb and redis. #2426's brace-scope fixtures misparsed only through PROTOBUF_FUTURE_ADD_EARLY_NODISCARD and ALIGN_AS(64U), which the preParse now blanks. They now use macros it doesn't know (NODISCARD, cacheline_aligned(64U)), with the same expected scopes. CHANGELOG entry under [Unreleased] -> Fixes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
Merged
Merged
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
tree-sitter-cpp has no preprocessor, so an attribute macro where it expects a name or a type misparses the declaration around it, and often the class around that. The C++ preParse already blanked one export macro before a class name, a curated list of inline macros before a return type,
*_APIprefixes and lone macro lines. It missed four shapes, all common in Google-style C++:classand the name. Every generated class in protocolbuffers/protobuf's checked-in.pb.hfiles startsclass PROTOBUF_EXPORT PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED Any final : public ::google::protobuf::Message.repeated_field.hsplits two macros over two lines, and rocksdb hasstruct ALIGN_AS(64U) HandleImpl.class PROTOBUF_DECLSPEC_EMPTY_BASES RepeatedFieldProxyWithSet<…> {.PROTOBUF_FUTURE_ADD_EARLY_NODISCARD absl::string_view name() const(250 members ofdescriptor.h),GTEST_API_ std::string …;const Descriptor* PROTOBUF_NONNULL descriptor(),Any* PROTOBUF_RESTRICT PROTOBUF_NONNULL other;unknown_fields() const ABSL_ATTRIBUTE_LIFETIME_BOUND {,~Any() PROTOBUF_FINAL;, leveldb'svoid Wait() LOCKS_EXCLUDED(mu_) {;int count_ GUARDED_BY(mu_);, protobuf'sAny_globals_ PROTOBUF_MESSAGE_GLOBALS_SECTION(.data.rel.ro);.FMT_BEGIN_EXPORTabove// A generic formatting context …andtemplate <…> class generic_context {.blankLoneMacroLinesrequired the next line to start a declaration, so the comment rejected it.The effects on the index:
GetDescriptor,GetReflection,descriptor,unknown_fields, …) were indexed as namespace-level functions. Out-of-line definitions carried the macro in their name (google::protobuf::PROTOBUF_NONNULL Any::mutable_type_url). A definition whose parameter is annotated (void Any::InternalSwap(Any* PROTOBUF_NONNULL other) { … }) parsed as a variable and was not indexed at all.AtomicCounter::LOCKS_EXCLUDEDinstead ofIncrementBy,port::Mutex::EXCLUSIVE_LOCK_FUNCTIONinstead ofLock. Somutex_.Lock()linked to nothing.The rules (
src/extraction/languages/c-cpp.ts)Every pass replaces macro tokens with spaces, so offsets, lines and columns stay as they were. Each pass matches on a masked copy of the source:
\continuation lines, are spaces.Only the macro tokens are blanked, never the text between them.
blankCppExportMacros(extended). It now blanks a run of ALL-CAPS macros, each optionally with arguments, betweenclass/structand the name. The name may be qualified and may take template arguments, which must balance before[final] :or{. Declarations ending in;or=are still left alone, and so areclass GTEST_1_TUPLE_(T) {(a macro that is the name) and a comment.blankCppPointerAnnotationMacros. An ALL-CAPS macro with an underscore right after*or&, followed by the declared name or (after*) another*.a * MAX_LEN + 1,flags & FOO_BIT).*after),]or a number is a product too (sizeof(int) * CHAR_BIT * 3).&, only a name counts (flags & FOO_BIT && okstays).blankCppTrailingAttributeMacros. One or more macros, optionally with arguments, after a parameter list and before{,;,=, a constructor's:,->,override,final,constornoexcept.)must close a parameter list: the(it matches must follow a function name (name(,~Name(,operator==(,operator()().if,while,return, andconstexpr, as inif constexpr (…).FMT_PRAGMA_CLANG(…)aboveTEST(…) {.blankCppDeclaratorAttributeMacros. An ALL-CAPS macro with an underscore, optionally with arguments, after]or after a declared name and before;,=,,or).*,&,>, orconstafter a pointer.Foo DEFAULT_OPTIONS;,const Foo DEFAULT_OPTIONS = …;), a typedef (typedef Foo BAR_T;),return x FOO;andf(a, b FOO)stay.blankCppLeadingAttributeMacros. A run of ALL-CAPS macros with an underscore, at the start of a line or after[[…]], followed by something a type can't be followed by:static,inline,virtual,void,struct,template,return,[[, …);Any::Impl_::Impl_();Type name(,ns::Type<T>* name;).A type in capitals is followed by the name alone (
DWORD_PTR value = 0;,HANDLE_T Open(…), GNU-styleRESULT_T\nFunc(…)), so it never matches.constafter the token (SIZE_T const n) only counts with a type and a name after it.blankLoneMacroLines(shared with C) skips comments when it looks for the next line of code.All new passes are C++ only, except the lone-macro change.
Validation
Tests
__tests__/cpp-attribute-macros.test.ts, 19 tests:if constexpr, directive lines, macro calls, capitals-named variables and types, string concatenations with macros, comments);extends, no phantoms).__tests__/kernel-ccpp-parity.test.ts: a protobuf/leveldb-shaped header (LF and CRLF) now parses clean, goes through the kernel instead of deferring, and matches the wasm walker.c-cpp.tsall 21 new tests fail.cpp-brace-scopes.test.ts. Its fixtures misparsed only through the two macros this PR now blanks (PROTOBUF_FUTURE_ADD_EARLY_NODISCARD,ALIGN_AS(64U)), so its "every fixture still misparses" check failed. The fixtures now use macros the preParse doesn't know,NODISCARDandcacheline_aligned(64U). Every other expectation in that file is unchanged.Parse errors (C++ files, wasm grammar after the preParse)
io/coded_stream.cc, a lonePROTOBUF_ALWAYS_INLINElet the parser readPROTOBUF_ALWAYS_INLINE::std::pair<…>as one qualified type.Kernel parity
The preParse is hoisted to the kernel route point, so a file that stops erroring now goes through the native kernel.
scripts/kernel-parity.mjs --lang c,cpp --max-deferral 0.5gives 0 files with diffs on all five repos. Deferrals, main → this branch: leveldb 20 → 9, fmt 32 → 31, protobuf 343 → 292, rocksdb 223 → 214, redis 186 → 176.CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT=1), files with diffs go from 4 → 2, 9 → 5, 56 → 10 and 11 → 8. This branch's set is a strict subset of main's: no new divergence.A/B on main
ed199e60(with #2426 and #2413)Each repo was fully indexed with
codegraph init -yon main and on this branch, both built from the same tree with the kernel rebuilt.Nodes (C/C++):
google::protobuf::Arena, the gtest/plugin classes andRepeatedField's specializations.google::protobuf::PROTOBUF_NONNULL Any::mutable_type_url,google::protobuf::void Any::SharedDtor). 117 gain the class they are in (Impl_::Impl_→Any::Impl_::Impl_,Map::operator++→Map::const_iterator::operator++).Impl_structs and 135 type aliases move into their class.…::LOCKS_EXCLUDED,port::Mutex::EXCLUSIVE_LOCK_FUNCTION, …) become the 14 real methods. The 15th phantom,MutexLock::EXCLUSIVE_LOCK_FUNCTION, had held the body of the existingMutexLock::MutexLock, which now spans it.ABSL_ATTRIBUTE_LIFETIME_BOUND,PROTOBUF_CONSTINIT,GTEST_LOCK_EXCLUDED_, …), a field misread as a namespace-level variable, or a misparse (fmt's "method" named after the whole text ofclass basic_string_view {…).Edges, by call site (kind, file, line, column, ref name, so a source that became a method is still the same site):
What they are:
protobuf, removed (790).
Any::Any(::google::protobuf::Arena* PROTOBUF_NULLABLE arena)).Super_(…)at one class's alias._impl_.methods_.InternalSwap(…)toInternalHelper.default_instance().GetMetadata()in every class toAny's.[self class]to a phantomgoogle::protobuf::class.line.find('#'),v.emplace_back(…)), dropped because the name is no longer unique.#include <set>linking to a phantomset, and_InternalSerializecalls sent toAny's._internal_metadata_.mutable_unknown_fields<…>()now carries its receiver; it is counted again under "added".msg->GetReflection()guessed ontoMessage::GetReflection. That only worked while it was the oneGetReflectionmethod; see the note on name-only guesses below.protobuf, added (2,286).
Arena::Create<…>,CodedInputStream::ReadTag,Any::PackFrom,MapKey::SetUInt64Value,extends Message). The one wrong one is a template parameter namedMapread as the project'sMapclass.PROTOBUF_MUSTTAIL return SingularVarint<…>(…)tail calls insideTcParser, calls to the class's own members, the*_globals_reads.protobuf, retargeted (1,147). Checked with explicit per-pattern rules (
verdict-retargets.mjs):instantiatesedges move from phantom functions to theCodedInputStream,CodedOutputStream,Gzip*StreamandArenaclasses.arena.SpaceUsed()-style calls reachArena, notSerialArena/ThreadSafeArena.new (&_impl_) Impl_(…)reaches each class's ownImpl_.WireFormatLite::WireTypeForFieldType, and calls insideRepeatedField/Mapthat reach their own members.this_._impl_._extensions_.IsInitialized(…),MessageType::GetDescriptor()on a template parameter,repeated->GetArena()._impl_.type_url_.Destroy()goes fromCleanupNode::DestroytoArena::Destroy.leveldb.
mutex_.Lock()/Unlock()/AssertHeld()reachport::Mutex(the methods didn't exist before), plus own-class calls likeIncrementBy(1).env_->random_read_counter_.Reset()/Read()reachAtomicCounterinstead ofBlockBuilder::Reset/CountingFile::Read, andstate.Wait(…)reachesTestState::Wait.CondVar::Waitedges (shared->cv.Wait(),w.cv.Wait(),state_cv_.Wait()).state.cvar.Wait()calls move toTestState::Wait.locks_.Remove(…)guesses on astd::set.rocksdb.
InstrumentedMutex::Lock/Unlock/AssertHeld(88);HandleImpl/NextWithShiftmembers;CountingSemaphore;extendsedges for theALIGN_AS(…)classes.t.join()guesses (astd::thread) ontoport::WindowsThread::join, plus calls misread from declarations.~linked_ptr() { depart(); }.CacheShard::ComputeHashon a template parameter now matchesClockCacheShard::ComputeHashby an unanchored string suffix (name-matcher.ts:7423).fmt.
basic_string_viewnow instantiate the class or call its constructor.lhs.compare(rhs)inbasic_string_view's operators reachesbasic_string_view::compareinstead ofbigint::compare.basic_ostream_formatterandgeneric_contextare indexed.#include <locale>linking to a project node, and edges from phantom gtest methods.redis (C control). jemalloc's tests now run from
TEST_BEGIN(name)toTEST_END. ATEST_ENDabove a comment is now blanked, so the next test no longer starts at the previous one'sTEST_ENDunder the nameTEST_BEGIN. It is named like the first test in each file,(test_oom_errors)(the C extractor's existing form). A typedef below a lone macro and a comment is indexed. No edge changes.Name-only guesses onto the restored members
When the
.pb.hmembers became methods, names that used to have one method stopped being unique, and other names gained their first. Strategy 3's "the only method with this name" and receiver-word scoring then pick differently.expected_to_fail_.contains(…),seen.insert(…)and similar no longer guess onto the restoredgoogle::protobuf::Map.FeatureSet::descriptor()(descriptor.pb.his over the 1 MB indexing cap),OneofOptions::default_instance(). The receiver-word score picks a restored generated class (JavaFeatures_NestInFileClassFeature::descriptor, about 80).GetArena()→Arena::InternalHelper::GetArenainstead ofMessageLite::GetArena(34).InternalSwap(&from)in each generated class →Any::InternalSwap(86, already wrong before, towardInternalHelper).classes_.emplace(…),result->emplace(…)→Map::emplace(20).CondVar::Waitedges come from receivers the C++ inference can't type: a member reached through another (shared->cv), or a field declared below the inline method that uses it (it scans upward only). They resolved only whileCondVar::Waitwas the oneWait.These are filed as follow-ups rather than folded in here.
Overlap
Map's members from turning into hundreds of std-container guesses.c-cpp.ts, but only new helpers at the top of the file, and the walker. There is no shared code. Whichever lands second keeps both CHANGELOG bullets.->call on a smart pointer or optional reaches the type it holds #2440 / fix(cpp): a receiver declared with nested template arguments calls its own class's method #2443 (open C++ receiver-inference PRs) don't touch the preParse. Their A/B counts on protobuf will shift once this lands, because the restored members become targets.🤖 Generated with Claude Code