From 3a87295ede87b2dc722dfe13e71fc06d06f61b02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Strus?=
<61932823+lukasz-strus@users.noreply.github.com>
Date: Sun, 29 Sep 2024 18:19:29 +0200
Subject: [PATCH 1/2] #8 Adding projects
---
Patternify.sln | 21 +++++++
.../Patternify.Examples.Prototype.csproj | 16 ++++++
.../Patternify.Examples.Prototype/Program.cs | 2 +
.../Patternify.Prototype.csproj | 55 +++++++++++++++++++
src/Patternify.Prototype/README.md | 0
.../Patternify.Prototype.Tests.csproj | 43 +++++++++++++++
6 files changed, 137 insertions(+)
create mode 100644 examples/Patternify.Examples.Prototype/Patternify.Examples.Prototype.csproj
create mode 100644 examples/Patternify.Examples.Prototype/Program.cs
create mode 100644 src/Patternify.Prototype/Patternify.Prototype.csproj
create mode 100644 src/Patternify.Prototype/README.md
create mode 100644 tests/Patternify.Prototype.Tests/Patternify.Prototype.Tests.csproj
diff --git a/Patternify.sln b/Patternify.sln
index e9a55a4..970b6b1 100644
--- a/Patternify.sln
+++ b/Patternify.sln
@@ -27,6 +27,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Patternify.NullObject.Tests
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Patternify.Examples.NullObject", "examples\Patternify.Examples.NullObject\Patternify.Examples.NullObject.csproj", "{6EFB467B-5C42-49C4-B403-9B86E9C7CD5E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Patternify.Prototype", "src\Patternify.Prototype\Patternify.Prototype.csproj", "{82C0FE66-59E2-462A-A881-72523BC02944}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Patternify.Prototype.Tests", "tests\Patternify.Prototype.Tests\Patternify.Prototype.Tests.csproj", "{39F4F238-C07E-4912-B0C3-8D942C8484BF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Patternify.Examples.Prototype", "examples\Patternify.Examples.Prototype\Patternify.Examples.Prototype.csproj", "{7B67611D-48B5-4B8A-850F-08A85AD0E25E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -69,6 +75,18 @@ Global
{6EFB467B-5C42-49C4-B403-9B86E9C7CD5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6EFB467B-5C42-49C4-B403-9B86E9C7CD5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6EFB467B-5C42-49C4-B403-9B86E9C7CD5E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {82C0FE66-59E2-462A-A881-72523BC02944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {82C0FE66-59E2-462A-A881-72523BC02944}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {82C0FE66-59E2-462A-A881-72523BC02944}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {82C0FE66-59E2-462A-A881-72523BC02944}.Release|Any CPU.Build.0 = Release|Any CPU
+ {39F4F238-C07E-4912-B0C3-8D942C8484BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {39F4F238-C07E-4912-B0C3-8D942C8484BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {39F4F238-C07E-4912-B0C3-8D942C8484BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {39F4F238-C07E-4912-B0C3-8D942C8484BF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7B67611D-48B5-4B8A-850F-08A85AD0E25E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7B67611D-48B5-4B8A-850F-08A85AD0E25E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7B67611D-48B5-4B8A-850F-08A85AD0E25E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7B67611D-48B5-4B8A-850F-08A85AD0E25E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -83,6 +101,9 @@ Global
{F9734EC6-AD7C-4727-BA6A-0DBF791376F2} = {0C52C505-41D2-433A-9A53-F693FF3C1596}
{47991FD1-0140-47EC-9194-27E0FD854882} = {F5334383-D2D3-4836-8E69-5A5C3ABB4B89}
{6EFB467B-5C42-49C4-B403-9B86E9C7CD5E} = {491381AD-CF5A-4EBB-997B-09A66A586DF3}
+ {82C0FE66-59E2-462A-A881-72523BC02944} = {0C52C505-41D2-433A-9A53-F693FF3C1596}
+ {39F4F238-C07E-4912-B0C3-8D942C8484BF} = {F5334383-D2D3-4836-8E69-5A5C3ABB4B89}
+ {7B67611D-48B5-4B8A-850F-08A85AD0E25E} = {491381AD-CF5A-4EBB-997B-09A66A586DF3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B9321E16-075D-4DC4-8570-9DC648EED48B}
diff --git a/examples/Patternify.Examples.Prototype/Patternify.Examples.Prototype.csproj b/examples/Patternify.Examples.Prototype/Patternify.Examples.Prototype.csproj
new file mode 100644
index 0000000..bc97115
--- /dev/null
+++ b/examples/Patternify.Examples.Prototype/Patternify.Examples.Prototype.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
diff --git a/examples/Patternify.Examples.Prototype/Program.cs b/examples/Patternify.Examples.Prototype/Program.cs
new file mode 100644
index 0000000..3751555
--- /dev/null
+++ b/examples/Patternify.Examples.Prototype/Program.cs
@@ -0,0 +1,2 @@
+// See https://aka.ms/new-console-template for more information
+Console.WriteLine("Hello, World!");
diff --git a/src/Patternify.Prototype/Patternify.Prototype.csproj b/src/Patternify.Prototype/Patternify.Prototype.csproj
new file mode 100644
index 0000000..b55a622
--- /dev/null
+++ b/src/Patternify.Prototype/Patternify.Prototype.csproj
@@ -0,0 +1,55 @@
+
+
+
+ netstandard2.0
+ enable
+ enable
+ latest
+ true
+ true
+ Patternify.Prototype
+ Łukasz Strus
+ MIT
+ icon.png
+
+ This package provides a Roslyn source generator that automatically generates the Prototype design pattern for C# classes.
+ It simplifies the creation of cloneable objects by generating the necessary cloning methods, ensuring deep or shallow copies
+ depending on the object’s structure. The generator supports customization to tailor the clone behavior for your specific needs.
+
+ https://github.com/lukasz-strus/Patternify
+
+ Roslyn;
+ SourceGenerator;
+ Prototype;
+ DesignPatterns;
+ CSharp;
+ ThreadSafe;
+ CodeGeneration;
+ AOT;
+ DependencyInjection;
+
+ Patternify - Prototype Design Pattern Source Generator (C# / Roslyn)
+ README.md
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Patternify.Prototype/README.md b/src/Patternify.Prototype/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/tests/Patternify.Prototype.Tests/Patternify.Prototype.Tests.csproj b/tests/Patternify.Prototype.Tests/Patternify.Prototype.Tests.csproj
new file mode 100644
index 0000000..c5eccdc
--- /dev/null
+++ b/tests/Patternify.Prototype.Tests/Patternify.Prototype.Tests.csproj
@@ -0,0 +1,43 @@
+
+
+
+ net8.0
+ enable
+ enable
+ latest
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 9cf015d35acd4a7aa1e8ef74c07583eb8a64bb96 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Strus?=
<61932823+lukasz-strus@users.noreply.github.com>
Date: Thu, 3 Oct 2024 19:49:21 +0200
Subject: [PATCH 2/2] #8 Implement prototype builder
---
.../Generators/MainGenerator.cs | 3 +-
.../Generators/MainSyntaxReceiver.cs | 7 +-
.../Patternify.Abstraction.csproj | 2 +
.../Generators/Helpers/DeepCopyHelper.cs | 53 +++++++++++++
.../Generators/PrototypeBuilder.cs | 74 +++++++++++++++++++
.../Generators/PrototypeSyntaxReceiver.cs | 8 ++
.../PrototypeAttribute.cs | 17 +++++
...rSource_ReturnGeneratedSource.verified.txt | 40 ++++++++++
.../Generators/PrototypeBuilderTests.cs | 62 ++++++++++++++++
9 files changed, 262 insertions(+), 4 deletions(-)
create mode 100644 src/Patternify.Prototype/Generators/Helpers/DeepCopyHelper.cs
create mode 100644 src/Patternify.Prototype/Generators/PrototypeBuilder.cs
create mode 100644 src/Patternify.Prototype/Generators/PrototypeSyntaxReceiver.cs
create mode 100644 src/Patternify.Prototype/PrototypeAttribute.cs
create mode 100644 tests/Patternify.Prototype.Tests/Generators/PrototypeBuilderTests.Build_ForSource_ReturnGeneratedSource.verified.txt
create mode 100644 tests/Patternify.Prototype.Tests/Generators/PrototypeBuilderTests.cs
diff --git a/src/Patternify.Abstraction/Generators/MainGenerator.cs b/src/Patternify.Abstraction/Generators/MainGenerator.cs
index 6d7e017..9590129 100644
--- a/src/Patternify.Abstraction/Generators/MainGenerator.cs
+++ b/src/Patternify.Abstraction/Generators/MainGenerator.cs
@@ -1,5 +1,4 @@
-using System.Diagnostics;
-using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text;
diff --git a/src/Patternify.Abstraction/Generators/MainSyntaxReceiver.cs b/src/Patternify.Abstraction/Generators/MainSyntaxReceiver.cs
index 2bddefd..375c71a 100644
--- a/src/Patternify.Abstraction/Generators/MainSyntaxReceiver.cs
+++ b/src/Patternify.Abstraction/Generators/MainSyntaxReceiver.cs
@@ -10,9 +10,12 @@ internal abstract class MainSyntaxReceiver : ISyntaxReceiver
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
- if (syntaxNode is not AttributeSyntax { Name: IdentifierNameSyntax { Identifier.Text: var attributeName } } attribute
+ if (syntaxNode is not AttributeSyntax
+ {
+ Name: IdentifierNameSyntax { Identifier.Text: var attributeName }
+ } attribute
|| attributeName != AttributeName.Replace(nameof(Attribute), string.Empty)) return;
-
+
Attributes.Add(attribute);
}
}
\ No newline at end of file
diff --git a/src/Patternify.Abstraction/Patternify.Abstraction.csproj b/src/Patternify.Abstraction/Patternify.Abstraction.csproj
index ca3fd1f..5773fcd 100644
--- a/src/Patternify.Abstraction/Patternify.Abstraction.csproj
+++ b/src/Patternify.Abstraction/Patternify.Abstraction.csproj
@@ -37,6 +37,8 @@
+
+
diff --git a/src/Patternify.Prototype/Generators/Helpers/DeepCopyHelper.cs b/src/Patternify.Prototype/Generators/Helpers/DeepCopyHelper.cs
new file mode 100644
index 0000000..842812c
--- /dev/null
+++ b/src/Patternify.Prototype/Generators/Helpers/DeepCopyHelper.cs
@@ -0,0 +1,53 @@
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Patternify.Prototype.Generators.Helpers;
+
+internal static class DeepCopyHelper
+{
+ internal static string WriteObjectFieldsClone(
+ ClassDeclarationSyntax group,
+ ICollection allClassGroups)
+ {
+ var typeNames = allClassGroups.Select(y => y.Identifier.Text);
+
+ var properties = group.Members
+ .OfType()
+ .Distinct()
+ .Where(property => typeNames
+ .Any(typeName => property.Type.ToString().TrimEnd('?').Contains(typeName)));
+
+ return $"{string.Join("\n\n\t\t",
+ properties.Select(p => $"clone.{p.Identifier.Text} = {WriteNewObject(allClassGroups, p)}"))}";
+ }
+
+ private static string WriteNewObject(
+ IEnumerable allClasses,
+ PropertyDeclarationSyntax property)
+ {
+ var typeName = property.Type.ToString();
+
+ var classDeclaration = allClasses
+ .FirstOrDefault(@class => @class.Identifier.Text.Contains(typeName.TrimEnd('?')));
+
+ return classDeclaration is null
+ ? string.Empty
+ : $$"""
+ new {{classDeclaration.Identifier.Text}}()
+ {
+ {{WriteAssignFields(classDeclaration, property)}}
+ };
+ """;
+ }
+
+ private static string WriteAssignFields(ClassDeclarationSyntax classDeclaration,
+ PropertyDeclarationSyntax propertyObject)
+ {
+ var properties = classDeclaration.Members.OfType();
+
+ return string.Join(",\n\t\t\t", properties.Select(p => WriteAssign(p, propertyObject)));
+ }
+
+ private static string WriteAssign(PropertyDeclarationSyntax propertyToAssign,
+ PropertyDeclarationSyntax propertyObject)
+ => $"{propertyToAssign.Identifier.Text} = {propertyObject.Identifier.Text}.{propertyToAssign.Identifier.Text}";
+}
\ No newline at end of file
diff --git a/src/Patternify.Prototype/Generators/PrototypeBuilder.cs b/src/Patternify.Prototype/Generators/PrototypeBuilder.cs
new file mode 100644
index 0000000..a0f2c3c
--- /dev/null
+++ b/src/Patternify.Prototype/Generators/PrototypeBuilder.cs
@@ -0,0 +1,74 @@
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Patternify.Abstraction.Generators;
+using Patternify.Prototype.Generators.Helpers;
+using System.Text;
+
+namespace Patternify.Prototype.Generators;
+
+internal sealed class PrototypeBuilder : MainBuilder
+{
+ private readonly StringBuilder _deepCopyMethod = new();
+ private readonly StringBuilder _shallowCopyMethod = new();
+
+ internal void SetDeepCopyMethod(
+ ClassDeclarationSyntax @class,
+ ICollection allClasses)
+ {
+ var src =
+ $$"""
+ {{AccessModifier}} {{ClassName}} DeepCopy()
+ {
+ {{ClassName}} clone = ({{ClassName}})this.ShallowCopy();
+
+ {{DeepCopyHelper.WriteObjectFieldsClone(@class, allClasses)}}
+
+ return clone;
+ }
+ """;
+
+ _deepCopyMethod.Clear();
+ _deepCopyMethod.Append(src);
+ }
+
+ internal void SetShallowCopyMethod(ClassDeclarationSyntax @class)
+ {
+ var src =
+ $$"""
+ {{AccessModifier}} {{ClassName}} ShallowCopy()
+ {
+ return ({{ClassName}})this.MemberwiseClone();
+ }
+ """;
+
+ _shallowCopyMethod.Clear();
+ _shallowCopyMethod.Append(src);
+ }
+
+ protected override string BuildSource() =>
+ $$"""
+ //
+ {{Usings}}
+
+ {{Namespace}}
+
+ {{AccessModifier}} partial class {{ClassName}}
+ {
+ {{_shallowCopyMethod}}
+
+ {{_deepCopyMethod}}
+ }
+ """;
+
+ protected override bool IsEmpty() =>
+ Usings.Length == 0 &&
+ Namespace.Length == 0 &&
+ AccessModifier.Length == 0 &&
+ ClassName.Length == 0;
+
+ internal override void Clear()
+ {
+ base.Clear();
+ _deepCopyMethod.Clear();
+ _shallowCopyMethod.Clear();
+ }
+}
\ No newline at end of file
diff --git a/src/Patternify.Prototype/Generators/PrototypeSyntaxReceiver.cs b/src/Patternify.Prototype/Generators/PrototypeSyntaxReceiver.cs
new file mode 100644
index 0000000..2f9b961
--- /dev/null
+++ b/src/Patternify.Prototype/Generators/PrototypeSyntaxReceiver.cs
@@ -0,0 +1,8 @@
+using Patternify.Abstraction.Generators;
+
+namespace Patternify.Prototype.Generators;
+
+internal sealed class PrototypeSyntaxReceiver : MainSyntaxReceiver
+{
+ protected override string AttributeName => nameof(PrototypeAttribute);
+}
\ No newline at end of file
diff --git a/src/Patternify.Prototype/PrototypeAttribute.cs b/src/Patternify.Prototype/PrototypeAttribute.cs
new file mode 100644
index 0000000..258ded2
--- /dev/null
+++ b/src/Patternify.Prototype/PrototypeAttribute.cs
@@ -0,0 +1,17 @@
+namespace Patternify.Prototype;
+
+///
+/// Represents an attribute that marks a class as a Prototype.
+///
+/// This attribute should be applied to a class that is intended to implement
+/// the Prototype design pattern. The marked class must be a partial class,
+/// allowing the source generator to inject the necessary cloning methods.
+/// The generator will automatically create methods for shallow and deep
+/// copying of objects, making it easier to clone instances of the class.
+///
+/// The generated class will include:
+/// - `ShallowCopy()`: Creates a shallow copy of the object.
+/// - `DeepCopy()`: Creates a deep copy of the object, ensuring that complex types are cloned correctly.
+///
+[AttributeUsage(AttributeTargets.Class)]
+public class PrototypeAttribute : Attribute;
\ No newline at end of file
diff --git a/tests/Patternify.Prototype.Tests/Generators/PrototypeBuilderTests.Build_ForSource_ReturnGeneratedSource.verified.txt b/tests/Patternify.Prototype.Tests/Generators/PrototypeBuilderTests.Build_ForSource_ReturnGeneratedSource.verified.txt
new file mode 100644
index 0000000..8bc49af
--- /dev/null
+++ b/tests/Patternify.Prototype.Tests/Generators/PrototypeBuilderTests.Build_ForSource_ReturnGeneratedSource.verified.txt
@@ -0,0 +1,40 @@
+//
+using Patternify.NullObject;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Text.Json;
+
+namespace Test;
+
+public partial class Person
+{
+ public Person ShallowCopy()
+ {
+ return (Person)this.MemberwiseClone();
+ }
+
+ public Person DeepCopy()
+ {
+ Person clone = (Person)this.ShallowCopy();
+
+ clone.PersonAddress = new Address()
+ {
+ HouseNumber = PersonAddress.HouseNumber,
+ Street = PersonAddress.Street,
+ City = PersonAddress.City
+ };
+
+ clone.PersonContacts = new Contacts()
+ {
+ PhoneNumber = PersonContacts.PhoneNumber,
+ Email = PersonContacts.Email
+ };
+
+ return clone;
+ }
+}
\ No newline at end of file
diff --git a/tests/Patternify.Prototype.Tests/Generators/PrototypeBuilderTests.cs b/tests/Patternify.Prototype.Tests/Generators/PrototypeBuilderTests.cs
new file mode 100644
index 0000000..c19f6be
--- /dev/null
+++ b/tests/Patternify.Prototype.Tests/Generators/PrototypeBuilderTests.cs
@@ -0,0 +1,62 @@
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Patternify.Prototype.Generators;
+using Patternify.Tests.Helpers.Creators;
+
+namespace Patternify.Prototype.Tests.Generators;
+
+public sealed class PrototypeBuilderTests
+{
+ [Fact]
+ internal async Task Build_ForSource_ReturnGeneratedSource()
+ {
+ // Arrange
+ var allClasses = SyntaxNodeCreator.GetSyntaxNodes(Source).ToList();
+
+ var classSyntax = allClasses
+ .First(x => x.AttributeLists
+ .SelectMany(y => y.Attributes)
+ .Any(z => z.Name.ToString() == "Prototype"));
+
+ var builder = new PrototypeBuilder();
+
+ // Act
+ builder.SetUsings(classSyntax);
+ builder.SetNamespace(classSyntax);
+ builder.SetAccessModifier(classSyntax);
+ builder.SetClassName(classSyntax);
+ builder.SetShallowCopyMethod(classSyntax);
+ builder.SetDeepCopyMethod(classSyntax, allClasses);
+ var result = builder.Build();
+ // Assert
+ await Verify(result);
+ }
+
+ private const string Source =
+ """
+ using Patternify.NullObject;
+
+ namespace Test;
+
+ [Prototype]
+ public partial class Person
+ {
+ public string? Name { get; set; }
+ public string? LastName { get; set; }
+ public Address? PersonAddress { get; set; }
+ public Contacts? PersonContacts { get; set; }
+ }
+
+ public class Address
+ {
+ public string? HouseNumber { get; set; }
+ public string? Street { get; set; }
+ public string? City { get; set; }
+ }
+
+ public class Contacts
+ {
+ public string? PhoneNumber { get; set; }
+ public string? Email { get; set; }
+ }
+ """;
+}
\ No newline at end of file