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