From 054d9b00dc72e7d76deb22892df66bb21d227f46 Mon Sep 17 00:00:00 2001 From: GrahamTheCoder <2490482+GrahamTheCoder@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:11:05 +0000 Subject: [PATCH] Fix property backing field conversion for virtual properties This commit fixes issue #827 where accessing the auto-generated backing field (e.g. `_Prop`) of an overridable (virtual) property in VB.NET was incorrectly converted to the virtual property access (`Prop`) in C#. It now correctly maps these accesses to the explicitly generated non-virtual backing property `MyClassProp` and ensures that the MyClassProp property is correctly generated when such an access occurs. A test case has been added to MemberTests.cs to verify the correct translation of such field access. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- CodeConverter/CSharp/CommonConversions.cs | 3 ++ .../CSharp/DeclarationNodeVisitor.cs | 15 +++++- Tests/VB/MemberTests.cs | 47 ++++++++++++++++++- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/CodeConverter/CSharp/CommonConversions.cs b/CodeConverter/CSharp/CommonConversions.cs index 6647937fe..ef50db057 100644 --- a/CodeConverter/CSharp/CommonConversions.cs +++ b/CodeConverter/CSharp/CommonConversions.cs @@ -294,6 +294,9 @@ public SyntaxToken ConvertIdentifier(SyntaxToken id, bool isAttribute = false, S text = "value"; } else if (normalizedText.StartsWith("_", StringComparison.OrdinalIgnoreCase) && idSymbol is IFieldSymbol propertyFieldSymbol && propertyFieldSymbol.AssociatedSymbol?.IsKind(SymbolKind.Property) == true) { text = propertyFieldSymbol.AssociatedSymbol.Name; + if (propertyFieldSymbol.AssociatedSymbol.IsVirtual && !propertyFieldSymbol.AssociatedSymbol.IsAbstract) { + text = "MyClass" + text; + } } else if (normalizedText.EndsWith("Event", StringComparison.OrdinalIgnoreCase) && idSymbol is IFieldSymbol eventFieldSymbol && eventFieldSymbol.AssociatedSymbol?.IsKind(SymbolKind.Event) == true) { text = eventFieldSymbol.AssociatedSymbol.Name; } else if (WinformsConversions.MayNeedToInlinePropertyAccess(id.Parent, idSymbol) && _typeContext.HandledEventsAnalysis.ShouldGeneratePropertyFor(idSymbol.Name)) { diff --git a/CodeConverter/CSharp/DeclarationNodeVisitor.cs b/CodeConverter/CSharp/DeclarationNodeVisitor.cs index f6fd6709e..54642321b 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -336,7 +336,7 @@ var dummyClass public override async Task VisitClassBlock(VBSyntax.ClassBlockSyntax node) { - _accessorDeclarationNodeConverter.AccessedThroughMyClass = GetMyClassAccessedNames(node); + _accessorDeclarationNodeConverter.AccessedThroughMyClass = GetMyClassAccessedNames(node, _semanticModel); var classStatement = node.ClassStatement; var attributes = await CommonConversions.ConvertAttributesAsync(classStatement.AttributeLists); var (parameters, constraints) = await SplitTypeParametersAsync(classStatement.TypeParameterList); @@ -689,12 +689,23 @@ private static async Task ConvertStatementsAsync(SyntaxList (IEnumerable) await s.Accept(methodBodyVisitor))); } - private static HashSet GetMyClassAccessedNames(VBSyntax.ClassBlockSyntax classBlock) + private static HashSet GetMyClassAccessedNames(VBSyntax.ClassBlockSyntax classBlock, SemanticModel semanticModel) { var memberAccesses = classBlock.DescendantNodes().OfType(); var accessedTextNames = new HashSet(memberAccesses .Where(mae => mae.Expression is VBSyntax.MyClassExpressionSyntax) .Select(mae => mae.Name.Identifier.Text), StringComparer.OrdinalIgnoreCase); + + var identifierNames = classBlock.DescendantNodes().OfType().Where(id => id.Identifier.Text.StartsWith("_", StringComparison.OrdinalIgnoreCase)); + foreach (var id in identifierNames) + { + var symbolInfo = semanticModel.GetSymbolInfo(id); + if (symbolInfo.Symbol is IFieldSymbol fieldSymbol && fieldSymbol.AssociatedSymbol != null && fieldSymbol.AssociatedSymbol.IsVirtual && !fieldSymbol.AssociatedSymbol.IsAbstract) + { + accessedTextNames.Add(fieldSymbol.AssociatedSymbol.Name); + } + } + return accessedTextNames; } diff --git a/Tests/VB/MemberTests.cs b/Tests/VB/MemberTests.cs index 03bd4dd0b..7aab41005 100644 --- a/Tests/VB/MemberTests.cs +++ b/Tests/VB/MemberTests.cs @@ -1495,4 +1495,49 @@ End Sub End Class"); } -} \ No newline at end of file + + [Fact] + public async Task TestMyClassPropertyAccess() + { + await TestConversionVisualBasicToCSharpAsync(@" +Class Foo + Overridable Property Prop As Integer = 5 + + Sub Test() + _Prop = 10 ' This should convert to MyClassProp = 10 not to Prop = 10 + Dim isCorrect = MyClass.Prop = 10 ' After conversion this will return 5instead of 10 because we wrote to Child.Prop + End Sub +End Class +Class Child + Inherits Foo + Overrides Property Prop As Integer = 20 +End Class", @" +internal partial class Foo +{ + public int MyClassProp { get; set; } = 5; + + public virtual int Prop + { + get + { + return MyClassProp; + } + + set + { + MyClassProp = value; + } + } + + public void Test() + { + MyClassProp = 10; // This should convert to MyClassProp = 10 not to Prop = 10 + bool isCorrect = MyClassProp == 10; // After conversion this will return 5instead of 10 because we wrote to Child.Prop + } +} +internal partial class Child : Foo +{ + public override int Prop { get; set; } = 20; +}"); + } +}