I’m currently working on an Icon Pack for my Material Design library and need to generate an enum containing a list of all the icons. Before you delve into this code you should know that the same result could no doubt be achieved using simple string manipulation in a lot less time. But I took this as an opportunity to learn a little about Roslyn, so if that’s what you’re after then read on.
I set about creating a code template for the enum with the intention of using Roslyn to clear the dummy enum values out and re-populate the enum with a new list. Sourcing the list of values is the easy part and not discussed here.
To begin with, this is my starting code template:
namespace MaterialDesignThemes.Wpf { /// ****************************************** /// This code is auto generated. Do not amend. /// ****************************************** /// <summary> /// List of available icons for use with <see cref="Icon"/>. /// </summary> /// <remarks> /// All icons sourced from Material Design Icons Font - <see cref="https://materialdesignicons.com/"> - in accordance of /// <see cref="https://github.com/Templarian/MaterialDesign/blob/master/license.txt"/>. /// </remarks>; public enum IconType { AutoGeneratedDoNotAmend } }
First lesson. If you want to do something similar, install the .Net Compiler Platform SDK. Stupidly I didn’t do this until I had the code 75% complete. The solution below looks pretty simple but as is oft the way, figuring it out was the hard part. The Syntax Visualizer Visual Studio add-in included in the SDK would have got me there a lot quicker.
If I had of used the Visualizer in VS 2015 earlier (View > Other Windows > Syntax Visualizer), upon highlighting my above code template I would have seen this, which gives you a pretty good idea of what we’re working with:
Second lesson. The nuget package you are probably after is (took me a bit of digging to get the right one):
Microsoft.CodeAnalysis.CSharp.Workspaces
Also, I found ReSharper’s Hierarchy tool pretty useful to start understanding the class relationships; soon realising that we’re going to be spending a lot of time working with SyntaxNode objects and derivations thereof:
One of the first things to learn about Roslyn SyntaxNode objects is that they are immutable. If you make a change then you’ll get a new one. Obviously what we want to do is remove our AutoGeneratedDoNotAmend enum value (EnumMemberDeclarationSyntax) from the owning enum (EnumDeclarationSyntax) and replace it with a bunch of new members. Meaning we’ll end up with a new enum/EnumDeclarationSyntax, in turn meaning we need to swap it out in the parent (NamespaceDeclaration) which again give us a new namespace, so we’ll have to swap that in the root’s children giving us a new root. Effectively we start at the bottom and replace/renew everything walking up the ancestry of the tree.
Maybe there’s a better way, but this is my first crack at Roslyn and is very much a learning experience.
My first cut of the code (done prior to me installing the Visualizer/SDK, and using some pretend enum values) turned out like this:
private void UpdateEnum(string sourceFile) { var sourceText = SourceText.From(new FileStream(sourceFile, FileMode.Open)); var syntaxTree = CSharpSyntaxTree.ParseText(sourceText); var rootNode = syntaxTree.GetRoot(); var namespaceDeclarationNode = rootNode.ChildNodes().Single(); var enumDeclarationSyntaxNode = namespaceDeclarationNode.ChildNodes().OfType&amp;amp;amp;amp;lt;EnumDeclarationSyntax&amp;amp;amp;amp;gt;().Single(); var emptyEnumDeclarationSyntaxNode = enumDeclarationSyntaxNode.RemoveNodes(enumDeclarationSyntaxNode.ChildNodes().OfType&amp;amp;amp;amp;lt;EnumMemberDeclarationSyntax&amp;amp;amp;amp;gt;(), SyntaxRemoveOptions.KeepDirectives); var generatedEnumDeclarationSyntax = emptyEnumDeclarationSyntaxNode.AddMembers( SyntaxFactory.EnumMemberDeclaration("Aston"), SyntaxFactory.EnumMemberDeclaration("Villa")); var generatedNamespaceDeclarationSyntaxNode = namespaceDeclarationNode.ReplaceNode(enumDeclarationSyntaxNode, generatedEnumDeclarationSyntax); var generatedRootNode = rootNode.ReplaceNode(namespaceDeclarationNode, generatedNamespaceDeclarationSyntaxNode); Console.WriteLine(generatedRootNode.ToFullString()); }
The end result was pretty good, we retain the namespace declaration, comments, but we’ve lost something in the formatting:
namespace MaterialDesignThemes.Wpf { /// ****************************************** /// This code is auto generated. Do not amend. /// ****************************************** /// <summary> /// List of available icons for use with <see cref="Icon"/>. /// </summary> /// <remarks> /// All icons sourced from Material Design Icons Font - <see cref="https://materialdesignicons.com/"> - in accordance of /// <see cref="https://github.com/Templarian/MaterialDesign/blob/master/license.txt"/>. /// </remarks> public enum IconType { Aston,Villa } }
More digging and I learn how to create my enum member with “trivia” consisting of leading white space:
var leadingTriviaList = SyntaxTriviaList.Create(SyntaxFactory.Whitespace(" ")); var generatedEnumDeclarationSyntax = emptyEnumDeclarationSyntaxNode.AddMembers( SyntaxFactory.EnumMemberDeclaration(SyntaxFactory.Identifier(leadingTriviaList, "Aston", SyntaxTriviaList.Empty)), SyntaxFactory.EnumMemberDeclaration(SyntaxFactory.Identifier(leadingTriviaList, "Villa", SyntaxTriviaList.Empty)));
Current result:
public enum IconType { Aston, Villa }
This is good, but I now realised I needed to add a line feed, but to do this I needed the sibling “CommaToken” node to have the correct trailing trivia. I discovered this by tweaking my initial template to include two enum values and taking a look at the Syntax Visualizer:
Yikes. This is starting to give me a headache.
Remembering that these toys we are playing with are immutable, I concocted a new method to pull all the comma tokens, and replace with new comma tokens with trailing line feed trivia:
. . . generatedEnumDeclarationSyntax = AddLineFeedsToCommas(generatedEnumDeclarationSyntax); . . . private static EnumDeclarationSyntax AddLineFeedsToCommas(EnumDeclarationSyntax enumDeclarationSyntax) { var none = new SyntaxToken(); var trailingTriviaList = SyntaxTriviaList.Create(SyntaxFactory.ElasticCarriageReturnLineFeed); Func<EnumDeclarationSyntax, SyntaxToken> next = enumSyntax => enumSyntax.ChildNodesAndTokens() .Where(nodeOrToken => nodeOrToken.IsToken) .Select(nodeOrToken => nodeOrToken.AsToken()) .FirstOrDefault( token => token.Value.Equals(",") && (!token.HasTrailingTrivia || !token.TrailingTrivia.Any(SyntaxKind.EndOfLineTrivia))); SyntaxToken current; while ((current = next(enumDeclarationSyntax)) != none) { enumDeclarationSyntax = enumDeclarationSyntax.ReplaceToken(current, SyntaxFactory.Identifier(SyntaxTriviaList.Empty, ",", trailingTriviaList) ); } return enumDeclarationSyntax; }
Our result now is looking much better:
namespace MaterialDesignThemes.Wpf { /// ****************************************** /// This code is auto generated. Do not amend. /// ****************************************** /// <summary>; /// List of available icons for use with <see cref="Icon">. /// </summary> /// <remarks> /// All icons sourced from Material Design Icons Font - <see cref="https://materialdesignicons.com/"/> - in accordance of /// <see cref="https://github.com/Templarian/MaterialDesign/blob/master/license.txt"/>. /// </remarks> public enum IconType { Aston, Villa } }
There’s just that last curly bracket which needs knocking down a line, but I guess I’ll just have to get back to that…