专栏/源生成器:根据需要自动生成机械重复代码(C#、Attribute)

源生成器:根据需要自动生成机械重复代码(C#、Attribute)

2022年02月02日 13:03--浏览 · --点赞 · --评论
粉丝:122文章:26

本文概述了利用.NET Compiler Platform(“Roslyn”)SDK 附带的源生成器(Source Generator)自动生成机械重复的代码。关于这部分的基础入门知识可以在MSDN[1]学到。

本文默认已经有一个解决方案,包含两个项目。一个是普通C#项目,依赖于另一个源生成器项目。

注:红色加粗表示重点;绿色表示次重点;蓝色表示参考资料及链接,在文章最后显示。

创建及使用Attribute

此处以DependencyPropertyAttribute为例,可以为拥有本Attribute的类,添加一条DependencyProperty的属性。

本DependencyProperty的名称类型属性改变处理函数都是必须指定的,可选指定内容是属性setter的公共性、该类型的null性、和默认值。可选内容有默认值。

以下是DependencyPropertyAttribute的实现:

using System;

namespace Attributes;

/// <summary>
///     生成如下代码
///     <code>
/// public static readonly DependencyProperty Property = DependencyProperty.Register("Field", typeof(Type), typeof(TClass), new PropertyMetadata(DefaultValue, OnPropertyChanged));
/// public Type Field { get => (Type)GetValue(Property); set => SetValue(Property, value); }
/// </code>
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class DependencyPropertyAttribute : Attribute
{
    public DependencyPropertyAttribute(string name, Type type, string propertyChanged = "")
    {
        Name = name;
        Type = type;
        PropertyChanged = propertyChanged;
    }

    public string Name { get; }

    public Type Type { get; }

    public string PropertyChanged { get; }

    public bool IsSetterPublic { get; init; } = true;

    public bool IsNullable { get; init; } = true;

    public string DefaultValue { get; init; } = "DependencyProperty.UnsetValue";
}

以下是使用示例:

namespace Controls.IconButton;

[DependencyProperty("Text", typeof(string), nameof(OnTextChanged))]
[DependencyProperty("Icon", typeof(IconElement), nameof(OnIconChanged))]
public partial class IconButton : Button
{
    ...
}

这将会生成如下代码:

using Microsoft.UI.Xaml;
using System;
using Microsoft.UI.Xaml.Controls;

#nullable enable
namespace Controls.IconButton
{
    partial class IconButton
    {
        public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(IconButton), new PropertyMetadata(DependencyProperty.UnsetValue, OnTextChanged));
        public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }

        public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(IconElement), typeof(IconButton), new PropertyMetadata(DependencyProperty.UnsetValue, OnIconChanged));
        public IconElement Icon { get => (IconElement)GetValue(IconProperty); set => SetValue(IconProperty, value); }
    }
}

:DependencyPropertyAttribute中建议只使用基本类型的常量,因为复杂类型不方便获取。

:被添加Attribute的类(如IconButton)要加partial关键字,否则会出重定义错误。

注:DependencyPropertyAttribute中,只会用到构造函数可选指定内容的属性,这说明实现可以简化为:

using System;

namespace Attributes;

///...
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class DependencyPropertyAttribute : Attribute
{
    public DependencyPropertyAttribute(string name, Type type, string propertyChanged = "") { }

    public bool IsSetterPublic { get; init; }

    public bool IsNullable { get; init; }

    public string DefaultValue { get; init; }
}

因为当源生成器分析的时候,分析的是被捕获的类(如IconButton)及其上下文,而非DependencyPropertyAttribute的,所以其他内容实际上用不上。

但原来的写法方便将来可能需要反射本Attribute的操作,也方便阅读,所以建议保留。

创建通用AttributeReceiver

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;

internal class AttributeReceiver : ISyntaxContextReceiver
{
    private readonly string _attributeName;
    private readonly List<TypeDeclarationSyntax> _candidateTypes = new();

    private INamedTypeSymbol? _attributeSymbol;

    public AttributeReceiver(string attributeName)
    {
        _attributeName = attributeName;
    }

    public IReadOnlyList<TypeDeclarationSyntax> CandidateTypes => _candidateTypes;

    public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
    {
        _attributeSymbol ??= context.SemanticModel.Compilation.GetTypeByMetadataName(_attributeName);

        if (_attributeSymbol is null) return;

        if (context.Node is TypeDeclarationSyntax typeDeclaration && typeDeclaration.AttributeLists
                .SelectMany(l => l.Attributes, (_, attribute) => context.SemanticModel.GetSymbolInfo(attribute))
                .Any(symbolInfo => SymbolEqualityComparer.Default.Equals(symbolInfo.Symbol?.ContainingType, _attributeSymbol)))
            _candidateTypes.Add(typeDeclaration);
    }
}

