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
13 changes: 8 additions & 5 deletions src/PatternKit.Generators/Builders/BuilderGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,11 @@ private static GenerationResult BuildProjection(
mb.AppendLine();
if (asyncEnabled)
{
mb.Append(" public static ValueTask<").Append(returnType).Append("> BuildAsync(Func<").Append(builderTypeName).AppendLine(", ValueTask> configure)");
var asyncReturnType = IsValueTask(defaultProjector.ReturnType)
? returnType
: $"ValueTask<{returnType}>";

mb.Append(" public static ").Append(asyncReturnType).Append(" BuildAsync(Func<").Append(builderTypeName).AppendLine(", ValueTask> configure)");
mb.AppendLine(" {");
mb.Append(" var builder = ").Append(builderTypeName).Append('.').Append(newMethodName).AppendLine("();");
mb.AppendLine(" var pending = configure(builder);");
Expand All @@ -599,7 +603,7 @@ private static GenerationResult BuildProjection(
mb.Append(" return builder.").Append(buildMethodName).AppendLine("Async();");
}
mb.AppendLine();
mb.Append(" static async ValueTask<").Append(returnType).Append("> AwaitAsync(ValueTask wait, ").Append(builderTypeName).AppendLine(" b)");
mb.Append(" static async ").Append(asyncReturnType).Append(" AwaitAsync(ValueTask wait, ").Append(builderTypeName).AppendLine(" b)");
mb.AppendLine(" {");
mb.AppendLine(" await wait.ConfigureAwait(false);");
mb.Append(" return await b.").Append(buildMethodName).AppendLine("Async().ConfigureAwait(false);");
Expand Down Expand Up @@ -714,9 +718,8 @@ private static bool IsValueTask(ITypeSymbol type)
return false;

var constructed = named.ConstructedFrom;
var full = constructed.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
return string.Equals(full, "global::System.Threading.Tasks.ValueTask<T>", StringComparison.Ordinal) ||
string.Equals(full, "global::System.Threading.Tasks.ValueTask", StringComparison.Ordinal);
return string.Equals(constructed.Name, "ValueTask", StringComparison.Ordinal) &&
string.Equals(constructed.ContainingNamespace.ToDisplayString(), "System.Threading.Tasks", StringComparison.Ordinal);

}

Expand Down
12 changes: 9 additions & 3 deletions src/PatternKit.Generators/DecoratorGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -660,9 +660,15 @@ private static string FormatDefaultValue(IParameterSymbol param)
return $"({enumType.ToDisplayString(TypeFormat)}){param.ExplicitDefaultValue}";
}

// Use Roslyn's culture-invariant literal formatting for all other types.
return Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatPrimitive(param.ExplicitDefaultValue, quoteStrings: true, useHexadecimalNumbers: false)
?? "default";
var value = param.ExplicitDefaultValue;
return value switch
{
float f => f.ToString(System.Globalization.CultureInfo.InvariantCulture) + "f",
double d => d.ToString(System.Globalization.CultureInfo.InvariantCulture) + "d",
decimal m => m.ToString(System.Globalization.CultureInfo.InvariantCulture) + "m",
_ => Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatPrimitive(value, quoteStrings: true, useHexadecimalNumbers: false)
?? "default"
};
Comment on lines +663 to +671
}

private static bool HasAttribute(ISymbol symbol, string attributeName)
Expand Down
77 changes: 77 additions & 0 deletions test/PatternKit.Generators.Tests/BuilderGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -798,4 +798,81 @@ public static async Task<string> RunAsync()
var emit = updated.Emit(pe);
ScenarioExpect.True(emit.Success, string.Join(Environment.NewLine, emit.Diagnostics));
}

