/** * #930 (part 2, widened): recover REAL dependents for a C# file when the * import graph finds none, because of `global using` / implicit enclosing- * namespace access (see `csharp.ts`'s `isGlobalUsingDirective` and * `enclosingNamespaceAccess` doc comments for the mechanism -- a file that * genuinely uses a type declared elsewhere carries NO per-file import * naming it at all). * * The import graph is structurally the wrong place to look for this: the * information lives in type references, not import statements. This module * recovers a DIFFERENT signal already sitting in the indexed chunk store: a * type's declaring file, matched against every other C# chunk's raw source * text for an identifier-boundary occurrence of that type's name. * * Two resolution tiers run for every query, both feeding the same returned * dependent set: * * TIER 1 -- global uniqueness (source S declares type T -> dependent D references T): * 1. T is declared (`class`/`struct`/`interface`/`record`/`enum`) in * EXACTLY ONE C# file project-wide (S) -- see * `buildCSharpTypeOwnerMap`/`uniqueCSharpTypeOwners`. Ambiguous * (multiply-declared) names fall through to tier 2, never guessed at * here. * 2. D (any other C# chunk, production or test) contains an * identifier-boundary occurrence of T's name in its raw source text * (`identifierBoundaryRe` below -- plain `\b` semantics, deliberately * NOT `doc-reference-matching.ts`'s `wordBoundaryRe`: that primitive * treats `.`/`-` as identifier-CONTINUATION characters, correct for * markdown path/prose matching but wrong here -- `Alignment.Apply(...)` * and `pt.Alignment.HasValue` are the DOMINANT real C# usage shapes * (static member access, property access), and in both a `.` * genuinely DELIMITS `Alignment` from its neighbor rather than * continuing the same token). * 3. D !== S. * * S is no longer required to be a non-test file (widened from the original * #930-part-2 shape): measured against serilog/serilog, 53 of the corpus's * 216 `.cs` files were EXCLUSIVELY test-declared types (test helper * fixtures like `DummyRollingFileSink.cs`, `CollectingSink.cs`) that real * OTHER test files genuinely reference -- excluding test files from the * declaring side made `get_dependents` report "not determinable" on all 53 * even though the reference is exactly as textually unambiguous as a * production one. D was already allowed to be test-or-production (point 2 * above); restricting only S was an asymmetry with no precision benefit -- * the SAME uniqueness gate still drops a name if it collides with anything * else project-wide, test or production. * * TIER 2 -- namespace-scoped shadow resolution (new): for a type name T that * IS ambiguous globally (declared in more than one file), C# does not * actually leave the reference unresolvable -- real C# resolves an * unqualified name via lexical namespace scoping: a reference in namespace * `Serilog.Core` sees unqualified members of `Serilog.Core` itself AND every * ENCLOSING namespace (`Serilog`, the global namespace), never a sibling or * descendant namespace, and when more than one visible declaration shares the * name, the INNERMOST (closest-enclosing) one wins (real C# shadowing). * `enclosingNamespaceChain` implements this by decomposing the dotted * namespace string into progressively shorter prefixes -- valid because a * dotted namespace declaration (`namespace Serilog.Core.Foo`) is defined by * the C# spec to behave identically to nested blocks * (`namespace Serilog { namespace Core { namespace Foo { ... } } }`). * * For each ambiguous name T that `targetFile` declares in namespace N: every * OTHER C# file D whose own namespace's enclosing chain contains N, where no * OTHER declaration of T sits at an equal-or-closer position in that same * chain (i.e. targetFile's declaration is the unique closest/winning one for * D specifically), and whose text contains a word-boundary match of T, is * recovered as a dependent of `targetFile`. A referencer whose chain contains * TWO declarations of T at the same depth (a genuine same-namespace name * clash) is dropped for that name, matching tier 1's "never guess" rule. * * Both tiers need each file's own namespace. Getting it costs NO schema * change: rather than adding a persisted `namespace` field (a real SQLite * column + `INDEX_FORMAT_VERSION` bump + migration), `deriveCSharpNamespace` * recovers it from already-indexed chunk CONTENT -- a namespace declaration * line (block-style `namespace Foo.Bar {` or C# 10 file-scoped * `namespace Foo.Bar;`) sits in a file's own "uncovered" chunk (the gap * before/around its first real declaration -- see `chunker.ts`'s * `extractUncoveredCode`), which already carries the raw source text. * Measured against serilog/serilog: 205/216 files (95%) yield a derivable * namespace this way. The 11 misses are files with no namespace at all * (`GlobalUsings.cs`, `AssemblyInfo.cs`) or short internal-only (non-public, * so no `exports`, so the uncovered range can fall under the chunker's * `minChunkSize` and get dropped) files -- `deriveCSharpNamespace` returns * `undefined` rather than guessing for these, and both tiers treat "namespace * not determinable" as "skip this file as a scoping candidate," never as * "assume the global namespace." Failing to determine a namespace can only * ever suppress a recovery, never fabricate one. * * Why uniqueness alone is a strong enough gate for tier 1, unlike Swift's * call-site symbol matching (`swift-symbol-usage-signals.ts`, #869): that * signal had to additionally demote purely-lowercase-method-driven edges, * because a bare METHOD name can collide with a stdlib protocol witness, an * external package's same-named free function, or a same-named overload the * indexer never sees. A bare TYPE name referenced unqualified doesn't have * that problem in the same way: if a same-named external type were in * unqualified scope at the same point, the C# compiler would refuse to * build over the ambiguity rather than silently resolving one -- so "this * project's only declaration of the name" is a much stronger match for "the * declaration this reference actually resolves to" than it is for a method * name. This module intentionally does NOT add Swift's multi-segment/ * type-shaped gates on top: every candidate here is already a real type * declaration by construction (never a method/property name), so that * problem class doesn't arise. * * What this does NOT solve: a word-boundary text match cannot distinguish a * genuine type reference from an unrelated PROPERTY, FIELD, or PARAMETER * that happens to share the exact same identifier (C# convention often * names a property after its type, e.g. `Alignment? Alignment { get; }` on * `PropertyToken` -- see the fixture below; this is actually the common * case, not a false positive, but the matcher can't tell the two apart in * principle). Nor can it catch a reference via an alias (`using A = * Some.Alignment;`), a generic type argument written without the bare name * on its own line in an unusual way, or reflection-based usage. Tier 2 adds * one more limitation on top: it can only place a referencing FILE in the * namespace hierarchy when `deriveCSharpNamespace` succeeds for it, and a * TRUE nested-block declaration split across multiple physical `namespace` * lines in one file (`namespace A { namespace B { ... } }`, vanishingly rare * in modern C#) is read as just its outermost segment, not the full nested * path -- again a fail-safe direction (a missed recovery, never a fabricated * one). Residual risk is accepted and hedged, not eliminated -- callers must * surface this as a lower-confidence, non-import-verified signal (see * `DependentInfo.confidence` in `dependency-analyzer.ts`), never fold it in * unhedged next to a real import edge. * * Verified against a real clone (serilog/serilog, the corpus that motivated * #930): word-boundary matching for `Alignment` and `Padding` -- both * uniquely-declared, single-segment PascalCase type names, the exact shape * most likely to collide with an unrelated identifier -- reproduced all 5 * known real dependents of `Alignment.cs` (plus its one real test * dependent) with ZERO false positives project-wide (checked via * `grep -rlw` across the entire `src`+`test` tree, not just the 5 known * files). * * Originally scoped to file-level `get_dependents` recovery only (via this * package's `dependency-analyzer.ts`) -- #930's gap was specifically that * `get_dependents` itself reported a false `dependentCount: 0` / * `riskLevel: "low"` "all clear" on a file that has 5 real callers, which an * honesty label (`dependentAttributionIncomplete`, #936) already covered for * the "we don't know" case; this module is what lets the tool answer "we * found some" instead, when it genuinely can. * * #1040 widens this to test-association too: this is the SAME mechanism * that lets a C# test file in a nested namespace (`MediatR.Tests`) reach its * subject's types (`MediatR`) with no `using` directive at all -- the exact * shape Go's `sameDirectoryTestConvention` and Java's `samePackageTestConvention` * exist to paper over for their own no-import test conventions. Rather than * a THIRD, independent namespace notion for test-association specifically, * `test-associations.ts` (and `get_files_context`) reuse * `buildCSharpTypeReferenceIndex`/`resolveCSharpTypeReferenceDependents` * directly, filtering the recovered dependents down to the test-file subset * -- see those call sites for the measured MediatR corpus numbers. * * #1071 batches the per-target cost above away. Both tiers, as originally * written, resolved one target by looping over EVERY file in the project and * running `identifierBoundaryRe(typeName).test(chunk.content)` against it -- * fine for a single `get_dependents` call, but O(target's type names x files * x chunk content) makes sweeping every file in a project (`dependent-count- * index.ts`'s batch reverse-dependency pass, and any future "annotate every * file" caller) cost minutes on a real corpus (measured: 54.5s to sweep all * 5423 C# files of a real OrchardCore clone one target at a time). Neither * tier's MATCHING DECISION changes: `buildCSharpReferenceIndex` inverts "does * file F contain type name T as a complete identifier" into one project-wide * tokenizing pass (159ms on that same corpus), and `candidateFilesForName` * uses it to narrow WHICH files each tier asks `identifierBoundaryRe` about -- * every file it returns is still confirmed with the exact same regex (or, for * tier 2, the exact same `resolvesToTargetViaNamespace` predicate) before it * can affect the result. `resolveCSharpTypeReferenceDependentsBruteForce` * keeps the original never-pruned loop alongside the fast path specifically so * `csharp-type-reference-signals.test.ts` can assert the two always agree, * rather than trusting the pruning argument unverified. */ import type { CodeChunk } from './types.js'; interface CSharpTypeDeclaration { file: string; typeName: string; /** This declaration's own enclosing namespace, or `undefined` when `deriveCSharpNamespace` couldn't determine one (never guessed at). */ namespace: string | undefined; } /** * Everything `resolveCSharpTypeReferenceDependents` needs to resolve any * number of target files against ONE project-wide scan -- built once by * `buildCSharpTypeReferenceIndex` and reused per target, so a caller * resolving many target files (e.g. `test-associations.ts`'s per-file loop, * #1040, or `dependent-count-index.ts`'s whole-corpus sweep, #1071) doesn't * re-scan the full chunk set for every one of them, the same "build the index * once, resolve many" discipline `go-same-directory-tests.ts`/ * `java-same-package-tests.ts` already use for their own directory/package * indexes. * * `referenceIndex` and `allCSharpFiles` are #1071's candidate-pruning * addition -- see `buildCSharpReferenceIndex` and `candidateFilesForName`. * Nothing in either tier's MATCH decision reads them directly; only * `candidateFilesForName` does, to narrow which files the unchanged * predicates get asked about. */ export interface CSharpTypeReferenceIndex { chunksByFile: Map; namespaceByFile: Map; declarations: CSharpTypeDeclaration[]; defMap: Map>; referenceIndex: Map>; allCSharpFiles: Set; } /** * Build the project-wide index `resolveCSharpTypeReferenceDependents` needs * (file->chunks, file->derived namespace, every type declaration, the * type-name->declaring-files map, and the #1071 candidate-pruning indexes) * from `chunks` once. `chunks` should be the FULL project chunk set -- * uniqueness (tier 1) and namespace scoping (tier 2) are both project-wide * properties, not scoped to any one target file. */ export declare function buildCSharpTypeReferenceIndex(chunks: CodeChunk[]): CSharpTypeReferenceIndex; /** * Find C# files (any file, production or test) that reference one of * `targetFile`'s declared type names against an already-built `index` (see * `buildCSharpTypeReferenceIndex`), either because the name is uniquely * declared project-wide (tier 1) or because namespace scoping + shadowing * unambiguously resolves an otherwise globally-ambiguous name back to * `targetFile` for that specific referencer (tier 2) -- see the module doc * for both rules. Excludes `targetFile` itself. Returns a sorted, * deduplicated list of filepaths -- empty when `targetFile` declares no * resolvable type or genuinely has no textual referrers in the index. * * `targetFile` must be the exact `chunk.metadata.file` string used by * `targetFile`'s own chunks within the chunks `index` was built from (not a * separately-normalized path) -- this function does no path normalization of * its own and relies on plain string equality throughout, mirroring * `swift-symbol-usage-signals.ts`'s same discipline. */ export declare function resolveCSharpTypeReferenceDependents(targetFile: string, index: CSharpTypeReferenceIndex): string[]; /** * BRUTE-FORCE reference implementation of `resolveCSharpTypeReferenceDependents`: * the exact tier 1 + tier 2 logic this module used before #1071, with no * candidate-index pruning anywhere -- every file in `index.chunksByFile` is * tested directly against `identifierBoundaryRe`, for every target name. * Exported for `csharp-type-reference-signals.test.ts`, which asserts the * pruned `resolveCSharpTypeReferenceDependents` agrees with this exactly * across a fixture corpus spanning every resolution path (a uniquely-declared * type, an ambiguous type resolved by namespace scoping, a shadowed type, a * referencer that declares its own same-named type, a non-C# file, and the * target itself) -- the property that makes the reference index a pruning * optimization rather than a second matching dialect. Mirrors * `dependent-count-index.ts`'s `computeDependentCountsBruteForce` convention. * * Never call this in production: it is precisely the O(target's type names x * files x chunk content) cost the reference index exists to avoid. */ export declare function resolveCSharpTypeReferenceDependentsBruteForce(targetFile: string, index: CSharpTypeReferenceIndex): string[]; /** * Single-target convenience wrapper around `buildCSharpTypeReferenceIndex` + * `resolveCSharpTypeReferenceDependents`, for callers resolving just ONE * target file (`get_dependents`'s file-level recovery, #930/#943). Callers * resolving MANY target files against the same chunk set should build the * index once themselves instead of calling this in a loop -- see * `CSharpTypeReferenceIndex`'s doc comment. */ export declare function findCSharpTypeReferenceDependents(targetFile: string, chunks: CodeChunk[]): string[]; export {}; //# sourceMappingURL=csharp-type-reference-signals.d.ts.map