| File: src\roslyn\src\Analyzers\CSharp\CodeFixes\UseAutoProperty\CSharpUseAutoPropertyCodeFixProvider.cs | Web Access |
| Project: src\roslyn\src\CodeStyle\CSharp\CodeFixes\Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle.Fixes) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Immutable; using System.Composition; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Formatting; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Formatting.Rules; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.UseAutoProperty; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.UseAutoProperty; using static CSharpSyntaxTokens; using static SyntaxFactory; [ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseAutoProperty), Shared] [method: ImportingConstructor] [method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] internal sealed partial class CSharpUseAutoPropertyCodeFixProvider() : AbstractUseAutoPropertyCodeFixProvider< CSharpUseAutoPropertyCodeFixProvider, TypeDeclarationSyntax, PropertyDeclarationSyntax, VariableDeclaratorSyntax, ConstructorDeclarationSyntax, ExpressionSyntax> { protected override ISyntaxFormatting SyntaxFormatting => CSharpSyntaxFormatting.Instance; protected override PropertyDeclarationSyntax GetPropertyDeclaration(SyntaxNode node) => (PropertyDeclarationSyntax)node; private static bool SupportsReadOnlyProperties(Compilation compilation) => compilation.LanguageVersion() >= LanguageVersion.CSharp6; private static bool IsSetOrInitAccessor(AccessorDeclarationSyntax accessor) => accessor.Kind() is SyntaxKind.SetAccessorDeclaration or SyntaxKind.InitAccessorDeclaration; private static FieldDeclarationSyntax GetFieldDeclaration(VariableDeclaratorSyntax declarator) => (FieldDeclarationSyntax)declarator.GetRequiredParent().GetRequiredParent(); protected override SyntaxNode GetNodeToRemove(VariableDeclaratorSyntax declarator) { var fieldDeclaration = GetFieldDeclaration(declarator); return fieldDeclaration.Declaration.Variables.Count > 1 ? declarator : fieldDeclaration; } protected override PropertyDeclarationSyntax RewriteFieldReferencesInProperty( PropertyDeclarationSyntax property, ImmutableArray<ReferencedSymbol> fieldLocations, CancellationToken cancellationToken) { // We're going to walk this property body, converting most reference of the field to use the `field` keyword // instead. However, not all reference can be updated. For example, reference through another instance. Those // we update to point at the property instead. So we grab that property name here to use in the rewriter. var propertyIdentifierName = IdentifierName(property.Identifier.WithoutTrivia()); var identifierNames = fieldLocations.SelectMany(loc => loc.Locations.Select(loc => loc.Location.FindNode(getInnermostNodeForTie: true, cancellationToken) as IdentifierNameSyntax)) .WhereNotNull() .ToSet(); var rewriter = new UseAutoPropertyRewriter(propertyIdentifierName, identifierNames); return (PropertyDeclarationSyntax)rewriter.Visit(property); } protected override async Task<SyntaxNode> UpdatePropertyAsync( Document propertyDocument, Compilation compilation, IFieldSymbol fieldSymbol, IPropertySymbol propertySymbol, VariableDeclaratorSyntax fieldDeclarator, PropertyDeclarationSyntax propertyDeclaration, bool isWrittenOutsideOfConstructor, bool isTrivialGetAccessor, bool isTrivialSetAccessor, CancellationToken cancellationToken) { var project = propertyDocument.Project; var generator = SyntaxGenerator.GetGenerator(project); // Ensure that any attributes on the field are moved over to the property. propertyDeclaration = MoveAttributes(propertyDeclaration, GetFieldDeclaration(fieldDeclarator)); // We may need to add a setter if the field is written to outside of the constructor // of it's class. var needsSetter = NeedsSetter(compilation, propertyDeclaration, isWrittenOutsideOfConstructor); var fieldInitializer = fieldDeclarator.Initializer?.Value; if (!isTrivialGetAccessor && !isTrivialSetAccessor && !needsSetter && fieldInitializer == null) { // Nothing to actually do. We're not changing the accessors to `get;set;` accessors, and we didn't have to // add an setter. We also had no field initializer to move over. This can happen when we're converting to // using `field` and that rewrite already happened. return propertyDeclaration; } // 1. If we have a trivial getters/setter then we want to convert to an accessor list to have `get;set;` // 2. If we need a setter, we have to convert to having an accessor list to place the setter in. // 3. If we have a field initializer, we need to convert to an accessor list to add the initializer expression after. var updatedProperty = propertyDeclaration .WithExpressionBody(null) .WithSemicolonToken(default) .WithAccessorList(ConvertToAccessorList( propertyDeclaration, isTrivialGetAccessor, isTrivialSetAccessor)); if (needsSetter) { var accessor = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SemicolonToken); if (fieldSymbol.DeclaredAccessibility != propertySymbol.DeclaredAccessibility) accessor = (AccessorDeclarationSyntax)generator.WithAccessibility(accessor, fieldSymbol.DeclaredAccessibility); updatedProperty = updatedProperty .AddAccessorListAccessors(accessor) .WithModifiers(TokenList(updatedProperty.Modifiers.Where(token => !token.IsKind(SyntaxKind.ReadOnlyKeyword)))); } // Move any field initializer over to the property as well. if (fieldInitializer != null) { updatedProperty = updatedProperty.WithoutTrailingTrivia() .WithInitializer(EqualsValueClause(EqualsToken.WithLeadingTrivia(Space).WithTrailingTrivia(Space), fieldInitializer)) .WithSemicolonToken(SemicolonToken); } var finalProperty = updatedProperty .WithTrailingTrivia(propertyDeclaration.GetTrailingTrivia()) .WithAdditionalAnnotations(SpecializedFormattingAnnotation); return finalProperty; static PropertyDeclarationSyntax MoveAttributes( PropertyDeclarationSyntax property, FieldDeclarationSyntax field) { var fieldAttributes = field.AttributeLists; if (fieldAttributes.Count == 0) return property; var leadingTrivia = property.GetLeadingTrivia(); var indentation = leadingTrivia is [.., (kind: SyntaxKind.WhitespaceTrivia) whitespaceTrivia] ? whitespaceTrivia : default; using var _ = ArrayBuilder<AttributeListSyntax>.GetInstance(out var finalAttributes); foreach (var attributeList in fieldAttributes) { // Change any field attributes to be `[field: ...]` attributes. Take the property's trivia and place it // on the first field attribute we move over. var converted = ConvertAttributeList(attributeList); finalAttributes.Add(attributeList == fieldAttributes[0] ? converted.WithLeadingTrivia(leadingTrivia) : converted); } foreach (var attributeList in property.AttributeLists) { // Remove the leading trivia off of the first attribute. We're going to move it before all the new // field attributes we're adding. finalAttributes.Add(attributeList == property.AttributeLists[0] ? attributeList.WithLeadingTrivia(indentation) : attributeList); } return property .WithAttributeLists([]) .WithLeadingTrivia(indentation) .WithAttributeLists(List(finalAttributes)); } static AttributeListSyntax ConvertAttributeList(AttributeListSyntax attributeList) => attributeList.WithTarget(AttributeTargetSpecifier(Identifier(SyntaxFacts.GetText(SyntaxKind.FieldKeyword)), ColonToken.WithTrailingTrivia(Space))); static AccessorListSyntax ConvertToAccessorList( PropertyDeclarationSyntax propertyDeclaration, bool isTrivialGetAccessor, bool isTrivialSetAccessor) { // If we don't have an accessor list at all, convert the property's expr body to a `get => ...` accessor. var accessorList = propertyDeclaration.AccessorList ?? AccessorList(SingletonList( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithExpressionBody(propertyDeclaration.ExpressionBody) .WithSemicolonToken(SemicolonToken))); // Now that we have an accessor list, convert the getter/setter to `get;`/`set;` form if requested. return accessorList.WithAccessors(List(accessorList.Accessors.Select( accessor => { var convert = (isTrivialGetAccessor && accessor.Kind() is SyntaxKind.GetAccessorDeclaration) || (isTrivialSetAccessor && IsSetOrInitAccessor(accessor)); if (convert) { if (accessor.ExpressionBody != null) return accessor.WithExpressionBody(null).WithKeyword(accessor.Keyword.WithoutTrailingTrivia()); if (accessor.Body != null) return accessor.WithBody(null).WithSemicolonToken(SemicolonToken.WithTrailingTrivia(accessor.Body.CloseBraceToken.TrailingTrivia)); } return accessor; }))); } } protected override ImmutableArray<AbstractFormattingRule> GetFormattingRules( Document document, SyntaxNode propertyDeclaration) { // If the final property is only simple `get;set;` accessors, then reformat the property to be on a single line. if (propertyDeclaration is PropertyDeclarationSyntax { AccessorList.Accessors: var accessors } && accessors.All(a => a is { ExpressionBody: null, Body: null, AttributeLists.Count: 0 })) { return [new SingleLinePropertyFormattingRule(), .. this.SyntaxFormatting.GetDefaultFormattingRules()]; } return default; } private static bool NeedsSetter(Compilation compilation, PropertyDeclarationSyntax propertyDeclaration, bool isWrittenOutsideOfConstructor) { // Don't need to add if we already have a setter. if (propertyDeclaration.AccessorList != null && propertyDeclaration.AccessorList.Accessors.Any(IsSetOrInitAccessor)) { return false; } // If the language doesn't have readonly properties, then we'll need a setter here. if (!SupportsReadOnlyProperties(compilation)) return true; // If we're written outside a constructor we need a setter. return isWrittenOutsideOfConstructor; } }