From fed95a9ef451bee9f9cdd8d31681b7b3e98f0185 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Wed, 22 Apr 2026 08:58:12 +0100 Subject: [PATCH] test(csharp-scope): integration fixtures for Units 6c/6d/6e runtime behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close the integration-coverage gap flagged in the production-readiness review. Units 6c (collection-accessor unwrap), 6d (using-static member injection), and 6e (overload disambig + interface dispatch) previously had only hook-level unit tests; the end-to-end wiring was exercised only by the parity harness. Three minimal fixtures + four new it() blocks: * csharp-collection-accessor — RenderAll iterates Dictionary.Values and calls .Render(); asserts the CALLS edge lands on Widget.Render. * csharp-using-static — `using static Helpers.MathUtils;` makes Square(int) a free-callable in the consumer; asserts the CALLS edge lands on MathUtils.Square. * csharp-overload-interface — three assertions: 1. Run → Log binds to the 2-arg overload only (arity narrowing); verified via target Method node's parameterTypes.length === 2. 2. Run → Greet emits one primary edge to IGreeter.Greet plus two reason='interface-dispatch' siblings to En/FrGreeter.Greet. 3. Interface-dispatch fan-out excludes the primary target. Verified: - csharp integration 189/189 passing --- .../CollectionAccessor.csproj | 5 + .../Models/Widget.cs | 8 ++ .../Services/Renderer.cs | 17 ++++ .../csharp-overload-interface/App/Caller.cs | 14 +++ .../csharp-overload-interface/App/Logger.cs | 12 +++ .../Greeting/EnGreeter.cs | 9 ++ .../Greeting/FrGreeter.cs | 9 ++ .../Greeting/IGreeter.cs | 6 ++ .../OverloadInterface.csproj | 5 + .../csharp-using-static/App/Calculator.cs | 11 +++ .../csharp-using-static/Helpers/MathUtils.cs | 9 ++ .../csharp-using-static/UsingStatic.csproj | 5 + .../test/integration/resolvers/csharp.test.ts | 94 +++++++++++++++++++ 13 files changed, 204 insertions(+) create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/CollectionAccessor.csproj create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/Models/Widget.cs create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/Services/Renderer.cs create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/App/Caller.cs create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/App/Logger.cs create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/EnGreeter.cs create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/FrGreeter.cs create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/IGreeter.cs create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/OverloadInterface.csproj create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-using-static/App/Calculator.cs create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-using-static/Helpers/MathUtils.cs create mode 100644 gitnexus/test/fixtures/lang-resolution/csharp-using-static/UsingStatic.csproj diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/CollectionAccessor.csproj b/gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/CollectionAccessor.csproj new file mode 100644 index 000000000..ec2cce143 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/CollectionAccessor.csproj @@ -0,0 +1,5 @@ + + + net8.0 + + diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/Models/Widget.cs b/gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/Models/Widget.cs new file mode 100644 index 000000000..fda3acc87 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/Models/Widget.cs @@ -0,0 +1,8 @@ +namespace Models; + +public class Widget +{ + public void Render() + { + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/Services/Renderer.cs b/gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/Services/Renderer.cs new file mode 100644 index 000000000..6197b611b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-collection-accessor/Services/Renderer.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Models; + +namespace Services; + +public class Renderer +{ + private Dictionary _widgets = new(); + + public void RenderAll() + { + foreach (var w in _widgets.Values) + { + w.Render(); + } + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/App/Caller.cs b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/App/Caller.cs new file mode 100644 index 000000000..8a2523381 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/App/Caller.cs @@ -0,0 +1,14 @@ +using Greeting; + +namespace App; + +public class Caller +{ + private Logger _logger = new(); + + public string Run(IGreeter greeter) + { + _logger.Log("starting", 1); + return greeter.Greet(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/App/Logger.cs b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/App/Logger.cs new file mode 100644 index 000000000..1faa0cd13 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/App/Logger.cs @@ -0,0 +1,12 @@ +namespace App; + +public class Logger +{ + public void Log(string message) + { + } + + public void Log(string message, int level) + { + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/EnGreeter.cs b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/EnGreeter.cs new file mode 100644 index 000000000..9da61a945 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/EnGreeter.cs @@ -0,0 +1,9 @@ +namespace Greeting; + +public class EnGreeter : IGreeter +{ + public string Greet() + { + return "hello"; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/FrGreeter.cs b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/FrGreeter.cs new file mode 100644 index 000000000..4c68c408c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/FrGreeter.cs @@ -0,0 +1,9 @@ +namespace Greeting; + +public class FrGreeter : IGreeter +{ + public string Greet() + { + return "bonjour"; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/IGreeter.cs b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/IGreeter.cs new file mode 100644 index 000000000..395f301ef --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/Greeting/IGreeter.cs @@ -0,0 +1,6 @@ +namespace Greeting; + +public interface IGreeter +{ + string Greet(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/OverloadInterface.csproj b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/OverloadInterface.csproj new file mode 100644 index 000000000..ec2cce143 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-overload-interface/OverloadInterface.csproj @@ -0,0 +1,5 @@ + + + net8.0 + + diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-using-static/App/Calculator.cs b/gitnexus/test/fixtures/lang-resolution/csharp-using-static/App/Calculator.cs new file mode 100644 index 000000000..881243b1c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-using-static/App/Calculator.cs @@ -0,0 +1,11 @@ +using static Helpers.MathUtils; + +namespace App; + +public class Calculator +{ + public int Compute(int value) + { + return Square(value); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-using-static/Helpers/MathUtils.cs b/gitnexus/test/fixtures/lang-resolution/csharp-using-static/Helpers/MathUtils.cs new file mode 100644 index 000000000..8fb070c39 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-using-static/Helpers/MathUtils.cs @@ -0,0 +1,9 @@ +namespace Helpers; + +public static class MathUtils +{ + public static int Square(int x) + { + return x * x; + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/csharp-using-static/UsingStatic.csproj b/gitnexus/test/fixtures/lang-resolution/csharp-using-static/UsingStatic.csproj new file mode 100644 index 000000000..ec2cce143 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/csharp-using-static/UsingStatic.csproj @@ -0,0 +1,5 @@ + + + net8.0 + + diff --git a/gitnexus/test/integration/resolvers/csharp.test.ts b/gitnexus/test/integration/resolvers/csharp.test.ts index 31c49378c..e2cf06850 100644 --- a/gitnexus/test/integration/resolvers/csharp.test.ts +++ b/gitnexus/test/integration/resolvers/csharp.test.ts @@ -197,6 +197,100 @@ describe('C# member-call resolution', () => { }); }); +// --------------------------------------------------------------------------- +// Collection-accessor unwrap (Unit 6c): data.Values on Dictionary +// resolves to the value type's class. +// --------------------------------------------------------------------------- + +describe('C# collection-accessor unwrap', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-collection-accessor'), () => {}); + }, 60000); + + it('resolves RenderAll → Render through Dictionary.Values', () => { + const calls = getRelationships(result, 'CALLS'); + const renderCall = calls.find((c) => c.source === 'RenderAll' && c.target === 'Render'); + expect(renderCall).toBeDefined(); + expect(renderCall!.targetFilePath).toBe('Models/Widget.cs'); + expect(['import-resolved', 'global']).toContain(renderCall!.rel.reason); + }); +}); + +// --------------------------------------------------------------------------- +// using-static member injection (Unit 6d): `using static X.Y;` exposes Y's +// static methods as free-callables in the consumer. +// --------------------------------------------------------------------------- + +describe('C# using static member injection', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-using-static'), () => {}); + }, 60000); + + it('resolves Compute → Square via `using static Helpers.MathUtils;`', () => { + const calls = getRelationships(result, 'CALLS'); + const sqCall = calls.find((c) => c.source === 'Compute' && c.target === 'Square'); + expect(sqCall).toBeDefined(); + expect(sqCall!.targetFilePath).toBe('Helpers/MathUtils.cs'); + expect(['import-resolved', 'global']).toContain(sqCall!.rel.reason); + }); +}); + +// --------------------------------------------------------------------------- +// Overload disambiguation + interface dispatch (Unit 6e). +// --------------------------------------------------------------------------- + +describe('C# overload disambiguation and interface dispatch', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'csharp-overload-interface'), () => {}); + }, 60000); + + it('Run → Log resolves to the 2-arg overload (arity narrowing)', () => { + const calls = getRelationships(result, 'CALLS'); + const logCalls = calls.filter((c) => c.source === 'Run' && c.target === 'Log'); + // With collapse-by-caller-target enabled and arity narrowing, Run + // should bind to the 2-arg overload only — not the 1-arg sibling. + expect(logCalls.length).toBe(1); + // Verify targetId points to the 2-arg overload by checking the + // target Method node's parameterTypes length. + const target = result.graph.getNode(logCalls[0].rel.targetId); + expect(target).toBeDefined(); + const parameterTypes = (target!.properties as { parameterTypes?: string[] }).parameterTypes; + expect(parameterTypes).toBeDefined(); + expect(parameterTypes!.length).toBe(2); + }); + + it('Run → Greet emits primary edge to IGreeter.Greet plus interface-dispatch siblings', () => { + const calls = getRelationships(result, 'CALLS'); + const greetCalls = calls.filter((c) => c.source === 'Run' && c.target === 'Greet'); + // One primary edge (IGreeter.Greet) + two interface-dispatch edges + // (EnGreeter.Greet, FrGreeter.Greet). + expect(greetCalls.length).toBe(3); + + const primaries = greetCalls.filter((c) => c.rel.reason !== 'interface-dispatch'); + expect(primaries.length).toBe(1); + expect(primaries[0].targetFilePath).toBe('Greeting/IGreeter.cs'); + + const fanout = greetCalls.filter((c) => c.rel.reason === 'interface-dispatch'); + expect(fanout.length).toBe(2); + const fanoutPaths = fanout.map((c) => c.targetFilePath).sort(); + expect(fanoutPaths).toEqual(['Greeting/EnGreeter.cs', 'Greeting/FrGreeter.cs']); + }); + + it('interface-dispatch fan-out excludes the primary target (no self-edge)', () => { + const calls = getRelationships(result, 'CALLS'); + const fanout = calls.filter((c) => c.source === 'Run' && c.rel.reason === 'interface-dispatch'); + for (const edge of fanout) { + expect(edge.targetFilePath).not.toBe('Greeting/IGreeter.cs'); + } + }); +}); + // --------------------------------------------------------------------------- // Primary constructor resolution: class User(string name, int age) { } // ---------------------------------------------------------------------------