diff --git a/CodeConverter/CSharp/ExpressionNodeVisitor.cs b/CodeConverter/CSharp/ExpressionNodeVisitor.cs index a05a9739d..0963a9f8c 100644 --- a/CodeConverter/CSharp/ExpressionNodeVisitor.cs +++ b/CodeConverter/CSharp/ExpressionNodeVisitor.cs @@ -412,6 +412,9 @@ public override async Task VisitUnaryExpression(VBasic.Syntax. private async Task NegateAndSimplifyOrNullAsync(VBSyntax.UnaryExpressionSyntax node, ExpressionSyntax expr, ITypeSymbol operandConvertedType) { + if (expr.SkipIntoParens().HasAnnotation(VisualBasicNullableExpressionsConverter.NegatedForParentNotAnnotation)) { + return expr; + } if (await _operatorConverter.ConvertReferenceOrNothingComparisonOrNullAsync(node.Operand.SkipIntoParens(), TriviaConvertingExpressionVisitor.IsWithinExpressionTree, true) is { } nothingComparison) { return nothingComparison; } diff --git a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs index 6e9720622..34a0c64e8 100644 --- a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs @@ -571,8 +571,7 @@ await node.Name.AcceptAsync(_expressionVisitor), public override async Task> VisitSingleLineIfStatement(VBSyntax.SingleLineIfStatementSyntax node) { - var condition = await node.Condition.AcceptAsync(_expressionVisitor); - condition = CommonConversions.TypeConversionAnalyzer.AddExplicitConversion(node.Condition, condition, forceTargetType: CommonConversions.KnownTypes.Boolean); + var condition = await ConvertConditionAsync(node.Condition); var block = SyntaxFactory.Block(await ConvertStatementsAsync(node.Statements)); ElseClauseSyntax elseClause = null; @@ -585,8 +584,7 @@ public override async Task> VisitSingleLineIfStateme public override async Task> VisitMultiLineIfBlock(VBSyntax.MultiLineIfBlockSyntax node) { - var condition = await node.IfStatement.Condition.AcceptAsync(_expressionVisitor); - condition = CommonConversions.TypeConversionAnalyzer.AddExplicitConversion(node.IfStatement.Condition, condition, forceTargetType: CommonConversions.KnownTypes.Boolean); + var condition = await ConvertConditionAsync(node.IfStatement.Condition); var block = SyntaxFactory.Block(await ConvertStatementsAsync(node.Statements)); var elseClause = await ConvertElseClauseAsync(node.ElseBlock); @@ -604,11 +602,19 @@ public override async Task> VisitMultiLineIfBlock(VB private async Task<(ExpressionSyntax ElseIfCondition, BlockSyntax ElseBlock)> ConvertElseIfAsync(VBSyntax.ElseIfBlockSyntax elseIf) { var elseBlock = SyntaxFactory.Block(await ConvertStatementsAsync(elseIf.Statements)); - var elseIfCondition = await elseIf.ElseIfStatement.Condition.AcceptAsync(_expressionVisitor); - elseIfCondition = CommonConversions.TypeConversionAnalyzer.AddExplicitConversion(elseIf.ElseIfStatement.Condition, elseIfCondition, forceTargetType: CommonConversions.KnownTypes.Boolean); + var elseIfCondition = await ConvertConditionAsync(elseIf.ElseIfStatement.Condition); return (elseIfCondition, elseBlock); } + /// + /// VB converts a condition to Boolean (e.g. Nothing to False), whereas C# requires it to already be bool + /// + private async Task ConvertConditionAsync(VBSyntax.ExpressionSyntax vbCondition) + { + var condition = await vbCondition.AcceptAsync(_expressionVisitor); + return CommonConversions.TypeConversionAnalyzer.AddExplicitConversion(vbCondition, condition, forceTargetType: CommonConversions.KnownTypes.Boolean); + } + private async Task ConvertElseClauseAsync(VBSyntax.ElseBlockSyntax elseBlock) { if (elseBlock == null) return null; @@ -1137,7 +1143,7 @@ async Task ConvertCatchBlockAsync(VBasic.Syntax.CatchBlockSyn async Task ConvertCatchFilterClauseAsync(VBasic.Syntax.CatchFilterClauseSyntax node) { if (node == null) return null; - return SyntaxFactory.CatchFilterClause(await node.Filter.AcceptAsync(_expressionVisitor)); + return SyntaxFactory.CatchFilterClause(await ConvertConditionAsync(node.Filter)); } async Task ConvertFinallyBlockAsync(VBasic.Syntax.FinallyBlockSyntax node) @@ -1182,7 +1188,7 @@ public override async Task> VisitUsingBlock(VBSyntax public override async Task> VisitWhileBlock(VBSyntax.WhileBlockSyntax node) { return SingleStatement(SyntaxFactory.WhileStatement( - await node.WhileStatement.Condition.AcceptAsync(_expressionVisitor), + await ConvertConditionAsync(node.WhileStatement.Condition), SyntaxFactory.Block(await ConvertStatementsAsync(node.Statements)).UnpackNonNestedBlock() )); } @@ -1191,35 +1197,24 @@ public override async Task> VisitDoLoopBlock(VBSynta { var statements = SyntaxFactory.Block(await ConvertStatementsAsync(node.Statements)).UnpackNonNestedBlock(); - if (node.DoStatement.WhileOrUntilClause != null) { - var stmt = node.DoStatement.WhileOrUntilClause; - if (stmt.WhileOrUntilKeyword.IsKind(VBasic.SyntaxKind.WhileKeyword)) - return SingleStatement(SyntaxFactory.WhileStatement( - await stmt.Condition.AcceptAsync(_expressionVisitor), - statements - )); - return SingleStatement(SyntaxFactory.WhileStatement( - (await stmt.Condition.AcceptAsync(_expressionVisitor)).InvertCondition(), - statements - )); + if (node.DoStatement.WhileOrUntilClause is { } topClause) { + return SingleStatement(SyntaxFactory.WhileStatement(await ConvertLoopContinueConditionAsync(topClause), statements)); } - var whileOrUntilStmt = node.LoopStatement.WhileOrUntilClause; - ExpressionSyntax conditionExpression; - bool isUntilStmt; - if (whileOrUntilStmt != null) { - conditionExpression = await whileOrUntilStmt.Condition.AcceptAsync(_expressionVisitor); - isUntilStmt = whileOrUntilStmt.WhileOrUntilKeyword.IsKind(VBasic.SyntaxKind.UntilKeyword); - } else { - conditionExpression = SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression); - isUntilStmt = false; - } - - if (isUntilStmt) { - conditionExpression = conditionExpression.InvertCondition(); - } + var bottomCondition = node.LoopStatement.WhileOrUntilClause is { } bottomClause + ? await ConvertLoopContinueConditionAsync(bottomClause) + : SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression); + return SingleStatement(SyntaxFactory.DoStatement(statements, bottomCondition)); + } - return SingleStatement(SyntaxFactory.DoStatement(statements, conditionExpression)); + /// + /// C# only has While loops, so an Until loop continues while its condition is false. + /// VB converts the Until condition to Boolean (Nothing to False) before negating it, so an Until loop continues while it's Nothing. + /// + private async Task ConvertLoopContinueConditionAsync(VBSyntax.WhileOrUntilClauseSyntax clause) + { + var condition = await ConvertConditionAsync(clause.Condition); + return clause.WhileOrUntilKeyword.IsKind(VBasic.SyntaxKind.UntilKeyword) ? condition.InvertCondition() : condition; } public override async Task> VisitCallStatement(VBSyntax.CallStatementSyntax node) diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index d1c3d3d24..b2a2362c0 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -1,4 +1,4 @@ -using ICSharpCode.CodeConverter.Util.FromRoslyn; +using ICSharpCode.CodeConverter.Util.FromRoslyn; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.FindSymbols; @@ -108,12 +108,11 @@ private ExpressionSyntax AddTypeConversion(VBSyntax.ExpressionSyntax vbNode, Exp return addParenthesisIfNeeded ? vbNode.ParenthesizeIfPrecedenceCouldChange(csNode) : csNode; case TypeConversionKind.DestructiveCast: case TypeConversionKind.NonDestructiveCast: - return CreateCast(csNode, vbConvertedType); + return CreateCastOrConditionCast(vbNode, csNode, vbType, vbConvertedType); case TypeConversionKind.Conversion: return AddExplicitConvertTo(vbNode, csNode, vbType, vbConvertedType); case TypeConversionKind.NullableBool: - return SyntaxFactory.BinaryExpression(SyntaxKind.EqualsExpression, csNode, - LiteralConversions.GetLiteralExpression(true)); + return csNode.EqualsTrue(); case TypeConversionKind.StringToCharArray: var memberAccessExpressionSyntax = SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, csNode, ValidSyntaxFactory.IdentifierName(nameof(string.ToCharArray))); return SyntaxFactory.InvocationExpression(memberAccessExpressionSyntax, @@ -340,9 +339,8 @@ private bool TryAnalyzeCsConversion(VBasic.VisualBasicCompilation vbCompilation, typeConversionKind = TypeConversionKind.Conversion; return true; } - else if (csConversion.IsNullable && csConvertedType.SpecialType == SpecialType.System_Boolean && vbNode.AlwaysHasBooleanTypeInCSharp() && - (vbNode is not VBSyntax.BinaryExpressionSyntax and not VBSyntax.UnaryExpressionSyntax || vbNode.IsKind(VBasic.SyntaxKind.AndExpression, VBasic.SyntaxKind.OrExpression, VBasic.SyntaxKind.ExclusiveOrExpression))) { - typeConversionKind = TypeConversionKind.NullableBool; + else if (csConversion.IsNullable && csConvertedType.SpecialType == SpecialType.System_Boolean && vbNode.AlwaysHasBooleanTypeInCSharp()) { + typeConversionKind = IsMadeBoolByNullableOperatorConversion(vbNode) ? TypeConversionKind.Identity : TypeConversionKind.NullableBool; return true; } else if (csConversion.IsExplicit) { @@ -354,6 +352,16 @@ private bool TryAnalyzeCsConversion(VBasic.VisualBasicCompilation vbCompilation, return false; } + /// + /// In a condition, the nullable operator conversions already give bool (so no conversion is needed) for operators such as comparisons, Not, AndAlso and OrElse, but not for And, Or and Xor + /// + private static bool IsMadeBoolByNullableOperatorConversion(VBSyntax.ExpressionSyntax vbNode) + { + var unparenthesized = vbNode.SkipIntoParens(); + return unparenthesized is VBSyntax.BinaryExpressionSyntax or VBSyntax.UnaryExpressionSyntax && + !unparenthesized.IsKind(VBasic.SyntaxKind.AndExpression, VBasic.SyntaxKind.OrExpression, VBasic.SyntaxKind.ExclusiveOrExpression); + } + private static bool IsExactTypeNumericLiteral(VBSyntax.ExpressionSyntax vbNode, ITypeSymbol vbConvertedType) => vbNode is VBSyntax.LiteralExpressionSyntax literal && LiteralConversions.ConvertLiteralNumericValueOrNull(literal.Token.Value, vbConvertedType) is {}; @@ -426,6 +434,39 @@ private ExpressionSyntax AddRoundInvocation(ExpressionSyntax csNode) return SyntaxFactory.InvocationExpression(memberAccess, arguments); } + /// + /// Converting Nothing to a non-nullable type throws in VB, except in a condition: there VB lifts the conversion to Boolean? and treats Nothing as False. + /// The default (zero) of every type convertible to Boolean also converts to False, so GetValueOrDefault matches that with a single evaluation. + /// + private static ExpressionSyntax GetUnderlyingValueToConvert(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNullableNode) => + vbNode.AlwaysHasBooleanTypeInCSharp() ? csNullableNode.GetValueOrDefault() : csNullableNode.NullableGetValueExpression(); + + private ExpressionSyntax CreateCastOrConditionCast(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, ITypeSymbol vbType, ITypeSymbol vbConvertedType) + { + if (!vbNode.AlwaysHasBooleanTypeInCSharp()) return CreateCast(csNode, vbConvertedType); + if (GetNullableReturnTypeOfUserDefinedConversion(vbNode) is { } nullableBoolean) { + var operand = vbType.IsNullable() ? csNode.NullableGetValueExpression() : csNode; + return CreateCast(operand, nullableBoolean).EqualsTrue(); + } + return IsNonBooleanNullable(vbType, vbConvertedType) + ? _vbNullableExpressionsConverter.ConvertWhenNotNullElseFalse(vbNode, csNode, value => CreateCast(value, vbConvertedType)) + : CreateCast(csNode, vbConvertedType); + } + + /// + /// In a condition, VB doesn't lift a user-defined CType returning Boolean? (a Nothing operand throws, whereas C# would lift it to give null), + /// but treats a Nothing result as False, like any other Boolean? condition + /// + private ITypeSymbol GetNullableReturnTypeOfUserDefinedConversion(VBSyntax.ExpressionSyntax vbNode) => + VBasic.VisualBasicExtensions.GetConversion(_semanticModel, vbNode).MethodSymbol?.ReturnType is { } returnType && returnType.IsNullable() ? returnType : null; + + /// + /// In a condition, VB lifts any other conversion to Boolean (e.g. a user-defined CType returning Boolean), so Nothing is False rather than throwing. + /// A Boolean? condition is excluded: the nullable Boolean logic already makes it bool. + /// + private static bool IsNonBooleanNullable(ITypeSymbol vbType, ITypeSymbol vbConvertedType) => + vbType.IsNullable(out var underlyingType) && underlyingType.SpecialType != SpecialType.System_Boolean && !vbConvertedType.IsNullable(); + private ExpressionSyntax AddExplicitConvertTo(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, ITypeSymbol currentType, ITypeSymbol targetType) { var displayType = targetType.ToMinimalDisplayString(_semanticModel, vbNode.SpanStart); @@ -443,7 +484,7 @@ invoke.Expression is MemberAccessExpressionSyntax expr && currentType.IsNullable(out var nullableCurrentType); targetType.IsNullable(out var nullableTargetType); if (nullableCurrentType != null && nullableTargetType == null) { - csNode = csNode.NullableGetValueExpression(); + csNode = GetUnderlyingValueToConvert(vbNode, csNode); } var typeNameForConversionMethod = nullableTargetType ?? targetType; diff --git a/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs b/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs index 7fce4ae6a..8f67fbe8d 100644 --- a/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs +++ b/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs @@ -20,15 +20,34 @@ public static bool PrecedenceCouldChange(this VBasic.VisualBasicSyntaxNode node) return parentIsMemberAccessExpression || parentIsNonArgumentExpression && !parentIsBinaryExpression && !parentIsLambda && !parentIsParenthesis; } + public static bool IsOrderingComparison(this VBSyntax.ExpressionSyntax node) => + node.IsKind(VBasic.SyntaxKind.LessThanExpression, VBasic.SyntaxKind.LessThanOrEqualExpression, + VBasic.SyntaxKind.GreaterThanExpression, VBasic.SyntaxKind.GreaterThanOrEqualExpression); + + /// + /// Where VB binds a condition as Boolean (e.g. Nothing becomes False), the C# condition must be bool. + /// VB's binder passes the condition context through parentheses (isOperandOfConditionalBranch in Binder_Expressions.vb), so this does too. + /// public static bool AlwaysHasBooleanTypeInCSharp(this VBSyntax.ExpressionSyntax vbNode) { - var parent = vbNode.SkipOutOfParens()?.Parent; + var condition = vbNode.OutermostParenthesizedOrSelf(); - return parent is VBSyntax.SingleLineIfStatementSyntax singleLine && singleLine.Condition == vbNode || - parent is VBSyntax.IfStatementSyntax ifStatement && ifStatement.Condition == vbNode || - parent is VBSyntax.ElseIfStatementSyntax elseIfStatement && elseIfStatement.Condition == vbNode || - parent is VBSyntax.TernaryConditionalExpressionSyntax ternary && ternary.Condition == vbNode || - parent is VBSyntax.WhereClauseSyntax or VBSyntax.PartitionWhileClauseSyntax; + return condition.Parent switch { + VBSyntax.SingleLineIfStatementSyntax singleLine => singleLine.Condition == condition, + VBSyntax.IfStatementSyntax ifStatement => ifStatement.Condition == condition, + VBSyntax.ElseIfStatementSyntax elseIfStatement => elseIfStatement.Condition == condition, + VBSyntax.TernaryConditionalExpressionSyntax ternary => ternary.Condition == condition, + VBSyntax.WhileStatementSyntax whileStatement => whileStatement.Condition == condition, + VBSyntax.WhileOrUntilClauseSyntax or VBSyntax.CatchFilterClauseSyntax => true, + VBSyntax.WhereClauseSyntax or VBSyntax.PartitionWhileClauseSyntax => true, + _ => false + }; + } + + public static VBSyntax.ExpressionSyntax OutermostParenthesizedOrSelf(this VBSyntax.ExpressionSyntax vbNode) + { + while (vbNode.Parent is VBSyntax.ParenthesizedExpressionSyntax parenthesized) vbNode = parenthesized; + return vbNode; } /// diff --git a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs index 14aba516a..aa7feba4a 100644 --- a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs +++ b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs @@ -33,6 +33,12 @@ internal class VisualBasicNullableExpressionsConverter /// private static readonly SyntaxAnnotation IsNotNullableAnnotation = new("CodeConverter.Nullable", false.ToString()); + /// + /// Marks a comparison already negated for its parent Not. By the time Not is visited, its operand has already been expanded to the HasValue form, + /// so the comparison negates itself while it can still use the lifted operator, and Not skips its own negation. + /// + public static readonly SyntaxAnnotation NegatedForParentNotAnnotation = new("CodeConverter.NegatedForParentNot"); + public VisualBasicNullableExpressionsConverter(SemanticModel semanticModel) => _semanticModel = semanticModel; public ExpressionSyntax InvokeConversionWhenNotNull(VBSyntax.ExpressionSyntax vbExpr, ExpressionSyntax csExpr, MemberAccessExpressionSyntax conversionMethod, TypeSyntax castType) @@ -66,7 +72,7 @@ public ExpressionSyntax WithBinaryExpressionLogicForNullableTypes(VBSyntax.Binar private ExpressionSyntax ForExpressionTree(VBSyntax.BinaryExpressionSyntax vbNode, TypeInfo lhsTypeInfo, TypeInfo rhsTypeInfo, BinaryExpressionSyntax csBinExp, ExpressionSyntax lhs, ExpressionSyntax rhs) { if (IsSupported(vbNode.Kind()) && !vbNode.IsKind(VBasic.SyntaxKind.AndAlsoExpression, VBasic.SyntaxKind.OrElseExpression)) { - return csBinExp.WithAdditionalAnnotations(IsNotNullableAnnotation); // C#'s lifted comparisons are bool + return csBinExp.WithAdditionalAnnotations(IsNotNullableAnnotation, CSharpUtil.MayCompareNullAnnotation); // C#'s lifted comparisons are bool } if (!vbNode.IsKind(VBasic.SyntaxKind.AndAlsoExpression, VBasic.SyntaxKind.OrElseExpression) || !vbNode.IsOnlyTestedForTrue(_semanticModel)) return csBinExp; var isLhsNullable = IsNullable(vbNode.Left, lhs, lhsTypeInfo); @@ -100,6 +106,16 @@ private bool IsNullable(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNod return typeInfo.Type.IsNullable() && !csNode.AnyInParens(x => x.HasAnnotation(IsNotNullableAnnotation)) && GetNullabilityWithinBooleanExpression(vbNode) != KnownNullability.NotNull; } + /// + /// In a condition, VB lifts a conversion to Boolean and treats Nothing as False. + /// The converted default value isn't necessarily False (e.g. a user-defined operator), so check for a value rather than using GetValueOrDefault. + /// + public ExpressionSyntax ConvertWhenNotNullElseFalse(VBSyntax.ExpressionSyntax vbExpr, ExpressionSyntax csExpr, Func convertValue) + { + var hasValueCheck = HasValue(vbExpr, ref csExpr); + return hasValueCheck.And(convertValue(csExpr)); + } + private string GetArgName() => $"arg{Interlocked.Increment(ref _argCounter)}"; private ExpressionSyntax? PatternVar(VBSyntax.ExpressionSyntax vbLeft, ExpressionSyntax csLeft, out ExpressionSyntax lhsName) @@ -118,7 +134,7 @@ private ExpressionSyntax PatternVar(ExpressionSyntax expr, out ExpressionSyntax var identifier = SyntaxFactory.Identifier(arg); name = ValidSyntaxFactory.IdentifierName(identifier); - return SyntaxFactory.IsPatternExpression(expr, SyntaxFactory.VarPattern(SyntaxFactory.SingleVariableDesignation(identifier))); + return SyntaxFactory.IsPatternExpression(expr.AddParensUnlessPrimary(), SyntaxFactory.VarPattern(SyntaxFactory.SingleVariableDesignation(identifier))); } private ExpressionSyntax PatternObject(ExpressionSyntax expr, out ExpressionSyntax name) @@ -128,7 +144,7 @@ private ExpressionSyntax PatternObject(ExpressionSyntax expr, out ExpressionSynt var variable = SyntaxFactory.SingleVariableDesignation(identifier); var recursivePattern = (RecursivePatternSyntax)NotFormattedIsPattern.Pattern; - return NotFormattedIsPattern.WithExpression(expr).WithPattern(recursivePattern.WithDesignation(variable)); + return NotFormattedIsPattern.WithExpression(expr.AddParensUnlessPrimary()).WithPattern(recursivePattern.WithDesignation(variable)); } private ExpressionSyntax NegatedPatternObject(ExpressionSyntax expr, out ExpressionSyntax name) @@ -140,7 +156,7 @@ private ExpressionSyntax NegatedPatternObject(ExpressionSyntax expr, out Express var recursivePattern = (RecursivePatternSyntax)unaryPattern.Pattern; unaryPattern = unaryPattern.WithPattern(recursivePattern.WithDesignation(variable)); - return NotFormattedNegatedIsPattern.WithExpression(expr).WithPattern(unaryPattern); + return NotFormattedNegatedIsPattern.WithExpression(expr.AddParensUnlessPrimary()).WithPattern(unaryPattern); } private static bool IsSupported(VBasic.SyntaxKind op) @@ -222,7 +238,11 @@ private ExpressionSyntax ForRelationalOperators(VBSyntax.BinaryExpressionSyntax bool isLhsNullable, bool isRhsNullable) { if (vbNode.IsOnlyTestedForTrue(_semanticModel) && IsLiftedOperatorFalseWhenVbIsNothing(vbNode, isLhsNullable, isRhsNullable)) { - return csNode.WithAdditionalAnnotations(IsNotNullableAnnotation); + return csNode.WithAdditionalAnnotations(IsNotNullableAnnotation, CSharpUtil.MayCompareNullAnnotation); + } + if (IsNegationOnlyTestedForTrue(vbNode) && IsLiftedOrderingWithTotalOrder(vbNode)) { + var negated = SyntaxFactory.BinaryExpression(CSharpUtil.NegateRelationalOperator(csNode.Kind()), csNode.Left, csNode.Right); + return negated.WithAdditionalAnnotations(IsNotNullableAnnotation, CSharpUtil.MayCompareNullAnnotation, NegatedForParentNotAnnotation); } ExpressionSyntax? lhsName = lhs, rhsName = rhs; @@ -262,16 +282,27 @@ private ExpressionSyntax ForRelationalOperators(VBSyntax.BinaryExpressionSyntax return notNullCondition is null ? bin : notNullCondition.Conditional(bin, Null); } + private bool IsNegationOnlyTestedForTrue(VBSyntax.BinaryExpressionSyntax vbNode) => + vbNode.OutermostParenthesizedOrSelf().Parent is VBSyntax.UnaryExpressionSyntax not && not.IsKind(VBasic.SyntaxKind.NotExpression) && + not.IsOnlyTestedForTrue(_semanticModel); + + /// + /// Where VB's Not of a comparison is only tested for True, the lifted opposite comparison is equivalent: both are false when an operand is Nothing. + /// That needs every pair of values to be ordered, which isn't the case for floating point NaN. + /// C# has no ordering operators for bool, and a structure's user-defined operators needn't be a total order. + /// + private bool IsLiftedOrderingWithTotalOrder(VBSyntax.BinaryExpressionSyntax vbNode) => + vbNode.IsOrderingComparison() && HasTotalOrder(_semanticModel.GetTypeInfo(vbNode.Left).ConvertedType) && HasTotalOrder(_semanticModel.GetTypeInfo(vbNode.Right).ConvertedType); + + private static bool HasTotalOrder(ITypeSymbol? type) => + (type?.GetNullableUnderlyingType() ?? type) is { IsValueType: true } valueType && + (valueType.TypeKind == TypeKind.Enum || valueType.SpecialType is not (SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Boolean or SpecialType.None)); + /// /// C#'s lifted relational operators are false when an operand is null, as is its lifted == unless both are null /// private static bool IsLiftedOperatorFalseWhenVbIsNothing(VBSyntax.BinaryExpressionSyntax vbNode, bool isLhsNullable, bool isRhsNullable) => - vbNode.Kind() switch { - VBasic.SyntaxKind.LessThanExpression or VBasic.SyntaxKind.LessThanOrEqualExpression or - VBasic.SyntaxKind.GreaterThanExpression or VBasic.SyntaxKind.GreaterThanOrEqualExpression => true, - VBasic.SyntaxKind.EqualsExpression => !(isLhsNullable && isRhsNullable), - _ => false - }; + vbNode.IsOrderingComparison() || vbNode.IsKind(VBasic.SyntaxKind.EqualsExpression) && !(isLhsNullable && isRhsNullable); private ExpressionSyntax HasValue(VBasic.Syntax.ExpressionSyntax vbNode, ref ExpressionSyntax csName) => HasValue(IsSafelyReusable(vbNode), ref csName); private ExpressionSyntax HasValue(bool reusable, ref ExpressionSyntax csName) diff --git a/CodeConverter/Util/CSharpUtil.cs b/CodeConverter/Util/CSharpUtil.cs index 3e9e80157..fc996fe88 100644 --- a/CodeConverter/Util/CSharpUtil.cs +++ b/CodeConverter/Util/CSharpUtil.cs @@ -5,6 +5,12 @@ namespace ICSharpCode.CodeConverter.Util; internal static class CSharpUtil { + /// + /// Marks a lifted comparison whose operands may be null. A lifted <, <=, > or >= is false when an operand is null, + /// so its opposite operator would be false too, rather than true. + /// + public static readonly SyntaxAnnotation MayCompareNullAnnotation = new("CodeConverter.MayCompareNull"); + /// /// Inverts a boolean condition. Note: The condition object can be frozen (from AST) it's cloned internally. /// @@ -36,21 +42,13 @@ private static ExpressionSyntax InvertConditionInternal(ExpressionSyntax conditi if (bOp.IsKind(SyntaxKind.LogicalAndExpression) || bOp.IsKind(SyntaxKind.LogicalOrExpression)) return SyntaxFactory.BinaryExpression(NegateConditionOperator(bOp.Kind()), InvertCondition(bOp.Left), InvertCondition(bOp.Right)); - if (bOp.IsKind(SyntaxKind.EqualsExpression) || - bOp.IsKind(SyntaxKind.NotEqualsExpression) || - bOp.IsKind(SyntaxKind.GreaterThanExpression) || - bOp.IsKind(SyntaxKind.GreaterThanOrEqualExpression) || - bOp.IsKind(SyntaxKind.LessThanExpression) || - bOp.IsKind(SyntaxKind.LessThanOrEqualExpression)) + if (bOp.IsKind(SyntaxKind.EqualsExpression) || bOp.IsKind(SyntaxKind.NotEqualsExpression) || + IsOrderingComparison(bOp) && !bOp.HasAnnotation(MayCompareNullAnnotation)) return SyntaxFactory.BinaryExpression(NegateRelationalOperator(bOp.Kind()), bOp.Left, bOp.Right); return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, SyntaxFactory.ParenthesizedExpression(condition)); } - if (condition is ConditionalExpressionSyntax cEx) { - return cEx.WithCondition(InvertCondition(cEx.Condition)); - } - if (condition is LiteralExpressionSyntax) { if (condition.Kind() == SyntaxKind.TrueLiteralExpression) return SyntaxFactory.LiteralExpression(SyntaxKind.FalseLiteralExpression); @@ -61,6 +59,10 @@ private static ExpressionSyntax InvertConditionInternal(ExpressionSyntax conditi return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, condition.AddParens()); } + private static bool IsOrderingComparison(BinaryExpressionSyntax bOp) => + bOp.IsKind(SyntaxKind.GreaterThanExpression) || bOp.IsKind(SyntaxKind.GreaterThanOrEqualExpression) || + bOp.IsKind(SyntaxKind.LessThanExpression) || bOp.IsKind(SyntaxKind.LessThanOrEqualExpression); + public static SyntaxKind GetExpressionOperatorTokenKind(SyntaxKind op) { switch (op) { diff --git a/Tests/CSharp/ExpressionTests/BinaryExpressionTests.cs b/Tests/CSharp/ExpressionTests/BinaryExpressionTests.cs index 847a81c46..2e28f890a 100644 --- a/Tests/CSharp/ExpressionTests/BinaryExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/BinaryExpressionTests.cs @@ -420,6 +420,38 @@ private void TestMethod() }"); } + [Fact] + public async Task ParenthesizedNullableConditionsTreatNothingAsFalseAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Class TestClass + Private Sub TestMethod(x As Integer?, y As Integer?, b As Boolean?) + If (x < y) Then Return + If ((x = y)) Then Return + If (b) Then Return + If (Not b) Then Return + If (b AndAlso x > y) Then Return + Dim r = If((x > y), 1, 2) + End Sub +End Class", @" +internal partial class TestClass +{ + private void TestMethod(int? x, int? y, bool? b) + { + if (x < y) + return; + if (x.HasValue && y.HasValue && x.Value == y.Value) + return; + if (b == true) + return; + if (b == false) + return; + if ((!b.HasValue || b.Value) && x > y && b.HasValue) + return; + int r = x > y ? 1 : 2; + } +}"); + } + [Fact] public async Task SimplifiesAlreadyCheckedNullableComparison_HasValueAsync() { diff --git a/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index d30e023c5..06841ed62 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -38,6 +38,414 @@ private void TestMethod(Random rand) }"); } + [Fact] + public async Task NullableComparisonInWhileConditionIssue1300Async() + { + await TestConversionVisualBasicToCSharpAsync(@"Imports System + +Class TestClass + Sub TestMethod(startDate As Date?, endDate As Date?) + Dim currentDate = startDate + While currentDate <= endDate + currentDate = currentDate.Value.AddMinutes(1) + End While + While (currentDate <= endDate) + currentDate = currentDate.Value.AddMinutes(1) + End While + End Sub +End Class", @"using System; + +internal partial class TestClass +{ + public void TestMethod(DateTime? startDate, DateTime? endDate) + { + var currentDate = startDate; + while (currentDate <= endDate) + currentDate = currentDate.Value.AddMinutes(1d); + while (currentDate <= endDate) + currentDate = currentDate.Value.AddMinutes(1d); + } +}"); + } + + [Fact] + public async Task NullableLoopConditionsTreatNothingAsFalseAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Class TestClass + Private Sub TestMethod(x As Integer?, y As Integer?, b As Boolean?, i As Integer) + While x = y : End While + While b : End While + While Not b : End While + Do While x <> y : Loop + Do While b AndAlso x > y : Loop + Do Until x < y : Loop + Do Until x = y : Loop + Do Until b : Loop + Do Until b OrElse x > 2 : Loop + Do : Loop While x >= y + Do : Loop While b + Do : Loop Until x > y + Do : Loop Until Not b + Do Until b = True : Loop + Do Until x > y AndAlso i < 3 : Loop + End Sub +End Class", @" +internal partial class TestClass +{ + private void TestMethod(int? x, int? y, bool? b, int i) + { + while (x.HasValue && y.HasValue && x.Value == y.Value) + { + } + while (b == true) + { + } + while (b == false) + { + } + while (x.HasValue && y.HasValue && x.Value != y.Value) + { + } + while ((!b.HasValue || b.Value) && x > y && b.HasValue) + { + } + while (!(x < y)) + { + } + while (!x.HasValue || !y.HasValue || x.Value != y.Value) + { + } + while (b != true) + { + } + while (!b.GetValueOrDefault() && !(x > 2)) + { + } + do + { + } + while (x >= y); + do + { + } + while (b == true); + do + { + } + while (!(x > y)); + do + { + } + while (b != false); + while (b != true) + { + } + while (!(x > y) || i >= 3) + { + } + } +}"); + } + + [Fact] + public async Task OptionStrictOffNonBooleanConditionsTreatNothingAsFalseAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Option Strict Off + +Class TestClass + Private Sub TestMethod(x As Integer?, o As Object) + If x Then Return + While x : End While + Do Until x : Loop + While o : End While + Do Until o : Loop + Do Until o > 1 : Loop + End Sub +End Class", @"using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic + +internal partial class TestClass +{ + private void TestMethod(int? x, object o) + { + if (Conversions.ToBoolean(x.GetValueOrDefault())) + return; + while (Conversions.ToBoolean(x.GetValueOrDefault())) + { + } + while (!Conversions.ToBoolean(x.GetValueOrDefault())) + { + } + while (Conversions.ToBoolean(o)) + { + } + while (!Conversions.ToBoolean(o)) + { + } + while (!Conversions.ToBoolean(Operators.ConditionalCompareObjectGreater(o, 1, false))) + { + } + } +}", incompatibleWithAutomatedCommentTesting: true); // Option statement comments are lost + } + + [Fact] + public async Task NullableStructureWithWideningToBooleanConditionsTreatNothingAsFalseAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Structure S + Public Shared Widening Operator CType(s As S) As Boolean + Return True + End Operator +End Structure + +Class TestClass + Private Function GetS() As S? + Return Nothing + End Function + + Private Sub TestMethod(s As S?) + If s Then Return + While (s) : End While + Do Until s : Loop + Do : Loop While GetS() + If If(s, GetS()) Then Return + Do Until If(s.HasValue, s, GetS()) : Loop + End Sub +End Class", @" +internal partial struct S +{ + public static implicit operator bool(S s) + { + return true; + } +} + +internal partial class TestClass +{ + private S? GetS() + { + return default; + } + + private void TestMethod(S? s) + { + if (s.HasValue && (bool)s.Value) + return; + while (s.HasValue && (bool)s.Value) + { + } + while (!s.HasValue || !(bool)s.Value) + { + } + do + { + } + while (GetS() is { } arg1 && (bool)arg1); + if ((s ?? GetS()) is { } arg2 && (bool)arg2) + return; + while (!((s.HasValue ? s : GetS()) is { } arg3) || !(bool)arg3) + { + } + } +}"); + } + + /// + /// VB doesn't lift a CType returning Boolean? (a Nothing operand throws, so .Value), but treats a Nothing result as False + /// + [Fact] + public async Task StructureWithWideningToNullableBooleanConditionsCompareResultWithTrueAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Structure S + Public Shared Widening Operator CType(s As S) As Boolean? + Return Nothing + End Operator +End Structure + +Class TestClass + Private Sub TestMethod(s As S?, ns As S) + If s Then Return + Do Until s : Loop + While ns : End While + End Sub +End Class", @" +internal partial struct S +{ + public static implicit operator bool?(S s) + { + return default; + } +} + +internal partial class TestClass +{ + private void TestMethod(S? s, S ns) + { + if ((bool?)s.Value == true) + return; + while ((bool?)s.Value != true) + { + } + while ((bool?)ns == true) + { + } + } +}"); + } + + [Fact] + public async Task OptionStrictOffNullableToBooleanOutsideConditionsKeepsValueAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Option Strict Off + +Structure S + Public Shared Widening Operator CType(s As S) As Boolean + Return True + End Operator +End Structure + +Class TestClass + Private Function IsTrue(b As Boolean) As Boolean + Return b + End Function + + Private Sub TestMethod(x As Integer?, s As S?) + Dim b As Boolean = x + If IsTrue(x) Then Return + If CBool(x) Then Return + Do Until s : Loop + End Sub +End Class", @"using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic + +internal partial struct S +{ + public static implicit operator bool(S s) + { + return true; + } +} + +internal partial class TestClass +{ + private bool IsTrue(bool b) + { + return b; + } + + private void TestMethod(int? x, S? s) + { + bool b = Conversions.ToBoolean(x.Value); + if (IsTrue(Conversions.ToBoolean(x.Value))) + return; + if (Conversions.ToBoolean(x.Value)) + return; + while (!s.HasValue || !(bool)s.Value) + { + } + } +}", incompatibleWithAutomatedCommentTesting: true); // Option statement comments are lost + } + + [Fact] + public async Task NotOfNullableOrderingComparisonFlipsLiftedOperatorAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Enum Size + Small +End Enum + +Class TestClass + Private Sub TestMethod(x As Integer?, y As Integer?, d As Decimal?, s As Size?) + While Not (x < y) : End While + Do Until Not (x < y) : Loop + Do While Not x >= y AndAlso Not (d > 1) : Loop + If Not (x <= y) Then Return + If Not (s > Size.Small) Then Return + End Sub +End Class", @" +internal enum Size +{ + Small +} + +internal partial class TestClass +{ + private void TestMethod(int? x, int? y, decimal? d, Size? s) + { + while (x >= y) + { + } + while (!(x >= y)) + { + } + while (x < y && d <= 1) + { + } + if (x > y) + return; + if (s <= Size.Small) + return; + } +}"); + } + + /// + /// Flipping the operator is wrong for NaN, and for = and <> when both operands are Nothing + /// + [Fact] + public async Task NotOfFloatingPointOrEqualityNullableComparisonIsNotFlippedAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Class TestClass + Private Sub TestMethod(x As Integer?, y As Integer?, a As Double?, b As Single?) + While Not (a < 1.0) : End While + If Not (b >= 1) Then Return + If Not (x = y) Then Return + If Not (x <> y) Then Return + End Sub +End Class", @" +internal partial class TestClass +{ + private void TestMethod(int? x, int? y, double? a, float? b) + { + while ((a.HasValue ? a.Value < 1.0d : null) == false) + { + } + if ((b.HasValue ? b.Value >= 1 : null) == false) + return; + if ((x.HasValue && y.HasValue ? x.Value == y.Value : null) == false) + return; + if ((x.HasValue && y.HasValue ? x.Value != y.Value : null) == false) + return; + } +}"); + } + + [Fact] + public async Task NonNullableUntilConditionsAreInvertedAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Class TestClass + Private Sub TestMethod(i As Integer, b As Boolean) + Do Until i < 1 : Loop + Do Until Not b : Loop + Do : Loop Until i = 1 OrElse b + End Sub +End Class", @" +internal partial class TestClass +{ + private void TestMethod(int i, bool b) + { + while (i >= 1) + { + } + while (b) + { + } + do + { + } + while (i != 1 && !b); + } +}"); + } + [Fact] public async Task TwoForEachStatementsWithImplicitVariableCreationAsync() { diff --git a/Tests/CSharp/StatementTests/MethodStatementTests.cs b/Tests/CSharp/StatementTests/MethodStatementTests.cs index 1bcc291c6..9635d2c73 100644 --- a/Tests/CSharp/StatementTests/MethodStatementTests.cs +++ b/Tests/CSharp/StatementTests/MethodStatementTests.cs @@ -1463,6 +1463,41 @@ private void TestMethod(int number) }"); } + [Fact] + public async Task TryCatchWithNullableFilterTreatsNothingAsFalseAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Imports System + +Class TestClass + Private Sub TestMethod(x As Integer?, y As Integer?, b As Boolean?) + Try + Catch e As ArgumentException When x = y + Catch e As InvalidOperationException When (b) + Catch e As Exception When x < y + End Try + End Sub +End Class", @"using System; + +internal partial class TestClass +{ + private void TestMethod(int? x, int? y, bool? b) + { + try + { + } + catch (ArgumentException e) when (x.HasValue && y.HasValue && x.Value == y.Value) + { + } + catch (InvalidOperationException e) when (b == true) + { + } + catch (Exception e) when (x < y) + { + } + } +}"); + } + [Fact] public async Task SwitchIntToEnumAsync() { diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/BooleanTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/BooleanTests.vb index 788755064..21bc33fba 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/BooleanTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/BooleanTests.vb @@ -583,5 +583,29 @@ Public Class BooleanTests Assert.True(a1 = a2, "Identical values stored in objects should be equal") Assert.False(a1 = b, "Different values stored in objects should not be equal") End Sub + + + + + + + + + Public Sub UntilTernaryConditions(f As Boolean, a As Boolean, b As Boolean) + Dim expected = If(f, a, b) + Dim count = 0 + Do Until If(f, a, b) + count += 1 + If count = 3 Then Exit Do + Loop + Assert.Equal(If(expected, 0, 3), count) + + count = 0 + Do Until If(f, True, b) + count += 1 + If count = 3 Then Exit Do + Loop + Assert.Equal(If(f OrElse b, 0, 3), count) + End Sub End Class diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb new file mode 100644 index 000000000..bb8a728be --- /dev/null +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb @@ -0,0 +1,133 @@ +Option Strict Off + +Imports System +Imports Xunit + +''' +''' With Option Strict Off, a condition of any type convertible to Boolean is allowed. +''' VB converts a nullable condition to Boolean? first, so Nothing becomes False rather than throwing. +''' Each loop stops after so that a condition that never ends the loop shows up as that count. +''' +Public Class NullableConditionOptionStrictOffTests + Private Const MaxIterations As Integer = 3 + + + + + + Public Sub NullableIntegerConditions(x As Integer?, expectedIf As Boolean, expectedWhile As Integer, expectedDoUntil As Integer) + Dim result = False + If x Then result = True + Assert.Equal(expectedIf, result) + + Dim count = 0 + While x + count += 1 + If count = MaxIterations Then Exit While + End While + Assert.Equal(expectedWhile, count) + + count = 0 + Do Until x + count += 1 + If count = MaxIterations Then Exit Do + Loop + Assert.Equal(expectedDoUntil, count) + + count = 0 + Do + count += 1 + If count = MaxIterations Then Exit Do + Loop While (x) + Assert.Equal(If(expectedWhile = 0, 1, 3), count) + End Sub + + + + + + Public Sub NullableDoubleIfCondition(x As Double?, expected As Boolean) + Dim result = False + If x Then + result = True + End If + Assert.Equal(expected, result) + End Sub + + + Public Sub NullableToBooleanOutsideConditionsThrowsForNothing() + Dim x As Integer? = Nothing + Assert.Throws(Of InvalidOperationException)(Sub() + Dim b As Boolean = x + End Sub) + Assert.Throws(Of InvalidOperationException)(Sub() + If IsTrue(x) Then Return + End Sub) + Assert.Throws(Of InvalidOperationException)(Sub() + If CBool(x) Then Return + End Sub) + End Sub + + Private Shared Function IsTrue(b As Boolean) As Boolean + Return b + End Function + + + + + + Public Sub NullableStructureWithWideningToBooleanConditions(hasValue As Boolean, value As Integer, expectedIf As Boolean, expectedDoUntil As Integer) + Dim s As ZeroIsTrueStrictOff? = If(hasValue, New ZeroIsTrueStrictOff With {.Value = value}, CType(Nothing, ZeroIsTrueStrictOff?)) + Dim result = False + If s Then result = True + Assert.Equal(expectedIf, result) + + Dim count = 0 + Do Until s + count += 1 + If count = MaxIterations Then Exit Do + Loop + Assert.Equal(expectedDoUntil, count) + End Sub + + + + + + Public Sub ObjectConditions(o As Object, expectedWhile As Integer, expectedDoUntil As Integer) + Dim count = 0 + While o + count += 1 + If count = MaxIterations Then Exit While + End While + Assert.Equal(expectedWhile, count) + + count = 0 + Do Until o + count += 1 + If count = MaxIterations Then Exit Do + Loop + Assert.Equal(expectedDoUntil, count) + End Sub + + + + + + Public Sub ObjectComparisonCondition(o As Object, expected As Integer) + Dim count = 0 + Do Until o > 1 + count += 1 + If count = MaxIterations Then Exit Do + Loop + Assert.Equal(expected, count) + End Sub +End Class + +Public Structure ZeroIsTrueStrictOff + Public Value As Integer + + Public Shared Widening Operator CType(s As ZeroIsTrueStrictOff) As Boolean + Return s.Value = 0 + End Operator +End Structure diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb new file mode 100644 index 000000000..9041961ce --- /dev/null +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb @@ -0,0 +1,262 @@ +Imports System +Imports System.Linq +Imports Xunit + +''' +''' VB converts a Boolean? condition to Boolean with Nothing becoming False, before deciding whether to branch, continue (While) or stop (Until). +''' +Public Class NullableConditionTests + + + + + + + Public Sub ParenthesizedIfConditions(a As Integer?, b As Integer?, expectedLessThan As Boolean, expectedEquals As Boolean) + Dim lessThan = False + If (a < b) Then lessThan = True + Assert.Equal(expectedLessThan, lessThan) + + Dim equals = False + If ((a = b)) Then + equals = True + End If + Assert.Equal(expectedEquals, equals) + + Assert.Equal(expectedLessThan, If((a < b), True, False)) + End Sub + + + + + + Public Sub ParenthesizedBooleanIfConditions(flag As Boolean?, expectedFlag As Boolean, expectedNotFlag As Boolean) + Dim result = False + If (flag) Then result = True + Assert.Equal(expectedFlag, result) + + result = False + If (Not flag) Then + result = True + ElseIf ((flag)) Then + result = False + End If + Assert.Equal(expectedNotFlag, result) + End Sub + + + + + + + Public Sub CatchWhenFilters(a As Integer?, b As Integer?, expected As String) + Dim caught As String + Try + Throw New InvalidOperationException() + Catch ex As Exception When a < b + caught = "LessThan" + Catch ex As Exception When (a = b) + caught = "Equal" + Catch ex As Exception + caught = "Fallback" + End Try + Assert.Equal(expected, caught) + End Sub + + + + + + Public Sub CatchWhenBooleanFilters(flag As Boolean?, expectedFlag As String, expectedNotFlag As String, expectedAndAlso As String) + Assert.Equal(expectedFlag, CaughtWhen(Function() flag)) + Assert.Equal(expectedNotFlag, CaughtWhen(Function() Not flag)) + Assert.Equal(expectedAndAlso, CaughtWhen(Function() flag AndAlso True)) + End Sub + + Private Shared Function CaughtWhen(getFilter As Func(Of Boolean?)) As String + Try + Throw New InvalidOperationException() + Catch ex As Exception When getFilter() + Return "1" + Catch ex As Exception + Return "" + End Try + End Function + + + Public Sub QueryClausesTreatNothingAsFalse() + Dim values As Integer?() = {1, Nothing, 3, 2} + Dim limit As Integer? = 2 + Assert.Equal({1, 2}, (From v In values Where v <= limit Select v.Value).ToArray()) + Assert.Equal({1}, (From v In values Take While v <= limit Select v.Value).ToArray()) + Assert.Equal(New Integer?() {Nothing, 3, 2}, (From v In values Skip While v <= limit).ToArray()) + Assert.Equal(New Integer?() {1}, (From v In values Take While Not v > limit).ToArray()) + End Sub + + + + + + Public Sub NullableStructureWithWideningToBooleanConditions(hasValue As Boolean, value As Integer, expectedIf As Boolean, expectedWhile As Integer, expectedDoUntil As Integer) + Dim s As ZeroIsTrue? = If(hasValue, New ZeroIsTrue With {.Value = value}, CType(Nothing, ZeroIsTrue?)) + Dim result = False + If s Then result = True + Assert.Equal(expectedIf, result) + + Dim count = 0 + While (s) + count += 1 + If count = 3 Then Exit While + End While + Assert.Equal(expectedWhile, count) + + count = 0 + Do Until s + count += 1 + If count = 3 Then Exit Do + Loop + Assert.Equal(expectedDoUntil, count) + + Dim calls = 0 + Dim getS = Function() + calls += 1 + Return s + End Function + result = False + If getS() Then result = True + Assert.Equal(expectedIf, result) + Assert.Equal(1, calls) + End Sub + + + + + + Public Sub NullableStructureConditionalExpressionConditions(useFirst As Boolean, firstHasValue As Boolean, expected As Boolean) + Dim first As ZeroIsTrue? = If(firstHasValue, New ZeroIsTrue, CType(Nothing, ZeroIsTrue?)) + Dim second As ZeroIsTrue? = Nothing + Dim result = False + If If(useFirst, first, second) Then result = True + Assert.Equal(expected, result) + + result = False + If If(If(useFirst, first, second), second) Then result = True + Assert.Equal(expected, result) + + Dim count = 0 + Do Until If(useFirst, first, second) + count += 1 + If count = 3 Then Exit Do + Loop + Assert.Equal(If(expected, 0, 3), count) + + count = 0 + Do Until If(If(useFirst, first, second), second) + count += 1 + If count = 3 Then Exit Do + Loop + Assert.Equal(If(expected, 0, 3), count) + End Sub + + + + + + Public Sub StructureWithWideningToNullableBooleanConditions(converted As Boolean?, expectedIf As Boolean, expectedDoUntil As Integer) + Dim ns As New ToNullableBoolean With {.Converted = converted} + Dim s As ToNullableBoolean? = ns + Dim result = False + If ns Then result = True + Assert.Equal(expectedIf, result) + + result = False + If s Then result = True + Assert.Equal(expectedIf, result) + + Dim count = 0 + Do Until s + count += 1 + If count = 3 Then Exit Do + Loop + Assert.Equal(expectedDoUntil, count) + End Sub + + + + + + + + + Public Sub NullableBooleanTernaryConditions(f As Boolean, x As Integer?, y As Integer?) + Dim expected = x.HasValue AndAlso y.HasValue AndAlso If(f, x.Value < y.Value, x.Value > y.Value) + Dim result = False + If If(f, x < y, x > y) Then result = True + Assert.Equal(expected, result) + + Dim count = 0 + Do Until If(f, x < y, x > y) + count += 1 + If count = 3 Then Exit Do + Loop + Assert.Equal(If(expected, 0, 3), count) + End Sub + + + + + + + + + Public Sub NullableBooleanTernaryUntilConditions(f As Boolean, b As Boolean?) + Dim notB As Boolean? = Not b + Dim expectedEitherWay = If(f, b, notB).GetValueOrDefault() + Dim count = 0 + Do Until If(f, b, Not b) + count += 1 + If count = 3 Then Exit Do + Loop + Assert.Equal(If(expectedEitherWay, 0, 3), count) + + count = 0 + Do + count += 1 + If count = 3 Then Exit Do + Loop Until If(f, b, b) + Assert.Equal(If(b.GetValueOrDefault(), 1, 3), count) + End Sub + + + Public Sub NothingStructureWithWideningToNullableBooleanConditionThrows() + Dim s As ToNullableBoolean? = Nothing + Assert.Throws(Of InvalidOperationException)(Sub() + If s Then Return + End Sub) + Assert.Throws(Of InvalidOperationException)(Sub() + Do Until s : Exit Do : Loop + End Sub) + End Sub +End Class + +''' +''' VB doesn't lift a conversion to Boolean? for a Nothing operand +''' +Public Structure ToNullableBoolean + Public Converted As Boolean? + + Public Shared Widening Operator CType(s As ToNullableBoolean) As Boolean? + Return s.Converted + End Operator +End Structure + +''' +''' Its default value converts to True, so a condition that is Nothing must become False without converting the default +''' +Public Structure ZeroIsTrue + Public Value As Integer + + Public Shared Widening Operator CType(s As ZeroIsTrue) As Boolean + Return s.Value = 0 + End Operator +End Structure diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb new file mode 100644 index 000000000..1e993dfa2 --- /dev/null +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb @@ -0,0 +1,336 @@ +Imports System +Imports Xunit + +''' +''' VB converts a Boolean? loop condition to Boolean with Nothing becoming False, before deciding whether to continue (While) or stop (Until). +''' So a While loop whose condition is Nothing doesn't run, and an Until loop whose condition is Nothing isn't done. +''' Each loop stops after so that a condition that never ends the loop shows up as that count. +''' +Public Class NullableLoopConditionTests + Private Const MaxIterations As Integer = 3 + + + + + + + + Public Sub WhileLessThan(a As Integer?, b As Integer?, expected As Integer) + Dim count = 0 + While a < b + count += 1 + If count = MaxIterations Then Exit While + End While + Assert.Equal(expected, count) + End Sub + + + + + + + Public Sub WhileEquals(a As Integer?, b As Integer?, expected As Integer) + Dim count = 0 + While (a = b) + count += 1 + If count = MaxIterations Then Exit While + End While + Assert.Equal(expected, count) + End Sub + + + + + + + Public Sub DoWhileNotEquals(a As Integer?, b As Integer?, expected As Integer) + Dim count = 0 + Do While a <> b + count += 1 + If count = MaxIterations Then Exit Do + Loop + Assert.Equal(expected, count) + End Sub + + + + + + + + Public Sub DoUntilLessThan(a As Integer?, b As Integer?, expected As Integer) + Dim count = 0 + Do Until a < b + count += 1 + If count = MaxIterations Then Exit Do + Loop + Assert.Equal(expected, count) + End Sub + + + + + + Public Sub LoopWhileGreaterThanOrEqual(a As Integer?, b As Integer?, expected As Integer) + Dim count = 0 + Do + count += 1 + If count = MaxIterations Then Exit Do + Loop While b >= a + Assert.Equal(expected, count) + End Sub + + + + + + Public Sub LoopUntilGreaterThan(a As Integer?, b As Integer?, expected As Integer) + Dim count = 0 + Do + count += 1 + If count = MaxIterations Then Exit Do + Loop Until b > a + Assert.Equal(expected, count) + End Sub + + + + + + Public Sub BooleanVariable(flag As Boolean?, expectedWhile As Integer, expectedWhileNot As Integer, expectedDoUntil As Integer, expectedLoopWhile As Integer, expectedLoopUntil As Integer, expectedLoopUntilNot As Integer) + Dim count = 0 + While flag + count += 1 + If count = MaxIterations Then Exit While + End While + Assert.Equal(expectedWhile, count) + + count = 0 + While Not flag + count += 1 + If count = MaxIterations Then Exit While + End While + Assert.Equal(expectedWhileNot, count) + + count = 0 + Do Until flag + count += 1 + If count = MaxIterations Then Exit Do + Loop + Assert.Equal(expectedDoUntil, count) + + count = 0 + Do + count += 1 + If count = MaxIterations Then Exit Do + Loop While flag + Assert.Equal(expectedLoopWhile, count) + + count = 0 + Do + count += 1 + If count = MaxIterations Then Exit Do + Loop Until flag + Assert.Equal(expectedLoopUntil, count) + + count = 0 + Do + count += 1 + If count = MaxIterations Then Exit Do + Loop Until Not flag + Assert.Equal(expectedLoopUntilNot, count) + End Sub + + + + + + + + Public Sub CombinedWithLogicalOperators(flag As Boolean?, a As Integer?, expectedWhileAndAlso As Integer, expectedUntilOrElse As Integer) + Dim count = 0 + While flag AndAlso a < 2 + count += 1 + If count = MaxIterations Then Exit While + End While + Assert.Equal(expectedWhileAndAlso, count) + + count = 0 + Do Until flag OrElse a > 2 + count += 1 + If count = MaxIterations Then Exit Do + Loop + Assert.Equal(expectedUntilOrElse, count) + End Sub + + + + + + Public Sub UntilNotComparison(a As Integer?, b As Integer?, expectedDoUntil As Integer, expectedLoopUntil As Integer) + Dim count = 0 + Do Until Not (a < b) + count += 1 + If count = MaxIterations Then Exit Do + Loop + Assert.Equal(expectedDoUntil, count) + + count = 0 + Do + count += 1 + If count = MaxIterations Then Exit Do + Loop Until Not a >= b + Assert.Equal(expectedLoopUntil, count) + End Sub + + + Public Sub ConditionWithSideEffectsEvaluatedOncePerIteration() + Dim log As New System.Text.StringBuilder() + Dim values As Integer?() = {1, 2, Nothing, 3} + Dim index = 0 + Dim nextValue = Function() + log.Append("n") + index += 1 + Return values(index - 1) + End Function + Dim limit = Function() As Integer? + log.Append("l") + Return 3 + End Function + While nextValue() < limit() + log.Append("b") + End While + Assert.Equal("nlbnlbnl", log.ToString()) + + log.Clear() + index = 0 + Dim limitValue As Integer? = 3 + Do Until nextValue() = limitValue + log.Append("b") + Loop + Assert.Equal("nbnbnbn", log.ToString()) + End Sub + + + Public Sub WhileStopsWhenComparisonBecomesNothing() + Dim currentDate As Date? = New Date(2000, 1, 1) + Dim endDate As Date? = currentDate.Value.AddMinutes(2) + Dim count = 0 + While currentDate <= endDate + count += 1 + If count = 2 Then currentDate = Nothing Else currentDate = currentDate.Value.AddMinutes(1) + End While + Assert.Equal(2, count) + End Sub + + + + + Public Sub TernaryIfOperator(a As Integer?, expected As Integer) + Assert.Equal(expected, If(a < 2, 1, 2)) + End Sub + + + + + + + + + Public Sub NotOrderingComparisons(a As Integer?, b As Integer?) + Dim bothHaveValues = a.HasValue AndAlso b.HasValue + AssertNotComparison(bothHaveValues AndAlso Not a.Value < b.Value, Function() + Dim count = 0 + While Not (a < b) + count += 1 + If count = MaxIterations Then Exit While + End While + Return count + End Function, Function() + Dim count = 0 + Do Until Not (a < b) + count += 1 + If count = MaxIterations Then Exit Do + Loop + Return count + End Function) + AssertNotComparison(bothHaveValues AndAlso Not a.Value <= b.Value, Function() + Dim count = 0 + While Not a <= b + count += 1 + If count = MaxIterations Then Exit While + End While + Return count + End Function, Function() + Dim count = 0 + Do + count += 1 + If count = MaxIterations Then Exit Do + Loop Until Not a <= b + Return If(count = 1, 0, MaxIterations) + End Function) + AssertNotComparison(bothHaveValues AndAlso Not a.Value > b.Value, Function() + Dim count = 0 + Do While Not (a > b) + count += 1 + If count = MaxIterations Then Exit Do + Loop + Return count + End Function, Function() + Dim count = 0 + Do Until (Not a > b) + count += 1 + If count = MaxIterations Then Exit Do + Loop + Return count + End Function) + Dim result = False + If Not (a >= b) Then result = True + Assert.Equal(bothHaveValues AndAlso Not a.Value >= b.Value, result) + result = False + If Not (a >= b) AndAlso a <> 0 Then result = True + Assert.Equal(bothHaveValues AndAlso Not a.Value >= b.Value AndAlso a.Value <> 0, result) + End Sub + + + + + + + Public Sub DoUntilNullableAndNonNullableComparisons(a As Integer?, b As Integer?) + Dim i = 0 + Do Until a < b AndAlso i < 3 + i += 1 + If i = MaxIterations Then Exit Do + Loop + Assert.Equal(If(a.HasValue AndAlso b.HasValue AndAlso a.Value < b.Value, 0, MaxIterations), i) + End Sub + + ''' + ''' A While loop runs while its condition is True, and an Until loop runs while it isn't + ''' + Private Shared Sub AssertNotComparison(isTrue As Boolean, countWhile As Func(Of Integer), countUntil As Func(Of Integer)) + Assert.Equal(If(isTrue, MaxIterations, 0), countWhile()) + Assert.Equal(If(isTrue, 0, MaxIterations), countUntil()) + End Sub + + + + + + + Public Sub NotComparisonsThatCannotBeFlipped(a As Double?, b As Double?, expectedNotLessOrEqual As Boolean) + Dim count = 0 + While Not (a <= b) + count += 1 + If count = MaxIterations Then Exit While + End While + Assert.Equal(If(expectedNotLessOrEqual, MaxIterations, 0), count) + + Dim x As Integer? = If(a.HasValue AndAlso Not Double.IsNaN(a.Value), CInt(a.Value), CType(Nothing, Integer?)) + Dim y As Integer? = If(b.HasValue, CInt(b.Value), CType(Nothing, Integer?)) + Dim result = False + If Not (x = y) Then result = True + Assert.Equal(x.HasValue AndAlso y.HasValue AndAlso x.Value <> y.Value, result) + End Sub +End Class