[Scenario("BuilderDiagnostics CoverNameConflictAndPrivateSetter")]
[Fact]
public void BuilderDiagnostics_CoverNameConflictAndPrivateSetter()
{
const string source = """
using PatternKit.Generators.Builders;

namespace PatternKit.Examples.Builders;

public sealed class ConflictingBuilder
{
}

[GenerateBuilder(BuilderTypeName = "ConflictingBuilder")]
public partial class Conflicting
{
public string? Name { get; set; }
}

[GenerateBuilder]
public partial class PrivateSetterSample
{
public string? Name { get; private set; }
public int Count { get; set; }
}
""";

var comp = RoslynTestHelpers.CreateCompilation(source, nameof(BuilderDiagnostics_CoverNameConflictAndPrivateSetter));
var gen = new BuilderGenerator();
_ = RoslynTestHelpers.Run(comp, gen, out var run, out _);

var diagnostics = run.Results.SelectMany(r => r.Diagnostics).ToArray();
ScenarioExpect.Contains(diagnostics, diagnostic => diagnostic.Id == "B002");
ScenarioExpect.Contains(diagnostics, diagnostic => diagnostic.Id == "B005" && diagnostic.GetMessage().Contains("Name"));
}

[Scenario("StateProjectionBuilder GeneratesValueTaskDefaultProjectorPath")]
[Fact]
public void StateProjectionBuilder_GeneratesValueTaskDefaultProjectorPath()
{
const string source = """
using System.Threading.Tasks;
using PatternKit.Generators.Builders;

namespace PatternKit.Examples.Builders;

public readonly record struct AsyncState(string Name);
public sealed record AsyncDto(string Name);

[GenerateBuilder(Model = BuilderModel.StateProjection, GenerateBuilderMethods = true)]
public static partial class AsyncProjectionHost
{
public static AsyncState Seed() => default;

[BuilderProjector]
public static ValueTask<AsyncDto> ProjectAsync(AsyncState state)
=> new(new AsyncDto(state.Name ?? ""));
}
""";

var comp = RoslynTestHelpers.CreateCompilation(source, nameof(StateProjectionBuilder_GeneratesValueTaskDefaultProjectorPath));
var gen = new BuilderGenerator();
_ = RoslynTestHelpers.Run(comp, gen, out var run, out var updated);

ScenarioExpect.All(run.Results, r => ScenarioExpect.Empty(r.Diagnostics));

var generatedSource = string.Join(Environment.NewLine, run.Results
.SelectMany(r => r.GeneratedSources)
.Select(source => source.SourceText.ToString()));

ScenarioExpect.Contains("public ValueTask<global::PatternKit.Examples.Builders.AsyncDto> BuildAsync()", generatedSource);
ScenarioExpect.Contains("AsyncProjectionHost.ProjectAsync(state)", generatedSource);

var emit = updated.Emit(Stream.Null);
ScenarioExpect.True(emit.Success, string.Join(Environment.NewLine, emit.Diagnostics));
}
}
34 changes: 34 additions & 0 deletions test/PatternKit.Generators.Tests/ComposerGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1077,4 +1077,38 @@ private Task<Response> TerminalAsync(Request req, CancellationToken ct)
var emit = updated.Emit(Stream.Null);
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
}

[Scenario("InvalidStep WithNonFuncNext ReportsDiagnostic")]
[Fact]
public void InvalidStep_WithNonFuncNext_ReportsDiagnostic()
{
var source = """
using PatternKit.Generators.Composer;

public readonly record struct Request(string Path);
public readonly record struct Response(int Status);

public delegate Response RequestDelegate(Request request);

[Composer]
public partial class RequestPipeline
{
[ComposeStep(0)]
private Response Step(in Request req, RequestDelegate next)
=> next(req);

[ComposeTerminal]
private Response Terminal(in Request req)
=> new(200);
}
""";

var comp = RoslynTestHelpers.CreateCompilation(source, nameof(InvalidStep_WithNonFuncNext_ReportsDiagnostic));
var gen = new ComposerGenerator();
_ = RoslynTestHelpers.Run(comp, gen, out var result, out _);

var diagnostic = ScenarioExpect.Single(result.Results.SelectMany(r => r.Diagnostics));
ScenarioExpect.Equal("PKCOM006", diagnostic.Id);
ScenarioExpect.Contains("Step", diagnostic.GetMessage(), StringComparison.Ordinal);
}
}
46 changes: 46 additions & 0 deletions test/PatternKit.Generators.Tests/DecoratorGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1160,4 +1160,50 @@ public class NestedType
ScenarioExpect.Contains(diagnostics, d => d.Id == "PKDEC002" && d.GetMessage().Contains("Field"));
ScenarioExpect.Contains(diagnostics, d => d.Id == "PKDEC002" && d.GetMessage().Contains("NestedType"));
}

