diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index f6fd6709..7c3f83c4 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,64 @@ public override async Task VisitMethodBlock(VBSyntax.MethodBlo convertedStatements = convertedStatements.InsertNodesBefore(firstResumeLayout, _typeContext.HandledEventsAnalysis.GetInitializeComponentClassEventHandlers()); } + (methodBlock, convertedStatements) = FixCharDefaultsForStringParams(declaredSymbol, methodBlock, convertedStatements, _semanticModel); + 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, SemanticModel semanticModel) + { + 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) 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; + 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/MemberTests.cs b/Tests/CSharp/MemberTests/MemberTests.cs index da604f2d..a5b404b5 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 diff --git a/Tests/CSharp/MemberTests/PropertyMemberTests.cs b/Tests/CSharp/MemberTests/PropertyMemberTests.cs index 790859d7..7e7e3708 100644 --- a/Tests/CSharp/MemberTests/PropertyMemberTests.cs +++ b/Tests/CSharp/MemberTests/PropertyMemberTests.cs @@ -874,4 +874,5 @@ public static IEnumerable SomeObjects } }"); } + } \ No newline at end of file