File: CodeRefactorings\CodeRefactoringServiceTest.cs
Web Access
Project: src\src\EditorFeatures\Test\Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.EditorFeatures.UnitTests)
// 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.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Extensions;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Test.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Editor.UnitTests.CodeRefactoringService;
 
[UseExportProvider]
public class CodeRefactoringServiceTest
{
    [Fact]
    public async Task TestExceptionInComputeRefactorings()
        => await VerifyRefactoringDisabledAsync<ErrorCases.ExceptionInCodeActions>();
 
    [Fact]
    public async Task TestExceptionInComputeRefactoringsAsync()
        => await VerifyRefactoringDisabledAsync<ErrorCases.ExceptionInComputeRefactoringsAsync>();
 
    [Fact]
    public async Task TestProjectRefactoringAsync()
    {
        var code = @"
    a
";
 
        using var workspace = TestWorkspace.CreateCSharp(code, composition: FeaturesTestCompositions.Features);
        var refactoringService = workspace.GetService<ICodeRefactoringService>();
 
        var reference = new StubAnalyzerReference();
        var project = workspace.CurrentSolution.Projects.Single().AddAnalyzerReference(reference);
        var document = project.Documents.Single();
        var refactorings = await refactoringService.GetRefactoringsAsync(document, TextSpan.FromBounds(0, 0), CancellationToken.None);
 
        var stubRefactoringAction = refactorings.Single(refactoring => refactoring.CodeActions.FirstOrDefault().action?.Title == nameof(StubRefactoring));
        Assert.True(stubRefactoringAction is object);
    }
 
    [ExportCodeRefactoringProvider(InternalLanguageNames.TypeScript, Name = "TypeScript CodeRefactoring Provider"), Shared, PartNotDiscoverable]
    internal sealed class TypeScriptCodeRefactoringProvider : CodeRefactoringProvider
    {
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public TypeScriptCodeRefactoringProvider()
        {
        }
 
        public override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
        {
            context.RegisterRefactoring(CodeAction.Create($"Blocking=false", _ => Task.FromResult<Document>(null)));
 
            return Task.CompletedTask;
        }
    }
 
    [Fact]
    public async Task TestTypeScriptRefactorings()
    {
        var composition = FeaturesTestCompositions.Features.AddParts(typeof(TypeScriptCodeRefactoringProvider));
 
        using var workspace = TestWorkspace.Create(@"
<Workspace>
    <Project Language=""TypeScript"">
        <Document FilePath=""Test"">abc</Document>
    </Project>
</Workspace>", composition: composition);
 
        var refactoringService = workspace.GetService<ICodeRefactoringService>();
 
        var document = workspace.CurrentSolution.Projects.Single().Documents.Single();
        var refactorings = await refactoringService.GetRefactoringsAsync(document, TextSpan.FromBounds(0, 0), CancellationToken.None);
        Assert.Equal($"Blocking=false", refactorings.Single().CodeActions.Single().action.Title);
    }
 
    private static async Task VerifyRefactoringDisabledAsync<T>()
        where T : CodeRefactoringProvider
    {
        using var workspace = TestWorkspace.CreateCSharp(@"class Program {}",
            composition: EditorTestCompositions.EditorFeatures.AddParts(typeof(T)));
 
        var errorReportingService = (TestErrorReportingService)workspace.Services.GetRequiredService<IErrorReportingService>();
 
        var errorReported = false;
        errorReportingService.OnError = message => errorReported = true;
 
        var refactoringService = workspace.GetService<ICodeRefactoringService>();
        var codeRefactoring = workspace.ExportProvider.GetExportedValues<CodeRefactoringProvider>().OfType<T>().Single();
 
        var project = workspace.CurrentSolution.Projects.Single();
        var document = project.Documents.Single();
        var extensionManager = (EditorLayerExtensionManager.ExtensionManager)document.Project.Solution.Services.GetRequiredService<IExtensionManager>();
        var result = await refactoringService.GetRefactoringsAsync(document, TextSpan.FromBounds(0, 0), CancellationToken.None);
        Assert.True(extensionManager.IsDisabled(codeRefactoring));
        Assert.False(extensionManager.IsIgnored(codeRefactoring));
 
        Assert.True(errorReported);
    }
 
    internal class StubRefactoring : CodeRefactoringProvider
    {
        public override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
        {
            context.RegisterRefactoring(CodeAction.Create(
                nameof(StubRefactoring),
                cancellationToken => Task.FromResult(context.Document),
                equivalenceKey: nameof(StubRefactoring)));
 
            return Task.CompletedTask;
        }
    }
 
    private class StubAnalyzerReference : AnalyzerReference, ICodeRefactoringProviderFactory
    {
        private readonly ImmutableArray<CodeRefactoringProvider> _refactorings;
 
        public StubAnalyzerReference() : this(new StubRefactoring()) { }
 
        public StubAnalyzerReference(params CodeRefactoringProvider[] codeRefactorings)
            => _refactorings = codeRefactorings.ToImmutableArray();
 
        public override string Display => nameof(StubAnalyzerReference);
 
        public override string FullPath => string.Empty;
 
        public override object Id => nameof(StubAnalyzerReference);
 
        public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzers(string language)
            => ImmutableArray<DiagnosticAnalyzer>.Empty;
 
        public override ImmutableArray<DiagnosticAnalyzer> GetAnalyzersForAllLanguages()
            => ImmutableArray<DiagnosticAnalyzer>.Empty;
 
        public ImmutableArray<CodeRefactoringProvider> GetRefactorings()
            => _refactorings;
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/62877")]
    public async Task TestAdditionalDocumentRefactoringAsync()
    {
        using var workspace = TestWorkspace.CreateCSharp("", composition: FeaturesTestCompositions.Features);
        var refactoringService = workspace.GetService<ICodeRefactoringService>();
 
        var refactoring1 = new NonSourceFileRefactoringWithDocumentKindsAndExtensions();
        var refactoring2 = new NonSourceFileRefactoringWithDocumentKinds();
        var refactoring3 = new NonSourceFileRefactoringWithDocumentExtensions();
        var refactoring4 = new NonSourceFileRefactoringWithoutDocumentKindsAndExtensions();
        var reference = new StubAnalyzerReference(refactoring1, refactoring2, refactoring3, refactoring4);
        var project = workspace.CurrentSolution.Projects.Single()
            .AddAnalyzerReference(reference)
            .AddAdditionalDocument("test.txt", "", filePath: "test.txt").Project
            .AddAdditionalDocument("test.log", "", filePath: "test.log").Project;
 
        // Verify available refactorings for .txt additional document
        var txtAdditionalDocument = project.AdditionalDocuments.Single(t => t.Name == "test.txt");
        var txtRefactorings = await refactoringService.GetRefactoringsAsync(txtAdditionalDocument, TextSpan.FromBounds(0, 0), CancellationToken.None);
        Assert.Equal(2, txtRefactorings.Length);
        var txtRefactoringTitles = txtRefactorings.Select(s => s.CodeActions.Single().action.Title).ToImmutableArray();
        Assert.Contains(refactoring1.Title, txtRefactoringTitles);
        Assert.Contains(refactoring2.Title, txtRefactoringTitles);
 
        // Verify code refactoring application
        var codeAction = txtRefactorings.Single(s => s.CodeActions.Single().action.Title == refactoring1.Title).CodeActions.Single().action;
        var solution = await codeAction.GetChangedSolutionInternalAsync(project.Solution, CodeAnalysisProgress.None);
        var changedtxtDocument = solution.Projects.Single().AdditionalDocuments.Single(t => t.Id == txtAdditionalDocument.Id);
        Assert.Empty(txtAdditionalDocument.GetTextSynchronously(CancellationToken.None).ToString());
        Assert.Equal(refactoring1.Title, changedtxtDocument.GetTextSynchronously(CancellationToken.None).ToString());
 
        // Verify available refactorings for .log additional document
        var logAdditionalDocument = project.AdditionalDocuments.Single(t => t.Name == "test.log");
        var logRefactorings = await refactoringService.GetRefactoringsAsync(logAdditionalDocument, TextSpan.FromBounds(0, 0), CancellationToken.None);
        var logRefactoring = Assert.Single(logRefactorings);
        var logRefactoringTitle = logRefactoring.CodeActions.Single().action.Title;
        Assert.Equal(refactoring2.Title, logRefactoringTitle);
    }
 
    [Fact]
    public async Task TestAnalyzerConfigDocumentRefactoringAsync()
    {
        using var workspace = TestWorkspace.CreateCSharp("", composition: FeaturesTestCompositions.Features);
        var refactoringService = workspace.GetService<ICodeRefactoringService>();
 
        var refactoring1 = new NonSourceFileRefactoringWithDocumentKindsAndExtensions();
        var refactoring2 = new NonSourceFileRefactoringWithDocumentKinds();
        var refactoring3 = new NonSourceFileRefactoringWithDocumentExtensions();
        var refactoring4 = new NonSourceFileRefactoringWithoutDocumentKindsAndExtensions();
        var reference = new StubAnalyzerReference(refactoring1, refactoring2, refactoring3, refactoring4);
        var project = workspace.CurrentSolution.Projects.Single()
            .AddAnalyzerReference(reference)
            .AddAnalyzerConfigDocument(".editorconfig", SourceText.From(""), filePath: "c:\\.editorconfig").Project
            .AddAnalyzerConfigDocument(".globalconfig", SourceText.From("is_global = true"), filePath: "c:\\.globalconfig").Project;
 
        // Verify available refactorings for .editorconfig document
        var editorConfig = project.AnalyzerConfigDocuments.Single(t => t.Name == ".editorconfig");
        var editorConfigRefactorings = await refactoringService.GetRefactoringsAsync(editorConfig, TextSpan.FromBounds(0, 0), CancellationToken.None);
        Assert.Equal(2, editorConfigRefactorings.Length);
        var editorConfigRefactoringTitles = editorConfigRefactorings.Select(s => s.CodeActions.Single().action.Title).ToImmutableArray();
        Assert.Contains(refactoring1.Title, editorConfigRefactoringTitles);
        Assert.Contains(refactoring2.Title, editorConfigRefactoringTitles);
 
        // Verify code refactoring application
        var codeAction = editorConfigRefactorings.Single(s => s.CodeActions.Single().action.Title == refactoring1.Title).CodeActions.Single().action;
        var solution = await codeAction.GetChangedSolutionInternalAsync(project.Solution, CodeAnalysisProgress.None);
        var changedEditorConfig = solution.Projects.Single().AnalyzerConfigDocuments.Single(t => t.Id == editorConfig.Id);
        Assert.Empty(editorConfig.GetTextSynchronously(CancellationToken.None).ToString());
        Assert.Equal(refactoring1.Title, changedEditorConfig.GetTextSynchronously(CancellationToken.None).ToString());
 
        // Verify available refactorings for .globalconfig document
        var globalConfig = project.AnalyzerConfigDocuments.Single(t => t.Name == ".globalconfig");
        var globalConfigRefactorings = await refactoringService.GetRefactoringsAsync(globalConfig, TextSpan.FromBounds(0, 0), CancellationToken.None);
        var globalConfigRefactoring = Assert.Single(globalConfigRefactorings);
        var globalConfigRefactoringTitle = globalConfigRefactoring.CodeActions.Single().action.Title;
        Assert.Equal(refactoring2.Title, globalConfigRefactoringTitle);
    }
 
    internal abstract class AbstractNonSourceFileRefactoring : CodeRefactoringProvider
    {
        public string Title { get; }
 
        protected AbstractNonSourceFileRefactoring(string title)
            => Title = title;
 
        public override Task ComputeRefactoringsAsync(CodeRefactoringContext context)
        {
            context.RegisterRefactoring(CodeAction.Create(Title,
                createChangedSolution: async ct =>
                {
                    var document = context.TextDocument;
                    var text = await document.GetTextAsync(ct).ConfigureAwait(false);
                    var newText = SourceText.From(text.ToString() + Title);
                    if (document.Kind == TextDocumentKind.AdditionalDocument)
                        return document.Project.Solution.WithAdditionalDocumentText(document.Id, newText);
                    return document.Project.Solution.WithAnalyzerConfigDocumentText(document.Id, newText);
                }));
 
            return Task.CompletedTask;
        }
    }
 
#pragma warning disable RS0034 // Exported parts should be marked with 'ImportingConstructorAttribute'
    [ExportCodeRefactoringProvider(
        LanguageNames.CSharp,
        DocumentKinds = [nameof(TextDocumentKind.AdditionalDocument), nameof(TextDocumentKind.AnalyzerConfigDocument)],
        DocumentExtensions = [".txt", ".editorconfig"])]
    [Shared]
    internal sealed class NonSourceFileRefactoringWithDocumentKindsAndExtensions : AbstractNonSourceFileRefactoring
    {
        public NonSourceFileRefactoringWithDocumentKindsAndExtensions() : base(nameof(NonSourceFileRefactoringWithDocumentKindsAndExtensions)) { }
    }
 
    [ExportCodeRefactoringProvider(
        LanguageNames.CSharp,
        DocumentKinds = [nameof(TextDocumentKind.AdditionalDocument), nameof(TextDocumentKind.AnalyzerConfigDocument)])]
    [Shared]
    internal sealed class NonSourceFileRefactoringWithDocumentKinds : AbstractNonSourceFileRefactoring
    {
        public NonSourceFileRefactoringWithDocumentKinds() : base(nameof(NonSourceFileRefactoringWithDocumentKinds)) { }
    }
 
    [ExportCodeRefactoringProvider(
        LanguageNames.CSharp,
        DocumentExtensions = [".txt", ".editorconfig"])]
    [Shared]
    internal sealed class NonSourceFileRefactoringWithDocumentExtensions : AbstractNonSourceFileRefactoring
    {
        public NonSourceFileRefactoringWithDocumentExtensions() : base(nameof(NonSourceFileRefactoringWithDocumentExtensions)) { }
    }
 
    [ExportCodeRefactoringProvider(LanguageNames.CSharp)]
    [Shared]
    internal sealed class NonSourceFileRefactoringWithoutDocumentKindsAndExtensions : AbstractNonSourceFileRefactoring
    {
        public NonSourceFileRefactoringWithoutDocumentKindsAndExtensions() : base(nameof(NonSourceFileRefactoringWithoutDocumentKindsAndExtensions)) { }
    }
#pragma warning restore RS0034 // Exported parts should be marked with 'ImportingConstructorAttribute'
}