// 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.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; 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.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 RewriteFieldNameAndAccessibilityAsync(string originalFieldName, bool makePrivate, Document document, SyntaxAnnotation declarationAnnotation, CancellationToken cancellationToken); protected abstract Task> GetFieldsAsync(Document document, TextSpan span, CancellationToken cancellationToken); public async Task EncapsulateFieldsInSpanAsync(Document document, TextSpan span, 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, useDefaultBehavior, c)); } public async Task> GetEncapsulateFieldCodeActionsAsync(Document document, TextSpan span, CancellationToken cancellationToken) { var fields = await GetFieldsAsync(document, span, cancellationToken).ConfigureAwait(false); if (fields.IsDefaultOrEmpty) return ImmutableArray.Empty; if (fields.Length == 1) { // there is only one field return EncapsulateOneField(document, fields[0]); } // there are multiple fields. using var _ = ArrayBuilder.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)); } builder.AddRange(EncapsulateAllFields(document, fields)); return builder.ToImmutable(); } private ImmutableArray EncapsulateAllFields(Document document, ImmutableArray fields) { return ImmutableArray.Create( new MyCodeAction( FeaturesResources.Encapsulate_fields_and_use_property, c => EncapsulateFieldsAsync(document, fields, updateReferences: true, c)), new MyCodeAction( FeaturesResources.Encapsulate_fields_but_still_use_field, c => EncapsulateFieldsAsync(document, fields, updateReferences: false, c))); } private ImmutableArray EncapsulateOneField(Document document, IFieldSymbol field) { var fields = ImmutableArray.Create(field); return ImmutableArray.Create( new MyCodeAction( string.Format(FeaturesResources.Encapsulate_field_colon_0_and_use_property, field.Name), c => EncapsulateFieldsAsync(document, fields, updateReferences: true, c)), new MyCodeAction( string.Format(FeaturesResources.Encapsulate_field_colon_0_but_still_use_field, field.Name), c => EncapsulateFieldsAsync(document, fields, updateReferences: false, c))); } public async Task EncapsulateFieldsAsync( Document document, ImmutableArray fields, bool updateReferences, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); using (Logger.LogBlock(FunctionId.Renamer_FindRenameLocationsAsync, cancellationToken)) { var solution = document.Project.Solution; var client = await RemoteHostClient.TryGetClientAsync(solution.Workspace, cancellationToken).ConfigureAwait(false); if (client != null) { var result = await client.TryRunRemoteAsync<(DocumentId, TextChange[])[]>( WellKnownServiceHubService.CodeAnalysis, nameof(IRemoteEncapsulateFieldService.EncapsulateFieldsAsync), solution, new object[] { document.Id, fields.Select(f => SymbolKey.CreateString(f, cancellationToken)).ToArray(), updateReferences, }, callbackTarget: null, cancellationToken).ConfigureAwait(false); if (result.HasValue) { return await RemoteUtilities.UpdateSolutionAsync( solution, result.Value, cancellationToken).ConfigureAwait(false); } } } return await EncapsulateFieldsInCurrentProcessAsync( document, fields, updateReferences, cancellationToken).ConfigureAwait(false); } private async Task EncapsulateFieldsInCurrentProcessAsync(Document document, ImmutableArray fields, 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().Resolve(compilation, cancellationToken: cancellationToken).Symbol is IFieldSymbol currentField)) continue; var nextSolution = await EncapsulateFieldAsync(document, currentField, updateReferences, cancellationToken).ConfigureAwait(false); if (nextSolution == null) continue; currentSolution = nextSolution; } return currentSolution; } private async Task EncapsulateFieldAsync( Document document, IFieldSymbol field, bool updateReferences, 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().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, cancellationToken).ConfigureAwait(false); document = solutionNeedingProperty.GetDocument(document.Id); var markFieldPrivate = field.DeclaredAccessibility != Accessibility.Private; var rewrittenFieldDeclaration = await RewriteFieldNameAndAccessibilityAsync(finalFieldName, markFieldPrivate, document, declarationAnnotation, cancellationToken).ConfigureAwait(false); document = await Formatter.FormatAsync(document.WithSyntaxRoot(rewrittenFieldDeclaration), Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false); solution = document.Project.Solution; foreach (var linkedDocumentId in document.GetLinkedDocumentIds()) { var linkedDocument = solution.GetDocument(linkedDocumentId); var updatedLinkedRoot = await RewriteFieldNameAndAccessibilityAsync(finalFieldName, markFieldPrivate, linkedDocument, declarationAnnotation, cancellationToken).ConfigureAwait(false); var updatedLinkedDocument = await Formatter.FormatAsync(linkedDocument.WithSyntaxRoot(updatedLinkedRoot), Formatter.Annotation, cancellationToken: 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(declarationAnnotation).First(); field = semanticModel.GetDeclaredSymbol(newDeclaration, cancellationToken) as IFieldSymbol; var generatedProperty = GenerateProperty( generatedPropertyName, finalFieldName, originalField.DeclaredAccessibility, originalField, field.ContainingType, new SyntaxAnnotation(), document); var solutionWithProperty = await AddPropertyAsync( document, document.Project.Solution, field, generatedProperty, cancellationToken).ConfigureAwait(false); return solutionWithProperty; } private async Task UpdateReferencesAsync( bool updateReferences, Solution solution, Document document, IFieldSymbol field, string finalFieldName, string generatedPropertyName, 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(field.ContainingType); if (finalFieldName != field.Name && constructorLocations.Count > 0) { solution = await RenameAsync( solution, field, finalFieldName, location => IntersectsWithAny(location, constructorLocations), cancellationToken).ConfigureAwait(false); document = solution.GetDocument(document.Id); var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); field = field.GetSymbolKey().Resolve(compilation, cancellationToken: cancellationToken).Symbol as IFieldSymbol; constructorLocations = GetConstructorLocations(field.ContainingType); } // Outside the constructor we want to rename references to the field to final property name. return await RenameAsync( solution, field, generatedPropertyName, location => !IntersectsWithAny(location, constructorLocations), cancellationToken).ConfigureAwait(false); } else { // Just rename everything. return await Renamer.RenameSymbolAsync( solution, field, generatedPropertyName, solution.Options, cancellationToken).ConfigureAwait(false); } } private async Task RenameAsync( Solution solution, IFieldSymbol field, string finalName, Func filter, CancellationToken cancellationToken) { var initialLocations = await Renamer.FindRenameLocationsAsync( solution, field, RenameOptionSet.From(solution), cancellationToken).ConfigureAwait(false); var resolution = await initialLocations.Filter(filter).ResolveConflictsAsync( finalName, nonConflictSymbols: null, cancellationToken).ConfigureAwait(false); Contract.ThrowIfTrue(resolution.ErrorMessage != null); return resolution.NewSolution; } private bool IntersectsWithAny(Location location, ISet constructorLocations) { foreach (var constructor in constructorLocations) { if (location.IntersectsWith(constructor)) return true; } return false; } private ISet GetConstructorLocations(INamedTypeSymbol containingType) => GetConstructorNodes(containingType).Select(n => n.GetLocation()).ToSet(); internal abstract IEnumerable GetConstructorNodes(INamedTypeSymbol containingType); protected async Task AddPropertyAsync(Document document, Solution destinationSolution, IFieldSymbol field, IPropertySymbol property, CancellationToken cancellationToken) { var codeGenerationService = document.GetLanguageService(); var fieldDeclaration = field.DeclaringSyntaxReferences.First(); var options = new CodeGenerationOptions( contextLocation: fieldDeclaration.SyntaxTree.GetLocation(fieldDeclaration.Span), parseOptions: fieldDeclaration.SyntaxTree.Options); var destination = field.ContainingType; var updatedDocument = await codeGenerationService.AddPropertyAsync(destinationSolution, destination, property, options, cancellationToken) .ConfigureAwait(false); updatedDocument = await Formatter.FormatAsync(updatedDocument, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false); updatedDocument = await Simplifier.ReduceAsync(updatedDocument, cancellationToken: cancellationToken).ConfigureAwait(false); return updatedDocument.Project.Solution; } protected IPropertySymbol GenerateProperty( string propertyName, string fieldName, Accessibility accessibility, IFieldSymbol field, INamedTypeSymbol containingSymbol, SyntaxAnnotation annotation, Document document) { var factory = document.GetLanguageService(); var propertySymbol = annotation.AddAnnotationToSymbol(CodeGenerationSymbolFactory.CreatePropertySymbol(containingType: containingSymbol, attributes: ImmutableArray.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.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 Accessibility ComputeAccessibility(Accessibility accessibility, ITypeSymbol type) { var computedAccessibility = accessibility; if (accessibility == Accessibility.NotApplicable || accessibility == Accessibility.Private) { computedAccessibility = Accessibility.Public; } var returnTypeAccessibility = type.DetermineMinimalAccessibility(); return AccessibilityUtilities.Minimum(computedAccessibility, returnTypeAccessibility); } protected 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.Empty, Accessibility.NotApplicable, ImmutableArray.Create(body)); } protected 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.Empty, Accessibility.NotApplicable, ImmutableArray.Create(body)); } private static readonly char[] s_underscoreCharArray = new[] { '_' }; protected string GeneratePropertyName(string fieldName) { // Trim leading underscores var baseName = fieldName.TrimStart(s_underscoreCharArray); // Trim leading "m_" if (baseName.Length >= 2 && baseName[0] == 'm' && baseName[1] == '_') { baseName = baseName.Substring(2); } // 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.Substring(1); } private static readonly CultureInfo EnUSCultureInfo = new CultureInfo("en-US"); private class MyCodeAction : CodeAction.SolutionChangeAction { public MyCodeAction(string title, Func> createChangedSolution) : base(title, createChangedSolution) { } } } }