From e38a778bfb128f25dd57b316f53e7856a97cd4f9 Mon Sep 17 00:00:00 2001 From: GrahamTheCoder <2490482+GrahamTheCoder@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:14:44 +0000 Subject: [PATCH 1/3] Fix VB -> C# char comparison with empty string Map VB.NET char comparison with an empty string to char.MinValue to match runtime behavior and prevent logic errors during compilation. Added a unit test. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .../CSharp/BinaryExpressionConverter.cs | 19 +- .../CSharp/VisualBasicEqualityComparison.cs | 11 +- Program.vb | 9 + TestDotNet/Program.vb | 12 + TestDotNet/TestDotNet.vbproj | 6 + TestEquality.csproj | 8 + .../ExpressionTests/StringExpressionTests.cs | 21 + .../StringExpressionTests.cs.orig | 546 ++++++++++++++++++ .../StringExpressionTests.cs.patch | 20 + .../StringExpressionTests.cs.rej | 20 + test_char.cs | 10 + 11 files changed, 677 insertions(+), 5 deletions(-) create mode 100644 Program.vb create mode 100644 TestDotNet/Program.vb create mode 100644 TestDotNet/TestDotNet.vbproj create mode 100644 TestEquality.csproj create mode 100644 Tests/CSharp/ExpressionTests/StringExpressionTests.cs.orig create mode 100644 Tests/CSharp/ExpressionTests/StringExpressionTests.cs.patch create mode 100644 Tests/CSharp/ExpressionTests/StringExpressionTests.cs.rej create mode 100644 test_char.cs diff --git a/CodeConverter/CSharp/BinaryExpressionConverter.cs b/CodeConverter/CSharp/BinaryExpressionConverter.cs index f02c085f6..4de57b1fb 100644 --- a/CodeConverter/CSharp/BinaryExpressionConverter.cs +++ b/CodeConverter/CSharp/BinaryExpressionConverter.cs @@ -82,11 +82,24 @@ 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 ((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/VisualBasicEqualityComparison.cs b/CodeConverter/CSharp/VisualBasicEqualityComparison.cs index b05a643a5..4b6d82b69 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/Program.vb b/Program.vb new file mode 100644 index 000000000..7f5437d93 --- /dev/null +++ b/Program.vb @@ -0,0 +1,9 @@ +Imports System + +Module Program + Sub Main() + Dim testChar As Char = Nothing + Dim testResult = testChar = "" + Console.WriteLine(testResult) + End Sub +End Module diff --git a/TestDotNet/Program.vb b/TestDotNet/Program.vb new file mode 100644 index 000000000..8d9d9f4e9 --- /dev/null +++ b/TestDotNet/Program.vb @@ -0,0 +1,12 @@ +Imports System + +Module Program + Sub Main() + Dim testChar As Char = Nothing + Dim testResult = testChar = "" + Console.WriteLine(testResult) + + Dim testResult2 = "" = testChar + Console.WriteLine(testResult2) + End Sub +End Module diff --git a/TestDotNet/TestDotNet.vbproj b/TestDotNet/TestDotNet.vbproj new file mode 100644 index 000000000..dd4b56868 --- /dev/null +++ b/TestDotNet/TestDotNet.vbproj @@ -0,0 +1,6 @@ + + + Exe + net8.0 + + diff --git a/TestEquality.csproj b/TestEquality.csproj new file mode 100644 index 000000000..41f1d5ad4 --- /dev/null +++ b/TestEquality.csproj @@ -0,0 +1,8 @@ + + + + Exe + net6.0 + + + diff --git a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs index 414ba0e54..72440abb8 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/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.orig b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.orig new file mode 100644 index 000000000..232e759c0 --- /dev/null +++ b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.orig @@ -0,0 +1,546 @@ +using System.Threading.Tasks; +using ICSharpCode.CodeConverter.Tests.TestRunners; +using Xunit; + +namespace ICSharpCode.CodeConverter.Tests.CSharp.ExpressionTests; + +public class StringExpressionTests : ConverterTestBase +{ + [Fact] + public async Task MultilineStringAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Class TestClass + Private Sub TestMethod() + Dim x = ""Hello\ All strings in VB are verbatim """" < that's just a single escaped quote +World!"" + Dim y = $""Hello\ All strings in VB are verbatim """" < that's just a single escaped quote +World!"" + End Sub +End Class", @" +internal partial class TestClass +{ + private void TestMethod() + { + string x = @""Hello\ All strings in VB are verbatim """" < that's just a single escaped quote +World!""; + string y = $@""Hello\ All strings in VB are verbatim """" < that's just a single escaped quote +World!""; + } +}"); + } + + [Fact] + public async Task QuoteCharacterAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Public Class C + Public Sub s + Dim x As String = Chr(34) + x = Chr(92) + End Sub +End Class", @" +public partial class C +{ + public void s() + { + string x = ""\""""; + x = @""\""; + } +}"); + } + + [Fact] + public async Task QuotesAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Class TestClass + Shared Function GetTextFeedInput(pStream As String, pTitle As String, pText As String) As String + Return ""{"" & AccessKey() & "",""""streamName"""": """""" & pStream & """""",""""point"""": ["" & GetTitleTextPair(pTitle, pText) & ""]}"" + End Function + + Shared Function AccessKey() As String + Return """"""accessKey"""": """"8iaiHNZpNbBkYHHGbMNiHhAp4uPPyQke"""""" + End Function + + Shared Function GetNameValuePair(pName As String, pValue As Integer) As String + Return (""{""""name"""": """""" & pName & """""", """"value"""": """""" & pValue & """"""}"") + End Function + + Shared Function GetNameValuePair(pName As String, pValue As String) As String + Return (""{""""name"""": """""" & pName & """""", """"value"""": """""" & pValue & """"""}"") + End Function + + Shared Function GetTitleTextPair(pName As String, pValue As String) As String + Return (""{""""title"""": """""" & pName & """""", """"msg"""": """""" & pValue & """"""}"") + End Function + Shared Function GetDeltaPoint(pDelta As Integer) As String + Return (""{""""delta"""": """""" & pDelta & """"""}"") + End Function +End Class", @" +internal partial class TestClass +{ + public static string GetTextFeedInput(string pStream, string pTitle, string pText) + { + return ""{"" + AccessKey() + "",\""streamName\"": \"""" + pStream + ""\"",\""point\"": ["" + GetTitleTextPair(pTitle, pText) + ""]}""; + } + + public static string AccessKey() + { + return ""\""accessKey\"": \""8iaiHNZpNbBkYHHGbMNiHhAp4uPPyQke\""""; + } + + public static string GetNameValuePair(string pName, int pValue) + { + return ""{\""name\"": \"""" + pName + ""\"", \""value\"": \"""" + pValue + ""\""}""; + } + + public static string GetNameValuePair(string pName, string pValue) + { + return ""{\""name\"": \"""" + pName + ""\"", \""value\"": \"""" + pValue + ""\""}""; + } + + public static string GetTitleTextPair(string pName, string pValue) + { + return ""{\""title\"": \"""" + pName + ""\"", \""msg\"": \"""" + pValue + ""\""}""; + } + public static string GetDeltaPoint(int pDelta) + { + return ""{\""delta\"": \"""" + pDelta + ""\""}""; + } +}"); + } + + [Fact] + public async Task StringCompareAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Public Class Class1 + Sub Foo() + Dim s1 As String = Nothing + Dim s2 As String = """" + If s1 <> s2 Then + Throw New Exception() + End If + If s1 = ""something"" Then + Throw New Exception() + End If + If ""something"" = s1 Then + Throw New Exception() + End If + If s1 = Nothing Then + ' + End If + If s1 = """" Then + ' + End If + End Sub +End Class", @"using System; + +public partial class Class1 +{ + public void Foo() + { + string s1 = null; + string s2 = """"; + if ((s1 ?? """") != (s2 ?? """")) + { + throw new Exception(); + } + if (s1 == ""something"") + { + throw new Exception(); + } + if (""something"" == s1) + { + throw new Exception(); + } + if (string.IsNullOrEmpty(s1)) + { + // + } + if (string.IsNullOrEmpty(s1)) + { + // + } + } +}"); + } + + [Fact] + public async Task StringCompareTextAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Option Compare Text +Public Class Class1 + Sub Foo() + Dim s1 As String = Nothing + Dim s2 As String = """" + If s1 <> s2 Then + Throw New Exception() + End If + If s1 = ""something"" Then + Throw New Exception() + End If + If ""something"" = s1 Then + Throw New Exception() + End If + If s1 = Nothing Then + ' + End If + If s1 = """" Then + ' + End If + End Sub +End Class", @"using System; +using System.Globalization; + +public partial class Class1 +{ + public void Foo() + { + string s1 = null; + string s2 = """"; + if (CultureInfo.CurrentCulture.CompareInfo.Compare(s1 ?? """", s2 ?? """", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) != 0) + { + throw new Exception(); + } + if (CultureInfo.CurrentCulture.CompareInfo.Compare(s1, ""something"", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0) + { + throw new Exception(); + } + if (CultureInfo.CurrentCulture.CompareInfo.Compare(""something"", s1, CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0) + { + throw new Exception(); + } + if (CultureInfo.CurrentCulture.CompareInfo.Compare(s1 ?? """", """", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0) + { + // + } + if (CultureInfo.CurrentCulture.CompareInfo.Compare(s1 ?? """", """", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0) + { + // + } + } +}"); + } + + [Fact] + public async Task StringCompareDefaultInstrAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Imports Microsoft.VisualBasic + +Class Issue655 + Dim s1 = InStr(1, ""obj"", ""object '"") + Dim s2 = InStrRev(1, ""obj"", ""object '"") + Dim s3 = Replace(1, ""obj"", ""object '"") + Dim s4 = Split(1, ""obj"", ""object '"") + Dim s5 = Filter(New String() { 1, 2}, ""obj"") + Dim s6 = StrComp(1, ""obj"") + Dim s7 = OtherFunction() + + Function OtherFunction(Optional c As CompareMethod = CompareMethod.Binary) As Boolean + Return c = CompareMethod.Binary + End Function +End Class", + @"using Microsoft.VisualBasic; // Install-Package Microsoft.VisualBasic +using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic + +internal partial class Issue655 +{ + private object s1 = Strings.InStr(1, ""obj"", ""object '""); + private object s2 = Strings.InStrRev(1.ToString(), ""obj"", Conversions.ToInteger(""object '"")); + private object s3 = Strings.Replace(1.ToString(), ""obj"", ""object '""); + private object s4 = Strings.Split(1.ToString(), ""obj"", Conversions.ToInteger(""object '"")); + private object s5 = Strings.Filter(new string[] { 1.ToString(), 2.ToString() }, ""obj""); + private object s6 = Strings.StrComp(1.ToString(), ""obj""); + private object s7; + + public Issue655() + { + s7 = OtherFunction(); + } + + public bool OtherFunction(CompareMethod c = CompareMethod.Binary) + { + return c == CompareMethod.Binary; + } +}"); + } + + [Fact] + public async Task StringCompareTextInstrAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Option Compare Text ' Comment omitted since line has no conversion +Imports Microsoft.VisualBasic + +Class Issue655 + Dim s1 = InStr(1, ""obj"", ""object '"") + Dim s2 = InStrRev(1, ""obj"", ""object '"") + Dim s3 = Replace(1, ""obj"", ""object '"") + Dim s4 = Split(1, ""obj"", ""object '"") + Dim s5 = Filter(New String() { 1, 2}, ""obj"") + Dim s6 = StrComp(1, ""obj"") + Dim s7 = OtherFunction() + + Function OtherFunction(Optional c As CompareMethod = CompareMethod.Binary) As Boolean + Return c = CompareMethod.Binary + End Function +End Class", + @"using Microsoft.VisualBasic; // Install-Package Microsoft.VisualBasic +using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic + +internal partial class Issue655 +{ + private object s1 = Strings.InStr(1, ""obj"", ""object '"", Compare: CompareMethod.Text); + private object s2 = Strings.InStrRev(1.ToString(), ""obj"", Conversions.ToInteger(""object '""), Compare: CompareMethod.Text); + private object s3 = Strings.Replace(1.ToString(), ""obj"", ""object '"", Compare: CompareMethod.Text); + private object s4 = Strings.Split(1.ToString(), ""obj"", Conversions.ToInteger(""object '""), Compare: CompareMethod.Text); + private object s5 = Strings.Filter(new string[] { 1.ToString(), 2.ToString() }, ""obj""); + private object s6 = Strings.StrComp(1.ToString(), ""obj"", Compare: CompareMethod.Text); + private object s7; + + public Issue655() + { + s7 = OtherFunction(); + } + + public bool OtherFunction(CompareMethod c = CompareMethod.Binary) + { + return c == CompareMethod.Binary; + } +}"); + } + + [Fact] + public async Task StringConcatPrecedenceAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Public Class Class1 + Sub Foo() + Dim x = ""x "" & 5 - 4 & "" y"" + End Sub +End Class", @" +public partial class Class1 +{ + public void Foo() + { + string x = ""x "" + (5 - 4) + "" y""; + } +}"); + } + + [Fact] + public async Task StringConcatenationAssignmentAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Class TestClass + Private Sub TestMethod() + Dim str = ""Hello, "" + str &= ""World"" + End Sub +End Class", @" +internal partial class TestClass +{ + private void TestMethod() + { + string str = ""Hello, ""; + str += ""World""; + } +}"); + } + + [Fact] + public async Task StringInterpolationWithConditionalOperatorAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Function GetString(yourBoolean as Boolean) As String + Return $""You {if (yourBoolean, ""do"", ""do not"")} have a true value"" +End Function", + @"public string GetString(bool yourBoolean) +{ + return $""You {(yourBoolean ? ""do"" : ""do not"")} have a true value""; +}"); + } + + [Fact] + public async Task StringInterpolationWithDoubleQuotesAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Imports System + +Namespace Global.InnerNamespace + Public Class Test + Public Function StringInter(t As String, dt As DateTime) As String + Dim a = $""pre{t} t"" + Dim b = $""pre{t} """" t"" + Dim c = $""pre{t} """"\ t"" + Dim d = $""pre{t & """"""""} """" t"" + Dim e = $""pre{t & """"""""} """"\ t"" + Dim f = $""pre{{escapedBraces}}{dt,4:hh}"" + Return a & b & c & d & e & f + End Function + End Class +End Namespace", + @"using System; + +namespace InnerNamespace +{ + public partial class Test + { + public string StringInter(string t, DateTime dt) + { + string a = $""pre{t} t""; + string b = $""pre{t} \"" t""; + string c = $@""pre{t} """"\ t""; + string d = $""pre{t + ""\""""} \"" t""; + string e = $@""pre{t + ""\""""} """"\ t""; + string f = $""pre{{escapedBraces}}{dt,4:hh}""; + return a + b + c + d + e + f; + } + } +}"); + } + + [Fact] + public async Task StringInterpolationWithDateFormatAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Imports System + +Namespace Global.InnerNamespace + Public Class Test + public function InterStringDateFormat(dt As DateTime) As String + Dim a As String = $""Soak: {dt: d\.h\:mm\:ss\.f}"" + return a + End function + End Class +End Namespace", + @"using System; + +namespace InnerNamespace +{ + public partial class Test + { + public string InterStringDateFormat(DateTime dt) + { + string a = $""Soak: {dt: d\\.h\\:mm\\:ss\\.f}""; + return a; + } + } +}"); + } + [Fact] + public async Task NoConversionRequiredWithinConcatenationAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Issue508 + Sub Foo() + Dim x = ""x"" & 4 & ""y"" + End Sub +End Class", + @" +public partial class Issue508 +{ + public void Foo() + { + string x = ""x"" + 4 + ""y""; + } +}"); + } + + [Fact] + public async Task EmptyStringCoalesceSkippedForLiteralComparisonAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class VisualBasicClass + + Sub Foo() + Dim x = """" + Dim y = x = ""something"" + End Sub + +End Class", + @" +public partial class VisualBasicClass +{ + + public void Foo() + { + string x = """"; + bool y = x == ""something""; + } + +}"); + } + + [Fact] + public async Task Issue396ComparisonOperatorForStringsAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Issue396ComparisonOperatorForStringsAsync + Private str = 1.ToString() + Private b = str > """" +End Class", + @"using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic + +public partial class Issue396ComparisonOperatorForStringsAsync +{ + private object str = 1.ToString(); + private object b; + + public Issue396ComparisonOperatorForStringsAsync() + { + b = Operators.ConditionalCompareObjectGreater(str, """", false); + } +}"); + } + + [Fact] + public async Task Issue590EnumConvertsToNumericStringAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class EnumTests + Private Enum RankEnum As SByte + First = 1 + Second = 2 + End Enum + + Public Sub TestEnumConcat() + Console.Write(RankEnum.First & RankEnum.Second) + End Sub +End Class", + @"using System; +using System.IO; + +public partial class EnumTests +{ + private enum RankEnum : sbyte + { + First = 1, + Second = 2 + } + + public void TestEnumConcat() + { + Console.Write(RankEnum.First + RankEnum.Second); + } +} +1 target compilation errors: +CS0019: Operator '+' cannot be applied to operands of type 'EnumTests.RankEnum' and 'EnumTests.RankEnum'"); + } + + [Fact] + public async Task Issue806DateTimeConvertsToStringWithinConcatenationAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Public Class Issue806 + Sub Foo() + Dim x = #2022-01-01# & "" 15:00"" + End Sub +End Class", + @"using System; +using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic + +public partial class Issue806 +{ + public void Foo() + { + string x = Conversions.ToString(DateTime.Parse(""2022-01-01"")) + "" 15:00""; + } +}"); + } +} \ No newline at end of file diff --git a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.patch b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.patch new file mode 100644 index 000000000..ac2ede9b0 --- /dev/null +++ b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.patch @@ -0,0 +1,20 @@ +--- Tests/CSharp/ExpressionTests/StringExpressionTests.cs ++++ Tests/CSharp/ExpressionTests/StringExpressionTests.cs +@@ -489,5 +489,17 @@ + b = Operators.ConditionalCompareObjectGreater(str, """", false); + } + }"); + } ++ ++ [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", @""); ++ } + } diff --git a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.rej b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.rej new file mode 100644 index 000000000..b9dd7f9e6 --- /dev/null +++ b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.rej @@ -0,0 +1,20 @@ +--- StringExpressionTests.cs ++++ StringExpressionTests.cs +@@ -489,5 +489,17 @@ + b = Operators.ConditionalCompareObjectGreater(str, """", false); + } + }"); + } ++ ++ [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", @""); ++ } + } diff --git a/test_char.cs b/test_char.cs new file mode 100644 index 000000000..36fd0a21d --- /dev/null +++ b/test_char.cs @@ -0,0 +1,10 @@ +using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Linq; + +class Test { + static void Main() { + Console.WriteLine("Hello"); + } +} From 947ab8c99a1812a3f53da0f024b33df7734989a3 Mon Sep 17 00:00:00 2001 From: GrahamTheCoder <2490482+GrahamTheCoder@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:27:38 +0000 Subject: [PATCH 2/3] Fix VB -> C# char comparison with empty string Map VB.NET char comparison with an empty string to char.MinValue to match runtime behavior and prevent logic errors during compilation. For `Option Compare Text`, map it appropriately using `Microsoft.VisualBasic.CompilerServices.Conversions.ToString` to prevent `char` being compared to an empty string. Added characterization tests. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .../CSharp/BinaryExpressionConverter.cs | 5 +- .../CSharp/BinaryExpressionConverter.cs.orig | 127 ++++ .../CSharp/BinaryExpressionConverter.cs.rej | 14 + Program.vb | 9 - TestDotNet/Program.vb | 12 - ...estDotNet.vbproj => TestComparison.vbproj} | 0 TestDotNet/TestComparisonCS.cs | 23 + .../TestComparisonCS.csproj | 4 +- TestDotNet/TestComparisonVB.vb | 20 + TestDotNet/TestComparisonVB.vbproj | 6 + .../StringExpressionTests.cs.orig | 546 ------------------ .../StringExpressionTests.cs.patch | 20 - .../StringExpressionTests.cs.rej | 20 - patch_binary.patch | 14 + test_char.cs | 10 - 15 files changed, 208 insertions(+), 622 deletions(-) create mode 100644 CodeConverter/CSharp/BinaryExpressionConverter.cs.orig create mode 100644 CodeConverter/CSharp/BinaryExpressionConverter.cs.rej delete mode 100644 Program.vb delete mode 100644 TestDotNet/Program.vb rename TestDotNet/{TestDotNet.vbproj => TestComparison.vbproj} (100%) create mode 100644 TestDotNet/TestComparisonCS.cs rename TestEquality.csproj => TestDotNet/TestComparisonCS.csproj (70%) create mode 100644 TestDotNet/TestComparisonVB.vb create mode 100644 TestDotNet/TestComparisonVB.vbproj delete mode 100644 Tests/CSharp/ExpressionTests/StringExpressionTests.cs.orig delete mode 100644 Tests/CSharp/ExpressionTests/StringExpressionTests.cs.patch delete mode 100644 Tests/CSharp/ExpressionTests/StringExpressionTests.cs.rej create mode 100644 patch_binary.patch delete mode 100644 test_char.cs diff --git a/CodeConverter/CSharp/BinaryExpressionConverter.cs b/CodeConverter/CSharp/BinaryExpressionConverter.cs index 4de57b1fb..c9d8d76ab 100644 --- a/CodeConverter/CSharp/BinaryExpressionConverter.cs +++ b/CodeConverter/CSharp/BinaryExpressionConverter.cs @@ -87,8 +87,9 @@ private async Task ConvertBinaryExpressionAsync(VBasic.Syntax. } 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/BinaryExpressionConverter.cs.orig b/CodeConverter/CSharp/BinaryExpressionConverter.cs.orig new file mode 100644 index 000000000..2ab4613c9 --- /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 000000000..a1fb7e8a5 --- /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/Program.vb b/Program.vb deleted file mode 100644 index 7f5437d93..000000000 --- a/Program.vb +++ /dev/null @@ -1,9 +0,0 @@ -Imports System - -Module Program - Sub Main() - Dim testChar As Char = Nothing - Dim testResult = testChar = "" - Console.WriteLine(testResult) - End Sub -End Module diff --git a/TestDotNet/Program.vb b/TestDotNet/Program.vb deleted file mode 100644 index 8d9d9f4e9..000000000 --- a/TestDotNet/Program.vb +++ /dev/null @@ -1,12 +0,0 @@ -Imports System - -Module Program - Sub Main() - Dim testChar As Char = Nothing - Dim testResult = testChar = "" - Console.WriteLine(testResult) - - Dim testResult2 = "" = testChar - Console.WriteLine(testResult2) - End Sub -End Module diff --git a/TestDotNet/TestDotNet.vbproj b/TestDotNet/TestComparison.vbproj similarity index 100% rename from TestDotNet/TestDotNet.vbproj rename to TestDotNet/TestComparison.vbproj diff --git a/TestDotNet/TestComparisonCS.cs b/TestDotNet/TestComparisonCS.cs new file mode 100644 index 000000000..8203f208c --- /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/TestEquality.csproj b/TestDotNet/TestComparisonCS.csproj similarity index 70% rename from TestEquality.csproj rename to TestDotNet/TestComparisonCS.csproj index 41f1d5ad4..dd4b56868 100644 --- a/TestEquality.csproj +++ b/TestDotNet/TestComparisonCS.csproj @@ -1,8 +1,6 @@ - Exe - net6.0 + net8.0 - diff --git a/TestDotNet/TestComparisonVB.vb b/TestDotNet/TestComparisonVB.vb new file mode 100644 index 000000000..22e8218dc --- /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 000000000..dd4b56868 --- /dev/null +++ b/TestDotNet/TestComparisonVB.vbproj @@ -0,0 +1,6 @@ + + + Exe + net8.0 + + diff --git a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.orig b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.orig deleted file mode 100644 index 232e759c0..000000000 --- a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.orig +++ /dev/null @@ -1,546 +0,0 @@ -using System.Threading.Tasks; -using ICSharpCode.CodeConverter.Tests.TestRunners; -using Xunit; - -namespace ICSharpCode.CodeConverter.Tests.CSharp.ExpressionTests; - -public class StringExpressionTests : ConverterTestBase -{ - [Fact] - public async Task MultilineStringAsync() - { - await TestConversionVisualBasicToCSharpAsync(@"Class TestClass - Private Sub TestMethod() - Dim x = ""Hello\ All strings in VB are verbatim """" < that's just a single escaped quote -World!"" - Dim y = $""Hello\ All strings in VB are verbatim """" < that's just a single escaped quote -World!"" - End Sub -End Class", @" -internal partial class TestClass -{ - private void TestMethod() - { - string x = @""Hello\ All strings in VB are verbatim """" < that's just a single escaped quote -World!""; - string y = $@""Hello\ All strings in VB are verbatim """" < that's just a single escaped quote -World!""; - } -}"); - } - - [Fact] - public async Task QuoteCharacterAsync() - { - await TestConversionVisualBasicToCSharpAsync(@"Public Class C - Public Sub s - Dim x As String = Chr(34) - x = Chr(92) - End Sub -End Class", @" -public partial class C -{ - public void s() - { - string x = ""\""""; - x = @""\""; - } -}"); - } - - [Fact] - public async Task QuotesAsync() - { - await TestConversionVisualBasicToCSharpAsync(@"Class TestClass - Shared Function GetTextFeedInput(pStream As String, pTitle As String, pText As String) As String - Return ""{"" & AccessKey() & "",""""streamName"""": """""" & pStream & """""",""""point"""": ["" & GetTitleTextPair(pTitle, pText) & ""]}"" - End Function - - Shared Function AccessKey() As String - Return """"""accessKey"""": """"8iaiHNZpNbBkYHHGbMNiHhAp4uPPyQke"""""" - End Function - - Shared Function GetNameValuePair(pName As String, pValue As Integer) As String - Return (""{""""name"""": """""" & pName & """""", """"value"""": """""" & pValue & """"""}"") - End Function - - Shared Function GetNameValuePair(pName As String, pValue As String) As String - Return (""{""""name"""": """""" & pName & """""", """"value"""": """""" & pValue & """"""}"") - End Function - - Shared Function GetTitleTextPair(pName As String, pValue As String) As String - Return (""{""""title"""": """""" & pName & """""", """"msg"""": """""" & pValue & """"""}"") - End Function - Shared Function GetDeltaPoint(pDelta As Integer) As String - Return (""{""""delta"""": """""" & pDelta & """"""}"") - End Function -End Class", @" -internal partial class TestClass -{ - public static string GetTextFeedInput(string pStream, string pTitle, string pText) - { - return ""{"" + AccessKey() + "",\""streamName\"": \"""" + pStream + ""\"",\""point\"": ["" + GetTitleTextPair(pTitle, pText) + ""]}""; - } - - public static string AccessKey() - { - return ""\""accessKey\"": \""8iaiHNZpNbBkYHHGbMNiHhAp4uPPyQke\""""; - } - - public static string GetNameValuePair(string pName, int pValue) - { - return ""{\""name\"": \"""" + pName + ""\"", \""value\"": \"""" + pValue + ""\""}""; - } - - public static string GetNameValuePair(string pName, string pValue) - { - return ""{\""name\"": \"""" + pName + ""\"", \""value\"": \"""" + pValue + ""\""}""; - } - - public static string GetTitleTextPair(string pName, string pValue) - { - return ""{\""title\"": \"""" + pName + ""\"", \""msg\"": \"""" + pValue + ""\""}""; - } - public static string GetDeltaPoint(int pDelta) - { - return ""{\""delta\"": \"""" + pDelta + ""\""}""; - } -}"); - } - - [Fact] - public async Task StringCompareAsync() - { - await TestConversionVisualBasicToCSharpAsync(@"Public Class Class1 - Sub Foo() - Dim s1 As String = Nothing - Dim s2 As String = """" - If s1 <> s2 Then - Throw New Exception() - End If - If s1 = ""something"" Then - Throw New Exception() - End If - If ""something"" = s1 Then - Throw New Exception() - End If - If s1 = Nothing Then - ' - End If - If s1 = """" Then - ' - End If - End Sub -End Class", @"using System; - -public partial class Class1 -{ - public void Foo() - { - string s1 = null; - string s2 = """"; - if ((s1 ?? """") != (s2 ?? """")) - { - throw new Exception(); - } - if (s1 == ""something"") - { - throw new Exception(); - } - if (""something"" == s1) - { - throw new Exception(); - } - if (string.IsNullOrEmpty(s1)) - { - // - } - if (string.IsNullOrEmpty(s1)) - { - // - } - } -}"); - } - - [Fact] - public async Task StringCompareTextAsync() - { - await TestConversionVisualBasicToCSharpAsync(@"Option Compare Text -Public Class Class1 - Sub Foo() - Dim s1 As String = Nothing - Dim s2 As String = """" - If s1 <> s2 Then - Throw New Exception() - End If - If s1 = ""something"" Then - Throw New Exception() - End If - If ""something"" = s1 Then - Throw New Exception() - End If - If s1 = Nothing Then - ' - End If - If s1 = """" Then - ' - End If - End Sub -End Class", @"using System; -using System.Globalization; - -public partial class Class1 -{ - public void Foo() - { - string s1 = null; - string s2 = """"; - if (CultureInfo.CurrentCulture.CompareInfo.Compare(s1 ?? """", s2 ?? """", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) != 0) - { - throw new Exception(); - } - if (CultureInfo.CurrentCulture.CompareInfo.Compare(s1, ""something"", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0) - { - throw new Exception(); - } - if (CultureInfo.CurrentCulture.CompareInfo.Compare(""something"", s1, CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0) - { - throw new Exception(); - } - if (CultureInfo.CurrentCulture.CompareInfo.Compare(s1 ?? """", """", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0) - { - // - } - if (CultureInfo.CurrentCulture.CompareInfo.Compare(s1 ?? """", """", CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth) == 0) - { - // - } - } -}"); - } - - [Fact] - public async Task StringCompareDefaultInstrAsync() - { - await TestConversionVisualBasicToCSharpAsync(@"Imports Microsoft.VisualBasic - -Class Issue655 - Dim s1 = InStr(1, ""obj"", ""object '"") - Dim s2 = InStrRev(1, ""obj"", ""object '"") - Dim s3 = Replace(1, ""obj"", ""object '"") - Dim s4 = Split(1, ""obj"", ""object '"") - Dim s5 = Filter(New String() { 1, 2}, ""obj"") - Dim s6 = StrComp(1, ""obj"") - Dim s7 = OtherFunction() - - Function OtherFunction(Optional c As CompareMethod = CompareMethod.Binary) As Boolean - Return c = CompareMethod.Binary - End Function -End Class", - @"using Microsoft.VisualBasic; // Install-Package Microsoft.VisualBasic -using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic - -internal partial class Issue655 -{ - private object s1 = Strings.InStr(1, ""obj"", ""object '""); - private object s2 = Strings.InStrRev(1.ToString(), ""obj"", Conversions.ToInteger(""object '"")); - private object s3 = Strings.Replace(1.ToString(), ""obj"", ""object '""); - private object s4 = Strings.Split(1.ToString(), ""obj"", Conversions.ToInteger(""object '"")); - private object s5 = Strings.Filter(new string[] { 1.ToString(), 2.ToString() }, ""obj""); - private object s6 = Strings.StrComp(1.ToString(), ""obj""); - private object s7; - - public Issue655() - { - s7 = OtherFunction(); - } - - public bool OtherFunction(CompareMethod c = CompareMethod.Binary) - { - return c == CompareMethod.Binary; - } -}"); - } - - [Fact] - public async Task StringCompareTextInstrAsync() - { - await TestConversionVisualBasicToCSharpAsync(@"Option Compare Text ' Comment omitted since line has no conversion -Imports Microsoft.VisualBasic - -Class Issue655 - Dim s1 = InStr(1, ""obj"", ""object '"") - Dim s2 = InStrRev(1, ""obj"", ""object '"") - Dim s3 = Replace(1, ""obj"", ""object '"") - Dim s4 = Split(1, ""obj"", ""object '"") - Dim s5 = Filter(New String() { 1, 2}, ""obj"") - Dim s6 = StrComp(1, ""obj"") - Dim s7 = OtherFunction() - - Function OtherFunction(Optional c As CompareMethod = CompareMethod.Binary) As Boolean - Return c = CompareMethod.Binary - End Function -End Class", - @"using Microsoft.VisualBasic; // Install-Package Microsoft.VisualBasic -using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic - -internal partial class Issue655 -{ - private object s1 = Strings.InStr(1, ""obj"", ""object '"", Compare: CompareMethod.Text); - private object s2 = Strings.InStrRev(1.ToString(), ""obj"", Conversions.ToInteger(""object '""), Compare: CompareMethod.Text); - private object s3 = Strings.Replace(1.ToString(), ""obj"", ""object '"", Compare: CompareMethod.Text); - private object s4 = Strings.Split(1.ToString(), ""obj"", Conversions.ToInteger(""object '""), Compare: CompareMethod.Text); - private object s5 = Strings.Filter(new string[] { 1.ToString(), 2.ToString() }, ""obj""); - private object s6 = Strings.StrComp(1.ToString(), ""obj"", Compare: CompareMethod.Text); - private object s7; - - public Issue655() - { - s7 = OtherFunction(); - } - - public bool OtherFunction(CompareMethod c = CompareMethod.Binary) - { - return c == CompareMethod.Binary; - } -}"); - } - - [Fact] - public async Task StringConcatPrecedenceAsync() - { - await TestConversionVisualBasicToCSharpAsync(@"Public Class Class1 - Sub Foo() - Dim x = ""x "" & 5 - 4 & "" y"" - End Sub -End Class", @" -public partial class Class1 -{ - public void Foo() - { - string x = ""x "" + (5 - 4) + "" y""; - } -}"); - } - - [Fact] - public async Task StringConcatenationAssignmentAsync() - { - await TestConversionVisualBasicToCSharpAsync(@"Class TestClass - Private Sub TestMethod() - Dim str = ""Hello, "" - str &= ""World"" - End Sub -End Class", @" -internal partial class TestClass -{ - private void TestMethod() - { - string str = ""Hello, ""; - str += ""World""; - } -}"); - } - - [Fact] - public async Task StringInterpolationWithConditionalOperatorAsync() - { - await TestConversionVisualBasicToCSharpAsync( - @"Public Function GetString(yourBoolean as Boolean) As String - Return $""You {if (yourBoolean, ""do"", ""do not"")} have a true value"" -End Function", - @"public string GetString(bool yourBoolean) -{ - return $""You {(yourBoolean ? ""do"" : ""do not"")} have a true value""; -}"); - } - - [Fact] - public async Task StringInterpolationWithDoubleQuotesAsync() - { - await TestConversionVisualBasicToCSharpAsync( - @"Imports System - -Namespace Global.InnerNamespace - Public Class Test - Public Function StringInter(t As String, dt As DateTime) As String - Dim a = $""pre{t} t"" - Dim b = $""pre{t} """" t"" - Dim c = $""pre{t} """"\ t"" - Dim d = $""pre{t & """"""""} """" t"" - Dim e = $""pre{t & """"""""} """"\ t"" - Dim f = $""pre{{escapedBraces}}{dt,4:hh}"" - Return a & b & c & d & e & f - End Function - End Class -End Namespace", - @"using System; - -namespace InnerNamespace -{ - public partial class Test - { - public string StringInter(string t, DateTime dt) - { - string a = $""pre{t} t""; - string b = $""pre{t} \"" t""; - string c = $@""pre{t} """"\ t""; - string d = $""pre{t + ""\""""} \"" t""; - string e = $@""pre{t + ""\""""} """"\ t""; - string f = $""pre{{escapedBraces}}{dt,4:hh}""; - return a + b + c + d + e + f; - } - } -}"); - } - - [Fact] - public async Task StringInterpolationWithDateFormatAsync() - { - await TestConversionVisualBasicToCSharpAsync( - @"Imports System - -Namespace Global.InnerNamespace - Public Class Test - public function InterStringDateFormat(dt As DateTime) As String - Dim a As String = $""Soak: {dt: d\.h\:mm\:ss\.f}"" - return a - End function - End Class -End Namespace", - @"using System; - -namespace InnerNamespace -{ - public partial class Test - { - public string InterStringDateFormat(DateTime dt) - { - string a = $""Soak: {dt: d\\.h\\:mm\\:ss\\.f}""; - return a; - } - } -}"); - } - [Fact] - public async Task NoConversionRequiredWithinConcatenationAsync() - { - await TestConversionVisualBasicToCSharpAsync( - @"Public Class Issue508 - Sub Foo() - Dim x = ""x"" & 4 & ""y"" - End Sub -End Class", - @" -public partial class Issue508 -{ - public void Foo() - { - string x = ""x"" + 4 + ""y""; - } -}"); - } - - [Fact] - public async Task EmptyStringCoalesceSkippedForLiteralComparisonAsync() - { - await TestConversionVisualBasicToCSharpAsync( - @"Public Class VisualBasicClass - - Sub Foo() - Dim x = """" - Dim y = x = ""something"" - End Sub - -End Class", - @" -public partial class VisualBasicClass -{ - - public void Foo() - { - string x = """"; - bool y = x == ""something""; - } - -}"); - } - - [Fact] - public async Task Issue396ComparisonOperatorForStringsAsync() - { - await TestConversionVisualBasicToCSharpAsync( - @"Public Class Issue396ComparisonOperatorForStringsAsync - Private str = 1.ToString() - Private b = str > """" -End Class", - @"using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic - -public partial class Issue396ComparisonOperatorForStringsAsync -{ - private object str = 1.ToString(); - private object b; - - public Issue396ComparisonOperatorForStringsAsync() - { - b = Operators.ConditionalCompareObjectGreater(str, """", false); - } -}"); - } - - [Fact] - public async Task Issue590EnumConvertsToNumericStringAsync() - { - await TestConversionVisualBasicToCSharpAsync( - @"Public Class EnumTests - Private Enum RankEnum As SByte - First = 1 - Second = 2 - End Enum - - Public Sub TestEnumConcat() - Console.Write(RankEnum.First & RankEnum.Second) - End Sub -End Class", - @"using System; -using System.IO; - -public partial class EnumTests -{ - private enum RankEnum : sbyte - { - First = 1, - Second = 2 - } - - public void TestEnumConcat() - { - Console.Write(RankEnum.First + RankEnum.Second); - } -} -1 target compilation errors: -CS0019: Operator '+' cannot be applied to operands of type 'EnumTests.RankEnum' and 'EnumTests.RankEnum'"); - } - - [Fact] - public async Task Issue806DateTimeConvertsToStringWithinConcatenationAsync() - { - await TestConversionVisualBasicToCSharpAsync( - @"Public Class Issue806 - Sub Foo() - Dim x = #2022-01-01# & "" 15:00"" - End Sub -End Class", - @"using System; -using Microsoft.VisualBasic.CompilerServices; // Install-Package Microsoft.VisualBasic - -public partial class Issue806 -{ - public void Foo() - { - string x = Conversions.ToString(DateTime.Parse(""2022-01-01"")) + "" 15:00""; - } -}"); - } -} \ No newline at end of file diff --git a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.patch b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.patch deleted file mode 100644 index ac2ede9b0..000000000 --- a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.patch +++ /dev/null @@ -1,20 +0,0 @@ ---- Tests/CSharp/ExpressionTests/StringExpressionTests.cs -+++ Tests/CSharp/ExpressionTests/StringExpressionTests.cs -@@ -489,5 +489,17 @@ - b = Operators.ConditionalCompareObjectGreater(str, """", false); - } - }"); - } -+ -+ [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", @""); -+ } - } diff --git a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.rej b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.rej deleted file mode 100644 index b9dd7f9e6..000000000 --- a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs.rej +++ /dev/null @@ -1,20 +0,0 @@ ---- StringExpressionTests.cs -+++ StringExpressionTests.cs -@@ -489,5 +489,17 @@ - b = Operators.ConditionalCompareObjectGreater(str, """", false); - } - }"); - } -+ -+ [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", @""); -+ } - } diff --git a/patch_binary.patch b/patch_binary.patch new file mode 100644 index 000000000..bf04f0bf0 --- /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; diff --git a/test_char.cs b/test_char.cs deleted file mode 100644 index 36fd0a21d..000000000 --- a/test_char.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using System.Linq; - -class Test { - static void Main() { - Console.WriteLine("Hello"); - } -} From fd91263d6791a6303b819ffa19e90ce1ad3bf6d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 21:49:13 +0000 Subject: [PATCH 3/3] Clean up stray files and expand test coverage for char/empty-string comparison fix Remove leftover patch artifacts (.orig, .rej, patch file) and temporary TestDotNet debugging files that were committed by mistake. Expand the regression test to also cover the not-equal (<>) operator case. https://claude.ai/code/session_01AkwUvu3XuCdj3D4axoX4UX --- .../CSharp/BinaryExpressionConverter.cs.orig | 127 ------------------ .../CSharp/BinaryExpressionConverter.cs.rej | 14 -- TestDotNet/TestComparison.vbproj | 6 - TestDotNet/TestComparisonCS.cs | 23 ---- TestDotNet/TestComparisonCS.csproj | 6 - TestDotNet/TestComparisonVB.vb | 20 --- TestDotNet/TestComparisonVB.vbproj | 6 - .../ExpressionTests/StringExpressionTests.cs | 2 + patch_binary.patch | 14 -- 9 files changed, 2 insertions(+), 216 deletions(-) delete mode 100644 CodeConverter/CSharp/BinaryExpressionConverter.cs.orig delete mode 100644 CodeConverter/CSharp/BinaryExpressionConverter.cs.rej delete mode 100644 TestDotNet/TestComparison.vbproj delete mode 100644 TestDotNet/TestComparisonCS.cs delete mode 100644 TestDotNet/TestComparisonCS.csproj delete mode 100644 TestDotNet/TestComparisonVB.vb delete mode 100644 TestDotNet/TestComparisonVB.vbproj delete mode 100644 patch_binary.patch diff --git a/CodeConverter/CSharp/BinaryExpressionConverter.cs.orig b/CodeConverter/CSharp/BinaryExpressionConverter.cs.orig deleted file mode 100644 index 2ab4613c9..000000000 --- a/CodeConverter/CSharp/BinaryExpressionConverter.cs.orig +++ /dev/null @@ -1,127 +0,0 @@ -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 deleted file mode 100644 index a1fb7e8a5..000000000 --- a/CodeConverter/CSharp/BinaryExpressionConverter.cs.rej +++ /dev/null @@ -1,14 +0,0 @@ ---- 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/TestDotNet/TestComparison.vbproj b/TestDotNet/TestComparison.vbproj deleted file mode 100644 index dd4b56868..000000000 --- a/TestDotNet/TestComparison.vbproj +++ /dev/null @@ -1,6 +0,0 @@ - - - Exe - net8.0 - - diff --git a/TestDotNet/TestComparisonCS.cs b/TestDotNet/TestComparisonCS.cs deleted file mode 100644 index 8203f208c..000000000 --- a/TestDotNet/TestComparisonCS.cs +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index dd4b56868..000000000 --- a/TestDotNet/TestComparisonCS.csproj +++ /dev/null @@ -1,6 +0,0 @@ - - - Exe - net8.0 - - diff --git a/TestDotNet/TestComparisonVB.vb b/TestDotNet/TestComparisonVB.vb deleted file mode 100644 index 22e8218dc..000000000 --- a/TestDotNet/TestComparisonVB.vb +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index dd4b56868..000000000 --- a/TestDotNet/TestComparisonVB.vbproj +++ /dev/null @@ -1,6 +0,0 @@ - - - Exe - net8.0 - - diff --git a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs index 72440abb8..59f09e7e6 100644 --- a/Tests/CSharp/ExpressionTests/StringExpressionTests.cs +++ b/Tests/CSharp/ExpressionTests/StringExpressionTests.cs @@ -552,6 +552,7 @@ Private Sub TestMethod() Dim testChar As Char = Nothing Dim testResult = testChar = """" Dim testResult2 = """" = testChar + Dim testResult3 = testChar <> """" End Sub End Class", @" internal partial class TestClass @@ -561,6 +562,7 @@ private void TestMethod() char testChar = default; bool testResult = testChar == char.MinValue; bool testResult2 = char.MinValue == testChar; + bool testResult3 = testChar != char.MinValue; } }"); } diff --git a/patch_binary.patch b/patch_binary.patch deleted file mode 100644 index bf04f0bf0..000000000 --- a/patch_binary.patch +++ /dev/null @@ -1,14 +0,0 @@ ---- 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;