Skip to content

Commit dcfb918

Browse files
committed
Fix #827: VB auto-property backing field access maps to MyClassProp for virtual properties
When a VB.NET virtual auto-property (e.g. `Overridable Property Prop As Integer = 5`) is accessed via its backing field (`_Prop`) in the same class, this should map to the non-virtual `MyClassProp` backing property in C#, not the virtual `Prop` property. Also handles the edge case where `MyClass._Prop` is written explicitly: in that case, `NameExpressionNodeVisitor` already adds the `MyClass` prefix, so we return the bare property name to avoid double-prefixing. Additionally, extends `GetMyClassAccessedNames` to detect backing field accesses so that the `MyClassProp` property is generated when needed. https://claude.ai/code/session_01AkwUvu3XuCdj3D4axoX4UX
1 parent 76b1e25 commit dcfb918

File tree

3 files changed

+66
-2
lines changed

3 files changed

+66
-2
lines changed

CodeConverter/CSharp/CommonConversions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,12 @@ 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;
296+
// For virtual auto-properties, VB backing field _Prop maps to the C# MyClassProp backing property (bypasses virtual dispatch).
297+
// Exception: when accessed as MyClass._Prop, NameExpressionNodeVisitor adds the "MyClass" prefix itself, so we just return the property name.
298+
var isAccessedViaMyClass = id.Parent?.Parent is VBSyntax.MemberAccessExpressionSyntax { Expression: VBSyntax.MyClassExpressionSyntax };
299+
text = !isAccessedViaMyClass && propertyFieldSymbol.IsImplicitlyDeclared && propertyFieldSymbol.AssociatedSymbol is IPropertySymbol { IsVirtual: true, IsAbstract: false } vProp
300+
? "MyClass" + vProp.Name
301+
: propertyFieldSymbol.AssociatedSymbol.Name;
297302
} else if (normalizedText.EndsWith("Event", StringComparison.OrdinalIgnoreCase) && idSymbol is IFieldSymbol eventFieldSymbol && eventFieldSymbol.AssociatedSymbol?.IsKind(SymbolKind.Event) == true) {
298303
text = eventFieldSymbol.AssociatedSymbol.Name;
299304
} else if (WinformsConversions.MayNeedToInlinePropertyAccess(id.Parent, idSymbol) && _typeContext.HandledEventsAnalysis.ShouldGeneratePropertyFor(idSymbol.Name)) {

CodeConverter/CSharp/DeclarationNodeVisitor.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,12 +689,25 @@ 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)
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);
698+
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);
708+
}
709+
}
710+
698711
return accessedTextNames;
699712
}
700713

Tests/CSharp/MemberTests/PropertyMemberTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,52 @@ 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 (_Prop) should map to MyClassProp for overridable properties</summary>
879+
[Fact]
880+
public async Task TestOverridableAutoPropertyBackingFieldAccessAsync()
881+
{
882+
await TestConversionVisualBasicToCSharpAsync(@"Class Foo
883+
Overridable Property Prop As Integer = 5
884+
885+
Sub Test()
886+
_Prop = 10
887+
Dim isCorrect = MyClass.Prop = 10
888+
End Sub
889+
End Class
890+
Class Child
891+
Inherits Foo
892+
Overrides Property Prop As Integer = 20
893+
End Class", @"
894+
internal partial class Foo
895+
{
896+
public int MyClassProp { get; set; } = 5;
897+
898+
public virtual int Prop
899+
{
900+
get
901+
{
902+
return MyClassProp;
903+
}
904+
905+
set
906+
{
907+
MyClassProp = value;
908+
}
909+
}
910+
911+
public void Test()
912+
{
913+
MyClassProp = 10;
914+
bool isCorrect = MyClassProp == 10;
915+
}
916+
}
917+
918+
internal partial class Child : Foo
919+
{
920+
public override int Prop { get; set; } = 20;
875921
}");
876922
}
877923
}

0 commit comments

Comments
 (0)