mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +00:00
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / scope-parity (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
* fix(csharp): adaptive tree-sitter buffer + frozen-bucket clone for cross-namespace siblings (#1066) Two coupled regressions surfaced when analyzing real-world C# repos with large source files (issue #1066): 1. Tree-sitter `parser.parse()` is hard-coded to a 32 KB buffer by default. Any file exceeding that threshold throws `Invalid argument` on the worker re-parse path of `populateCsharpNamespaceSiblings` (and the analogous Python / TypeScript captures fallbacks). 2. After the buffer fix unblocks the AST walk, the hook tries to `push()` onto the inner `BindingRef[]` array fetched from `indexes.bindings` — but `materializeBindings` froze that array via `Object.freeze(refs.slice())`. Result: `Cannot add property N, object is not extensible`. Fixes: - `csharp/captures.ts`, `python/captures.ts`, `typescript/captures.ts`: pass `bufferSize: getTreeSitterBufferSize(sourceText.length)` to `parser.parse()` on the cache-miss path so multi-MB files parse. - `csharp/namespace-siblings.ts`: introduce `cloneBindingBucket` to copy the frozen array before mutating, then `set()` the new array back. This is a working but architecturally compromised workaround (#1050 follow-up will replace it with an explicit augmentation channel — see docs/plans/2026-04-26-001 plan). Tests: - New `csharp-large-cache-miss-resolution` fixture (Models/Services/ Other layout, ~77 KB padded UserService.cs) drives the buffer-size failure end-to-end through worker mode. - `csharp.test.ts`: 4 new regression assertions covering both the parse-time buffer-size failure and the freeze workaround. - Per-language captures unit tests gain "large cache-miss file uses adaptive buffer" coverage (TS, Python, C#). - `csharp-hooks.test.ts`: in-memory freeze regression test that reproduces the `Cannot add property` crash without invoking the C# parser at all. Made-with: Cursor * refactor(scope-resolution): add bindingAugmentations channel to indexes Step 1 of the binding-augmentation-channel refactor (issue #1066 follow-up). Pure shape change — no consumers yet. Adds a new `readonly bindingAugmentations` field to `ScopeResolutionIndexes` initialized as an empty `Map` by `finalizeScopeModel`. The new channel is the dedicated post-finalize write target for hooks like `populateCsharpNamespaceSiblings`, so `indexes.bindings` can stay frozen and finalize-owned. Behavior unchanged: nothing reads or writes the new field yet. tsc and the full unit suite remain green. Plan: docs/plans/2026-04-26-001-binding-augmentation-channel.md (local only — `docs/plans/` is gitignored). Made-with: Cursor * feat(scope-resolution): add lookupBindingsAt dual-source helper Step 2 of the binding-augmentation-channel refactor. Introduces a single primitive every walker uses to read both the finalize-owned `indexes.bindings` channel and the post-finalize `indexes.bindingAugmentations` channel. Contract: - Finalized refs come first (preserves existing precedence). - Augmented refs append, deduped by `def.nodeId`. - Empty input on both channels returns a shared frozen empty array. - Single-channel hits return the bucket by reference (no allocation). No consumers are wired yet — Step 3 routes the existing walker primitives through this helper. Augmentations remain empty for every language; behavior of the full suite is unchanged. 8 unit tests pin precedence, dedup, identity for single-channel hits, and the shared-empty-frozen-array sentinel. Made-with: Cursor * refactor(scope-resolution): route binding lookups through lookupBindingsAt Step 3 of the binding-augmentation-channel refactor. Every direct `indexes.bindings.get(...)` consumer in the post-finalize phase is now routed through `lookupBindingsAt` (per-name) or `namesAtScope` + `lookupBindingsAt` (bulk iteration). Routed sites: - `findClassBindingInScope` (walkers.ts) — class-receiver lookups. - `findCallableBindingInScope` (walkers.ts) — free-call lookups. - `findExportedDefByName` (walkers.ts) — module-scope-fallback callable lookups. - `propagateImportedReturnTypes` (passes/imported-return-types.ts) — bulk iteration over an importer's binding entries; switched to `namesAtScope` + per-name `lookupBindingsAt` so post-finalize augmentations are visible to import-derived typeBinding mirrors. Behavior unchanged: augmentations are empty across the suite (Step 4 populates them for C# `populateNamespaceSiblings`). 587 scope-resolution unit tests + 50 integration resolver suites green (4 pre-existing Swift method-implements failures unrelated to this work). Adds `namesAtScope` companion helper for the bulk-iteration callers. Made-with: Cursor * refactor(csharp): write namespace siblings to bindingAugmentations channel Step 4 of the binding-augmentation-channel refactor. The C# `populateNamespaceSiblings` hook is the only consumer that needed to inject cross-file bindings post-finalize, and prior to this change it cloned the (frozen) finalized `BindingRef[]` arrays through a `cloneBindingBucket` helper, then `set()`-back the new array — a workaround for the `Object.freeze` applied by `finalize-algorithm.ts` (issue #1066 root cause). Architecturally that violated `ScopeResolver` Invariant I8 (which permits post-finalize modifications but not in-place mutation of finalized buckets). It also forced read-side consumers to be aware of the workaround. This change: * Switches the three C# write sites to append into `indexes.bindingAugmentations` via `getAugmentationBucket`. The augmentation channel was added in Step 1 and is mutable by contract: inner `BindingRef[]` arrays here are NEVER frozen. * Deletes `cloneBindingBucket` and `getMutableScopeBindings` (workaround helpers no longer needed). * `lookupBindingsAt` (Step 2) merges the two channels transparently for every walker (Step 3), so behavior is unchanged for callers. * Updates the unit test to assert against both channels: finalized bucket stays frozen and untouched, cross-file siblings show up in augmentations only. Renamed the test accordingly. Validation: * `npx tsc --noEmit` clean. * csharp hooks unit + walkers-augmentations unit + csharp integration resolver suite all green (236/236). * Wider `test/unit/scope-resolution test/integration/resolvers` suite: 2507 pass, only 4 pre-existing Swift METHOD_IMPLEMENTS failures remain (unrelated to this work, present on baseline). Refs: issue #1066, ADR-pending binding-augmentation-channel. Made-with: Cursor * feat(scope-resolution): tighten I8 + add validateBindingsImmutability dev guard Step 5 of the binding-augmentation-channel refactor. Captures the new two-channel binding lifecycle in the contract docs and adds a dev-mode runtime validator so a future hook cannot silently drift back into mutating `indexes.bindings`. Contract changes: * `contract/scope-resolver.ts` — rewrote Invariant I8 to describe the two channels (`indexes.bindings` is finalize-output and immutable post-finalize; `indexes.bindingAugmentations` is the append-only post-finalize channel populated by hooks like `populateNamespaceSiblings`). Documented `lookupBindingsAt` as the read-side merger and pointed at the new validator as the enforcement mechanism. * `gitnexus-shared/src/scope-resolution/types.ts` — extended the module-header lifecycle contract to call out `bindingAugmentations` alongside `ReferenceIndex` as the two structures populated after the freeze. Validator: * New `pipeline/validate-bindings-immutability.ts` mirrors the shape of `validateOwnershipParity` (#909): runs only when `NODE_ENV !== 'production' && VALIDATE_SEMANTIC_MODEL !== '0'`, emits via `onWarn`, never throws. Asserts (a) every inner `BindingRef[]` in `indexes.bindings` is `Object.isFrozen`, and (b) every inner array in `indexes.bindingAugmentations` is NOT frozen. * Wired into `pipeline/run.ts` after both `populateNamespaceSiblings` and `propagateImportedReturnTypes`, before `resolveReferenceSites`. One sweep covers the full post-finalize surface. Tests: * `validate-bindings-immutability.test.ts` — 6 cases pinning happy path, both drift directions, multi-violation accumulation, and both production no-op gates. All scope-resolution + csharp resolver tests green (242/242 in the focused run; matches the wider Step 4 baseline). Made-with: Cursor * fix(ingestion): size tree-sitter buffers from UTF-8 bytes Tree-sitter buffer sizing is byte-based, so computing adaptive buffers from JavaScript string length under-sized UTF-8-heavy files. Make getTreeSitterBufferSize accept source text directly and compute Buffer.byteLength internally, then update all parse call sites and max-buffer skip checks to use byte length. Add multibyte cache-miss and cap regressions for C#, Python, TypeScript, and the C# namespace-sibling fallback parse path. Made-with: Cursor * test(scope-resolution): pin augmentation read paths Add focused unit coverage for augmented-only binding reads across the routed walker helpers and imported-return-type propagation path. Clarify I8 wording around lexical Scope.bindings versus post-finalize index channels, and document the intentional local-only behavior of findExportedDef. Also switch the immutability validator tests to Vitest env stubs, document one intentional validator blind spot, and split C# namespace-sibling tests so UTF-8 parsing and augmentation-channel behavior are asserted independently. Made-with: Cursor * test(scope-resolution): avoid slow parser stress fixtures Replace high-cardinality large-file capture fixtures with large padding plus a trailing declaration. This still proves adaptive tree-sitter buffers parse beyond large ASCII and UTF-8-heavy input, without making query matching process thousands of declarations and risking timeouts. Made-with: Cursor * test(scope-resolution): add python and typescript cache-miss resolver regressions Add worker-mode resolver integration coverage mirroring the C# #1066 scenario for Python and TypeScript. Each test builds a temp fixture with large ASCII and UTF-8-heavy source padding, then asserts trailing declarations and call edges still resolve after scope-resolution cache-miss reparsing. Made-with: Cursor * refactor(scope-resolution): gate I8 validator and fast-path namesAtScope Addresses SPARC reviewer feedback on the binding-augmentation channel: - Validator gate is now opt-in outside development. Extract isSemanticModelValidatorEnabled() in utils/env.ts as the single predicate; both validateBindingsImmutability and phase.ts's warn handler share it. Default CLI runs no longer pay the O(binding-buckets) scan, and explicit VALIDATE_SEMANTIC_MODEL=1 now emits warnings even when NODE_ENV is unset. - namesAtScope returns Iterable<string> and zero-allocates when at most one channel is populated (returns Map.keys() directly), only materializing a Set when both channels carry names. The caller-side branching and EMPTY_NAMES escape hatch in propagateImportedReturnTypes are gone -- both helpers handle the empty-augmentation case internally. - C# namespace-siblings header/JSDoc, model JSDoc, I8 contract prose, and the #1066 integration-test header rewritten to say post-finalize fanout appends only to bindingAugmentations; finalized refs come first and win duplicate def.nodeId metadata; local lexical Scope.bindings remains the first-tier shadowing channel. Validator unit-test setup deduplicated via beforeEach and extended with default-CLI no-op + explicit-opt-in cases. Made-with: Cursor
349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
/**
|
|
* Unit 3 coverage for C# simple hooks.
|
|
*
|
|
* Exercises the small-surface hooks that mirror Python's simple-hooks:
|
|
* `bindingScopeFor`, `importOwningScope`, `receiverBinding`. Each hook
|
|
* is tiny, but the tests pin the delegation semantics so refactors
|
|
* don't silently re-route bindings.
|
|
*
|
|
* `isSuperReceiver` lives on the ScopeResolver contract (Unit 6) rather
|
|
* than the LanguageProvider, so it isn't exercised here.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
csharpBindingScopeFor,
|
|
csharpImportOwningScope,
|
|
csharpReceiverBinding,
|
|
} from '../../../../src/core/ingestion/languages/csharp/simple-hooks.js';
|
|
import { csharpMergeBindings } from '../../../../src/core/ingestion/languages/csharp/merge-bindings.js';
|
|
import { csharpArityCompatibility } from '../../../../src/core/ingestion/languages/csharp/arity.js';
|
|
import { populateCsharpNamespaceSiblings } from '../../../../src/core/ingestion/languages/csharp/namespace-siblings.js';
|
|
import type {
|
|
BindingRef,
|
|
Callsite,
|
|
CaptureMatch,
|
|
ParsedFile,
|
|
ParsedImport,
|
|
Scope,
|
|
ScopeId,
|
|
ScopeTree,
|
|
SymbolDefinition,
|
|
TypeRef,
|
|
} from 'gitnexus-shared';
|
|
import type { ScopeResolutionIndexes } from '../../../../src/core/ingestion/model/scope-resolution-indexes.js';
|
|
|
|
function fakeScope(
|
|
kind: Scope['kind'],
|
|
id = 's1',
|
|
typeBindings = new Map<string, TypeRef>(),
|
|
): Scope {
|
|
return {
|
|
id,
|
|
kind,
|
|
parentId: null,
|
|
childrenIds: [],
|
|
bindings: new Map(),
|
|
typeBindings,
|
|
} as unknown as Scope;
|
|
}
|
|
|
|
const fakeTree = {} as ScopeTree;
|
|
const fakeCapture = {} as CaptureMatch;
|
|
const fakeImport: ParsedImport = {
|
|
kind: 'namespace',
|
|
localName: 'System',
|
|
importedName: 'System',
|
|
targetRaw: 'System',
|
|
};
|
|
|
|
describe('csharpBindingScopeFor', () => {
|
|
it('delegates to innermost for method-body declarations', () => {
|
|
const fn = fakeScope('Function');
|
|
expect(csharpBindingScopeFor(fakeCapture, fn, fakeTree)).toBe(null);
|
|
});
|
|
|
|
it('delegates to innermost for namespace-body class declarations', () => {
|
|
const ns = fakeScope('Namespace');
|
|
expect(csharpBindingScopeFor(fakeCapture, ns, fakeTree)).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('csharpImportOwningScope', () => {
|
|
it('binds `using` inside a namespace to the namespace scope', () => {
|
|
const ns = fakeScope('Namespace', 'ns-1');
|
|
expect(csharpImportOwningScope(fakeImport, ns, fakeTree)).toBe('ns-1');
|
|
});
|
|
|
|
it('delegates file-level `using` to the module default', () => {
|
|
const mod = fakeScope('Module');
|
|
expect(csharpImportOwningScope(fakeImport, mod, fakeTree)).toBe(null);
|
|
});
|
|
|
|
it('attaches `using` inside a function scope to that function', () => {
|
|
// Not legal C# at the source level, but defensive — Unit 7 parity
|
|
// gate flags any regression.
|
|
const fn = fakeScope('Function', 'fn-1');
|
|
expect(csharpImportOwningScope(fakeImport, fn, fakeTree)).toBe('fn-1');
|
|
});
|
|
});
|
|
|
|
describe('csharpMergeBindings — shadowing precedence', () => {
|
|
const def = (nodeId: string): SymbolDefinition =>
|
|
({ nodeId, filePath: 't.cs', type: 'Function' }) as SymbolDefinition;
|
|
const binding = (origin: BindingRef['origin'], nodeId: string): BindingRef =>
|
|
({ def: def(nodeId), origin }) as BindingRef;
|
|
|
|
it('local declaration shadows `using` import', () => {
|
|
const local = binding('local', 'L');
|
|
const imp = binding('import', 'I');
|
|
expect(csharpMergeBindings([imp, local])).toEqual([local]);
|
|
});
|
|
|
|
it('explicit `using` shadows `using static` (wildcard)', () => {
|
|
const imp = binding('import', 'I');
|
|
const wc = binding('wildcard', 'W');
|
|
expect(csharpMergeBindings([wc, imp])).toEqual([imp]);
|
|
});
|
|
|
|
it('local shadows both `using` and `using static`', () => {
|
|
const local = binding('local', 'L');
|
|
const imp = binding('import', 'I');
|
|
const wc = binding('wildcard', 'W');
|
|
expect(csharpMergeBindings([wc, imp, local])).toEqual([local]);
|
|
});
|
|
|
|
it('keeps overload siblings at the same tier', () => {
|
|
const a = binding('local', 'A');
|
|
const b = binding('local', 'B');
|
|
expect(csharpMergeBindings([a, b])).toEqual([a, b]);
|
|
});
|
|
|
|
it('dedupes same-nodeId bindings', () => {
|
|
const a = binding('local', 'A');
|
|
const a2 = binding('local', 'A');
|
|
expect(csharpMergeBindings([a, a2])).toHaveLength(1);
|
|
});
|
|
|
|
it('namespace and reexport tie with explicit import (same tier)', () => {
|
|
const ns = binding('namespace', 'N');
|
|
const re = binding('reexport', 'R');
|
|
const imp = binding('import', 'I');
|
|
expect(csharpMergeBindings([ns, re, imp])).toHaveLength(3);
|
|
});
|
|
|
|
it('empty in → empty out', () => {
|
|
expect(csharpMergeBindings([])).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('csharpArityCompatibility', () => {
|
|
const callsite = (arity: number): Callsite => ({ arity });
|
|
const def = (o: Partial<SymbolDefinition> = {}): SymbolDefinition =>
|
|
({ nodeId: 'd1', filePath: 't.cs', type: 'Function', ...o }) as SymbolDefinition;
|
|
|
|
it('unknown when both parameter counts are missing', () => {
|
|
expect(csharpArityCompatibility(def(), callsite(2))).toBe('unknown');
|
|
});
|
|
|
|
it('compatible inside [required, total]', () => {
|
|
expect(
|
|
csharpArityCompatibility(def({ parameterCount: 3, requiredParameterCount: 1 }), callsite(2)),
|
|
).toBe('compatible');
|
|
});
|
|
|
|
it('incompatible below required', () => {
|
|
expect(
|
|
csharpArityCompatibility(def({ parameterCount: 3, requiredParameterCount: 2 }), callsite(1)),
|
|
).toBe('incompatible');
|
|
});
|
|
|
|
it('incompatible above max without variadic', () => {
|
|
expect(
|
|
csharpArityCompatibility(def({ parameterCount: 2, requiredParameterCount: 0 }), callsite(5)),
|
|
).toBe('incompatible');
|
|
});
|
|
|
|
it('compatible above declared params when def has `params` variadic', () => {
|
|
expect(
|
|
csharpArityCompatibility(
|
|
def({ parameterCount: undefined, requiredParameterCount: 0, parameterTypes: ['params'] }),
|
|
callsite(7),
|
|
),
|
|
).toBe('compatible');
|
|
});
|
|
|
|
it('compatible above declared params when variadic token prefixes', () => {
|
|
expect(
|
|
csharpArityCompatibility(
|
|
def({
|
|
parameterCount: undefined,
|
|
requiredParameterCount: 1,
|
|
parameterTypes: ['string', 'params int[]'],
|
|
}),
|
|
callsite(4),
|
|
),
|
|
).toBe('compatible');
|
|
});
|
|
|
|
it('unknown for negative arity (defensive)', () => {
|
|
expect(
|
|
csharpArityCompatibility(def({ parameterCount: 3, requiredParameterCount: 1 }), callsite(-1)),
|
|
).toBe('unknown');
|
|
});
|
|
});
|
|
|
|
describe('populateCsharpNamespaceSiblings', () => {
|
|
const classDef = (nodeId: string, filePath: string, qualifiedName: string): SymbolDefinition =>
|
|
({ nodeId, filePath, qualifiedName, type: 'Class' }) as SymbolDefinition;
|
|
|
|
const scope = (
|
|
id: string,
|
|
kind: Scope['kind'],
|
|
filePath: string,
|
|
parent: ScopeId | null = null,
|
|
ownedDefs: readonly SymbolDefinition[] = [],
|
|
): Scope =>
|
|
({
|
|
id: id as ScopeId,
|
|
kind,
|
|
parent,
|
|
filePath,
|
|
range: { startLine: 1, startColumn: 0, endLine: 10, endColumn: 0 },
|
|
bindings: new Map(),
|
|
imports: [],
|
|
ownedDefs,
|
|
typeBindings: new Map(),
|
|
}) as unknown as Scope;
|
|
|
|
it('writes namespace siblings to the augmentation channel without touching frozen finalized bindings', () => {
|
|
// Verifies the post-finalize binding-augmentation contract for the
|
|
// C# namespace-siblings hook (per ScopeResolver I8 + the
|
|
// `bindingAugmentations` doc on `ScopeResolutionIndexes`):
|
|
// * `indexes.bindings` (the finalize output) stays frozen and
|
|
// its inner `BindingRef[]` arrays are NEVER mutated by the
|
|
// hook — proven here by passing a frozen bucket and asserting
|
|
// it survives unchanged.
|
|
// * Cross-file siblings are appended to
|
|
// `indexes.bindingAugmentations`, the dedicated mutable
|
|
// append-only buffer.
|
|
// * Walkers downstream (`lookupBindingsAt`) merge the two layers
|
|
// transparently — covered by walkers-augmentations.test.ts.
|
|
// Reproduces the pre-architecture `Cannot add property N, object
|
|
// is not extensible` crash by carrying a pre-frozen `BindingRef[]`
|
|
// through `indexes.bindings`. End-to-end coverage is in the
|
|
// `csharp-large-cache-miss-resolution` fixture.
|
|
const existing = classDef('def:external.B', 'external.cs', 'Other.B');
|
|
const sibling = classDef('def:b.B', 'b.cs', 'Demo.B');
|
|
const moduleA = scope('scope:a:module', 'Module', 'a.cs');
|
|
const moduleB = scope('scope:b:module', 'Module', 'b.cs');
|
|
const classB = scope('scope:b:class', 'Class', 'b.cs', moduleB.id, [sibling]);
|
|
const parsedFiles: ParsedFile[] = [
|
|
{
|
|
filePath: 'a.cs',
|
|
moduleScope: moduleA.id,
|
|
scopes: Object.freeze([moduleA]),
|
|
parsedImports: Object.freeze([]),
|
|
localDefs: Object.freeze([]),
|
|
referenceSites: Object.freeze([]),
|
|
} as ParsedFile,
|
|
{
|
|
filePath: 'b.cs',
|
|
moduleScope: moduleB.id,
|
|
scopes: Object.freeze([moduleB, classB]),
|
|
parsedImports: Object.freeze([]),
|
|
localDefs: Object.freeze([sibling]),
|
|
referenceSites: Object.freeze([]),
|
|
} as ParsedFile,
|
|
];
|
|
const frozenBucket = Object.freeze([{ def: existing, origin: 'import' } as BindingRef]);
|
|
const bindings = new Map<ScopeId, ReadonlyMap<string, readonly BindingRef[]>>([
|
|
[moduleA.id, new Map<string, readonly BindingRef[]>([['B', frozenBucket]])],
|
|
]);
|
|
const bindingAugmentations = new Map<ScopeId, ReadonlyMap<string, readonly BindingRef[]>>();
|
|
|
|
populateCsharpNamespaceSiblings(
|
|
parsedFiles,
|
|
{ bindings, bindingAugmentations } as unknown as ScopeResolutionIndexes,
|
|
{
|
|
fileContents: new Map([
|
|
['a.cs', 'namespace Demo;\nclass A { }\n'],
|
|
['b.cs', 'namespace Demo;\nclass B { }\n'],
|
|
]),
|
|
},
|
|
);
|
|
|
|
const finalized = bindings.get(moduleA.id)?.get('B') ?? [];
|
|
expect(finalized).toBe(frozenBucket);
|
|
expect(finalized.map((b) => b.def.nodeId)).toEqual(['def:external.B']);
|
|
expect(Object.isFrozen(finalized)).toBe(true);
|
|
|
|
const augmented = bindingAugmentations.get(moduleA.id)?.get('B') ?? [];
|
|
expect(augmented.map((b) => b.def.nodeId)).toEqual(['def:b.B']);
|
|
expect(Object.isFrozen(augmented)).toBe(false);
|
|
});
|
|
|
|
it('parses UTF-8-heavy cache-miss files before namespace sibling injection', () => {
|
|
const sibling = classDef('def:b.B', 'b.cs', 'Demo.B');
|
|
const moduleA = scope('scope:a:module', 'Module', 'a.cs');
|
|
const moduleB = scope('scope:b:module', 'Module', 'b.cs');
|
|
const classB = scope('scope:b:class', 'Class', 'b.cs', moduleB.id, [sibling]);
|
|
const parsedFiles: ParsedFile[] = [
|
|
{
|
|
filePath: 'a.cs',
|
|
moduleScope: moduleA.id,
|
|
scopes: Object.freeze([moduleA]),
|
|
parsedImports: Object.freeze([]),
|
|
localDefs: Object.freeze([]),
|
|
referenceSites: Object.freeze([]),
|
|
} as ParsedFile,
|
|
{
|
|
filePath: 'b.cs',
|
|
moduleScope: moduleB.id,
|
|
scopes: Object.freeze([moduleB, classB]),
|
|
parsedImports: Object.freeze([]),
|
|
localDefs: Object.freeze([sibling]),
|
|
referenceSites: Object.freeze([]),
|
|
} as ParsedFile,
|
|
];
|
|
const bindingAugmentations = new Map<ScopeId, ReadonlyMap<string, readonly BindingRef[]>>();
|
|
const padding = '漢'.repeat(190_000);
|
|
|
|
populateCsharpNamespaceSiblings(
|
|
parsedFiles,
|
|
{ bindings: new Map(), bindingAugmentations } as unknown as ScopeResolutionIndexes,
|
|
{
|
|
fileContents: new Map([
|
|
['a.cs', `namespace Demo;\n// ${padding}\nclass A { }\n`],
|
|
['b.cs', `namespace Demo;\n// ${padding}\nclass B { }\n`],
|
|
]),
|
|
},
|
|
);
|
|
|
|
expect(bindingAugmentations.get(moduleA.id)?.get('B')?.[0]?.def.nodeId).toBe('def:b.B');
|
|
});
|
|
});
|
|
|
|
describe('csharpReceiverBinding', () => {
|
|
it('returns the `this` type binding for an instance method scope', () => {
|
|
const binding: TypeRef = { rawName: 'User', source: 'self' } as unknown as TypeRef;
|
|
const fn = fakeScope('Function', 'm-1', new Map([['this', binding]]));
|
|
expect(csharpReceiverBinding(fn)).toBe(binding);
|
|
});
|
|
|
|
it('falls back to `base` when `this` is absent', () => {
|
|
const binding: TypeRef = { rawName: 'Parent', source: 'self' } as unknown as TypeRef;
|
|
const fn = fakeScope('Function', 'm-1', new Map([['base', binding]]));
|
|
expect(csharpReceiverBinding(fn)).toBe(binding);
|
|
});
|
|
|
|
it('returns null for a static method (no synthesized `this`/`base`)', () => {
|
|
const fn = fakeScope('Function', 'm-1');
|
|
expect(csharpReceiverBinding(fn)).toBe(null);
|
|
});
|
|
|
|
it('returns null for non-Function scopes', () => {
|
|
expect(csharpReceiverBinding(fakeScope('Class'))).toBe(null);
|
|
expect(csharpReceiverBinding(fakeScope('Module'))).toBe(null);
|
|
});
|
|
});
|