diff --git a/CodeConverter/CSharp/BinaryExpressionConverter.cs b/CodeConverter/CSharp/BinaryExpressionConverter.cs index f02c085f..c9d8d76a 100644 --- a/CodeConverter/CSharp/BinaryExpressionConverter.cs +++ b/CodeConverter/CSharp/BinaryExpressionConverter.cs @@ -82,11 +82,25 @@ private async Task ConvertBinaryExpressionAsync(VBasic.Syntax. case VisualBasicEqualityComparison.RequiredType.StringOnly: if (lhsTypeInfo.ConvertedType?.SpecialType == SpecialType.System_String && rhsTypeInfo.ConvertedType?.SpecialType == SpecialType.System_String && - _visualBasicEqualityComparison.TryConvertToNullOrEmptyCheck(node, lhs, rhs, out CSharpSyntaxNode visitBinaryExpression)) { + _visualBasicEqualityComparison.TryConvertToNullOrEmptyCheck(node, lhs, rhs, lhsTypeInfo, rhsTypeInfo, out CSharpSyntaxNode visitBinaryExpression)) { return visitBinaryExpression; } - (lhs, rhs) = _visualBasicEqualityComparison.AdjustForVbStringComparison(node.Left, lhs, lhsTypeInfo, false, node.Right, rhs, rhsTypeInfo, false); - omitConversion = true; // Already handled within for the appropriate types (rhs can become int in comparison) + if (lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && rhsTypeInfo.Type?.SpecialType == SpecialType.System_Char) { + // Do nothing, char comparison + } else if (!_visualBasicEqualityComparison.OptionCompareTextCaseInsensitive && + ((lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && rhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Right)) || + (rhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && lhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Left)))) { + if (lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char) { + rhs = CS.SyntaxFactory.MemberAccessExpression(CS.SyntaxKind.SimpleMemberAccessExpression, CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.CharKeyword)), CS.SyntaxFactory.IdentifierName("MinValue")); + omitConversion = true; + } else { + lhs = CS.SyntaxFactory.MemberAccessExpression(CS.SyntaxKind.SimpleMemberAccessExpression, CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.CharKeyword)), CS.SyntaxFactory.IdentifierName("MinValue")); + omitConversion = true; + } + } else { + (lhs, rhs) = _visualBasicEqualityComparison.AdjustForVbStringComparison(node.Left, lhs, lhsTypeInfo, false, node.Right, rhs, rhsTypeInfo, false); + omitConversion = true; // Already handled within for the appropriate types (rhs can become int in comparison) + } break; case VisualBasicEqualityComparison.RequiredType.Object: return _visualBasicEqualityComparison.GetFullExpressionForVbObjectComparison(lhs, rhs, VisualBasicEqualityComparison.ComparisonKind.Equals, node.IsKind(VBasic.SyntaxKind.NotEqualsExpression)); diff --git a/CodeConverter/CSharp/BinaryExpressionConverter.cs.orig b/CodeConverter/CSharp/BinaryExpressionConverter.cs.orig new file mode 100644 index 00000000..2ab4613c --- /dev/null +++ b/CodeConverter/CSharp/BinaryExpressionConverter.cs.orig @@ -0,0 +1,127 @@ +using ICSharpCode.CodeConverter.Util.FromRoslyn; + +namespace ICSharpCode.CodeConverter.CSharp; + +internal class BinaryExpressionConverter +{ + private readonly SemanticModel _semanticModel; + private readonly IOperatorConverter _operatorConverter; + private readonly VisualBasicEqualityComparison _visualBasicEqualityComparison; + private readonly VisualBasicNullableExpressionsConverter _visualBasicNullableTypesConverter; + public CommonConversions CommonConversions { get; } + + public CommentConvertingVisitorWrapper TriviaConvertingExpressionVisitor { get; } + + public BinaryExpressionConverter(SemanticModel semanticModel, IOperatorConverter operatorConverter, VisualBasicEqualityComparison visualBasicEqualityComparison, + VisualBasicNullableExpressionsConverter visualBasicNullableTypesConverter, CommonConversions commonConversions) + { + CommonConversions = commonConversions; + _semanticModel = semanticModel; + _operatorConverter = operatorConverter; + _visualBasicEqualityComparison = visualBasicEqualityComparison; + _visualBasicNullableTypesConverter = visualBasicNullableTypesConverter; + TriviaConvertingExpressionVisitor = commonConversions.TriviaConvertingExpressionVisitor; + } + + public async Task ConvertBinaryExpressionAsync(VBSyntax.BinaryExpressionSyntax entryNode) + { + // Walk down the syntax tree for deeply nested binary expressions to avoid stack overflow + // e.g. 3 + 4 + 5 + ... + // Test "DeeplyNestedBinaryExpressionShouldNotStackOverflowAsync()" skipped because it's too slow + + CSSyntax.ExpressionSyntax csLhs = null; + int levelsToConvert = 0; + VBSyntax.BinaryExpressionSyntax currentNode = entryNode; + + // Walk down the nested levels to count them + for (var nextNode = entryNode; nextNode != null; currentNode = nextNode, nextNode = currentNode.Left as VBSyntax.BinaryExpressionSyntax, levelsToConvert++) { + // Don't go beyond a rewritten operator because that code has many paths that can call VisitBinaryExpression. Passing csLhs through all of that would harm the code quality more than it's worth to help that edge case. + if (await RewriteBinaryOperatorOrNullAsync(nextNode) is { } operatorNode) { + csLhs = operatorNode; + break; + } + } + + // Walk back up the same levels converting as we go. + for (; levelsToConvert > 0; currentNode = currentNode!.Parent as VBSyntax.BinaryExpressionSyntax, levelsToConvert--) { + csLhs = (CSSyntax.ExpressionSyntax)await ConvertBinaryExpressionAsync(currentNode, csLhs); + } + + return csLhs; + } + + private async Task ConvertBinaryExpressionAsync(VBasic.Syntax.BinaryExpressionSyntax node, CSSyntax.ExpressionSyntax lhs, CSSyntax.ExpressionSyntax rhs = null) + { + lhs ??= await node.Left.AcceptAsync(TriviaConvertingExpressionVisitor); + rhs ??= await node.Right.AcceptAsync(TriviaConvertingExpressionVisitor); + + var lhsTypeInfo = _semanticModel.GetTypeInfo(node.Left); + var rhsTypeInfo = _semanticModel.GetTypeInfo(node.Right); + + ITypeSymbol forceLhsTargetType = null; + bool omitRightConversion = false; + bool omitConversion = false; + if (lhsTypeInfo.Type != null && rhsTypeInfo.Type != null) + { + if (node.IsKind(VBasic.SyntaxKind.ConcatenateExpression) && + !lhsTypeInfo.Type.IsEnumType() && !rhsTypeInfo.Type.IsEnumType() && + !lhsTypeInfo.Type.IsDateType() && !rhsTypeInfo.Type.IsDateType()) + { + omitRightConversion = true; + omitConversion = lhsTypeInfo.Type.SpecialType == SpecialType.System_String || + rhsTypeInfo.Type.SpecialType == SpecialType.System_String; + if (lhsTypeInfo.ConvertedType.SpecialType != SpecialType.System_String) { + forceLhsTargetType = CommonConversions.KnownTypes.String; + } + } + } + + var objectEqualityType = _visualBasicEqualityComparison.GetObjectEqualityType(node, lhsTypeInfo, rhsTypeInfo); + + switch (objectEqualityType) { + case VisualBasicEqualityComparison.RequiredType.StringOnly: + if (lhsTypeInfo.ConvertedType?.SpecialType == SpecialType.System_String && + rhsTypeInfo.ConvertedType?.SpecialType == SpecialType.System_String && + _visualBasicEqualityComparison.TryConvertToNullOrEmptyCheck(node, lhs, rhs, lhsTypeInfo, rhsTypeInfo, out CSharpSyntaxNode visitBinaryExpression)) { + return visitBinaryExpression; + } + if (lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && rhsTypeInfo.Type?.SpecialType == SpecialType.System_Char) { + // Do nothing, char comparison + } else if ((lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && rhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Right)) || + (rhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && lhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Left))) { + if (lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char) { + rhs = CS.SyntaxFactory.MemberAccessExpression(CS.SyntaxKind.SimpleMemberAccessExpression, CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.CharKeyword)), CS.SyntaxFactory.IdentifierName("MinValue")); + omitConversion = true; + } else { + lhs = CS.SyntaxFactory.MemberAccessExpression(CS.SyntaxKind.SimpleMemberAccessExpression, CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.CharKeyword)), CS.SyntaxFactory.IdentifierName("MinValue")); + omitConversion = true; + } + } else { + (lhs, rhs) = _visualBasicEqualityComparison.AdjustForVbStringComparison(node.Left, lhs, lhsTypeInfo, false, node.Right, rhs, rhsTypeInfo, false); + omitConversion = true; // Already handled within for the appropriate types (rhs can become int in comparison) + } + break; + case VisualBasicEqualityComparison.RequiredType.Object: + return _visualBasicEqualityComparison.GetFullExpressionForVbObjectComparison(lhs, rhs, VisualBasicEqualityComparison.ComparisonKind.Equals, node.IsKind(VBasic.SyntaxKind.NotEqualsExpression)); + } + + var lhsTypeIgnoringNullable = lhsTypeInfo.Type.GetNullableUnderlyingType() ?? lhsTypeInfo.Type; + var rhsTypeIgnoringNullable = rhsTypeInfo.Type.GetNullableUnderlyingType() ?? rhsTypeInfo.Type; + omitConversion |= lhsTypeIgnoringNullable != null && rhsTypeIgnoringNullable != null && + lhsTypeIgnoringNullable.IsEnumType() && SymbolEqualityComparer.Default.Equals(lhsTypeIgnoringNullable, rhsTypeIgnoringNullable) + && !node.IsKind(VBasic.SyntaxKind.AddExpression, VBasic.SyntaxKind.SubtractExpression, VBasic.SyntaxKind.MultiplyExpression, VBasic.SyntaxKind.DivideExpression, VBasic.SyntaxKind.IntegerDivideExpression, VBasic.SyntaxKind.ModuloExpression) + && forceLhsTargetType == null; + lhs = omitConversion ? lhs : CommonConversions.TypeConversionAnalyzer.AddExplicitConversion(node.Left, lhs, forceTargetType: forceLhsTargetType); + rhs = omitConversion || omitRightConversion ? rhs : CommonConversions.TypeConversionAnalyzer.AddExplicitConversion(node.Right, rhs); + + var kind = VBasic.VisualBasicExtensions.Kind(node).ConvertToken(); + var op = CS.SyntaxFactory.Token(CSharpUtil.GetExpressionOperatorTokenKind(kind)); + + var csBinExp = CS.SyntaxFactory.BinaryExpression(kind, lhs, op, rhs); + var exp = _visualBasicNullableTypesConverter.WithBinaryExpressionLogicForNullableTypes(node, lhsTypeInfo, rhsTypeInfo, csBinExp, lhs, rhs); + return node.Parent.IsKind(VBasic.SyntaxKind.SimpleArgument) ? exp : exp.AddParens(); + } + + private async Task RewriteBinaryOperatorOrNullAsync(VBSyntax.BinaryExpressionSyntax node) => + await _operatorConverter.ConvertRewrittenBinaryOperatorOrNullAsync(node, TriviaConvertingExpressionVisitor.IsWithinQuery); +} \ No newline at end of file diff --git a/CodeConverter/CSharp/BinaryExpressionConverter.cs.rej b/CodeConverter/CSharp/BinaryExpressionConverter.cs.rej new file mode 100644 index 00000000..a1fb7e8a --- /dev/null +++ b/CodeConverter/CSharp/BinaryExpressionConverter.cs.rej @@ -0,0 +1,14 @@ +--- BinaryExpressionConverter.cs ++++ BinaryExpressionConverter.cs +@@ -87,8 +87,9 @@ + } + if (lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && rhsTypeInfo.Type?.SpecialType == SpecialType.System_Char) { + // Do nothing, char comparison +- } else if ((lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && rhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Right)) || +- (rhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && lhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Left))) { ++ } else if (!_visualBasicEqualityComparison.OptionCompareTextCaseInsensitive && ++ ((lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && rhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Right)) || ++ (rhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && lhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Left)))) { + if (lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char) { + rhs = CS.SyntaxFactory.MemberAccessExpression(CS.SyntaxKind.SimpleMemberAccessExpression, CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.CharKeyword)), CS.SyntaxFactory.IdentifierName("MinValue")); + omitConversion = true; diff --git a/CodeConverter/CSharp/VisualBasicEqualityComparison.cs b/CodeConverter/CSharp/VisualBasicEqualityComparison.cs index b05a643a..4b6d82b6 100644 --- a/CodeConverter/CSharp/VisualBasicEqualityComparison.cs +++ b/CodeConverter/CSharp/VisualBasicEqualityComparison.cs @@ -79,6 +79,7 @@ public RequiredType GetObjectEqualityType(params TypeInfo[] typeInfos) if (typeInfos.All( t => t.Type == null || t.Type.SpecialType == SpecialType.System_String || + t.Type.SpecialType == SpecialType.System_Char || t.Type.IsArrayOf(SpecialType.System_Char) ) ) { return RequiredType.StringOnly; } @@ -177,7 +178,7 @@ private static ObjectCreationExpressionSyntax NewStringFromArg(ExpressionSyntax } public bool TryConvertToNullOrEmptyCheck(VBSyntax.BinaryExpressionSyntax node, ExpressionSyntax lhs, - ExpressionSyntax rhs, out CSharpSyntaxNode? visitBinaryExpression) + ExpressionSyntax rhs, TypeInfo lhsTypeInfo, TypeInfo rhsTypeInfo, out CSharpSyntaxNode? visitBinaryExpression) { if (OptionCompareTextCaseInsensitive) { @@ -191,6 +192,12 @@ public bool TryConvertToNullOrEmptyCheck(VBSyntax.BinaryExpressionSyntax node, E if (lhsEmpty || rhsEmpty) { var arg = lhsEmpty ? rhs : lhs; + var argType = lhsEmpty ? rhsTypeInfo : lhsTypeInfo; + if (argType.Type?.SpecialType != SpecialType.System_String && argType.Type?.SpecialType != SpecialType.System_Object) { + visitBinaryExpression = null; + return false; + } + var nullOrEmpty = SyntaxFactory.InvocationExpression( SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.StringKeyword)), @@ -207,7 +214,7 @@ public bool TryConvertToNullOrEmptyCheck(VBSyntax.BinaryExpressionSyntax node, E return false; } - private bool IsNothingOrEmpty(VBSyntax.ExpressionSyntax expressionSyntax) + public bool IsNothingOrEmpty(VBSyntax.ExpressionSyntax expressionSyntax) { expressionSyntax = expressionSyntax.SkipIntoParens(); diff --git a/TestDotNet/TestComparison.vbproj b/TestDotNet/TestComparison.vbproj new file mode 100644 index 00000000..dd4b5686 --- /dev/null +++ b/TestDotNet/TestComparison.vbproj @@ -0,0 +1,6 @@ + + + Exe + net8.0 + + diff --git a/TestDotNet/TestComparisonCS.cs b/TestDotNet/TestComparisonCS.cs new file mode 100644 index 00000000..8203f208 --- /dev/null +++ b/TestDotNet/TestComparisonCS.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.VisualBasic.CompilerServices; +using System.Globalization; + +class Program +{ + static void Main() + { + char testChar = default; + Console.WriteLine(CultureInfo.CurrentCulture.CompareInfo.Compare(Conversions.ToString(testChar), "", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0); + Console.WriteLine(CultureInfo.CurrentCulture.CompareInfo.Compare(Conversions.ToString(testChar), "a", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0); + Console.WriteLine(CultureInfo.CurrentCulture.CompareInfo.Compare("a", Conversions.ToString(testChar), CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0); + Console.WriteLine(CultureInfo.CurrentCulture.CompareInfo.Compare("ab", Conversions.ToString(testChar), CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0); + + char c = 'a'; + Console.WriteLine(CultureInfo.CurrentCulture.CompareInfo.Compare(Conversions.ToString(c), "a", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0); + Console.WriteLine(CultureInfo.CurrentCulture.CompareInfo.Compare(Conversions.ToString(c), "ab", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0); + + string s = ""; + Console.WriteLine(CultureInfo.CurrentCulture.CompareInfo.Compare(Conversions.ToString(c), s, CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0); + Console.WriteLine(CultureInfo.CurrentCulture.CompareInfo.Compare(Conversions.ToString(testChar), s, CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0); + } +} diff --git a/TestDotNet/TestComparisonCS.csproj b/TestDotNet/TestComparisonCS.csproj new file mode 100644 index 00000000..dd4b5686 --- /dev/null +++ b/TestDotNet/TestComparisonCS.csproj @@ -0,0 +1,6 @@ + + + Exe + net8.0 + + diff --git a/TestDotNet/TestComparisonVB.vb b/TestDotNet/TestComparisonVB.vb new file mode 100644 index 00000000..22e8218d --- /dev/null +++ b/TestDotNet/TestComparisonVB.vb @@ -0,0 +1,20 @@ +Option Compare Text +Imports System + +Module Program + Sub Main() + Dim testChar As Char = Nothing + Console.WriteLine(testChar = "") + Console.WriteLine(testChar = "a") + Console.WriteLine("a" = testChar) + Console.WriteLine("ab" = testChar) + + Dim c As Char = "a"c + Console.WriteLine(c = "a") + Console.WriteLine(c = "ab") + + Dim s As String = "" + Console.WriteLine(c = s) + Console.WriteLine(testChar = s) + End Sub +End Module diff --git a/TestDotNet/TestComparisonVB.vbproj b/TestDotNet/TestComparisonVB.vbproj new file mode 100644 index 00000000..dd4b5686 --- /dev/null +++ b/TestDotNet/TestComparisonVB.vbproj @@ -0,0 +1,6 @@ + + + Exe + net8.0 + + diff --git a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs index 414ba0e5..72440abb 100644 --- a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs @@ -541,6 +541,27 @@ public void Foo() { string x = Conversions.ToString(DateTime.Parse(""2022-01-01"")) + "" 15:00""; } +}"); + } + + [Fact] + public async Task CharEqualityEmptyStringAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Class TestClass + Private Sub TestMethod() + Dim testChar As Char = Nothing + Dim testResult = testChar = """" + Dim testResult2 = """" = testChar + End Sub +End Class", @" +internal partial class TestClass +{ + private void TestMethod() + { + char testChar = default; + bool testResult = testChar == char.MinValue; + bool testResult2 = char.MinValue == testChar; + } }"); } } \ No newline at end of file diff --git a/patch_binary.patch b/patch_binary.patch new file mode 100644 index 00000000..bf04f0bf --- /dev/null +++ b/patch_binary.patch @@ -0,0 +1,14 @@ +--- CodeConverter/CSharp/BinaryExpressionConverter.cs ++++ CodeConverter/CSharp/BinaryExpressionConverter.cs +@@ -87,8 +87,9 @@ + } + if (lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && rhsTypeInfo.Type?.SpecialType == SpecialType.System_Char) { + // Do nothing, char comparison +- } else if ((lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && rhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Right)) || +- (rhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && lhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Left))) { ++ } else if (!_visualBasicEqualityComparison.OptionCompareTextCaseInsensitive && ++ ((lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && rhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Right)) || ++ (rhsTypeInfo.Type?.SpecialType == SpecialType.System_Char && lhsTypeInfo.Type?.SpecialType == SpecialType.System_String && _visualBasicEqualityComparison.IsNothingOrEmpty(node.Left)))) { + if (lhsTypeInfo.Type?.SpecialType == SpecialType.System_Char) { + rhs = CS.SyntaxFactory.MemberAccessExpression(CS.SyntaxKind.SimpleMemberAccessExpression, CS.SyntaxFactory.PredefinedType(CS.SyntaxFactory.Token(CS.SyntaxKind.CharKeyword)), CS.SyntaxFactory.IdentifierName("MinValue")); + omitConversion = true;