Creating a code fix that fixes comments
In a previous post we created an analyzer that looked for blank comments. Since empty comments are a very fixable problem (just remove them), I figured tackling the code fix for this would be a useful exercise.
To start, we need to register our code fix in the RegisterCodeFixesAsync
method. In this method we simply loop over the diagnostics that have an ID we care about and register a method to perform the code fix.
public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
{
foreach (var diagnostic in context.Diagnostics.Where(d => FixableDiagnosticIds.Contains(d.Id)))
{
context.RegisterCodeFix(CodeAction.Create("Remove the empty comment",
token => GetTransformedDocumentAsync(context.Document, diagnostic, token)), diagnostic);
}
return Task.FromResult<object>(null);
}
Now that the code fix is registered we can implement the GetTransformedDocumentAsync
method to get a new document that represents the fixed code. To start, we can simply find the node in the document and replace it with a blank node.
private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var node = root.FindTrivia(diagnostic.Location.SourceSpan.Start, true);
var newDoc = document.WithSyntaxRoot(root.ReplaceTrivia(node, new SyntaxTrivia()));
return newDoc;
}
If you run this analyzer as is, it correctly removes the comment node. So, we are done, right? Not exactly. While it removes the comment node, it leaves the blank line there, which you would expect to also go away. You might be tempted to just find the node after the comment and if it is a SyntaxKind.EndOfLineTrivia
then remove it. However, if you do this, you will run into scenarios where this doesn't work well. For example if there was content on the same line prior to the empty comment, you don't want to remove the blank line.
public void Bar()
{
System.Console.WriteLine(""A""); //
}
If you removed the blank line in this case, the resulting code would be:
public void Bar()
{
System.Console.WriteLine(""A""); }
So, when it comes down to it, we need to have a few basic rules in place for determining what to do next.
- We will remove the whitespace prior to the comment node if there is any and there is no trailing content on the same line.
- We will remove an end of line node following the comment node if there is one and there is no leading content on the same line.
In order to determine if there is leading content, we need to move to the node prior to the list of trivia in which our diagnostic node is contained and see if it is on the same line. The triviaList
is calculated by a helper method which combines all of the leading and trailing trivia around the node we care about.
private static bool TriviaHasLeadingContentOnLine(SyntaxNode root, IReadOnlyList<SyntaxTrivia> triviaList)
{
var nodeBeforeStart = triviaList[0].SpanStart - 1;
var nodeBefore = root.FindNode(new Microsoft.CodeAnalysis.Text.TextSpan(nodeBeforeStart, 1));
if (GetLineSpan(nodeBefore).EndLinePosition.Line == GetLineSpan(triviaList[0]).StartLinePosition.Line)
{
return true;
}
return false;
}
To determine if there is trailing content, we do the same, just looking at the node after the last node in the list of trivia.
private static bool TriviaHasTrailingContentOnLine(SyntaxNode root, IReadOnlyList<SyntaxTrivia> triviaList)
{
var nodeAfterTriviaStart = triviaList[triviaList.Count - 1].SpanStart - 1;
var nodeAfterTrivia = root.FindNode(new Microsoft.CodeAnalysis.Text.TextSpan(nodeAfterTriviaStart, 1));
if (GetLineSpan(nodeAfterTrivia).StartLinePosition.Line == GetLineSpan(triviaList[triviaList.Count - 1]).EndLinePosition.Line)
{
return true;
}
return false;
}
Now that we have those helper methods, we can use them inside our GetTransformedDocumentAnsync
method and follow the rules we laid out above:
private static async Task<Document> GetTransformedDocumentAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var node = root.FindTrivia(diagnostic.Location.SourceSpan.Start, true);
int diagnosticIndex = 0;
var triviaList = TriviaHelper.GetContainingTriviaList(node, out diagnosticIndex);
var nodesToRemove = new List<SyntaxTrivia>();
nodesToRemove.Add(node);
// If there is trialing content on the line, we don't want to remove the leading whitespace
bool hasTrailingContent = TriviaHasTrailingContentOnLine(root, triviaList);
if (diagnosticIndex > 0 && !hasTrailingContent)
{
var previousStart = triviaList[diagnosticIndex - 1].SpanStart;
var previousNode = root.FindTrivia(previousStart, true);
nodesToRemove.Add(previousNode);
}
// If there is leading content on the line, then we don't want to remove the trailing end of lines
bool hasLeadingContent = TriviaHasLeadingContentOnLine(root, triviaList);
if (diagnosticIndex < triviaList.Count - 1)
{
var nextStart = triviaList[diagnosticIndex + 1].SpanStart;
var nextNode = root.FindTrivia(nextStart, true);
if (nextNode.IsKind(SyntaxKind.EndOfLineTrivia) && !hasLeadingContent)
{
nodesToRemove.Add(nextNode);
}
}
// Replace all roots with an empty node
var newRoot = root.ReplaceTrivia(nodesToRemove, (original, rewritten) =>
{
return new SyntaxTrivia();
});
Document updatedDocument = document.WithSyntaxRoot(newRoot);
return updatedDocument;
}
Notice that we now have a list of nodes that we will remove from the SyntaxRoot
and we use the ReplaceTrivia
overload that allows us to specify an IEnumerable<SyntaxTrivia>
.
As I mentioned in the previous article about this analyzer, it is diagnostic SA1120 in the StyleCopAnalyzers project. The full code fix is named SA120CodeFixProvider.The exact code sample I worked on here is available in my AnalyzersSamples project on Github under the EmptyCommentsCodeFix.
Now we have an analyzer and a code fix. What's missing? Unit tests. The next time we delve into this analyzer we will go through how we can unit test this to make sure we are handling all of the cases we want to handle correctly.