Reputation: 2123
It would be helpful to know at runtime which #define
symbols were used to build a C# application. Since it doesn't appear to be possible to actually do that, I'm instead using Roslyn to generate an enormous string[]
with every #define
I might possibly be interested in listing. Each #define
should be stored in this array as a string
literal which itself is only included if the associated symbol is #define
d, like so:
// 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...
};
}
The easy part is generating a big-ass array of string
literals. The hard part is placing the commas properly. Using this code...
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))
;
}
...places commas outside of the #if
/#endif
pairs, so the generated code won't compile unless every tested symbol is defined.
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...
};
So, here's my question: How can I ensure the commas are wrapped inside the #if
/#endif
pairs just like the string
literals are?
Upvotes: 1
Views: 797
Reputation: 2123
With the help of Roslyn Quoter, I figured it out! Roslyn Quoter is a tool that lets you enter a C# snippet and see what API calls you'd need to generate it.
Providing Roslyn Quoter my original code sample returns a snippet that's too long to post verbatim. It was useful in helping me write something simpler and more concise, however.
TLDR: To solve my exact problem, you need to provide everything as a sequence of SyntaxTrivia
nodes that leads a closing-brace token (}
).
Here are some more details. I created three SyntaxTrivia
nodes for each #define
symbol:
#if
directivestring
literal, as a piece of disabled text (which is fine, because the disabled text itself is simple)#endif
directiveHere's what that looks like:
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
*/
After that, the tricky part is the array initialization expression. Here's how I did that.
The tricky part after that is putting everything in an initializer expression, which in practice looks like an empty array initializer with a lot of leading trivia. Here's how I did that:
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
};
*/
It's verbose, but you can write some extension methods to help; I did, but I omitted them to simplify copy-pasting. There might even be a NuGet package full of them floating around somewhere.
Upvotes: 1