|
// 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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.UseAutoProperty;
using static UseAutoPropertiesHelpers;
internal abstract partial class AbstractUseAutoPropertyCodeFixProvider<
TProvider,
TTypeDeclarationSyntax,
TPropertyDeclaration,
TVariableDeclarator,
TConstructorDeclaration,
TExpression>
: CodeFixProvider
where TProvider : AbstractUseAutoPropertyCodeFixProvider<
TProvider,
TTypeDeclarationSyntax,
TPropertyDeclaration,
TVariableDeclarator,
TConstructorDeclaration,
TExpression>
where TTypeDeclarationSyntax : SyntaxNode
where TPropertyDeclaration : SyntaxNode
where TVariableDeclarator : SyntaxNode
where TConstructorDeclaration : SyntaxNode
where TExpression : SyntaxNode
{
protected static SyntaxAnnotation SpecializedFormattingAnnotation = new();
public sealed override ImmutableArray<string> FixableDiagnosticIds
=> [IDEDiagnosticIds.UseAutoPropertyDiagnosticId];
public sealed override FixAllProvider GetFixAllProvider()
=> new UseAutoPropertyFixAllProvider((TProvider)this);
protected abstract TPropertyDeclaration GetPropertyDeclaration(SyntaxNode node);
protected abstract SyntaxNode GetNodeToRemove(TVariableDeclarator declarator);
protected abstract TPropertyDeclaration RewriteFieldReferencesInProperty(
TPropertyDeclaration property, ImmutableArray<ReferencedSymbol> fieldLocations, CancellationToken cancellationToken);
protected abstract ImmutableArray<AbstractFormattingRule> GetFormattingRules(
Document document, SyntaxNode finalPropertyDeclaration);
protected abstract Task<SyntaxNode> UpdatePropertyAsync(
Document propertyDocument,
Compilation compilation,
IFieldSymbol fieldSymbol,
IPropertySymbol propertySymbol,
TVariableDeclarator fieldDeclarator,
TPropertyDeclaration propertyDeclaration,
bool isWrittenOutsideConstructor,
bool isTrivialGetAccessor,
bool isTrivialSetAccessor,
CancellationToken cancellationToken);
public sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
{
var solution = context.Document.Project.Solution;
foreach (var diagnostic in context.Diagnostics)
{
var priority = diagnostic.Severity == DiagnosticSeverity.Hidden
? CodeActionPriority.Low
: CodeActionPriority.Default;
context.RegisterCodeFix(CodeAction.SolutionChangeAction.Create(
AnalyzersResources.Use_auto_property,
cancellationToken => ProcessResultAsync(solution, solution, diagnostic, cancellationToken),
equivalenceKey: nameof(AnalyzersResources.Use_auto_property),
priority),
diagnostic);
}
return Task.CompletedTask;
}
private async Task<Solution> ProcessResultAsync(
Solution originalSolution, Solution currentSolution, Diagnostic diagnostic, CancellationToken cancellationToken)
{
try
{
return await ProcessResultWorkerAsync(originalSolution, currentSolution, diagnostic, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex))
{
return currentSolution;
}
}
private async Task<Solution> ProcessResultWorkerAsync(
Solution originalSolution, Solution currentSolution, Diagnostic diagnostic, CancellationToken cancellationToken)
{
var (field, property) = await MapDiagnosticToCurrentSolutionAsync(
diagnostic, originalSolution, currentSolution, cancellationToken).ConfigureAwait(false);
if (field == null || property == null)
return currentSolution;
var fieldDocument = currentSolution.GetRequiredDocument(field.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken).SyntaxTree);
var propertyDocument = currentSolution.GetRequiredDocument(property.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken).SyntaxTree);
var isTrivialGetAccessor = diagnostic.Properties.ContainsKey(IsTrivialGetAccessor);
var isTrivialSetAccessor = diagnostic.Properties.ContainsKey(IsTrivialSetAccessor);
Debug.Assert(fieldDocument.Project == propertyDocument.Project);
var project = fieldDocument.Project;
var compilation = await project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
var fieldLocations = await SymbolFinder.FindReferencesAsync(
field, currentSolution, FindReferencesSearchOptions.Default, cancellationToken).ConfigureAwait(false);
var declarator = (TVariableDeclarator)field.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken);
var propertyDeclaration = GetPropertyDeclaration(property.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken));
// First, create the updated property we want to replace the old property with
var isWrittenToOutsideOfConstructor = IsWrittenToOutsideOfConstructorOrProperty(
field, fieldLocations, propertyDeclaration, cancellationToken);
if (!isTrivialGetAccessor ||
(property.SetMethod != null && !isTrivialSetAccessor))
{
// We have at least a non-trivial getter/setter. Those will not be rewritten to `get;/set;`. As such, we
// need to update the property to reference `field` or itself instead of the actual field.
propertyDeclaration = RewriteFieldReferencesInProperty(propertyDeclaration, fieldLocations, cancellationToken);
}
var updatedProperty = await UpdatePropertyAsync(
propertyDocument, compilation,
field, property,
declarator, propertyDeclaration,
isWrittenToOutsideOfConstructor, isTrivialGetAccessor, isTrivialSetAccessor,
cancellationToken).ConfigureAwait(false);
// Note: rename will try to update all the references in linked files as well. However,
// this can lead to some very bad behavior as we will change the references in linked files
// but only remove the field and update the property in a single document. So, you can
// end in the state where you do this in one of the linked file:
//
// int Prop { get { return this.field; } } => int Prop { get { return this.Prop } }
//
// But in the main file we'll replace:
//
// int Prop { get { return this.field; } } => int Prop { get; }
//
// The workspace will see these as two irreconcilable edits. To avoid this, we disallow
// any edits to the other links for the files containing the field and property. i.e.
// rename will only be allowed to edit the exact same doc we're removing the field from
// and the exact doc we're updating the property in. It can't touch the other linked
// files for those docs. (It can of course touch any other documents unrelated to the
// docs that the field and prop are declared in).
var linkedFiles = new HashSet<DocumentId>();
linkedFiles.AddRange(fieldDocument.GetLinkedDocumentIds());
linkedFiles.AddRange(propertyDocument.GetLinkedDocumentIds());
// Now, rename all usages of the field to point at the property.
currentSolution = await UpdateReferencesAsync(
currentSolution, linkedFiles, fieldLocations, property, cancellationToken).ConfigureAwait(false);
// Now find the field and property again post rename.
fieldDocument = currentSolution.GetRequiredDocument(fieldDocument.Id);
propertyDocument = currentSolution.GetRequiredDocument(propertyDocument.Id);
Debug.Assert(fieldDocument.Project == propertyDocument.Project);
compilation = await fieldDocument.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
field = (IFieldSymbol?)field.GetSymbolKey(cancellationToken).Resolve(compilation, cancellationToken: cancellationToken).Symbol;
property = (IPropertySymbol?)property.GetSymbolKey(cancellationToken).Resolve(compilation, cancellationToken: cancellationToken).Symbol;
Contract.ThrowIfTrue(field == null || property == null);
declarator = (TVariableDeclarator)field.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken);
propertyDeclaration = GetPropertyDeclaration(property.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken));
var nodeToRemove = GetNodeToRemove(declarator);
// If we have a situation where the property is the second member in a type, and it
// would become the first, then remove any leading blank lines from it so we don't have
// random blanks above it that used to space it from the field that was there.
//
// The reason we do this special processing is that the first member of a type tends to
// be special wrt leading trivia. i.e. users do not normally put blank lines before the
// first member. And so, when a type now becomes the first member, we want to follow the
// user's common pattern here.
//
// In all other code cases, i.e.when there are multiple fields above, or the field is
// below the property, then the property isn't now becoming "the first member", and as
// such, it doesn't want this special behavior about it's leading blank lines. i.e. if
// the user has:
//
// class C
// {
// int i;
// int j;
//
// int Prop => j;
// }
//
// Then if we remove 'j' (or even 'i'), then 'Prop' would stay the non-first member, and
// would definitely want to keep that blank line above it.
//
// In essence, the blank line above the property exists for separation from what's above
// it. As long as something is above it, we keep the separation. However, if the
// property becomes the first member in the type, the separation is now inappropriate
// because there's nothing to actually separate it from.
var fieldDocumentSyntaxFacts = fieldDocument.GetRequiredLanguageService<ISyntaxFactsService>();
if (fieldDocument == propertyDocument)
{
var bannerService = fieldDocument.GetRequiredLanguageService<IFileBannerFactsService>();
if (WillRemoveFirstFieldInTypeDirectlyAboveProperty(fieldDocumentSyntaxFacts, propertyDeclaration, nodeToRemove) &&
bannerService.GetLeadingBlankLines(nodeToRemove).Length == 0)
{
updatedProperty = bannerService.GetNodeWithoutLeadingBlankLines(updatedProperty);
}
}
var syntaxRemoveOptions = CreateSyntaxRemoveOptions(fieldDocumentSyntaxFacts, nodeToRemove);
if (fieldDocument == propertyDocument)
{
// Same file. Have to do this in a slightly complicated fashion.
var declaratorTreeRoot = await fieldDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var editor = new SyntaxEditor(declaratorTreeRoot, fieldDocument.Project.Solution.Services);
editor.ReplaceNode(propertyDeclaration, updatedProperty);
editor.RemoveNode(nodeToRemove, syntaxRemoveOptions);
var updatedFieldDocument = fieldDocument.WithSyntaxRoot(editor.GetChangedRoot());
var finalFieldRoot = await FormatAsync(updatedFieldDocument, updatedProperty, cancellationToken).ConfigureAwait(false);
return currentSolution.WithDocumentSyntaxRoot(fieldDocument.Id, finalFieldRoot);
}
else
{
// In different files. Just update both files.
var fieldTreeRoot = await fieldDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var propertyTreeRoot = await propertyDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var newFieldTreeRoot = fieldTreeRoot.RemoveNode(nodeToRemove, syntaxRemoveOptions);
Contract.ThrowIfNull(newFieldTreeRoot);
var newPropertyTreeRoot = propertyTreeRoot.ReplaceNode(propertyDeclaration, updatedProperty);
var updatedFieldDocument = fieldDocument.WithSyntaxRoot(newFieldTreeRoot);
var updatedPropertyDocument = propertyDocument.WithSyntaxRoot(newPropertyTreeRoot);
newFieldTreeRoot = await FormatAsync(updatedFieldDocument, updatedProperty, cancellationToken).ConfigureAwait(false);
newPropertyTreeRoot = await FormatAsync(updatedPropertyDocument, updatedProperty, cancellationToken).ConfigureAwait(false);
var updatedSolution = currentSolution.WithDocumentSyntaxRoot(fieldDocument.Id, newFieldTreeRoot);
updatedSolution = updatedSolution.WithDocumentSyntaxRoot(propertyDocument.Id, newPropertyTreeRoot);
return updatedSolution;
}
}
private static async Task<Solution> UpdateReferencesAsync(
Solution solution,
HashSet<DocumentId> linkedDocuments,
ImmutableArray<ReferencedSymbol> fieldLocations,
IPropertySymbol property,
CancellationToken cancellationToken)
{
var solutionEditor = new SolutionEditor(solution);
var canEditMap = new Dictionary<DocumentId, bool>();
foreach (var group in fieldLocations.SelectMany(loc => loc.Locations).GroupBy(loc => loc.Document))
{
cancellationToken.ThrowIfCancellationRequested();
var document = group.Key;
if (!CanEditDocument(document.Id))
continue;
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
var editor = await solutionEditor.GetDocumentEditorAsync(document.Id, cancellationToken).ConfigureAwait(false);
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var generator = editor.Generator;
var newNameNode = generator.IdentifierName(property.Name);
foreach (var location in group.Distinct(LinkedFileReferenceLocationEqualityComparer.Instance))
{
if (location.IsImplicit)
continue;
var node = location.Location.FindNode(getInnermostNodeForTie: true, cancellationToken);
if (syntaxFacts.GetRootStandaloneExpression(node) == node)
{
// We're referencing the field as a trivial name (like `fieldName`). In this case, we might run into
// problems with symbol collisions if we just change the name to `propertyName`. So instead, we check
// if that name is in scope and isn't a reference to the new property. If that's the case, then we
// qualify with `this.fieldName` or `ClassName.FieldName` to avoid any collisions.
var symbols = semanticModel.LookupSymbols(node.SpanStart, name: property.Name);
if (symbols.Length > 0 && symbols.All(s => !s.OriginalDefinition.Equals(property.OriginalDefinition)))
{
var qualifiedName = generator.MemberAccessExpression(
property.IsStatic ? generator.TypeExpression(property.ContainingType) : generator.ThisExpression(),
newNameNode);
editor.ReplaceNode(node, qualifiedName.WithTriviaFrom(node));
}
else
{
// The name was standing alone and didn't bind to any other symbol. Just do the trivial rename here.
editor.ReplaceNode(node, newNameNode.WithTriviaFrom(node));
}
}
else
{
// Otherwise, we're referencing the field in a complex way (like `this.fieldName`). In this case, we can just
// trivially replace `fieldName` with `propertyName` and have it work. Note: we add the simplifier annotation
// here as well. That way we can attempt to simplify the code if the user does not prefer `this` qualifiers
// for properties.
editor.ReplaceNode(node, newNameNode.WithTriviaFrom(node));
editor.ReplaceNode(node.GetRequiredParent(), (current, _) => current.WithAdditionalAnnotations(Simplifier.Annotation));
}
}
}
return solutionEditor.GetChangedSolution();
bool CanEditDocument(DocumentId documentId)
{
if (!canEditMap.TryGetValue(documentId, out var canEditDocument))
{
var document = solution.GetDocument(documentId);
canEditDocument = document != null && !linkedDocuments.Contains(document.Id);
canEditMap[documentId] = canEditDocument;
}
return canEditDocument;
}
}
private async Task<(IFieldSymbol? fieldSymbol, IPropertySymbol? propertySymbol)> MapDiagnosticToCurrentSolutionAsync(
Diagnostic diagnostic,
Solution originalSolution,
Solution currentSolution,
CancellationToken cancellationToken)
{
var locations = diagnostic.AdditionalLocations;
var propertyLocation = locations[0];
var declaratorLocation = locations[1];
// Look up everything in the original solution.
var declarator = (TVariableDeclarator)declaratorLocation.FindNode(cancellationToken);
var fieldDocument = originalSolution.GetRequiredDocument(declarator.SyntaxTree);
var fieldSemanticModel = await fieldDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var fieldSymbol = (IFieldSymbol)fieldSemanticModel.GetRequiredDeclaredSymbol(declarator, cancellationToken);
var property = GetPropertyDeclaration(propertyLocation.FindNode(cancellationToken));
var propertyDocument = originalSolution.GetRequiredDocument(property.SyntaxTree);
var propertySemanticModel = await propertyDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var propertySymbol = (IPropertySymbol)propertySemanticModel.GetRequiredDeclaredSymbol(property, cancellationToken);
Contract.ThrowIfFalse(fieldDocument.Project == propertyDocument.Project);
// If we're just starting, no need to map anything.
if (originalSolution != currentSolution)
{
var currentProject = currentSolution.GetRequiredProject(fieldDocument.Project.Id);
var currentCompilation = await currentProject.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
fieldSymbol = fieldSymbol.GetSymbolKey(cancellationToken).Resolve(currentCompilation, cancellationToken: cancellationToken).GetAnySymbol() as IFieldSymbol;
propertySymbol = propertySymbol.GetSymbolKey(cancellationToken).Resolve(currentCompilation, cancellationToken: cancellationToken).GetAnySymbol() as IPropertySymbol;
}
return (fieldSymbol, propertySymbol);
}
private static SyntaxRemoveOptions CreateSyntaxRemoveOptions(
ISyntaxFacts syntaxFacts, SyntaxNode nodeToRemove)
{
var syntaxRemoveOptions = SyntaxGenerator.DefaultRemoveOptions;
if (nodeToRemove.GetLeadingTrivia().Any(t => t.IsDirective || syntaxFacts.IsRegularComment(t)))
syntaxRemoveOptions |= SyntaxRemoveOptions.KeepLeadingTrivia;
return syntaxRemoveOptions;
}
private static bool WillRemoveFirstFieldInTypeDirectlyAboveProperty(
ISyntaxFactsService syntaxFacts, TPropertyDeclaration property, SyntaxNode fieldToRemove)
{
if (fieldToRemove.Parent == property.Parent &&
fieldToRemove.Parent is TTypeDeclarationSyntax typeDeclaration)
{
var members = syntaxFacts.GetMembersOfTypeDeclaration(typeDeclaration);
return members[0] == fieldToRemove && members[1] == property;
}
return false;
}
private async Task<SyntaxNode> FormatAsync(
Document document,
SyntaxNode finalPropertyDeclaration,
CancellationToken cancellationToken)
{
// First see if we need to apply any specialized formatting rules.
var formattingRules = GetFormattingRules(document, finalPropertyDeclaration);
if (!formattingRules.IsDefault)
{
var options = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
document = await Formatter.FormatAsync(
document, SpecializedFormattingAnnotation, options, formattingRules, cancellationToken).ConfigureAwait(false);
}
var codeCleanupOptions = await document.GetCodeCleanupOptionsAsync(cancellationToken).ConfigureAwait(false);
var cleanedDocument = await CodeAction.CleanupSyntaxAsync(
document, codeCleanupOptions, cancellationToken).ConfigureAwait(false);
return await cleanedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
}
private static bool IsWrittenToOutsideOfConstructorOrProperty(
IFieldSymbol field,
ImmutableArray<ReferencedSymbol> referencedSymbols,
TPropertyDeclaration propertyDeclaration,
CancellationToken cancellationToken)
{
var constructorSpans = field.ContainingType
.GetMembers()
.Where(m => m.IsConstructor())
.SelectMany(c => c.DeclaringSyntaxReferences)
.Select(s => s.GetSyntax(cancellationToken))
.Select(n => n.FirstAncestorOrSelf<TConstructorDeclaration>())
.WhereNotNull()
.Select(d => (d.SyntaxTree.FilePath, d.Span))
.ToSet();
foreach (var referencedSymbol in referencedSymbols)
{
foreach (var location in referencedSymbol.LocationsArray)
{
if (IsWrittenToOutsideOfConstructorOrProperty(location, propertyDeclaration, constructorSpans, cancellationToken))
return true;
}
}
return false;
}
private static bool IsWrittenToOutsideOfConstructorOrProperty(
ReferenceLocation location,
TPropertyDeclaration propertyDeclaration,
ISet<(string filePath, TextSpan span)> constructorSpans,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// We don't need a setter if we're not writing to this field.
if (!location.IsWrittenTo)
return false;
if (location.IsImplicit)
return false;
var syntaxFacts = location.Document.GetRequiredLanguageService<ISyntaxFactsService>();
var node = location.Location.FindNode(getInnermostNodeForTie: true, cancellationToken);
while (node != null && !syntaxFacts.IsAnonymousOrLocalFunction(node))
{
if (node == propertyDeclaration)
{
// Not a write outside the property declaration.
return false;
}
if (constructorSpans.Contains((node.SyntaxTree.FilePath, node.Span)))
{
// Not a write outside a constructor of the field's class
return false;
}
node = node.Parent;
}
// We do need a setter
return true;
}
}
|