Initial work to add records support.

This commit is contained in:
Mohammadreza Taikandi 2021-06-29 07:15:17 +01:00
parent 98c899e078
commit 91f5e9bcf5
6 changed files with 61 additions and 59 deletions

View File

@ -28,11 +28,11 @@ namespace MapTo.Extensions
public static CompilationUnitSyntax GetCompilationUnit(this SyntaxNode syntaxNode) => syntaxNode.Ancestors().OfType<CompilationUnitSyntax>().Single();
public static string GetClassName(this ClassDeclarationSyntax classSyntax) => classSyntax.Identifier.Text;
public static string GetIdentifierName(this TypeDeclarationSyntax typeSyntax) => typeSyntax.Identifier.Text;
public static AttributeSyntax? GetAttribute(this ClassDeclarationSyntax classSyntax, string attributeName)
public static AttributeSyntax? GetAttribute(this TypeDeclarationSyntax typeDeclarationSyntax, string attributeName)
{
return classSyntax.AttributeLists
return typeDeclarationSyntax.AttributeLists
.SelectMany(al => al.Attributes)
.SingleOrDefault(a =>
(a.Name as IdentifierNameSyntax)?.Identifier.ValueText == attributeName ||
@ -48,8 +48,8 @@ namespace MapTo.Extensions
public static AttributeData? GetAttribute(this ISymbol symbol, ITypeSymbol attributeSymbol) =>
symbol.GetAttributes(attributeSymbol).FirstOrDefault();
public static string? GetNamespace(this ClassDeclarationSyntax classDeclarationSyntax) =>
classDeclarationSyntax.Ancestors()
public static string? GetNamespace(this TypeDeclarationSyntax typeDeclarationSyntax) =>
typeDeclarationSyntax.Ancestors()
.OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault()
?.Name

View File

@ -35,9 +35,9 @@ namespace MapTo
.AddSource(ref context, MapPropertyAttributeSource.Generate(options))
.AddSource(ref context, MappingContextSource.Generate(options));
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateClasses.Any())
if (context.SyntaxReceiver is MapToSyntaxReceiver receiver && receiver.CandidateTypes.Any())
{
AddGeneratedMappingsClasses(context, compilation, receiver.CandidateClasses, options);
AddGeneratedMappingsClasses(context, compilation, receiver.CandidateTypes, options);
}
}
catch (Exception ex)
@ -47,7 +47,7 @@ namespace MapTo
}
}
private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable<ClassDeclarationSyntax> candidateClasses, SourceGenerationOptions options)
private static void AddGeneratedMappingsClasses(GeneratorExecutionContext context, Compilation compilation, IEnumerable<TypeDeclarationSyntax> candidateClasses, SourceGenerationOptions options)
{
foreach (var classSyntax in candidateClasses)
{

View File

@ -8,12 +8,12 @@ namespace MapTo
{
internal class MapToSyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();
public List<TypeDeclarationSyntax> CandidateTypes { get; } = new();
/// <inheritdoc />
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is not ClassDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } classDeclaration)
if (syntaxNode is not TypeDeclarationSyntax { AttributeLists: { Count: >= 1 } attributes } typeDeclarationSyntax)
{
return;
}
@ -21,7 +21,7 @@ namespace MapTo
var attributeSyntax = attributes
.SelectMany(a => a.Attributes)
.SingleOrDefault(a => a.Name is
IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom]
IdentifierNameSyntax { Identifier: { ValueText: MapFromAttributeSource.AttributeName } } // For: [MapFrom]
or
QualifiedNameSyntax // For: [MapTo.MapFrom]
{
@ -32,7 +32,7 @@ namespace MapTo
if (attributeSyntax is not null)
{
CandidateClasses.Add(classDeclaration);
CandidateTypes.Add(typeDeclarationSyntax);
}
}
}

View File

