如何使用 Roslyn 生成一个字符串文字数组,每个字符串文字都包含在 #if 中?

How to use Roslyn generate an array of string literals that are each wrapped in an #if?

用例

在运行时了解哪些 #define 符号用于构建 C# 应用程序会很有帮助。由于实际上似乎不可能做到这一点,我改为使用 Roslyn 生成一个巨大的 string[],每个 #define 我可能对列表感兴趣。每个 #define 都应该作为 string 文字存储在这个数组中,只有当相关符号是 #defined 时才会包含它本身,如下所示:

// This class is generated, do not change it manually.

public static class DefineSymbols
{
    public static System.Collections.Generic.IReadOnlyList<string> Symbols => _symbols;

    private static readonly string[] _symbols = new string[] {
#if DEBUG
        "DEBUG",
#endif
#if UNITY
        "UNITY",
#endif
#if UNITY_ASSERTIONS
        "UNITY_ASSERTIONS",
#endif
        // ...and lots more symbols...
    };
}

问题

最简单的部分是生成一个庞大的 string 文字数组。困难的部分是正确放置逗号。使用此代码...

private SyntaxNode CreateSymbolsArray(SyntaxGenerator generator, string[] defines)
{
    var stringType = generator.TypeExpression(SpecialType.System_String);

    return generator.FieldDeclaration(
        name: "_symbols",
        type: generator.ArrayTypeExpression(stringType),
        accessibility: Accessibility.Private,
        modifiers: DeclarationModifiers.Static | DeclarationModifiers.ReadOnly,
        initializer: generator.ArrayCreationExpression(
            generator.TypeExpression(SpecialType.System_String),
            defines.Select(d => CreateDefine(generator, d))
        )
    );
}


private SyntaxNode CreateDefine(SyntaxGenerator generator, string define)
{
    var @if = SyntaxFactory.IfDirectiveTrivia(SyntaxFactory.ParseExpression(define), true, true, true);
    var @endif = SyntaxFactory.EndIfDirectiveTrivia(true);

    return generator.LiteralExpression(define)
            .WithLeadingTrivia(SyntaxFactory.Trivia(@if))
            .WithTrailingTrivia(SyntaxFactory.Trivia(@endif))
        ;
}

...在 #if/#endif 对之外放置逗号,因此生成的代码将无法编译,除非定义了每个测试符号。

    private static readonly string[] _symbols = new string[] {
#if DEBUG
        "DEBUG"
#endif
    ,
#if UNITY
        "UNITY"
#endif
    ,
#if UNITY_ASSERTIONS
        "UNITY_ASSERTIONS"
#endif
        // ...and lots more symbols...
    };

所以,这是我的问题:我如何确保逗号包含在 #if/#endif 对中,就像 string 文字一样?

Roslyn Quoter的帮助下,我想通了! Roslyn Quoter 是一种工具,可让您输入 C# 片段并查看 API 生成它所需的调用。

向 Roslyn Quoter 提供我的原始代码示例 returns 一个太长的片段,无法 post 逐字记录。但是,它有助于我写一些更简单、更简洁的东西。

TLDR: 要解决我的确切问题,您需要提供所有内容作为 SyntaxTrivia 节点序列,这些节点导致右括号标记(}).


这里有更多详细信息。我为每个 #define 符号创建了三个 SyntaxTrivia 节点:

  1. #if 指令
  2. string文字,作为一段禁用文本(这很好,因为禁用文本本身很简单)
  3. #endif 指令

这是它的样子:

using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

// ...later, within the actual class...

private SyntaxTrivia[] CreateDefine(string define)
{
    return new[]
    {
        Trivia(
            IfDirectiveTrivia(
                IdentifierName(define),
                true,
                false,
                false
            )
        ), // # if MY_DEFINE
        DisabledText($"        \"{define}\",\n"),  // "MY_DEFINE",
        Trivia(EndIfDirectiveTrivia(true)),  // #endif
    };
}

/* Output looks like this:
#if MY_DEFINE
        "MY_DEFINE",
#endif
*/

之后,棘手的部分是数组初始化表达式。这是我的做法。

之后棘手的部分是将所有内容都放在一个初始化表达式中,实际上它看起来像一个带有 lot 前导琐事的空数组初始化器。我是这样做的:

using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

// ...later, within the actual class...

private SyntaxNode CreateDefinesArrayInitializerExpression(string[] defines)
{
    var stringType = PredefinedType(Token(SyntaxKind.StringKeyword)); // string
    var arrayType = ArrayType(stringType)
        .WithRankSpecifiers(
            SingletonList(
                ArrayRankSpecifier(
                    SingletonSeparatedList<ExpressionSyntax>(
                        OmittedArraySizeExpression()
                    )
                ) // []
            )
        )
    ; // string[]

    return ArrayCreationExpression(arrayType) // new
        .WithInitializer(
            InitializerExpression(SyntaxKind.ArrayInitializerExpression) // string[] {
                .WithCloseBraceToken(
                    Token(
                        TriviaList(defines.SelectMany(CreateDefine)), // <all the #defines>
                        SyntaxKind.CloseBraceToken, // }
                        TriviaList() // <nothing>
                    )
                 )
        )
        .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) // ;
    ;
}

/* Output looks like this:
new string[] {
#if DEBUG
        "DEBUG"
#endif
    ,
#if UNITY
        "UNITY"
#endif
    ,
#if UNITY_ASSERTIONS
        "UNITY_ASSERTIONS"
#endif
    };
*/

比较冗长,但是你可以写一些扩展方法来帮忙;我做了,但我省略了它们以简化复制粘贴。甚至可能有一个充满它们的 NuGet 包漂浮在某个地方。