From 96e4685d5d728d9617651c83d17a0bfe56f4edd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:19:23 +0000 Subject: [PATCH 01/21] Extract ConvertConditionAsync for If statement conditions Pure refactor so loop conditions can share it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../MethodBodyExecutableStatementVisitor.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs index 6e9720622..273f53b38 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; From 86f9dc79fa3dfb480bde3bc145e991da984c0b78 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:22:47 +0000 Subject: [PATCH 02/21] Treat parenthesized nullable conditions as conditions VB's binder passes the condition context through parentheses, so Nothing means False. Previously a (bool) cast was emitted, which throws on null. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/TypeConversionAnalyzer.cs | 2 +- .../CSharp/VbSyntaxNodeExtensions.cs | 25 ++++++++--- .../ExpressionTests/BinaryExpressionTests.cs | 32 +++++++++++++ .../VBToCS/NullableConditionTests.vb | 45 +++++++++++++++++++ 4 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index d1c3d3d24..6e3cc7db2 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -341,7 +341,7 @@ private bool TryAnalyzeCsConversion(VBasic.VisualBasicCompilation vbCompilation, 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))) { + (vbNode.SkipIntoParens() is not VBSyntax.BinaryExpressionSyntax and not VBSyntax.UnaryExpressionSyntax || vbNode.SkipIntoParens().IsKind(VBasic.SyntaxKind.AndExpression, VBasic.SyntaxKind.OrExpression, VBasic.SyntaxKind.ExclusiveOrExpression))) { typeConversionKind = TypeConversionKind.NullableBool; return true; } diff --git a/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs b/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs index 7fce4ae6a..c2accc089 100644 --- a/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs +++ b/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs @@ -20,15 +20,28 @@ public static bool PrecedenceCouldChange(this VBasic.VisualBasicSyntaxNode node) return parentIsMemberAccessExpression || parentIsNonArgumentExpression && !parentIsBinaryExpression && !parentIsLambda && !parentIsParenthesis; } + /// + /// 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.WhereClauseSyntax or VBSyntax.PartitionWhileClauseSyntax => true, + _ => false + }; + } + + private static VBSyntax.ExpressionSyntax OutermostParenthesizedOrSelf(this VBSyntax.ExpressionSyntax vbNode) + { + while (vbNode.Parent is VBSyntax.ParenthesizedExpressionSyntax parenthesized) vbNode = parenthesized; + return vbNode; } /// 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/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb new file mode 100644 index 000000000..c8c239461 --- /dev/null +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb @@ -0,0 +1,45 @@ +Imports System +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 +End Class From 0109d8a595eb3d7fc2d1be14dd1e1e4bad99231f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:22:47 +0000 Subject: [PATCH 03/21] WIP: Add failing self-verifying tests for nullable loop conditions Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../VBToCS/NullableLoopConditionTests.vb | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb new file mode 100644 index 000000000..be1ce142d --- /dev/null +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb @@ -0,0 +1,184 @@ +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 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 +End Class From 25a298160e21d8baf03c143aeddccdaddace2718 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:40:45 +0000 Subject: [PATCH 04/21] Extract ConvertLoopContinueConditionAsync for Do loop conditions Pure refactor: top and bottom While/Until clauses share one conversion. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../MethodBodyExecutableStatementVisitor.cs | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs index 273f53b38..02d1730a6 100644 --- a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs @@ -1197,35 +1197,23 @@ 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 + /// + private async Task ConvertLoopContinueConditionAsync(VBSyntax.WhileOrUntilClauseSyntax clause) + { + var condition = await clause.Condition.AcceptAsync(_expressionVisitor); + return clause.WhileOrUntilKeyword.IsKind(VBasic.SyntaxKind.UntilKeyword) ? condition.InvertCondition() : condition; } public override async Task> VisitCallStatement(VBSyntax.CallStatementSyntax node) From b0daae13c039e9caa919845241a419316424a320 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 17:54:12 +0000 Subject: [PATCH 05/21] Convert nullable While, Do/Loop and Catch When conditions to bool VB converts these conditions to Boolean (Nothing to False) like If conditions, but they weren't treated as Boolean contexts, so a Boolean? condition gave CS0266. Until conditions are converted before being inverted, matching VB, so an Until loop continues while its condition is Nothing. Fixes icsharpcode/CodeConverter#1300 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../MethodBodyExecutableStatementVisitor.cs | 9 +- .../CSharp/VbSyntaxNodeExtensions.cs | 2 + .../StatementTests/LoopStatementTests.cs | 129 ++++++++++++++++++ .../StatementTests/MethodStatementTests.cs | 35 +++++ .../VBToCS/NullableConditionTests.vb | 50 +++++++ .../VBToCS/NullableLoopConditionTests.vb | 48 +++++++ 6 files changed, 269 insertions(+), 4 deletions(-) diff --git a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs index 02d1730a6..34a0c64e8 100644 --- a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs @@ -1143,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) @@ -1188,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() )); } @@ -1208,11 +1208,12 @@ public override async Task> VisitDoLoopBlock(VBSynta } /// - /// C# only has While loops, so an Until loop continues while its condition is false + /// 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 clause.Condition.AcceptAsync(_expressionVisitor); + var condition = await ConvertConditionAsync(clause.Condition); return clause.WhileOrUntilKeyword.IsKind(VBasic.SyntaxKind.UntilKeyword) ? condition.InvertCondition() : condition; } diff --git a/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs b/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs index c2accc089..791834bb1 100644 --- a/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs +++ b/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs @@ -33,6 +33,8 @@ public static bool AlwaysHasBooleanTypeInCSharp(this VBSyntax.ExpressionSyntax v 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 }; diff --git a/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index d30e023c5..669652980 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -38,6 +38,135 @@ 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?) + 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 + End Sub +End Class", @" +internal partial class TestClass +{ + private void TestMethod(int? x, int? y, bool? b) + { + 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)); + } +}"); + } + + [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/NullableConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb index c8c239461..ce209bd2f 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb @@ -1,4 +1,5 @@ Imports System +Imports System.Linq Imports Xunit ''' @@ -42,4 +43,53 @@ Public Class NullableConditionTests 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 End Class diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb index be1ce142d..ec3963341 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb @@ -163,6 +163,54 @@ Public Class NullableLoopConditionTests 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) From bb4abbe023536ecc986ebab62b64d7286fa99cce Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 18:12:23 +0000 Subject: [PATCH 06/21] Treat Nothing as False for nullable non-Boolean conditions With Option Strict Off, a nullable non-Boolean condition (e.g. If x Then, While x with x As Integer?) converted to Conversions.ToBoolean(x.Value), which throws when x is Nothing. VB lifts the conversion and treats Nothing as False, so use GetValueOrDefault: its zero default also converts to False. Also pins Option Strict Off Object conditions in loops, which already worked. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/TypeConversionAnalyzer.cs | 9 +- .../StatementTests/LoopStatementTests.cs | 41 +++++++++ .../NullableConditionOptionStrictOffTests.vb | 89 +++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index 6e3cc7db2..8ae028767 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -426,6 +426,13 @@ 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 AddExplicitConvertTo(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, ITypeSymbol currentType, ITypeSymbol targetType) { var displayType = targetType.ToMinimalDisplayString(_semanticModel, vbNode.SpanStart); @@ -443,7 +450,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/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index 669652980..011ec58bf 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -139,6 +139,47 @@ private void TestMethod(int? x, int? y, bool? b) }"); } + [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 NonNullableUntilConditionsAreInvertedAsync() { diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb new file mode 100644 index 000000000..f2a8d6061 --- /dev/null +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb @@ -0,0 +1,89 @@ +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 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 From 7337b176027a6620a1a42c346c105727ae06cc62 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 18:15:26 +0000 Subject: [PATCH 07/21] Compute the unparenthesized condition once in NullableBool analysis Pure refactor. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/TypeConversionAnalyzer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index 8ae028767..837d5ab5e 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -341,7 +341,8 @@ private bool TryAnalyzeCsConversion(VBasic.VisualBasicCompilation vbCompilation, return true; } else if (csConversion.IsNullable && csConvertedType.SpecialType == SpecialType.System_Boolean && vbNode.AlwaysHasBooleanTypeInCSharp() && - (vbNode.SkipIntoParens() is not VBSyntax.BinaryExpressionSyntax and not VBSyntax.UnaryExpressionSyntax || vbNode.SkipIntoParens().IsKind(VBasic.SyntaxKind.AndExpression, VBasic.SyntaxKind.OrExpression, VBasic.SyntaxKind.ExclusiveOrExpression))) { + vbNode.SkipIntoParens() is var unparenthesized && + (unparenthesized is not VBSyntax.BinaryExpressionSyntax and not VBSyntax.UnaryExpressionSyntax || unparenthesized.IsKind(VBasic.SyntaxKind.AndExpression, VBasic.SyntaxKind.OrExpression, VBasic.SyntaxKind.ExclusiveOrExpression))) { typeConversionKind = TypeConversionKind.NullableBool; return true; } From 05406ef22ec35af066a1707b5878f04a79a861e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 18:46:45 +0000 Subject: [PATCH 08/21] Treat Nothing as False for nullable conditions with a user-defined conversion A nullable structure with a Widening CType to Boolean used as a condition converted to (bool)s, which throws when s is Nothing. VB lifts the conversion and treats Nothing as False. Since the operator may convert the default value to True, check HasValue (or pattern-match a non-reusable expression) first. Also pins that conversions outside conditions keep .Value and throw like VB. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/TypeConversionAnalyzer.cs | 12 +- ...VisualBasicNullableExpressionsConverter.cs | 10 ++ .../StatementTests/LoopStatementTests.cs | 107 ++++++++++++++++++ .../NullableConditionOptionStrictOffTests.vb | 44 +++++++ .../VBToCS/NullableConditionTests.vb | 46 ++++++++ 5 files changed, 218 insertions(+), 1 deletion(-) diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index 837d5ab5e..ff3017ac4 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -108,7 +108,9 @@ private ExpressionSyntax AddTypeConversion(VBSyntax.ExpressionSyntax vbNode, Exp return addParenthesisIfNeeded ? vbNode.ParenthesizeIfPrecedenceCouldChange(csNode) : csNode; case TypeConversionKind.DestructiveCast: case TypeConversionKind.NonDestructiveCast: - return CreateCast(csNode, vbConvertedType); + return IsNonBooleanNullableCondition(vbNode, vbType, vbConvertedType) + ? _vbNullableExpressionsConverter.ConvertWhenNotNullElseFalse(vbNode, csNode, value => CreateCast(value, vbConvertedType)) + : CreateCast(csNode, vbConvertedType); case TypeConversionKind.Conversion: return AddExplicitConvertTo(vbNode, csNode, vbType, vbConvertedType); case TypeConversionKind.NullableBool: @@ -434,6 +436,14 @@ private ExpressionSyntax AddRoundInvocation(ExpressionSyntax csNode) private static ExpressionSyntax GetUnderlyingValueToConvert(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNullableNode) => vbNode.AlwaysHasBooleanTypeInCSharp() ? csNullableNode.GetValueOrDefault() : csNullableNode.NullableGetValueExpression(); + /// + /// In a condition, VB lifts a conversion to Boolean (e.g. a user-defined CType), so Nothing is False rather than throwing. + /// A Boolean? condition is excluded: the nullable Boolean logic already makes it bool, so a cast on it is redundant and simplified away. + /// + private static bool IsNonBooleanNullableCondition(VBSyntax.ExpressionSyntax vbNode, ITypeSymbol vbType, ITypeSymbol vbConvertedType) => + vbType.IsNullable(out var underlyingType) && underlyingType.SpecialType != SpecialType.System_Boolean && + !vbConvertedType.IsNullable() && vbNode.AlwaysHasBooleanTypeInCSharp(); + private ExpressionSyntax AddExplicitConvertTo(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, ITypeSymbol currentType, ITypeSymbol targetType) { var displayType = targetType.ToMinimalDisplayString(_semanticModel, vbNode.SpanStart); diff --git a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs index 14aba516a..b5ba4c315 100644 --- a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs +++ b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs @@ -100,6 +100,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) diff --git a/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index 011ec58bf..71c407ce4 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -180,6 +180,113 @@ private void TestMethod(int? x, object o) }", 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() + 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); + } +}"); + } + + [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 NonNullableUntilConditionsAreInvertedAsync() { diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb index f2a8d6061..bb8a728be 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionOptionStrictOffTests.vb @@ -54,6 +54,42 @@ Public Class NullableConditionOptionStrictOffTests 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 + @@ -87,3 +123,11 @@ Public Class NullableConditionOptionStrictOffTests 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 index ce209bd2f..dc6dda843 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb @@ -92,4 +92,50 @@ Public Class NullableConditionTests 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 End Class + +''' +''' 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 From 1e40a742dbb5deb97aa2f2c6676a090ce1132864 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 21:17:03 +0000 Subject: [PATCH 09/21] Parenthesize the operand of generated is-patterns A conditional or coalesce expression as the operand of a generated 'is { } argN' / 'is var argN' pattern was emitted without parentheses, so the C# text parsed differently (e.g. If(a, b, c) as a nullable structure condition gave CS0173). The syntax tree was right, so only reparsing showed it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- ...VisualBasicNullableExpressionsConverter.cs | 6 ++-- .../StatementTests/LoopStatementTests.cs | 7 +++++ .../VBToCS/NullableConditionTests.vb | 30 +++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs index b5ba4c315..8b97aa420 100644 --- a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs +++ b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs @@ -128,7 +128,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) @@ -138,7 +138,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) @@ -150,7 +150,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) diff --git a/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index 71c407ce4..6cf7825ab 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -199,6 +199,8 @@ 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 @@ -230,6 +232,11 @@ private void TestMethod(S? s) { } while (GetS() is { } arg1 && (bool)arg1); + if ((s ?? GetS()) is { } arg2 && (bool)arg2) + return; + while (!((s.HasValue ? s : GetS()) is { } arg3) || !(bool)arg3) + { + } } }"); } diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb index dc6dda843..045864768 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb @@ -127,6 +127,36 @@ Public Class NullableConditionTests 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 End Class ''' From 74ff12483d932fcdf601dad457419e3eb6566f48 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 21:43:27 +0000 Subject: [PATCH 10/21] Don't lift user-defined conversions returning Boolean? in conditions VB doesn't lift a CType returning Boolean? the same way: it throws for a Nothing operand and treats a Nothing result as False. Leave such conditions with the previous cast output rather than emitting HasValue checks that hide the throw and still throw on a Nothing result. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/TypeConversionAnalyzer.cs | 10 +++--- .../StatementTests/LoopStatementTests.cs | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index ff3017ac4..1aae57d6c 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; @@ -437,12 +437,14 @@ private static ExpressionSyntax GetUnderlyingValueToConvert(VBSyntax.ExpressionS vbNode.AlwaysHasBooleanTypeInCSharp() ? csNullableNode.GetValueOrDefault() : csNullableNode.NullableGetValueExpression(); /// - /// In a condition, VB lifts a conversion to Boolean (e.g. a user-defined CType), so Nothing is False rather than throwing. + /// In a condition, VB lifts a conversion to Boolean (e.g. a user-defined CType returning Boolean), so Nothing is False rather than throwing. + /// A user-defined CType returning Boolean? isn't lifted that way: VB throws for a Nothing operand, and treats a Nothing result as False. /// A Boolean? condition is excluded: the nullable Boolean logic already makes it bool, so a cast on it is redundant and simplified away. /// - private static bool IsNonBooleanNullableCondition(VBSyntax.ExpressionSyntax vbNode, ITypeSymbol vbType, ITypeSymbol vbConvertedType) => + private bool IsNonBooleanNullableCondition(VBSyntax.ExpressionSyntax vbNode, ITypeSymbol vbType, ITypeSymbol vbConvertedType) => vbType.IsNullable(out var underlyingType) && underlyingType.SpecialType != SpecialType.System_Boolean && - !vbConvertedType.IsNullable() && vbNode.AlwaysHasBooleanTypeInCSharp(); + !vbConvertedType.IsNullable() && vbNode.AlwaysHasBooleanTypeInCSharp() && + !(VBasic.VisualBasicExtensions.GetConversion(_semanticModel, vbNode).MethodSymbol?.ReturnType).IsNullable(); private ExpressionSyntax AddExplicitConvertTo(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, ITypeSymbol currentType, ITypeSymbol targetType) { diff --git a/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index 6cf7825ab..b25d60942 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -241,6 +241,42 @@ private void TestMethod(S? s) }"); } + [Fact] + public async Task NullableStructureWithWideningToNullableBooleanConditionsAreNotLiftedAsync() + { + 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?) + If s Then Return + Do Until s : Loop + 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) + { + if ((bool)s) + return; + while (!(bool)s) + { + } + } +}"); + } + [Fact] public async Task OptionStrictOffNullableToBooleanOutsideConditionsKeepsValueAsync() { From 1f3c81ed565d6ac5236cb213171347d998668fb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 21:54:17 +0000 Subject: [PATCH 11/21] Extract IsMadeBoolByNullableOperatorConversion predicate Pure refactor of the NullableBool condition, plus a comment explaining why a test pins master's output for CType returning Boolean?. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/TypeConversionAnalyzer.cs | 13 +++++++++++-- Tests/CSharp/StatementTests/LoopStatementTests.cs | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index 1aae57d6c..644480aac 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -343,8 +343,7 @@ private bool TryAnalyzeCsConversion(VBasic.VisualBasicCompilation vbCompilation, return true; } else if (csConversion.IsNullable && csConvertedType.SpecialType == SpecialType.System_Boolean && vbNode.AlwaysHasBooleanTypeInCSharp() && - vbNode.SkipIntoParens() is var unparenthesized && - (unparenthesized is not VBSyntax.BinaryExpressionSyntax and not VBSyntax.UnaryExpressionSyntax || unparenthesized.IsKind(VBasic.SyntaxKind.AndExpression, VBasic.SyntaxKind.OrExpression, VBasic.SyntaxKind.ExclusiveOrExpression))) { + !IsMadeBoolByNullableOperatorConversion(vbNode)) { typeConversionKind = TypeConversionKind.NullableBool; return true; } @@ -357,6 +356,16 @@ private bool TryAnalyzeCsConversion(VBasic.VisualBasicCompilation vbCompilation, return false; } + /// + /// In a condition, the nullable operator conversions already give bool 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 {}; diff --git a/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index b25d60942..4bf697a68 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -241,6 +241,7 @@ private void TestMethod(S? s) }"); } + // Deliberately pins master's known imperfect output: VB doesn't lift a CType returning Boolean? (a Nothing operand throws), so it's left alone [Fact] public async Task NullableStructureWithWideningToNullableBooleanConditionsAreNotLiftedAsync() { From 4209a8bafb1fbe8f97660b685da9217ce248d8e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 23:48:08 +0000 Subject: [PATCH 12/21] Extract CreateCastForContext Pure refactor. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/TypeConversionAnalyzer.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index 644480aac..3272809c6 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -108,9 +108,7 @@ private ExpressionSyntax AddTypeConversion(VBSyntax.ExpressionSyntax vbNode, Exp return addParenthesisIfNeeded ? vbNode.ParenthesizeIfPrecedenceCouldChange(csNode) : csNode; case TypeConversionKind.DestructiveCast: case TypeConversionKind.NonDestructiveCast: - return IsNonBooleanNullableCondition(vbNode, vbType, vbConvertedType) - ? _vbNullableExpressionsConverter.ConvertWhenNotNullElseFalse(vbNode, csNode, value => CreateCast(value, vbConvertedType)) - : CreateCast(csNode, vbConvertedType); + return CreateCastForContext(vbNode, csNode, vbType, vbConvertedType); case TypeConversionKind.Conversion: return AddExplicitConvertTo(vbNode, csNode, vbType, vbConvertedType); case TypeConversionKind.NullableBool: @@ -445,6 +443,11 @@ private ExpressionSyntax AddRoundInvocation(ExpressionSyntax csNode) private static ExpressionSyntax GetUnderlyingValueToConvert(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNullableNode) => vbNode.AlwaysHasBooleanTypeInCSharp() ? csNullableNode.GetValueOrDefault() : csNullableNode.NullableGetValueExpression(); + private ExpressionSyntax CreateCastForContext(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, ITypeSymbol vbType, ITypeSymbol vbConvertedType) => + IsNonBooleanNullableCondition(vbNode, vbType, vbConvertedType) + ? _vbNullableExpressionsConverter.ConvertWhenNotNullElseFalse(vbNode, csNode, value => CreateCast(value, vbConvertedType)) + : CreateCast(csNode, vbConvertedType); + /// /// In a condition, VB lifts a conversion to Boolean (e.g. a user-defined CType returning Boolean), so Nothing is False rather than throwing. /// A user-defined CType returning Boolean? isn't lifted that way: VB throws for a Nothing operand, and treats a Nothing result as False. From 92cb1ef3866d3b24356e30aec693549894f7f7f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 23:52:43 +0000 Subject: [PATCH 13/21] Compare a CType returning Boolean? with True in conditions A condition converted by a user-defined CType returning Boolean? gave (bool)s, which throws when the operator returns Nothing; VB treats that as False. Emit (bool?)s.Value == true: .Value keeps VB's throw for a Nothing operand, which C# would otherwise lift to null. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/TypeConversionAnalyzer.cs | 27 +++++++---- .../StatementTests/LoopStatementTests.cs | 18 +++++--- .../VBToCS/NullableConditionTests.vb | 45 +++++++++++++++++++ 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index 3272809c6..84223ab6f 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -443,20 +443,31 @@ private ExpressionSyntax AddRoundInvocation(ExpressionSyntax csNode) private static ExpressionSyntax GetUnderlyingValueToConvert(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNullableNode) => vbNode.AlwaysHasBooleanTypeInCSharp() ? csNullableNode.GetValueOrDefault() : csNullableNode.NullableGetValueExpression(); - private ExpressionSyntax CreateCastForContext(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, ITypeSymbol vbType, ITypeSymbol vbConvertedType) => - IsNonBooleanNullableCondition(vbNode, vbType, vbConvertedType) + private ExpressionSyntax CreateCastForContext(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 a conversion to Boolean (e.g. a user-defined CType returning Boolean), so Nothing is False rather than throwing. - /// A user-defined CType returning Boolean? isn't lifted that way: VB throws for a Nothing operand, and treats a Nothing result as False. + /// 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, so a cast on it is redundant and simplified away. /// - private bool IsNonBooleanNullableCondition(VBSyntax.ExpressionSyntax vbNode, ITypeSymbol vbType, ITypeSymbol vbConvertedType) => - vbType.IsNullable(out var underlyingType) && underlyingType.SpecialType != SpecialType.System_Boolean && - !vbConvertedType.IsNullable() && vbNode.AlwaysHasBooleanTypeInCSharp() && - !(VBasic.VisualBasicExtensions.GetConversion(_semanticModel, vbNode).MethodSymbol?.ReturnType).IsNullable(); + 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) { diff --git a/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index 4bf697a68..dea68e9f1 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -241,9 +241,11 @@ private void TestMethod(S? s) }"); } - // Deliberately pins master's known imperfect output: VB doesn't lift a CType returning Boolean? (a Nothing operand throws), so it's left alone + /// + /// 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 NullableStructureWithWideningToNullableBooleanConditionsAreNotLiftedAsync() + public async Task StructureWithWideningToNullableBooleanConditionsCompareResultWithTrueAsync() { await TestConversionVisualBasicToCSharpAsync(@"Structure S Public Shared Widening Operator CType(s As S) As Boolean? @@ -252,9 +254,10 @@ End Operator End Structure Class TestClass - Private Sub TestMethod(s As S?) + 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 @@ -267,11 +270,14 @@ internal partial struct S internal partial class TestClass { - private void TestMethod(S? s) + private void TestMethod(S? s, S ns) { - if ((bool)s) + if ((bool?)s.Value == true) return; - while (!(bool)s) + while ((bool?)s.Value != true) + { + } + while ((bool?)ns == true) { } } diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb index 045864768..ed9f1fecb 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb @@ -157,8 +157,53 @@ Public Class NullableConditionTests 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 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 ''' From 453417ffcb3b2d0b3279ae47c9215cc446010fcb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 00:14:36 +0000 Subject: [PATCH 14/21] Don't negate nullable ordering comparisons when inverting Until conditions A lifted <, <=, > or >= is false when an operand is null, so its negated operator would be false too rather than true. The output is unchanged: the inversion previously stopped at a redundant (bool) cast by coincidence. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../MethodBodyExecutableStatementVisitor.cs | 10 ++++++- CodeConverter/Util/CSharpUtil.cs | 30 +++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs index 34a0c64e8..0b956b0a6 100644 --- a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs @@ -1214,9 +1214,17 @@ public override async Task> VisitDoLoopBlock(VBSynta private async Task ConvertLoopContinueConditionAsync(VBSyntax.WhileOrUntilClauseSyntax clause) { var condition = await ConvertConditionAsync(clause.Condition); - return clause.WhileOrUntilKeyword.IsKind(VBasic.SyntaxKind.UntilKeyword) ? condition.InvertCondition() : condition; + return clause.WhileOrUntilKeyword.IsKind(VBasic.SyntaxKind.UntilKeyword) + ? condition.InvertCondition(negateOrderingComparisons: !ContainsNullableOrderingComparison(clause.Condition)) + : condition; } + private bool ContainsNullableOrderingComparison(VBSyntax.ExpressionSyntax vbCondition) => + vbCondition.DescendantNodesAndSelf().OfType().Any(comparison => + comparison.IsKind(VBasic.SyntaxKind.LessThanExpression, VBasic.SyntaxKind.LessThanOrEqualExpression, + VBasic.SyntaxKind.GreaterThanExpression, VBasic.SyntaxKind.GreaterThanOrEqualExpression) && + (_semanticModel.GetTypeInfo(comparison.Left).ConvertedType.IsNullable() || _semanticModel.GetTypeInfo(comparison.Right).ConvertedType.IsNullable())); + public override async Task> VisitCallStatement(VBSyntax.CallStatementSyntax node) { return SingleStatement(await node.Invocation.AcceptAsync(_expressionVisitor)); diff --git a/CodeConverter/Util/CSharpUtil.cs b/CodeConverter/Util/CSharpUtil.cs index 3e9e80157..0adc77ba8 100644 --- a/CodeConverter/Util/CSharpUtil.cs +++ b/CodeConverter/Util/CSharpUtil.cs @@ -9,15 +9,19 @@ internal static class CSharpUtil /// Inverts a boolean condition. Note: The condition object can be frozen (from AST) it's cloned internally. /// /// The condition to invert. - public static ExpressionSyntax InvertCondition(this ExpressionSyntax condition) + /// + /// False where an operand of <, <=, > or >= may be nullable: a lifted comparison is false when an operand is null, + /// so the negated operator would be false too, rather than true. + /// + public static ExpressionSyntax InvertCondition(this ExpressionSyntax condition, bool negateOrderingComparisons = true) { - return InvertConditionInternal(condition); + return InvertConditionInternal(condition, negateOrderingComparisons); } - private static ExpressionSyntax InvertConditionInternal(ExpressionSyntax condition) + private static ExpressionSyntax InvertConditionInternal(ExpressionSyntax condition, bool negateOrderingComparisons) { if (condition is ParenthesizedExpressionSyntax) { - return SyntaxFactory.ParenthesizedExpression(InvertCondition(((ParenthesizedExpressionSyntax)condition).Expression)); + return SyntaxFactory.ParenthesizedExpression(InvertCondition(((ParenthesizedExpressionSyntax)condition).Expression, negateOrderingComparisons)); } if (condition is PrefixUnaryExpressionSyntax) { @@ -34,21 +38,17 @@ private static ExpressionSyntax InvertConditionInternal(ExpressionSyntax conditi var bOp = (BinaryExpressionSyntax)condition; 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)) + return SyntaxFactory.BinaryExpression(NegateConditionOperator(bOp.Kind()), InvertCondition(bOp.Left, negateOrderingComparisons), InvertCondition(bOp.Right, negateOrderingComparisons)); + + if (bOp.IsKind(SyntaxKind.EqualsExpression) || bOp.IsKind(SyntaxKind.NotEqualsExpression) || + negateOrderingComparisons && IsOrderingComparison(bOp)) 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)); + return cEx.WithCondition(InvertCondition(cEx.Condition, negateOrderingComparisons)); } if (condition is LiteralExpressionSyntax) { @@ -61,6 +61,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) { From 0f361de9b1d684f4c919c84e01def9fa652811da Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 00:14:51 +0000 Subject: [PATCH 15/21] Don't cast conditions the nullable operator logic already made bool The (bool) cast was redundant (the simplifier removed it), but it stopped an Until loop's inversion from simplifying, e.g. Loop Until Not b gave !(b == false). Now it gives b != false, and Do Until b = True gives b != true. Other Until conditions are now inverted through && and ||. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/TypeConversionAnalyzer.cs | 9 ++++----- Tests/CSharp/StatementTests/LoopStatementTests.cs | 10 +++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index 84223ab6f..e62146569 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -340,9 +340,8 @@ private bool TryAnalyzeCsConversion(VBasic.VisualBasicCompilation vbCompilation, typeConversionKind = TypeConversionKind.Conversion; return true; } - else if (csConversion.IsNullable && csConvertedType.SpecialType == SpecialType.System_Boolean && vbNode.AlwaysHasBooleanTypeInCSharp() && - !IsMadeBoolByNullableOperatorConversion(vbNode)) { - 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) { @@ -355,7 +354,7 @@ private bool TryAnalyzeCsConversion(VBasic.VisualBasicCompilation vbCompilation, } /// - /// In a condition, the nullable operator conversions already give bool for operators such as comparisons, Not, AndAlso and OrElse, but not for And, Or and Xor + /// 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) { @@ -464,7 +463,7 @@ private ITypeSymbol GetNullableReturnTypeOfUserDefinedConversion(VBSyntax.Expres /// /// 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, so a cast on it is redundant and simplified away. + /// 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(); diff --git a/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index dea68e9f1..0d023c20d 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -86,6 +86,7 @@ Private Sub TestMethod(x As Integer?, y As Integer?, b As Boolean?) Do : Loop While b Do : Loop Until x > y Do : Loop Until Not b + Do Until b = True : Loop End Sub End Class", @" internal partial class TestClass @@ -110,13 +111,13 @@ private void TestMethod(int? x, int? y, bool? b) while (!(x < y)) { } - while (!(x.HasValue && y.HasValue && x.Value == y.Value)) + while (!x.HasValue || !y.HasValue || x.Value != y.Value) { } while (b != true) { } - while (!(b.GetValueOrDefault() || x > 2)) + while (!b.GetValueOrDefault() && !(x > 2)) { } do @@ -134,7 +135,10 @@ private void TestMethod(int? x, int? y, bool? b) do { } - while (!(b == false)); + while (b != false); + while (b != true) + { + } } }"); } From eaf0c03b9a8d0aba1dc56d193bcf71cccb3d9ad7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 00:22:43 +0000 Subject: [PATCH 16/21] Flip the lifted operator for Not of a nullable ordering comparison in conditions Where Not (x < y) is only tested for True, x >= y is equivalent: both are false when an operand is Nothing. So While Not (x < y) gives while (x >= y), and Do Until Not (x < y) gives while (!(x >= y)). Floating point (NaN is unordered), Boolean, structures, and = and <> keep the existing form. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/ExpressionNodeVisitor.cs | 3 + .../CSharp/VbSyntaxNodeExtensions.cs | 2 +- ...VisualBasicNullableExpressionsConverter.cs | 27 ++++++ .../StatementTests/LoopStatementTests.cs | 73 +++++++++++++++ .../VBToCS/NullableLoopConditionTests.vb | 90 +++++++++++++++++++ 5 files changed, 194 insertions(+), 1 deletion(-) 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/VbSyntaxNodeExtensions.cs b/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs index 791834bb1..9fd8d9273 100644 --- a/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs +++ b/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs @@ -40,7 +40,7 @@ public static bool AlwaysHasBooleanTypeInCSharp(this VBSyntax.ExpressionSyntax v }; } - private static VBSyntax.ExpressionSyntax OutermostParenthesizedOrSelf(this VBSyntax.ExpressionSyntax vbNode) + 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 8b97aa420..8f454ee81 100644 --- a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs +++ b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs @@ -33,6 +33,11 @@ internal class VisualBasicNullableExpressionsConverter /// private static readonly SyntaxAnnotation IsNotNullableAnnotation = new("CodeConverter.Nullable", false.ToString()); + /// + /// Marks a comparison already negated for its parent Not + /// + 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) @@ -234,6 +239,10 @@ private ExpressionSyntax ForRelationalOperators(VBSyntax.BinaryExpressionSyntax if (vbNode.IsOnlyTestedForTrue(_semanticModel) && IsLiftedOperatorFalseWhenVbIsNothing(vbNode, isLhsNullable, isRhsNullable)) { return csNode.WithAdditionalAnnotations(IsNotNullableAnnotation); } + if (IsNegationOnlyTestedForTrue(vbNode) && IsLiftedOrderingWithTotalOrder(vbNode)) { + var negated = SyntaxFactory.BinaryExpression(CSharpUtil.NegateRelationalOperator(csNode.Kind()), csNode.Left, csNode.Right); + return negated.WithAdditionalAnnotations(IsNotNullableAnnotation, NegatedForParentNotAnnotation); + } ExpressionSyntax? lhsName = lhs, rhsName = rhs; var lhsReusable = IsSafelyReusable(vbNode.Left); @@ -272,6 +281,24 @@ 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.IsKind(VBasic.SyntaxKind.LessThanExpression, VBasic.SyntaxKind.LessThanOrEqualExpression, + VBasic.SyntaxKind.GreaterThanExpression, VBasic.SyntaxKind.GreaterThanOrEqualExpression) && + 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 /// diff --git a/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index 0d023c20d..06d9924f4 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -341,6 +341,79 @@ private void TestMethod(int? x, S? s) }", 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() { diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb index ec3963341..b0652ae55 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb @@ -229,4 +229,94 @@ Public Class NullableLoopConditionTests 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 + + ''' + ''' 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 From 813cfc568636967fa472b1f20ce79f979a70990f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 02:49:01 +0000 Subject: [PATCH 17/21] Parenthesize a Boolean? condition before comparing it with true A ternary Boolean? condition such as If If(f, x < y, x > y) or Do Until If(f, b, Not b) gave f ? A : B == true, which reparses as f ? A : (B == true) (CS0173/CS0266). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../CSharp/TypeConversionAnalyzer.cs | 3 +- .../VBToCS/NullableConditionTests.vb | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index e62146569..79ac714aa 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -112,8 +112,7 @@ private ExpressionSyntax AddTypeConversion(VBSyntax.ExpressionSyntax vbNode, Exp 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, diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb index ed9f1fecb..9041961ce 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableConditionTests.vb @@ -181,6 +181,52 @@ Public Class NullableConditionTests 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 From 2e01c2344b6318cea3009bb1b50631ebbb5b5eb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 02:49:01 +0000 Subject: [PATCH 18/21] Negate the whole ternary when inverting an Until condition Inverting only the condition of a ternary swaps its arms rather than negating it: Do Until If(f, a, b) gave while (!f ? a : b). Now it gives while (!(f ? a : b)). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/Util/CSharpUtil.cs | 4 ---- .../SelfVerifyingTests/VBToCS/BooleanTests.vb | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CodeConverter/Util/CSharpUtil.cs b/CodeConverter/Util/CSharpUtil.cs index 0adc77ba8..1dddd87bb 100644 --- a/CodeConverter/Util/CSharpUtil.cs +++ b/CodeConverter/Util/CSharpUtil.cs @@ -47,10 +47,6 @@ private static ExpressionSyntax InvertConditionInternal(ExpressionSyntax conditi return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, SyntaxFactory.ParenthesizedExpression(condition)); } - if (condition is ConditionalExpressionSyntax cEx) { - return cEx.WithCondition(InvertCondition(cEx.Condition, negateOrderingComparisons)); - } - if (condition is LiteralExpressionSyntax) { if (condition.Kind() == SyntaxKind.TrueLiteralExpression) return SyntaxFactory.LiteralExpression(SyntaxKind.FalseLiteralExpression); 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 From fe268ed4114630c4b7d5ab95c387b476d17da2d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 02:54:01 +0000 Subject: [PATCH 19/21] Keep only lifted ordering comparisons unflipped when inverting conditions InvertCondition now refuses to flip a <, <=, > or >= the nullable converter marked as a lifted comparison whose operand may be null, rather than Until loops rescanning the VB condition and refusing every ordering comparison in it. So Do Until x > y AndAlso i < 3 (x, y nullable) gives !(x > y) || i >= 3 instead of !(x > y) || !(i < 3). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- .../MethodBodyExecutableStatementVisitor.cs | 10 +-------- ...VisualBasicNullableExpressionsConverter.cs | 6 ++--- CodeConverter/Util/CSharpUtil.cs | 22 ++++++++++--------- .../StatementTests/LoopStatementTests.cs | 8 +++++-- .../VBToCS/NullableLoopConditionTests.vb | 14 ++++++++++++ 5 files changed, 36 insertions(+), 24 deletions(-) diff --git a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs index 0b956b0a6..34a0c64e8 100644 --- a/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs +++ b/CodeConverter/CSharp/MethodBodyExecutableStatementVisitor.cs @@ -1214,17 +1214,9 @@ public override async Task> VisitDoLoopBlock(VBSynta private async Task ConvertLoopContinueConditionAsync(VBSyntax.WhileOrUntilClauseSyntax clause) { var condition = await ConvertConditionAsync(clause.Condition); - return clause.WhileOrUntilKeyword.IsKind(VBasic.SyntaxKind.UntilKeyword) - ? condition.InvertCondition(negateOrderingComparisons: !ContainsNullableOrderingComparison(clause.Condition)) - : condition; + return clause.WhileOrUntilKeyword.IsKind(VBasic.SyntaxKind.UntilKeyword) ? condition.InvertCondition() : condition; } - private bool ContainsNullableOrderingComparison(VBSyntax.ExpressionSyntax vbCondition) => - vbCondition.DescendantNodesAndSelf().OfType().Any(comparison => - comparison.IsKind(VBasic.SyntaxKind.LessThanExpression, VBasic.SyntaxKind.LessThanOrEqualExpression, - VBasic.SyntaxKind.GreaterThanExpression, VBasic.SyntaxKind.GreaterThanOrEqualExpression) && - (_semanticModel.GetTypeInfo(comparison.Left).ConvertedType.IsNullable() || _semanticModel.GetTypeInfo(comparison.Right).ConvertedType.IsNullable())); - public override async Task> VisitCallStatement(VBSyntax.CallStatementSyntax node) { return SingleStatement(await node.Invocation.AcceptAsync(_expressionVisitor)); diff --git a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs index 8f454ee81..8095f1ad0 100644 --- a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs +++ b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs @@ -71,7 +71,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); @@ -237,11 +237,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, NegatedForParentNotAnnotation); + return negated.WithAdditionalAnnotations(IsNotNullableAnnotation, CSharpUtil.MayCompareNullAnnotation, NegatedForParentNotAnnotation); } ExpressionSyntax? lhsName = lhs, rhsName = rhs; diff --git a/CodeConverter/Util/CSharpUtil.cs b/CodeConverter/Util/CSharpUtil.cs index 1dddd87bb..fc996fe88 100644 --- a/CodeConverter/Util/CSharpUtil.cs +++ b/CodeConverter/Util/CSharpUtil.cs @@ -5,23 +5,25 @@ 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. /// /// The condition to invert. - /// - /// False where an operand of <, <=, > or >= may be nullable: a lifted comparison is false when an operand is null, - /// so the negated operator would be false too, rather than true. - /// - public static ExpressionSyntax InvertCondition(this ExpressionSyntax condition, bool negateOrderingComparisons = true) + public static ExpressionSyntax InvertCondition(this ExpressionSyntax condition) { - return InvertConditionInternal(condition, negateOrderingComparisons); + return InvertConditionInternal(condition); } - private static ExpressionSyntax InvertConditionInternal(ExpressionSyntax condition, bool negateOrderingComparisons) + private static ExpressionSyntax InvertConditionInternal(ExpressionSyntax condition) { if (condition is ParenthesizedExpressionSyntax) { - return SyntaxFactory.ParenthesizedExpression(InvertCondition(((ParenthesizedExpressionSyntax)condition).Expression, negateOrderingComparisons)); + return SyntaxFactory.ParenthesizedExpression(InvertCondition(((ParenthesizedExpressionSyntax)condition).Expression)); } if (condition is PrefixUnaryExpressionSyntax) { @@ -38,10 +40,10 @@ private static ExpressionSyntax InvertConditionInternal(ExpressionSyntax conditi var bOp = (BinaryExpressionSyntax)condition; if (bOp.IsKind(SyntaxKind.LogicalAndExpression) || bOp.IsKind(SyntaxKind.LogicalOrExpression)) - return SyntaxFactory.BinaryExpression(NegateConditionOperator(bOp.Kind()), InvertCondition(bOp.Left, negateOrderingComparisons), InvertCondition(bOp.Right, negateOrderingComparisons)); + return SyntaxFactory.BinaryExpression(NegateConditionOperator(bOp.Kind()), InvertCondition(bOp.Left), InvertCondition(bOp.Right)); if (bOp.IsKind(SyntaxKind.EqualsExpression) || bOp.IsKind(SyntaxKind.NotEqualsExpression) || - negateOrderingComparisons && IsOrderingComparison(bOp)) + IsOrderingComparison(bOp) && !bOp.HasAnnotation(MayCompareNullAnnotation)) return SyntaxFactory.BinaryExpression(NegateRelationalOperator(bOp.Kind()), bOp.Left, bOp.Right); return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.LogicalNotExpression, SyntaxFactory.ParenthesizedExpression(condition)); diff --git a/Tests/CSharp/StatementTests/LoopStatementTests.cs b/Tests/CSharp/StatementTests/LoopStatementTests.cs index 06d9924f4..06841ed62 100644 --- a/Tests/CSharp/StatementTests/LoopStatementTests.cs +++ b/Tests/CSharp/StatementTests/LoopStatementTests.cs @@ -72,7 +72,7 @@ public void TestMethod(DateTime? startDate, DateTime? endDate) public async Task NullableLoopConditionsTreatNothingAsFalseAsync() { await TestConversionVisualBasicToCSharpAsync(@"Class TestClass - Private Sub TestMethod(x As Integer?, y As Integer?, b As Boolean?) + 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 @@ -87,11 +87,12 @@ Private Sub TestMethod(x As Integer?, y As Integer?, b As Boolean?) 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) + private void TestMethod(int? x, int? y, bool? b, int i) { while (x.HasValue && y.HasValue && x.Value == y.Value) { @@ -139,6 +140,9 @@ private void TestMethod(int? x, int? y, bool? b) while (b != true) { } + while (!(x > y) || i >= 3) + { + } } }"); } diff --git a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb index b0652ae55..1e993dfa2 100644 --- a/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb +++ b/Tests/TestData/SelfVerifyingTests/VBToCS/NullableLoopConditionTests.vb @@ -292,6 +292,20 @@ Public Class NullableLoopConditionTests 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 ''' From cc1a3308c675f82b10e91575d5c405c340fa2885 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 02:54:29 +0000 Subject: [PATCH 20/21] Share a VB IsOrderingComparison extension Pure refactor. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/VbSyntaxNodeExtensions.cs | 4 ++++ .../CSharp/VisualBasicNullableExpressionsConverter.cs | 11 ++--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs b/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs index 9fd8d9273..8f67fbe8d 100644 --- a/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs +++ b/CodeConverter/CSharp/VbSyntaxNodeExtensions.cs @@ -20,6 +20,10 @@ 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. diff --git a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs index 8095f1ad0..544e2918e 100644 --- a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs +++ b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs @@ -291,9 +291,7 @@ private bool IsNegationOnlyTestedForTrue(VBSyntax.BinaryExpressionSyntax vbNode) /// 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.IsKind(VBasic.SyntaxKind.LessThanExpression, VBasic.SyntaxKind.LessThanOrEqualExpression, - VBasic.SyntaxKind.GreaterThanExpression, VBasic.SyntaxKind.GreaterThanOrEqualExpression) && - HasTotalOrder(_semanticModel.GetTypeInfo(vbNode.Left).ConvertedType) && HasTotalOrder(_semanticModel.GetTypeInfo(vbNode.Right).ConvertedType); + 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 && @@ -303,12 +301,7 @@ private static bool HasTotalOrder(ITypeSymbol? type) => /// 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) From 6bd27d9ab0219ef2e09af1ff7116b45f16b2a499 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 02:54:44 +0000 Subject: [PATCH 21/21] Rename CreateCastForContext to CreateCastOrConditionCast and explain NegatedForParentNotAnnotation Pure refactor. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019FEr8FVAmHV9bscZ82CChg --- CodeConverter/CSharp/TypeConversionAnalyzer.cs | 4 ++-- .../CSharp/VisualBasicNullableExpressionsConverter.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CodeConverter/CSharp/TypeConversionAnalyzer.cs b/CodeConverter/CSharp/TypeConversionAnalyzer.cs index 79ac714aa..b2a2362c0 100644 --- a/CodeConverter/CSharp/TypeConversionAnalyzer.cs +++ b/CodeConverter/CSharp/TypeConversionAnalyzer.cs @@ -108,7 +108,7 @@ private ExpressionSyntax AddTypeConversion(VBSyntax.ExpressionSyntax vbNode, Exp return addParenthesisIfNeeded ? vbNode.ParenthesizeIfPrecedenceCouldChange(csNode) : csNode; case TypeConversionKind.DestructiveCast: case TypeConversionKind.NonDestructiveCast: - return CreateCastForContext(vbNode, csNode, vbType, vbConvertedType); + return CreateCastOrConditionCast(vbNode, csNode, vbType, vbConvertedType); case TypeConversionKind.Conversion: return AddExplicitConvertTo(vbNode, csNode, vbType, vbConvertedType); case TypeConversionKind.NullableBool: @@ -441,7 +441,7 @@ private ExpressionSyntax AddRoundInvocation(ExpressionSyntax csNode) private static ExpressionSyntax GetUnderlyingValueToConvert(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNullableNode) => vbNode.AlwaysHasBooleanTypeInCSharp() ? csNullableNode.GetValueOrDefault() : csNullableNode.NullableGetValueExpression(); - private ExpressionSyntax CreateCastForContext(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, ITypeSymbol vbType, ITypeSymbol vbConvertedType) + private ExpressionSyntax CreateCastOrConditionCast(VBSyntax.ExpressionSyntax vbNode, ExpressionSyntax csNode, ITypeSymbol vbType, ITypeSymbol vbConvertedType) { if (!vbNode.AlwaysHasBooleanTypeInCSharp()) return CreateCast(csNode, vbConvertedType); if (GetNullableReturnTypeOfUserDefinedConversion(vbNode) is { } nullableBoolean) { diff --git a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs index 544e2918e..aa7feba4a 100644 --- a/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs +++ b/CodeConverter/CSharp/VisualBasicNullableExpressionsConverter.cs @@ -34,7 +34,8 @@ internal class VisualBasicNullableExpressionsConverter private static readonly SyntaxAnnotation IsNotNullableAnnotation = new("CodeConverter.Nullable", false.ToString()); /// - /// Marks a comparison already negated for its parent Not + /// 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");