@ -11,7 +11,7 @@ namespace MapTo
{
internal class MappingContext
{
private readonly ClassDeclarationSyntax _classSyntax;
private readonly TypeDeclarationSyntax _typeSyntax;
private readonly Compilation _compilation;
private readonly List<Diagnostic> _diagnostics;
private readonly INamedTypeSymbol _ignorePropertyAttributeTypeSymbol;
@ -23,12 +23,12 @@ namespace MapTo
private readonly INamedTypeSymbol _typeConverterInterfaceTypeSymbol;
private readonly List<string> _usings;
internal MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, ClassDeclarationSyntax classSyntax)
internal MappingContext(Compilation compilation, SourceGenerationOptions sourceGenerationOptions, TypeDeclarationSyntax typeSyntax)
{
_diagnostics = new List<Diagnostic>();
_usings = new List<string> { "System", Constants.RootNamespace };
_sourceGenerationOptions = sourceGenerationOptions;
_classSyntax = classSyntax;
_typeSyntax = typeSyntax;
_compilation = compilation;
_ignorePropertyAttributeTypeSymbol = compilation.GetTypeByMetadataNameOrThrow(IgnorePropertyAttributeSource.FullyQualifiedName);
@ -49,29 +49,29 @@ namespace MapTo
private void Initialize()
{
var semanticModel = _compilation.GetSemanticModel(_classSyntax.SyntaxTree);
if (!(ModelExtensions.GetDeclaredSymbol(semanticModel, _classSyntax) is INamedTypeSymbol classTypeSymbol))
var semanticModel = _compilation.GetSemanticModel(_typeSyntax.SyntaxTree);
if (ModelExtensions.GetDeclaredSymbol(semanticModel, _typeSyntax) is not INamedTypeSymbol classTypeSymbol)
{
_diagnostics.Add(DiagnosticsFactory.TypeNotFoundError(_classSyntax.GetLocation(), _classSyntax.Identifier.ValueText));
_diagnostics.Add(DiagnosticsFactory.TypeNotFoundError(_typeSyntax.GetLocation(), _typeSyntax.Identifier.ValueText));
return;
}
var sourceTypeSymbol = GetSourceTypeSymbol(_classSyntax, semanticModel);
var sourceTypeSymbol = GetSourceTypeSymbol(_typeSyntax, semanticModel);
if (sourceTypeSymbol is null)
{
_diagnostics.Add(DiagnosticsFactory.MapFromAttributeNotFoundError(_classSyntax.GetLocation()));
_diagnostics.Add(DiagnosticsFactory.MapFromAttributeNotFoundError(_typeSyntax.GetLocation()));
return;
}
var className = _classSyntax.GetClassName();
var sourceClassName = sourceTypeSymbol.Name;
var isClassInheritFromMappedBaseClass = IsClassInheritFromMappedBaseClass(semanticModel);
var typeIdentifierName = _typeSyntax.GetIdentifierName();
var sourceTypeIdentifierName = sourceTypeSymbol.Name;
var isTypeInheritFromMappedBaseClass = IsTypeInheritFromMappedBaseClass(semanticModel);
var shouldGenerateSecondaryConstructor = ShouldGenerateSecondaryConstructor(semanticModel, sourceTypeSymbol);
var mappedProperties = GetMappedProperties(classTypeSymbol, sourceTypeSymbol, isClassInheritFromMappedBaseClass);
var mappedProperties = GetMappedProperties(classTypeSymbol, sourceTypeSymbol, isTypeInheritFromMappedBaseClass);
if (!mappedProperties.Any())
{
_diagnostics.Add(DiagnosticsFactory.NoMatchingPropertyFoundError(_classSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol));
_diagnostics.Add(DiagnosticsFactory.NoMatchingPropertyFoundError(_typeSyntax.GetLocation(), classTypeSymbol, sourceTypeSymbol));
return;
}
@ -80,21 +80,22 @@ namespace MapTo
Model = new MappingModel(
_sourceGenerationOptions,
_classSyntax.GetNamespace(),
_classSyntax.Modifiers,
className,
_typeSyntax.GetNamespace(),
_typeSyntax.Modifiers,
_typeSyntax.Keyword.Text,
typeIdentifierName,
sourceTypeSymbol.ContainingNamespace.ToString(),
sourceClassName,
sourceTypeIdentifierName,
sourceTypeSymbol.ToString(),
mappedProperties.ToImmutableArray(),
isClassInheritFromMappedBaseClass,
isTypeInheritFromMappedBaseClass,
_usings.ToImmutableArray(),
shouldGenerateSecondaryConstructor);
}
private bool ShouldGenerateSecondaryConstructor(SemanticModel semanticModel, ISymbol sourceTypeSymbol)
{
var constructorSyntax = _classSyntax.DescendantNodes()
var constructorSyntax = _typeSyntax.DescendantNodes()
.OfType<ConstructorDeclarationSyntax>()
.SingleOrDefault(c =>
c.ParameterList.Parameters.Count == 1 &&
@ -116,18 +117,18 @@ namespace MapTo
return false;
}
private bool IsClassInheritFromMappedBaseClass(SemanticModel semanticModel)
private bool IsTypeInheritFromMappedBaseClass(SemanticModel semanticModel)
{
return _classSyntax.BaseList is not null && _classSyntax.BaseList.Types
return _typeSyntax.BaseList is not null && _typeSyntax.BaseList.Types
.Select(t => ModelExtensions.GetTypeInfo(semanticModel, t.Type).Type)
.Any(t => t?.GetAttribute(_mapFromAttributeTypeSymbol) != null);
}
private ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol classSymbol, ITypeSymbol sourceTypeSymbol, bool isClassInheritFromMappedBaseClass)
private ImmutableArray<MappedProperty> GetMappedProperties(ITypeSymbol typeSymbol, ITypeSymbol sourceTypeSymbol, bool isClassInheritFromMappedBaseClass)
{
var mappedProperties = new List<MappedProperty>();
var sourceProperties = sourceTypeSymbol.GetAllMembers().OfType<IPropertySymbol>().ToArray();
var classProperties = classSymbol.GetAllMembers(!isClassInheritFromMappedBaseClass)
var classProperties = typeSymbol.GetAllMembers(!isClassInheritFromMappedBaseClass)
.OfType<IPropertySymbol>()
.Where(p => !p.HasAttribute(_ignorePropertyAttributeTypeSymbol));
@ -207,7 +208,7 @@ namespace MapTo
private void AddUsingIfRequired(bool condition, string? ns)
{
if (condition && ns is not null && ns != _classSyntax.GetNamespace() && !_usings.Contains(ns))
if (condition && ns is not null && ns != _typeSyntax.GetNamespace() && !_usings.Contains(ns))
{
_usings.Add(ns);
}
@ -270,8 +271,8 @@ namespace MapTo
: converterParameter.Values.Where(v => v.Value is not null).Select(v => v.Value!.ToSourceCodeString()).ToImmutableArray();
}
private INamedTypeSymbol? GetSourceTypeSymbol(ClassDeclarationSyntax classDeclarationSyntax, SemanticModel? semanticModel = null) =>
GetSourceTypeSymbol(classDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel);
private INamedTypeSymbol? GetSourceTypeSymbol(TypeDeclarationSyntax typeDeclarationSyntax, SemanticModel? semanticModel = null) =>
GetSourceTypeSymbol(typeDeclarationSyntax.GetAttribute(MapFromAttributeSource.AttributeName), semanticModel);
private INamedTypeSymbol? GetSourceTypeSymbol(AttributeSyntax? attributeSyntax, SemanticModel? semanticModel = null)
{

View File

@ -22,11 +22,12 @@ namespace MapTo
internal record MappingModel (
SourceGenerationOptions Options,
string? Namespace,
SyntaxTokenList ClassModifiers,
string ClassName,
SyntaxTokenList Modifiers,
string Type,
string TypeIdentifierName,
string SourceNamespace,
string SourceClassName,
string SourceClassFullName,
string SourceTypeIdentifierName,
string SourceTypeFullName,
ImmutableArray<MappedProperty> MappedProperties,
bool HasMappedBaseClass,
ImmutableArray<string> Usings,

View File

@ -18,7 +18,7 @@ namespace MapTo.Sources
.WriteLine()
// Class declaration
.WriteLine($"partial class {model.ClassName}")
.WriteLine($"partial class {model.TypeIdentifierName}")
.WriteOpeningBracket();
// Class body
@ -44,37 +44,37 @@ namespace MapTo.Sources
// End namespace declaration
.WriteClosingBracket();
return new(builder.ToString(), $"{model.ClassName}.g.cs");
return new(builder.ToString(), $"{model.TypeIdentifierName}.g.cs");
}
private static SourceBuilder GenerateSecondaryConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
if (model.Options.GenerateXmlDocument)
{
builder
.WriteLine("/// <summary>")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.ClassName}\"/> class")
.WriteLine($"/// Initializes a new instance of the <see cref=\"{model.TypeIdentifierName}\"/> class")
.WriteLine($"/// using the property values from the specified <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <exception cref=\"ArgumentNullException\">{sourceClassParameterName} is null</exception>");
}
return builder
.WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.ClassName}({model.SourceClassName} {sourceClassParameterName})")
.WriteLine($"{model.Options.ConstructorAccessModifier.ToLowercaseString()} {model.TypeIdentifierName}({model.SourceTypeIdentifierName} {sourceClassParameterName})")
.WriteLine($" : this(new {MappingContextSource.ClassName}(), {sourceClassParameterName}) {{ }}");
}
private static SourceBuilder GeneratePrivateConstructor(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
const string mappingContextParameterName = "context";
var baseConstructor = model.HasMappedBaseClass ? $" : base({mappingContextParameterName}, {sourceClassParameterName})" : string.Empty;
builder
.WriteLine($"private protected {model.ClassName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceClassName} {sourceClassParameterName}){baseConstructor}")
.WriteLine($"private protected {model.TypeIdentifierName}({MappingContextSource.ClassName} {mappingContextParameterName}, {model.SourceTypeIdentifierName} {sourceClassParameterName}){baseConstructor}")
.WriteOpeningBracket()
.WriteLine($"if ({mappingContextParameterName} == null) throw new ArgumentNullException(nameof({mappingContextParameterName}));")
.WriteLine($"if ({sourceClassParameterName} == null) throw new ArgumentNullException(nameof({sourceClassParameterName}));")
@ -113,14 +113,14 @@ namespace MapTo.Sources
private static SourceBuilder GenerateFactoryMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName}{model.Options.NullableReferenceSyntax} From({model.SourceClassName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} From({model.SourceTypeIdentifierName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{model.SourceClassName}, {model.ClassName}>({sourceClassParameterName});")
.WriteLine($"return {sourceClassParameterName} == null ? null : {MappingContextSource.ClassName}.{MappingContextSource.FactoryMethodName}<{model.SourceTypeIdentifierName}, {model.TypeIdentifierName}>({sourceClassParameterName});")
.WriteClosingBracket();
}
@ -133,17 +133,17 @@ namespace MapTo.Sources
return builder
.WriteLine("/// <summary>")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.ClassName}\"/> and sets its participating properties")
.WriteLine($"/// Creates a new instance of <see cref=\"{model.TypeIdentifierName}\"/> and sets its participating properties")
.WriteLine($"/// using the property values from <paramref name=\"{sourceClassParameterName}\"/>.")
.WriteLine("/// </summary>")
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceClassName}\"/> to use as source.</param>")
.WriteLine($"/// <returns>A new instance of <see cred=\"{model.ClassName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
.WriteLine($"/// <param name=\"{sourceClassParameterName}\">The instance of <see cref=\"{model.SourceTypeIdentifierName}\"/> to use as source.</param>")
.WriteLine($"/// <returns>A new instance of <see cred=\"{model.TypeIdentifierName}\"/> -or- <c>null</c> if <paramref name=\"{sourceClassParameterName}\"/> is <c>null</c>.</returns>");
}
private static SourceBuilder GenerateSourceTypeExtensionClass(this SourceBuilder builder, MappingModel model)
{
return builder
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceClassName}To{model.ClassName}Extensions")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static partial class {model.SourceTypeIdentifierName}To{model.TypeIdentifierName}Extensions")
.WriteOpeningBracket()
.GenerateSourceTypeExtensionMethod(model)
.WriteClosingBracket();
@ -151,14 +151,14 @@ namespace MapTo.Sources
private static SourceBuilder GenerateSourceTypeExtensionMethod(this SourceBuilder builder, MappingModel model)
{
var sourceClassParameterName = model.SourceClassName.ToCamelCase();
var sourceClassParameterName = model.SourceTypeIdentifierName.ToCamelCase();
return builder
.GenerateConvertorMethodsXmlDocs(model, sourceClassParameterName)
.WriteLineIf(model.Options.SupportNullableStaticAnalysis, $"[return: NotNullIfNotNull(\"{sourceClassParameterName}\")]")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.ClassName}{model.Options.NullableReferenceSyntax} To{model.ClassName}(this {model.SourceClassName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteLine($"{model.Options.GeneratedMethodsAccessModifier.ToLowercaseString()} static {model.TypeIdentifierName}{model.Options.NullableReferenceSyntax} To{model.TypeIdentifierName}(this {model.SourceTypeIdentifierName}{model.Options.NullableReferenceSyntax} {sourceClassParameterName})")
.WriteOpeningBracket()
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.ClassName}({sourceClassParameterName});")
.WriteLine($"return {sourceClassParameterName} == null ? null : new {model.TypeIdentifierName}({sourceClassParameterName});")
.WriteClosingBracket();
}
}