Skip to content

Commit ff1f79b

Browse files
committed
test: harden proxy generator coverage
1 parent 89ad9a7 commit ff1f79b

4 files changed

Lines changed: 155 additions & 68 deletions

File tree

docs/generators/proxy.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ Main attribute for marking interfaces or abstract classes for proxy generation.
232232
|----------|------|---------|-------------|
233233
| `ProxyTypeName` | `string?` | `{ContractName}Proxy` | Name of the generated proxy class |
234234
| `InterceptorMode` | `ProxyInterceptorMode` | `Single` | Interceptor support mode |
235-
| `GenerateAsync` | `bool?` | Auto-detected | Generate async interceptor methods |
235+
| `GenerateAsync` | `bool` | Auto-detected when omitted | Generate async interceptor methods |
236236
| `ForceAsync` | `bool` | `false` | Force async even if no async members detected |
237237
| `Exceptions` | `ProxyExceptionPolicy` | `Rethrow` | Exception handling policy |
238238

src/PatternKit.Generators.Abstractions/Proxy/GenerateProxyAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public sealed class GenerateProxyAttribute : Attribute
7676
/// If not specified, async support is inferred from the contract
7777
/// (enabled if any member returns Task/ValueTask or has a CancellationToken parameter).
7878
/// </summary>
79-
public bool? GenerateAsync { get; set; }
79+
public bool GenerateAsync { get; set; }
8080

8181
/// <summary>
8282
/// Gets or sets whether to force async interceptor hooks even if no async members are detected.

src/PatternKit.Generators/ProxyGenerator.cs

