-
-
Save emir01/ee980807fed54736068118e41c84f7ba to your computer and use it in GitHub Desktop.
Final Fix Provider for the Roslyn Post Series on the Blog
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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