933 lines
41 KiB
C#
933 lines
41 KiB
C#
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.Diagnostics;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using System.Reflection.Metadata.Ecma335;
|
|
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 string FullName => Namespace + "." + Name;
|
|
|
|
public record struct ProperyGenerationInfo(PropertyDeclarationSyntax prop, ITypeSymbol propTypeSymbol, bool customFrom, IMethodSymbol? customFromMethod, bool customTo, IMethodSymbol? customToMethod);
|
|
public List<ProperyGenerationInfo> Properties;
|
|
|
|
public struct MappableTypes
|
|
{
|
|
public ITypeSymbol FromType;
|
|
public ITypeSymbol ToType;
|
|
|
|
public ClassToGenerate OurTargetType;
|
|
public IPropertySymbol FromProperty;
|
|
public bool FromIsReadOnly => FromProperty.SetMethod == null;
|
|
|
|
public bool CustomFrom;
|
|
public IMethodSymbol? CustomFromMethod;
|
|
public bool CustomTo;
|
|
public IMethodSymbol? CustomToMethod;
|
|
public MappableTypes(ITypeSymbol from, ITypeSymbol to, IPropertySymbol fromProperty, bool customFrom, IMethodSymbol? customFromMethod, bool customTo, IMethodSymbol? customToMethod, ClassToGenerate? isOurType)
|
|
{
|
|
FromType = from;
|
|
ToType = to;
|
|
FromProperty = fromProperty;
|
|
CustomFrom = customFrom;
|
|
CustomFromMethod = customFromMethod;
|
|
CustomTo = customTo;
|
|
CustomToMethod = customToMethod;
|
|
OurTargetType = isOurType;
|
|
}
|
|
}
|
|
|
|
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 (AttributeData 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>());
|
|
}
|
|
|
|
PropertyDeclarationSyntax? 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)
|
|
);
|
|
}
|
|
|
|
var typeSymbol = propSymbol.Type;
|
|
|
|
//If we have a special overwrite type, replace it
|
|
if (attr.ConstructorArguments.Count() >= 2)
|
|
{
|
|
INamedTypeSymbol dataTypename = attr?.ConstructorArguments[1].Value as INamedTypeSymbol;
|
|
mem = mem.WithType(SyntaxFactory.ParseTypeName(dataTypename.ToDisplayString() + " "));
|
|
typeSymbol = dataTypename;
|
|
}
|
|
|
|
|
|
//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);
|
|
|
|
//if property is abstract remove the abstract modifier
|
|
if (mem.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword)))
|
|
{
|
|
mem = mem.WithModifiers(SyntaxFactory.TokenList(mem.Modifiers.Where(x => !x.IsKind(SyntaxKind.AbstractKeyword))));
|
|
}
|
|
|
|
//now the has trailing semicolon, which is wrong
|
|
mem = mem.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None));
|
|
|
|
//if we dont want to generate converter
|
|
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, typeSymbol, customFrom, customFromMethod, customTo, customToMethod)
|
|
);
|
|
}
|
|
}
|
|
|
|
foreach (ClassToGenerate cls in classesToGenerate.Values)
|
|
{
|
|
ProcessClassToGenerateVariations(context, compilation, classesToGenerate, cls);
|
|
}
|
|
|
|
return classesToGenerate.Values.ToList();
|
|
}
|
|
|
|
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(ITypeSymbol 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)
|
|
{
|
|
var possibleCandidates = new List<(string, PropertyGenInfo)>();
|
|
//check if the newName already exists in compilation
|
|
|
|
var sym = compilation.GetTypesByMetadataName(cls.FullName).FirstOrDefault();
|
|
|
|
if (compilation.GetTypesByMetadataName(cls.FullName).FirstOrDefault() is INamedTypeSymbol symb)
|
|
{
|
|
possibleCandidates.AddRange(GetSymbolsForTypedSymbol(compilation, symb, classesToGenerate)
|
|
.Select(x => (x.Item1, x.Item2)));
|
|
}
|
|
else
|
|
{
|
|
possibleCandidates.AddRange(GetInfoFromClassToGenerate(compilation, 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 =>
|
|
{
|
|
var concreteType = GetConcreteType(possibleNames[x].type);
|
|
var fullName = GetFullString(concreteType);
|
|
var ourType = classesToGenerate.FirstOrDefault(y => y.Value.FullName == fullName).Value;
|
|
|
|
//could not be resolved due to missing namespace, lets be optimistic
|
|
if (ourType == null && string.IsNullOrEmpty(concreteType.ContainingNamespace?.Name))
|
|
{
|
|
ourType = classesToGenerate.FirstOrDefault(y => y.Value.Name == fullName).Value;
|
|
}
|
|
/*
|
|
if (!isOurType && !fullName.StartsWith("System"))
|
|
{
|
|
Console.WriteLine(x + "'s type: " + fullName + " is not in our types");
|
|
Console.WriteLine(concreteType.ContainingNamespace.Name);
|
|
Console.WriteLine(concreteType.Name);
|
|
}*/
|
|
|
|
return new ClassToGenerate.MappableTypes(
|
|
sourceNames[x].type,
|
|
possibleNames[x].type,
|
|
sourceNames[x].symbol,
|
|
possibleNames[x].customFrom,
|
|
possibleNames[x].customFromMethod,
|
|
possibleNames[x].customTo,
|
|
possibleNames[x].customToMethod,
|
|
ourType);
|
|
});
|
|
//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));
|
|
|
|
}
|
|
|
|
static ITypeSymbol GetConcreteType(ITypeSymbol symb)
|
|
{
|
|
//if it's array, dicionary, etc. then we weant to check the underlying type
|
|
if (symb is IArrayTypeSymbol arr)
|
|
{
|
|
//Console.WriteLine(symb.GetFullMetadataName() + " is array: " + GetFullString(arr.ElementType));
|
|
return GetConcreteType(arr.ElementType);
|
|
}
|
|
//also check for ICollection and IEnumerable
|
|
if (symb.Name == "ICollection" || symb.Name == "IEnumerable")
|
|
{
|
|
//if it's generic, then we want to check the underlying type
|
|
if (symb is INamedTypeSymbol gen)
|
|
{
|
|
//Console.WriteLine(symb.GetFullMetadataName() + " is generic: " + GetFullString(gen.TypeArguments[0]));
|
|
return GetConcreteType(gen.TypeArguments[0]);
|
|
}
|
|
}
|
|
|
|
return symb;
|
|
}
|
|
|
|
static SymbolDisplayFormat displayFormat = new SymbolDisplayFormat(
|
|
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
|
|
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
|
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
|
|
|
|
|
|
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, 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 GetInfoFromClassToGenerate(comp, c);
|
|
}
|
|
return new List<(string, PropertyGenInfo)>();
|
|
}
|
|
public static List<(string, PropertyGenInfo)> GetInfoFromClassToGenerate(Compilation comp, ClassToGenerate cls)
|
|
{
|
|
return cls.Properties.Select(x =>
|
|
{
|
|
var name = x.prop.Identifier.ToFullString().Trim();
|
|
return (name,
|
|
new PropertyGenInfo(x.propTypeSymbol, (IPropertySymbol?)null, x.customFrom, x.customFromMethod, x.customTo, x.customToMethod));
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
#region Attribute_Editing
|
|
public static SyntaxList<AttributeListSyntax> GetModifiedAttributeList(Compilation com, SyntaxList<AttributeListSyntax> attListList)
|
|
{
|
|
if (attListList == null)
|
|
return attListList;
|
|
if (attListList.Count <= 0)
|
|
return attListList;
|
|
|
|
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> attList)
|
|
{
|
|
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 syntax.ToString().Contains(AutoMapPropertyAttributeName);
|
|
|
|
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 GetFullMethodName(IMethodSymbol method)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.Append(method.ContainingType.ToDisplayString()).Append(".").Append(method.Name);
|
|
return sb.ToString();
|
|
}
|
|
public static string GeneratePropertyFrom(string Key, ITypeSymbol FromType, ITypeSymbol ToType, bool nullable = true)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.Append(Key).Append(@" = ");
|
|
|
|
var concreteToType = GetConcreteType(ToType);
|
|
var concreteFromType = GetConcreteType(FromType);
|
|
|
|
bool isArray = !SymbolEqualityComparer.Default.Equals(concreteToType, ToType);
|
|
|
|
if (isArray)
|
|
{
|
|
sb.Append("(").Append(ToType).Append(@")(source.").Append(Key).Append(" != null ? ");
|
|
sb.Append("source.").Append(Key);
|
|
sb.Append(nullable ? "?" : "");
|
|
if (!SymbolEqualityComparer.Default.Equals(concreteToType, concreteFromType))
|
|
{
|
|
sb.Append(@".Select(x => (").Append(GetFullString(concreteToType)).Append(@")x)");
|
|
sb.Append(nullable ? "?" : "");
|
|
}
|
|
|
|
//Add the appropriate ToHashSet or ToSet or ToCollection or ToList
|
|
if (ToType.Name.Contains("Set"))
|
|
sb.Append(@".ToHashSet()");
|
|
else
|
|
sb.Append(@".ToList()");
|
|
|
|
sb.Append(" : new ");
|
|
if (ToType.Name.Contains("Set"))
|
|
sb.Append("HashSet<").Append(GetFullString(concreteToType)).Append(">()");
|
|
else
|
|
sb.Append("List<").Append(GetFullString(concreteToType)).Append(">()");
|
|
|
|
sb.Append(")");
|
|
}
|
|
else
|
|
{
|
|
sb.Append("(").Append(ToType).Append(@")source.").Append(Key);
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
public static string GetFullString(ITypeSymbol symbol)
|
|
{
|
|
return symbol.ToDisplayString(format: displayFormat);
|
|
}
|
|
|
|
public static string? GenerateCorrectFromProperty(string Key, ClassToGenerate.MappableTypes Value, bool nullable = true)
|
|
{
|
|
if (!Value.CustomFrom)
|
|
{
|
|
return GeneratePropertyFrom(Key, Value.FromType, Value.ToType, nullable);
|
|
}
|
|
else
|
|
{
|
|
if (Value.CustomFromMethod == null)
|
|
return null;
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.Append(Key).Append(" = (").Append(GetFullString(Value.ToType)).Append(@")").Append(GetFullMethodName(Value.CustomFromMethod)).Append(@"(providers, this, source.").Append(Key).Append(")");
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
public static string GeneratePropertyTo(string Key, ITypeSymbol Type)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.Append(Key).Append(@" = (").Append(GetFullString(Type)).Append(@")this.").Append(Key);
|
|
|
|
return sb.ToString();
|
|
}
|
|
public static string? GenerateCorrectToProperty(string Key, ClassToGenerate.MappableTypes Value)
|
|
{
|
|
var retSb = new StringBuilder();
|
|
retSb.Append("source.");
|
|
if (!Value.CustomTo)
|
|
{
|
|
if (Value.FromIsReadOnly)
|
|
return null;
|
|
retSb.Append(GeneratePropertyTo(Key, Value.FromType));
|
|
}
|
|
else
|
|
{
|
|
if (Value.CustomToMethod == null)
|
|
return null;
|
|
|
|
//if it's readonly we are probably just mutating some other data based of of this one
|
|
if (Value.FromIsReadOnly)
|
|
{
|
|
var callerSb = new StringBuilder();
|
|
callerSb.Append(GetFullMethodName(Value.CustomToMethod)).Append(@"(providers, source, this.").Append(Key).Append(");");
|
|
return callerSb.ToString();
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.Append(Key).Append(" = (").Append(GetFullString(Value.FromType)).Append(@")").Append(GetFullMethodName(Value.CustomToMethod)).Append(@"(providers, source, this.").Append(Key).Append(")");
|
|
retSb.Append(sb.ToString());
|
|
}
|
|
retSb.Append(";");
|
|
return retSb.ToString();
|
|
}
|
|
public static string? GenerateEqualityOperator(string name, ClassToGenerate.MappableTypes Value, string? prefix = null)
|
|
{
|
|
var prefixedName = prefix == null ? name : prefix + "." + name;
|
|
//if the type is also being generated we actually want to use the generated EqualityExpression
|
|
if (Value.OurTargetType != null)
|
|
{
|
|
var concrecteType = GetConcreteType(Value.ToType);
|
|
//cannot process collections/arrays
|
|
if (SymbolEqualityComparer.Default.Equals(concrecteType, Value.ToType))
|
|
{
|
|
var childSb = new StringBuilder();
|
|
|
|
foreach (var prop in Value.OurTargetType.MappableProperties)
|
|
{
|
|
var op = GenerateEqualityOperator(prop.Key, prop.Value, prefixedName);
|
|
if (string.IsNullOrEmpty(op))
|
|
{
|
|
continue;
|
|
}
|
|
childSb.Append(@"
|
|
").Append(op).Append(" && ");
|
|
}
|
|
childSb.Append(@"true");
|
|
return childSb.ToString();
|
|
}
|
|
else
|
|
return null;
|
|
}
|
|
|
|
if (!SymbolEqualityComparer.Default.Equals(Value.FromType, Value.ToType))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var sb = new StringBuilder();
|
|
sb.Append("item1.").Append(prefixedName).Append(" == item2.").Append(prefixedName);
|
|
|
|
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
|
|
|
|
#pragma warning disable CS8600
|
|
#pragma warning disable CS8601
|
|
#pragma warning disable CS8602
|
|
#pragma warning disable CS8603
|
|
#pragma warning disable CS8604
|
|
#pragma warning disable CS8618
|
|
#pragma warning disable CS8625
|
|
|
|
#pragma warning disable CS0109
|
|
#pragma warning disable CS8767
|
|
#pragma warning disable CS0659
|
|
|
|
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)))
|
|
{
|
|
sb.Append(": IAutomappedAttribute<").Append(classToGenerate.SourceContainer.Identifier).Append(",").Append(classToGenerate.Name).Append(">");
|
|
sb.Append(", System.Collections.Generic.IEqualityComparer<").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(@"();
|
|
return dat.ApplyFrom(null, source);
|
|
}");
|
|
sb.Append(@"
|
|
public new ").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)
|
|
{
|
|
var property = GenerateCorrectFromProperty(prop.Key, prop.Value);
|
|
if (property == null)
|
|
continue;
|
|
|
|
sb.Append(@"
|
|
").Append(property).Append(@";");
|
|
}
|
|
sb.Append(@"
|
|
}
|
|
return this;
|
|
}");
|
|
sb.Append(@"
|
|
public new 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)
|
|
{
|
|
var property = GenerateCorrectFromProperty(prop.Key, prop.Value, false);
|
|
if (property == null)
|
|
continue;
|
|
|
|
sb.Append(@"
|
|
").Append(property).Append(@",");
|
|
}
|
|
sb.Append(@"
|
|
};");
|
|
|
|
sb.Append(@"
|
|
public static new 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)
|
|
{
|
|
var op = GenerateEqualityOperator(prop.Key, prop.Value);
|
|
if (string.IsNullOrEmpty(op))
|
|
{
|
|
continue;
|
|
}
|
|
sb.Append(@"
|
|
").Append(op).Append(" && ");
|
|
}
|
|
sb.Append(@"true
|
|
);");
|
|
|
|
sb.Append(@"
|
|
public static new 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)
|
|
{
|
|
var op = GenerateEqualityOperator(prop.Key, prop.Value);
|
|
if (string.IsNullOrEmpty(op))
|
|
{
|
|
continue;
|
|
}
|
|
sb.Append(@"
|
|
").Append(op).Append(" && ");
|
|
}
|
|
sb.Append(@"true
|
|
);");
|
|
|
|
sb.Append(@"
|
|
public override bool Equals(object? obj){
|
|
if(obj == null)
|
|
return false;
|
|
|
|
if (ReferenceEquals(obj, this))
|
|
return true;
|
|
|
|
").Append(classToGenerate.Name).Append(@" item2 = obj as ").Append(classToGenerate.Name).Append(@";
|
|
var item1 = this;
|
|
|
|
return (");
|
|
foreach (var prop in classToGenerate.MappableProperties)
|
|
{
|
|
var op = GenerateEqualityOperator(prop.Key, prop.Value);
|
|
if (string.IsNullOrEmpty(op))
|
|
{
|
|
continue;
|
|
}
|
|
sb.Append(@"
|
|
").Append(op).Append(" && ");
|
|
}
|
|
sb.Append(@"true
|
|
);
|
|
}");
|
|
sb.Append(@"
|
|
public bool Equals(").Append(classToGenerate.Name).Append(" item1, ").Append(classToGenerate.Name).Append(@" item2){
|
|
|
|
if (item1 is null || item2 is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (ReferenceEquals(item1, item2))
|
|
return true;
|
|
|
|
return (");
|
|
foreach (var prop in classToGenerate.MappableProperties)
|
|
{
|
|
var op = GenerateEqualityOperator(prop.Key, prop.Value);
|
|
if (string.IsNullOrEmpty(op))
|
|
{
|
|
continue;
|
|
}
|
|
sb.Append(@"
|
|
").Append(op).Append(" && ");
|
|
}
|
|
sb.Append(@"true
|
|
);
|
|
}");
|
|
|
|
sb.Append(@"
|
|
public int GetHashCode(").Append(classToGenerate.Name).Append(@" obj)
|
|
{
|
|
return obj.GetHashCode();
|
|
}");
|
|
|
|
var abstr = cds.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword));
|
|
if (!abstr)
|
|
{
|
|
|
|
sb.Append(@"
|
|
|
|
public new ").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)
|
|
{
|
|
var property = GenerateCorrectToProperty(prop.Key, prop.Value);
|
|
if (property == null)
|
|
continue;
|
|
sb.Append(@"
|
|
").Append(property);
|
|
}
|
|
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(@"();
|
|
return source.ApplyTo(null, dat);
|
|
}");
|
|
}
|
|
}
|
|
|
|
sb.Append(@"
|
|
}
|
|
}
|
|
#pragma warning restore CS8600
|
|
#pragma warning restore CS8601
|
|
#pragma warning restore CS8602
|
|
#pragma warning restore CS8603
|
|
#pragma warning restore CS8604
|
|
#pragma warning restore CS8618
|
|
#pragma warning restore CS8625
|
|
|
|
#pragma warning restore CS0109
|
|
#pragma warning restore CS8767
|
|
#pragma warning restore CS0659
|
|
|
|
#nullable disable
|
|
");
|
|
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
|
|
public static class Utils
|
|
{
|
|
public static string GetFullMetadataName(this ISymbol s)
|
|
{
|
|
if (s == null || IsRootNamespace(s))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var sb = new StringBuilder(s.MetadataName);
|
|
var last = s;
|
|
|
|
s = s.ContainingSymbol;
|
|
|
|
while (!IsRootNamespace(s))
|
|
{
|
|
if (s is ITypeSymbol && last is ITypeSymbol)
|
|
{
|
|
sb.Insert(0, '+');
|
|
}
|
|
else
|
|
{
|
|
sb.Insert(0, '.');
|
|
}
|
|
|
|
sb.Insert(0, s.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
|
|
//sb.Insert(0, s.MetadataName);
|
|
s = s.ContainingSymbol;
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static bool IsRootNamespace(ISymbol symbol)
|
|
{
|
|
INamespaceSymbol s = null;
|
|
return ((s = symbol as INamespaceSymbol) != null) && s.IsGlobalNamespace;
|
|
}
|
|
}
|
|
}
|