|
// 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;
using Microsoft.CodeAnalysis.AddFileBanner;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.RemoveUnnecessaryImports;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType;
internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TCompilationUnitSyntax>
{
private sealed class MoveTypeEditor(
TService service,
State state,
string fileName,
CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken)
{
/// <summary>
/// Given a document and a type contained in it, moves the type
/// out to its own document. The new document's name typically
/// is the type name, or is at least based on the type name.
/// </summary>
/// <remarks>
/// The algorithm for this, is as follows:
/// 1. Fork the original document that contains the type to be moved.
/// 2. Keep the type, required namespace containers and using statements.
/// remove everything else from the forked document.
/// 3. Add this forked document to the solution.
/// 4. Finally, update the original document and remove the type from it.
/// </remarks>
public override async Task<Solution> GetModifiedSolutionAsync()
{
// Fork, update and add as new document.
var projectToBeUpdated = SemanticDocument.Document.Project;
var newDocumentId = DocumentId.CreateNewId(projectToBeUpdated.Id, FileName);
// We do this process in the following steps:
//
// 1. Produce the new document, with the moved type, with all the same imports as the original file.
// 2. Remove the original type from the first document, not touching the imports in it. This is
// necessary to prevent duplicate symbol errors (and other compiler issues) as we process imports.
// 3. Now that the type has been moved to the new file, remove the unnecessary imports from the new
// file. This will also tell us which imports are necessary in the new file.
// 4. Now go back to the original file and remove any unnecessary imports *if* they are in the new file.
// these imports only were needed for the moved type, and so they shouldn't stay in the original
// file.
var documentWithMovedType = await AddNewDocumentWithSingleTypeDeclarationAsync(newDocumentId).ConfigureAwait(false);
var solutionWithNewDocument = documentWithMovedType.Project.Solution;
// Get the original source document again, from the latest forked solution.
var sourceDocument = solutionWithNewDocument.GetRequiredDocument(SemanticDocument.Document.Id);
// update source document to add partial modifiers to type chain
// and/or remove type declaration from original source document.
var solutionWithBothDocumentsUpdated = await RemoveTypeFromSourceDocumentAsync(sourceDocument).ConfigureAwait(false);
return await RemoveUnnecessaryImportsAsync(solutionWithBothDocumentsUpdated, sourceDocument.Id, documentWithMovedType.Id).ConfigureAwait(false);
}
private async Task<Solution> RemoveUnnecessaryImportsAsync(
Solution solution, DocumentId sourceDocumentId, DocumentId documentWithMovedTypeId)
{
var documentWithMovedType = solution.GetRequiredDocument(documentWithMovedTypeId);
var syntaxFacts = documentWithMovedType.GetRequiredLanguageService<ISyntaxFactsService>();
var removeUnnecessaryImports = documentWithMovedType.GetRequiredLanguageService<IRemoveUnnecessaryImportsService>();
// Remove all unnecessary imports from the new document we've created.
documentWithMovedType = await removeUnnecessaryImports.RemoveUnnecessaryImportsAsync(documentWithMovedType, CancellationToken).ConfigureAwait(false);
solution = solution.WithDocumentSyntaxRoot(
documentWithMovedTypeId, await documentWithMovedType.GetRequiredSyntaxRootAsync(CancellationToken).ConfigureAwait(false));
// See which imports we kept around.
var rootWithMovedType = await documentWithMovedType.GetRequiredSyntaxRootAsync(CancellationToken).ConfigureAwait(false);
var movedImports = rootWithMovedType.DescendantNodes()
.Where(syntaxFacts.IsUsingOrExternOrImport)
.ToImmutableArray();
// Now remove any unnecessary imports from the original doc that moved to the new doc.
var sourceDocument = solution.GetRequiredDocument(sourceDocumentId);
sourceDocument = await removeUnnecessaryImports.RemoveUnnecessaryImportsAsync(
sourceDocument,
n => movedImports.Contains(i => syntaxFacts.AreEquivalent(i, n)),
CancellationToken).ConfigureAwait(false);
return solution.WithDocumentSyntaxRoot(
sourceDocumentId, await sourceDocument.GetRequiredSyntaxRootAsync(CancellationToken).ConfigureAwait(false));
}
/// <summary>
/// Forks the source document, keeps required type, namespace containers
/// and adds it the solution.
/// </summary>
/// <param name="newDocumentId">id for the new document to be added</param>
private async Task<Document> AddNewDocumentWithSingleTypeDeclarationAsync(DocumentId newDocumentId)
{
var document = SemanticDocument.Document;
Debug.Assert(document.Name != FileName, $"New document name is same as old document name:{FileName}");
var root = SemanticDocument.Root;
var projectToBeUpdated = document.Project;
var documentEditor = await DocumentEditor.CreateAsync(document, CancellationToken).ConfigureAwait(false);
// Make the type chain above this new type partial. Also, remove any
// attributes from the containing partial types. We don't want to create
// duplicate attributes on things.
AddPartialModifiersToTypeChain(
documentEditor, removeAttributesAndComments: true, removeTypeInheritance: true, removePrimaryConstructor: true);
// Keep track of any associated directives on any of the nodes we're removing. If those directives are then
// contained in the leading trivia of the type we're moving, we'll remove them from there as well.
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
using var _ = PooledHashSet<SyntaxNode>.GetInstance(out var correspondingDirectives);
// remove things that are not being moved, from the forked document.
var membersToRemove = GetMembersToRemove(root);
foreach (var member in membersToRemove)
{
AddCorrespondingDirectives(member, correspondingDirectives);
documentEditor.RemoveNode(member, SyntaxRemoveOptions.KeepNoTrivia);
}
// Remove attributes from the root node as well, since those will apply as AttributeTarget.Assembly and
// don't need to be specified multiple times
documentEditor.RemoveAllAttributes(root);
// Now remove any leading directives on the type-node that actually correspond to prior nodes we removed.
var leadingTrivia = State.TypeNode.GetLeadingTrivia().ToSet();
foreach (var directive in correspondingDirectives)
{
if (leadingTrivia.Contains(directive.ParentTrivia))
documentEditor.RemoveNode(directive);
}
RemoveLeadingBlankLinesFromMovedType(documentEditor);
var modifiedRoot = documentEditor.GetChangedRoot();
modifiedRoot = await AddFinalNewLineIfDesiredAsync(document, modifiedRoot).ConfigureAwait(false);
// add an empty document to solution, so that we'll have options from the right context.
var solutionWithNewDocument = projectToBeUpdated.Solution.AddDocument(
newDocumentId, FileName, text: string.Empty, folders: document.Folders);
// update the text for the new document
solutionWithNewDocument = solutionWithNewDocument.WithDocumentSyntaxRoot(newDocumentId, modifiedRoot, PreservationMode.PreserveIdentity);
// get the updated document, give it the minimal set of imports that the type
// inside it needs.
var newDocument = solutionWithNewDocument.GetRequiredDocument(newDocumentId);
var newDocumentWithUpdatedBanner = await AddFileBannerHelpers.CopyBannerAsync(
newDocument, FileName, document, this.CancellationToken).ConfigureAwait(false);
return newDocumentWithUpdatedBanner;
void AddCorrespondingDirectives(SyntaxNode member, HashSet<SyntaxNode> directives)
{
foreach (var trivia in member.GetLeadingTrivia())
{
if (trivia.IsDirective)
{
directives.AddIfNotNull(syntaxFacts.GetMatchingDirective(trivia.GetStructure()!, this.CancellationToken));
foreach (var directive in syntaxFacts.GetMatchingConditionalDirectives(trivia.GetStructure()!, this.CancellationToken))
directives.Add(directive);
}
}
}
}
private void RemoveLeadingBlankLinesFromMovedType(DocumentEditor documentEditor)
{
documentEditor.ReplaceNode(State.TypeNode,
(currentNode, generator) =>
{
var currentTypeNode = (TTypeDeclarationSyntax)currentNode;
// Trim leading blank lines from the type so we don't have an
// excessive number of them.
return RemoveLeadingBlankLines(currentTypeNode);
});
}
/// <summary>
/// Add a trailing newline if we don't already have one if that's what the user's
/// preference is.
/// </summary>
private async Task<SyntaxNode> AddFinalNewLineIfDesiredAsync(Document document, SyntaxNode modifiedRoot)
{
var documentFormattingOptions = await document.GetDocumentFormattingOptionsAsync(CancellationToken).ConfigureAwait(false);
var insertFinalNewLine = documentFormattingOptions.InsertFinalNewLine;
if (insertFinalNewLine)
{
var endOfFileToken = ((ICompilationUnitSyntax)modifiedRoot).EndOfFileToken;
var previousToken = endOfFileToken.GetPreviousToken(includeZeroWidth: true, includeSkipped: true);
var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
if (endOfFileToken.LeadingTrivia.IsEmpty() &&
!previousToken.TrailingTrivia.Any(syntaxFacts.IsEndOfLineTrivia))
{
var lineFormattingOptions = await document.GetLineFormattingOptionsAsync(CancellationToken).ConfigureAwait(false);
var generator = document.GetRequiredLanguageService<SyntaxGeneratorInternal>();
var endOfLine = generator.EndOfLine(lineFormattingOptions.NewLine);
return modifiedRoot.ReplaceToken(
previousToken, previousToken.WithAppendedTrailingTrivia(endOfLine));
}
}
return modifiedRoot;
}
/// <summary>
/// update the original document and remove the type that was moved.
/// perform other fix ups as necessary.
/// </summary>
/// <returns>an updated solution with the original document fixed up as appropriate.</returns>
private async Task<Solution> RemoveTypeFromSourceDocumentAsync(Document sourceDocument)
{
var documentEditor = await DocumentEditor.CreateAsync(sourceDocument, CancellationToken).ConfigureAwait(false);
// Make the type chain above the type we're moving 'partial'. However, keep all the attributes on these
// types as theses are the original attributes and we don't want to mess with them.
AddPartialModifiersToTypeChain(documentEditor,
removeAttributesAndComments: false, removeTypeInheritance: false, removePrimaryConstructor: false);
// Now cleanup and remove the type we're moving to the new file.
RemoveLeadingBlankLinesFromMovedType(documentEditor);
documentEditor.RemoveNode(State.TypeNode, SyntaxRemoveOptions.KeepUnbalancedDirectives);
var updatedDocument = documentEditor.GetChangedDocument();
updatedDocument = await AddFileBannerHelpers.CopyBannerAsync(updatedDocument, sourceDocument.FilePath, sourceDocument, this.CancellationToken).ConfigureAwait(false);
return updatedDocument.Project.Solution;
}
/// <summary>
/// Traverses the syntax tree of the forked document and
/// collects a list of nodes that are not being moved.
/// This list of nodes are then removed from the forked copy.
/// </summary>
/// <param name="root">root, of the syntax tree of forked document</param>
/// <returns>list of syntax nodes, to be removed from the forked copy.</returns>
private ISet<SyntaxNode> GetMembersToRemove(SyntaxNode root)
{
var spine = new HashSet<SyntaxNode>();
// collect the parent chain of declarations to keep.
spine.AddRange(State.TypeNode.GetAncestors());
// get potential namespace, types and members to remove.
var removableCandidates = root
.DescendantNodes(spine.Contains)
.Where(n => FilterToTopLevelMembers(n, State.TypeNode)).ToSet();
// diff candidates with items we want to keep.
removableCandidates.ExceptWith(spine);
#if DEBUG
// None of the nodes we're removing should also have any of their parent
// nodes removed. If that happened we could get a crash by first trying to remove
// the parent, then trying to remove the child.
foreach (var node in removableCandidates)
{
foreach (var ancestor in node.GetAncestors())
{
Debug.Assert(!removableCandidates.Contains(ancestor));
}
}
#endif
return removableCandidates;
}
private bool FilterToTopLevelMembers(SyntaxNode node, SyntaxNode typeNode)
{
// We never filter out the actual node we're trying to keep around.
if (node == typeNode)
return false;
return node is TTypeDeclarationSyntax or TNamespaceDeclarationSyntax || this.Service.IsMemberDeclaration(node);
}
/// <summary>
/// if a nested type is being moved, this ensures its containing type is partial.
/// </summary>
private void AddPartialModifiersToTypeChain(
DocumentEditor documentEditor,
bool removeAttributesAndComments,
bool removeTypeInheritance,
bool removePrimaryConstructor)
{
var semanticFacts = State.SemanticDocument.Document.GetRequiredLanguageService<ISemanticFactsService>();
var typeChain = State.TypeNode.Ancestors().OfType<TTypeDeclarationSyntax>();
foreach (var node in typeChain)
{
var symbol = (INamedTypeSymbol?)State.SemanticDocument.SemanticModel.GetDeclaredSymbol(node, CancellationToken);
Contract.ThrowIfNull(symbol);
if (!semanticFacts.IsPartial(symbol, CancellationToken))
{
documentEditor.SetModifiers(node,
documentEditor.Generator.GetModifiers(node) | DeclarationModifiers.Partial);
}
if (removeAttributesAndComments)
{
documentEditor.RemoveAllAttributes(node);
documentEditor.RemoveAllComments(node);
}
if (removeTypeInheritance)
{
documentEditor.RemoveAllTypeInheritance(node);
}
if (removePrimaryConstructor)
{
documentEditor.RemovePrimaryConstructor(node);
}
}
}
private TTypeDeclarationSyntax RemoveLeadingBlankLines(
TTypeDeclarationSyntax currentTypeNode)
{
var syntaxFacts = State.SemanticDocument.Document.GetRequiredLanguageService<ISyntaxFactsService>();
var bannerService = State.SemanticDocument.Document.GetRequiredLanguageService<IFileBannerFactsService>();
var withoutBlankLines = bannerService.GetNodeWithoutLeadingBlankLines(currentTypeNode);
// Add an elastic marker so the formatter can add any blank lines it thinks are
// important to have (i.e. after a block of usings/imports).
return withoutBlankLines.WithPrependedLeadingTrivia(syntaxFacts.ElasticMarker);
}
}
}
|