Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 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,64 @@ 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.
/// Fix: replace the default with null and prepend a null-coalescing assignment in the method body.
/// </summary>
private static (BaseMethodDeclarationSyntax MethodBlock, BlockSyntax ConvertedStatements) FixCharDefaultsForStringParams(
IMethodSymbol declaredSymbol, BaseMethodDeclarationSyntax methodBlock, BlockSyntax convertedStatements, SemanticModel semanticModel)
{
var prependedStatements = new List<StatementSyntax>();
var updatedParams = methodBlock.ParameterList.Parameters.ToList();
var vbParams = declaredSymbol?.Parameters ?? ImmutableArray<IParameterSymbol>.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));
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern of mutating previously set values is not recommended. We should set it to be null at the point of creation, then separately here we should add the null coalesce and use comments to mention the other place. This is obviously also not ideal, but a conceptual dependency from a distance is generally better than a concrete one where someone else might intercept it in between in the same style otherwise.


// 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);
Expand Down
45 changes: 45 additions & 0 deletions Tests/CSharp/MemberTests/MemberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}");
}
}
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
}
}");
}

}
Loading