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
41 changes: 31 additions & 10 deletions src/PatternKit.Generators/MementoGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -348,17 +348,20 @@ private string GenerateMementoStruct(TypeInfo typeInfo, SourceProductionContext
}
sb.AppendLine();

// Constructor
sb.Append($" private {typeInfo.TypeName}Memento(");
sb.Append(string.Join(", ", typeInfo.Members.Select(m => $"{m.Type} {ToCamelCase(m.Name)}")));
sb.AppendLine(")");
sb.AppendLine(" {");
foreach (var member in typeInfo.Members)
if (typeInfo.Members.Count > 0)
{
sb.AppendLine($" {member.Name} = {ToCamelCase(member.Name)};");
// Constructor
sb.Append($" private {typeInfo.TypeName}Memento(");
sb.Append(string.Join(", ", typeInfo.Members.Select(m => $"{m.Type} {ToCamelCase(m.Name)}")));
sb.AppendLine(")");
sb.AppendLine(" {");
foreach (var member in typeInfo.Members)
{
sb.AppendLine($" {member.Name} = {ToCamelCase(member.Name)};");
}
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine();

// Capture method
GenerateCaptureMethod(sb, typeInfo);
Expand Down Expand Up @@ -449,7 +452,7 @@ private void GenerateRestoreNewMethod(StringBuilder sb, TypeInfo typeInfo, Sourc
else
{
// Fall back to object initializer
sb.AppendLine("()");
AppendRecordFallbackConstructor(sb, typeInfo);
sb.AppendLine(" {");
foreach (var member in typeInfo.Members)
{
Expand Down Expand Up @@ -487,6 +490,24 @@ private void GenerateRestoreNewMethod(StringBuilder sb, TypeInfo typeInfo, Sourc
sb.AppendLine(" }");
}

private static void AppendRecordFallbackConstructor(StringBuilder sb, TypeInfo typeInfo)
{
var fallbackCtor = typeInfo.TypeSymbol.Constructors
.Where(c => !c.IsStatic && c.DeclaredAccessibility == Accessibility.Public)
.OrderBy(c => c.Parameters.Length)
.FirstOrDefault();

if (fallbackCtor is null || fallbackCtor.Parameters.Length == 0)
{
sb.AppendLine("()");
return;
}

sb.Append("(");
sb.Append(string.Join(", ", fallbackCtor.Parameters.Select(_ => "default!")));
sb.AppendLine(")");
}
Comment on lines +493 to +509

private void GenerateInPlaceRestoreMethod(StringBuilder sb, TypeInfo typeInfo)
{
sb.AppendLine($" /// <summary>Restores the memento state to an existing originator instance (in-place).</summary>");
Expand Down
35 changes: 35 additions & 0 deletions test/PatternKit.Generators.Tests/DecoratorGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1206,4 +1206,39 @@ string Format(
var emit = updated.Emit(Stream.Null);
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
}

[Scenario("Diagnostic PKDEC004 InaccessiblePropertyDeclaration")]
[Fact]
public void Diagnostic_PKDEC004_InaccessiblePropertyDeclaration()
{
const string source = """
using PatternKit.Generators.Decorator;

namespace TestNamespace;

[GenerateDecorator]
public abstract class SecretRepository
{
private protected abstract string Confidential { get; }
public abstract string Visible { get; }
}
""";

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

ScenarioExpect.Contains(result.Results.SelectMany(r => r.Diagnostics), d => d.Id == "PKDEC004" && d.GetMessage().Contains("Confidential"));

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

ScenarioExpect.Contains("Visible", generatedSource);
ScenarioExpect.DoesNotContain("Confidential", generatedSource);

var emit = updated.Emit(Stream.Null);
ScenarioExpect.True(emit.Success, string.Join("\n", emit.Diagnostics));
}
}
188 changes: 188 additions & 0 deletions test/PatternKit.Generators.Tests/MementoGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -399,4 +399,192 @@ public partial record class EditorState(string Text, int Cursor);
ScenarioExpect.Matches(@"private\s+const\s+int\s+MaxCapacity\s*=\s*50", caretakerSource);
ScenarioExpect.Matches(@"if\s*\(\s*_states\.Count\s*>\s*MaxCapacity\s*\)", caretakerSource);
}

[Scenario("ExplicitCaptureStrategies AreAcceptedWithoutReferenceWarnings")]
[Fact]
public void ExplicitCaptureStrategies_AreAcceptedWithoutReferenceWarnings()
{
const string source = """
using PatternKit.Generators;
using System.Collections.Generic;

namespace TestNamespace;

[Memento]
public partial class Document
{
[MementoStrategy(MementoCaptureStrategy.ByReference)]
public List<string> Tags { get; set; } = new();

[MementoStrategy(MementoCaptureStrategy.Clone)]
public CloneableBuffer Cloneable { get; set; } = new();

[MementoStrategy(MementoCaptureStrategy.DeepCopy)]
public CloneableBuffer Deep { get; set; } = new();

[MementoStrategy(MementoCaptureStrategy.Custom)]
public CloneableBuffer Custom { get; set; } = new();
}

public sealed class CloneableBuffer
{
}
""";

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

ScenarioExpect.DoesNotContain(result.Results.SelectMany(r => r.Diagnostics), d => d.Id == "PKMEM003");

var mementoSource = result.Results
.SelectMany(r => r.GeneratedSources)
.Single(gs => gs.HintName == "Document.Memento.g.cs")
.SourceText.ToString();

ScenarioExpect.Contains("global::System.Collections.Generic.List<string> Tags", mementoSource);
ScenarioExpect.Contains("global::TestNamespace.CloneableBuffer Cloneable", mementoSource);
ScenarioExpect.Contains("global::TestNamespace.CloneableBuffer Deep", mementoSource);
ScenarioExpect.Contains("global::TestNamespace.CloneableBuffer Custom", mementoSource);

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

[Scenario("RecordWithoutPrimaryConstructor RestoresWithObjectInitializer")]
[Fact]
public void RecordWithoutPrimaryConstructor_RestoresWithObjectInitializer()
{
const string source = """
using PatternKit.Generators;

namespace TestNamespace;

[Memento]
public partial record class EditorState
{
public string Text { get; init; } = "";
public int Cursor { get; init; }
}
""";

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

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

var mementoSource = result.Results
.SelectMany(r => r.GeneratedSources)
.Single(gs => gs.HintName == "EditorState.Memento.g.cs")
.SourceText.ToString();

ScenarioExpect.Contains("return new global::TestNamespace.EditorState()", mementoSource);
ScenarioExpect.Contains("Text = this.Text,", mementoSource);
ScenarioExpect.Contains("Cursor = this.Cursor,", mementoSource);

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

[Scenario("ClassWithOnlyReadonlyMembers RestoresWithDefaultConstructor")]
[Fact]
public void ClassWithOnlyReadonlyMembers_RestoresWithDefaultConstructor()
{
const string source = """
using PatternKit.Generators;

namespace TestNamespace;

[Memento]
public partial class ReadOnlyState
{
public readonly int Version;
}
""";

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

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

var mementoSource = result.Results
.SelectMany(r => r.GeneratedSources)
.Single(gs => gs.HintName == "ReadOnlyState.Memento.g.cs")
.SourceText.ToString();

ScenarioExpect.Contains("public int Version { get; }", mementoSource);
ScenarioExpect.Contains("return new global::TestNamespace.ReadOnlyState();", mementoSource);
ScenarioExpect.DoesNotContain("originator.Version = this.Version;", mementoSource);

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

[Scenario("RecordWithNoCapturedMembers RestoresWithDefaultConstructor")]
[Fact]
public void RecordWithNoCapturedMembers_RestoresWithDefaultConstructor()
{
const string source = """
using PatternKit.Generators;

namespace TestNamespace;

[Memento]
public partial record class EmptyState;
""";

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

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

var mementoSource = result.Results
.SelectMany(r => r.GeneratedSources)
.Single(gs => gs.HintName == "EmptyState.Memento.g.cs")
.SourceText.ToString();

ScenarioExpect.DoesNotContain("private EmptyStateMemento()", mementoSource);
ScenarioExpect.Contains("return new global::TestNamespace.EmptyState();", mementoSource);

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

[Scenario("RecordWithIgnoredPrimaryConstructorMember FallsBackToObjectInitializer")]
[Fact]
public void RecordWithIgnoredPrimaryConstructorMember_FallsBackToObjectInitializer()
{
const string source = """
using PatternKit.Generators;

namespace TestNamespace;

[Memento]
public partial record class FilteredState([property: MementoIgnore] string Raw)
{
public string Name { get; init; } = Raw;
}
""";

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

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

var mementoSource = result.Results
.SelectMany(r => r.GeneratedSources)
.Single(gs => gs.HintName == "FilteredState.Memento.g.cs")
.SourceText.ToString();

ScenarioExpect.Contains("return new global::TestNamespace.FilteredState(default!)", mementoSource);
ScenarioExpect.Contains("Name = this.Name,", mementoSource);
ScenarioExpect.DoesNotContain("Raw = this.Raw", mementoSource);

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