[Scenario("GenerateDecorator PreservesPrimitiveAndStringDefaultLiterals")]
[Fact]
public void GenerateDecorator_PreservesPrimitiveAndStringDefaultLiterals()
{
const string source = """
using PatternKit.Generators.Decorator;

namespace TestNamespace;

public enum Mode { None = 0, Known = 1 }

[GenerateDecorator]
public interface ILiteralService
{
string Format(
string text = "line\n\"quoted\"\tend",
bool enabled = true,
float ratio = 1.25f,
double weight = 2.5d,
decimal amount = 3.75m,
Mode mode = (Mode)99);
}
""";

var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateDecorator_PreservesPrimitiveAndStringDefaultLiterals));
var gen = new DecoratorGenerator();
_ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);

ScenarioExpect.All(result.Results, r => ScenarioExpect.Empty(r.Diagnostics));

var generatedSource = result.Results
.SelectMany(r => r.GeneratedSources)
.First(gs => gs.HintName == "TestNamespace_ILiteralService.Decorator.g.cs")
.SourceText.ToString();

ScenarioExpect.Contains("string text = \"line\\n\\\"quoted\\\"\\tend\"", generatedSource);
ScenarioExpect.Contains("bool enabled = true", generatedSource);
ScenarioExpect.Contains("float ratio = 1.25f", generatedSource);
ScenarioExpect.Contains("double weight = 2.5d", generatedSource);
ScenarioExpect.Contains("decimal amount = 3.75m", generatedSource);
ScenarioExpect.Contains("global::TestNamespace.Mode mode = (global::TestNamespace.Mode)99", generatedSource);

var emit = updated.Emit(Stream.Null);
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
}
}
44 changes: 44 additions & 0 deletions test/PatternKit.Generators.Tests/ProxyGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1487,4 +1487,48 @@ string Format(
var emit = updated.Emit(Stream.Null);
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
}

[Scenario("GenerateProxy Defaults CoverPrimitiveStringAndNumericBranches")]
[Fact]
public void GenerateProxy_Defaults_CoverPrimitiveStringAndNumericBranches()
{
const string source = """
using PatternKit.Generators.Proxy;

namespace TestNamespace;

[GenerateProxy(InterceptorMode = ProxyInterceptorMode.None)]
public partial interface ILiteralProxy
{
string Format(
string text = "line\n\"quoted\"\tend",
bool enabled = true,
float ratio = 1.25f,
double weight = 2.5d,
decimal amount = 3.75m,
char apostrophe = '\'');
}
""";

var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateProxy_Defaults_CoverPrimitiveStringAndNumericBranches));
var gen = new ProxyGenerator();
_ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);

ScenarioExpect.All(result.Results, r => ScenarioExpect.Empty(r.Diagnostics));

var proxySource = result.Results
.SelectMany(r => r.GeneratedSources)
.Single(gs => gs.HintName == "TestNamespace_ILiteralProxy.Proxy.g.cs")
.SourceText.ToString();

ScenarioExpect.Contains("string text = \"line\\n\\\"quoted\\\"\\tend\"", proxySource);
ScenarioExpect.Contains("bool enabled = true", proxySource);
ScenarioExpect.Contains("float ratio = 1.25f", proxySource);
ScenarioExpect.Contains("double weight = 2.5d", proxySource);
ScenarioExpect.Contains("decimal amount = 3.75m", proxySource);
ScenarioExpect.Contains("char apostrophe = '\\''", proxySource);

var emit = updated.Emit(Stream.Null);
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
}
}
Loading