File: CaseCorrection\VisualBasicCaseCorrectionService.Rewriter.vb
Web Access
Project: src\src\Workspaces\VisualBasic\Portable\Microsoft.CodeAnalysis.VisualBasic.Workspaces.vbproj (Microsoft.CodeAnalysis.VisualBasic.Workspaces)
' Licensed to the .NET Foundation under one or more agreements.
' The .NET Foundation licenses this file to you under the MIT license.
' See the LICENSE file in the project root for more information.
 
Imports System.Collections.Immutable
Imports System.Runtime.InteropServices
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.LanguageService
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
 
Namespace Microsoft.CodeAnalysis.VisualBasic.CaseCorrection
    Partial Friend Class VisualBasicCaseCorrectionService
        Private Class Rewriter
            Inherits VisualBasicSyntaxRewriter
 
            Private ReadOnly _createAliasSet As Func(Of ImmutableHashSet(Of String)) =
                Function()
                    Dim model = Me._semanticModel.GetOriginalSemanticModel()
 
                    ' root should be already available
                    If Not model.SyntaxTree.HasCompilationUnitRoot Then
                        Return ImmutableHashSet.Create(Of String)()
                    End If
 
                    Dim root = model.SyntaxTree.GetCompilationUnitRoot()
 
                    Dim [set] = ImmutableHashSet.CreateBuilder(Of String)(StringComparer.OrdinalIgnoreCase)
                    For Each importsClause In root.GetAliasImportsClauses()
                        If Not String.IsNullOrWhiteSpace(importsClause.Alias.Identifier.ValueText) Then
                            [set].Add(importsClause.Alias.Identifier.ValueText)
                        End If
                    Next
 
                    For Each import In model.Compilation.AliasImports
                        [set].Add(import.Name)
                    Next
 
                    Return [set].ToImmutable()
                End Function
 
            Private ReadOnly _syntaxFactsService As ISyntaxFactsService
            Private ReadOnly _semanticModel As SemanticModel
            Private ReadOnly _aliasSet As Lazy(Of ImmutableHashSet(Of String))
            Private ReadOnly _cancellationToken As CancellationToken
 
            Public Sub New(syntaxFactsService As ISyntaxFactsService, semanticModel As SemanticModel, cancellationToken As CancellationToken)
                MyBase.New(visitIntoStructuredTrivia:=True)
                Me._syntaxFactsService = syntaxFactsService
                Me._semanticModel = semanticModel
                Me._aliasSet = New Lazy(Of ImmutableHashSet(Of String))(_createAliasSet)
                Me._cancellationToken = cancellationToken
            End Sub
 
            Public Overrides Function VisitToken(token As SyntaxToken) As SyntaxToken
                Dim newToken = MyBase.VisitToken(token)
 
                If _syntaxFactsService.IsIdentifier(newToken) Then
                    Return VisitIdentifier(token, newToken)
                ElseIf _syntaxFactsService.IsReservedOrContextualKeyword(newToken) Then
                    Return VisitKeyword(newToken)
                ElseIf token.IsNumericLiteral() Then
                    Return VisitNumericLiteral(newToken)
                ElseIf token.IsCharacterLiteral() Then
                    Return VisitCharacterLiteral(newToken)
                End If
 
                Return newToken
            End Function
 
            Private Function VisitIdentifier(token As SyntaxToken, newToken As SyntaxToken) As SyntaxToken
                If newToken.IsMissing OrElse TypeOf newToken.Parent Is ArgumentSyntax OrElse _semanticModel Is Nothing Then
                    Return newToken
                End If
 
                If token.Parent.IsPartOfStructuredTrivia() Then
                    Dim identifierSyntax = TryCast(token.Parent, IdentifierNameSyntax)
                    If identifierSyntax IsNot Nothing Then
                        Dim preprocessingSymbolInfo = _semanticModel.GetPreprocessingSymbolInfo(identifierSyntax)
                        If preprocessingSymbolInfo.Symbol IsNot Nothing Then
                            Dim name = preprocessingSymbolInfo.Symbol.Name
                            If Not String.IsNullOrEmpty(name) AndAlso name <> token.ValueText Then
                                ' Name should differ only in case
                                Debug.Assert(name.Equals(token.ValueText, StringComparison.OrdinalIgnoreCase))
 
                                Return GetIdentifierWithCorrectedName(name, newToken)
                            End If
                        End If
                    End If
 
                    Return newToken
                Else
                    Dim methodDeclaration = TryCast(token.Parent, MethodStatementSyntax)
                    If methodDeclaration IsNot Nothing Then
                        ' If this is a partial method implementation part, then case correct the method name to match the partial method definition part.
                        Dim definitionPart As IMethodSymbol = Nothing
                        Dim otherPartOfPartial = GetOtherPartOfPartialMethod(methodDeclaration, definitionPart)
                        If otherPartOfPartial IsNot Nothing And Equals(otherPartOfPartial, definitionPart) Then
                            Return CaseCorrectIdentifierIfNamesDiffer(token, newToken, otherPartOfPartial)
                        End If
                    Else
                        Dim parameterSyntax = token.GetAncestor(Of ParameterSyntax)()
                        If parameterSyntax IsNot Nothing Then
                            ' If this is a parameter declaration for a partial method implementation part,
                            ' then case correct the parameter name to match the corresponding parameter in the partial method definition part.
                            methodDeclaration = parameterSyntax.GetAncestor(Of MethodStatementSyntax)()
                            If methodDeclaration IsNot Nothing Then
                                Dim definitionPart As IMethodSymbol = Nothing
                                Dim otherPartOfPartial = GetOtherPartOfPartialMethod(methodDeclaration, definitionPart)
                                If otherPartOfPartial IsNot Nothing And Equals(otherPartOfPartial, definitionPart) Then
                                    Dim ordinal As Integer = 0
                                    For Each param As SyntaxNode In methodDeclaration.ParameterList.Parameters
                                        If param Is parameterSyntax Then
                                            Exit For
                                        End If
 
                                        ordinal = ordinal + 1
                                    Next
 
                                    Debug.Assert(otherPartOfPartial.Parameters.Length > ordinal)
                                    Dim otherPartParam = otherPartOfPartial.Parameters(ordinal)
 
                                    ' We don't want to rename the parameter if names are not equal ignoring case.
                                    ' Compiler will anyways generate an error for this case.
                                    Return CaseCorrectIdentifierIfNamesDiffer(token, newToken, otherPartParam, namesMustBeEqualIgnoringCase:=True)
                                End If
                            End If
                        Else
                            ' Named tuple expression
                            Dim nameColonEquals = TryCast(token.Parent?.Parent, NameColonEqualsSyntax)
                            If nameColonEquals IsNot Nothing AndAlso TypeOf nameColonEquals.Parent?.Parent Is TupleExpressionSyntax Then
                                Return newToken
                            End If
                        End If
                    End If
                End If
 
                Dim symbol = GetAliasOrAnySymbol(_semanticModel, token.Parent, _cancellationToken)
                If symbol Is Nothing Then
                    Return newToken
                End If
 
                Dim expression = TryCast(token.Parent, ExpressionSyntax)
                If expression IsNot Nothing AndAlso SyntaxFacts.IsInNamespaceOrTypeContext(expression) AndAlso Not IsNamespaceOrTypeRelatedSymbol(symbol) Then
                    Return newToken
                End If
 
                If TypeOf symbol Is ITypeSymbol AndAlso DirectCast(symbol, ITypeSymbol).TypeKind = TypeKind.Error Then
                    Return newToken
                End If
 
                ' If it's a constructor we bind to, then we want to compare the name on the token to the
                ' name of the type.  The name of the bound symbol will be something useless like '.ctor'.
                ' However, if it's an explicit New on the right side of a member access or qualified name, we want to use "New".
                If symbol.IsConstructor Then
                    If token.IsNewOnRightSideOfDotOrBang() Then
                        Return SyntaxFactory.Identifier(newToken.LeadingTrivia, "New", newToken.TrailingTrivia)
                    End If
 
                    symbol = symbol.ContainingType
                End If
 
                Return CaseCorrectIdentifierIfNamesDiffer(token, newToken, symbol)
            End Function
 
            Private Shared Function IsNamespaceOrTypeRelatedSymbol(symbol As ISymbol) As Boolean
                Return TypeOf symbol Is INamespaceOrTypeSymbol OrElse
                    (TypeOf symbol Is IAliasSymbol AndAlso TypeOf DirectCast(symbol, IAliasSymbol).Target Is INamespaceOrTypeSymbol) OrElse
                    (symbol.IsKind(SymbolKind.Method) AndAlso DirectCast(symbol, IMethodSymbol).MethodKind = MethodKind.Constructor)
            End Function
 
            Private Function GetAliasOrAnySymbol(model As SemanticModel, node As SyntaxNode, cancellationToken As CancellationToken) As ISymbol
                Dim identifier = TryCast(node, IdentifierNameSyntax)
                If identifier IsNot Nothing AndAlso Me._aliasSet.Value.Contains(identifier.Identifier.ValueText) Then
                    Dim [alias] = model.GetAliasInfo(identifier, cancellationToken)
                    If [alias] IsNot Nothing Then
                        Return [alias]
                    End If
                End If
 
                Return model.GetSymbolInfo(node, cancellationToken).GetAnySymbol()
            End Function
 
            Private Shared Function CaseCorrectIdentifierIfNamesDiffer(
                token As SyntaxToken,
                newToken As SyntaxToken,
                symbol As ISymbol,
                Optional namesMustBeEqualIgnoringCase As Boolean = False
            ) As SyntaxToken
                If NamesDiffer(symbol, token) Then
                    If namesMustBeEqualIgnoringCase AndAlso Not String.Equals(symbol.Name, token.ValueText, StringComparison.OrdinalIgnoreCase) Then
                        Return newToken
                    End If
 
                    Dim correctedName = GetCorrectedName(token, symbol)
                    Return GetIdentifierWithCorrectedName(correctedName, newToken)
                End If
 
                Return newToken
            End Function
 
            Private Function GetOtherPartOfPartialMethod(methodDeclaration As MethodStatementSyntax, <Out> ByRef definitionPart As IMethodSymbol) As IMethodSymbol
                Contract.ThrowIfNull(methodDeclaration)
                Contract.ThrowIfNull(_semanticModel)
 
                Dim methodSymbol = _semanticModel.GetDeclaredSymbol(methodDeclaration, _cancellationToken)
                If methodSymbol IsNot Nothing Then
                    definitionPart = If(methodSymbol.PartialDefinitionPart, methodSymbol)
                    Return If(methodSymbol.PartialDefinitionPart, methodSymbol.PartialImplementationPart)
                End If
 
                Return Nothing
            End Function
 
            Private Shared Function GetCorrectedName(token As SyntaxToken, symbol As ISymbol) As String
                If symbol.IsAttribute Then
                    If String.Equals(token.ValueText & s_attributeSuffix, symbol.Name, StringComparison.OrdinalIgnoreCase) Then
                        Return symbol.Name.Substring(0, symbol.Name.Length - s_attributeSuffix.Length)
                    End If
                End If
 
                Return symbol.Name
            End Function
 
            Private Shared Function GetIdentifierWithCorrectedName(correctedName As String, token As SyntaxToken) As SyntaxToken
                If token.IsBracketed Then
                    Return SyntaxFactory.BracketedIdentifier(token.LeadingTrivia, correctedName, token.TrailingTrivia)
                Else
                    Return SyntaxFactory.Identifier(token.LeadingTrivia, correctedName, token.TrailingTrivia)
                End If
            End Function
 
            Private Shared Function NamesDiffer(symbol As ISymbol,
                                         token As SyntaxToken) As Boolean
                If String.IsNullOrEmpty(symbol.Name) Then
                    Return False
                End If
 
                If symbol.Name = token.ValueText Then
                    Return False
                End If
 
                If symbol.IsAttribute() Then
                    If symbol.Name = token.ValueText & s_attributeSuffix Then
                        Return False
                    End If
                End If
 
                Return True
            End Function
 
            Private Function VisitKeyword(token As SyntaxToken) As SyntaxToken
                If Not token.IsMissing Then
                    Dim actualText = token.ToString()
                    Dim expectedText = _syntaxFactsService.GetText(token.Kind)
 
                    If Not String.IsNullOrWhiteSpace(expectedText) AndAlso actualText <> expectedText Then
                        Return SyntaxFactory.Token(token.LeadingTrivia, token.Kind, token.TrailingTrivia, expectedText)
                    End If
                End If
 
                Return token
            End Function
 
            Private Shared Function VisitNumericLiteral(token As SyntaxToken) As SyntaxToken
                If Not token.IsMissing Then
 
                    ' For any numeric literal, we simply case correct any letters to uppercase.
                    ' The only letters allowed in a numeric literal are:
                    '   * Type characters: S, US, I, UI, L, UL, D, F, R
                    '   * Hex/Octal literals: H, O and A, B, C, D, E, F
                    '   * Exponent: E
                    '   * Time literals: AM, PM 
 
                    Dim actualText = token.ToString()
                    Dim expectedText = actualText.ToUpperInvariant()
 
                    If actualText <> expectedText Then
                        Return SyntaxFactory.ParseToken(expectedText).WithLeadingTrivia(token.LeadingTrivia).WithTrailingTrivia(token.TrailingTrivia)
                    End If
                End If
 
                Return token
            End Function
 
            Private Shared Function VisitCharacterLiteral(token As SyntaxToken) As SyntaxToken
                If Not token.IsMissing Then
 
                    ' For character literals, we case correct the type character to "c".
                    Dim actualText = token.ToString()
 
                    If actualText.EndsWith("C", StringComparison.Ordinal) Then
                        Dim expectedText = actualText.Substring(0, actualText.Length - 1) & "c"
                        Return SyntaxFactory.ParseToken(expectedText).WithLeadingTrivia(token.LeadingTrivia).WithTrailingTrivia(token.TrailingTrivia)
                    End If
                End If
 
                Return token
            End Function
 
            Public Overrides Function VisitTrivia(trivia As SyntaxTrivia) As SyntaxTrivia
                trivia = MyBase.VisitTrivia(trivia)
 
                If trivia.Kind = SyntaxKind.CommentTrivia AndAlso trivia.Width >= 3 Then
                    Dim remText = trivia.ToString().Substring(0, 3)
                    Dim remKeywordText As String = _syntaxFactsService.GetText(SyntaxKind.REMKeyword)
                    If remText <> remKeywordText AndAlso SyntaxFacts.GetKeywordKind(remText) = SyntaxKind.REMKeyword Then
                        Dim expectedText = remKeywordText & trivia.ToString().Substring(3)
                        Return SyntaxFactory.CommentTrivia(expectedText)
                    End If
                End If
 
                Return trivia
            End Function
 
        End Class
    End Class
End Namespace