From c2fe430801631062c42777083dde66cf32c1b5be Mon Sep 17 00:00:00 2001 From: honzapatCZ Date: Sun, 25 Oct 2026 23:48:22 +0100 Subject: [PATCH] Split AMP into multiple files --- AutoMapProperty.Tests/.gitignore | 263 ++++ .../AutoMapProperty.Tests.csproj | 16 + .../GeneratorTestCompilation.cs | 70 ++ .../MappingRegressionTests.cs | 198 +++ AutoMapProperty.Tests/NullabilityTests.cs | 142 +++ AutoMapProperty/AutoMapProperty.cs | 1117 ----------------- AutoMapProperty/AutoMapPropertyGenerator.cs | 32 + AutoMapProperty/MappingEmitter.cs | 264 ++++ AutoMapProperty/MappingExpressionEmitter.cs | 277 ++++ AutoMapProperty/MappingModel.cs | 66 + AutoMapProperty/MappingModelBuilder.cs | 123 ++ AutoMapProperty/PropertyDeclarationBuilder.cs | 110 ++ AutoMapProperty/TypeSymbolHelpers.cs | 30 + AutoMapPropertyHelper/AutoMapNullability.cs | 12 + .../AutoMapPropertyAttribute.cs | 6 + 15 files changed, 1609 insertions(+), 1117 deletions(-) create mode 100644 AutoMapProperty.Tests/.gitignore create mode 100644 AutoMapProperty.Tests/AutoMapProperty.Tests.csproj create mode 100644 AutoMapProperty.Tests/GeneratorTestCompilation.cs create mode 100644 AutoMapProperty.Tests/MappingRegressionTests.cs create mode 100644 AutoMapProperty.Tests/NullabilityTests.cs delete mode 100644 AutoMapProperty/AutoMapProperty.cs create mode 100644 AutoMapProperty/AutoMapPropertyGenerator.cs create mode 100644 AutoMapProperty/MappingEmitter.cs create mode 100644 AutoMapProperty/MappingExpressionEmitter.cs create mode 100644 AutoMapProperty/MappingModel.cs create mode 100644 AutoMapProperty/MappingModelBuilder.cs create mode 100644 AutoMapProperty/PropertyDeclarationBuilder.cs create mode 100644 AutoMapProperty/TypeSymbolHelpers.cs create mode 100644 AutoMapPropertyHelper/AutoMapNullability.cs diff --git a/AutoMapProperty.Tests/.gitignore b/AutoMapProperty.Tests/.gitignore new file mode 100644 index 0000000..10913e0 --- /dev/null +++ b/AutoMapProperty.Tests/.gitignore @@ -0,0 +1,263 @@ +es (213 sloc) 4.2 KB + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/AutoMapProperty.Tests/AutoMapProperty.Tests.csproj b/AutoMapProperty.Tests/AutoMapProperty.Tests.csproj new file mode 100644 index 0000000..9694b44 --- /dev/null +++ b/AutoMapProperty.Tests/AutoMapProperty.Tests.csproj @@ -0,0 +1,16 @@ + + + net10.0 + enable + enable + false + + + + + + + + + + diff --git a/AutoMapProperty.Tests/GeneratorTestCompilation.cs b/AutoMapProperty.Tests/GeneratorTestCompilation.cs new file mode 100644 index 0000000..139ffbe --- /dev/null +++ b/AutoMapProperty.Tests/GeneratorTestCompilation.cs @@ -0,0 +1,70 @@ +using AutoMapPropertyHelper; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Reflection; +using Xunit; + +namespace AutoMapProperty.Tests; + +internal sealed class GeneratorTestCompilation +{ + private static readonly MetadataReference[] References = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!) + .Split(Path.PathSeparator) + .Append(typeof(AutoMapPropertyAttribute).Assembly.Location) + .Append(typeof(LinqKit.PredicateBuilder).Assembly.Location) + .Distinct() + .Select(path => MetadataReference.CreateFromFile(path)) + .ToArray(); + + private const string Support = """ + using System; + using System.Collections.Generic; + using System.Linq; + using AutoMapPropertyHelper; + + namespace AutoMapProperty + { + public static class AutoMapPropertyUtils + { + public static T Create(IServiceProvider providers) where T : class => Activator.CreateInstance(); + public static T Create(IServiceProvider providers, TParent parent, string name) + where T : class, new() where TParent : class => new T(); + } + } + """; + + public Compilation Output { get; } + public GeneratorDriverRunResult RunResult { get; } + + public GeneratorTestCompilation(string source) + { + var compilation = CSharpCompilation.Create("MappingTests_" + Guid.NewGuid().ToString("N"), + new[] { CSharpSyntaxTree.ParseText(Support + "\n" + source) }, References, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new AutoMapPropertyGenerator()); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var output, out _); + Output = output; + RunResult = driver.GetRunResult(); + Assert.All(RunResult.Results, result => Assert.Null(result.Exception)); + } + + public void AssertCompiles() + { + var errors = Output.GetDiagnostics().Concat(RunResult.Diagnostics) + .Where(x => x.Severity == DiagnosticSeverity.Error).ToArray(); + Assert.True(errors.Length == 0, string.Join(Environment.NewLine, errors.Select(x => x.ToString()))); + } + + public IPropertySymbol Property(string type, string property) => + Assert.IsAssignableFrom(Assert.Single(Output.GetTypeByMetadataName(type)!.GetMembers(property))); + + public object? Evaluate() + { + AssertCompiles(); + using var stream = new MemoryStream(); + var result = Output.Emit(stream); + Assert.True(result.Success, string.Join(Environment.NewLine, result.Diagnostics)); + return Assembly.Load(stream.ToArray()).GetType("Fixtures.Check")!.GetMethod("Run")!.Invoke(null, null); + } +} diff --git a/AutoMapProperty.Tests/MappingRegressionTests.cs b/AutoMapProperty.Tests/MappingRegressionTests.cs new file mode 100644 index 0000000..68ef447 --- /dev/null +++ b/AutoMapProperty.Tests/MappingRegressionTests.cs @@ -0,0 +1,198 @@ +using Microsoft.CodeAnalysis; +using Xunit; + +namespace AutoMapProperty.Tests; + +public class MappingRegressionTests +{ + [Fact] + public void NullableNestedDtosMapNullAndNonNullValues() + { + var compilation = new GeneratorTestCompilation(""" + namespace Fixtures + { + public class Child + { + [AutoMapProperty("Response")] + public int Value { get; set; } + } + public class Parent + { + [AutoMapProperty("Response", typeof(ChildResponse), Nullability = AutoMapNullability.Nullable)] + public Child Child { get; set; } = new Child(); + } + public static class Check + { + public static bool Run() + { + var parent = new Parent { Child = new Child { Value = 42 } }; + var dto = new ParentResponse().ApplyFrom(null, parent); + if (dto.Child.Value != 42 || dto.GetProjectorFrom(null).Compile()(parent).Child.Value != 42) + return false; + dto.Child = null; + dto.ApplyTo(null, parent); + if (parent.Child != null || new ParentResponse().ApplyFrom(null, parent).Child != null || + dto.GetProjectorFrom(null).Compile()(parent).Child != null) + return false; + if (!ParentResponse.EqualExpression(null, parent).Compile()(dto) || + !dto.Equals(new ParentResponse { Child = null })) + return false; + dto.Child = new ChildResponse { Value = 17 }; + dto.ApplyTo(null, parent); + return parent.Child.Value == 17; + } + } + } + """); + Assert.Equal(NullableAnnotation.Annotated, compilation.Property("Fixtures.ParentResponse", "Child").Type.NullableAnnotation); + Assert.Equal(true, compilation.Evaluate()); + } + + [Fact] + public void InheritedGeneratedPropertiesParticipateInMappings() + { + var compilation = new GeneratorTestCompilation(""" + namespace Fixtures + { + public class Base + { + [AutoMapProperty("Request", Nullability = AutoMapNullability.NonNullable)] + public string? Name { get; set; } + } + public class Derived : Base + { + [AutoMapProperty("Request")] + public int Value { get; set; } + } + public partial class DerivedRequest : BaseRequest { } + public static class Check + { + public static bool Run() + { + var entity = new Derived { Name = "name", Value = 42 }; + var dto = new DerivedRequest().ApplyFrom(null, entity); + var projected = dto.GetProjectorFrom(null).Compile()(entity); + dto.Name = "changed"; + dto.ApplyTo(null, entity); + return entity.Name == "changed" && projected.Name == "name" && projected.Value == 42; + } + } + } + """); + Assert.Equal(true, compilation.Evaluate()); + } + + [Fact] + public void CustomConvertersReceiveOverriddenNullableValues() + { + var compilation = new GeneratorTestCompilation(""" + namespace Fixtures + { + public class Entity + { + [AutoMapProperty("Request", typeof(string), nameof(Parse), nameof(Format), Nullability = AutoMapNullability.Nullable)] + public int Value { get; set; } + public static int Parse(IServiceProvider providers, Entity entity, string? value) => value == null ? 0 : int.Parse(value); + public static string Format(IServiceProvider providers, Entity entity, int value) => value.ToString(); + } + public static class Check + { + public static bool Run() + { + var entity = new Entity { Value = 42 }; + var dto = new EntityRequest().ApplyFrom(null, entity); + if (dto.Value != "42" || dto.GetProjectorFrom(null).Compile()(entity).Value != "42") + return false; + dto.Value = null; + dto.ApplyTo(null, entity); + return entity.Value == 0; + } + } + } + """); + Assert.Equal(true, compilation.Evaluate()); + } + + [Fact] + public void FiltersMappingAttributesFromSharedListsAndKeepsValidationAttributes() + { + var compilation = new GeneratorTestCompilation(""" + namespace Fixtures + { + public class Entity + { + [System.ComponentModel.DataAnnotations.Required, AutoMapProperty("Response", Nullability = AutoMapNullability.Nullable)] + public string Value { get; set; } = "initial"; + } + } + """); + compilation.AssertCompiles(); + var attribute = Assert.Single(compilation.Property("Fixtures.EntityResponse", "Value").GetAttributes()); + Assert.Equal("RequiredAttribute", attribute.AttributeClass!.Name); + } + + [Fact] + public void NestedCollectionsAndArrayTypeReplacementsKeepMappingAndInitializerBehavior() + { + var compilation = new GeneratorTestCompilation(""" + namespace Fixtures + { + public class Child + { + [AutoMapProperty("Response")] + public int Value { get; set; } + } + public class Parent + { + [AutoMapProperty("Response", typeof(ChildResponse[]), Nullability = AutoMapNullability.Nullable)] + public Child[] Children { get; set; } = Array.Empty(); + } + public static class Check + { + public static bool Run() + { + var parent = new Parent { Children = new[] { new Child { Value = 42 } } }; + var dto = new ParentResponse().ApplyFrom(null, parent); + if (dto.Children[0].Value != 42 || dto.GetProjectorFrom(null).Compile()(parent).Children[0].Value != 42) + return false; + dto.Children[0].Value = 17; + dto.ApplyTo(null, parent); + if (parent.Children[0].Value != 17) + return false; + dto.Children = null; + dto.ApplyTo(null, parent); + return parent.Children.Length == 0; + } + } + } + """); + Assert.Equal(true, compilation.Evaluate()); + } + + [Fact] + public void SupportsInterfacesComputedPropertiesAndNullTypeArguments() + { + var compilation = new GeneratorTestCompilation(""" + namespace Fixtures + { + public interface ISource + { + [AutoMapProperty("Request", Nullability = AutoMapNullability.Nullable)] + string Name { get; } + } + public class Entity + { + [AutoMapProperty("Response", null, Nullability = AutoMapNullability.NonNullable)] + public string? Name => "name"; + } + public static class Check + { + public static bool Run() => new EntityResponse().ApplyFrom(null, new Entity()).Name == "name"; + } + } + """); + Assert.Equal(NullableAnnotation.Annotated, compilation.Property("Fixtures.ISourceRequest", "Name").Type.NullableAnnotation); + Assert.Equal(NullableAnnotation.NotAnnotated, compilation.Property("Fixtures.EntityResponse", "Name").Type.NullableAnnotation); + Assert.Equal(true, compilation.Evaluate()); + } +} diff --git a/AutoMapProperty.Tests/NullabilityTests.cs b/AutoMapProperty.Tests/NullabilityTests.cs new file mode 100644 index 0000000..cb3509f --- /dev/null +++ b/AutoMapProperty.Tests/NullabilityTests.cs @@ -0,0 +1,142 @@ +using Microsoft.CodeAnalysis; +using Xunit; + +namespace AutoMapProperty.Tests; + +public class NullabilityTests +{ + [Theory] + [InlineData("string", "Nullable", true)] + [InlineData("string?", "NonNullable", false)] + [InlineData("string?", "Preserve", true)] + [InlineData("string", "Preserve", false)] + [InlineData("int", "Nullable", true)] + [InlineData("int?", "Nullable", true)] + [InlineData("int?", "NonNullable", false)] + [InlineData("int", "NonNullable", false)] + [InlineData("int?", "Preserve", true)] + [InlineData("DateTime", "Nullable", true)] + [InlineData("DateTime?", "NonNullable", false)] + [InlineData("string[]", "Nullable", true)] + [InlineData("string[]?", "NonNullable", false)] + [InlineData("ICollection", "Nullable", true)] + public void OverridesTopLevelNullability(string type, string mode, bool nullable) + { + var compilation = new GeneratorTestCompilation($$""" + namespace Fixtures + { + public class Entity + { + [AutoMapProperty("Response", Nullability = AutoMapNullability.{{mode}})] + public {{type}} Value { get; set; } + } + } + """); + compilation.AssertCompiles(); + var generatedType = compilation.Property("Fixtures.EntityResponse", "Value").Type; + Assert.Equal(nullable, generatedType.NullableAnnotation == NullableAnnotation.Annotated || + generatedType is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T }); + if (generatedType is INamedTypeSymbol { Name: "ICollection" } collection) + Assert.Equal(NullableAnnotation.Annotated, collection.TypeArguments[0].NullableAnnotation); + } + + [Fact] + public void OverridesAreIndependentForEachDtoAndAppliedAfterTypeReplacement() + { + var compilation = new GeneratorTestCompilation(""" + namespace Fixtures + { + public class Entity + { + [AutoMapProperty("Request", typeof(long), Nullability = AutoMapNullability.Nullable)] + [AutoMapProperty("Response", typeof(long?), Nullability = AutoMapNullability.NonNullable)] + [AutoMapProperty("Copy")] + public int Value { get; set; } + } + } + """); + compilation.AssertCompiles(); + var request = Assert.IsAssignableFrom(compilation.Property("Fixtures.EntityRequest", "Value").Type); + Assert.Equal(SpecialType.System_Nullable_T, request.OriginalDefinition.SpecialType); + Assert.Equal(SpecialType.System_Int64, request.TypeArguments[0].SpecialType); + Assert.Equal(SpecialType.System_Int64, compilation.Property("Fixtures.EntityResponse", "Value").Type.SpecialType); + Assert.Equal(SpecialType.System_Int32, compilation.Property("Fixtures.EntityCopy", "Value").Type.SpecialType); + } + + [Fact] + public void NullableValuesMapInBothDirectionsAndThroughProjectors() + { + var compilation = new GeneratorTestCompilation(""" + namespace Fixtures + { + public class Entity + { + [AutoMapProperty("Request", Nullability = AutoMapNullability.Nullable)] + public int Value { get; set; } + } + public static class Check + { + public static bool Run() + { + var entity = new Entity { Value = 42 }; + var dto = new EntityRequest().ApplyFrom(null, entity); + var projected = dto.GetProjectorFrom(null).Compile()(entity); + dto.Value = 17; + dto.ApplyTo(null, entity); + dto.Value = null; + try { dto.ApplyTo(null, entity); } + catch (InvalidOperationException) { return entity.Value == 17 && projected.Value == 42; } + return false; + } + } + } + """); + Assert.Equal(true, compilation.Evaluate()); + } + + [Fact] + public void ValueTypeNullabilityOverridesKeepValidInitializers() + { + var compilation = new GeneratorTestCompilation(""" + namespace Fixtures + { + public class Entity + { + [AutoMapProperty("Request", Nullability = AutoMapNullability.Nullable)] + public DateTime Date { get; set; } = new DateTime(2026, 1, 2); + [AutoMapProperty("Request", Nullability = AutoMapNullability.NonNullable)] + public int? Count { get; set; } = null; + [AutoMapProperty("Request", Nullability = AutoMapNullability.NonNullable)] + public int? Total { get; set; } = default(int?); + } + public static class Check + { + public static bool Run() + { + var dto = new EntityRequest(); + return dto.Date == new DateTime(2026, 1, 2) && dto.Count == 0 && dto.Total == 0; + } + } + } + """); + Assert.Equal(true, compilation.Evaluate()); + } + + [Fact] + public void InvalidNullabilityReportsPropertyDiagnostic() + { + var compilation = new GeneratorTestCompilation(""" + namespace Fixtures + { + public class Entity + { + [AutoMapProperty("Response", Nullability = (AutoMapNullability)99)] + public string Value { get; set; } + } + } + """); + var diagnostic = Assert.Single(compilation.RunResult.Diagnostics); + Assert.Equal("NEJ23", diagnostic.Id); + Assert.True(diagnostic.Location.IsInSource); + } +} diff --git a/AutoMapProperty/AutoMapProperty.cs b/AutoMapProperty/AutoMapProperty.cs deleted file mode 100644 index 581c704..0000000 --- a/AutoMapProperty/AutoMapProperty.cs +++ /dev/null @@ -1,1117 +0,0 @@ -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 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 string FullName => Namespace + "." + Name; - - public record struct ProperyGenerationInfo(PropertyDeclarationSyntax prop, ITypeSymbol propTypeSymbol, bool customFrom, IMethodSymbol? customFromMethod, bool customTo, IMethodSymbol? customToMethod); - public List 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 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(enumsToGenerate, 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 (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 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()); - } - - 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() + " ")); - - var oldConcreteType = GetConcreteType(propSymbol.Type); - var newConcreteType = GetConcreteType(dataTypename); - - if (mem.Initializer != null && !SymbolEqualityComparer.Default.Equals(oldConcreteType, newConcreteType)) - { - var rewriter = new TypeReplacer(oldConcreteType.Name, newConcreteType.Name); - var newInitializer = (EqualsValueClauseSyntax)rewriter.Visit(mem.Initializer); - mem = mem.WithInitializer(newInitializer); - } - - 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 - if (mem.Initializer == null) - { - mem = mem.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)); - } - else - { - mem = mem.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); - } - - //if we dont want to generate converter - bool customFrom = false; - IMethodSymbol? customFromMethod = null; - bool customTo = false; - IMethodSymbol? customToMethod = null; - if (attr.ConstructorArguments.Count() >= 3 && attr.ConstructorArguments[2].Value is not null) - { - customTo = true; - customToMethod = ProcessCustomAttribute(context, classSymbol, attr.ConstructorArguments[2]); - } - if (attr.ConstructorArguments.Count() >= 4 && attr.ConstructorArguments[3].Value is not null) - { - 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(); - //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 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 ctg) - { - var result = new List<(string, PropertyGenInfo)>(); - - result.AddRange(cls.GetMembers().OfType().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 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 GetModifiedAttributeList(Compilation com, SyntaxList attListList) - { - if (attListList == null) - return attListList; - if (attListList.Count <= 0) - return attListList; - - var toRemove = new List(); - - 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(attListList.Where(x => !toRemove.Contains(x))); - } - public static SeparatedSyntaxList GetModifiedAttributes(Compilation com, SeparatedSyntaxList attList) - { - var toRemove = new List(); - foreach (var x in attList.ToList().Where(x => x != null).Where(x => IsOurAttribute(com, x))) - { - toRemove.Add(x); - } - var newList = SyntaxFactory.SeparatedList(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 GetProjectorName(ClassToGenerate cls) - { - return cls.Name.ToLower(); - } - public static string GeneratePropertyFrom(List otherClasses, string Key, ITypeSymbol FromType, ITypeSymbol ToType, bool allowProjectors, List? requiredProjectors, bool nullable = true, string prefix = "") - { - 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(prefix).Append(".").Append(Key).Append(" != null ? "); - sb.Append("source").Append(prefix).Append(".").Append(Key); - sb.Append(nullable ? "?" : ""); - if (!SymbolEqualityComparer.Default.Equals(concreteToType, concreteFromType)) - { - var otherCls = otherClasses.FirstOrDefault(x => x.Name == concreteToType.Name); - if (otherCls != null && allowProjectors) - { - sb.Append(@".Select(x => ").Append(GetProjectorName(otherCls)).Append(@".Invoke(x))"); - sb.Append(nullable ? "?" : ""); - requiredProjectors!.Add(otherCls); - } - else - { - sb.Append(@".Select(x => new ").Append(GetFullString(concreteToType)).Append(@"().ApplyFrom(providers, x))"); - sb.Append(nullable ? "?" : ""); - } - } - - //Add the appropriate ToHashSet or ToSet or ToCollection or ToList - if (ToType.Name.Contains("Set")) - sb.Append(@".ToHashSet()"); - else if (ToType.TypeKind == TypeKind.Array) - sb.Append(@".ToArray()"); - else - sb.Append(@".ToList()"); - - sb.Append(" : "); - if (ToType.Name.Contains("Set")) - sb.Append("new HashSet<").Append(GetFullString(concreteToType)).Append(">()"); - else if (ToType.TypeKind == TypeKind.Array) - sb.Append("Array.Empty<").Append(GetFullString(concreteToType)).Append(">()"); - else - sb.Append("new List<").Append(GetFullString(concreteToType)).Append(">()"); - - sb.Append(")"); - } - else - { - if (!otherClasses.Select(x => x.Name).Contains(ToType.Name)) - sb.Append("(").Append(ToType).Append(@")source").Append(prefix).Append(".").Append(Key); - else - { - sb.Append("source").Append(prefix).Append(".").Append(Key).Append(" == null ? null : "); - var classToGenerate = otherClasses.First(x => x.Name == ToType.Name); - sb.Append(@"new ").Append(classToGenerate.Name).Append(@"() - {"); - foreach (var prop in classToGenerate.MappableProperties) - { - var property = GenerateCorrectFromProperty(otherClasses, prop.Key, prop.Value, allowProjectors, requiredProjectors, false, prefix + "." + Key); - if (property == null) - continue; - - sb.Append(@" - ").Append(property).Append(@","); - } - sb.Append(@" - }"); - } - } - - return sb.ToString(); - } - - public static string GetFullString(ITypeSymbol symbol) - { - return symbol.ToDisplayString(format: displayFormat); - } - - public static string? GenerateCorrectFromProperty(List otherClasses, string Key, ClassToGenerate.MappableTypes Value, bool allowProjectors, List? requiredProjectors, bool nullable = true, string prefix = "") - { - if (!Value.CustomFrom) - { - return GeneratePropertyFrom(otherClasses, Key, Value.FromType, Value.ToType, allowProjectors, requiredProjectors, nullable, prefix); - } - 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, source").Append(prefix).Append(", source").Append(prefix).Append(".").Append(Key).Append(")"); - return sb.ToString(); - } - } - - private static (string, List) GenerateFromExpression(List otherClasses, ClassToGenerate classToGenerate, SyntaxToken sourceName) - { - var projectors = new List(); - var exprSb = new StringBuilder(); - exprSb.Append(@"System.Linq.Expressions.Expression> expr = (").Append(sourceName).Append(" source)=>new ").Append(classToGenerate.Name).Append(@"() - {"); - foreach (var prop in classToGenerate.MappableProperties) - { - var property = GenerateCorrectFromProperty(otherClasses, prop.Key, prop.Value, true, projectors, false); - if (property == null) - continue; - - exprSb.Append(@" - ").Append(property).Append(@","); - } - exprSb.Append(@" - }; - return expr.Expand(); - "); - - return (exprSb.ToString(), projectors); - } - - public static string? GeneratePropertyTo(ClassToGenerate classToGenerate, List otherClasses, string Key, ITypeSymbol FromType, ITypeSymbol ToType, bool nullable = true) - { - - - var concreteToType = GetConcreteType(ToType); - var concreteFromType = GetConcreteType(FromType); - - bool isArray = !SymbolEqualityComparer.Default.Equals(FromType, concreteFromType); - - if (concreteFromType.IsAbstract) - return null; - - var sb = new StringBuilder(); - - if (isArray) - { - sb.Append(Key).Append("?.Any(); source."); - } - - sb.Append(Key).Append(@" ="); - - if (isArray) - { - sb.Append("(").Append(FromType).Append(@")(this.").Append(Key).Append(" != null ? "); - sb.Append("this.").Append(Key); - sb.Append(nullable ? "?" : ""); - if (!SymbolEqualityComparer.Default.Equals(concreteToType, concreteFromType)) - { - sb.Append(@".Select(x => (").Append(GetFullString(concreteFromType)).Append(@")x.ApplyTo(providers, (").Append(GetFullString(concreteFromType)).Append(@")(AutoMapProperty.AutoMapPropertyUtils.Create<").Append(GetFullString(concreteFromType)).Append(",").Append(classToGenerate.SourceContainer.Identifier).Append(">(providers, source, \"").Append(Key).Append("\"))))"); - sb.Append(nullable ? "?" : ""); - } - - //Add the appropriate ToHashSet or ToSet or ToCollection or ToList - if (FromType.Name.Contains("Set")) - sb.Append(@".ToHashSet()"); - else if (FromType.TypeKind == TypeKind.Array) - sb.Append(@".ToArray()"); - else - sb.Append(@".ToList()"); - - sb.Append(" : "); - if (FromType.Name.Contains("Set")) - sb.Append("new HashSet<").Append(GetFullString(concreteFromType)).Append(">()"); - else if (FromType.TypeKind == TypeKind.Array) - sb.Append("Array.Empty<").Append(GetFullString(concreteFromType)).Append(">()"); - else - sb.Append("new List<").Append(GetFullString(concreteFromType)).Append(">()"); - - sb.Append(")"); - } - else - { - if (otherClasses.Select(x => x.Name).Contains(ToType.Name)) - sb.Append("this.").Append(Key).Append(".ApplyTo(providers, (").Append(GetFullString(FromType)).Append(@")(source.").Append(Key).Append(" != null ? source.").Append(Key).Append(" : AutoMapProperty.AutoMapPropertyUtils.Create<").Append(GetFullString(FromType)).Append(",").Append(classToGenerate.SourceContainer.Identifier).Append(">(providers, source, \"").Append(Key).Append("\")))"); - else - sb.Append("(").Append(GetFullString(FromType)).Append(@")this.").Append(Key); - } - return sb.ToString(); - } - public static string? GenerateCorrectToProperty(ClassToGenerate classToGenerate, List otherClasses, string Key, ClassToGenerate.MappableTypes Value, bool nullable = true) - { - var retSb = new StringBuilder(); - retSb.Append("source."); - if (!Value.CustomTo) - { - if (Value.FromIsReadOnly) - return null; - var generated = GeneratePropertyTo(classToGenerate, otherClasses, Key, Value.FromType, Value.ToType, nullable); - if (generated == null) - return null; - retSb.Append(generated); - } - 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(List otherClasses, ClassToGenerate classToGenerate) - { - var sb = new StringBuilder(); - - foreach (var usi in classToGenerate.Usings) - { - sb.AppendLine(usi); - } - - sb.Append(@" -using LinqKit; -#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(@" - object? IAutomappedAttribute.ApplyFrom(System.IServiceProvider? providers, object source) - { - if(source is ").Append(classToGenerate.SourceContainer.Identifier).Append(@" src) - return ApplyFrom(providers, src); - return null; - } - 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(otherClasses, prop.Key, prop.Value, false, null); - if (property == null) - continue; - - sb.Append(@" - ").Append(property).Append(@";"); - } - sb.Append(@" - } - return this; - }"); - sb.Append(@" - public new System.Linq.Expressions.Expression> GetProjectorFrom(System.IServiceProvider providers) { - - "); - var (expr, projectors) = GenerateFromExpression(otherClasses, classToGenerate, classToGenerate.SourceContainer.Identifier); - - foreach (var projector in projectors.Distinct()) - { - sb.Append("var ").Append(GetProjectorName(projector)).Append(@" = new ").Append(projector.Name).Append(@"().GetProjectorFrom(providers); - "); - } - sb.Append(expr); - sb.Append(@" - }"); - - sb.Append(@" - public static new System.Linq.Expressions.Expression> 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> 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(); - } - public override int GetHashCode() - { - return 7; - } - - object IAutomappedAttribute.ApplyTo(System.IServiceProvider? providers, object source) - { - if(source == null) - return null; - if(source is ").Append(classToGenerate.SourceContainer.Identifier).Append(@" src) - return ApplyTo(providers, src); - return null; - } - 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) - {/* - if (prop.Value.FromType.IsAbstract) - continue;*/ - var property = GenerateCorrectToProperty(classToGenerate, otherClasses, prop.Key, prop.Value); - if (property == null) - continue; - sb.Append(@" - ").Append(property); - } - sb.Append(@" - } - return source; - } - Type IAutomappedAttribute.GetSourceType() => typeof(").Append(classToGenerate.SourceContainer.Identifier).Append(@"); - "); - sb.Append(@" - - "); - - var abstr = cds.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword)); - if (!abstr) - { - - sb.Append(@" - public ").Append(classToGenerate.SourceContainer.Identifier).Append(@" Create(System.IServiceProvider? providers = null) - { - var dat = AutoMapProperty.AutoMapPropertyUtils.Create<").Append(classToGenerate.SourceContainer.Identifier).Append(@">(providers); - return ApplyTo(providers, dat); - } - public static explicit operator ").Append(classToGenerate.SourceContainer.Identifier).Append(@"(").Append(classToGenerate.Name).Append(@" source) - { - return source.Create(null); - }"); - } - } - - 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(); - } - - private class TypeReplacer : CSharpSyntaxRewriter - { - private readonly string _oldTypeName; - private readonly string _newTypeName; - - public TypeReplacer(string oldTypeName, string newTypeName) - { - _oldTypeName = oldTypeName; - _newTypeName = newTypeName; - } - - public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) - { - if (node.Identifier.ValueText == _oldTypeName) - { - return SyntaxFactory.IdentifierName(_newTypeName).WithTriviaFrom(node); - } - return base.VisitIdentifierName(node); - } - } - } - - 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; - } - } -} diff --git a/AutoMapProperty/AutoMapPropertyGenerator.cs b/AutoMapProperty/AutoMapPropertyGenerator.cs new file mode 100644 index 0000000..f220219 --- /dev/null +++ b/AutoMapProperty/AutoMapPropertyGenerator.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Text; + +namespace AutoMapProperty; + +[Generator] +public class AutoMapPropertyGenerator : IIncrementalGenerator +{ + internal const string AttributeName = "AutoMapPropertyHelper.AutoMapPropertyAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var properties = context.SyntaxProvider.ForAttributeWithMetadataName( + AttributeName, + static (node, _) => node is PropertyDeclarationSyntax, + static (context, _) => (PropertyDeclarationSyntax)context.TargetNode); + + context.RegisterSourceOutput(context.CompilationProvider.Combine(properties.Collect()), + static (context, input) => + { + var types = MappingModelBuilder.Build(input.Left, input.Right, context); + foreach (var type in types) + { + context.CancellationToken.ThrowIfCancellationRequested(); + context.AddSource(type.Name + ".g.cs", + SourceText.From(MappingEmitter.GenerateExtensionClass(types, type), Encoding.UTF8)); + } + }); + } +} diff --git a/AutoMapProperty/MappingEmitter.cs b/AutoMapProperty/MappingEmitter.cs new file mode 100644 index 0000000..7468dfc --- /dev/null +++ b/AutoMapProperty/MappingEmitter.cs @@ -0,0 +1,264 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Text; +using static AutoMapProperty.MappingExpressionEmitter; + +namespace AutoMapProperty; + +internal static class MappingEmitter +{ + public static string GenerateExtensionClass(List otherClasses, GeneratedType classToGenerate) + { + var sb = new StringBuilder(); + + foreach (var usi in classToGenerate.Usings) + { + sb.AppendLine(usi); + } + + sb.Append(@" +using LinqKit; +#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) + { + 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.Declaration.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(@" + object? IAutomappedAttribute.ApplyFrom(System.IServiceProvider? providers, object source) + { + if(source is ").Append(classToGenerate.SourceContainer.Identifier).Append(@" src) + return ApplyFrom(providers, src); + return null; + } + 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(otherClasses, prop.Key, prop.Value, false, null); + if (property == null) + continue; + + sb.Append(@" + ").Append(property).Append(@";"); + } + sb.Append(@" + } + return this; + }"); + sb.Append(@" + public new System.Linq.Expressions.Expression> GetProjectorFrom(System.IServiceProvider providers) { + + "); + var (expr, projectors) = GenerateFromExpression(otherClasses, classToGenerate, classToGenerate.SourceContainer.Identifier); + + foreach (var projector in projectors.Distinct()) + { + sb.Append("var ").Append(GetProjectorName(projector)).Append(@" = new ").Append(projector.Name).Append(@"().GetProjectorFrom(providers); + "); + } + sb.Append(expr); + sb.Append(@" + }"); + + sb.Append(@" + public static new System.Linq.Expressions.Expression> 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> 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(); + } + public override int GetHashCode() + { + return 7; + } + + object IAutomappedAttribute.ApplyTo(System.IServiceProvider? providers, object source) + { + if(source == null) + return null; + if(source is ").Append(classToGenerate.SourceContainer.Identifier).Append(@" src) + return ApplyTo(providers, src); + return null; + } + 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(classToGenerate, otherClasses, prop.Key, prop.Value); + if (property == null) + continue; + sb.Append(@" + ").Append(property); + } + sb.Append(@" + } + return source; + } + Type IAutomappedAttribute.GetSourceType() => typeof(").Append(classToGenerate.SourceContainer.Identifier).Append(@"); + "); + sb.Append(@" + + "); + + var abstr = cds.Modifiers.Any(x => x.IsKind(SyntaxKind.AbstractKeyword)); + if (!abstr) + { + + sb.Append(@" + public ").Append(classToGenerate.SourceContainer.Identifier).Append(@" Create(System.IServiceProvider? providers = null) + { + var dat = AutoMapProperty.AutoMapPropertyUtils.Create<").Append(classToGenerate.SourceContainer.Identifier).Append(@">(providers); + return ApplyTo(providers, dat); + } + public static explicit operator ").Append(classToGenerate.SourceContainer.Identifier).Append(@"(").Append(classToGenerate.Name).Append(@" source) + { + return source.Create(null); + }"); + } + } + + 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(); + } + +} diff --git a/AutoMapProperty/MappingExpressionEmitter.cs b/AutoMapProperty/MappingExpressionEmitter.cs new file mode 100644 index 0000000..5d8074d --- /dev/null +++ b/AutoMapProperty/MappingExpressionEmitter.cs @@ -0,0 +1,277 @@ +using Microsoft.CodeAnalysis; +using System.Text; +using static AutoMapProperty.TypeSymbolHelpers; + +namespace AutoMapProperty; + +internal static class MappingExpressionEmitter +{ + 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 GetProjectorName(GeneratedType cls) + { + return cls.Name.ToLower(); + } + public static string GeneratePropertyFrom(List otherClasses, string Key, ITypeSymbol FromType, ITypeSymbol ToType, bool allowProjectors, List? requiredProjectors, bool useNullConditionalAccess = true, string prefix = "") + { + 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(prefix).Append(".").Append(Key).Append(" != null ? "); + sb.Append("source").Append(prefix).Append(".").Append(Key); + sb.Append(useNullConditionalAccess ? "?" : ""); + if (!SymbolEqualityComparer.Default.Equals(concreteToType, concreteFromType)) + { + var otherCls = otherClasses.FirstOrDefault(x => x.Name == concreteToType.Name); + if (otherCls != null && allowProjectors) + { + sb.Append(@".Select(x => ").Append(GetProjectorName(otherCls)).Append(@".Invoke(x))"); + sb.Append(useNullConditionalAccess ? "?" : ""); + requiredProjectors!.Add(otherCls); + } + else + { + sb.Append(@".Select(x => new ").Append(GetFullString(concreteToType)).Append(@"().ApplyFrom(providers, x))"); + sb.Append(useNullConditionalAccess ? "?" : ""); + } + } + + //Add the appropriate ToHashSet or ToSet or ToCollection or ToList + if (ToType.Name.Contains("Set")) + sb.Append(@".ToHashSet()"); + else if (ToType.TypeKind == TypeKind.Array) + sb.Append(@".ToArray()"); + else + sb.Append(@".ToList()"); + + sb.Append(" : "); + if (ToType.Name.Contains("Set")) + sb.Append("new HashSet<").Append(GetFullString(concreteToType)).Append(">()"); + else if (ToType.TypeKind == TypeKind.Array) + sb.Append("Array.Empty<").Append(GetFullString(concreteToType)).Append(">()"); + else + sb.Append("new List<").Append(GetFullString(concreteToType)).Append(">()"); + + sb.Append(")"); + } + else + { + if (!otherClasses.Select(x => x.Name).Contains(ToType.Name)) + sb.Append("(").Append(ToType).Append(@")source").Append(prefix).Append(".").Append(Key); + else + { + sb.Append("source").Append(prefix).Append(".").Append(Key).Append(" == null ? null : "); + var classToGenerate = otherClasses.First(x => x.Name == ToType.Name); + sb.Append(@"new ").Append(classToGenerate.Name).Append(@"() + {"); + foreach (var prop in classToGenerate.MappableProperties) + { + var property = GenerateCorrectFromProperty(otherClasses, prop.Key, prop.Value, allowProjectors, requiredProjectors, false, prefix + "." + Key); + if (property == null) + continue; + + sb.Append(@" + ").Append(property).Append(@","); + } + sb.Append(@" + }"); + } + } + + return sb.ToString(); + } + + public static string? GenerateCorrectFromProperty(List otherClasses, string Key, MappableProperty Value, bool allowProjectors, List? requiredProjectors, bool useNullConditionalAccess = true, string prefix = "") + { + if (!Value.CustomFrom) + { + return GeneratePropertyFrom(otherClasses, Key, Value.FromType, Value.ToType, allowProjectors, requiredProjectors, useNullConditionalAccess, prefix); + } + 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, source").Append(prefix).Append(", source").Append(prefix).Append(".").Append(Key).Append(")"); + return sb.ToString(); + } + } + + public static (string, List) GenerateFromExpression(List otherClasses, GeneratedType classToGenerate, SyntaxToken sourceName) + { + var projectors = new List(); + var exprSb = new StringBuilder(); + exprSb.Append(@"System.Linq.Expressions.Expression> expr = (").Append(sourceName).Append(" source)=>new ").Append(classToGenerate.Name).Append(@"() + {"); + foreach (var prop in classToGenerate.MappableProperties) + { + var property = GenerateCorrectFromProperty(otherClasses, prop.Key, prop.Value, true, projectors, false); + if (property == null) + continue; + + exprSb.Append(@" + ").Append(property).Append(@","); + } + exprSb.Append(@" + }; + return expr.Expand(); + "); + + return (exprSb.ToString(), projectors); + } + + public static string? GeneratePropertyTo(GeneratedType classToGenerate, List otherClasses, string Key, ITypeSymbol FromType, ITypeSymbol ToType, bool useNullConditionalAccess = true) + { + var concreteToType = GetConcreteType(ToType); + var concreteFromType = GetConcreteType(FromType); + + bool isArray = !SymbolEqualityComparer.Default.Equals(FromType, concreteFromType); + + if (concreteFromType.IsAbstract) + return null; + + var sb = new StringBuilder(); + + if (isArray) + { + sb.Append(Key).Append("?.Any(); source."); + } + + sb.Append(Key).Append(@" ="); + + if (isArray) + { + sb.Append("(").Append(FromType).Append(@")(this.").Append(Key).Append(" != null ? "); + sb.Append("this.").Append(Key); + sb.Append(useNullConditionalAccess ? "?" : ""); + if (!SymbolEqualityComparer.Default.Equals(concreteToType, concreteFromType)) + { + sb.Append(@".Select(x => (").Append(GetFullString(concreteFromType)).Append(@")x.ApplyTo(providers, (").Append(GetFullString(concreteFromType)).Append(@")(AutoMapProperty.AutoMapPropertyUtils.Create<").Append(GetFullString(concreteFromType)).Append(",").Append(classToGenerate.SourceContainer.Identifier).Append(">(providers, source, \"").Append(Key).Append("\"))))"); + sb.Append(useNullConditionalAccess ? "?" : ""); + } + + //Add the appropriate ToHashSet or ToSet or ToCollection or ToList + if (FromType.Name.Contains("Set")) + sb.Append(@".ToHashSet()"); + else if (FromType.TypeKind == TypeKind.Array) + sb.Append(@".ToArray()"); + else + sb.Append(@".ToList()"); + + sb.Append(" : "); + if (FromType.Name.Contains("Set")) + sb.Append("new HashSet<").Append(GetFullString(concreteFromType)).Append(">()"); + else if (FromType.TypeKind == TypeKind.Array) + sb.Append("Array.Empty<").Append(GetFullString(concreteFromType)).Append(">()"); + else + sb.Append("new List<").Append(GetFullString(concreteFromType)).Append(">()"); + + sb.Append(")"); + } + else + { + if (otherClasses.Select(x => x.Name).Contains(ToType.Name)) + { + if (ToType.NullableAnnotation == NullableAnnotation.Annotated) + sb.Append("this.").Append(Key).Append(" == null ? null : "); + sb.Append("this.").Append(Key).Append(".ApplyTo(providers, (").Append(GetFullString(FromType)).Append(@")(source.").Append(Key).Append(" != null ? source.").Append(Key).Append(" : AutoMapProperty.AutoMapPropertyUtils.Create<").Append(GetFullString(FromType)).Append(",").Append(classToGenerate.SourceContainer.Identifier).Append(">(providers, source, \"").Append(Key).Append("\")))"); + } + else + sb.Append("(").Append(GetFullString(FromType)).Append(@")this.").Append(Key); + } + return sb.ToString(); + } + public static string? GenerateCorrectToProperty(GeneratedType classToGenerate, List otherClasses, string Key, MappableProperty Value, bool useNullConditionalAccess = true) + { + var retSb = new StringBuilder(); + retSb.Append("source."); + if (!Value.CustomTo) + { + if (Value.SourceIsReadOnly) + return null; + var generated = GeneratePropertyTo(classToGenerate, otherClasses, Key, Value.FromType, Value.ToType, useNullConditionalAccess); + if (generated == null) + return null; + retSb.Append(generated); + } + 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.SourceIsReadOnly) + { + 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, MappableProperty 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.NestedType != null) + { + var concreteType = GetConcreteType(Value.ToType); + //cannot process collections/arrays + if (SymbolEqualityComparer.Default.Equals(concreteType, Value.ToType)) + { + var childSb = new StringBuilder(); + + foreach (var prop in Value.NestedType.MappableProperties) + { + var op = GenerateEqualityOperator(prop.Key, prop.Value, prefixedName); + if (string.IsNullOrEmpty(op)) + { + continue; + } + childSb.Append(@" + ").Append(op).Append(" && "); + } + childSb.Append(@"true"); + if (Value.FromType.NullableAnnotation == NullableAnnotation.Annotated || + Value.ToType.NullableAnnotation == NullableAnnotation.Annotated) + { + return "(item1." + prefixedName + " == null ? item2." + prefixedName + + " == null : item2." + prefixedName + " != null && (" + childSb + "))"; + } + 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(); + } + +} diff --git a/AutoMapProperty/MappingModel.cs b/AutoMapProperty/MappingModel.cs new file mode 100644 index 0000000..74bc3cb --- /dev/null +++ b/AutoMapProperty/MappingModel.cs @@ -0,0 +1,66 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace AutoMapProperty; + +internal sealed class GeneratedType +{ + public TypeDeclarationSyntax SourceContainer { get; } + public List Usings { get; } + public string Namespace { get; } + public string Name { get; } + public string FullName => Namespace + "." + Name; + public List Properties { get; } = new(); + public Dictionary MappableProperties { get; set; } = new(); + + public GeneratedType(TypeDeclarationSyntax sourceContainer, string name, string namespaceName, List usings) + { + SourceContainer = sourceContainer; + Name = name; + Namespace = namespaceName; + Usings = usings; + } +} + +internal record struct GeneratedProperty( + PropertyDeclarationSyntax Declaration, + ITypeSymbol Type, + bool CustomFrom, + IMethodSymbol? CustomFromMethod, + bool CustomTo, + IMethodSymbol? CustomToMethod); + +internal record struct PropertyCandidate( + ITypeSymbol Type, + IPropertySymbol? Symbol = null, + bool CustomFrom = false, + IMethodSymbol? CustomFromMethod = null, + bool CustomTo = false, + IMethodSymbol? CustomToMethod = null); + +internal sealed class MappableProperty +{ + public ITypeSymbol FromType { get; } + public ITypeSymbol ToType { get; } + public IPropertySymbol FromProperty { get; } + public bool SourceIsReadOnly => FromProperty.SetMethod == null; + public GeneratedType? NestedType { get; } + public bool CustomFrom { get; } + public IMethodSymbol? CustomFromMethod { get; } + public bool CustomTo { get; } + public IMethodSymbol? CustomToMethod { get; } + + public MappableProperty(ITypeSymbol fromType, ITypeSymbol toType, IPropertySymbol fromProperty, + bool customFrom, IMethodSymbol? customFromMethod, bool customTo, IMethodSymbol? customToMethod, + GeneratedType? ourTargetType) + { + FromType = fromType; + ToType = toType; + FromProperty = fromProperty; + CustomFrom = customFrom; + CustomFromMethod = customFromMethod; + CustomTo = customTo; + CustomToMethod = customToMethod; + NestedType = ourTargetType; + } +} diff --git a/AutoMapProperty/MappingModelBuilder.cs b/AutoMapProperty/MappingModelBuilder.cs new file mode 100644 index 0000000..211c671 --- /dev/null +++ b/AutoMapProperty/MappingModelBuilder.cs @@ -0,0 +1,123 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static AutoMapProperty.TypeSymbolHelpers; + +namespace AutoMapProperty; + +internal static class MappingModelBuilder +{ + private static readonly DiagnosticDescriptor InvalidProperty = new( + "NEJ23", "Invalid mapped property", "Error processing {0}: {1}", "NEJ", DiagnosticSeverity.Error, true); + private static readonly DiagnosticDescriptor InvalidConverter = new( + "NEJ10", "Invalid mapping method", "Method {0} on {1} must exist and be static", "NEJ", DiagnosticSeverity.Error, true); + + public static List Build(Compilation compilation, IEnumerable properties, + SourceProductionContext context) + { + var types = new Dictionary(); + foreach (var property in properties.Distinct()) + { + context.CancellationToken.ThrowIfCancellationRequested(); + if (property.Parent is not TypeDeclarationSyntax container || + (container is not ClassDeclarationSyntax && container is not InterfaceDeclarationSyntax)) + continue; + + var semanticModel = compilation.GetSemanticModel(property.SyntaxTree); + if (semanticModel.GetDeclaredSymbol(container) is not INamedTypeSymbol sourceType || + semanticModel.GetDeclaredSymbol(property) is not IPropertySymbol sourceProperty) + continue; + + foreach (var attribute in sourceProperty.GetAttributes().Where(x => + x.AttributeClass?.ToDisplayString() == AutoMapPropertyGenerator.AttributeName)) + { + if (attribute.ConstructorArguments.Length == 0 || attribute.ConstructorArguments[0].Value is not string suffix) + continue; + + var name = sourceType.Name + suffix; + if (!types.TryGetValue(name, out var target)) + { + var namespaceName = sourceType.ContainingNamespace.ToDisplayString(); + var usings = property.SyntaxTree.GetCompilationUnitRoot().Usings.Select(x => x.ToString()).ToList(); + usings.Add("using " + namespaceName + ";"); + target = new GeneratedType(container, name, namespaceName, usings); + types.Add(name, target); + } + + try + { + var (declaration, type) = PropertyDeclarationBuilder.Build(compilation, property, sourceProperty, attribute); + var customTo = GetConverter(2); + var customFrom = GetConverter(3); + target.Properties.Add(new GeneratedProperty(declaration, type, + customFrom.Enabled, customFrom.Method, customTo.Enabled, customTo.Method)); + } + catch (ArgumentException exception) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidProperty, property.GetLocation(), sourceProperty.Name, exception.Message)); + } + + (bool Enabled, IMethodSymbol? Method) GetConverter(int index) + { + if (attribute.ConstructorArguments.Length <= index || attribute.ConstructorArguments[index].Value is not string methodName) + return (false, null); + var method = sourceType.GetMembers(methodName).OfType().FirstOrDefault(); + if (method == null || !method.IsStatic) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidConverter, property.GetLocation(), methodName, sourceType.Name)); + return (true, null); + } + return (true, method); + } + } + } + + foreach (var target in types.Values) + { + var source = (INamedTypeSymbol)compilation.GetSemanticModel(target.SourceContainer.SyntaxTree) + .GetDeclaredSymbol(target.SourceContainer)!; + var sourceProperties = GetProperties(source, types).GroupBy(x => x.Name).ToDictionary(x => x.Key, x => x.First().Info); + var existingTarget = compilation.GetTypeByMetadataName(target.FullName); + var targetProperties = existingTarget == null ? GetGeneratedProperties(target) : GetProperties(existingTarget, types); + + foreach (var candidate in targetProperties.GroupBy(x => x.Name).Select(x => x.First())) + { + if (!sourceProperties.TryGetValue(candidate.Name, out var from) || from.Symbol == null) + continue; + + var to = candidate.Info; + var concrete = GetConcreteType(to.Type); + var nested = types.Values.FirstOrDefault(x => x.FullName == GetFullString(concrete.WithNullableAnnotation(NullableAnnotation.NotAnnotated))); + if (nested == null && string.IsNullOrEmpty(concrete.ContainingNamespace?.Name)) + nested = types.Values.FirstOrDefault(x => x.Name == concrete.Name); + + target.MappableProperties[candidate.Name] = new MappableProperty(from.Type, to.Type, from.Symbol, + to.CustomFrom, to.CustomFromMethod, to.CustomTo, to.CustomToMethod, nested); + } + } + return types.Values.ToList(); + } + + private static IEnumerable<(string Name, PropertyCandidate Info)> GetProperties(INamedTypeSymbol type, + Dictionary generated) + { + foreach (var property in type.GetMembers().OfType()) + yield return (property.Name, new PropertyCandidate(property.Type, property)); + + if (generated.TryGetValue(type.Name, out var target)) + { + foreach (var property in GetGeneratedProperties(target)) + yield return property; + } + + if (type.BaseType != null) + { + foreach (var property in GetProperties(type.BaseType, generated)) + yield return property; + } + } + + private static IEnumerable<(string Name, PropertyCandidate Info)> GetGeneratedProperties(GeneratedType target) => + target.Properties.Select(x => (x.Declaration.Identifier.ValueText, + new PropertyCandidate(x.Type, null, x.CustomFrom, x.CustomFromMethod, x.CustomTo, x.CustomToMethod))); +} diff --git a/AutoMapProperty/PropertyDeclarationBuilder.cs b/AutoMapProperty/PropertyDeclarationBuilder.cs new file mode 100644 index 0000000..0bb990f --- /dev/null +++ b/AutoMapProperty/PropertyDeclarationBuilder.cs @@ -0,0 +1,110 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static AutoMapProperty.TypeSymbolHelpers; + +namespace AutoMapProperty; + +internal static class PropertyDeclarationBuilder +{ + public static (PropertyDeclarationSyntax Declaration, ITypeSymbol Type) Build( + Compilation compilation, PropertyDeclarationSyntax source, IPropertySymbol symbol, AttributeData attribute) + { + var type = attribute.ConstructorArguments.Length >= 2 && + attribute.ConstructorArguments[1].Value is ITypeSymbol replacement + ? replacement + : symbol.Type; + + var nullability = attribute.NamedArguments.FirstOrDefault(x => x.Key == "Nullability").Value.Value; + type = ApplyNullability(compilation, type, nullability is int value ? value : 0); + + var declaration = source.WithAttributeLists(FilterAttributes(compilation, source.AttributeLists)); + if (!SymbolEqualityComparer.IncludeNullability.Equals(type, symbol.Type)) + { + declaration = declaration.WithType(SyntaxFactory.ParseTypeName(GetFullString(type)).WithTriviaFrom(source.Type)); + var oldType = GetConcreteType(UnwrapNullableValueType(symbol.Type)); + var newType = GetConcreteType(UnwrapNullableValueType(type)); + if (declaration.Initializer != null && !SymbolEqualityComparer.Default.Equals(oldType, newType)) + { + declaration = declaration.WithInitializer((EqualsValueClauseSyntax?) + new TypeReplacer(oldType.Name, newType.Name).Visit(declaration.Initializer)); + } + } + + if (declaration.Initializer != null && type.IsValueType && + !SymbolEqualityComparer.Default.Equals(symbol.Type, UnwrapNullableValueType(symbol.Type)) && + SymbolEqualityComparer.Default.Equals(type, UnwrapNullableValueType(type))) + { + var nullableType = SyntaxFactory.NullableType(SyntaxFactory.ParseTypeName(GetFullString(type))); + var initializer = SyntaxFactory.ParenthesizedExpression(SyntaxFactory.CastExpression(nullableType, + SyntaxFactory.ParenthesizedExpression(declaration.Initializer.Value))); + declaration = declaration.WithInitializer(declaration.Initializer.WithValue(SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, initializer, + SyntaxFactory.IdentifierName("GetValueOrDefault"))))); + } + + declaration = declaration + .WithModifiers(SyntaxFactory.TokenList(source.Modifiers.Where(x => + !x.IsKind(SyntaxKind.ReadOnlyKeyword) && !x.IsKind(SyntaxKind.AbstractKeyword)))) + .WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List(new[] + { + SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)), + SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)) + }))) + .WithExpressionBody(null) + .WithSemicolonToken(SyntaxFactory.Token(declaration.Initializer == null ? SyntaxKind.None : SyntaxKind.SemicolonToken)); + + return (declaration.NormalizeWhitespace(), type); + } + + private static ITypeSymbol ApplyNullability(Compilation compilation, ITypeSymbol type, int nullability) + { + // Attribute enums are read by value so the analyzer does not depend on the runtime helper assembly. + if (nullability == 0) + return type; + if (nullability != 1 && nullability != 2) + throw new ArgumentException("Nullability must be Preserve, Nullable or NonNullable."); + + if (type is INamedTypeSymbol named && named.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + return nullability == 1 ? type : named.TypeArguments[0]; + + if (type.IsValueType && nullability == 1) + return compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(type); + + return type.WithNullableAnnotation(nullability == 1 ? NullableAnnotation.Annotated : NullableAnnotation.NotAnnotated); + } + + private static SyntaxList FilterAttributes(Compilation compilation, SyntaxList lists) + { + var result = new List(); + foreach (var list in lists) + { + var attributes = list.Attributes.Where(attribute => + compilation.GetSemanticModel(attribute.SyntaxTree).GetSymbolInfo(attribute).Symbol is not IMethodSymbol method || + method.ContainingType.ToDisplayString() != AutoMapPropertyGenerator.AttributeName); + var filtered = SyntaxFactory.SeparatedList(attributes); + if (filtered.Count > 0) + result.Add(list.WithAttributes(filtered)); + } + return SyntaxFactory.List(result); + } + + private sealed class TypeReplacer : CSharpSyntaxRewriter + { + private readonly string oldName; + private readonly string newName; + + public TypeReplacer(string oldName, string newName) + { + this.oldName = oldName; + this.newName = newName; + } + + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) => + node.Identifier.ValueText == oldName + ? SyntaxFactory.IdentifierName(newName).WithTriviaFrom(node) + : base.VisitIdentifierName(node); + } +} diff --git a/AutoMapProperty/TypeSymbolHelpers.cs b/AutoMapProperty/TypeSymbolHelpers.cs new file mode 100644 index 0000000..1ac63bd --- /dev/null +++ b/AutoMapProperty/TypeSymbolHelpers.cs @@ -0,0 +1,30 @@ +using Microsoft.CodeAnalysis; + +namespace AutoMapProperty; + +internal static class TypeSymbolHelpers +{ + private static readonly SymbolDisplayFormat DisplayFormat = new( + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + + public static string GetFullString(ITypeSymbol symbol) => symbol.ToDisplayString(DisplayFormat); + + public static ITypeSymbol UnwrapNullableValueType(ITypeSymbol symbol) => + symbol is INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullable + ? nullable.TypeArguments[0] + : symbol; + + public static ITypeSymbol GetConcreteType(ITypeSymbol symbol) + { + if (symbol is IArrayTypeSymbol array) + return GetConcreteType(array.ElementType); + + if (symbol is INamedTypeSymbol named && named.IsGenericType && + (named.Name == "ICollection" || named.Name == "IEnumerable")) + return GetConcreteType(named.TypeArguments[0]); + + return symbol; + } +} diff --git a/AutoMapPropertyHelper/AutoMapNullability.cs b/AutoMapPropertyHelper/AutoMapNullability.cs new file mode 100644 index 0000000..af14851 --- /dev/null +++ b/AutoMapPropertyHelper/AutoMapNullability.cs @@ -0,0 +1,12 @@ +namespace AutoMapPropertyHelper +{ + public enum AutoMapNullability + { + /// Keep the source type, or the explicitly supplied replacement type, unchanged. + Preserve, + /// Add a nullable annotation or wrap a value type in Nullable<T>. + Nullable, + /// Remove a nullable annotation or unwrap Nullable<T>. + NonNullable + } +} diff --git a/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs b/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs index 47e85e9..97274ee 100644 --- a/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs +++ b/AutoMapPropertyHelper/AutoMapPropertyAttribute.cs @@ -5,6 +5,12 @@ namespace AutoMapPropertyHelper [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] public class AutoMapPropertyAttribute : System.Attribute { + /// + /// Overrides the top-level nullability of the generated property after selecting its type. + /// Collection element and generic argument nullability are unchanged. + /// + public AutoMapNullability Nullability { get; set; } = AutoMapNullability.Preserve; + public AutoMapPropertyAttribute(string name) { }