diff --git a/CodeConverter/CSharp/CommonConversions.cs b/CodeConverter/CSharp/CommonConversions.cs index 8af2c712..3f37f198 100644 --- a/CodeConverter/CSharp/CommonConversions.cs +++ b/CodeConverter/CSharp/CommonConversions.cs @@ -293,7 +293,12 @@ public SyntaxToken ConvertIdentifier(SyntaxToken id, bool isAttribute = false, S // AND the first explicitly declared parameter is this symbol, we need to replace it with value. text = "value"; } else if (normalizedText.StartsWith("_", StringComparison.OrdinalIgnoreCase) && idSymbol is IFieldSymbol propertyFieldSymbol && propertyFieldSymbol.AssociatedSymbol?.IsKind(SymbolKind.Property) == true) { - text = propertyFieldSymbol.AssociatedSymbol.Name; + // For virtual auto-properties, VB backing field _Prop maps to the C# MyClassProp backing property (bypasses virtual dispatch). + // Exception: when accessed as MyClass._Prop, NameExpressionNodeVisitor adds the "MyClass" prefix itself, so we just return the property name. + var isAccessedViaMyClass = id.Parent?.Parent is VBSyntax.MemberAccessExpressionSyntax { Expression: VBSyntax.MyClassExpressionSyntax }; + text = !isAccessedViaMyClass && propertyFieldSymbol.IsImplicitlyDeclared && propertyFieldSymbol.AssociatedSymbol is IPropertySymbol { IsVirtual: true, IsAbstract: false } vProp + ? "MyClass" + vProp.Name + : propertyFieldSymbol.AssociatedSymbol.Name; } 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 f6fd6709..c7d3e72a 100644 --- a/CodeConverter/CSharp/DeclarationNodeVisitor.cs +++ b/CodeConverter/CSharp/DeclarationNodeVisitor.cs @@ -689,12 +689,25 @@ private static async Task ConvertStatementsAsync(SyntaxList (IEnumerable) await s.Accept(methodBodyVisitor))); } - private static HashSet GetMyClassAccessedNames(VBSyntax.ClassBlockSyntax classBlock) + private HashSet GetMyClassAccessedNames(VBSyntax.ClassBlockSyntax classBlock) { 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); + + // Also treat direct backing field access (_Prop) as MyClass access for virtual auto-properties. + // In VB, writing _Prop directly accesses the backing field, bypassing virtual dispatch - + // the same semantics as MyClass.Prop. In C#, these virtual properties get a MyClassProp + // backing property, so _Prop must map to MyClassProp. + var backingFieldIdentifiers = classBlock.DescendantNodes().OfType() + .Where(id => id.Identifier.ValueText.StartsWith("_", StringComparison.OrdinalIgnoreCase)); + foreach (var id in backingFieldIdentifiers) { + if (_semanticModel.GetSymbolInfo(id).Symbol is IFieldSymbol { IsImplicitlyDeclared: true, AssociatedSymbol: IPropertySymbol { IsVirtual: true, IsAbstract: false } associatedProp }) { + accessedTextNames.Add(associatedProp.Name); + } + } + return accessedTextNames; } diff --git a/Tests/CSharp/MemberTests/PropertyMemberTests.cs b/Tests/CSharp/MemberTests/PropertyMemberTests.cs index 790859d7..69a7de8a 100644 --- a/Tests/CSharp/MemberTests/PropertyMemberTests.cs +++ b/Tests/CSharp/MemberTests/PropertyMemberTests.cs @@ -872,6 +872,51 @@ public static IEnumerable SomeObjects yield return new object[3]; } } +}"); + } + + /// Issue #827: VB auto-property backing field access (_Prop) should map to MyClassProp for overridable properties + [Fact] + public async Task TestOverridableAutoPropertyBackingFieldAccessAsync() + { + await TestConversionVisualBasicToCSharpAsync(@"Class Foo + Overridable Property Prop As Integer = 5 + + Sub Test() + _Prop = 10 + Dim isCorrect = MyClass.Prop = 10 + 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; + bool isCorrect = MyClassProp == 10; + } +} +internal partial class Child : Foo +{ + public override int Prop { get; set; } = 20; }"); } } \ No newline at end of file diff --git a/Tests/TestRunners/TestFileRewriter.cs b/Tests/TestRunners/TestFileRewriter.cs index f05e8075..d92472dd 100644 --- a/Tests/TestRunners/TestFileRewriter.cs +++ b/Tests/TestRunners/TestFileRewriter.cs @@ -20,7 +20,7 @@ private static Dictionary GetTestFileContents() private static string GetTestSourceDirectoryPath() { string assemblyDir = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().Location).AbsolutePath); - string testSourceDirectoryPath = Path.Combine(assemblyDir, @"..\..\"); + string testSourceDirectoryPath = Path.Combine(assemblyDir, "..", "..", ".."); return testSourceDirectoryPath; }