File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\VisualBasic\Utilities\CastAnalyzer.vb
Web Access
Project: src\src\CodeStyle\VisualBasic\Analyzers\Microsoft.CodeAnalysis.VisualBasic.CodeStyle.vbproj (Microsoft.CodeAnalysis.VisualBasic.CodeStyle)
' 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.Runtime.InteropServices
Imports System.Threading
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.VisualBasic.Symbols
Imports Microsoft.CodeAnalysis.VisualBasic.Syntax
Imports Microsoft.CodeAnalysis.VisualBasic.Utilities
 
Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions
    Friend NotInheritable Class CastAnalyzer
        Private ReadOnly _castNode As ExpressionSyntax
        Private ReadOnly _castExpressionNode As ExpressionSyntax
        Private ReadOnly _semanticModel As SemanticModel
        Private ReadOnly _assumeCallKeyword As Boolean
        Private ReadOnly _cancellationToken As CancellationToken
 
        Private Sub New(
                castNode As ExpressionSyntax,
                castExpressionNode As ExpressionSyntax,
                semanticModel As SemanticModel,
                assumeCallKeyword As Boolean,
                cancellationToken As CancellationToken)
            _castNode = castNode
            _castExpressionNode = castExpressionNode
            _semanticModel = semanticModel
            _assumeCallKeyword = assumeCallKeyword
            _cancellationToken = cancellationToken
        End Sub
 
        Private Function CastPassedToParamArrayDefinitelyCantBeRemoved(castType As ITypeSymbol) As Boolean
            If _castExpressionNode.WalkDownParentheses().IsKind(SyntaxKind.NothingLiteralExpression) Then
                Dim argument = TryCast(_castNode.WalkUpParentheses().Parent, ArgumentSyntax)
                If argument IsNot Nothing Then
                    Dim parameter = argument.DetermineParameter(_semanticModel, cancellationToken:=_cancellationToken)
                    If parameter?.IsParams = True AndAlso TypeOf parameter.Type Is IArrayTypeSymbol Then
                        Dim parameterType = DirectCast(parameter.Type, IArrayTypeSymbol)
 
                        Dim conversion = _semanticModel.Compilation.ClassifyConversion(castType, parameterType)
                        If conversion.Exists AndAlso conversion.IsWidening Then
                            Return False
                        End If
 
                        Dim conversionElementType = _semanticModel.Compilation.ClassifyConversion(castType, parameterType.ElementType)
                        If conversionElementType.Exists AndAlso (conversionElementType.IsIdentity OrElse conversionElementType.IsWidening) Then
                            Return True
                        End If
                    End If
                End If
            End If
 
            Return False
        End Function
 
        Private Shared Function GetOuterCastType(expression As ExpressionSyntax, expressionTypeInfo As TypeInfo, semanticModel As SemanticModel, cancellationToken As CancellationToken) As ITypeSymbol
            expression = expression.WalkUpParentheses()
            Dim parent = expression.Parent
 
            Dim parentExpression = TryCast(parent, ExpressionSyntax)
            If parentExpression IsNot Nothing Then
                If TypeOf parentExpression Is CastExpressionSyntax OrElse
               TypeOf parentExpression Is PredefinedCastExpressionSyntax Then
                    Return semanticModel.GetTypeInfo(parentExpression, cancellationToken).Type
                End If
            End If
 
            If Not Object.Equals(expressionTypeInfo.Type, expressionTypeInfo.ConvertedType) Then
                Return expressionTypeInfo.ConvertedType
            End If
 
            Dim parentEqualsValue = TryCast(parent, EqualsValueSyntax)
            If parentEqualsValue IsNot Nothing Then
                Dim returnedType = AsTypeInVariableDeclarator(parentEqualsValue.Parent, semanticModel)
 
                If returnedType IsNot Nothing Then
                    Return returnedType
                End If
            End If
 
            Dim parentForEach = TryCast(parent, ForEachStatementSyntax)
            If parentForEach IsNot Nothing Then
                Dim returnedType = AsTypeInVariableDeclarator(parentForEach.ControlVariable, semanticModel)
 
                If returnedType IsNot Nothing Then
                    Return returnedType
                End If
            End If
 
            Dim parentAssignmentStatement = TryCast(parent, AssignmentStatementSyntax)
            If parentAssignmentStatement IsNot Nothing AndAlso parent.Kind = SyntaxKind.SimpleAssignmentStatement Then
                Return semanticModel.GetTypeInfo(parentAssignmentStatement.Left, cancellationToken).Type
            End If
 
            Dim parentUnaryExpression = TryCast(parentExpression, UnaryExpressionSyntax)
            If parentUnaryExpression IsNot Nothing AndAlso Not semanticModel.GetConversion(expression, cancellationToken).IsUserDefined Then
                Dim parentTypeInfo = semanticModel.GetTypeInfo(parentUnaryExpression, cancellationToken)
                Return GetOuterCastType(parentUnaryExpression, parentTypeInfo, semanticModel, cancellationToken)
            End If
 
            Dim parentTernaryConditional = TryCast(parent, TernaryConditionalExpressionSyntax)
            If parentTernaryConditional IsNot Nothing AndAlso
               parentTernaryConditional.Condition IsNot expression Then
 
                Dim otherExpression = If(parentTernaryConditional.WhenTrue Is expression,
                                         parentTernaryConditional.WhenFalse,
                                         parentTernaryConditional.WhenTrue)
 
                Return semanticModel.GetTypeInfo(otherExpression, cancellationToken).Type
            End If
 
            Dim parentSimpleArgument = TryCast(parent, SimpleArgumentSyntax)
            If TypeOf parentSimpleArgument?.Expression Is CastExpressionSyntax OrElse
               TypeOf parentSimpleArgument?.Expression Is PredefinedCastExpressionSyntax Then
                Return semanticModel.GetTypeInfo(parentSimpleArgument.Expression, cancellationToken).Type
            End If
 
            Return expressionTypeInfo.ConvertedType
        End Function
 
        Private Shared Function GetSpeculatedExpressionToOuterTypeConversion(speculationAnalyzer As SpeculationAnalyzer, speculatedExpression As ExpressionSyntax, outerSpeculatedExpression As ExpressionSyntax, cancellationToken As CancellationToken, <Out> ByRef speculatedExpressionOuterType As ITypeSymbol) As Conversion
            Dim innerSpeculatedExpression = speculatedExpression.WalkDownParentheses()
            Dim typeInfo = speculationAnalyzer.SpeculativeSemanticModel.GetTypeInfo(innerSpeculatedExpression, cancellationToken)
            Dim conv = speculationAnalyzer.SpeculativeSemanticModel.GetConversion(innerSpeculatedExpression, cancellationToken)
 
            If Not conv.IsIdentity OrElse Not Object.Equals(typeInfo.Type, typeInfo.ConvertedType) Then
                speculatedExpressionOuterType = typeInfo.ConvertedType
                Return conv
            End If
 
            speculatedExpression = speculatedExpression.WalkUpParentheses()
            typeInfo = speculationAnalyzer.SpeculativeSemanticModel.GetTypeInfo(speculatedExpression, cancellationToken)
            speculatedExpressionOuterType = GetOuterCastType(speculatedExpression, typeInfo, speculationAnalyzer.SpeculativeSemanticModel, cancellationToken)
            If speculatedExpressionOuterType Is Nothing OrElse outerSpeculatedExpression.IsParentKind(SyntaxKind.SimpleArgument) Then
                Return Nothing
            End If
 
            Return speculationAnalyzer.SpeculativeSemanticModel.ClassifyConversion(speculatedExpression, speculatedExpressionOuterType)
        End Function
 
        Private Shared Function AsTypeInVariableDeclarator(node As SyntaxNode, semanticModel As SemanticModel) As ITypeSymbol
            If node Is Nothing Then
                Return Nothing
            End If
 
            Dim variableDeclarator = TryCast(node, VariableDeclaratorSyntax)
            If variableDeclarator IsNot Nothing Then
 
                Dim asClause = TryCast(variableDeclarator.AsClause, SimpleAsClauseSyntax)
                If (asClause IsNot Nothing) Then
                    Return semanticModel.GetTypeInfo(asClause.Type).Type
                End If
            End If
 
            Return Nothing
        End Function
 
        Private Function IsStartOfExecutableStatement() As Boolean
            Dim parentExpression = _castNode.WalkUpParentheses()
 
            Dim parentStatement = parentExpression.FirstAncestorOrSelf(Of ExecutableStatementSyntax)()
            If parentStatement Is Nothing Then
                Return False
            End If
 
            ' If we are assuming that a call keyword will be inserted, then
            ' we can assume that we are not at the start of the part executable
            ' statement if it's a Call statement.
            If _assumeCallKeyword AndAlso
               parentStatement.IsKind(SyntaxKind.ExpressionStatement) AndAlso DirectCast(parentStatement, ExpressionStatementSyntax).Expression.IsKind(SyntaxKind.InvocationExpression) Then
                Return False
            End If
 
            Return parentStatement.GetFirstToken() = parentExpression.GetFirstToken()
        End Function
 
        Private Function ExpressionCanStartExecutableStatement() As Boolean
            Dim innerExpression = _castExpressionNode.WalkDownParentheses()
 
            Return TypeOf innerExpression Is CastExpressionSyntax OrElse
                   TypeOf innerExpression Is PredefinedCastExpressionSyntax
        End Function
 
        Private Function IsUnnecessary() As Boolean
            Dim speculationAnalyzer = New SpeculationAnalyzer(_castNode, _castExpressionNode, _semanticModel, _cancellationToken,
                                                              skipVerificationForReplacedNode:=True, failOnOverloadResolutionFailuresInOriginalCode:=True)
 
            ' First, check to see if the node ultimately parenting this cast has any
            ' syntax errors. If so, we bail.
            If speculationAnalyzer.SemanticRootOfOriginalExpression.ContainsDiagnostics() Then
                Return False
            End If
 
            If IsStartOfExecutableStatement() AndAlso Not ExpressionCanStartExecutableStatement() Then
                Return False
            End If
 
            Dim castTypeInfo = _semanticModel.GetTypeInfo(_castNode, _cancellationToken)
            Dim castType = castTypeInfo.Type
 
            If castType Is Nothing OrElse castType.IsErrorType() Then
                Return False
            End If
 
            Dim castExpressionType As ITypeSymbol
 
            If _castExpressionNode.Kind = SyntaxKind.CollectionInitializer Then
                ' Get type of the array literal in context without the target type
                castExpressionType = _semanticModel.GetSpeculativeTypeInfo(_castExpressionNode.SpanStart, _castExpressionNode, SpeculativeBindingOption.BindAsExpression).ConvertedType
            Else
                castExpressionType = _semanticModel.GetTypeInfo(_castExpressionNode, _cancellationToken).Type
            End If
 
            If castExpressionType IsNot Nothing AndAlso castExpressionType.IsErrorType() Then
                Return False
            End If
 
            If CastPassedToParamArrayDefinitelyCantBeRemoved(castType) Then
                Return False
            End If
 
            ' A casts to object can always be removed from an expression inside of an interpolation, since it'll be converted to object
            ' in order to call string.Format(...) anyway.
            If If(castType?.SpecialType = SpecialType.System_Object, False) AndAlso
                _castNode.WalkUpParentheses().IsParentKind(SyntaxKind.Interpolation) Then
                Return True
            End If
 
            ' If removing the cast will result in a change in semantics of any of the parenting nodes, we won't remove it.
            ' We do this even for identity casts in case removing that cast might affect type inference.
            If speculationAnalyzer.ReplacementChangesSemantics() Then
                Return False
            End If
 
            Dim expressionToCastType = _semanticModel.ClassifyConversion(_castNode.SpanStart, _castExpressionNode, castType)
 
            If expressionToCastType.IsIdentity Then
                ' Simple case: If the conversion from the inner expression to the cast type is identity,
                ' the cast can be removed.
                Return True
            ElseIf expressionToCastType.IsNarrowing AndAlso expressionToCastType.IsReference Then
                ' If the conversion from the inner expression to the cast type is narrowing reference conversion,
                ' the cast cannot be removed.
                Return False
            End If
 
            Dim outerType = GetOuterCastType(_castNode, castTypeInfo, _semanticModel, _cancellationToken)
 
            If outerType IsNot Nothing Then
                Dim castToOuterType As Conversion = _semanticModel.ClassifyConversion(_castNode.SpanStart, _castNode, outerType)
                Dim expressionToOuterType As Conversion
                Dim speculatedExpressionOuterType As ITypeSymbol = Nothing
                Dim outerSpeculatedExpression = _castNode.WalkUpParentheses()
 
                If outerSpeculatedExpression.IsParentKind(SyntaxKind.DirectCastExpression) OrElse
                    outerSpeculatedExpression.IsParentKind(SyntaxKind.TryCastExpression) OrElse
                    outerSpeculatedExpression.IsParentKind(SyntaxKind.CTypeExpression) Then
                    speculatedExpressionOuterType = outerType
                    expressionToOuterType = _semanticModel.ClassifyConversion(_castExpressionNode.WalkDownParentheses(), speculatedExpressionOuterType)
                Else
                    expressionToOuterType = GetSpeculatedExpressionToOuterTypeConversion(speculationAnalyzer, speculationAnalyzer.ReplacedExpression, outerSpeculatedExpression, _cancellationToken, speculatedExpressionOuterType)
                    If expressionToOuterType = Nothing AndAlso outerSpeculatedExpression.IsParentKind(SyntaxKind.SimpleArgument) Then
                        ' If we are here we might be inside a SimpleArgument but it is
                        ' not part of a ParamArray which is handled above.
                        speculatedExpressionOuterType = outerType
                        expressionToOuterType = _semanticModel.ClassifyConversion(_castExpressionNode.WalkDownParentheses(), speculatedExpressionOuterType)
                    End If
                End If
 
                ' CONSIDER: Anonymous function conversions cannot be compared from different semantic models as lambda symbol comparison requires syntax tree equality. Should this be a compiler bug?
                ' For now, just revert back to computing expressionToOuterType using the original semantic model.
                If expressionToOuterType.IsLambda Then
                    expressionToOuterType = _semanticModel.ClassifyConversion(_castNode.SpanStart, _castExpressionNode, outerType)
                End If
 
                ' If there is an user-defined conversion from the expression to the cast type or the cast
                ' to the outer type, we need to make sure that the same user-defined conversion will be
                ' called if the cast is removed.
                If castToOuterType.IsUserDefined OrElse expressionToCastType.IsUserDefined Then
                    Return (HaveSameUserDefinedConversion(expressionToCastType, expressionToOuterType) OrElse
                            HaveSameUserDefinedConversion(castToOuterType, expressionToOuterType)) AndAlso
                           (UserDefinedConversionIsAllowed(_castNode) AndAlso
                            Not expressionToCastType.IsNarrowing)
                ElseIf expressionToOuterType.IsUserDefined Then
                    Return False
                End If
 
                If (expressionToOuterType.IsIdentity OrElse
                      (_castExpressionNode.Kind = SyntaxKind.CollectionInitializer AndAlso expressionToOuterType.IsWidening AndAlso speculatedExpressionOuterType.IsArrayType())) AndAlso
                   expressionToCastType.IsWidening Then
                    Return True
                End If
 
                If Not (castToOuterType.IsNullableValueType AndAlso castToOuterType.IsWidening) Then
                    Dim expressionToCastTypeIsWideningRefOrDefault As Boolean = expressionToCastType.IsWidening AndAlso (expressionToCastType.IsReference OrElse expressionToCastType.IsDefault)
                    Dim expressionToOuterTypeIsWideningRefOrDefault As Boolean = expressionToOuterType.IsWidening AndAlso (expressionToOuterType.IsReference OrElse expressionToOuterType.IsDefault)
 
                    If expressionToCastTypeIsWideningRefOrDefault AndAlso expressionToOuterTypeIsWideningRefOrDefault Then
                        If expressionToCastType.IsDefault Then
                            Return Not CastRemovalChangesDefaultValue(castType, outerType)
                        End If
 
                        Return True
                    ElseIf expressionToCastTypeIsWideningRefOrDefault OrElse expressionToOuterTypeIsWideningRefOrDefault Then
                        Return Equals(castType, speculatedExpressionOuterType)
                    End If
 
                    If expressionToCastType.IsWidening AndAlso expressionToCastType.IsLambda AndAlso
                        expressionToOuterType.IsWidening AndAlso expressionToOuterType.IsLambda Then
 
                        Return Not speculationAnalyzer.ReplacementChangesSemanticsOfUnchangedLambda(_castExpressionNode, speculationAnalyzer.ReplacedExpression)
                    End If
                End If
 
                If Not castToOuterType.IsValueType AndAlso castToOuterType = expressionToOuterType Then
                    If castToOuterType.IsNullableValueType Then
                        Return expressionToOuterType.IsWidening AndAlso
                            DirectCast(castExpressionType.OriginalDefinition, ITypeSymbol).SpecialType = SpecialType.System_Nullable_T
                    ElseIf expressionToCastType.IsWidening AndAlso expressionToCastType.IsNumeric AndAlso Not castToOuterType.IsIdentity Then
                        ' Some widening numeric conversions can cause loss of precision and must not be removed.
                        Return Not IsRequiredWideningNumericConversion(castExpressionType, castType)
                    End If
 
                    Return True
                End If
 
                If castToOuterType.IsIdentity AndAlso
                   expressionToCastType = expressionToOuterType Then
                    If expressionToCastType.Exists Then
                        If expressionToCastType.IsWidening Then
                            Return True
                        End If
 
                        If expressionToCastType.IsNarrowing AndAlso
                            Not _semanticModel.OptionStrict = OptionStrict.On Then
                            Return True
                        End If
                    Else
                        Return True
                    End If
                End If
 
                If expressionToOuterType.IsIdentity AndAlso
                        castToOuterType.IsWidening AndAlso
                        castToOuterType.IsReference Then
                    Debug.Assert(Not (expressionToCastType.IsNarrowing AndAlso expressionToCastType.IsReference))
                    Return True
                End If
            End If
 
            Return False
        End Function
 
        Private Shared Function HaveSameUserDefinedConversion(conversion1 As Conversion, conversion2 As Conversion) As Boolean
            Return conversion1.IsUserDefined AndAlso conversion2.IsUserDefined AndAlso conversion1.MethodSymbol.Equals(conversion2.MethodSymbol)
        End Function
 
        Private Shared Function UserDefinedConversionIsAllowed(expression As ExpressionSyntax) As Boolean
            expression = expression.WalkUpParentheses()
 
            Dim parentNode = expression.Parent
            If parentNode Is Nothing Then
                Return False
            End If
 
            If parentNode.IsKind(SyntaxKind.ThrowStatement) Then
                Return False
            End If
 
            Return True
        End Function
 
        Private Shared Function IsRequiredWideningNumericConversion(sourceType As ITypeSymbol, destinationType As ITypeSymbol) As Boolean
            ' VB Language Specification: Section 8.3 Numeric Conversions
 
            ' Conversions from UInteger, Integer, ULong, Long, or Decimal to Single or Double are rounded to the nearest Single or Double value.
            ' While this conversion may cause a loss of precision, it will never cause a loss of magnitude
 
            Select Case destinationType.SpecialType
                Case SpecialType.System_Single, SpecialType.System_Double
                    Select Case sourceType.SpecialType
                        Case SpecialType.System_UInt32, SpecialType.System_Int32,
                            SpecialType.System_UInt64, SpecialType.System_Int64,
                            SpecialType.System_Decimal
 
                            Return True
                    End Select
            End Select
 
            Return False
        End Function
 
        Private Shared Function CastRemovalChangesDefaultValue(castType As ITypeSymbol, outerType As ITypeSymbol) As Boolean
            If castType.IsNumericType() Then
                Return Not outerType.IsNumericType()
            ElseIf castType.SpecialType = SpecialType.System_DateTime Then
                Return Not outerType.SpecialType = SpecialType.System_DateTime
            ElseIf castType.SpecialType = SpecialType.System_Boolean Then
                Return Not (outerType.IsNumericType OrElse outerType.SpecialType = SpecialType.System_Boolean)
            End If
 
            If castType.OriginalDefinition?.SpecialType = SpecialType.System_Nullable_T Then
                ' Don't allow casts of Nothing to T? to be removed unless the outer type is T? or Object.
                ' Otherwise, Nothing will lose its "nullness" and get the default value of T.
                '
                ' So, this is OK:
                '
                '   Dim x As Object = DirectCast(Nothing, Integer?)
                '
                ' But this is not:
                '
                '   Dim x As Integer = DirectCast(Nothing, Integer?)
 
                If castType.Equals(outerType) OrElse outerType.SpecialType = SpecialType.System_Object Then
                    Return False
                End If
 
                Return True
            End If
 
            Return False
        End Function
 
        Public Shared Function IsUnnecessary(
            castNode As ExpressionSyntax,
            castExpressionNode As ExpressionSyntax,
            semanticModel As SemanticModel,
            assumeCallKeyword As Boolean,
            cancellationToken As CancellationToken
        ) As Boolean
 
            Return New CastAnalyzer(castNode, castExpressionNode, semanticModel, assumeCallKeyword, cancellationToken).IsUnnecessary()
        End Function
 
    End Class
End Namespace