Skip to content

Commit

Permalink
Remove state type from 'move type' refactoring (#77248)
Browse files Browse the repository at this point in the history
  • Loading branch information
CyrusNajmabadi authored Feb 16, 2025
2 parents 567599f + c127487 commit d862b94
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 144 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@
// 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.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType;

Expand All @@ -19,43 +16,38 @@ internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarati
/// </summary>
private abstract class Editor(
TService service,
State state,
SemanticDocument semanticDocument,
TTypeDeclarationSyntax typeDeclaration,
string fileName,
CancellationToken cancellationToken)
{
protected State State { get; } = state;
protected TService Service { get; } = service;
protected SemanticDocument SemanticDocument { get; } = semanticDocument;
protected TTypeDeclarationSyntax TypeDeclaration { get; } = typeDeclaration;
protected string FileName { get; } = fileName;
protected CancellationToken CancellationToken { get; } = cancellationToken;
protected SemanticDocument SemanticDocument => State.SemanticDocument;

/// <summary>
/// Operations performed by CodeAction.
/// </summary>
public virtual async Task<ImmutableArray<CodeActionOperation>> GetOperationsAsync()
{
var solution = await GetModifiedSolutionAsync().ConfigureAwait(false);

if (solution == null)
{
return [];
}

return [new ApplyChangesOperation(solution)];
return solution == null ? [] : [new ApplyChangesOperation(solution)];
}

/// <summary>
/// Incremental solution edits that correlate to code operations
/// </summary>
public abstract Task<Solution> GetModifiedSolutionAsync();
public abstract Task<Solution?> GetModifiedSolutionAsync();

public static Editor GetEditor(MoveTypeOperationKind operationKind, TService service, State state, string fileName, CancellationToken cancellationToken)
public static Editor GetEditor(MoveTypeOperationKind operationKind, TService service, SemanticDocument document, TTypeDeclarationSyntax typeDeclaration, string fileName, CancellationToken cancellationToken)
=> operationKind switch
{
MoveTypeOperationKind.MoveType => new MoveTypeEditor(service, state, fileName, cancellationToken),
MoveTypeOperationKind.RenameType => new RenameTypeEditor(service, state, fileName, cancellationToken),
MoveTypeOperationKind.RenameFile => new RenameFileEditor(service, state, fileName, cancellationToken),
MoveTypeOperationKind.MoveTypeNamespaceScope => new MoveTypeNamespaceScopeEditor(service, state, fileName, cancellationToken),
MoveTypeOperationKind.MoveType => new MoveTypeEditor(service, document, typeDeclaration, fileName, cancellationToken),
MoveTypeOperationKind.RenameType => new RenameTypeEditor(service, document, typeDeclaration, fileName, cancellationToken),
MoveTypeOperationKind.RenameFile => new RenameFileEditor(service, document, typeDeclaration, fileName, cancellationToken),
MoveTypeOperationKind.MoveTypeNamespaceScope => new MoveTypeNamespaceScopeEditor(service, document, typeDeclaration, fileName, cancellationToken),
_ => throw ExceptionUtilities.UnexpectedValue(operationKind),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,34 @@
// 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.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType;

internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TCompilationUnitSyntax>
{
private sealed class MoveTypeCodeAction : CodeAction
{
private readonly State _state;
private readonly TService _service;
private readonly SemanticDocument _document;
private readonly TTypeDeclarationSyntax _typeDeclaration;
private readonly MoveTypeOperationKind _operationKind;
private readonly string _fileName;

public MoveTypeCodeAction(
TService service,
State state,
SemanticDocument document,
TTypeDeclarationSyntax typeDeclaration,
MoveTypeOperationKind operationKind,
string fileName)
{
_state = state;
_service = service;
_document = document;
_typeDeclaration = typeDeclaration;
_operationKind = operationKind;
_fileName = fileName;
this.Title = CreateDisplayText();
Expand All @@ -39,7 +39,7 @@ private string CreateDisplayText()
=> _operationKind switch
{
MoveTypeOperationKind.MoveType => string.Format(FeaturesResources.Move_type_to_0, _fileName),
MoveTypeOperationKind.RenameType => string.Format(FeaturesResources.Rename_type_to_0, _state.DocumentNameWithoutExtension),
MoveTypeOperationKind.RenameType => string.Format(FeaturesResources.Rename_type_to_0, GetDocumentNameWithoutExtension(_document)),
MoveTypeOperationKind.RenameFile => string.Format(FeaturesResources.Rename_file_to_0, _fileName),
MoveTypeOperationKind.MoveTypeNamespaceScope => string.Empty,
_ => throw ExceptionUtilities.UnexpectedValue(_operationKind),
Expand All @@ -50,7 +50,7 @@ private string CreateDisplayText()
protected override async Task<ImmutableArray<CodeActionOperation>> ComputeOperationsAsync(
IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
{
var editor = Editor.GetEditor(_operationKind, _service, _state, _fileName, cancellationToken);
var editor = Editor.GetEditor(_operationKind, _service, _document, _typeDeclaration, _fileName, cancellationToken);
return await editor.GetOperationsAsync().ConfigureAwait(false);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarati
{
private sealed class MoveTypeEditor(
TService service,
State state,
SemanticDocument document,
TTypeDeclarationSyntax typeDeclaration,
string fileName,
CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken)
CancellationToken cancellationToken) : Editor(service, document, typeDeclaration, fileName, cancellationToken)
{
/// <summary>
/// Given a document and a type contained in it, moves the type
Expand All @@ -42,10 +43,10 @@ private sealed class MoveTypeEditor(
/// 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()
public override async Task<Solution?> GetModifiedSolutionAsync()
{
// Fork, update and add as new document.
var projectToBeUpdated = SemanticDocument.Document.Project;
var projectToBeUpdated = SemanticDocument.Project;
var newDocumentId = DocumentId.CreateNewId(projectToBeUpdated.Id, FileName);

// We do this process in the following steps:
Expand Down Expand Up @@ -142,7 +143,7 @@ private async Task<Document> AddNewDocumentWithSingleTypeDeclarationAsync(Docume
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();
var leadingTrivia = this.TypeDeclaration.GetLeadingTrivia().ToSet();
foreach (var directive in correspondingDirectives)
{
if (leadingTrivia.Contains(directive.ParentTrivia))
Expand Down Expand Up @@ -185,7 +186,7 @@ void AddCorrespondingDirectives(SyntaxNode member, HashSet<SyntaxNode> directive

private void RemoveLeadingBlankLinesFromMovedType(DocumentEditor documentEditor)
{
documentEditor.ReplaceNode(State.TypeNode,
documentEditor.ReplaceNode(this.TypeDeclaration,
(currentNode, generator) =>
{
var currentTypeNode = (TTypeDeclarationSyntax)currentNode;
Expand Down Expand Up @@ -240,7 +241,7 @@ private async Task<Solution> RemoveTypeFromSourceDocumentAsync(Document sourceDo

// Now cleanup and remove the type we're moving to the new file.
RemoveLeadingBlankLinesFromMovedType(documentEditor);
documentEditor.RemoveNode(State.TypeNode, SyntaxRemoveOptions.KeepUnbalancedDirectives);
documentEditor.RemoveNode(this.TypeDeclaration, SyntaxRemoveOptions.KeepUnbalancedDirectives);

var updatedDocument = documentEditor.GetChangedDocument();
updatedDocument = await AddFileBannerHelpers.CopyBannerAsync(updatedDocument, sourceDocument.FilePath, sourceDocument, this.CancellationToken).ConfigureAwait(false);
Expand All @@ -260,12 +261,12 @@ private ISet<SyntaxNode> GetMembersToRemove(SyntaxNode root)
var spine = new HashSet<SyntaxNode>();

// collect the parent chain of declarations to keep.
spine.AddRange(State.TypeNode.GetAncestors());
spine.AddRange(this.TypeDeclaration.GetAncestors());

// get potential namespace, types and members to remove.
var removableCandidates = root
.DescendantNodes(spine.Contains)
.Where(n => FilterToTopLevelMembers(n, State.TypeNode)).ToSet();
.Where(n => FilterToTopLevelMembers(n, this.TypeDeclaration)).ToSet();

// diff candidates with items we want to keep.
removableCandidates.ExceptWith(spine);
Expand Down Expand Up @@ -304,12 +305,12 @@ private void AddPartialModifiersToTypeChain(
bool removeTypeInheritance,
bool removePrimaryConstructor)
{
var semanticFacts = State.SemanticDocument.Document.GetRequiredLanguageService<ISemanticFactsService>();
var typeChain = State.TypeNode.Ancestors().OfType<TTypeDeclarationSyntax>();
var semanticFacts = SemanticDocument.GetRequiredLanguageService<ISemanticFactsService>();
var typeChain = this.TypeDeclaration.Ancestors().OfType<TTypeDeclarationSyntax>();

foreach (var node in typeChain)
{
var symbol = (INamedTypeSymbol?)State.SemanticDocument.SemanticModel.GetDeclaredSymbol(node, CancellationToken);
var symbol = (INamedTypeSymbol)SemanticDocument.SemanticModel.GetRequiredDeclaredSymbol(node, CancellationToken);
Contract.ThrowIfNull(symbol);
if (!semanticFacts.IsPartial(symbol, CancellationToken))
{
Expand Down Expand Up @@ -338,8 +339,8 @@ private void AddPartialModifiersToTypeChain(
private TTypeDeclarationSyntax RemoveLeadingBlankLines(
TTypeDeclarationSyntax currentTypeNode)
{
var syntaxFacts = State.SemanticDocument.Document.GetRequiredLanguageService<ISyntaxFactsService>();
var bannerService = State.SemanticDocument.Document.GetRequiredLanguageService<IFileBannerFactsService>();
var syntaxFacts = SemanticDocument.GetRequiredLanguageService<ISyntaxFactsService>();
var bannerService = SemanticDocument.GetRequiredLanguageService<IFileBannerFactsService>();

var withoutBlankLines = bannerService.GetNodeWithoutLeadingBlankLines(currentTypeNode);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,43 +20,40 @@ internal abstract partial class AbstractMoveTypeService<
/// it will evaluate if the namespace scope needs to be closed and reopened to create a new scope.
/// </summary>
private sealed class MoveTypeNamespaceScopeEditor(
TService service, State state, string fileName, CancellationToken cancellationToken)
: Editor(service, state, fileName, cancellationToken)
TService service,
SemanticDocument document,
TTypeDeclarationSyntax typeDeclaration,
string fileName,
CancellationToken cancellationToken)
: Editor(service, document, typeDeclaration, fileName, cancellationToken)
{
public override async Task<Solution?> GetModifiedSolutionAsync()
{
var node = State.TypeNode;
var documentToEdit = State.SemanticDocument.Document;

if (node.Parent is not TNamespaceDeclarationSyntax namespaceDeclaration)
return null;

return await GetNamespaceScopeChangedSolutionAsync(namespaceDeclaration, node, documentToEdit, CancellationToken).ConfigureAwait(false);
return TypeDeclaration.Parent is TNamespaceDeclarationSyntax namespaceDeclaration
? await GetNamespaceScopeChangedSolutionAsync(namespaceDeclaration).ConfigureAwait(false)
: null;
}

private static async Task<Solution?> GetNamespaceScopeChangedSolutionAsync(
TNamespaceDeclarationSyntax namespaceDeclaration,
TTypeDeclarationSyntax typeToMove,
Document documentToEdit,
CancellationToken cancellationToken)
private async Task<Solution?> GetNamespaceScopeChangedSolutionAsync(
TNamespaceDeclarationSyntax namespaceDeclaration)
{
var syntaxFactsService = documentToEdit.GetRequiredLanguageService<ISyntaxFactsService>();
var syntaxFactsService = SemanticDocument.GetRequiredLanguageService<ISyntaxFactsService>();
var childNodes = syntaxFactsService.GetMembersOfBaseNamespaceDeclaration(namespaceDeclaration);

if (childNodes.Count <= 1)
return null;

var editor = await DocumentEditor.CreateAsync(documentToEdit, cancellationToken).ConfigureAwait(false);
editor.RemoveNode(typeToMove, SyntaxRemoveOptions.KeepNoTrivia);
var editor = await DocumentEditor.CreateAsync(SemanticDocument.Document, this.CancellationToken).ConfigureAwait(false);
editor.RemoveNode(this.TypeDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
var generator = editor.Generator;

var index = childNodes.IndexOf(typeToMove);
var index = childNodes.IndexOf(this.TypeDeclaration);

var itemsBefore = childNodes.Take(index).ToImmutableArray();
var itemsAfter = childNodes.Skip(index + 1).ToImmutableArray();

var name = syntaxFactsService.GetDisplayName(namespaceDeclaration, DisplayNameOptions.IncludeNamespaces);
var newNamespaceDeclaration = generator.NamespaceDeclaration(name, WithElasticTrivia(typeToMove)).WithAdditionalAnnotations(NamespaceScopeMovedAnnotation);
var newNamespaceDeclaration = generator.NamespaceDeclaration(name, WithElasticTrivia(this.TypeDeclaration)).WithAdditionalAnnotations(NamespaceScopeMovedAnnotation);

if (itemsBefore.Any() && itemsAfter.Any())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,25 @@ namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType;

internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TCompilationUnitSyntax>
{
private sealed class RenameFileEditor(TService service, State state, string fileName, CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken)
/// <summary>
/// Renames the file to match the type contained in it.
/// </summary>
private sealed class RenameFileEditor(
TService service,
SemanticDocument document,
TTypeDeclarationSyntax typeDeclaration,
string fileName,
CancellationToken cancellationToken) : Editor(service, document, typeDeclaration, fileName, cancellationToken)
{
/// <summary>
/// Renames the file to match the type contained in it.
/// </summary>
public override async Task<ImmutableArray<CodeActionOperation>> GetOperationsAsync()
{
var newSolution = await GetModifiedSolutionAsync().ConfigureAwait(false);
Contract.ThrowIfNull(newSolution);
return [new ApplyChangesOperation(newSolution)];
}

public override Task<Solution> GetModifiedSolutionAsync()
{
var modifiedSolution = SemanticDocument.Project.Solution
.WithDocumentName(SemanticDocument.Document.Id, FileName);

return Task.FromResult(modifiedSolution);
}
public override Task<Solution?> GetModifiedSolutionAsync()
=> Task.FromResult<Solution?>(
SemanticDocument.Project.Solution.WithDocumentName(SemanticDocument.Document.Id, FileName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,32 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Rename;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CodeRefactorings.MoveType;

internal abstract partial class AbstractMoveTypeService<TService, TTypeDeclarationSyntax, TNamespaceDeclarationSyntax, TCompilationUnitSyntax>
{
private sealed class RenameTypeEditor(TService service, State state, string fileName, CancellationToken cancellationToken) : Editor(service, state, fileName, cancellationToken)
private sealed class RenameTypeEditor(
TService service,
SemanticDocument document,
TTypeDeclarationSyntax typeDeclaration,
string fileName,
CancellationToken cancellationToken) : Editor(service, document, typeDeclaration, fileName, cancellationToken)
{

/// <summary>
/// Renames a type to match its containing file name.
/// </summary>
public override async Task<Solution> GetModifiedSolutionAsync()
public override async Task<Solution?> GetModifiedSolutionAsync()
{
// TODO: detect conflicts ahead of time and open an inline rename session if any exists.
// this will bring up dashboard with conflicts and will allow the user to resolve them.
// if no such conflicts exist, proceed with RenameSymbolAsync.
var solution = SemanticDocument.Document.Project.Solution;
var symbol = State.SemanticDocument.SemanticModel.GetDeclaredSymbol(State.TypeNode, CancellationToken);
var solution = SemanticDocument.Project.Solution;
var symbol = SemanticDocument.SemanticModel.GetRequiredDeclaredSymbol(this.TypeDeclaration, CancellationToken);
return await Renamer.RenameSymbolAsync(solution, symbol, new SymbolRenameOptions(), FileName, CancellationToken).ConfigureAwait(false);
}
}
Expand Down
Loading

0 comments on commit d862b94

Please sign in to comment.