From 3026e66d8b46670d1ec2e5cc51658c5063fab44e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 21:52:12 +0000 Subject: [PATCH 1/4] Fix #557: Char default value for String parameter generates null + null-coalescing assignment When VB has Optional ByVal strParam As String = charConst, C# can't use a char literal as a string default. Replace the default with null and prepend strParam = strParam ?? charConst.ToString() in the method body. https://claude.ai/code/session_01AkwUvu3XuCdj3D4axoX4UX --- .../CSharp/DeclarationNodeVisitor.cs | 49 +++++++++++++++++++ .../CSharp/MemberTests/PropertyMemberTests.cs | 37 ++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index f6fd6709e..1d9e61ec1 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Runtime.InteropServices; using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.CSharp; @@ -674,11 +675,59 @@ public override async Task VisitMethodBlock(VBSyntax.MethodBlo convertedStatements = convertedStatements.InsertNodesBefore(firstResumeLayout, _typeContext.HandledEventsAnalysis.GetInitializeComponentClassEventHandlers()); } + (methodBlock, convertedStatements) = FixCharDefaultsForStringParams(declaredSymbol, methodBlock, convertedStatements); + var body = _accessorDeclarationNodeConverter.WithImplicitReturnStatements(node, convertedStatements, csReturnVariableOrNull); return methodBlock.WithBody(body); } + /// + /// In VB, a Char constant can be the default value of a String parameter. In C#, this is invalid. + /// Fix: replace the default with null and prepend a null-coalescing assignment in the method body. + /// + private static (BaseMethodDeclarationSyntax MethodBlock, BlockSyntax ConvertedStatements) FixCharDefaultsForStringParams( + IMethodSymbol declaredSymbol, BaseMethodDeclarationSyntax methodBlock, BlockSyntax convertedStatements) + { + var prependedStatements = new List(); + var updatedParams = methodBlock.ParameterList.Parameters.ToList(); + var vbParams = declaredSymbol?.Parameters ?? ImmutableArray.Empty; + + for (int i = 0; i < updatedParams.Count && i < vbParams.Length; i++) { + var vbParam = vbParams[i]; + if (vbParam.Type.SpecialType != SpecialType.System_String + || !vbParam.HasExplicitDefaultValue + || vbParam.ExplicitDefaultValue is not char) continue; + + var csParam = updatedParams[i]; + var defaultExpr = csParam.Default?.Value; + if (defaultExpr is null) continue; + + // Replace the default value with null + updatedParams[i] = csParam.WithDefault( + CS.SyntaxFactory.EqualsValueClause(ValidSyntaxFactory.NullExpression)); + + // Build: paramName = paramName ?? existingDefaultExpr.ToString(); + var paramId = ValidSyntaxFactory.IdentifierName(csParam.Identifier.ValueText); + var toStringCall = CS.SyntaxFactory.InvocationExpression( + CS.SyntaxFactory.MemberAccessExpression( + CS.SyntaxKind.SimpleMemberAccessExpression, + defaultExpr.WithoutTrivia(), + CS.SyntaxFactory.IdentifierName("ToString"))); + var coalesce = CS.SyntaxFactory.BinaryExpression(CS.SyntaxKind.CoalesceExpression, paramId, toStringCall); + var assignment = CS.SyntaxFactory.AssignmentExpression(CS.SyntaxKind.SimpleAssignmentExpression, paramId, coalesce); + prependedStatements.Add(CS.SyntaxFactory.ExpressionStatement(assignment)); + } + + if (prependedStatements.Count == 0) return (methodBlock, convertedStatements); + + var newParamList = methodBlock.ParameterList.WithParameters(CS.SyntaxFactory.SeparatedList(updatedParams, methodBlock.ParameterList.Parameters.GetSeparators())); + methodBlock = methodBlock.WithParameterList(newParamList); + convertedStatements = convertedStatements.WithStatements(CS.SyntaxFactory.List(prependedStatements.Concat(convertedStatements.Statements))); + + return (methodBlock, convertedStatements); + } + private static bool IsThisResumeLayoutInvocation(StatementSyntax s) { return s is ExpressionStatementSyntax ess && ess.Expression is InvocationExpressionSyntax ies && ies.Expression.ToString().Equals("this.ResumeLayout", StringComparison.Ordinal); diff --git a/Tests/CSharp/MemberTests/PropertyMemberTests.cs b/Tests/CSharp/MemberTests/PropertyMemberTests.cs index 790859d75..07e4b3340 100644 --- a/Tests/CSharp/MemberTests/PropertyMemberTests.cs +++ b/Tests/CSharp/MemberTests/PropertyMemberTests.cs @@ -872,6 +872,43 @@ public static IEnumerable SomeObjects yield return new object[3]; } } +}"); + } + + /// Issue #827: VB auto-property backing field access via _Prop should map to MyClassProp for overridable properties + [Fact] + public async Task TestOverridableAutoPropertyBackingFieldAccessAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Class Foo + Overridable Property Prop As Integer = 5 + Sub Test() + _Prop = 10 + Dim x = _Prop + End Sub +End Class", @" +internal partial class Foo +{ + public int MyClassProp { get; set; } = 5; + + public virtual int Prop + { + get + { + return MyClassProp; + } + + set + { + MyClassProp = value; + } + } + + public void Test() + { + MyClassProp = 10; + int x = MyClassProp; + } }"); } } \ No newline at end of file From c5ba8e6ef8462b3cee21659083f65089cb222ff7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 21:58:52 +0000 Subject: [PATCH 2/4] Add regression tests for issue #557: char default value for string parameter Two tests cover the fix for converting VB Optional string parameters with char default values: one using a const reference and one using an inline char literal. https://claude.ai/code/session_01AkwUvu3XuCdj3D4axoX4UX --- Tests/CSharp/MemberTests/MemberTests.cs | 45 +++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Tests/CSharp/MemberTests/MemberTests.cs b/Tests/CSharp/MemberTests/MemberTests.cs index da604f2dc..a5b404b5a 100644 --- a/Tests/CSharp/MemberTests/MemberTests.cs +++ b/Tests/CSharp/MemberTests/MemberTests.cs @@ -1576,4 +1576,49 @@ private void OptionalByRefWithDefault([Optional][DefaultParameterValue(""a"")] r CS7036: There is no argument given that corresponds to the required parameter 'str1' of 'MissingByRefArgumentWithNoExplicitDefaultValue.ByRefNoDefault(ref string)' "); } + + [Fact] + public async Task TestCharConstDefaultValueForStringParameterAsync() + { + // Issue #557: VB allows a Char constant as a default value for a String parameter, but C# does not. + // Replace the default with null and prepend a null-coalescing assignment in the method body. + await TestConversionVisualBasicToCSharpAsync( + @"Module TestModule + Friend Const DlM As Char = ""^""c + + Friend Function LeftSideOf(Optional ByVal strDlM As String = DlM) As String + Return strDlM + End Function +End Module", @" +internal static partial class TestModule +{ + internal const char DlM = '^'; + + internal static string LeftSideOf(string strDlM = null) + { + strDlM = strDlM ?? DlM.ToString(); + return strDlM; + } +}"); + } + + [Fact] + public async Task TestCharLiteralDefaultValueForStringParameterAsync() + { + // Issue #557: inline char literal as default value for a String parameter. + await TestConversionVisualBasicToCSharpAsync( + @"Class TestClass + Friend Function Foo(Optional s As String = ""^""c) As String + Return s + End Function +End Class", @" +internal partial class TestClass +{ + internal string Foo(string s = null) + { + s = s ?? '^'.ToString(); + return s; + } +}"); + } } \ No newline at end of file From 71042ab4cc8af1ecc737a6962f5ab545f8d0ea6b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Apr 2026 22:53:06 +0000 Subject: [PATCH 3/4] Remove cross-contaminated #827 test from PropertyMemberTests.cs The TestOverridableAutoPropertyBackingFieldAccessAsync test (issue #827) was mistakenly added to this branch, which only contains the fix for issue #557. The #827 fix code is in a separate PR (#1252) and is not present here, so the test would always fail. https://claude.ai/code/session_01AkwUvu3XuCdj3D4axoX4UX --- .../CSharp/MemberTests/PropertyMemberTests.cs | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/Tests/CSharp/MemberTests/PropertyMemberTests.cs b/Tests/CSharp/MemberTests/PropertyMemberTests.cs index 07e4b3340..7e7e37083 100644 --- a/Tests/CSharp/MemberTests/PropertyMemberTests.cs +++ b/Tests/CSharp/MemberTests/PropertyMemberTests.cs @@ -875,40 +875,4 @@ public static IEnumerable SomeObjects }"); } - /// Issue #827: VB auto-property backing field access via _Prop should map to MyClassProp for overridable properties - [Fact] - public async Task TestOverridableAutoPropertyBackingFieldAccessAsync() - { - await TestConversionVisualBasicToCSharpAsync( - @"Class Foo - Overridable Property Prop As Integer = 5 - Sub Test() - _Prop = 10 - Dim x = _Prop - End Sub -End Class", @" -internal partial class Foo -{ - public int MyClassProp { get; set; } = 5; - - public virtual int Prop - { - get - { - return MyClassProp; - } - - set - { - MyClassProp = value; - } - } - - public void Test() - { - MyClassProp = 10; - int x = MyClassProp; - } -}"); - } } \ No newline at end of file From 6b46add2f35139ebc51a548e0d83422d247ad765 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 08:38:13 +0000 Subject: [PATCH 4/4] Fix #557: use VB semantic model to detect char-typed default for String params ExplicitDefaultValue is normalized to the parameter's declared type, so checking `ExplicitDefaultValue is not char` was always true for String params. Instead, retrieve the VB syntax default expression and check its actual type via the semantic model. https://claude.ai/code/session_01AkwUvu3XuCdj3D4axoX4UX --- CodeConverter/CSharp/DeclarationNodeVisitor.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index 1d9e61ec1..7c3f83c45 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -675,7 +675,7 @@ public override async Task VisitMethodBlock(VBSyntax.MethodBlo convertedStatements = convertedStatements.InsertNodesBefore(firstResumeLayout, _typeContext.HandledEventsAnalysis.GetInitializeComponentClassEventHandlers()); } - (methodBlock, convertedStatements) = FixCharDefaultsForStringParams(declaredSymbol, methodBlock, convertedStatements); + (methodBlock, convertedStatements) = FixCharDefaultsForStringParams(declaredSymbol, methodBlock, convertedStatements, _semanticModel); var body = _accessorDeclarationNodeConverter.WithImplicitReturnStatements(node, convertedStatements, csReturnVariableOrNull); @@ -687,7 +687,7 @@ public override async Task VisitMethodBlock(VBSyntax.MethodBlo /// Fix: replace the default with null and prepend a null-coalescing assignment in the method body. /// private static (BaseMethodDeclarationSyntax MethodBlock, BlockSyntax ConvertedStatements) FixCharDefaultsForStringParams( - IMethodSymbol declaredSymbol, BaseMethodDeclarationSyntax methodBlock, BlockSyntax convertedStatements) + IMethodSymbol declaredSymbol, BaseMethodDeclarationSyntax methodBlock, BlockSyntax convertedStatements, SemanticModel semanticModel) { var prependedStatements = new List(); var updatedParams = methodBlock.ParameterList.Parameters.ToList(); @@ -696,8 +696,13 @@ private static (BaseMethodDeclarationSyntax MethodBlock, BlockSyntax ConvertedSt for (int i = 0; i < updatedParams.Count && i < vbParams.Length; i++) { var vbParam = vbParams[i]; if (vbParam.Type.SpecialType != SpecialType.System_String - || !vbParam.HasExplicitDefaultValue - || vbParam.ExplicitDefaultValue is not char) continue; + || !vbParam.HasExplicitDefaultValue) continue; + // ExplicitDefaultValue is normalized to the parameter's declared type (String), so we + // must inspect the VB syntax to detect when the original expression is Char-typed. + var vbSyntaxParam = vbParam.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() as VBSyntax.ParameterSyntax; + var defaultValueNode = vbSyntaxParam?.Default?.Value; + if (defaultValueNode == null) continue; + if (semanticModel.GetTypeInfo(defaultValueNode).Type?.SpecialType != SpecialType.System_Char) continue; var csParam = updatedParams[i]; var defaultExpr = csParam.Default?.Value;