Skip to content
66 changes: 66 additions & 0 deletions CodeConverter/CSharp/DeclarationNodeVisitor.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -674,11 +676,75 @@ public override async Task<CSharpSyntaxNode> 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);
}

/// <summary>
/// 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.
/// </summary>
private static (BaseMethodDeclarationSyntax MethodBlock, BlockSyntax ConvertedStatements) FixCharDefaultsForStringParams(
IMethodSymbol declaredSymbol, BaseMethodDeclarationSyntax methodBlock, BlockSyntax convertedStatements, SemanticModel semanticModel)
{
var prependedStatements = new List<StatementSyntax>();
var vbParams = declaredSymbol?.Parameters ?? ImmutableArray<IParameterSymbol>.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);
Expand Down
12 changes: 10 additions & 2 deletions CodeConverter/CSharp/ExpressionNodeVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,16 @@ public override async Task<CSharpSyntaxNode> VisitParameter(VBSyntax.ParameterSy
attributes.Insert(0,
CS.SyntaxFactory.AttributeList(CS.SyntaxFactory.SeparatedList(optionalAttributes)));
} else {
@default = CS.SyntaxFactory.EqualsValueClause(
await node.Default.Value.AcceptAsync<ExpressionSyntax>(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<ExpressionSyntax>(TriviaConvertingExpressionVisitor));
}
}
}

Expand Down
88 changes: 88 additions & 0 deletions Tests/CSharp/MemberTests/MemberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}");
}
}
1 change: 1 addition & 0 deletions Tests/CSharp/MemberTests/PropertyMemberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -874,4 +874,5 @@ public static IEnumerable<object[]> SomeObjects
}
}");
}

}