|
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Rename.ConflictEngine;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.EncapsulateField
{
internal abstract partial class AbstractEncapsulateFieldService : ILanguageService
{
protected abstract Task<SyntaxNode> RewriteFieldNameAndAccessibilityAsync(string originalFieldName, bool makePrivate, Document document, SyntaxAnnotation declarationAnnotation, CodeAndImportGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken);
protected abstract Task<ImmutableArray<IFieldSymbol>> GetFieldsAsync(Document document, TextSpan span, CancellationToken cancellationToken);
public async Task<EncapsulateFieldResult> EncapsulateFieldsInSpanAsync(Document document, TextSpan span, CleanCodeGenerationOptionsProvider fallbackOptions, bool useDefaultBehavior, CancellationToken cancellationToken)
{
var fields = await GetFieldsAsync(document, span, cancellationToken).ConfigureAwait(false);
if (fields.IsDefaultOrEmpty)
return null;
var firstField = fields[0];
return new EncapsulateFieldResult(
firstField.ToDisplayString(),
firstField.GetGlyph(),
c => EncapsulateFieldsAsync(document, fields, fallbackOptions, useDefaultBehavior, c));
}
public async Task<ImmutableArray<CodeAction>> GetEncapsulateFieldCodeActionsAsync(Document document, TextSpan span, CleanCodeGenerationOptionsProvider fallbackOptions, CancellationToken cancellationToken)
{
var fields = await GetFieldsAsync(document, span, cancellationToken).ConfigureAwait(false);
if (fields.IsDefaultOrEmpty)
return ImmutableArray<CodeAction>.Empty;
if (fields.Length == 1)
{
// there is only one field
return EncapsulateOneField(document, fields[0], fallbackOptions);
}
// there are multiple fields.
using var _ = ArrayBuilder<CodeAction>.GetInstance(out var builder);
if (span.IsEmpty)
{
// if there is no selection, get action for each field + all of them.
foreach (var field in fields)
builder.AddRange(EncapsulateOneField(document, field, fallbackOptions));
}
builder.AddRange(EncapsulateAllFields(document, fields, fallbackOptions));
return builder.ToImmutable();
}
private ImmutableArray<CodeAction> EncapsulateAllFields(Document document, ImmutableArray<IFieldSymbol> fields, CleanCodeGenerationOptionsProvider fallbackOptions)
{
return ImmutableArray.Create(
CodeAction.Create(
FeaturesResources.Encapsulate_fields_and_use_property,
c => EncapsulateFieldsAsync(document, fields, fallbackOptions, updateReferences: true, c),
nameof(FeaturesResources.Encapsulate_fields_and_use_property)),
CodeAction.Create(
FeaturesResources.Encapsulate_fields_but_still_use_field,
c => EncapsulateFieldsAsync(document, fields, fallbackOptions, updateReferences: false, c),
nameof(FeaturesResources.Encapsulate_fields_but_still_use_field)));
}
private ImmutableArray<CodeAction> EncapsulateOneField(Document document, IFieldSymbol field, CleanCodeGenerationOptionsProvider fallbackOptions)
{
var fields = ImmutableArray.Create(field);
return ImmutableArray.Create(
CodeAction.Create(
string.Format(FeaturesResources.Encapsulate_field_colon_0_and_use_property, field.Name),
c => EncapsulateFieldsAsync(document, fields, fallbackOptions, updateReferences: true, c),
nameof(FeaturesResources.Encapsulate_field_colon_0_and_use_property) + "_" + field.Name),
CodeAction.Create(
string.Format(FeaturesResources.Encapsulate_field_colon_0_but_still_use_field, field.Name),
c => EncapsulateFieldsAsync(document, fields, fallbackOptions, updateReferences: false, c),
nameof(FeaturesResources.Encapsulate_field_colon_0_but_still_use_field) + "_" + field.Name));
}
public async Task<Solution> EncapsulateFieldsAsync(
Document document, ImmutableArray<IFieldSymbol> fields,
CleanCodeGenerationOptionsProvider fallbackOptions,
bool updateReferences, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using (Logger.LogBlock(FunctionId.Renamer_FindRenameLocationsAsync, cancellationToken))
{
var solution = document.Project.Solution;
var client = await RemoteHostClient.TryGetClientAsync(solution.Services, cancellationToken).ConfigureAwait(false);
if (client != null)
{
var fieldSymbolKeys = fields.SelectAsArray(f => SymbolKey.CreateString(f, cancellationToken));
var result = await client.TryInvokeAsync<IRemoteEncapsulateFieldService, ImmutableArray<(DocumentId, ImmutableArray<TextChange>)>>(
solution,
(service, solutionInfo, callbackId, cancellationToken) => service.EncapsulateFieldsAsync(solutionInfo, callbackId, document.Id, fieldSymbolKeys, updateReferences, cancellationToken),
callbackTarget: new RemoteOptionsProvider<CleanCodeGenerationOptions>(solution.Services, fallbackOptions),
cancellationToken).ConfigureAwait(false);
if (!result.HasValue)
{
return solution;
}
return await RemoteUtilities.UpdateSolutionAsync(
solution, result.Value, cancellationToken).ConfigureAwait(false);
}
}
return await EncapsulateFieldsInCurrentProcessAsync(
document, fields, fallbackOptions, updateReferences, cancellationToken).ConfigureAwait(false);
}
private async Task<Solution> EncapsulateFieldsInCurrentProcessAsync(Document document, ImmutableArray<IFieldSymbol> fields, CleanCodeGenerationOptionsProvider fallbackOptions, bool updateReferences, CancellationToken cancellationToken)
{
Contract.ThrowIfTrue(fields.Length == 0);
// For now, build up the multiple field case by encapsulating one at a time.
var currentSolution = document.Project.Solution;
foreach (var field in fields)
{
document = currentSolution.GetDocument(document.Id);
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var compilation = semanticModel.Compilation;
// We couldn't resolve this field. skip it
if (field.GetSymbolKey(cancellationToken).Resolve(compilation, cancellationToken: cancellationToken).Symbol is not IFieldSymbol currentField)
continue;
var nextSolution = await EncapsulateFieldAsync(document, currentField, updateReferences, fallbackOptions, cancellationToken).ConfigureAwait(false);
if (nextSolution == null)
continue;
currentSolution = nextSolution;
}
return currentSolution;
}
private async Task<Solution> EncapsulateFieldAsync(
Document document,
IFieldSymbol field,
bool updateReferences,
CleanCodeGenerationOptionsProvider fallbackOptions,
CancellationToken cancellationToken)
{
var originalField = field;
var (finalFieldName, generatedPropertyName) = GenerateFieldAndPropertyNames(field);
// Annotate the field declarations so we can find it after rename.
var fieldDeclaration = field.DeclaringSyntaxReferences.First();
var declarationAnnotation = new SyntaxAnnotation();
document = document.WithSyntaxRoot(fieldDeclaration.SyntaxTree.GetRoot(cancellationToken).ReplaceNode(fieldDeclaration.GetSyntax(cancellationToken),
fieldDeclaration.GetSyntax(cancellationToken).WithAdditionalAnnotations(declarationAnnotation)));
var solution = document.Project.Solution;
foreach (var linkedDocumentId in document.GetLinkedDocumentIds())
{
var linkedDocument = solution.GetDocument(linkedDocumentId);
var linkedRoot = await linkedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var linkedFieldNode = linkedRoot.FindNode(fieldDeclaration.Span);
if (linkedFieldNode.Span != fieldDeclaration.Span)
{
continue;
}
var updatedRoot = linkedRoot.ReplaceNode(linkedFieldNode, linkedFieldNode.WithAdditionalAnnotations(declarationAnnotation));
solution = solution.WithDocumentSyntaxRoot(linkedDocumentId, updatedRoot);
}
document = solution.GetDocument(document.Id);
// Resolve the annotated symbol and prepare for rename.
var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var compilation = semanticModel.Compilation;
field = field.GetSymbolKey(cancellationToken).Resolve(compilation, cancellationToken: cancellationToken).Symbol as IFieldSymbol;
// We couldn't resolve field after annotating its declaration. Bail
if (field == null)
return null;
var solutionNeedingProperty = await UpdateReferencesAsync(
updateReferences, solution, document, field, finalFieldName, generatedPropertyName, fallbackOptions, cancellationToken).ConfigureAwait(false);
document = solutionNeedingProperty.GetDocument(document.Id);
var markFieldPrivate = field.DeclaredAccessibility != Accessibility.Private;
var rewrittenFieldDeclaration = await RewriteFieldNameAndAccessibilityAsync(finalFieldName, markFieldPrivate, document, declarationAnnotation, fallbackOptions, cancellationToken).ConfigureAwait(false);
var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false);
document = await Formatter.FormatAsync(document.WithSyntaxRoot(rewrittenFieldDeclaration), Formatter.Annotation, formattingOptions, cancellationToken).ConfigureAwait(false);
solution = document.Project.Solution;
foreach (var linkedDocumentId in document.GetLinkedDocumentIds())
{
var linkedDocument = solution.GetDocument(linkedDocumentId);
var linkedDocumentFormattingOptions = await linkedDocument.GetSyntaxFormattingOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false);
var updatedLinkedRoot = await RewriteFieldNameAndAccessibilityAsync(finalFieldName, markFieldPrivate, linkedDocument, declarationAnnotation, fallbackOptions, cancellationToken).ConfigureAwait(false);
var updatedLinkedDocument = await Formatter.FormatAsync(linkedDocument.WithSyntaxRoot(updatedLinkedRoot), Formatter.Annotation, linkedDocumentFormattingOptions, cancellationToken).ConfigureAwait(false);
solution = updatedLinkedDocument.Project.Solution;
}
document = solution.GetDocument(document.Id);
semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var newRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var newDeclaration = newRoot.GetAnnotatedNodes<SyntaxNode>(declarationAnnotation).First();
field = semanticModel.GetDeclaredSymbol(newDeclaration, cancellationToken) as IFieldSymbol;
var generatedProperty = GenerateProperty(
generatedPropertyName,
finalFieldName,
originalField.DeclaredAccessibility,
originalField,
field.ContainingType,
new SyntaxAnnotation(),
document);
var simplifierOptions = await document.GetSimplifierOptionsAsync(fallbackOptions, cancellationToken).ConfigureAwait(false);
var documentWithProperty = await AddPropertyAsync(
document, document.Project.Solution, field, generatedProperty, fallbackOptions, cancellationToken).ConfigureAwait(false);
documentWithProperty = await Formatter.FormatAsync(documentWithProperty, Formatter.Annotation, formattingOptions, cancellationToken).ConfigureAwait(false);
documentWithProperty = await Simplifier.ReduceAsync(documentWithProperty, simplifierOptions, cancellationToken).ConfigureAwait(false);
return documentWithProperty.Project.Solution;
}
private async Task<Solution> UpdateReferencesAsync(
bool updateReferences, Solution solution, Document document, IFieldSymbol field, string finalFieldName, string generatedPropertyName, CodeCleanupOptionsProvider fallbackOptions, CancellationToken cancellationToken)
{
if (!updateReferences)
{
return solution;
}
var projectId = document.Project.Id;
if (field.IsReadOnly)
{
// Inside the constructor we want to rename references the field to the final field name.
var constructorLocations = GetConstructorLocations(solution, field.ContainingType);
if (finalFieldName != field.Name && constructorLocations.Count > 0)
{
solution = await RenameAsync(
solution, field, finalFieldName,
(docId, span) => IntersectsWithAny(docId, span, constructorLocations),
fallbackOptions,
cancellationToken).ConfigureAwait(false);
document = solution.GetDocument(document.Id);
var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
field = field.GetSymbolKey(cancellationToken).Resolve(compilation, cancellationToken: cancellationToken).Symbol as IFieldSymbol;
constructorLocations = GetConstructorLocations(solution, field.ContainingType);
}
// Outside the constructor we want to rename references to the field to final property name.
return await RenameAsync(
solution, field, generatedPropertyName,
(documentId, span) => !IntersectsWithAny(documentId, span, constructorLocations),
fallbackOptions,
cancellationToken).ConfigureAwait(false);
}
else
{
// Just rename everything.
return await Renamer.RenameSymbolAsync(
solution, field, new SymbolRenameOptions(), generatedPropertyName, cancellationToken).ConfigureAwait(false);
}
}
private static async Task<Solution> RenameAsync(
Solution solution,
IFieldSymbol field,
string finalName,
Func<DocumentId, TextSpan, bool> filter,
CodeCleanupOptionsProvider fallbackOptions,
CancellationToken cancellationToken)
{
var options = new SymbolRenameOptions(
RenameOverloads: false,
RenameInStrings: false,
RenameInComments: false,
RenameFile: false);
var initialLocations = await Renamer.FindRenameLocationsAsync(
solution, field, options, fallbackOptions, cancellationToken).ConfigureAwait(false);
var resolution = await initialLocations.Filter(filter).ResolveConflictsAsync(
field, finalName, nonConflictSymbolKeys: default, cancellationToken).ConfigureAwait(false);
Contract.ThrowIfFalse(resolution.IsSuccessful);
return resolution.NewSolution;
}
private static bool IntersectsWithAny(DocumentId documentId, TextSpan span, ISet<(DocumentId documentId, TextSpan span)> constructorLocations)
{
foreach (var constructor in constructorLocations)
{
if (constructor.documentId == documentId &&
span.IntersectsWith(constructor.span))
{
return true;
}
}
return false;
}
private ISet<(DocumentId documentId, TextSpan span)> GetConstructorLocations(Solution solution, INamedTypeSymbol containingType)
=> GetConstructorNodes(containingType).Select(n => (solution.GetRequiredDocument(n.SyntaxTree).Id, n.Span)).ToSet();
internal abstract IEnumerable<SyntaxNode> GetConstructorNodes(INamedTypeSymbol containingType);
protected static async Task<Document> AddPropertyAsync(
Document document,
Solution destinationSolution,
IFieldSymbol field,
IPropertySymbol property,
CodeAndImportGenerationOptionsProvider fallbackOptions,
CancellationToken cancellationToken)
{
var codeGenerationService = document.GetLanguageService<ICodeGenerationService>();
var fieldDeclaration = field.DeclaringSyntaxReferences.First();
var context = new CodeGenerationSolutionContext(
destinationSolution,
new CodeGenerationContext(
contextLocation: fieldDeclaration.SyntaxTree.GetLocation(fieldDeclaration.Span)),
fallbackOptions);
var destination = field.ContainingType;
return await codeGenerationService.AddPropertyAsync(
context, destination, property, cancellationToken).ConfigureAwait(false);
}
protected static IPropertySymbol GenerateProperty(
string propertyName, string fieldName,
Accessibility accessibility,
IFieldSymbol field,
INamedTypeSymbol containingSymbol,
SyntaxAnnotation annotation,
Document document)
{
var factory = document.GetLanguageService<SyntaxGenerator>();
var propertySymbol = annotation.AddAnnotationToSymbol(CodeGenerationSymbolFactory.CreatePropertySymbol(containingType: containingSymbol,
attributes: ImmutableArray<AttributeData>.Empty,
accessibility: ComputeAccessibility(accessibility, field.Type),
modifiers: new DeclarationModifiers(isStatic: field.IsStatic, isReadOnly: field.IsReadOnly, isUnsafe: field.RequiresUnsafeModifier()),
type: field.GetSymbolType(),
refKind: RefKind.None,
explicitInterfaceImplementations: default,
name: propertyName,
parameters: ImmutableArray<IParameterSymbol>.Empty,
getMethod: CreateGet(fieldName, field, factory),
setMethod: field.IsReadOnly || field.IsConst ? null : CreateSet(fieldName, field, factory)));
return Simplifier.Annotation.AddAnnotationToSymbol(
Formatter.Annotation.AddAnnotationToSymbol(propertySymbol));
}
protected abstract (string fieldName, string propertyName) GenerateFieldAndPropertyNames(IFieldSymbol field);
protected static Accessibility ComputeAccessibility(Accessibility accessibility, ITypeSymbol type)
{
var computedAccessibility = accessibility;
if (accessibility is Accessibility.NotApplicable or Accessibility.Private)
{
computedAccessibility = Accessibility.Public;
}
var returnTypeAccessibility = type.DetermineMinimalAccessibility();
return AccessibilityUtilities.Minimum(computedAccessibility, returnTypeAccessibility);
}
protected static IMethodSymbol CreateSet(string originalFieldName, IFieldSymbol field, SyntaxGenerator factory)
{
var assigned = !field.IsStatic
? factory.MemberAccessExpression(
factory.ThisExpression(),
factory.IdentifierName(originalFieldName))
: factory.IdentifierName(originalFieldName);
var body = factory.ExpressionStatement(
factory.AssignmentStatement(
assigned.WithAdditionalAnnotations(Simplifier.Annotation),
factory.IdentifierName("value")));
return CodeGenerationSymbolFactory.CreateAccessorSymbol(
ImmutableArray<AttributeData>.Empty,
Accessibility.NotApplicable,
ImmutableArray.Create(body));
}
protected static IMethodSymbol CreateGet(string originalFieldName, IFieldSymbol field, SyntaxGenerator factory)
{
var value = !field.IsStatic
? factory.MemberAccessExpression(
factory.ThisExpression(),
factory.IdentifierName(originalFieldName))
: factory.IdentifierName(originalFieldName);
var body = factory.ReturnStatement(
value.WithAdditionalAnnotations(Simplifier.Annotation));
return CodeGenerationSymbolFactory.CreateAccessorSymbol(
ImmutableArray<AttributeData>.Empty,
Accessibility.NotApplicable,
ImmutableArray.Create(body));
}
private static readonly char[] s_underscoreCharArray = new[] { '_' };
protected static string GeneratePropertyName(string fieldName)
{
// Trim leading underscores
var baseName = fieldName.TrimStart(s_underscoreCharArray);
// Trim leading "m_"
if (baseName is ['m', '_', .. var rest])
baseName = rest;
// Take original name if no characters left
if (baseName.Length == 0)
baseName = fieldName;
// Make the first character upper case using the "en-US" culture. See discussion at
// https://github.com/dotnet/roslyn/issues/5524.
var firstCharacter = EnUSCultureInfo.TextInfo.ToUpper(baseName[0]);
return firstCharacter.ToString() + baseName[1..];
}
private static readonly CultureInfo EnUSCultureInfo = new("en-US");
}
}
|