Skip to content

Commit 9ed5ef6

Browse files
committed
Fix #827: VB backing field _Prop correctly maps to MyClassProp for virtual auto-properties
When VB accesses _Prop (the backing field of a virtual auto-property), the converter now emits MyClassProp instead of Prop, matching the C# backing property name and avoiding virtual dispatch to child class overrides. https://claude.ai/code/session_01AkwUvu3XuCdj3D4axoX4UX
1 parent 41ba01d commit 9ed5ef6

File tree

3 files changed

+59
-12
lines changed

3 files changed

+59
-12
lines changed

CodeConverter/CSharp/CommonConversions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,10 +293,10 @@ public SyntaxToken ConvertIdentifier(SyntaxToken id, bool isAttribute = false, S
293293
// AND the first explicitly declared parameter is this symbol, we need to replace it with value.
294294
text = "value";
295295
} else if (normalizedText.StartsWith("_", StringComparison.OrdinalIgnoreCase) && idSymbol is IFieldSymbol propertyFieldSymbol && propertyFieldSymbol.AssociatedSymbol?.IsKind(SymbolKind.Property) == true) {
296-
text = propertyFieldSymbol.AssociatedSymbol.Name;
297-
if (propertyFieldSymbol.AssociatedSymbol.IsVirtual && !propertyFieldSymbol.AssociatedSymbol.IsAbstract) {
298-
text = "MyClass" + text;
299-
}
296+
// For virtual auto-properties, VB backing field _Prop maps to the C# MyClassProp backing property
297+
text = propertyFieldSymbol.IsImplicitlyDeclared && propertyFieldSymbol.AssociatedSymbol is IPropertySymbol { IsVirtual: true, IsAbstract: false } vProp
298+
? "MyClass" + vProp.Name
299+
: propertyFieldSymbol.AssociatedSymbol.Name;
300300
} else if (normalizedText.EndsWith("Event", StringComparison.OrdinalIgnoreCase) && idSymbol is IFieldSymbol eventFieldSymbol && eventFieldSymbol.AssociatedSymbol?.IsKind(SymbolKind.Event) == true) {
301301
text = eventFieldSymbol.AssociatedSymbol.Name;
302302
} else if (WinformsConversions.MayNeedToInlinePropertyAccess(id.Parent, idSymbol) && _typeContext.HandledEventsAnalysis.ShouldGeneratePropertyFor(idSymbol.Name)) {

CodeConverter/CSharp/DeclarationNodeVisitor.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -689,20 +689,22 @@ private static async Task<BlockSyntax> ConvertStatementsAsync(SyntaxList<VBSynta
689689
return CS.SyntaxFactory.Block(await statements.SelectManyAsync(async s => (IEnumerable<StatementSyntax>) await s.Accept(methodBodyVisitor)));
690690
}
691691

692-
private static HashSet<string> GetMyClassAccessedNames(VBSyntax.ClassBlockSyntax classBlock, SemanticModel semanticModel)
692+
private HashSet<string> GetMyClassAccessedNames(VBSyntax.ClassBlockSyntax classBlock)
693693
{
694694
var memberAccesses = classBlock.DescendantNodes().OfType<VBSyntax.MemberAccessExpressionSyntax>();
695695
var accessedTextNames = new HashSet<string>(memberAccesses
696696
.Where(mae => mae.Expression is VBSyntax.MyClassExpressionSyntax)
697697
.Select(mae => mae.Name.Identifier.Text), StringComparer.OrdinalIgnoreCase);
698698

699-
var identifierNames = classBlock.DescendantNodes().OfType<VBSyntax.IdentifierNameSyntax>().Where(id => id.Identifier.Text.StartsWith("_", StringComparison.OrdinalIgnoreCase));
700-
foreach (var id in identifierNames)
701-
{
702-
var symbolInfo = semanticModel.GetSymbolInfo(id);
703-
if (symbolInfo.Symbol is IFieldSymbol fieldSymbol && fieldSymbol.AssociatedSymbol != null && fieldSymbol.AssociatedSymbol.IsVirtual && !fieldSymbol.AssociatedSymbol.IsAbstract && !id.Identifier.Text.StartsWith("MyClass", StringComparison.OrdinalIgnoreCase))
704-
{
705-
accessedTextNames.Add(fieldSymbol.AssociatedSymbol.Name);
699+
// Also treat direct backing field access (_Prop) as MyClass access for virtual auto-properties.
700+
// In VB, writing _Prop directly accesses the backing field, bypassing virtual dispatch -
701+
// the same semantics as MyClass.Prop. In C#, these virtual properties get a MyClassProp
702+
// backing property, so _Prop must map to MyClassProp.
703+
var backingFieldIdentifiers = classBlock.DescendantNodes().OfType<VBSyntax.IdentifierNameSyntax>()
704+
.Where(id => id.Identifier.ValueText.StartsWith("_", StringComparison.OrdinalIgnoreCase));
705+
foreach (var id in backingFieldIdentifiers) {
706+
if (_semanticModel.GetSymbolInfo(id).Symbol is IFieldSymbol { IsImplicitlyDeclared: true, AssociatedSymbol: IPropertySymbol { IsVirtual: true, IsAbstract: false } associatedProp }) {
707+
accessedTextNames.Add(associatedProp.Name);
706708
}
707709
}
708710

Tests/CSharp/MemberTests/MemberTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1576,4 +1576,49 @@ private void OptionalByRefWithDefault([Optional][DefaultParameterValue(""a"")] r
15761576
CS7036: There is no argument given that corresponds to the required parameter 'str1' of 'MissingByRefArgumentWithNoExplicitDefaultValue.ByRefNoDefault(ref string)'
15771577
");
15781578
}
1579+
1580+
[Fact]
1581+
public async Task TestCharDefaultValueForStringParameterAsync()
1582+
{
1583+
// Issue #557: VB allows Char as a default value for a String parameter, but C# does not.
1584+
// The fix replaces the default with null and prepends a null-coalescing assignment in the body.
1585+
await TestConversionVisualBasicToCSharpAsync(
1586+
@"Module TestModule
1587+
Friend Const DlM As Char = ""^""c
1588+
1589+
Friend Function LeftSideOf(Optional ByVal strDlM As String = DlM) As String
1590+
Return strDlM
1591+
End Function
1592+
End Module", @"
1593+
internal static partial class TestModule
1594+
{
1595+
internal const char DlM = '^';
1596+
1597+
internal static string LeftSideOf(string strDlM = null)
1598+
{
1599+
strDlM = strDlM ?? DlM.ToString();
1600+
return strDlM;
1601+
}
1602+
}");
1603+
}
1604+
1605+
[Fact]
1606+
public async Task TestCharLiteralDefaultValueForStringParameterAsync()
1607+
{
1608+
// Issue #557: inline char literal as default value for a String parameter.
1609+
await TestConversionVisualBasicToCSharpAsync(
1610+
@"Class TestClass
1611+
Friend Function Foo(Optional s As String = ""^""c) As String
1612+
Return s
1613+
End Function
1614+
End Class", @"
1615+
internal partial class TestClass
1616+
{
1617+
internal string Foo(string s = null)
1618+
{
1619+
s = s ?? '^'.ToString();
1620+
return s;
1621+
}
1622+
}");
1623+
}
15791624
}

0 commit comments

Comments
 (0)