本类实现接受语法上下文(ISyntaxContextReceiver)接口,它会将所有拥有_attributeName的类型声明节点(TypeDeclarationSyntax)(例如class、struct、record、enum等)捕获下来(此处也可以捕获property、field、method等,但没有做)。

创建通用基类GetAttributeGenerator

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Utilities;

internal abstract class GetAttributeGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new AttributeReceiver(AttributePath));
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.Compilation.GetTypeByMetadataName(AttributePath) is not { } attributeType)
        {
            return;
        }

        foreach (var typeDeclaration in ((AttributeReceiver) context.SyntaxContextReceiver!).CandidateTypes)
        {
            var semanticModel = context.Compilation.GetSemanticModel(typeDeclaration.SyntaxTree);

            if (semanticModel.GetDeclaredSymbol(typeDeclaration) is not { } specificType)
            {
                continue;
            }

            ExecuteForEach(context, attributeType, typeDeclaration, specificType);
        }
    }

    protected abstract string AttributePathGetter();

    /// <summary>
    /// 获取包含指定attribute的类型
    /// </summary>
    /// <param name="context">生成器运行上下文</param>
    /// <param name="attributeType">指定的attribute类型</param>
    /// <param name="typeDeclaration">该类型的声明</param>
    /// <param name="specificType">该类型</param>
    protected abstract void ExecuteForEach(GeneratorExecutionContext context, INamedTypeSymbol attributeType, TypeDeclarationSyntax typeDeclaration, INamedTypeSymbol specificType);

    private string AttributePath => AttributePathGetter();
}

本类继承于ISourceGenerator接口,如果拥有GeneratorAttribute则会被认为是源生成器了。它通过调用上面的AttributeReceiver获取所有符合的语法节点,AttributePathGetter()由子类实现,这样可以保证子类的Attribute名被本基类读取。

对于每个被捕获的类型分别用ExecuteForEach()处理。

创建具体的子类DependencyPropertyGenerator

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;
using System.Text;

[Generator]
internal class DependencyPropertyGenerator : Utilities.GetAttributeGenerator
{
    protected override string AttributePathGetter() => "Attributes.DependencyPropertyAttribute";

    protected override void ExecuteForEach(GeneratorExecutionContext context, INamedTypeSymbol attributeType, TypeDeclarationSyntax typeDeclaration, INamedTypeSymbol specificClass)
    {
        var members = new List<MemberDeclarationSyntax>();
        var namespaces = new HashSet<string> { specificClass.ContainingNamespace.ToDisplayString(), "Microsoft.UI.Xaml" };
        var usedTypes = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);

