Skip to content

Instantly share code, notes, and snippets.

@emir01
Last active November 15, 2020 23:57
Show Gist options
  • Save emir01/ee980807fed54736068118e41c84f7ba to your computer and use it in GitHub Desktop.
Save emir01/ee980807fed54736068118e41c84f7ba to your computer and use it in GitHub Desktop.
Final Fix Provider for the Roslyn Post Series on the Blog
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using MultipleMethodCallAnalyzer.Diagnostics.SyntaxFactories;
namespace MultipleMethodCallAnalyzer.Diagnostics
{
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(FixProvider))]
[Shared]
public class FixProvider : CodeFixProvider
{
private const string title = "Combine Multiple Method Usage";
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(Analyzer.DiagnosticId);
public sealed override FixAllProvider GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
}
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
// getting first because we are only listening to a single diagnostic here
// !important - we might have multiple diagnostics of the same type here for different places and methods in the code ?
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
// Find the type invocation identified by the diagnostic.
// we might need to find the multiple invocations?
var invocation = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf()
.OfType<InvocationExpressionSyntax>().First();
// Register a code action that will invoke the fix.
context.RegisterCodeFix(
CodeAction.Create(
title,
c => CombineMethodUsageAsync(context.Document, invocation, c),
title),
diagnostic);
}
private async Task<Document> CombineMethodUsageAsync(Document contextDocument,
InvocationExpressionSyntax invocationRequestingFix,
CancellationToken cancellationToken)
{
// need to get all occurrences of the invocation of our method
var expressionName = invocationRequestingFix.Expression.ToString();
var documentEditor = await DocumentEditor.CreateAsync(contextDocument, cancellationToken);
// find all of the Invocation Expression matching the expression name
var originalInvocationsMatchingFixRequestInvocation = documentEditor
.OriginalRoot
.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.Where(x => x.Expression.ToString().Equals(expressionName))
.Where(
x =>
x.FirstAncestorOrSelf<SyntaxNode>(t => t.IsKind(SyntaxKind.Block)) ==
invocationRequestingFix.FirstAncestorOrSelf<SyntaxNode>(t => t.IsKind(SyntaxKind.Block)))
.ToList();
// if there is only one invocation
if (originalInvocationsMatchingFixRequestInvocation.Count <= 1) return contextDocument;
// assume the first invocation is assigned to a variale and will be skipped
// variable is determined in next line of code.
var originalInvocationsToSkipWhenFixing = 1;
var variableIdentifier =
CheckIfFirstInvocationAssignedToVariableAndReturnVariableIdentifier(
originalInvocationsMatchingFixRequestInvocation);
// this means the first invocation of multiple is not assigned to a variable
// we will have to create a new invocation and assign it to a variable we can use to replace
// all the other invocations
if (variableIdentifier.IsKind(SyntaxKind.None))
{
var variableDeclarator = SyntaxFactory
.VariableDeclarator(GetNewDeclaredVariableForExpression(expressionName))
.WithInitializer(
SyntaxFactory.EqualsValueClause(
invocationRequestingFix
)
);
// Create the Variable declaration.
var variableDeclaration = SyntaxFactory.VariableDeclaration(TypeSyntaxFactory.GetTypeSyntax("var"))
.WithVariables(SyntaxFactory.SeparatedList<VariableDeclaratorSyntax>().Add(variableDeclarator));
var localDeclaration = SyntaxFactory.LocalDeclarationStatement(variableDeclaration)
.WithAdditionalAnnotations(Formatter.Annotation);
AddLocalDeclarationBeforeFirstReplacedInvocation(originalInvocationsMatchingFixRequestInvocation,
localDeclaration,
documentEditor);
variableIdentifier = variableDeclarator.Identifier;
originalInvocationsToSkipWhenFixing = 0;
}
var invocationsToReplace =
originalInvocationsMatchingFixRequestInvocation.Skip(originalInvocationsToSkipWhenFixing).ToList();
foreach (var invocation in invocationsToReplace)
{
var invocationParent = invocation.Parent;
var newNode = SyntaxFactory.IdentifierName(variableIdentifier.Text);
var newInvocationParent = invocationParent.ReplaceNode(invocation, newNode);
documentEditor.ReplaceNode(invocationParent, newInvocationParent);
}
return documentEditor.GetChangedDocument();
}
private static void AddLocalDeclarationBeforeFirstReplacedInvocation(
List<InvocationExpressionSyntax> allInvocationExpressions,
LocalDeclarationStatementSyntax localDeclaration, DocumentEditor editor)
{
var firstInvocationOccurrence = allInvocationExpressions.First();
var firstInvocationTopLevelParent = GetNodeTopLevelContainerThatIsChildOfCodeBlock(firstInvocationOccurrence);
var locDeclaration = localDeclaration.WithLeadingTrivia(firstInvocationTopLevelParent.GetLeadingTrivia());
editor.InsertBefore(firstInvocationTopLevelParent, locDeclaration);
}
private string GetNewDeclaredVariableForExpression(string expressionName)
{
var sanitizedExpressionName = expressionName.Replace(".", "");
var newVariableName = $"{sanitizedExpressionName}Result";
newVariableName = char.ToLower(newVariableName[0]) +
newVariableName.Substring(1, newVariableName.Length - 1);
return newVariableName;
}
private static SyntaxToken CheckIfFirstInvocationAssignedToVariableAndReturnVariableIdentifier(
List<InvocationExpressionSyntax> allInvocationExpressions)
{
// we need the first invocation
var firstInvocation = allInvocationExpressions.First();
// get the top level "invocation" container
var topLevelInvocationContainer = GetNodeTopLevelContainerThatIsChildOfCodeBlock(firstInvocation);
// The variable identifier of the first element
var variableIdentifier = new SyntaxToken();
// check if it is a variable declaration meaning that we will have a variable we can re-use
if (topLevelInvocationContainer.IsKind(SyntaxKind.LocalDeclarationStatement))
{
var variableDeclarator =
topLevelInvocationContainer
.DescendantNodes().ToList().Where(x => x.IsKind(SyntaxKind.VariableDeclarator))
.FirstOrDefault() as VariableDeclaratorSyntax;
variableIdentifier = variableDeclarator.Identifier;
}
return variableIdentifier;
}
private static SyntaxNode GetNodeTopLevelContainerThatIsChildOfCodeBlock(SyntaxNode node)
{
return node.FirstAncestorOrSelf<SyntaxNode>(x => x.Parent.IsKind(SyntaxKind.Block));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment