Getting whitespace right with Roslyn CSharpSyntaxRewriter

Most agree that Roslyn is a great C# code analysis and refactoring tool. One particularly powerful tool Roslyn has is it’s CSharpSyntaxRewriter [1]. The rewriter allows you to walk down the namespaces and classes of a project and add and edit code where necessary. While the documentation here [2] and the RoslynQuoter [3] can come in handy for generating new code blocks, there’s little guidance for formatting the code so that it’s placed at the right position with the appropriate amount of tabs and whitespace [4]. Hence, this post aims at providing some guidance for formatting code you wish to add using the CSharpSyntaxRewriter.

The CSharpSyntaxRewriter has multiple overrides for accessing code blocks but in this post we will use the VisitClassDeclaration method to add a method to an existing class. As an example we will add the method below to a class named ”Converter”

public int DecimalToInt(decimal i)  
{
return (int)i;
}

Firstly, in order to find the “Converter” class we use the VisitClassDeclaration method mentioned above

private int _positionToInsert; 

public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
{
if (node.Identifier.ValueText != "Converter")
return node;
var lastMem = node.Members.LastOrDefault();
if (lastMem != null)
{
_positionToInsert= lastMem.FullSpan.End;
}
else
{
_positionToInsert= node.CloseBraceToken.FullSpan.Start;
}
}

We look for the node with the name corresponding to the name of the class we’re looking for and then set the position we would like to insert the method. In this example – as described in the code above – we’re inserting the method at the end of the class.

Now that we have the position to insert the method, we need to get the Roslyn representation of the method code block. Using the RoslynQuoter, the code generated to represent the method is:

var m = MethodDeclaration(
PredefinedType(
Token(SyntaxKind.IntKeyword)),
Identifier("DecimalToInt"))
.WithModifiers(
TokenList(
Token(SyntaxKind.PublicKeyword)))
.WithParameterList(
ParameterList(
SingletonSeparatedList(
Parameter(
Identifier("i"))
.WithType(
PredefinedType(
Token(SyntaxKind.DecimalKeyword))))))
.WithBody(
Block(
SingletonList(
ReturnStatement(
CastExpression(
PredefinedType(
Token(SyntaxKind.IntKeyword)),
IdentifierName("i"))))))
.NormalizeWhitespace();

While the generated code above does the right thing, the call to NormalizeWhitespace() on the MethodDeclaration formats the code so that the method is justified to the left side of the file without any whitespace or tab indentations like the screenshot below.

Calling NormalizeWhitespace() on the MethodDeclaration is actually an aggressive way of formatting the code block. Instead, in order to properly format each line of code, we need to selectively call NormalizeWhitespace()

Let’s begin by figuring out how much whitespace we need at the start of the method. This can be done by either getting the amount of whitespace in front of the previous method, property or field declaration, or by using the amount of whitespace in front of the containing class itself. Since in this example we’re inserting the method at the end of the Converter class as explained above, we will use the “lastMem” var defined above as a reference for the amount of whitespace we should add

 private const string _tabSpace = "    ";
SyntaxTriviaList leadingWhiteSpace;
var lastMem = node.Members.LastOrDefault();

if (lastMem != null &&
(lastMem.Kind() == SyntaxKind.PropertyDeclaration ||
lastMem.Kind() == SyntaxKind.MethodDeclaration ||
lastMem.Kind() == SyntaxKind.FieldDeclaration ||
lastMem.Kind() == SyntaxKind.ConstructorDeclaration))
{
leadingWhiteSpace = lastMem.GetLeadingTrivia().Where(t =>
t.Kind() == SyntaxKind.WhitespaceTrivia).ToSyntaxTriviaList();

endingWhitespcae = lastMem.GetTrailingTrivia().Where(t =>
t.Kind() == SyntaxKind.WhitespaceTrivia).ToSyntaxTriviaList();
}
else
{
leadingWhiteSpace = node.GetLeadingTrivia().Where(t => t.Kind()
== SyntaxKind.WhitespaceTrivia).ToSyntaxTriviaList()
.Add(SyntaxTrivia(SyntaxKind.WhitespaceTrivia, _tabSpace));

endingWhitespcae = node.GetTrailingTrivia().Where(t =>
t.Kind() == SyntaxKind.WhitespaceTrivia).ToSyntaxTriviaList(); 
}

Now that we have the amount of whitespace, we need to add it to the correct places. Let’s break down the RoslynQuoter generated code. The MethodDeclaration takes in two parameters – the return type (TypeSyntax) and the method name (Identifier). The WithModifiers and WithParameterList methods as their names suggest add the modifier and parameters for the method. Since we want the identifier, modifier and parameters displayed on the first line of the method, we can call NormalizeWhitespace() after the WithModifiers and WithParameterList calls. Then we can add the leading whitespace trivia and a new line trivia.
*It is important that NormalizeWhitespace() is called first if not the leading and trailing trivias will be overriden by the formatting style made by NormalizeWhitespace()*

