This commit is contained in:
honzapatCZ 2023-03-14 20:46:34 +01:00
commit dae65b42e5
7 changed files with 622 additions and 0 deletions

40
AutoMapProperty/.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc
nupkg/
# Visual Studio Code
.vscode
# Rider
.idea
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
msbuild.log
msbuild.err
msbuild.wrn
# Visual Studio 2015
.vs/
.local-chromium/
node_modules

View File

@ -0,0 +1,487 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
namespace AutoMapProperty
{
[Generator]
public class AutoMapPropertyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<PropertyDeclarationSyntax> classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(IsSyntaxTargetForGeneration, GetSemanticTargetForGeneration)
.Where(m => m is not null)!;
IncrementalValueProvider<(Compilation, ImmutableArray<PropertyDeclarationSyntax>)> compilationAndClasses
= context.CompilationProvider.Combine(classDeclarations.Collect());
context.RegisterSourceOutput(compilationAndClasses,
static (spc, source) => Execute(source.Item1, source.Item2, spc));
}
static bool IsSyntaxTargetForGeneration(SyntaxNode node, CancellationToken ct)
=> node is PropertyDeclarationSyntax m && m.AttributeLists.Count > 0;
private const string AutoMapPropertyAttributeName = "AutoMapPropertyHelper.AutoMapPropertyAttribute";
static PropertyDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorSyntaxContext context, CancellationToken ct)
{
// we know the node is a MethodDeclarationSyntax thanks to IsSyntaxTargetForGeneration
var propertyDeclarationSyntax = (PropertyDeclarationSyntax)context.Node;
// loop through all the attributes on the method
foreach (AttributeListSyntax attributeListSyntax in propertyDeclarationSyntax.AttributeLists)
{
foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes)
{
if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol)
{
// weird, we couldn't get the symbol, ignore it
continue;
}
INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType;
string fullName = attributeContainingTypeSymbol.ToDisplayString();
// Is the attribute theAutoMapPropertyAttributeName attribute?
if (fullName == AutoMapPropertyAttributeName)
{
// return the parent class of the method
return propertyDeclarationSyntax;
}
}
}
// we didn't find the attribute we were looking for
return null;
}
public class ClassToGenerate
{
/// <summary>
/// Class or Interface
/// </summary>
public TypeDeclarationSyntax SourceContainer;
public List<string> Usings;
public string Namespace;
public string Name;
public record struct ProperyGenerationInfo(PropertyDeclarationSyntax prop, bool generateConverter = true);
public List<ProperyGenerationInfo> Properties;
public struct MappableTypes
{
public string From;
public string To;
public IPropertySymbol FromProperty;
public bool FromIsReadOnly => FromProperty.SetMethod == null;
public bool GenerateConverter = true;
public MappableTypes(string from, string to, IPropertySymbol fromProperty, bool generateConverter = true)
{
From = from;
To = to;
FromProperty = fromProperty;
GenerateConverter = generateConverter;
}
}
public Dictionary<string, MappableTypes> MappableProperties = new Dictionary<string, MappableTypes>();
public ClassToGenerate(TypeDeclarationSyntax sourceClass, string name, string namespaceName, List<string> usings, List<ProperyGenerationInfo> properties)
{
SourceContainer = sourceClass;
Namespace = namespaceName;
Name = name;
Properties = properties;
Usings = usings;
}
}
private static void Execute(Compilation compilation, ImmutableArray<PropertyDeclarationSyntax> properties, SourceProductionContext context)
{
if (properties.IsDefaultOrEmpty)
{
// nothing to do yet
return;
}
IEnumerable<PropertyDeclarationSyntax> distinctClasses = properties.Distinct();
List<ClassToGenerate> enumsToGenerate = GetTypesToGenerate(compilation, distinctClasses, context);
// If there were errors in the EnumDeclarationSyntax, we won't create an
// EnumToGenerate for it, so make sure we have something to generate
foreach (ClassToGenerate classToGenerate in enumsToGenerate)
{
//throw new Exception(classToGenerate.Name);
string result = GenerateExtensionClass(classToGenerate);
context.AddSource(classToGenerate.Name + ".g.cs", SourceText.From(result, Encoding.UTF8));
}
}
static List<ClassToGenerate> GetTypesToGenerate(Compilation compilation, IEnumerable<PropertyDeclarationSyntax> props, SourceProductionContext context)
{
// Create a list to hold our output
var classesToGenerate = new Dictionary<string, ClassToGenerate>();
// Get the semantic representation of our marker attribute
INamedTypeSymbol? propAttribute = compilation.GetTypeByMetadataName(AutoMapPropertyAttributeName);
if (propAttribute == null)
{
// If this is null, the compilation couldn't find the marker attribute type
// which suggests there's something very wrong! Bail out..
return classesToGenerate.Values.ToList();
}
foreach (PropertyDeclarationSyntax propSyntax in props)
{
// stop if we're asked to
context.CancellationToken.ThrowIfCancellationRequested();
// Get the semantic representation of the enum syntax
SemanticModel semanticModel = compilation.GetSemanticModel(propSyntax.Parent!.SyntaxTree);
//Get the parent class or interface
TypeDeclarationSyntax PropParentDecl = (TypeDeclarationSyntax)propSyntax.Parent!;
if (PropParentDecl is not ClassDeclarationSyntax && PropParentDecl is not InterfaceDeclarationSyntax)
continue;
if (semanticModel.GetDeclaredSymbol(PropParentDecl) is not INamedTypeSymbol classSymbol)
continue;
if (semanticModel.GetDeclaredSymbol(propSyntax) is not IPropertySymbol propSymbol)
continue;
var root = PropParentDecl.SyntaxTree.GetCompilationUnitRoot();
string className = classSymbol.Name;
foreach (var attr in propSymbol.GetAttributes())
{
if (!propAttribute.Equals(attr.AttributeClass, SymbolEqualityComparer.Default))
{
// This isn't the [EnumExtensions] attribute
continue;
}
//get the autogen name
string dataName = (string)attr.ConstructorArguments[0].Value!;
//the final name
string name = className + dataName;
if (!classesToGenerate.ContainsKey(name))
{
//Copy all usings
List<string> usings = root.Usings.Select(x => x.ToString()).ToList();
string ns = classSymbol.ContainingNamespace.ToString();
usings.Add("using " + ns + ";");
//Create the carrier type
classesToGenerate[name] = new ClassToGenerate(PropParentDecl, name, ns, usings, new List<ClassToGenerate.ProperyGenerationInfo>());
}
//Modify attributes to not include our generation attribute
var mem = propSyntax.WithAttributeLists(GetModifiedAttributeList(compilation, propSyntax.AttributeLists));
//If we have a special overwrite type, replace it
INamedTypeSymbol? dataTypename = (INamedTypeSymbol?)attr?.ConstructorArguments[1].Value;
if (dataTypename != null)
mem = mem.WithType(SyntaxFactory.ParseTypeName(dataTypename.ToDisplayString() + " "));
//if the property is readonly, remove the readonly modifier
if (mem.Modifiers.Any(x => x.IsKind(SyntaxKind.ReadOnlyKeyword)))
{
mem = mem.WithModifiers(SyntaxFactory.TokenList(mem.Modifiers.Where(x => !x.IsKind(SyntaxKind.ReadOnlyKeyword))));
}
//if property has non automatic(backing field) getter or setter, remove them
//add the default backing field getter and setter
mem = mem.WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List(new[]
{
SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
})));
//if property has initializer or expression body, remove them
mem = mem.WithInitializer(null);
mem = mem.WithExpressionBody(null);
//now the has trailing semicolon, which is wrong
mem = mem.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None));
//if we dont want to generate converter
//bool generateConverters = true;
bool generateConverters = !(attr?.ConstructorArguments[2] != null && attr?.ConstructorArguments[2].Value is bool b && b == false);
//Add the property to the carrier type
classesToGenerate[name].Properties.Add(new ClassToGenerate.ProperyGenerationInfo(mem, generateConverters));
}
}
foreach (ClassToGenerate cls in classesToGenerate.Values)
{
ProcessClassToGenerateVariations(context, compilation, classesToGenerate, cls);
}
return classesToGenerate.Values.ToList();
}
public record struct PropertyGenInfo(string type, IPropertySymbol? symbol = null, bool generateConverter = true);
public static void ProcessClassToGenerateVariations(SourceProductionContext context, Compilation compilation, Dictionary<string, ClassToGenerate> classesToGenerate, ClassToGenerate cls)
{
var possibleCandidates = new List<(string, PropertyGenInfo)>();
string newName = cls.Namespace + "." + cls.Name;
//check if the newName already exists in compilation
var sym = compilation.GetTypesByMetadataName(newName).FirstOrDefault();
if (compilation.GetTypesByMetadataName(newName).FirstOrDefault() is INamedTypeSymbol symb)
{
possibleCandidates.AddRange(GetSymbolsForTypedSymbol(compilation, symb, classesToGenerate)
.Select(x => (x.Item1, x.Item2)));
}
else
{
possibleCandidates.AddRange(GetClassToGenerate(cls));
}
//get properties on the source class
var comp = compilation.GetSemanticModel(cls.SourceContainer.SyntaxTree);
var symbol = comp.GetDeclaredSymbol(cls.SourceContainer)!;
var sourceCandidates = GetSymbolsForTypedSymbol(compilation, symbol, classesToGenerate);
//only add possible candidates that are also in sourceCanddidates
//check if there are multiples if so report a diagnostic
var source = sourceCandidates
.Where(x => !string.IsNullOrWhiteSpace(x.Item1))
//Remove entries with duplicate names
.GroupBy(x => x.Item1)
.Select(x => x.First());
var possible = possibleCandidates
.Where(x => !string.IsNullOrWhiteSpace(x.Item1))
//Remove entries with duplicate names
.GroupBy(x => x.Item1)
.Select(x => x.First());
var sourceNames = source.ToDictionary(x => x.Item1, x => x.Item2);
var possibleNames = possible.ToDictionary(x => x.Item1, x => x.Item2);
cls.MappableProperties = possibleNames.Keys.Where(x => sourceNames.Keys.Contains(x))
.ToDictionary(x => x, x => new ClassToGenerate.MappableTypes(sourceNames[x].type, possibleNames[x].type, sourceNames[x].symbol, possibleNames[x].generateConverter));
//c.MappableProperties = c.MappableProperties.ToDictionary(x => x.Item1, x => x.Item2).Select(x => (x.Key, x.Value)).ToList();
context.ReportDiagnostic(
Diagnostic.Create(
new DiagnosticDescriptor(
"NEJ01", "Report candidates vs real", "{0} had: {1} possible candidates and:{2} source values, but in the end there were: {3} values",
"NEJ", DiagnosticSeverity.Warning, true), null, cls.Name, possibleCandidates.Count, sourceCandidates.Count, cls.MappableProperties.Count));
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor("Nej10", "TestShow", "{0}", "NEJ", DiagnosticSeverity.Warning, true), null, string.Join(",", cls.MappableProperties.Select(x => x.Key + ":" + x.Value.From + "->" + x.Value.To))
));
}
public static List<(string, PropertyGenInfo)> GetSymbolsForTypedSymbol(Compilation comp, INamedTypeSymbol cls, Dictionary<string, ClassToGenerate> ctg)
{
var result = new List<(string, PropertyGenInfo)>();
result.AddRange(cls.GetMembers().OfType<IPropertySymbol>().Select(x => (x.Name.Trim(), new PropertyGenInfo(x.Type.ToDisplayString().Trim(), x))));
//add from to be generated symbols
result.AddRange(GetSymbolsFromCurrentGeneration(comp, cls.Name.Trim(), ctg));
//add parent symbols
if (cls.BaseType != null)
result.AddRange(GetSymbolsForTypedSymbol(comp, cls.BaseType, ctg));
return result;
}
public static List<(string, PropertyGenInfo)> GetSymbolsFromCurrentGeneration(Compilation comp, string className, Dictionary<string, ClassToGenerate> currentClassesToBeGenerated)
{
if (currentClassesToBeGenerated.ContainsKey(className))
{
var c = currentClassesToBeGenerated[className];
return GetClassToGenerate(c);
}
return new List<(string, PropertyGenInfo)>();
}
public static List<(string, PropertyGenInfo)> GetClassToGenerate(ClassToGenerate cls)
{
return cls.Properties.Select(x => (x.prop.Identifier.ToFullString().Trim(),
new PropertyGenInfo(x.prop.Type.ToFullString().Trim(), (IPropertySymbol?)null, x.generateConverter)))
.ToList();
}
#region Attribute_Editing
public static SyntaxList<AttributeListSyntax> GetModifiedAttributeList(Compilation com, SyntaxList<AttributeListSyntax> inList)
{
AttributeListSyntax node = inList.Where(x => x.Attributes.Where(y => IsOurAttribute(com, y)).Where(y => y != null).Count() > 0).FirstOrDefault();
if (node == null)
throw new Exception("We didnt find a single node in attribute list");
var newNode = node.WithAttributes(GetModifiedAttributes(com, node.Attributes));
if (newNode.Attributes.Count <= 0)
return inList.Remove(node);
return inList.Replace(node, newNode);
}
public static SeparatedSyntaxList<AttributeSyntax> GetModifiedAttributes(Compilation com, SeparatedSyntaxList<AttributeSyntax> inList)
{
AttributeSyntax node = inList.Where(x => IsOurAttribute(com, x)).FirstOrDefault();
if (node == null)
throw new Exception("We didnt find a single node in attributes");
return inList.Remove(node);
}
public static bool IsOurAttribute(Compilation com, AttributeSyntax syntax)
{
if (com.GetSemanticModel(syntax.SyntaxTree).GetSymbolInfo(syntax).Symbol is not IMethodSymbol attrSymbol)
return false;
return attrSymbol.ContainingType.Equals(com.GetTypeByMetadataName(AutoMapPropertyAttributeName), SymbolEqualityComparer.Default);
}
#endregion
public static string GeneratePropertyFrom(string Key, ClassToGenerate.MappableTypes Value)
{
var sb = new StringBuilder();
sb.Append(Key).Append(@" = (").Append(Value.To).Append(@")source.").Append(Key);
//using regex check if the type is IEnumerable or IList or List or ICollection or HashSet or ISet(it can have leading namespace), if so remove them to get the original type
//example System.Collections.Generic.IEnumerable<TestType>
//example System.Collections.Generic.IList<TestType>
//also catch the IEnumerable or IList or List or ICollection or HashSet or ISet and add the appropriate ToHashSet or ToSet or ToCollection or ToList
var regex = new Regex(@"(?<type>(System\.Collections\.Generic\.)?(IEnumerable|IList|List|ICollection|HashSet|ISet)<(?<type2>.*)>)");
var match = regex.Match(Value.To);
if (match.Success)
{
var type = match.Groups["type2"].Value;
sb.Append(@".Select(x => (").Append(type).Append(@")x)");
//Add the appropriate ToHashSet or ToSet or ToCollection or ToList
if (match.Groups["type"].Value.Contains("Set"))
sb.Append(@".ToHashSet()");
else
sb.Append(@".ToList()");
}
return sb.ToString();
}
public static string GenerateExtensionClass(ClassToGenerate classToGenerate)
{
var sb = new StringBuilder();
foreach (var usi in classToGenerate.Usings)
{
sb.AppendLine(usi);
}
sb.Append(@"
#nullable enable
namespace ").Append(classToGenerate.Namespace).Append(@"
{
public partial ").Append(classToGenerate.SourceContainer is ClassDeclarationSyntax ? "class " : "interface ").Append(classToGenerate.Name);
if (classToGenerate.SourceContainer is ClassDeclarationSyntax && !classToGenerate.SourceContainer.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword)))
sb.Append(": IAutomappedAttribute<").Append(classToGenerate.SourceContainer.Identifier).Append(",").Append(classToGenerate.Name).Append(">");
sb.Append(@"
{");
foreach (var prop in classToGenerate.Properties)
{
sb.Append(@"
").Append(prop.prop.ToString());
}
if (classToGenerate.SourceContainer is ClassDeclarationSyntax cds)
{
sb.Append(@"
public static implicit operator ").Append(classToGenerate.Name).Append(@"(").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
{
var dat = new ").Append(classToGenerate.Name).Append(@"();
dat.ApplyFrom(source);
return dat;
}");
sb.Append(@"
public ").Append(classToGenerate.Name).Append(@" ApplyFrom(").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
{
{");
foreach (var prop in classToGenerate.MappableProperties)
{
if (!prop.Value.GenerateConverter)
continue;
sb.Append(@"
").Append(GeneratePropertyFrom(prop.Key, prop.Value)).Append(@";");
}
sb.Append(@"
}
return this;
}");
sb.Append(@"
public System.Linq.Expressions.Expression<Func<").Append(classToGenerate.SourceContainer.Identifier).Append(", ").Append(classToGenerate.Name).Append(@">> GetProjectorFrom() => (source)=>new ").Append(classToGenerate.Name).Append(@"()
{");
foreach (var prop in classToGenerate.MappableProperties)
{
if (!prop.Value.GenerateConverter)
continue;
sb.Append(@"
").Append(GeneratePropertyFrom(prop.Key, prop.Value)).Append(@",");
}
sb.Append(@"
};");
var abstr = cds.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword));
if (!abstr)
{
sb.Append(@"
public ").Append(classToGenerate.SourceContainer.Identifier).Append(@" ApplyTo(").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
{
{");
foreach (var prop in classToGenerate.MappableProperties)
{
if (!prop.Value.GenerateConverter)
continue;
if (prop.Value.FromIsReadOnly)
continue;
sb.Append(@"
source.").Append(prop.Key).Append(@" = (").Append(prop.Value.From).Append(@")this.").Append(prop.Key).Append(@";");
}
sb.Append(@"
}
return source;
}");
sb.Append(@"
public static explicit operator ").Append(classToGenerate.SourceContainer.Identifier).Append(@"(").Append(classToGenerate.Name).Append(@" source)
{
var dat = new ").Append(classToGenerate.SourceContainer.Identifier).Append(@"();
source.ApplyTo(dat);
return dat;
}");
}
}
sb.Append(@"
}
}
#nullable disable
");
return sb.ToString();
}
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

40
AutoMapPropertyHelper/.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc
nupkg/
# Visual Studio Code
.vscode
# Rider
.idea
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
msbuild.log
msbuild.err
msbuild.wrn
# Visual Studio 2015
.vs/
.local-chromium/
node_modules

View File

@ -0,0 +1,12 @@
using System;
namespace AutoMapPropertyHelper
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class AutoMapPropertyAttribute : System.Attribute
{
public AutoMapPropertyAttribute(string name, Type? typeName = null, bool generateConverter = true)
{
}
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
namespace AutoMapPropertyHelper
{
public interface IAutomappedAttribute<TSource, TSelf>
{
public TSource ApplyTo(TSource value);
public TSelf ApplyFrom(TSource source);
public Expression<Func<TSource, TSelf>> GetProjectorFrom();
}
}