|
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information.
Imports System.Threading
Imports Analyzer.Utilities
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.CodeActions
Imports Microsoft.CodeAnalysis.CodeFixes
Imports Microsoft.CodeAnalysis.Editing
Imports Microsoft.CodeAnalysis.VisualBasic
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Imports Microsoft.NetCore.Analyzers.Performance
Namespace Microsoft.NetCore.VisualBasic.Analyzers.Performance
<ExportCodeFixProvider(LanguageNames.VisualBasic)>
Public NotInheritable Class BasicPreferDictionaryTryMethodsOverContainsKeyGuardFixer
Inherits PreferDictionaryTryMethodsOverContainsKeyGuardFixer
Public Overrides Async Function RegisterCodeFixesAsync(context As CodeFixContext) As Task
Dim diagnostic = context.Diagnostics.FirstOrDefault()
If diagnostic Is Nothing OrElse diagnostic.AdditionalLocations.Count < 0 Then
Return
End If
Dim document = context.Document
Dim root = Await document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(False)
Dim containsKeyInvocation = TryCast(root.FindNode(context.Span), InvocationExpressionSyntax)
Dim containsKeyAccess = TryCast(containsKeyInvocation?.Expression, MemberAccessExpressionSyntax)
If containsKeyInvocation Is Nothing OrElse containsKeyAccess Is Nothing Then
Return
End If
Dim action = If(diagnostic.Id = PreferDictionaryTryMethodsOverContainsKeyGuardAnalyzer.PreferTryGetValueRuleId,
Await GetTryGetValueActionAsync(root, diagnostic, document, containsKeyAccess, containsKeyInvocation, context.CancellationToken).ConfigureAwait(False),
GetTryAddAction(root, diagnostic, document, containsKeyAccess, containsKeyInvocation))
If action Is Nothing Then
Return
End If
context.RegisterCodeFix(action, context.Diagnostics)
End Function
Private Shared Async Function GetTryGetValueActionAsync(root As SyntaxNode, diagnostic As Diagnostic, document As Document, containsKeyAccess As MemberAccessExpressionSyntax, containsKeyInvocation As InvocationExpressionSyntax, cancellationToken As CancellationToken) As Task(Of CodeAction)
Dim dictionaryAccessors As New List(Of SyntaxNode)
Dim addStatementNode As ExecutableStatementSyntax = Nothing
Dim changedValueNode As SyntaxNode = Nothing
Dim variableName As String = Nothing
Dim additionalNodes = 0
Dim localDeclarationStatement As LocalDeclarationStatementSyntax = Nothing
Dim variableDeclarator As VariableDeclaratorSyntax = Nothing
For Each location As Location In diagnostic.AdditionalLocations
Dim node = root.FindNode(location.SourceSpan, getInnermostNodeForTie:=True)
Select Case node.GetType()
Case GetType(InvocationExpressionSyntax)
Dim invocation = DirectCast(node, InvocationExpressionSyntax)
If invocation.ArgumentList.Arguments.Count = 2 Then
Dim add = TryCast(invocation.Expression, MemberAccessExpressionSyntax)
If addStatementNode IsNot Nothing OrElse
add Is Nothing OrElse
add.Name.Identifier.Text <> PreferDictionaryTryMethodsOverContainsKeyGuardAnalyzer.Add Then
Return Nothing
End If
changedValueNode = invocation.ArgumentList.Arguments(1).GetExpression()
addStatementNode = invocation.FirstAncestorOrSelf(Of ExpressionStatementSyntax)
additionalNodes += 1
Else
dictionaryAccessors.Add(node)
End If
Case GetType(MemberAccessExpressionSyntax)
Dim memberAccess = DirectCast(node, MemberAccessExpressionSyntax)
If memberAccess.Kind() <> SyntaxKind.DictionaryAccessExpression Then
Return Nothing
End If
dictionaryAccessors.Add(node)
Case GetType(AssignmentStatementSyntax)
If addStatementNode IsNot Nothing Then
Return Nothing
End If
Dim assignment = DirectCast(node, AssignmentStatementSyntax)
changedValueNode = assignment.Right
addStatementNode = assignment
additionalNodes += 1
Case GetType(LocalDeclarationStatementSyntax)
localDeclarationStatement = DirectCast(node, LocalDeclarationStatementSyntax)
variableName = localDeclarationStatement.Declarators.Item(0).Names.Item(0).Identifier.ValueText
additionalNodes += 1
Case GetType(VariableDeclaratorSyntax)
variableDeclarator = DirectCast(node, VariableDeclaratorSyntax)
If variableDeclarator.Parent.GetType() <> GetType(LocalDeclarationStatementSyntax) Then
Return Nothing
End If
localDeclarationStatement = DirectCast(variableDeclarator.Parent, LocalDeclarationStatementSyntax)
variableName = variableDeclarator.Names.Item(0).Identifier.ValueText
additionalNodes += 1
Case Else
Return Nothing
End Select
Next
If diagnostic.AdditionalLocations.Count <> dictionaryAccessors.Count + additionalNodes Then
Return Nothing
End If
Dim semanticModel = Await document.GetSemanticModelAsync(cancellationToken).
ConfigureAwait(False)
Dim dictionaryValueType = GetDictionaryValueType(semanticModel, containsKeyAccess.Expression)
Dim replaceFunction =
Async Function(ct As CancellationToken) As Task(Of Document)
Dim editor = Await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(False)
Dim generator = editor.Generator
Dim identifierName = DirectCast(If(variableName Is Nothing,
generator.FirstUnusedIdentifierName(semanticModel,
containsKeyAccess.SpanStart,
Value),
generator.IdentifierName(variableName)),
IdentifierNameSyntax)
Dim tryGetValueAccess = generator.MemberAccessExpression(containsKeyAccess.Expression,
TryGetValue)
Dim keyArgument = containsKeyInvocation.ArgumentList.Arguments.FirstOrDefault()
Dim valueAssignment =
generator.LocalDeclarationStatement(dictionaryValueType,
identifierName.Identifier.ValueText,
generator.DefaultExpression(dictionaryValueType)).
WithLeadingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed).
WithoutTrailingTrivia()
Dim tryGetValueInvocation = generator.InvocationExpression(tryGetValueAccess,
keyArgument,
generator.Argument(identifierName))
#Disable Warning IDE0270 ' Use coalesce expression - suppressed for readability
Dim ifStatement As SyntaxNode = containsKeyAccess.FirstAncestorOrSelf(Of MultiLineIfBlockSyntax)
If ifStatement Is Nothing Then
ifStatement = containsKeyAccess.FirstAncestorOrSelf(Of SingleLineIfStatementSyntax)
End If
#Enable Warning IDE0270 ' Use coalesce expression
If ifStatement Is Nothing Then
' For ternary expressions, we need to add the value assignment before the parent of
' the expression, since the ternary expression is not an alone-standing expression.
ifStatement = containsKeyAccess.FirstAncestorOrSelf(Of TernaryConditionalExpressionSyntax)?.Parent
End If
If Not ifStatement.HasLeadingTrivia OrElse
Not ifStatement.GetLeadingTrivia().Any(Function(t) t.RawKind = SyntaxKind.EndOfLineTrivia) Then
valueAssignment = valueAssignment.WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed)
End If
editor.InsertBefore(ifStatement, valueAssignment)
editor.ReplaceNode(containsKeyInvocation, tryGetValueInvocation)
If addStatementNode IsNot Nothing Then
Dim newValueAssignment As SyntaxNode = generator.ExpressionStatement(
generator.AssignmentStatement(identifierName, changedValueNode)).
WithTrailingTrivia(SyntaxFactory.ElasticMarker)
editor.InsertBefore(addStatementNode, newValueAssignment)
editor.ReplaceNode(changedValueNode, identifierName)
End If
For Each dictionaryAccess In dictionaryAccessors
editor.ReplaceNode(dictionaryAccess, identifierName)
Next
If localDeclarationStatement IsNot Nothing Then
If variableDeclarator Is Nothing Then
editor.RemoveNode(localDeclarationStatement)
Else
editor.RemoveNode(variableDeclarator)
End If
End If
Return editor.GetChangedDocument()
End Function
Return CodeAction.Create(PreferDictionaryTryGetValueCodeFixTitle, replaceFunction, PreferDictionaryTryGetValueCodeFixTitle)
End Function
Private Shared Function GetTryAddAction(root As SyntaxNode, diagnostic As Diagnostic, document As Document, containsKeyAccess As MemberAccessExpressionSyntax, containsKeyInvocation As InvocationExpressionSyntax) As CodeAction
Dim dictionaryAddLocation = diagnostic.AdditionalLocations(0)
Dim dictionaryAddInvocation = TryCast(root.FindNode(dictionaryAddLocation.SourceSpan, getInnermostNodeForTie:=True), InvocationExpressionSyntax)
Dim replaceFunction = Async Function(ct As CancellationToken) As Task(Of Document)
Dim editor = Await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(False)
Dim generator = editor.Generator
Dim tryAddValueAccess = generator.MemberAccessExpression(containsKeyAccess.Expression, TryAdd)
Dim dictionaryAddArguments = dictionaryAddInvocation.ArgumentList.Arguments
Dim tryAddInvocation = generator.InvocationExpression(tryAddValueAccess, dictionaryAddArguments(0), dictionaryAddArguments(1))
Dim ifStatement = containsKeyInvocation.AncestorsAndSelf().OfType(Of MultiLineIfBlockSyntax).FirstOrDefault()
If ifStatement Is Nothing Then
Return editor.OriginalDocument
End If
Dim unary = TryCast(ifStatement.IfStatement.Condition, UnaryExpressionSyntax)
If unary IsNot Nothing And unary.IsKind(SyntaxKind.NotExpression) Then
If ifStatement.Statements.Count = 1 Then
If ifStatement.ElseBlock Is Nothing Then
Dim invocationWithTrivia = tryAddInvocation.WithTriviaFrom(ifStatement)
editor.ReplaceNode(ifStatement, generator.ExpressionStatement(invocationWithTrivia))
Else
Dim newIf = ifStatement.WithStatements(ifStatement.ElseBlock.Statements).
WithElseBlock(Nothing).
WithIfStatement(ifStatement.IfStatement.ReplaceNode(containsKeyInvocation, tryAddInvocation))
editor.ReplaceNode(ifStatement, newIf)
End If
Else
editor.RemoveNode(dictionaryAddInvocation.Parent, SyntaxRemoveOptions.KeepNoTrivia)
editor.ReplaceNode(unary, tryAddInvocation)
End If
ElseIf ifStatement.IfStatement.Condition.IsKind(SyntaxKind.InvocationExpression) And ifStatement.ElseBlock IsNot Nothing Then
Dim negatedTryAddInvocation = generator.LogicalNotExpression(tryAddInvocation)
editor.ReplaceNode(containsKeyInvocation, negatedTryAddInvocation)
If ifStatement.ElseBlock.Statements.Count = 1 Then
editor.RemoveNode(ifStatement.ElseBlock, SyntaxRemoveOptions.KeepNoTrivia)
Else
editor.RemoveNode(dictionaryAddInvocation.Parent, SyntaxRemoveOptions.KeepNoTrivia)
End If
End If
Return editor.GetChangedDocument()
End Function
Return CodeAction.Create(PreferDictionaryTryAddValueCodeFixTitle, replaceFunction, PreferDictionaryTryAddValueCodeFixTitle)
End Function
Private Shared Function GetDictionaryValueType(semanticModel As SemanticModel, dictionary As SyntaxNode) As ITypeSymbol
Dim type = DirectCast(semanticModel.GetTypeInfo(dictionary).Type, INamedTypeSymbol)
Return type.TypeArguments(1)
End Function
End Class
End Namespace
|