Skip to content

Commit 3026e66

Browse files
committed
Fix #557: Char default value for String parameter generates null + null-coalescing assignment
When VB has Optional ByVal strParam As String = charConst, C# can't use a char literal as a string default. Replace the default with null and prepend strParam = strParam ?? charConst.ToString() in the method body. https://claude.ai/code/session_01AkwUvu3XuCdj3D4axoX4UX
1 parent 0e1ce42 commit 3026e66

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

CodeConverter/CSharp/DeclarationNodeVisitor.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Immutable;
12
using System.Runtime.InteropServices;
23
using Microsoft.CodeAnalysis.Classification;
34
using Microsoft.CodeAnalysis.CSharp;
@@ -674,11 +675,59 @@ public override async Task<CSharpSyntaxNode> VisitMethodBlock(VBSyntax.MethodBlo
674675
convertedStatements = convertedStatements.InsertNodesBefore(firstResumeLayout, _typeContext.HandledEventsAnalysis.GetInitializeComponentClassEventHandlers());
675676
}
676677

678+
(methodBlock, convertedStatements) = FixCharDefaultsForStringParams(declaredSymbol, methodBlock, convertedStatements);
679+
677680
var body = _accessorDeclarationNodeConverter.WithImplicitReturnStatements(node, convertedStatements, csReturnVariableOrNull);
678681

679682
return methodBlock.WithBody(body);
680683
}
681684

685+
/// <summary>
686+
/// In VB, a Char constant can be the default value of a String parameter. In C#, this is invalid.
687+
/// Fix: replace the default with null and prepend a null-coalescing assignment in the method body.
688+
/// </summary>
689+
private static (BaseMethodDeclarationSyntax MethodBlock, BlockSyntax ConvertedStatements) FixCharDefaultsForStringParams(
690+
IMethodSymbol declaredSymbol, BaseMethodDeclarationSyntax methodBlock, BlockSyntax convertedStatements)
691+
{
692+
var prependedStatements = new List<StatementSyntax>();
693+
var updatedParams = methodBlock.ParameterList.Parameters.ToList();
694+
var vbParams = declaredSymbol?.Parameters ?? ImmutableArray<IParameterSymbol>.Empty;
695+
696+
for (int i = 0; i < updatedParams.Count && i < vbParams.Length; i++) {
697+
var vbParam = vbParams[i];
698+
if (vbParam.Type.SpecialType != SpecialType.System_String
699+
|| !vbParam.HasExplicitDefaultValue
700+
|| vbParam.ExplicitDefaultValue is not char) continue;
701+
702+
var csParam = updatedParams[i];
703+
var defaultExpr = csParam.Default?.Value;
704+
if (defaultExpr is null) continue;
705+
706+
// Replace the default value with null
707+
updatedParams[i] = csParam.WithDefault(
708+
CS.SyntaxFactory.EqualsValueClause(ValidSyntaxFactory.NullExpression));
709+
710+
// Build: paramName = paramName ?? existingDefaultExpr.ToString();
711+
var paramId = ValidSyntaxFactory.IdentifierName(csParam.Identifier.ValueText);
712+
var toStringCall = CS.SyntaxFactory.InvocationExpression(
713+
CS.SyntaxFactory.MemberAccessExpression(
714+
CS.SyntaxKind.SimpleMemberAccessExpression,
715+
defaultExpr.WithoutTrivia(),
716+
CS.SyntaxFactory.IdentifierName("ToString")));
717+
var coalesce = CS.SyntaxFactory.BinaryExpression(CS.SyntaxKind.CoalesceExpression, paramId, toStringCall);
718+
var assignment = CS.SyntaxFactory.AssignmentExpression(CS.SyntaxKind.SimpleAssignmentExpression, paramId, coalesce);
719+
prependedStatements.Add(CS.SyntaxFactory.ExpressionStatement(assignment));
720+
}
721+
722+
if (prependedStatements.Count == 0) return (methodBlock, convertedStatements);
723+
724+
var newParamList = methodBlock.ParameterList.WithParameters(CS.SyntaxFactory.SeparatedList(updatedParams, methodBlock.ParameterList.Parameters.GetSeparators()));
725+
methodBlock = methodBlock.WithParameterList(newParamList);
726+
convertedStatements = convertedStatements.WithStatements(CS.SyntaxFactory.List(prependedStatements.Concat(convertedStatements.Statements)));
727+
728+
return (methodBlock, convertedStatements);
729+
}
730+
682731
private static bool IsThisResumeLayoutInvocation(StatementSyntax s)
683732
{
684733
return s is ExpressionStatementSyntax ess && ess.Expression is InvocationExpressionSyntax ies && ies.Expression.ToString().Equals("this.ResumeLayout", StringComparison.Ordinal);

Tests/CSharp/MemberTests/PropertyMemberTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,43 @@ public static IEnumerable<object[]> SomeObjects
872872
yield return new object[3];
873873
}
874874
}
875+
}");
876+
}
877+
878+
/// <summary>Issue #827: VB auto-property backing field access via _Prop should map to MyClassProp for overridable properties</summary>
879+
[Fact]
880+
public async Task TestOverridableAutoPropertyBackingFieldAccessAsync()
881+
{
882+
await TestConversionVisualBasicToCSharpAsync(
883+
@"Class Foo
884+
Overridable Property Prop As Integer = 5
885+
Sub Test()
886+
_Prop = 10
887+
Dim x = _Prop
888+
End Sub
889+
End Class", @"
890+
internal partial class Foo
891+
{
892+
public int MyClassProp { get; set; } = 5;
893+
894+
public virtual int Prop
895+
{
896+
get
897+
{
898+
return MyClassProp;
899+
}
900+
901+
set
902+
{
903+
MyClassProp = value;
904+
}
905+
}
906+
907+
public void Test()
908+
{
909+
MyClassProp = 10;
910+
int x = MyClassProp;
911+
}
875912
}");
876913
}
877914
}

0 commit comments

Comments
 (0)