diff --git a/src/CodegenTests/FSharpGenerationTests.cs b/src/CodegenTests/FSharpGenerationTests.cs index ba81e49..336f87f 100644 --- a/src/CodegenTests/FSharpGenerationTests.cs +++ b/src/CodegenTests/FSharpGenerationTests.cs @@ -89,6 +89,17 @@ public interface IFSharpSyncTaskHandler Task HandleAsync(string name); } +// Types for the DerivedVariable → Argument IsReferenced propagation test. +// Models the Wolverine pattern where a concrete argument (e.g. MessageContext) is +// exposed under an interface alias (e.g. IMessageContext) in DerivedVariables. +public interface IFakeBusContext { } +public class FakeBusContext : IFakeBusContext { } + +public interface IFakeBusContextHandler +{ + Task HandleAsync(FakeBusContext context); +} + public interface IFSharpTupleConsumer { void Consume(); @@ -371,6 +382,39 @@ public void generates_let_bang_binding_for_async_tuple_return() code.ShouldContain("let! struct (red, _, _) = _target.AsyncReturnTuple()"); } + [Fact] + public void argument_is_not_prefixed_with_underscore_when_derived_variable_with_same_name_is_referenced() + { + var assembly = new GeneratedAssembly(new GenerationRules("Some.Generated")); + var type = assembly.AddType("GeneratedFakeContextHandler", typeof(IFakeBusContextHandler)); + var method = type.MethodFor(nameof(IFakeBusContextHandler.HandleAsync)); + + // Simulate the Wolverine pattern: the concrete argument (FakeBusContext context) is + // exposed under an interface alias in DerivedVariables, and middleware marks the alias + // as referenced without touching the original Argument object. + // InternalsVisibleTo lets us set `IsReferenced` directly here. + var derivedContext = new Variable(typeof(IFakeBusContext), "context"); + derivedContext.IsReferenced = true; + method.DerivedVariables.Add(derivedContext); + + // This frame does NOT use `context` directly — it represents middleware that only + // needs the interface alias. + var service = new InjectedField(typeof(FSharpControlService), "service"); + method.Frames.Add(new MethodCall(typeof(FSharpControlService), nameof(FSharpControlService.Record)) + { + Target = service + }); + + var code = assembly.GenerateFSharpCode(); + + // WriteFSharpMethod must propagate IsReferenced from the DerivedVariable to the + // Argument with the matching Usage name. Without the fix the argument would be + // emitted as `_context` (FS1182 suppression prefix), breaking any frame body that + // references `context`. + code.ShouldContain("member this.HandleAsync(context:"); + code.ShouldNotContain("member this.HandleAsync(_context:"); + } + public class UnsupportedFrame : SyncFrame { public override void GenerateCode(GeneratedMethod method, ISourceWriter writer) diff --git a/src/JasperFx/CodeGeneration/GeneratedMethod.cs b/src/JasperFx/CodeGeneration/GeneratedMethod.cs index 578a005..7d65251 100644 --- a/src/JasperFx/CodeGeneration/GeneratedMethod.cs +++ b/src/JasperFx/CodeGeneration/GeneratedMethod.cs @@ -205,6 +205,21 @@ public void WriteFSharpMethod(ISourceWriter writer) Header?.Write(writer); + // Propagate IsReferenced from DerivedVariables to Arguments that share the same Usage name. + // Handles the case where a method argument (e.g. `context: MessageContext`) is exposed under + // an interface alias in DerivedVariables (e.g. `ContextVariable("context", IMessageContext)`): + // middleware frames resolve the alias and mark it referenced, but the original argument object + // is a distinct Variable instance and would otherwise receive an erroneous `_` prefix. + // Setting IsReferenced here (within the JasperFx assembly, so `internal set` is accessible) + // ensures all downstream F# code-generation that reads arg.IsReferenced is also consistent. + foreach (var arg in Arguments) + { + if (!arg.IsReferenced && DerivedVariables.Any(d => d.Usage == arg.Usage && d.IsReferenced)) + { + arg.IsReferenced = true; + } + } + var arguments = Arguments.Select(x => $"{(x.IsReferenced ? x.Usage : "_" + x.Usage)}: {x.VariableType.FSharpName()}").Join(", "); var returnType = ReturnType.FSharpName(); var keyword = Overrides ? "override" : "member";