diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index f6fd6709..d2b940a8 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -1,8 +1,10 @@ +using System.Collections.Immutable; using System.Runtime.InteropServices; using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Simplification; using static Microsoft.CodeAnalysis.VisualBasic.VisualBasicExtensions; using ICSharpCode.CodeConverter.Util.FromRoslyn; using ISymbolExtensions = ICSharpCode.CodeConverter.Util.ISymbolExtensions; @@ -674,11 +676,75 @@ 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. + /// VisitParameter in ExpressionNodeVisitor sets the default to null for these cases; this method + /// prepends a null-coalescing assignment to restore the char default at runtime. + /// + private static (BaseMethodDeclarationSyntax MethodBlock, BlockSyntax ConvertedStatements) FixCharDefaultsForStringParams( + IMethodSymbol declaredSymbol, BaseMethodDeclarationSyntax methodBlock, BlockSyntax convertedStatements, SemanticModel semanticModel) + { + var prependedStatements = new List(); + var vbParams = declaredSymbol?.Parameters ?? ImmutableArray.Empty; + var csParams = methodBlock.ParameterList.Parameters; + + for (int i = 0; i < csParams.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 = csParams[i]; + // The default was set to null at point of creation in VisitParameter (ExpressionNodeVisitor). + // Reconstruct the char expression from VB syntax to avoid depending on the already-converted value. + var charExpr = BuildCharExpressionFromVbSyntax(defaultValueNode, semanticModel); + + // Build: paramName = paramName ?? charExpr.ToString(); + var paramId = ValidSyntaxFactory.IdentifierName(csParam.Identifier.ValueText); + var toStringCall = CS.SyntaxFactory.InvocationExpression( + CS.SyntaxFactory.MemberAccessExpression( + CS.SyntaxKind.SimpleMemberAccessExpression, + charExpr, + 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); + + return (methodBlock, convertedStatements.WithStatements(CS.SyntaxFactory.List(prependedStatements.Concat(convertedStatements.Statements)))); + } + + private static ExpressionSyntax BuildCharExpressionFromVbSyntax(VBSyntax.ExpressionSyntax defaultValueNode, SemanticModel semanticModel) + { + // For char literal expressions (e.g. "^"c), use the constant value directly + if (defaultValueNode.IsKind(VBasic.SyntaxKind.CharacterLiteralExpression)) { + var constant = semanticModel.GetConstantValue(defaultValueNode); + if (constant.HasValue && constant.Value is char c) { + return CS.SyntaxFactory.LiteralExpression(CS.SyntaxKind.CharacterLiteralExpression, CS.SyntaxFactory.Literal(c)); + } + } + // For named constant references (e.g. DlM or Module.DlM), build a member access expression. + // Strip VB's "Global." prefix (VB global namespace qualifier, has no C# identifier equivalent). + // Annotate for simplification so Roslyn reduces e.g. TestModule.DlM → DlM within TestModule. + var parts = defaultValueNode.ToString().Trim().Split('.'); + if (parts.Length > 1 && parts[0] == "Global") parts = parts.Skip(1).ToArray(); + return ValidSyntaxFactory.MemberAccess(parts).WithAdditionalAnnotations(Simplifier.Annotation); + } + 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/CodeConverter/CSharp/ExpressionNodeVisitor.cs b/CodeConverter/CSharp/ExpressionNodeVisitor.cs index e95dc0a6..c41ef557 100644 --- a/CodeConverter/CSharp/ExpressionNodeVisitor.cs +++ b/CodeConverter/CSharp/ExpressionNodeVisitor.cs @@ -476,8 +476,16 @@ public override async Task VisitParameter(VBSyntax.ParameterSy attributes.Insert(0, CS.SyntaxFactory.AttributeList(CS.SyntaxFactory.SeparatedList(optionalAttributes))); } else { - @default = CS.SyntaxFactory.EqualsValueClause( - await node.Default.Value.AcceptAsync(TriviaConvertingExpressionVisitor)); + var paramSymbol = _semanticModel.GetDeclaredSymbol(node) as IParameterSymbol; + if (paramSymbol?.Type?.SpecialType == SpecialType.System_String && + _semanticModel.GetTypeInfo(defaultValue).Type?.SpecialType == SpecialType.System_Char) { + // VB allows a Char constant as default for a String parameter; C# does not. + // Set null here; FixCharDefaultsForStringParams (DeclarationNodeVisitor) adds the null-coalesce assignment. + @default = CS.SyntaxFactory.EqualsValueClause(ValidSyntaxFactory.NullExpression); + } else { + @default = CS.SyntaxFactory.EqualsValueClause( + await node.Default.Value.AcceptAsync(TriviaConvertingExpressionVisitor)); + } } } diff --git a/Tests/CSharp/MemberTests/MemberTests.cs b/Tests/CSharp/MemberTests/MemberTests.cs index da604f2d..57dca815 100644 --- a/Tests/CSharp/MemberTests/MemberTests.cs +++ b/Tests/CSharp/MemberTests/MemberTests.cs @@ -1576,4 +1576,92 @@ 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; + } +}"); + } + + [Fact] + public async Task TestCharConstInSameClassDefaultValueForStringParameterAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Class TestClass + Friend Const Sep As Char = "",""c + + Friend Function Join(Optional s As String = Sep) As String + Return s + End Function +End Class", @" +internal partial class TestClass +{ + internal const char Sep = ','; + + internal string Join(string s = null) + { + s = s ?? Sep.ToString(); + return s; + } +}"); + } + + [Fact] + public async Task TestMultipleCharDefaultValuesForStringParametersAsync() + { + await TestConversionVisualBasicToCSharpAsync( + @"Class TestClass + Friend Function Format(Optional prefix As String = ""[""c, Optional suffix As String = ""]""c) As String + Return prefix & suffix + End Function +End Class", @" +internal partial class TestClass +{ + internal string Format(string prefix = null, string suffix = null) + { + prefix = prefix ?? '['.ToString(); + suffix = suffix ?? ']'.ToString(); + return prefix + suffix; + } +}"); + } } \ 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