chore: initial commit
Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
namespace MapperSourceGen.SourceGenerator.Extensions;
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="ITypeSymbol" />.
|
||||
/// </summary>
|
||||
internal static class TypeSymbolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an <see cref="ITypeSymbol" /> to a <see cref="TypeSyntax" />.
|
||||
/// </summary>
|
||||
/// <param name="source">The source <see cref="ITypeSymbol" /> instance to convert.</param>
|
||||
/// <returns>An instance of <see cref="TypeSyntax" />.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static TypeSyntax ToTypeSyntax(this ITypeSymbol source)
|
||||
{
|
||||
string typeName = source.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
|
||||
return SyntaxFactory.ParseTypeName(typeName);
|
||||
}
|
||||
}
|
||||
215
src/MapperSourceGen.SourceGenerator/MapperGenerator.cs
Normal file
215
src/MapperSourceGen.SourceGenerator/MapperGenerator.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
namespace MapperSourceGen.SourceGenerator;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Model;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Incremental source generator that is responsible for generating domain transfer objects (DTOs) and mapper from a
|
||||
/// class decorated with the <c>MapperAttribute</c>.
|
||||
/// </summary>
|
||||
[Generator]
|
||||
public sealed class MapperGenerator : IIncrementalGenerator
|
||||
{
|
||||
private const string MapIgnoreAttributeName = "MapperSourceGen.MapIgnoreAttribute";
|
||||
|
||||
private const string MapperAttributeName = "MapperSourceGen.MapperAttribute";
|
||||
|
||||
private const string Source = "source";
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// This configures the generator pipeline to look for classes decorated with the MapperAttribute which will then extract
|
||||
// the hierarchy information and properties to generate DTOs and mapping methods.
|
||||
IncrementalValuesProvider<(HierarchyInfo Hierarchy, ImmutableArray<PropertyInfo> Properties)> items =
|
||||
context.SyntaxProvider
|
||||
.ForAttributeWithMetadataName(MapperAttributeName,
|
||||
static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
|
||||
static (ctx, _) =>
|
||||
{
|
||||
if ( ctx.TargetSymbol is not INamedTypeSymbol classSymbol )
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
HierarchyInfo hierarchy = HierarchyInfo.From(classSymbol);
|
||||
ImmutableArray<PropertyInfo> properties =
|
||||
[
|
||||
..classSymbol.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(w => !w.IsIndexer
|
||||
&& w is { IsAbstract: false, DeclaredAccessibility: Accessibility.Public }
|
||||
&& !w.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == MapIgnoreAttributeName))
|
||||
.Select(s => new PropertyInfo(s))
|
||||
];
|
||||
|
||||
|
||||
return ( hierarchy, properties );
|
||||
});
|
||||
|
||||
// This registers the source output for generating the domain transfer object (DTO).
|
||||
context.RegisterSourceOutput(items,
|
||||
static (ctx, item) =>
|
||||
{
|
||||
ImmutableArray<MemberDeclarationSyntax> properties = [..GenerateAutoProperties(item.Properties)];
|
||||
|
||||
CompilationUnitSyntax? compilationUnit = item.Hierarchy.GetCompilationUnit(properties, suffix: "Dto");
|
||||
if ( compilationUnit is not null )
|
||||
{
|
||||
ctx.AddSource($"{item.Hierarchy.FileNameHint}Dto.g", compilationUnit.GetText(Encoding.UTF8));
|
||||
}
|
||||
});
|
||||
|
||||
// This registers the source output for generating the mapper class with mapping methods.
|
||||
context.RegisterSourceOutput(items,
|
||||
static (ctx, item) =>
|
||||
{
|
||||
ImmutableArray<MemberDeclarationSyntax> declarations =
|
||||
[
|
||||
GenerateMapFromMethod(item.Hierarchy, item.Properties),
|
||||
GenerateMapToMethod(item.Hierarchy, item.Properties)
|
||||
];
|
||||
|
||||
CompilationUnitSyntax? compilationUnit = item.Hierarchy.GetCompilationUnit(declarations, suffix: "Mapper");
|
||||
if ( compilationUnit is not null )
|
||||
{
|
||||
ctx.AddSource($"{item.Hierarchy.FileNameHint}Mapper.g", compilationUnit.GetText(Encoding.UTF8));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an expression statement that validates the argument for null.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An expression statement that will output <code>ArgumentNullException.ThrowIfNull(nameof(source));</code>
|
||||
/// </returns>
|
||||
private static ExpressionStatementSyntax GenerateArgumentValidator()
|
||||
{
|
||||
MemberAccessExpressionSyntax member = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName("ArgumentNullException"),
|
||||
IdentifierName("ThrowIfNull"));
|
||||
|
||||
IdentifierNameSyntax nameofIdentifier = IdentifierName(Identifier(TriviaList(),
|
||||
SyntaxKind.NameOfKeyword,
|
||||
"nameof",
|
||||
"nameof",
|
||||
TriviaList()));
|
||||
|
||||
ArgumentSyntax argument = Argument(InvocationExpression(nameofIdentifier)
|
||||
.WithArgumentList(ArgumentList(SingletonSeparatedList(Argument(IdentifierName(Source))))));
|
||||
|
||||
return ExpressionStatement(InvocationExpression(member)
|
||||
.WithArgumentList(ArgumentList(SingletonSeparatedList(argument))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates auto properties used in the domain transfer object (DTO).
|
||||
/// </summary>
|
||||
/// <param name="properties">An <see cref="IEnumerable{T}" /> collection of properties to generate auto-properties for.</param>
|
||||
/// <returns>
|
||||
/// An <see cref="IEnumerable{T}" /> of <see cref="MemberDeclarationSyntax" /> items that will output auto-properties:
|
||||
/// <code>
|
||||
/// public string Name { get; set;
|
||||
/// </code>
|
||||
/// </returns>
|
||||
private static IEnumerable<MemberDeclarationSyntax> GenerateAutoProperties(IEnumerable<PropertyInfo> properties)
|
||||
{
|
||||
return properties.Select(property => PropertyDeclaration(property.PropertyType.ToTypeSyntax(), property.TargetPropertyName)
|
||||
.AddModifiers(Token(SyntaxKind.PublicKeyword))
|
||||
.AddAccessorListAccessors(
|
||||
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
|
||||
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
|
||||
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
|
||||
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
|
||||
));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a method that maps a DTO to a model.
|
||||
/// </summary>
|
||||
/// <param name="hierarchy">An <see cref="HierarchyInfo" /> instance that contains information about the model.</param>
|
||||
/// <param name="properties">An <see cref="IEnumerable{T}" /> of properties to be used in mapping the DTO to the model.</param>
|
||||
/// <returns>
|
||||
/// An instance of <see cref="MemberDeclarationSyntax" /> containing the method that returns an instance of the
|
||||
/// model.
|
||||
/// </returns>
|
||||
/// <remarks>Properties that are decorated with the <c>MapIgnoreAttribute</c> will not be included in the mapping.</remarks>
|
||||
private static MethodDeclarationSyntax GenerateMapFromMethod(HierarchyInfo hierarchy, ImmutableArray<PropertyInfo> properties)
|
||||
{
|
||||
ParameterSyntax parameter = Parameter(Identifier(Source))
|
||||
.WithType(ParseTypeName($"{hierarchy.FileNameHint}Dto"));
|
||||
|
||||
ReturnStatementSyntax returnStatement = ReturnStatement(GenerateMappingMethod(hierarchy, properties, isSource: false));
|
||||
|
||||
return MethodDeclaration(ParseTypeName($"{hierarchy.FileNameHint}"), "ToModel")
|
||||
.AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))
|
||||
.AddParameterListParameters(parameter)
|
||||
.WithBody(Block(GenerateArgumentValidator(), returnStatement));
|
||||
}
|
||||
|
||||
private static ObjectCreationExpressionSyntax GenerateMappingMethod(HierarchyInfo hierarchy,
|
||||
ImmutableArray<PropertyInfo> properties,
|
||||
string? suffix = null,
|
||||
bool isSource = true)
|
||||
{
|
||||
return ObjectCreationExpression(IdentifierName(ParseTypeName($"{hierarchy.FileNameHint}{suffix}").ToString()))
|
||||
.WithArgumentList(ArgumentList())
|
||||
.WithInitializer(InitializerExpression(SyntaxKind.ObjectInitializerExpression,
|
||||
SeparatedList<ExpressionSyntax>(GeneratePropertyAssignments(properties, isSource))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a method that maps a model to a DTO.
|
||||
/// </summary>
|
||||
/// <param name="hierarchy">An <see cref="HierarchyInfo" /> instance that contains information about the model.</param>
|
||||
/// <param name="properties">An <see cref="IEnumerable{T}" /> of properties to be used in mapping the model to the DTO.</param>
|
||||
/// <returns>
|
||||
/// An instance of <see cref="MemberDeclarationSyntax" /> containing the method that returns an instance of the
|
||||
/// DTO.
|
||||
/// </returns>
|
||||
/// <remarks>Properties that are decorated with the <c>MapIgnoreAttribute</c> will not be included in the mapping.</remarks>
|
||||
private static MethodDeclarationSyntax GenerateMapToMethod(HierarchyInfo hierarchy, ImmutableArray<PropertyInfo> properties)
|
||||
{
|
||||
ParameterSyntax parameter = Parameter(Identifier(Source))
|
||||
.WithType(ParseTypeName(hierarchy.FileNameHint));
|
||||
|
||||
ReturnStatementSyntax returnStatement = ReturnStatement(GenerateMappingMethod(hierarchy, properties, "Dto"));
|
||||
|
||||
return MethodDeclaration(ParseTypeName($"{hierarchy.FileNameHint}Dto"), "ToDto")
|
||||
.AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))
|
||||
.AddParameterListParameters(parameter)
|
||||
.WithBody(Block(GenerateArgumentValidator(), returnStatement));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates property assignments for the object initializer in the mapping methods.
|
||||
/// </summary>
|
||||
/// <param name="properties">An <see cref="IEnumerable{T}" /> of properties that are to be included in the initializer.</param>
|
||||
/// <param name="isSource">
|
||||
/// Determines whether the <c>MapAliasAttribute</c> is to be used on the left or right side of
|
||||
/// assignment.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// An <see cref="IEnumerable{T}" /> containing <see cref="SyntaxNodeOrToken" /> instances for property
|
||||
/// assignment.
|
||||
/// </returns>
|
||||
private static IEnumerable<SyntaxNodeOrToken> GeneratePropertyAssignments(IEnumerable<PropertyInfo> properties, bool isSource = true)
|
||||
{
|
||||
return properties.SelectMany(s => new SyntaxNodeOrToken[]
|
||||
{
|
||||
AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
|
||||
IdentifierName(!isSource ? s.SourcePropertyName : s.TargetPropertyName),
|
||||
MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
|
||||
IdentifierName(Source),
|
||||
IdentifierName(isSource ? s.SourcePropertyName : s.TargetPropertyName))),
|
||||
Token(SyntaxKind.CommaToken)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
Source generator projects must target .NET Standard 2.0 as the Roslyn SDK Compiler only supports this version
|
||||
for compiler addons.
|
||||
-->
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- AnalyzerLanguage is set to cs to denote that only C# is supported by the source generator. -->
|
||||
<AnalyzerLanguage>cs</AnalyzerLanguage>
|
||||
|
||||
<!-- EmitCompilerGeneratedFiles is set to true to ensure that the source generator can emit artifacts. -->
|
||||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||
|
||||
<!-- EnforceExtendedAnalyzerRules is set to true to enable additional rules for analyzers. -->
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
|
||||
<!-- IsRoslynComponent is set to true to indicate that this project is a Roslyn component. -->
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Microsoft.CodeAnalysis.CSharp is required for C# source generators. -->
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,71 @@
|
||||
// This file is ported and adapted from CommunityToolkit.Mvvm (CommunityToolkit/dotnet)
|
||||
|
||||
namespace MapperSourceGen.SourceGenerator.Model;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
internal partial class HierarchyInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a <see cref="CompilationUnitSyntax" /> representing the type hierarchy with the specified member declarations.
|
||||
/// </summary>
|
||||
/// <param name="memberDeclarations">
|
||||
/// The <see cref="MemberDeclarationSyntax" /> instances to include in the
|
||||
/// <see cref="CompilationUnitSyntax" />.
|
||||
/// </param>
|
||||
/// <param name="prefix">The prefix to prepend to the file and class name.</param>
|
||||
/// <param name="suffix">The suffix to append to the file and class name.</param>
|
||||
/// <returns>An instance of <see cref="CompilationUnitSyntax" /> used for generating the final output.</returns>
|
||||
public CompilationUnitSyntax GetCompilationUnit(ImmutableArray<MemberDeclarationSyntax> memberDeclarations, string? prefix = null, string? suffix = null)
|
||||
{
|
||||
TypeDeclarationSyntax typeDeclarationSyntax =
|
||||
Hierarchy[0].GetSyntax(prefix, suffix)
|
||||
.AddModifiers(Token(TriviaList(Comment("/// <inheritdoc/>")),
|
||||
Hierarchy[0].AccessibilityKind,
|
||||
TriviaList()))
|
||||
.AddModifiers(GetKeywordModifierTokens(Hierarchy[0]))
|
||||
.AddMembers([.. memberDeclarations])
|
||||
.NormalizeWhitespace();
|
||||
|
||||
foreach ( TypeInfo parentType in Hierarchy.AsSpan().Slice(1) )
|
||||
{
|
||||
typeDeclarationSyntax =
|
||||
parentType.GetSyntax(prefix, suffix)
|
||||
.AddModifiers(Token(TriviaList(Comment("/// <inheritdoc/>")),
|
||||
parentType.AccessibilityKind,
|
||||
TriviaList()))
|
||||
.AddModifiers(GetKeywordModifierTokens(parentType))
|
||||
.AddMembers(typeDeclarationSyntax)
|
||||
.NormalizeWhitespace();
|
||||
}
|
||||
|
||||
SyntaxTriviaList syntaxTriviaList = TriviaList(
|
||||
Comment("// <auto-generated/>"),
|
||||
Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)),
|
||||
Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)));
|
||||
|
||||
return CompilationUnit()
|
||||
.AddMembers(NamespaceDeclaration(IdentifierName(Namespace))
|
||||
.WithLeadingTrivia(syntaxTriviaList)
|
||||
.AddMembers(typeDeclarationSyntax))
|
||||
.NormalizeWhitespace();
|
||||
}
|
||||
|
||||
private static SyntaxToken[] GetKeywordModifierTokens(TypeInfo typeInfo)
|
||||
{
|
||||
HashSet<SyntaxToken> tokens = [];
|
||||
|
||||
if ( typeInfo.IsSealed )
|
||||
{
|
||||
tokens.Add(Token(SyntaxKind.SealedKeyword));
|
||||
}
|
||||
|
||||
tokens.Add(Token(SyntaxKind.PartialKeyword));
|
||||
|
||||
return [.. tokens];
|
||||
}
|
||||
}
|
||||
115
src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.cs
Normal file
115
src/MapperSourceGen.SourceGenerator/Model/HierarchyInfo.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
// This file is ported and adapted from CommunityToolkit.Mvvm (CommunityToolkit/dotnet)
|
||||
|
||||
namespace MapperSourceGen.SourceGenerator.Model;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a type hierarchy in a source code file.
|
||||
/// </summary>
|
||||
internal sealed partial class HierarchyInfo : IEquatable<HierarchyInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HierarchyInfo" /> class.
|
||||
/// </summary>
|
||||
/// <param name="fileNameHint">The filename hint for the type (including namespace) without extension.</param>
|
||||
/// <param name="namespace">The containing namespace for the type.</param>
|
||||
/// <param name="hierarchy">The current hierarchy for the type.</param>
|
||||
/// <exception cref="ArgumentException"><paramref name="fileNameHint" /> is <c>null</c> or empty.</exception>
|
||||
private HierarchyInfo(string fileNameHint, string @namespace, ImmutableArray<TypeInfo> hierarchy)
|
||||
{
|
||||
if ( string.IsNullOrWhiteSpace(fileNameHint) )
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(fileNameHint)}' cannot be null or empty.", nameof(fileNameHint));
|
||||
}
|
||||
|
||||
FileNameHint = fileNameHint;
|
||||
Hierarchy = hierarchy;
|
||||
Namespace = @namespace;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file name hint (including full namespace) for the type hierarchy.
|
||||
/// </summary>
|
||||
public string FileNameHint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a collection of <see cref="TypeInfo" /> representing the hierarchy of types.
|
||||
/// </summary>
|
||||
public ImmutableArray<TypeInfo> Hierarchy { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the namespace of the type hierarchy.
|
||||
/// </summary>
|
||||
public string Namespace { get; }
|
||||
|
||||
public static HierarchyInfo From(INamedTypeSymbol typeSymbol)
|
||||
{
|
||||
if ( typeSymbol is null )
|
||||
{
|
||||
throw new ArgumentNullException(nameof(typeSymbol));
|
||||
}
|
||||
|
||||
LinkedList<TypeInfo> hierarchy = [];
|
||||
|
||||
for ( INamedTypeSymbol? parent = typeSymbol;
|
||||
parent is not null;
|
||||
parent = parent.ContainingType )
|
||||
{
|
||||
hierarchy.AddLast(new TypeInfo(parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
|
||||
parent.TypeKind,
|
||||
parent.DeclaredAccessibility,
|
||||
parent.IsRecord,
|
||||
parent.IsSealed));
|
||||
}
|
||||
|
||||
return new HierarchyInfo(typeSymbol.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: NameAndContainingTypesAndNamespaces)),
|
||||
typeSymbol.ContainingNamespace.ToDisplayString(new SymbolDisplayFormat(typeQualificationStyle: NameAndContainingTypesAndNamespaces)),
|
||||
[..hierarchy]);
|
||||
}
|
||||
|
||||
public static bool operator ==(HierarchyInfo? left, HierarchyInfo? right)
|
||||
{
|
||||
return Equals(left, right);
|
||||
}
|
||||
|
||||
public static bool operator !=(HierarchyInfo? left, HierarchyInfo? right)
|
||||
{
|
||||
return !Equals(left, right);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(HierarchyInfo? other)
|
||||
{
|
||||
if ( other is null )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ReferenceEquals(this, other) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return string.Equals(FileNameHint, other.FileNameHint, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(Namespace, other.Namespace, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || ( obj is HierarchyInfo other && Equals(other) );
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
return ( StringComparer.OrdinalIgnoreCase.GetHashCode(FileNameHint) * 397 )
|
||||
^ StringComparer.OrdinalIgnoreCase.GetHashCode(Namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/MapperSourceGen.SourceGenerator/Model/PropertyInfo.cs
Normal file
105
src/MapperSourceGen.SourceGenerator/Model/PropertyInfo.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
namespace MapperSourceGen.SourceGenerator.Model;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a property in a source code file.
|
||||
/// </summary>
|
||||
internal readonly struct PropertyInfo : IEquatable<PropertyInfo>
|
||||
{
|
||||
private const string MapAliasAttributeName = "MapperSourceGen.MapAliasAttribute";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PropertyInfo" /> struct.
|
||||
/// </summary>
|
||||
/// <param name="propertySymbol">instance of <see cref="IPropertySymbol" />.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="propertySymbol" /> is <c>null</c>.</exception>
|
||||
public PropertyInfo(IPropertySymbol propertySymbol)
|
||||
{
|
||||
if ( propertySymbol is null )
|
||||
{
|
||||
throw new ArgumentNullException(nameof(propertySymbol));
|
||||
}
|
||||
|
||||
SourcePropertyName = TargetPropertyName = propertySymbol.Name;
|
||||
|
||||
if ( !propertySymbol.GetAttributes().IsEmpty )
|
||||
{
|
||||
AttributeData? attribute = propertySymbol
|
||||
.GetAttributes()
|
||||
.FirstOrDefault(f => f.AttributeClass?.ToDisplayString() == MapAliasAttributeName);
|
||||
|
||||
if ( attribute is not null )
|
||||
{
|
||||
TargetPropertyName = attribute.ConstructorArguments[0].Value!.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
PropertyType = propertySymbol.Type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the property.
|
||||
/// </summary>
|
||||
public ITypeSymbol PropertyType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the source property.
|
||||
/// </summary>
|
||||
public string SourcePropertyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the target property (if different from the source).
|
||||
/// </summary>
|
||||
public string TargetPropertyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether two <see cref="PropertyInfo" /> instances are equal based on their properties.
|
||||
/// </summary>
|
||||
/// <param name="left">the first instance to compare</param>
|
||||
/// <param name="right">the second instance to compare</param>
|
||||
/// <returns><c>true</c> if both instances are equal, otherwise <c>false</c>.</returns>
|
||||
public static bool operator ==(PropertyInfo left, PropertyInfo right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether two <see cref="PropertyInfo" /> instances are not equal based on their properties.
|
||||
/// </summary>
|
||||
/// <param name="left">the first instance to compare</param>
|
||||
/// <param name="right">the second instance to compare</param>
|
||||
/// <returns><c>true</c> if both instances are not equal, otherwise <c>false</c>.</returns>
|
||||
public static bool operator !=(PropertyInfo left, PropertyInfo right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(PropertyInfo other)
|
||||
{
|
||||
return SymbolEqualityComparer.Default.Equals(PropertyType, other.PropertyType)
|
||||
&& string.Equals(SourcePropertyName, other.SourcePropertyName, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(TargetPropertyName, other.TargetPropertyName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is PropertyInfo other && Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hashCode = SymbolEqualityComparer.Default.GetHashCode(PropertyType);
|
||||
|
||||
hashCode = ( hashCode * 397 ) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(SourcePropertyName);
|
||||
hashCode = ( hashCode * 397 ) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(TargetPropertyName);
|
||||
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/MapperSourceGen.SourceGenerator/Model/TypeInfo.cs
Normal file
48
src/MapperSourceGen.SourceGenerator/Model/TypeInfo.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
// This file is ported and adapted from CommunityToolkit.Mvvm (CommunityToolkit/dotnet)
|
||||
|
||||
namespace MapperSourceGen.SourceGenerator.Model;
|
||||
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
internal sealed record TypeInfo(
|
||||
string QualifiedName,
|
||||
TypeKind Kind,
|
||||
Accessibility DeclaredAccessibility,
|
||||
bool IsRecord,
|
||||
bool IsSealed)
|
||||
{
|
||||
public SyntaxKind AccessibilityKind =>
|
||||
DeclaredAccessibility switch
|
||||
{
|
||||
Accessibility.Public => SyntaxKind.PublicKeyword,
|
||||
Accessibility.Internal => SyntaxKind.InternalKeyword,
|
||||
Accessibility.Private => SyntaxKind.PrivateKeyword,
|
||||
Accessibility.Protected => SyntaxKind.ProtectedKeyword,
|
||||
_ => SyntaxKind.None
|
||||
};
|
||||
|
||||
public Accessibility DeclaredAccessibility { get; } = DeclaredAccessibility;
|
||||
|
||||
public bool IsRecord { get; } = IsRecord;
|
||||
|
||||
public bool IsSealed { get; } = IsSealed;
|
||||
|
||||
public TypeKind Kind { get; } = Kind;
|
||||
|
||||
public string QualifiedName { get; } = QualifiedName;
|
||||
|
||||
public TypeDeclarationSyntax GetSyntax(string? prefix = null, string? suffix = null)
|
||||
{
|
||||
return Kind switch
|
||||
{
|
||||
TypeKind.Class when IsRecord =>
|
||||
RecordDeclaration(Token(SyntaxKind.RecordKeyword), $"{prefix}{QualifiedName}{suffix}")
|
||||
.WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken))
|
||||
.WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)),
|
||||
_ => ClassDeclaration($"{prefix}{QualifiedName}{suffix}")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"SourceGenerator": {
|
||||
"commandName": "DebugRoslynComponent",
|
||||
"targetProject": "../../sample/MapperSourceGen.Sample/MapperSourceGen.Sample.csproj"
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/MapperSourceGen.SourceGenerator/packages.lock.json
Normal file
121
src/MapperSourceGen.SourceGenerator/packages.lock.json
Normal file
@@ -0,0 +1,121 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
".NETStandard,Version=v2.0": {
|
||||
"Microsoft.CodeAnalysis.CSharp": {
|
||||
"type": "Direct",
|
||||
"requested": "[4.11.0, 4.11.0]",
|
||||
"resolved": "4.11.0",
|
||||
"contentHash": "6XYi2EusI8JT4y2l/F3VVVS+ISoIX9nqHsZRaG6W5aFeJ5BEuBosHfT/ABb73FN0RZ1Z3cj2j7cL28SToJPXOw==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
|
||||
"Microsoft.CodeAnalysis.Common": "[4.11.0]",
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Collections.Immutable": "8.0.0",
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Numerics.Vectors": "4.5.0",
|
||||
"System.Reflection.Metadata": "8.0.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
|
||||
"System.Text.Encoding.CodePages": "7.0.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
},
|
||||
"NETStandard.Library": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.0.3, )",
|
||||
"resolved": "2.0.3",
|
||||
"contentHash": "st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Platforms": "1.1.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.3.4",
|
||||
"contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g=="
|
||||
},
|
||||
"Microsoft.NETCore.Platforms": {
|
||||
"type": "Transitive",
|
||||
"resolved": "1.1.0",
|
||||
"contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A=="
|
||||
},
|
||||
"System.Buffers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.1",
|
||||
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
|
||||
},
|
||||
"System.Collections.Immutable": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Memory": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.5",
|
||||
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
|
||||
"dependencies": {
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Numerics.Vectors": "4.4.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"System.Numerics.Vectors": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.0",
|
||||
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
|
||||
},
|
||||
"System.Reflection.Metadata": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.0.0",
|
||||
"contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==",
|
||||
"dependencies": {
|
||||
"System.Collections.Immutable": "8.0.0",
|
||||
"System.Memory": "4.5.5"
|
||||
}
|
||||
},
|
||||
"System.Runtime.CompilerServices.Unsafe": {
|
||||
"type": "Transitive",
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
},
|
||||
"System.Text.Encoding.CodePages": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
"contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==",
|
||||
"dependencies": {
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||
}
|
||||
},
|
||||
"System.Threading.Tasks.Extensions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "4.5.4",
|
||||
"contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
|
||||
"dependencies": {
|
||||
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Common": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[4.11.0, )",
|
||||
"resolved": "4.11.0",
|
||||
"contentHash": "djf8ujmqYImFgB04UGtcsEhHrzVqzHowS+EEl/Yunc5LdrYrZhGBWUTXoCF0NzYXJxtfuD+UVQarWpvrNc94Qg==",
|
||||
"dependencies": {
|
||||
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
|
||||
"System.Buffers": "4.5.1",
|
||||
"System.Collections.Immutable": "8.0.0",
|
||||
"System.Memory": "4.5.5",
|
||||
"System.Numerics.Vectors": "4.5.0",
|
||||
"System.Reflection.Metadata": "8.0.0",
|
||||
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
|
||||
"System.Text.Encoding.CodePages": "7.0.0",
|
||||
"System.Threading.Tasks.Extensions": "4.5.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/MapperSourceGen/MapAliasAttribute.cs
Normal file
27
src/MapperSourceGen/MapAliasAttribute.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace MapperSourceGen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the target property should appear in the domain transfer object with a different name.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class MapAliasAttribute : Attribute
|
||||
{
|
||||
private MapAliasAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MapAliasAttribute" /> class with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="name">The name to use instead of the property name.</param>
|
||||
public MapAliasAttribute(string name)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
Name = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the property in the domain transfer object.
|
||||
/// </summary>
|
||||
public string? Name { get; }
|
||||
}
|
||||
9
src/MapperSourceGen/MapIgnoreAttribute.cs
Normal file
9
src/MapperSourceGen/MapIgnoreAttribute.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MapperSourceGen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the target property should not be included in the domain transfer object or mapper.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class MapIgnoreAttribute : Attribute
|
||||
{
|
||||
}
|
||||
9
src/MapperSourceGen/MapperAttribute.cs
Normal file
9
src/MapperSourceGen/MapperAttribute.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace MapperSourceGen;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies that the target class is a candidate for DTO and mapper generation.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class MapperAttribute : Attribute
|
||||
{
|
||||
}
|
||||
19
src/MapperSourceGen/MapperSourceGen.csproj
Normal file
19
src/MapperSourceGen/MapperSourceGen.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--
|
||||
When referencing a source generator that is to part of the nuget package as an analyzer, ensure that the following properties are set:
|
||||
|
||||
- ReferenceOutputAssembly="false" to prevent the source generator from being treated as a regular assembly.
|
||||
- PackAsAnalyzer="true" to include the source generator as an analyzer in the NuGet package.
|
||||
-->
|
||||
<ProjectReference Include="..\MapperSourceGen.SourceGenerator\MapperSourceGen.SourceGenerator.csproj"
|
||||
ReferenceOutputAssembly="false"
|
||||
PackAsAnalyzer="true"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
src/MapperSourceGen/packages.lock.json
Normal file
6
src/MapperSourceGen/packages.lock.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net8.0": {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user