        foreach (var attribute in specificClass.GetAttributes().Where(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeType)))
        {
            if (attribute.ConstructorArguments[0].Value is not string propertyName || attribute.ConstructorArguments[1].Value is not INamedTypeSymbol type)
            {
                continue;
            }

            if (attribute.ConstructorArguments.Length < 3 || attribute.ConstructorArguments[2].Value is not string propertyChanged)
            {
                continue;
            }

            var isSetterPublic = true;
            var defaultValue = "DependencyProperty.UnsetValue";
            var isNullable = false;

            foreach (var namedArgument in attribute.NamedArguments)
            {
                if (namedArgument.Value.Value is { } value)
                {
                    switch (namedArgument.Key)
                    {
                        case "IsSetterPublic":
                            isSetterPublic = (bool) value;
                            break;
                        case "DefaultValue":
                            defaultValue = (string) value;
                            break;
                        case "IsNullable":
                            isNullable = (bool) value;
                            break;
                    }
                }
            }

            var fieldName = propertyName + "Property";

            namespaces.UseNamespace(usedTypes, type);
            var defaultValueExpression = SyntaxFactory.ParseExpression(defaultValue);
            var metadataCreation = GetObjectCreationExpression(defaultValueExpression);
            if (propertyChanged is not "")
            {
                metadataCreation = GetMetadataCreation(metadataCreation, propertyChanged);
            }

            var registration = GetRegistration(propertyName, type, specificClass, metadataCreation);
            var staticFieldDeclaration = GetStaticFieldDeclaration(fieldName, registration);
            var getter = GetGetter(fieldName, isNullable, type, context);
            var setter = GetSetter(fieldName, isSetterPublic);
            var propertyDeclaration = GetPropertyDeclaration(propertyName, isNullable, type, getter, setter);

            members.Add(staticFieldDeclaration);
            members.Add(propertyDeclaration);
        }

        if (members.Count > 0)
        {
            var generatedClass = GetClassDeclaration(specificClass, members);
            var generatedNamespace = GetNamespaceDeclaration(specificClass, generatedClass);
            var compilationUnit = GetCompilationUnit(generatedNamespace, namespaces.Skip(1));
            var fileName = specificClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat
                .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)) + ".g.cs";
            context.AddSource(fileName, SyntaxFactory.SyntaxTree(compilationUnit, encoding: Encoding.UTF8).GetText());
        }
    }


    /// <summary>
    ///     生成如下代码
    ///     <code>
    /// new PropertyMetadata(<paramref name="defaultValueExpression" />);
    /// </code>
    /// </summary>
    /// <returns>ObjectCreationExpression</returns>
    private static ObjectCreationExpressionSyntax GetObjectCreationExpression(ExpressionSyntax defaultValueExpression)
    {
        return SyntaxFactory.ObjectCreationExpression(SyntaxFactory.IdentifierName("PropertyMetadata"))
            .AddArgumentListArguments(SyntaxFactory.Argument(defaultValueExpression));
    }

    /// <summary>
    ///     生成如下代码
    ///     <code>
    /// new PropertyMetadata(<paramref name="metadataCreation" />, <paramref name="partialMethodName" />)
    /// </code>
    /// </summary>
    /// <returns>MetadataCreation</returns>
    private static ObjectCreationExpressionSyntax GetMetadataCreation(ObjectCreationExpressionSyntax metadataCreation, string partialMethodName)
    {
        return metadataCreation.AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.IdentifierName(partialMethodName)));
    }

    /// <summary>
    ///     生成如下代码
    ///     <code>
    /// DependencyProperty.Register("<paramref name="propertyName" />", typeof(<paramref name="type" />), typeof(<paramref
    ///             name="specificClass" />), <paramref name="metadataCreation" />);
    /// </code>
    /// </summary>
    /// <returns>Registration</returns>
    private static InvocationExpressionSyntax GetRegistration(string propertyName, ITypeSymbol type, ITypeSymbol specificClass, ExpressionSyntax metadataCreation)
    {
        return SyntaxFactory.InvocationExpression(SyntaxFactory.MemberAccessExpression(
                SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName("DependencyProperty"), SyntaxFactory.IdentifierName("Register")))
            .AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(propertyName))), SyntaxFactory.Argument(SyntaxFactory.TypeOfExpression(type.GetTypeSyntax(false))), SyntaxFactory.Argument(SyntaxFactory.TypeOfExpression(specificClass.GetTypeSyntax(false))), SyntaxFactory.Argument(metadataCreation));
    }

    /// <summary>
    ///     生成如下代码
    ///     <code>
    /// public static readonly DependencyProperty <paramref name="fieldName" /> = <paramref name="registration" />;
    /// </code>
    /// </summary>
    /// <returns>StaticFieldDeclaration</returns>
    private static FieldDeclarationSyntax GetStaticFieldDeclaration(string fieldName, ExpressionSyntax registration)
    {
        return SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration(SyntaxFactory.IdentifierName("DependencyProperty")))
            .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword))
            .AddDeclarationVariables(SyntaxFactory.VariableDeclarator(fieldName).WithInitializer(SyntaxFactory.EqualsValueClause(registration)));
    }

    /// <summary>
    ///     生成如下代码
    ///     <code>
    /// get => (<paramref name="type" /><<paramref name="isNullable" />>)GetValue(<paramref name="fieldName" />);
    /// </code>
    /// </summary>
    /// <returns>Getter</returns>
    private static AccessorDeclarationSyntax GetGetter(string fieldName, bool isNullable, ITypeSymbol type, GeneratorExecutionContext context)
    {
        ExpressionSyntax getProperty = SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("GetValue"))
            .AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.IdentifierName(fieldName)));
        if (!SymbolEqualityComparer.Default.Equals(type, context.Compilation.GetSpecialType(SpecialType.System_Object)))
        {
            getProperty = SyntaxFactory.CastExpression(type.GetTypeSyntax(isNullable), getProperty);
        }

        return SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
            .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(getProperty))
            .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));
    }

    /// <summary>
    ///     生成如下代码
    ///     <code>
    /// <<paramref name="isSetterPublic" />> set => SetValue(<paramref name="fieldName" />, value);
    /// </code>
    /// </summary>
    /// <returns>Setter</returns>
    private static AccessorDeclarationSyntax GetSetter(string fieldName, bool isSetterPublic)
    {
        ExpressionSyntax setProperty = SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("SetValue"))
            .AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.IdentifierName(fieldName)), SyntaxFactory.Argument(SyntaxFactory.IdentifierName("value")));
        var setter = SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
            .WithExpressionBody(SyntaxFactory.ArrowExpressionClause(setProperty))
            .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));
        return !isSetterPublic ? setter.AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)) : setter;
    }

    /// <summary>
    ///     生成如下代码
    ///     <code>
    /// public <paramref name="type" /><<paramref name="isNullable" />> <paramref name="propertyName" /> { <paramref
    ///             name="getter" />; <paramref name="setter" />; }
    /// </code>
    /// </summary>
    /// <returns>PropertyDeclaration</returns>
    private static PropertyDeclarationSyntax GetPropertyDeclaration(string propertyName, bool isNullable, ITypeSymbol type, AccessorDeclarationSyntax getter, AccessorDeclarationSyntax setter)
    {
        return SyntaxFactory.PropertyDeclaration(type.GetTypeSyntax(isNullable), propertyName)
            .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
            .AddAccessorListAccessors(getter, setter);
    }

    /// <summary>
    ///     生成如下代码
    ///     <code>
    /// partial class <paramref name="specificClass" /><br />
    /// {<br /> <paramref name="members" /><br />}
    /// </code>
    /// </summary>
    /// <returns>ClassDeclaration</returns>
    private static ClassDeclarationSyntax GetClassDeclaration(ISymbol specificClass, IEnumerable<MemberDeclarationSyntax> members)
    {
        return SyntaxFactory.ClassDeclaration(specificClass.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))
            .AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword))
            .AddMembers(members.ToArray());
    }

    /// <summary>
    ///     生成如下代码
    ///     <code>
    /// namespace <paramref name="specificClass" />.ContainingNamespace<br />
    /// {<br /> <paramref name="generatedClass" /><br />}
    /// </code>
    /// </summary>
    /// <returns>NamespaceDeclaration</returns>
    private static NamespaceDeclarationSyntax GetNamespaceDeclaration(ISymbol specificClass, MemberDeclarationSyntax generatedClass)
    {
        return SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(specificClass.ContainingNamespace.ToDisplayString()))
            .AddMembers(generatedClass)
            .WithNamespaceKeyword(SyntaxFactory.Token(SyntaxKind.NamespaceKeyword)
                .WithLeadingTrivia(SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(SyntaxFactory.Token(SyntaxKind.EnableKeyword), true))));
    }

    /// <summary>
    ///     生成如下代码
    ///     <code>
    /// using Microsoft.UI.Xaml;<br />
    /// ...<br />
    /// <paramref name="generatedNamespace" />
    /// </code>
    /// </summary>
    /// <returns>CompilationUnit</returns>
    private static CompilationUnitSyntax GetCompilationUnit(MemberDeclarationSyntax generatedNamespace, IEnumerable<string> namespaces)
    {
        return SyntaxFactory.CompilationUnit()
            .AddMembers(generatedNamespace)
            .AddUsings(namespaces.Select(ns => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(ns))).ToArray())
            .NormalizeWhitespace();
    }
}