var newLineTrivia = SyntaxTrivia(SyntaxKind.EndOfLineTrivia, "\n");

var m = MethodDeclaration(
PredefinedType(
Token(SyntaxKind.IntKeyword)),
Identifier("DecimalToInt"))

.WithModifiers(
TokenList(
Token(SyntaxKind.PublicKeyword)))
.WithParameterList(
ParameterList(
SingletonSeparatedList(
Parameter(
Identifier("i"))
.WithType(
PredefinedType(
Token(SyntaxKind.DecimalKeyword))))))
.NormalizeWhitespace()
.WithLeadingTrivia(leadingWhiteSpace.Insert(0, newLineTrivia))

The next part is the method’s code block. Because in this example, we want the braces on their own lines, we need to define our own open and close braces. We use the same leading whitespace used for the first line of the method described above and also insert a new line trivia. We don’t add trailing trivias for the open brace because that will be added to the code block (as leading trivias)

var openBrace = Token(SyntaxKind.OpenBraceToken)
.WithLeadingTrivia(leadingWhiteSpace.Insert(0, newLineTrivia));

var closeBrace = Token(SyntaxKind.CloseBraceToken)
.WithLeadingTrivia(leadingWhiteSpace.Insert(0, newLineTrivia))
.WithTrailingTrivia(endingWhitespace.Insert(0, newLineTrivia));

Now that we’ve defined the braces, we just need to fix each line of code in the method’s code block. And because this method only has one line which is the return statement we just have to call NormalizeWhitespace() on the ReturnStatementSyntax and add the leading whitespace, a new line and one tab space and finally we add the braces to the code block like below:

var tabTrivia = SyntaxTrivia(SyntaxKind.WhitespaceTrivia, _tabSpace);

.WithBody(
Block(
SingletonList(
ReturnStatement(
CastExpression(
PredefinedType(
Token(SyntaxKind.IntKeyword)),
IdentifierName("i")))
.NormalizeWhitespace()
.WithLeadingTrivia(leadingWhiteSpace.InsertRange(0, new[]
{ newLineTrivia, tabTrivia }))))

.WithOpenBraceToken(openBrace).WithCloseBraceToken(closeBrace)
);

Final MethodDeclarationSyntax :

var m = MethodDeclaration(
PredefinedType(
Token(SyntaxKind.IntKeyword)),
Identifier("DecimalToInt"))
.WithModifiers(
TokenList(
Token(SyntaxKind.PublicKeyword)))
.WithParameterList(
ParameterList(
SingletonSeparatedList(
Parameter(
Identifier("i"))
.WithType(
PredefinedType(
Token(SyntaxKind.DecimalKeyword))))))
.NormalizeWhitespace()
.WithLeadingTrivia(leadingWhiteSpace.Insert(0, newLineTrivia))
.WithBody(
Block(
SingletonList(
ReturnStatement(
CastExpression(
PredefinedType(
Token(SyntaxKind.IntKeyword)),
IdentifierName("i")))
.NormalizeWhitespace()
.WithLeadingTrivia(leadingWhiteSpace.InsertRange(0, new[]
{ newLineTrivia, tabTrivia }))))
.WithOpenBraceToken(openBrace)
.WithCloseBraceToken(closeBrace));

A few key takeaways:
1.) Do not call NormalizeWhitespace() on large blocks of code. NormalizeWhitespace() adds whitespace, newlines and tabs and justifies the entire DeclarationSyntax to the left of the file. Instead call NormalizeWhitespace() on declarations, identifiers, expression statements that you want displayed on a line.

2.) A block of code has predefined open and close braces. When adding blocks, instead of calling NormalizeWhitespace() on the whole block, create your own open and close braces with the correct leading and trailing trivias and call NormalizeWhitespace() on the contents of the block and subsequently add the braces.

3.) Sequence of the calls made matters. Do not call NormalizeWhitespace() after adding leading or trailing trivias. The trivias will be overriden by the call to NormalizeWhitespace().

References
1.) https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.csharp.csharpsyntaxrewriter?view=roslyn-dotnet
2.) https://github.com/dotnet/roslyn/wiki/Getting-Started-C%23-Syntax-Transformation
3.) http://roslynquoter.azurewebsites.net/
4.) https://stackoverflow.com/questions/30628608/how-do-i-format-code-selection-and-maintain-leading-trivia-with-roslyn

Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.