Lines changed: 27 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,7 @@ private void GenerateInterceptedDelegation(StringBuilder sb, MemberInfo member,
971971
var interceptorCheck = config.InterceptorMode == ProxyInterceptorMode.Single
972972
? "_interceptor is null"
973973
: "_interceptors is null || _interceptors.Count == 0";
974+
var useAsync = contractInfo.HasAsyncMembers && member.IsAsync;
974975

975976
sb.AppendLine($" if ({interceptorCheck})");
976977
sb.AppendLine(" {");
@@ -996,13 +997,13 @@ private void GenerateInterceptedDelegation(StringBuilder sb, MemberInfo member,
996997
else
997998
{
998999
var refModifier = member.ReturnsByRef || member.ReturnsByRefReadonly ? "ref " : "";
999-
if (member.IsAsync && !member.IsGenericAsyncReturnType)
1000+
if (useAsync && !member.IsGenericAsyncReturnType)
10001001
{
10011002
sb.Append($" await _inner.{member.Name}(");
10021003
}
10031004
else
10041005
{
1005-
var awaitModifier = member.IsAsync ? "await " : "";
1006+
var awaitModifier = useAsync ? "await " : "";
10061007
sb.Append($" return {awaitModifier}{refModifier}_inner.{member.Name}(");
10071008
}
10081009
sb.Append(string.Join(", ", member.Parameters.Select(p =>
@@ -1017,7 +1018,7 @@ private void GenerateInterceptedDelegation(StringBuilder sb, MemberInfo member,
10171018
return $"{refKind}{p.Name}";
10181019
})));
10191020
sb.AppendLine(");");
1020-
if (member.IsAsync && !member.IsGenericAsyncReturnType)
1021+
if (useAsync && !member.IsGenericAsyncReturnType)
10211022
{
10221023
sb.AppendLine(" return;");
10231024
}
@@ -1035,8 +1036,6 @@ private void GenerateInterceptedDelegation(StringBuilder sb, MemberInfo member,
10351036
sb.AppendLine();
10361037

10371038
// Use async or sync based on detection and configuration
1038-
bool useAsync = contractInfo.HasAsyncMembers && member.IsAsync;
1039-
10401039
if (useAsync)
10411040
{
10421041
GenerateAsyncInterceptedCall(sb, member, config, contextTypeName);
@@ -1172,70 +1171,32 @@ private void GenerateAsyncInterceptedCall(StringBuilder sb, MemberInfo member, P
11721171
sb.AppendLine(" }");
11731172
}
11741173

1175-
// Actual method call
1176-
if (member.IsVoid)
1177-
{
1178-
sb.Append($" _inner.{member.Name}(");
1179-
sb.Append(string.Join(", ", member.Parameters.Select(p =>
1180-
{
1181-
var refKind = p.RefKind switch
1182-
{
1183-
RefKind.Ref => "ref ",
1184-
RefKind.Out => "out ",
1185-
RefKind.In => "in ",
1186-
_ => ""
1187-
};
1188-
return $"{refKind}{p.Name}";
1189-
})));
1190-
sb.AppendLine(");");
1191-
}
1192-
else if (member.IsAsync)
1174+
// For async methods, get the task and await it.
1175+
sb.Append($" var __task = _inner.{member.Name}(");
1176+
sb.Append(string.Join(", ", member.Parameters.Select(p =>
11931177
{
1194-
// For async methods, get the task and await it
1195-
sb.Append($" var __task = _inner.{member.Name}(");
1196-
sb.Append(string.Join(", ", member.Parameters.Select(p =>
1178+
var refKind = p.RefKind switch
11971179
{
1198-
var refKind = p.RefKind switch
1199-
{
1200-
RefKind.Ref => "ref ",
1201-
RefKind.Out => "out ",
1202-
RefKind.In => "in ",
1203-
_ => ""
1204-
};
1205-
return $"{refKind}{p.Name}";
1206-
})));
1207-
sb.AppendLine(");");
1208-
sb.AppendLine(" __context.SetResult(__task);");
1180+
RefKind.Ref => "ref ",
1181+
RefKind.Out => "out ",
1182+
RefKind.In => "in ",
1183+
_ => ""
1184+
};
1185+
return $"{refKind}{p.Name}";
1186+
})));
1187+
sb.AppendLine(");");
1188+
sb.AppendLine(" __context.SetResult(__task);");
12091189

1210-
// Check if the async method returns a value (Task<T> or ValueTask<T> vs Task or ValueTask)
1211-
if (member.IsGenericAsyncReturnType)
1212-
{
1213-
// Await and store result for later return
1214-
sb.AppendLine(" var __result = await __task.ConfigureAwait(false);");
1215-
}
1216-
else
1217-
{
1218-
// Task or ValueTask with no result - just await
1219-
sb.AppendLine(" await __task.ConfigureAwait(false);");
1220-
}
1190+
// Check if the async method returns a value (Task<T> or ValueTask<T> vs Task or ValueTask)
1191+
if (member.IsGenericAsyncReturnType)
1192+
{
1193+
// Await and store result for later return
1194+
sb.AppendLine(" var __result = await __task.ConfigureAwait(false);");
12211195
}
12221196
else
12231197
{
1224-
var refModifier = member.ReturnsByRef || member.ReturnsByRefReadonly ? "ref " : "";
1225-
sb.Append($" var __result = {refModifier}_inner.{member.Name}(");
1226-
sb.Append(string.Join(", ", member.Parameters.Select(p =>
1227-
{
1228-
var refKind = p.RefKind switch
1229-
{
1230-
RefKind.Ref => "ref ",
1231-
RefKind.Out => "out ",
1232-
RefKind.In => "in ",
1233-
_ => ""
1234-
};
1235-
return $"{refKind}{p.Name}";
1236-
})));
1237-
sb.AppendLine(");");
1238-
sb.AppendLine(" __context.SetResult(__result);");
1198+
// Task or ValueTask with no result - just await
1199+
sb.AppendLine(" await __task.ConfigureAwait(false);");
12391200
}
12401201
sb.AppendLine();
12411202

@@ -1252,8 +1213,8 @@ private void GenerateAsyncInterceptedCall(StringBuilder sb, MemberInfo member, P
12521213
sb.AppendLine(" }");
12531214
}
12541215

1255-
// Return statement (only for non-void and for async methods with generic Task<T>/ValueTask<T>)
1256-
if (!member.IsVoid && (!member.IsAsync || member.IsGenericAsyncReturnType))
1216+
// Return statement for async methods with generic Task<T>/ValueTask<T>.
1217+
if (member.IsGenericAsyncReturnType)
12571218
{
12581219
sb.AppendLine(" return __result;");
12591220
}
@@ -1282,7 +1243,7 @@ private void GenerateAsyncInterceptedCall(StringBuilder sb, MemberInfo member, P
12821243
}
12831244
else // Swallow
12841245
{
1285-
if (!member.IsVoid && (!member.IsAsync || member.IsGenericAsyncReturnType))
1246+
if (member.IsGenericAsyncReturnType)
12861247
{
12871248
sb.AppendLine(" return default!;");
12881249
}

test/PatternKit.Generators.Tests/ProxyGeneratorTests.cs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,4 +1531,130 @@ string Format(
15311531
var emit = updated.Emit(Stream.Null);
15321532
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
15331533
}
1534+
1535+
[Scenario("GenerateProxy GenerateAsyncFalse ReportsWarningsAndSuppressesAsyncHooks")]
1536+
[Fact]
1537+
public void GenerateProxy_GenerateAsyncFalse_ReportsWarningsAndSuppressesAsyncHooks()
1538+
{
1539+
const string source = """
1540+
using PatternKit.Generators.Proxy;
1541+
using System.Threading;
1542+
using System.Threading.Tasks;
1543+
1544+
namespace TestNamespace;
1545+
1546+
[GenerateProxy(GenerateAsync = false)]
1547+
public partial interface IExplicitSyncProxy
1548+
{
1549+
Task SaveAsync(CancellationToken cancellationToken = default);
1550+
string Poll(CancellationToken cancellationToken = default);
1551+
}
1552+
""";
1553+
1554+
var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateProxy_GenerateAsyncFalse_ReportsWarningsAndSuppressesAsyncHooks));
1555+
var gen = new ProxyGenerator();
1556+
_ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
1557+
1558+
var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
1559+
ScenarioExpect.Equal(2, diagnostics.Count(d => d.Id == "PKPRX005"));
1560+
ScenarioExpect.Contains(diagnostics, d => d.Id == "PKPRX005" && d.GetMessage().Contains("SaveAsync"));
1561+
ScenarioExpect.Contains(diagnostics, d => d.Id == "PKPRX005" && d.GetMessage().Contains("Poll"));
1562+
1563+
var interceptorSource = result.Results
1564+
.SelectMany(r => r.GeneratedSources)
1565+
.Single(gs => gs.HintName == "TestNamespace_IExplicitSyncProxy.Proxy.Interceptor.g.cs")
1566+
.SourceText.ToString();
1567+
1568+
ScenarioExpect.DoesNotContain("BeforeAsync", interceptorSource);
1569+
1570+
var emit = updated.Emit(Stream.Null);
1571+
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
1572+
}
1573+
1574+
[Scenario("GenerateProxy Defaults CoverDynamicNullParamsAndReservedContextNames")]
1575+
[Fact]
1576+
public void GenerateProxy_Defaults_CoverDynamicNullParamsAndReservedContextNames()
1577+
{
1578+
const string source = """
1579+
using PatternKit.Generators.Proxy;
1580+
1581+
namespace TestNamespace;
1582+
1583+
[GenerateProxy]
1584+
public partial interface IContextNameProxy
1585+
{
1586+
string Format(
1587+
dynamic payload = null,
1588+
string methodName = "calculate",
1589+
string arguments = "items",
1590+
string result = "ok",
1591+
params string[] tags);
1592+
}
1593+
""";
1594+
1595+
var comp = RoslynTestHelpers.CreateCompilation(
1596+
source,
1597+
nameof(GenerateProxy_Defaults_CoverDynamicNullParamsAndReservedContextNames),
1598+
extra:
1599+
[
1600+
MetadataReference.CreateFromFile(typeof(System.Runtime.CompilerServices.DynamicAttribute).Assembly.Location),
1601+
MetadataReference.CreateFromFile(typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly.Location)
1602+
]);
1603+
var gen = new ProxyGenerator();
1604+
_ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
1605+
1606+
ScenarioExpect.All(result.Results, r => ScenarioExpect.Empty(r.Diagnostics));
1607+
1608+
var proxySource = result.Results
1609+
.SelectMany(r => r.GeneratedSources)
1610+
.Single(gs => gs.HintName == "TestNamespace_IContextNameProxy.Proxy.g.cs")
1611+
.SourceText.ToString();
1612+
var interceptorSource = result.Results
1613+
.SelectMany(r => r.GeneratedSources)
1614+
.Single(gs => gs.HintName == "TestNamespace_IContextNameProxy.Proxy.Interceptor.g.cs")
1615+
.SourceText.ToString();
1616+
1617+
ScenarioExpect.Contains("dynamic payload = null", proxySource);
1618+
ScenarioExpect.Contains("params string[] tags", proxySource);
1619+
ScenarioExpect.Contains("Arg_MethodName", interceptorSource);
1620+
ScenarioExpect.Contains("Arg_Arguments", interceptorSource);
1621+
ScenarioExpect.Contains("Arg_Result", interceptorSource);
1622+
1623+
var emit = updated.Emit(Stream.Null);
1624+
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
1625+
}
1626+
1627+
[Scenario("GenerateProxy NoInterceptorVoidMethodWithPlainParameter Delegates")]
1628+
[Fact]
1629+
public void GenerateProxy_NoInterceptorVoidMethodWithPlainParameter_Delegates()
1630+
{
1631+
const string source = """
1632+
using PatternKit.Generators.Proxy;
1633+
1634+
namespace TestNamespace;
1635+
1636+
[GenerateProxy(InterceptorMode = ProxyInterceptorMode.None)]
1637+
public partial interface IVoidDelegateProxy
1638+
{
1639+
void Track(string message);
1640+
}
1641+
""";
1642+
1643+
var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateProxy_NoInterceptorVoidMethodWithPlainParameter_Delegates));
1644+
var gen = new ProxyGenerator();
1645+
_ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
1646+
1647+
ScenarioExpect.All(result.Results, r => ScenarioExpect.Empty(r.Diagnostics));
1648+
1649+
var proxySource = result.Results
1650+
.SelectMany(r => r.GeneratedSources)
1651+
.Single(gs => gs.HintName == "TestNamespace_IVoidDelegateProxy.Proxy.g.cs")
1652+
.SourceText.ToString();
1653+
1654+
ScenarioExpect.Contains("_inner.Track(message);", proxySource);
1655+
ScenarioExpect.DoesNotContain("return _inner.Track", proxySource);
1656+
1657+
var emit = updated.Emit(Stream.Null);
1658+
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
1659+
}
15341660
}

0 commit comments

Comments
 (0)