Repository navigation
fix(c,cpp): a file named like a test is visible to the code that includes it - #2421
Merged
Merged
Conversation
…udes it isVisibleAcrossFiles hides every symbol in a test-suite file from non-test code, decided by the file's name. A C or C++ header is part of every translation unit that includes it, whatever its name: protobuf's conformance framework lives in conformance/conformance_test.h/.cc and test_runner.h, and the 9 `suite_.ReportFailure(...)` calls in the suites found ConformanceTestSuite::ReportFailure through the supertype walk only to have it dropped by the test-suite check. The check now lets a C/C++ candidate through when the reference's file includes it, directly or through other headers. A definition in a source file counts through the same-named test-suite header it includes (conformance_test.cc -> conformance_test.h), so a test that includes the production header it tests (jemalloc's test/unit/hash.c and hash.h) does not become its implementation. The include graph (new cpp-includers.ts) is read from the import nodes extraction mints for every #include, each resolved the way the graph's include edge is, walked from the included file up, from a narrow query of six columns. protobuf +675 edges, none removed or retargeted: 112 in conformance/ and 563 from *_unittest.cc files (which isTestPath does not recognize as tests) into the test utilities they include. redis +1 (expr.c includes fastjson_test.c). leveldb, fmt, llm.c, jq, rocksdb, typeorm, tokio, okhttp and excalidraw are byte-identical. The same exception for Strategy 3's receiver-name guess was measured and left out: it moved rocksdb guesses onto test::StringFS, a test double production code includes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
Merged
colbymchenry
added a commit
that referenced
this pull request
Oct 7, 2026
…a test (#2434) Google names C++ tests `foo_unittest.cc` (protobuf, Breakpad, glog, Chromium), not `foo_test.cc`. The test-file check wanted a separator right before `test`, so each one read as production code: search and explore ranked it alongside the code it tests, `codegraph affected` never listed it, and the resolver's test rules ran backwards for it. Production calls landed on a unittest's own declarations (`stack_frame_entries_.size()` on a unittest's `StackHelper`, a template's `AddressType()` on a unittest's typedef), and a unittest's calls into test suites outside what it includes (a Python `*_unittest.py` using a `tests/` fake) were treated as production calls. #2421 already links a C/C++ unittest to the test helpers it includes. `unittest` and `unittests` join the separator-delimited test suffixes (`foo_unittest.cc`, `foo-unittest.cpp`, `foo.unittest.js`), and a file named so is a test suite to the resolver, as `foo_test.cc` is. A bare `unittest.go` stays production code: promtool's runs rule tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry
added a commit
that referenced
this pull request
Oct 10, 2026
…lares (#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 (#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 #2421, the 13 conformance runner calls reach ConformanceTestSuite's methods at 0.9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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
A C or C++ file named like a test suite is now visible to the code that
#includes it. Before, it was visible to no production code at all.isVisibleAcrossFileshides every symbol in a test-suite file (*_test.h,test_*.cc, atests/directory, …) from non-test files, because a test suite is not linked into the program. For C and C++ that is decided by the name alone. But a header is part of every translation unit that includes it, whatever its name.The motivating case is protocolbuffers/protobuf's conformance framework:
ConformanceTestSuitelives inconformance/conformance_test.h/.cc, andConformanceTestRunnerinconformance/test_runner.h. The suites (binary_json_conformance_suite.cc,text_format_conformance_suite.cc) and the runners (fork_pipe_runner.h,testee.h) include these headers and derive from or call the classes.suite_.ReportFailure(test, level, request, response)atbinary_json_conformance_suite.cc:1126and 8 more sites findsConformanceTestSuite::ReportFailurethrough the supertype walk (matchReferencereturns it at 0.9).resolveOneInnerthen drops it:isVisibleAcrossFiles→isTestSuitePath('conformance/conformance_test.cc'). So the calls resolve to nothing. Before fix(cpp): a base class links to the class C++ name lookup finds, not a namesake #2407 they reachedForkPipeRunner::ReportFailureby a wrong guess. The base classes themselves no longer go through this rule: since fix(cpp): a base class links to the class C++ name lookup finds, not a namesake #2407,cpp-supertypes.tsresolves them by C++ lookup.suite_.ReportFailuresites reachConformanceTestSuite::ReportFailureat 0.9, along with the rest of the suites' calls into the framework (112 edges inconformance/).The rule
isVisibleAcrossFiles' test-suite check now lets a C/C++ candidate through when the reference's translation unit includes it:ConformanceTestSuite::ReportFailureis defined inconformance_test.cc, and the in-class declaration in the header is not a node) counts through the header it implements. That is the header named like it (same stem) that the source itself includes, when that header is a test suite's too.test/unit/hash.cincludes the library'shash.hto test it, not to implement it.Everything else is unchanged:
Record, tokio'sOutput);Why per translation unit, not "a file any non-test file includes". The simpler rule was my first sketch, and the survey ruled it out. protobuf's
src/google/protobuf/test_util.his included by 15 unit-test files thatisTestPathdoesn't recognize as tests: 14*_unittest.ccandcompiler/cpp/unittest.cc(follow-up filed). A file-level rule would have madeTestUtilvisible to every production file in protobuf. The per-TU rule is the language's own visibility rule: a unittest that includestest_util.hsees it, anddescriptor.ccdoes not.Where the include graph comes from (
src/resolution/cpp-includers.ts, new):importnode extraction mints for every#include. Each include is resolved withcontext.resolveImport, the same branch that builds the graph's include edges, so "includes" here is the include edge a reader sees. The include resolver can't be imported from name-matcher without a cycle.getCppIncludeNodes: six columns of C/C++ import nodes). Decoding every import node took about 330 ms on protobuf, against about 45 ms for the narrow select.Validation
All arms ran with
codegraph init -yand the native kernel, on this Windows box. Main is4040eab9(with #2407). Edges are diffed per call site (kind, source node, line, col, refName), and every changed site was read against the source.suite_.ReportFailuresites;suite_.RunTest/ReportSuccess/ReportSkip/RunValidInputTest/RunValidBinaryInputTest;ReportFailure(…),ReportSuccess(…)andWireFormatToString(…)calls insideBinaryAndJsonConformanceSuite's own methods;setting.GetRequest()and the rest of the nestedConformanceRequestSettingclass, plus 6ConformanceRequestSetting setting(…)constructions;TextFormatConformanceTestSuite'sSetFailureListFlagName(…);testee.cc'srunner_->RunTest(…)→ConformanceTestRunner::RunTest.*_unittest.ccfiles (andwire_format_unittest.h) into the test utilities they include:test_util.h,test_util2.h,test_util_lite.cc,test_textproto.h. Since these files count as production code, the same rule had dropped them. All 563 were checked by script: the written name matches the target's, and every qualifier names the target's namespace.TestUtil::X(qualified-name 0.85);reflection_tester.X→TestUtil::ReflectionTester(0.9);TestUtilLite::X;ParseTextOrDie(…);TestUtil::ReflectionTester::X;EqualsToSerialized(…)underusing TestUtil::EqualsToSerialized;.modules/vector-sets/expr.cincludesfastjson_test.cunder#ifdef TEST_MAINand calls itsrun_fastjson_test().CODEGRAPH_NO_PARALLEL_RESOLVE=1) reproduces the parallel dumps byte for byte on protobuf, rocksdb, redis and leveldb.Cost (resolution on the main thread, instrumented copy):
Before the narrow query and the stem filter, the same checks took 936 ms on rocksdb and 1.2–1.8 s on protobuf, which decoded every import node and resolved every include of each test source. The instrumented runs' dumps are byte-identical to the final arm's.
Tried and not shipped: the same exception for the receiver-name guess
matchMethodCall's Strategy 3 has its own "production never calls into a test" filter (isTestPathon both sides). Giving it the same include exception gained 13 correct edges in protobuf:conformance_test_runner.cc'ssuite->SetVerbose(…)and its siblings. It cost rocksdb 1 new wrong edge and 3 wrong→wrong retargets.db_stress_listener.cc'sfs->FileExists(…)on astd::shared_ptr<FileSystem>moved fromFaultInjectionTestFStotest::StringFS, a test double intest_util/testutil.h, which production code includes. That filter exists to keep test doubles out of guesses, so it stays as is. The 13 runner calls fail for another reason: the C++ declarator regex never reads a range-for declaration (for (ConformanceTestSuite *suite : suites)). That is filed separately.Tests
__tests__/cpp-included-test-suite.test.tsis a fixture modelled on protobuf's conformance layout.suite_.ReportFailurethrough the base class; a free function the included header declares; a test-named.cthat a production file includes.*_test.cnobody includes stays out of reach; a test that includes the production header it tests (jemalloc's shape) doesn't become that header's implementation.CODEGRAPH_KERNEL_PATH).src/hash.clinkshash_selftestintest/unit/hash.c.prod-to-test-suite.test.ts(fix(resolution): production code never names a test suite's symbol #2209's typeorm/okhttp cases),cross-file-visibility,cpp-base-class-lookupand the other C/C++ resolution suites pass.tsc --noEmitis clean.function-ref's Caller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …) #1820 case hit its own 60 s limit, and it passes alone.wm.neworraft.Nonekeeps its function ref #2401, fix(sync): a Liquid section or snippet reference links its file when the file appears later #2403, neither touches C/C++ resolution), the touched suites and those two PRs' test files pass.Follow-ups filed
conformance_test_runner.cccalls).isTestPathdoesn't recognize Google's*_unittest.ccnaming (protobuf's 62 files count as production code).🤖 Generated with Claude Code