Apply attribute filter

This commit is contained in:
honzapatCZ 2023-03-28 19:26:14 +02:00
parent a6cb43b3e4
commit 69c2733046
4 changed files with 280 additions and 62 deletions

View File

@ -5,8 +5,10 @@ using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
@ -79,7 +81,7 @@ namespace AutoMapProperty
public string Namespace;
public string Name;
public record struct ProperyGenerationInfo(PropertyDeclarationSyntax prop, bool generateConverter = true);
public record struct ProperyGenerationInfo(PropertyDeclarationSyntax prop, bool customFrom, IMethodSymbol? customFromMethod, bool customTo, IMethodSymbol? customToMethod);
public List<ProperyGenerationInfo> Properties;
public struct MappableTypes
@ -89,13 +91,20 @@ namespace AutoMapProperty
public IPropertySymbol FromProperty;
public bool FromIsReadOnly => FromProperty.SetMethod == null;
public bool GenerateConverter = true;
public MappableTypes(string from, string to, IPropertySymbol fromProperty, bool generateConverter = true)
public bool CustomFrom;
public IMethodSymbol? CustomFromMethod;
public bool CustomTo;
public IMethodSymbol? CustomToMethod;
public MappableTypes(string from, string to, IPropertySymbol fromProperty, bool customFrom, IMethodSymbol? customFromMethod, bool customTo, IMethodSymbol? customToMethod, bool generateConverter = true)
{
From = from;
To = to;
FromProperty = fromProperty;
GenerateConverter = generateConverter;
CustomFrom = customFrom;
CustomFromMethod = customFromMethod;
CustomTo = customTo;
CustomToMethod = customToMethod;
}
}
@ -172,7 +181,7 @@ namespace AutoMapProperty
string className = classSymbol.Name;
foreach (var attr in propSymbol.GetAttributes())
foreach (AttributeData attr in propSymbol.GetAttributes())
{
if (!propAttribute.Equals(attr.AttributeClass, SymbolEqualityComparer.Default))
{
@ -196,13 +205,39 @@ namespace AutoMapProperty
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));
var mem = propSyntax;
try
{
var newList = GetModifiedAttributeList(compilation, propSyntax.AttributeLists);
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor("NEJ23", "Error", "New list on: {1} {0}", "NEJ", DiagnosticSeverity.Warning, true), null, string.Join(", ", newList.Select(x => x.ToString())), propSymbol.Name)
);
//Modify attributes to not include our generation attribute
mem = propSyntax.WithAttributeLists(newList);
}
catch (Exception ex)
{
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor("NEJ23", "Error", "Error processing {0}", "NEJ", DiagnosticSeverity.Error, true), null, propSymbol.Name)
);
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor("NEJ23", "Error", "{0}", "NEJ", DiagnosticSeverity.Error, true), null, ex.Message)
);
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor("NEJ23", "Error", "{0}", "NEJ", DiagnosticSeverity.Error, true), null, ex.StackTrace)
);
}
//If we have a special overwrite type, replace it
INamedTypeSymbol? dataTypename = (INamedTypeSymbol?)attr?.ConstructorArguments[1].Value;
if (dataTypename != null)
if (attr.ConstructorArguments.Count() >= 2)
{
INamedTypeSymbol dataTypename = attr?.ConstructorArguments[1].Value as INamedTypeSymbol;
mem = mem.WithType(SyntaxFactory.ParseTypeName(dataTypename.ToDisplayString() + " "));
}
//if the property is readonly, remove the readonly modifier
@ -227,11 +262,24 @@ namespace AutoMapProperty
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);
bool customFrom = false;
IMethodSymbol? customFromMethod = null;
bool customTo = false;
IMethodSymbol? customToMethod = null;
if (attr.ConstructorArguments.Count() >= 3)
{
customTo = true;
customToMethod = ProcessCustomAttribute(context, classSymbol, attr.ConstructorArguments[2]);
}
if (attr.ConstructorArguments.Count() >= 4)
{
customFrom = true;
customFromMethod = ProcessCustomAttribute(context, classSymbol, attr.ConstructorArguments[3]);
}
//Add the property to the carrier type
classesToGenerate[name].Properties.Add(new ClassToGenerate.ProperyGenerationInfo(mem, generateConverters));
classesToGenerate[name].Properties.Add(
new ClassToGenerate.ProperyGenerationInfo(mem, customFrom, customFromMethod, customTo, customToMethod)
);
}
}
@ -243,7 +291,34 @@ namespace AutoMapProperty
return classesToGenerate.Values.ToList();
}
public record struct PropertyGenInfo(string type, IPropertySymbol? symbol = null, bool generateConverter = true);
public static IMethodSymbol? ProcessCustomAttribute(SourceProductionContext cont, INamedTypeSymbol classSymbol, TypedConstant attr)
{
var arg = attr.Value as string;
//get all methods of the declaring parent type
var methods = classSymbol.GetMembers().OfType<IMethodSymbol>();
//get the method with the same name as the attribute argument
var method = methods.FirstOrDefault(x => x.Name == arg);
if (method == null)
{
cont.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor("NEJ10", "No method found", "Couldn't find method {0} on {1}", "NEJ", DiagnosticSeverity.Error, true), null, arg, classSymbol.Name)
);
return null;
}
//check if the method is static
if (!method.IsStatic)
{
cont.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor("NEJ10", "Method not static", "Method {0} on {1} is not static", "NEJ", DiagnosticSeverity.Error, true), null, arg, classSymbol.Name)
);
return null;
}
return method;
}
public record struct PropertyGenInfo(string type, IPropertySymbol? symbol = null, bool customFrom = false, IMethodSymbol? customFromMethod = null, bool customTo = false, IMethodSymbol? customToMethod = null);
public static void ProcessClassToGenerateVariations(SourceProductionContext context, Compilation compilation, Dictionary<string, ClassToGenerate> classesToGenerate, ClassToGenerate cls)
{
@ -260,7 +335,7 @@ namespace AutoMapProperty
}
else
{
possibleCandidates.AddRange(GetClassToGenerate(cls));
possibleCandidates.AddRange(GetInfoFromClassToGenerate(cls));
}
//get properties on the source class
@ -275,26 +350,24 @@ namespace AutoMapProperty
//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));
.ToDictionary(x => x, x => new ClassToGenerate.MappableTypes(sourceNames[x].type, possibleNames[x].type, sourceNames[x].symbol, possibleNames[x].customFrom, possibleNames[x].customFromMethod, possibleNames[x].customTo, possibleNames[x].customToMethod));
//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)
@ -315,70 +388,144 @@ namespace AutoMapProperty
{
var c = currentClassesToBeGenerated[className];
return GetClassToGenerate(c);
return GetInfoFromClassToGenerate(c);
}
return new List<(string, PropertyGenInfo)>();
}
public static List<(string, PropertyGenInfo)> GetClassToGenerate(ClassToGenerate cls)
public static List<(string, PropertyGenInfo)> GetInfoFromClassToGenerate(ClassToGenerate cls)
{
return cls.Properties.Select(x => (x.prop.Identifier.ToFullString().Trim(),
new PropertyGenInfo(x.prop.Type.ToFullString().Trim(), (IPropertySymbol?)null, x.generateConverter)))
new PropertyGenInfo(x.prop.Type.ToFullString().Trim(), (IPropertySymbol?)null, x.customFrom, x.customFromMethod, x.customTo, x.customToMethod)))
.ToList();
}
#region Attribute_Editing
public static SyntaxList<AttributeListSyntax> GetModifiedAttributeList(Compilation com, SyntaxList<AttributeListSyntax> inList)
public static SyntaxList<AttributeListSyntax> GetModifiedAttributeList(Compilation com, SyntaxList<AttributeListSyntax> attListList)
{
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");
if (attListList == null)
return attListList;
if (attListList.Count <= 0)
return attListList;
var newNode = node.WithAttributes(GetModifiedAttributes(com, node.Attributes));
if (newNode.Attributes.Count <= 0)
return inList.Remove(node);
return inList.Replace(node, newNode);
var toRemove = new List<AttributeListSyntax>();
foreach (var node in attListList.ToList().Where(x => x != null && x.Attributes != null && x.Attributes.Count > 0))
{
var atts = GetModifiedAttributes(com, node.Attributes);
node.WithAttributes(atts);
if (atts.Count <= 0)
toRemove.Add(node);
}
return new SyntaxList<AttributeListSyntax>(attListList.Where(x => !toRemove.Contains(x)));
}
public static SeparatedSyntaxList<AttributeSyntax> GetModifiedAttributes(Compilation com, SeparatedSyntaxList<AttributeSyntax> inList)
public static SeparatedSyntaxList<AttributeSyntax> GetModifiedAttributes(Compilation com, SeparatedSyntaxList<AttributeSyntax> attList)
{
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);
var toRemove = new List<AttributeSyntax>();
foreach (var x in attList.ToList().Where(x => x != null).Where(x => IsOurAttribute(com, x)))
{
toRemove.Add(x);
}
var newList = SyntaxFactory.SeparatedList<AttributeSyntax>(attList.Where(x => !toRemove.Contains(x)));
return newList;
}
public static bool IsOurAttribute(Compilation com, AttributeSyntax syntax)
{
if (com.GetSemanticModel(syntax.SyntaxTree).GetSymbolInfo(syntax).Symbol is not IMethodSymbol attrSymbol)
return false;
return syntax.ToString().Contains(AutoMapPropertyAttributeName);
return attrSymbol.ContainingType.Equals(com.GetTypeByMetadataName(AutoMapPropertyAttributeName), SymbolEqualityComparer.Default);
var t = com.GetTypeByMetadataName(AutoMapPropertyAttributeName);
if (t == null)
throw new Exception("AutoMapPropertyAttributeName is null");
return attrSymbol.ContainingSymbol.Equals(t, SymbolEqualityComparer.Default);
}
#endregion
public static string GeneratePropertyFrom(string Key, ClassToGenerate.MappableTypes Value)
public static string GetFullMethodName(IMethodSymbol method)
{
var sb = new StringBuilder();
sb.Append(Key).Append(@" = (").Append(Value.To).Append(@")source.").Append(Key);
sb.Append(method.ContainingType.ToDisplayString()).Append(".").Append(method.Name);
return sb.ToString();
}
public static string GeneratePropertyFrom(string Key, string Type, bool nullable = true)
{
var sb = new StringBuilder();
sb.Append(Key).Append(@" = ");
//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);
var match = regex.Match(Type);
if (match.Success)
{
var type = match.Groups["type2"].Value;
sb.Append(@".Select(x => (").Append(type).Append(@")x)");
sb.Append("(").Append(Type).Append(@")(source.").Append(Key).Append(" != null ? ");
sb.Append("source.").Append(Key);
sb.Append(nullable ? "?" : "").Append(@".Select(x => (").Append(type).Append(@")x)").Append(nullable ? "?" : "");
//Add the appropriate ToHashSet or ToSet or ToCollection or ToList
if (match.Groups["type"].Value.Contains("Set"))
sb.Append(@".ToHashSet()");
else
sb.Append(@".ToList()");
sb.Append(" : new ");
if (match.Groups["type"].Value.Contains("Set"))
sb.Append("HashSet<").Append(type).Append(">()");
else
sb.Append("List<").Append(type).Append(">()");
sb.Append(")");
}
else
{
sb.Append("(").Append(Type).Append(@")source.").Append(Key);
}
return sb.ToString();
}
public static string? GenerateCorrectFromProperty(string Key, ClassToGenerate.MappableTypes Value, bool nullable = true)
{
if (!Value.CustomFrom)
{
return GeneratePropertyFrom(Key, Value.To, nullable);
}
else
{
if (Value.CustomFromMethod == null)
return null;
StringBuilder sb = new StringBuilder();
sb.Append(Key).Append(" = (").Append(Value.To).Append(@")").Append(GetFullMethodName(Value.CustomFromMethod)).Append(@"(providers, this, source.").Append(Key).Append(")");
return sb.ToString();
}
}
public static string GeneratePropertyTo(string Key, string Type)
{
var sb = new StringBuilder();
sb.Append(Key).Append(@" = (").Append(Type).Append(@")this.").Append(Key);
return sb.ToString();
}
public static string? GenerateCorrectToProperty(string Key, ClassToGenerate.MappableTypes Value)
{
if (!Value.CustomTo)
{
return GeneratePropertyTo(Key, Value.From);
}
else
{
if (Value.CustomToMethod == null)
return null;
StringBuilder sb = new StringBuilder();
sb.Append(Key).Append(" = (").Append(Value.From).Append(@")").Append(GetFullMethodName(Value.CustomToMethod)).Append(@"(providers, source, this.").Append(Key).Append(")");
return sb.ToString();
}
}
public static string GenerateExtensionClass(ClassToGenerate classToGenerate)
{
@ -393,6 +540,7 @@ namespace AutoMapProperty
#nullable enable
namespace ").Append(classToGenerate.Namespace).Append(@"
{
[System.CodeDom.Compiler.GeneratedCode(""AutoMapProperty"", ""1.0.0"")]
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)))
@ -411,54 +559,111 @@ namespace ").Append(classToGenerate.Namespace).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;
return dat.ApplyFrom(null, source);
}");
sb.Append(@"
public ").Append(classToGenerate.Name).Append(@" ApplyFrom(").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
public ").Append(classToGenerate.Name).Append(@" ApplyFrom(System.IServiceProvider providers, ").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
{
if(source == null)
return null;
{");
foreach (var prop in classToGenerate.MappableProperties)
{
if (!prop.Value.GenerateConverter)
var property = GenerateCorrectFromProperty(prop.Key, prop.Value);
if (property == null)
continue;
sb.Append(@"
").Append(GeneratePropertyFrom(prop.Key, prop.Value)).Append(@";");
").Append(property).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(@"()
public System.Linq.Expressions.Expression<Func<").Append(classToGenerate.SourceContainer.Identifier).Append(", ").Append(classToGenerate.Name).Append(@">> GetProjectorFrom(System.IServiceProvider providers) => (source)=>new ").Append(classToGenerate.Name).Append(@"()
{");
foreach (var prop in classToGenerate.MappableProperties)
{
if (!prop.Value.GenerateConverter)
var property = GenerateCorrectFromProperty(prop.Key, prop.Value, false);
if (property == null)
continue;
sb.Append(@"
").Append(GeneratePropertyFrom(prop.Key, prop.Value)).Append(@",");
").Append(property).Append(@",");
}
sb.Append(@"
};");
sb.Append(@"
public static System.Linq.Expressions.Expression<Func<").Append(classToGenerate.Name).Append(@", bool>> EqualExpression(System.IServiceProvider providers,").Append(classToGenerate.SourceContainer.Identifier).Append(@" item1) => (item2)=>
(");
foreach (var prop in classToGenerate.MappableProperties)
{
if (prop.Value.From != prop.Value.To)
continue;
sb.Append(@"
item1.").Append(prop.Key).Append(" == item2.").Append(prop.Key).Append(" && ");
}
sb.Append(@"true
);");
sb.Append(@"
public static System.Linq.Expressions.Expression<Func<").Append(classToGenerate.SourceContainer.Identifier).Append(@", bool>> EqualExpression(System.IServiceProvider providers,").Append(classToGenerate.Name).Append(@" item1) => (item2)=>
(");
foreach (var prop in classToGenerate.MappableProperties)
{
if (prop.Value.From != prop.Value.To)
continue;
sb.Append(@"
item1.").Append(prop.Key).Append(" == item2.").Append(prop.Key).Append(" && ");
}
sb.Append(@"true
);");
sb.Append(@"
public override bool Equals(object obj){
if(obj == null)
return false;
if (ReferenceEquals(obj, this))
return true;
if (obj.GetType() != this.GetType())
return false;
").Append(classToGenerate.Name).Append(@" other = obj as ").Append(classToGenerate.Name).Append(@";
return (");
foreach (var prop in classToGenerate.MappableProperties)
{
sb.Append(@"
this.").Append(prop.Key).Append(" == other.").Append(prop.Key).Append(" && ");
}
sb.Append(@"true
);
}");
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)
public ").Append(classToGenerate.SourceContainer.Identifier).Append(@" ApplyTo(System.IServiceProvider providers, ").Append(classToGenerate.SourceContainer.Identifier).Append(@" source)
{
if(source == null)
return null;
{");
foreach (var prop in classToGenerate.MappableProperties)
{
if (!prop.Value.GenerateConverter)
continue;
if (prop.Value.FromIsReadOnly)
continue;
var property = GenerateCorrectToProperty(prop.Key, prop.Value);
if (property == null)
continue;
sb.Append(@"
source.").Append(prop.Key).Append(@" = (").Append(prop.Value.From).Append(@")this.").Append(prop.Key).Append(@";");
source.").Append(property).Append(@";");
}
sb.Append(@"
}
@ -469,8 +674,7 @@ namespace ").Append(classToGenerate.Namespace).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;
return source.ApplyTo(null, dat);
}");
}
}

View File

@ -5,8 +5,17 @@ namespace AutoMapPropertyHelper
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class AutoMapPropertyAttribute : System.Attribute
{
public AutoMapPropertyAttribute(string name, Type? typeName = null, bool generateConverter = true)
public AutoMapPropertyAttribute(string name)
{
}
}
public AutoMapPropertyAttribute(string name, Type? typeName)
{
}
public AutoMapPropertyAttribute(string name, Type? typeName, string? customTo)
{
}
public AutoMapPropertyAttribute(string name, Type? typeName, string? customTo, string? customFrom)
{
}
}
}

View File

@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
</ItemGroup>
</Project>

View File

@ -2,15 +2,16 @@
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
namespace AutoMapPropertyHelper
{
public interface IAutomappedAttribute<TSource, TSelf>
{
public TSource ApplyTo(TSource value);
public TSelf ApplyFrom(TSource source);
public TSource ApplyTo(IServiceProvider providers, TSource value);
public TSelf ApplyFrom(IServiceProvider providers, TSource source);
public Expression<Func<TSource, TSelf>> GetProjectorFrom();
public Expression<Func<TSource, TSelf>> GetProjectorFrom(IServiceProvider providers);
}
}