diff --git a/src/PatternKit.Generators/Builders/BuilderGenerator.cs b/src/PatternKit.Generators/Builders/BuilderGenerator.cs index 126a7cd0..01b282cc 100644 --- a/src/PatternKit.Generators/Builders/BuilderGenerator.cs +++ b/src/PatternKit.Generators/Builders/BuilderGenerator.cs @@ -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);"); @@ -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);"); @@ -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", 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); } diff --git a/src/PatternKit.Generators/DecoratorGenerator.cs b/src/PatternKit.Generators/DecoratorGenerator.cs index fa6b6afb..9cba58ca 100644 --- a/src/PatternKit.Generators/DecoratorGenerator.cs +++ b/src/PatternKit.Generators/DecoratorGenerator.cs @@ -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" + }; } private static bool HasAttribute(ISymbol symbol, string attributeName) diff --git a/test/PatternKit.Generators.Tests/BuilderGeneratorTests.cs b/test/PatternKit.Generators.Tests/BuilderGeneratorTests.cs index e5200dc2..09de5d58 100644 --- a/test/PatternKit.Generators.Tests/BuilderGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/BuilderGeneratorTests.cs @@ -798,4 +798,81 @@ public static async Task 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 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 BuildAsync()", generatedSource); + ScenarioExpect.Contains("AsyncProjectionHost.ProjectAsync(state)", generatedSource); + + var emit = updated.Emit(Stream.Null); + ScenarioExpect.True(emit.Success, string.Join(Environment.NewLine, emit.Diagnostics)); + } } diff --git a/test/PatternKit.Generators.Tests/ComposerGeneratorTests.cs b/test/PatternKit.Generators.Tests/ComposerGeneratorTests.cs index 1cd32f54..369aa9f8 100644 --- a/test/PatternKit.Generators.Tests/ComposerGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/ComposerGeneratorTests.cs @@ -1077,4 +1077,38 @@ private Task 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); + } } diff --git a/test/PatternKit.Generators.Tests/DecoratorGeneratorTests.cs b/test/PatternKit.Generators.Tests/DecoratorGeneratorTests.cs index 4a03e9fa..d1c3f68c 100644 --- a/test/PatternKit.Generators.Tests/DecoratorGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/DecoratorGeneratorTests.cs @@ -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)); + } } diff --git a/test/PatternKit.Generators.Tests/ProxyGeneratorTests.cs b/test/PatternKit.Generators.Tests/ProxyGeneratorTests.cs index e1715d22..291682ad 100644 --- a/test/PatternKit.Generators.Tests/ProxyGeneratorTests.cs +++ b/test/PatternKit.Generators.Tests/ProxyGeneratorTests.cs @@ -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)); + } }