此类继承于GetAttributeGenerator,拥有GeneratorAttribute,可以被编译器作为源生成器执行。

其中AttributePathGetter()要写全名,否则会找不到或者重名。

接下来分析的语句来自于形如这里的地方:

[DependencyProperty("Name", typeof(string), nameof(Method), IsNullable = true)]

以下这句可以读取构造函数的前两个参数,其中Type类型用INamedTypeSymbol代替。

if (attribute.ConstructorArguments[0].Value is not string propertyName || attribute.ConstructorArguments[1].Value is not INamedTypeSymbol type)
{
    continue;
}

以下这句读取了第三个构造函数的缺省参数,因为可能不存在

if (attribute.ConstructorArguments.Length < 3 || attribute.ConstructorArguments[2].Value is not string propertyChanged)
{
    continue;
}

以下这段是对象初始化的内容(不在构造函数里):

var isSetterPublic = true;
var defaultValue = "DependencyProperty.UnsetValue";
var isNullable = false;

foreach (var namedArgument in attribute.NamedArguments)
{
    if (namedArgument.Value.Value is { } value)
    {
        switch (namedArgument.Key)
        {
            case "IsSetterPublic":
                isSetterPublic = (bool) value;
                break;
            case "DefaultValue":
                defaultValue = (string) value;
                break;
            case "IsNullable":
                isNullable = (bool) value;
                break;
        }
    }
}

:源生成器生成的文件若要使用nullable必须加上如下指令,项目的null性不会影响到生成的文件。

#nullable enable

注:本例使用Roslyn的AST(Abstract Syntax Tree)实现,但其实没必要一定如此,因为直接string操作可以达到同样效果,生成代码耗时也不算在编译时间内。

用string可以如此AddSource:

context.AddSource(fileNameString, sourceString);

引用图片

[小強] ID = 30885114

参考资料

[1] SourceGenerators(https://docs.microsoft.com/zh-cn/dotnet/csharp/roslyn-sdk/source-generators-overview)

QQ:2639914082

投诉或建议