commit dae65b42e5e334af19a76508d57980100c306fd5 Author: honzapatCZ Date: Tue Mar 14 20:46:34 2023 +0100 init diff --git a/AutoMapProperty/.gitignore b/AutoMapProperty/.gitignore new file mode 100644 index 0000000..ffcfc52 --- /dev/null +++ b/AutoMapProperty/.gitignore @@ -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 \ No newline at end of file diff --git a/AutoMapProperty/AutoMapProperty.cs b/AutoMapProperty/AutoMapProperty.cs new file mode 100644 index 0000000..21b915e --- /dev/null +++ b/AutoMapProperty/AutoMapProperty.cs @@ -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 classDeclarations = context.SyntaxProvider + .CreateSyntaxProvider(IsSyntaxTargetForGeneration, GetSemanticTargetForGeneration) + .Where(m => m is not null)!; + + + IncrementalValueProvider<(Compilation, ImmutableArray)> 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 + { + /// + /// Class or Interface + /// + public TypeDeclarationSyntax SourceContainer; + + public List Usings; + public string Namespace; + public string Name; + + public record struct ProperyGenerationInfo(PropertyDeclarationSyntax prop, bool generateConverter = true); + public List 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 MappableProperties = new Dictionary(); + + public ClassToGenerate(TypeDeclarationSyntax sourceClass, string name, string namespaceName, List usings, List properties) + { + SourceContainer = sourceClass; + + Namespace = namespaceName; + Name = name; + Properties = properties; + Usings = usings; + } + } + + + private static void Execute(Compilation compilation, ImmutableArray properties, SourceProductionContext context) + { + if (properties.IsDefaultOrEmpty) + { + // nothing to do yet + return; + } + + IEnumerable distinctClasses = properties.Distinct(); + List 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 GetTypesToGenerate(Compilation compilation, IEnumerable props, SourceProductionContext context) + { + // Create a list to hold our output + var classesToGenerate = new Dictionary(); + // 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 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()); + } + + //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 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 ctg) + { + var result = new List<(string, PropertyGenInfo)>(); + result.AddRange(cls.GetMembers().OfType().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 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 GetModifiedAttributeList(Compilation com, SyntaxList 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 GetModifiedAttributes(Compilation com, SeparatedSyntaxList 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 + //example System.Collections.Generic.IList + //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(@"(?(System\.Collections\.Generic\.)?(IEnumerable|IList|List|ICollection|HashSet|ISet)<(?.*)>)"); + 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> 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(); + } + } +} diff --git a/AutoMapProperty/AutoMapProperty.csproj b/AutoMapProperty/AutoMapProperty.csproj new file mode 100644 index 0000000..835fdce --- /dev/null +++ b/AutoMapProperty/AutoMapProperty.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0 + latest + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/AutoMapPropertyHelper/.gitignore b/AutoMapPropertyHelper/.gitignore new file mode 100644 index 0000000..ffcfc52 --- /dev/null +++ b/AutoMapPropertyHelper/.gitignore @@ -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 \ No newline at end of file diff --git a/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs b/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs new file mode 100644 index 0000000..b919992 --- /dev/null +++ b/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs @@ -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) + { + } + } +} diff --git a/AutoMapPropertyHelper/AutoMapPropertyHelper.csproj b/AutoMapPropertyHelper/AutoMapPropertyHelper.csproj new file mode 100644 index 0000000..e83944c --- /dev/null +++ b/AutoMapPropertyHelper/AutoMapPropertyHelper.csproj @@ -0,0 +1,9 @@ + + + + netstandard2.0 + latest + enable + enable + + diff --git a/AutoMapPropertyHelper/IAutomappedAttribute.cs b/AutoMapPropertyHelper/IAutomappedAttribute.cs new file mode 100644 index 0000000..773810e --- /dev/null +++ b/AutoMapPropertyHelper/IAutomappedAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; + +namespace AutoMapPropertyHelper +{ + public interface IAutomappedAttribute + { + public TSource ApplyTo(TSource value); + public TSelf ApplyFrom(TSource source); + + public Expression> GetProjectorFrom(); + } + +}