Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/CodegenTests/FSharpGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions src/JasperFx/CodeGeneration/GeneratedMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading