Skip to content

fix(cpp): a call or type name never links to what only another source file can see - #2538

Merged
colbymchenry merged 2 commits into
mainfrom
claude/cpp-tu-local-classes
Oct 13, 2026
Merged

colbymchenry merged 2 commits into
mainfrom
claude/cpp-tu-local-classes

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

Name matching linked C and C++ names to declarations no other translation unit can name. That covers a class, struct or alias that a .cc file declares for itself, the members of such a class, and anything inside a source file's unnamed namespace { … }. In facebook/rocksdb, other test files' bare Put(…), Flush(), Get(…) and dbfull() calls reached the methods of ColumnFamilyTestBase, the fixture column_family_test.cc declares, 7,989 times at 0.4. inlineskiplist.h's Node* receivers reached the Node that hash_linklist_rep.cc declares in an unnamed namespace.

What changed (name-matcher.ts)

  • isCppTranslationUnitLocal. isVisibleAcrossFiles already rejects a static C function for a ref in another file. It now also rejects, for a C or C++ ref, a candidate that only another translation unit can name:
    • a type or alias that only source files declare;
    • a member written inside such a class (cppEnclosingClassDecl, the innermost class around the member in its file);
    • an out-of-line member of a type that only source files declare. Only the type's declarations that the member's own file sees count. A scope its file sees no type of may be a namespace: fmt's posix-mock-test.cc defines posix-mock.h's test::close, and ostream-test.cc declares a struct test of its own. An alias of the scope keeps the member, since the class may be declared anywhere.
    • for anything else, what an unnamed namespace of a source file holds (cppUnnamedNamespaceSpans).
  • What a source file's declarations are still seen by: the file itself, a file that #includes it (cppIncluders, transitive), and a header it includes below them (cppIncludesAfter). rocksdb's version_set.cc includes version_set_sync_and_async.h after its FilePicker classes.
  • cppUnnamedNamespaceSpans finds the outermost unnamed namespaces with a scanner. The scanner skips comments, preprocessor lines (with \ continuations, before a CRLF too) and string, character and raw string literals.
  • cppVisibleFirst. The typed lookups (cppMethodOf, resolveMethodOnType) take the candidates the call's file can see first. rocksdb's db_stress_shared_state.h and two benchmark .cc files each declare a SharedState, and a call in db_stress_common.cc is the header's.
  • A rejection leaves the ref unresolved. As with the static rule (fix(resolution): fuzzy matching skips a nested function the reference cannot reach #1709 / C: a call resolves by exact name onto a static function in another translation unit (4,448 edges on betaflight) #1730), the pipeline's pick is rejected, with no fallback to the next candidate.

Validation

Site-keyed edge diffs (diff-sites.mjs) of main df3ce972 against this branch. Both arms were built from the same tree, kernel included. All repos are checked out with CRLF line endings.

repo removed added retargeted metadata-only
google/leveldb 76 0 0 0
fmtlib/fmt 107 0 0 0
protocolbuffers/protobuf 361 0 0 0
facebook/rocksdb 18,035 0 36 0
redis/redis (C, control) 0 0 0 0

Nodes and files are identical between the arms.

Removed, by the rule that rejects the old target. Every target group was read (tri-anon.mjs groups by target and prints its declaration and a call site). All were wrong.

repo member inside a source file's class out-of-line member type unnamed namespace
leveldb 71 5 – –
fmt 51 – 56 –
protobuf 124 56 4 177
rocksdb 17,119 93 25 798
  • rocksdb. Other test files' bare calls hit other test files' fixtures: ColumnFamilyTestBase (dbfull, Flush, Get, Reopen, NumTableFilesAtLevel…), CompactionJobStatsTest, CuckooTableDBTest::CurrentOptions and others. In those files the right method is usually DBTestBase's (fix(cpp): a bare call in a gtest TEST_F or TEST_P body reaches the fixture's method #2537 links those). Other wrong hits:

    • the C API's H::Put / H::Delete in c.cc (3,041);
    • DummyOldStats (options.statistics->getTickerCount(…));
    • PosixEnv and LevelCompactionBuilder methods from outside their .cc;
    • unnamed-namespace helpers like Timestamp, IterStatus and Key1;
    • log::LogTest::StringSource for new test::StringSource(…);
    • HashSkipListRep::Iterator for MemtableSkipList::Iterator.

    The removed typed calls are inlineskiplist.h's Node reaching hash_linklist_rep.cc's unnamed-namespace Node (27, at 0.9). Its own InlineSkipList<C>::Node is indexed <anonymous>, an extraction gap followed up separately. The other 13 (at 0.8) are a BitFields state typed as another test's ConcurrentTest::State.

  • protobuf:

    • compiler::IsEmpty and json_internal::IsEmpty (unnamed-namespace helpers) for gmock's IsEmpty() matcher;
    • lite_unittest.cc's misparsed absl::Cord "method" for absl::Cord("foo");
    • arena.cc's <anonymous> nested classes for .size(), .insert(…) and .capacity() on library types;
    • FlatAllocation::data, FileDescriptorTables::FindFieldByLowercaseName and LeftoverBuffer::append from other files;
    • conformance::Harness::RunTest for a mock's EXPECT_CALL(delegate, RunTest(…));
    • MapReflectionFriend::MakeMapFieldDirty for the free function map_container.h declares.
  • fmt:

    • #include <string> and std::string reaching format-test.cc's adl_test::string;
    • conversion operators indexed as fmt::string_view / fmt::wstring_view methods for fmt::string_view(…) constructions;
    • fmt::formatter<…> specializations in args-test.cc / scan-test.cc for format(…) and s.scan(…);
    • os-test.cc's using wstring_view;
    • ranges-test.cc's non_const_only_range::begin / end for scan-test.cc's range.begin().
  • leveldb:

Retargeted (rocksdb, 36), all wrong → right. Calls in db_stress_tool/ on a SharedState* (shared->GetMutex(), GetCondVar(), …) now reach db_stress_shared_state.h's class instead of cache_bench_tool.cc's own SharedState.

A rule I dropped after the A/B. The first version checked an out-of-line member's scope against every type of that name. fmt's 62 test::close / fdopen / fopen removals came only from an unrelated struct test {} in ostream-test.cc. Those edges were wrong anyway, but the reason wasn't, so only the declarations the member's own file sees count now. Those 62 sites are unchanged from main.

Tests

__tests__/cpp-translation-unit-visibility.test.ts has 14 cases checking targets and confidence. They cover:

  • an unnamed-namespace struct in another .cc;
  • another test file's fixture method;
  • an unnamed-namespace function;
  • the header's SharedState over a .cc namesake, for a parameter and for a namespace-scope variable;
  • a header class with no include the index follows;
  • an out-of-line method of a .cc-only class;
  • a .cc-only class used as a type;
  • the scanner past a line comment, a block comment, a #define, a CRLF-continued #define, a string and a raw string;
  • the file's own unnamed-namespace class, and a header it includes below it;
  • a test that includes the .cc it tests;
  • out-of-line methods through an alias, and of a header's class;
  • a namespace's function beside another file's struct of its name;
  • a base class only another .cc declares.

Mutation check. 19 guards were each disabled alone (mutate-anon.mjs), and each one fails at least one test:

  • the type, enclosing-class, out-of-line-scope, alias and own-file-view rules;
  • the unnamed-namespace rule;
  • the own-file, includer, include-below and header checks;
  • cppVisibleFirst in both lookups;
  • each scanner skip (preprocessor line, CRLF continuation, string, line comment, block comment, raw string) and the unnamed-only match.

Full suite: 6,977 passed, 82 skipped. 8 tests in 5 files failed, all in unrelated areas: git index currency, sync, the MCP daemon, the unindexed-root policy, and a 60-second function-ref timeout. The run overlapped two background repo indexings, and those 5 files pass when rerun alone (114 passed, 1 skipped).

🤖 Generated with Claude Code

colbymchenry and others added 2 commits October 13, 2026 00:45
… file can see

Name matching linked C and C++ names to declarations no other
translation unit can name: a class or alias a `.cc` file declares for
itself, its members, and what a source file's unnamed namespace
declares. In rocksdb, other test files' bare `Put(…)`, `Flush()` and
`dbfull()` reached column_family_test.cc's `ColumnFamilyTestBase`
thousands of times, and inlineskiplist.h's `Node*` receivers reached the
`Node` hash_linklist_rep.cc declares in `namespace { … }`.

- isVisibleAcrossFiles rejects such a candidate for a C or C++ ref
  (isCppTranslationUnitLocal), as it does a `static` C function: a type
  only source files declare; a member written inside such a class; an
  out-of-line member of a type the member's own file sees only source
  files declare (a scope its file sees no type of may be a namespace:
  fmt's posix-mock-test.cc defines posix-mock.h's `test::close` beside
  ostream-test.cc's own `struct test`); and, for the rest, what an
  unnamed namespace of a source file holds. An alias of the scope keeps
  the member, since the class may be declared anywhere.
- A source file's declarations are still seen by the file itself, by a
  file that includes it, and by a header it includes below them
  (rocksdb's version_set.cc includes version_set_sync_and_async.h after
  its `FilePicker` classes).
- The unnamed-namespace spans come from a scanner that skips comments,
  preprocessor lines (continued past a CRLF too) and string, character
  and raw string literals.
- The typed lookups (cppMethodOf, resolveMethodOnType) take the
  candidates the call's file can see first (cppVisibleFirst): rocksdb's
  db_stress_shared_state.h and two benchmark `.cc` files each declare a
  `SharedState`, and a call in db_stress_common.cc is the header's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 61cb418 into main Oct 13, 2026
@colbymchenry
colbymchenry deleted the claude/cpp-tu-local-classes branch October 13, 2026 17:44
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