Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype #15

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
21 changes: 21 additions & 0 deletions Patternify.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Patternify.Prototype\Patternify.Prototype.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="true" />
<ProjectReference Include="..\..\src\Patternify.Abstraction\Patternify.Abstraction.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="true" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions examples/Patternify.Examples.Prototype/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
3 changes: 1 addition & 2 deletions src/Patternify.Abstraction/Generators/MainGenerator.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
7 changes: 5 additions & 2 deletions src/Patternify.Abstraction/Generators/MainSyntaxReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 2 additions & 0 deletions src/Patternify.Abstraction/Patternify.Abstraction.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
<InternalsVisibleTo Include="Patternify.Singleton.Tests" />
<InternalsVisibleTo Include="Patternify.NullObject" />
<InternalsVisibleTo Include="Patternify.NullObject.Tests" />
<InternalsVisibleTo Include="Patternify.Prototype" />
<InternalsVisibleTo Include="Patternify.Prototype.Tests" />
</ItemGroup>

<ItemGroup>
Expand Down
53 changes: 53 additions & 0 deletions src/Patternify.Prototype/Generators/Helpers/DeepCopyHelper.cs
Original file line number Diff line number Diff line change
@@ -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<ClassDeclarationSyntax> allClassGroups)
{
var typeNames = allClassGroups.Select(y => y.Identifier.Text);

var properties = group.Members
.OfType<PropertyDeclarationSyntax>()
.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<ClassDeclarationSyntax> 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<PropertyDeclarationSyntax>();

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}";
}
74 changes: 74 additions & 0 deletions src/Patternify.Prototype/Generators/PrototypeBuilder.cs
Original file line number Diff line number Diff line change
@@ -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<ClassDeclarationSyntax> 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() =>
$$"""
// <auto-generated/>
{{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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Patternify.Abstraction.Generators;

namespace Patternify.Prototype.Generators;

internal sealed class PrototypeSyntaxReceiver : MainSyntaxReceiver
{
protected override string AttributeName => nameof(PrototypeAttribute);
}
55 changes: 55 additions & 0 deletions src/Patternify.Prototype/Patternify.Prototype.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsPackable>true</IsPackable>
<PackageId>Patternify.Prototype</PackageId>
<Authors>Łukasz Strus</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>icon.png</PackageIcon>
<PackageDescription>
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.
</PackageDescription>
<RepositoryUrl>https://github.com/lukasz-strus/Patternify</RepositoryUrl>
<PackageTags>
Roslyn;
SourceGenerator;
Prototype;
DesignPatterns;
CSharp;
ThreadSafe;
CodeGeneration;
AOT;
DependencyInjection;
</PackageTags>
<Title>Patternify - Prototype Design Pattern Source Generator (C# / Roslyn)</Title>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Patternify.Abstraction\Patternify.Abstraction.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.11.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.11.0" />
</ItemGroup>

<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="../../icon.png" Pack="true" PackagePath="/" />
<None Include="README.md" Pack="true" PackagePath="/" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Patternify.Prototype.Tests" />
</ItemGroup>

</Project>
17 changes: 17 additions & 0 deletions src/Patternify.Prototype/PrototypeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Patternify.Prototype;

/// <summary>
/// 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.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class PrototypeAttribute : Attribute;
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// <auto-generated/>
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;
}
}
Loading