Skip to content
83 changes: 83 additions & 0 deletions CodeConverter/CSharp/DeclarationNodeVisitor.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.CSharp;
Expand Down Expand Up @@ -674,11 +675,93 @@ 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));
}
return VbNameExprToCsExpr(defaultValueNode);
}

private static ExpressionSyntax VbNameExprToCsExpr(VBSyntax.ExpressionSyntax vbExpr)
{
switch (vbExpr) {
case VBSyntax.IdentifierNameSyntax identifier:
return CS.SyntaxFactory.IdentifierName(identifier.Identifier.Text);
case VBSyntax.MemberAccessExpressionSyntax memberAccess:
// Skip VB's "Global." qualifier — it's VB's global namespace prefix, has no direct C# identifier equivalent
if (memberAccess.Expression.IsKind(VBasic.SyntaxKind.GlobalName))
return CS.SyntaxFactory.IdentifierName(memberAccess.Name.Identifier.Text);
return CS.SyntaxFactory.MemberAccessExpression(CS.SyntaxKind.SimpleMemberAccessExpression,
VbNameExprToCsExpr(memberAccess.Expression),
CS.SyntaxFactory.IdentifierName(memberAccess.Name.Identifier.Text));
case VBSyntax.QualifiedNameSyntax qualifiedName:
// Qualified names (e.g. Global.TestModule) appear in name/type context within default values
if (qualifiedName.Left.IsKind(VBasic.SyntaxKind.GlobalName))
return CS.SyntaxFactory.IdentifierName(qualifiedName.Right.Identifier.Text);
return CS.SyntaxFactory.MemberAccessExpression(CS.SyntaxKind.SimpleMemberAccessExpression,
VbNameExprToCsExpr(qualifiedName.Left),
CS.SyntaxFactory.IdentifierName(qualifiedName.Right.Identifier.Text));
default:
throw new NotSupportedException($"Unexpected VB expression in char default value: {vbExpr}");
}
}

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;
}
}");
}
}
Loading