From ae6e9cd93b23dbc610c7e985763bef5fdc513860 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 31 Jan 2025 20:15:11 -0800 Subject: [PATCH 1/2] Keep initializer when converting properties --- ...tyToFullPropertyCodeRefactoringProvider.cs | 6 ++-- .../ConvertAutoPropertyToFullPropertyTests.cs | 30 +++++++++++++++++++ ...tyToFullPropertyCodeRefactoringProvider.cs | 8 ++--- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs index c238e4b2dd533..6472d3dac8a02 100644 --- a/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs @@ -168,8 +168,10 @@ protected override async Task ExpandToFieldPropertyAsync( // Update the getter/setter to reference the 'field' expression instead. var (newGetAccessor, newSetAccessor) = GetNewAccessors(info, property, FieldExpression(), cancellationToken); - var finalProperty = CreateFinalProperty(document, property, info, newGetAccessor, newSetAccessor); - var finalRoot = root.ReplaceNode(property, finalProperty); + // The normal helper will strip off the semicolon (as we're normally moving the initializer to a field). + // Don't do that here as we will keep the current initializer on the property if it is there. + var finalProperty = (PropertyDeclarationSyntax)CreateFinalProperty(document, property, info, newGetAccessor, newSetAccessor); + var finalRoot = root.ReplaceNode(property, finalProperty.WithSemicolonToken(property.SemicolonToken)); return document.WithSyntaxRoot(finalRoot); } diff --git a/src/Features/CSharpTest/ConvertAutoPropertyToFullProperty/ConvertAutoPropertyToFullPropertyTests.cs b/src/Features/CSharpTest/ConvertAutoPropertyToFullProperty/ConvertAutoPropertyToFullPropertyTests.cs index 111cea0267bfa..1884ab2379f76 100644 --- a/src/Features/CSharpTest/ConvertAutoPropertyToFullProperty/ConvertAutoPropertyToFullPropertyTests.cs +++ b/src/Features/CSharpTest/ConvertAutoPropertyToFullProperty/ConvertAutoPropertyToFullPropertyTests.cs @@ -1444,4 +1444,34 @@ public int Goo """; await TestInRegularAndScriptAsync(text, expected, options: DoNotPreferExpressionBodiedAccessors, index: 1, parseOptions: CSharp14); } + + [Theory] + [InlineData("set"), InlineData("init")] + [WorkItem("https://github.com/dotnet/roslyn/issues/76992")] + public async Task ProduceFieldBackedProperty2(string setter) + { + var text = $$""" + class TestClass + { + public int G[||]oo { get; {{setter}}; } = 0; + } + """; + var expected = $$""" + class TestClass + { + public int Goo + { + get + { + return field; + } + {{setter}} + { + field = value; + } + } = 0; + } + """; + await TestInRegularAndScriptAsync(text, expected, options: DoNotPreferExpressionBodiedAccessors, index: 1, parseOptions: CSharp14); + } } diff --git a/src/Features/Core/Portable/ConvertAutoPropertyToFullProperty/AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs b/src/Features/Core/Portable/ConvertAutoPropertyToFullProperty/AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs index 9fdb11ae24361..1275577e9282f 100644 --- a/src/Features/Core/Portable/ConvertAutoPropertyToFullProperty/AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ConvertAutoPropertyToFullProperty/AbstractConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs @@ -99,9 +99,9 @@ private async Task ExpandToFullPropertyAsync( var fieldName = await GetFieldNameAsync(document, propertySymbol, cancellationToken).ConfigureAwait(false); var (newGetAccessor, newSetAccessor) = GetNewAccessors(info, property, fieldName, cancellationToken); - editor.ReplaceNode( - property, - CreateFinalProperty(document, property, info, newGetAccessor, newSetAccessor)); + var finalProperty = CreateFinalProperty( + document, GetPropertyWithoutInitializer(property), info, newGetAccessor, newSetAccessor); + editor.ReplaceNode(property, finalProperty); // add backing field, plus initializer if it exists var newField = CodeGenerationSymbolFactory.CreateFieldSymbol( @@ -137,7 +137,7 @@ protected SyntaxNode CreateFinalProperty( var fullProperty = generator .WithAccessorDeclarations( - GetPropertyWithoutInitializer(property), + property, newSetAccessor == null ? [newGetAccessor] : [newGetAccessor, newSetAccessor]) From ea802900d3a7f091f23103d33b2e0b4b6c7ac341 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Fri, 31 Jan 2025 20:19:39 -0800 Subject: [PATCH 2/2] Simplify syntactic manipulatinos --- ...pertyToFullPropertyCodeRefactoringProvider.cs | 12 ++++-------- .../CodeGeneration/CSharpSyntaxGenerator.cs | 16 +++++++++------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs index 6472d3dac8a02..5cb11e73a8cf6 100644 --- a/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertAutoPropertyToFullProperty/CSharpConvertAutoPropertyToFullPropertyCodeRefactoringProvider.cs @@ -129,9 +129,7 @@ protected override SyntaxNode ConvertPropertyToExpressionBodyIfDesired( var preference = info.Options.PreferExpressionBodiedProperties.Value; if (preference == ExpressionBodyPreference.Never) - { - return propertyDeclaration.WithSemicolonToken(default); - } + return propertyDeclaration; // if there is a get accessors only, we can move the expression body to the property if (propertyDeclaration.AccessorList?.Accessors.Count == 1 && @@ -146,7 +144,7 @@ protected override SyntaxNode ConvertPropertyToExpressionBodyIfDesired( } } - return propertyDeclaration.WithSemicolonToken(default); + return propertyDeclaration; } protected override SyntaxNode GetTypeBlock(SyntaxNode syntaxNode) @@ -168,10 +166,8 @@ protected override async Task ExpandToFieldPropertyAsync( // Update the getter/setter to reference the 'field' expression instead. var (newGetAccessor, newSetAccessor) = GetNewAccessors(info, property, FieldExpression(), cancellationToken); - // The normal helper will strip off the semicolon (as we're normally moving the initializer to a field). - // Don't do that here as we will keep the current initializer on the property if it is there. - var finalProperty = (PropertyDeclarationSyntax)CreateFinalProperty(document, property, info, newGetAccessor, newSetAccessor); - var finalRoot = root.ReplaceNode(property, finalProperty.WithSemicolonToken(property.SemicolonToken)); + var finalProperty = CreateFinalProperty(document, property, info, newGetAccessor, newSetAccessor); + var finalRoot = root.ReplaceNode(property, finalProperty); return document.WithSyntaxRoot(finalRoot); } diff --git a/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpSyntaxGenerator.cs b/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpSyntaxGenerator.cs index cf2aa2ed0d0e9..2c005f7ad2bd6 100644 --- a/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpSyntaxGenerator.cs +++ b/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpSyntaxGenerator.cs @@ -440,13 +440,15 @@ private static SyntaxNode AccessorDeclaration( public override SyntaxNode WithAccessorDeclarations(SyntaxNode declaration, IEnumerable accessorDeclarations) => declaration switch { - PropertyDeclarationSyntax property => property.WithAccessorList(CreateAccessorList(property.AccessorList, accessorDeclarations)) - .WithExpressionBody(null) - .WithSemicolonToken(default), - - IndexerDeclarationSyntax indexer => indexer.WithAccessorList(CreateAccessorList(indexer.AccessorList, accessorDeclarations)) - .WithExpressionBody(null) - .WithSemicolonToken(default), + PropertyDeclarationSyntax property => + property.WithAccessorList(CreateAccessorList(property.AccessorList, accessorDeclarations)) + .WithExpressionBody(null) + .WithSemicolonToken(property.Initializer is null ? default : property.SemicolonToken), + + IndexerDeclarationSyntax indexer => + indexer.WithAccessorList(CreateAccessorList(indexer.AccessorList, accessorDeclarations)) + .WithExpressionBody(null) + .WithSemicolonToken(default), _ => declaration, };