File: CodeCleanup\AbstractCodeCleanUpFixer.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Progress;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Editor.CodeCleanup;
using Microsoft.VisualStudio.Language.CodeCleanUp;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
using __VSHPROPID8 = Microsoft.VisualStudio.Shell.Interop.__VSHPROPID8;
using IVsHierarchyItemManager = Microsoft.VisualStudio.Shell.IVsHierarchyItemManager;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.CodeCleanup;
 
/// <summary>
/// Roslyn implementations of <see cref="ICodeCleanUpFixer"/> extend this class. Since other extensions could also
/// be implementing the <see cref="ICodeCleanUpFixer"/> interface, this abstract base class allows Roslyn to operate
/// on MEF instances of fixers known to be relevant in the context of Roslyn languages.
/// </summary>
internal abstract partial class AbstractCodeCleanUpFixer(
    IThreadingContext threadingContext,
    VisualStudioWorkspaceImpl workspace,
    IVsHierarchyItemManager vsHierarchyItemManager) : ICodeCleanUpFixer
{
    private readonly IThreadingContext _threadingContext = threadingContext;
    private readonly VisualStudioWorkspaceImpl _workspace = workspace;
    private readonly IVsHierarchyItemManager _vsHierarchyItemManager = vsHierarchyItemManager;
 
    public Task<bool> FixAsync(ICodeCleanUpScope scope, ICodeCleanUpExecutionContext context)
        => scope switch
        {
            TextBufferCodeCleanUpScope textBufferScope => FixTextBufferAsync(textBufferScope, context),
            IVsHierarchyCodeCleanupScope hierarchyContentScope => FixHierarchyContentAsync(hierarchyContentScope, context),
            _ => Task.FromResult(false),
        };
 
    private async Task<bool> FixHierarchyContentAsync(IVsHierarchyCodeCleanupScope hierarchyContent, ICodeCleanUpExecutionContext context)
    {
        var hierarchy = hierarchyContent.Hierarchy;
        if (hierarchy == null)
        {
            var solution = _workspace.CurrentSolution;
            return await FixAsync(
                _workspace,
                // Just defer to FixProjectsAsync, passing in all fixable projects in the solution.
                (progress, cancellationToken) => FixProjectsAsync(
                    solution, solution.Projects.Where(p => p.SupportsCompilation).ToImmutableArray(), context.EnabledFixIds, progress, cancellationToken),
                context).ConfigureAwait(false);
        }
 
        // Map the hierarchy to a ProjectId. For hierarchies mapping to multitargeted projects, we first try to
        // get the project in the most recent active context, but fall back to the first target framework if no
        // active context is available.
        var hierarchyToProjectMap = _workspace.Services.GetRequiredService<IHierarchyItemToProjectIdMap>();
 
        await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(context.OperationContext.UserCancellationToken);
 
        ProjectId? projectId = null;
        if (ErrorHandler.Succeeded(hierarchy.GetProperty((uint)VSConstants.VSITEMID.Root, (int)__VSHPROPID8.VSHPROPID_ActiveIntellisenseProjectContext, out var contextProjectNameObject))
            && contextProjectNameObject is string contextProjectName)
        {
            projectId = _workspace.GetProjectWithHierarchyAndName(hierarchy, contextProjectName)?.Id;
        }
 
        if (projectId is null)
        {
            var projectHierarchyItem = _vsHierarchyItemManager.GetHierarchyItem(hierarchyContent.Hierarchy, (uint)VSConstants.VSITEMID.Root);
            if (!hierarchyToProjectMap.TryGetProjectId(projectHierarchyItem, targetFrameworkMoniker: null, out projectId))
                return false;
        }
 
        var itemId = hierarchyContent.ItemId;
        if (itemId == (uint)VSConstants.VSITEMID.Root)
        {
            await TaskScheduler.Default;
 
            var project = _workspace.CurrentSolution.GetProject(projectId);
            if (project == null || !project.SupportsCompilation)
                return false;
 
            return await FixAsync(
                _workspace,
                // Just defer to FixProjectsAsync, passing in this single project to fix.
                (progress, cancellationToken) => FixProjectsAsync(
                    project.Solution, [project], context.EnabledFixIds, progress, cancellationToken),
                context).ConfigureAwait(false);
        }
        else if (hierarchy.GetCanonicalName(itemId, out var path) == 0)
        {
            var attr = File.GetAttributes(path);
            if (attr.HasFlag(FileAttributes.Directory))
            {
                // directory
                // TODO: this one will be implemented later
                // https://github.com/dotnet/roslyn/issues/30165
            }
            else
            {
                // Handle code cleanup for a single document
                await TaskScheduler.Default;
 
                var solution = _workspace.CurrentSolution;
                var documentIds = solution.GetDocumentIdsWithFilePath(path);
                var documentId = documentIds.FirstOrDefault(id => id.ProjectId == projectId);
                if (documentId is null)
                    return false;
 
                var document = solution.GetRequiredDocument(documentId);
 
                return await FixAsync(
                    _workspace,
                    async (progress, cancellationToken) =>
                    {
                        var newDocument = await FixDocumentAsync(document, context.EnabledFixIds, progress, cancellationToken).ConfigureAwait(true);
                        return newDocument.Project.Solution;
                    },
                    context).ConfigureAwait(false);
            }
        }
 
        return false;
    }
 
    private Task<bool> FixTextBufferAsync(TextBufferCodeCleanUpScope textBufferScope, ICodeCleanUpExecutionContext context)
    {
        var buffer = textBufferScope.SubjectBuffer;
 
        // Let LSP handle code cleanup in the cloud scenario
        if (buffer.IsInLspEditorContext())
            return SpecializedTasks.False;
 
        var document = buffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
        if (document == null)
            return SpecializedTasks.False;
 
        var workspace = buffer.GetWorkspace();
        if (workspace is not VisualStudioWorkspace visualStudioWorkspace)
            return SpecializedTasks.False;
 
        return FixAsync(visualStudioWorkspace, ApplyFixAsync, context);
 
        // Local function
        async Task<Solution> ApplyFixAsync(IProgress<CodeAnalysisProgress> progress, CancellationToken cancellationToken)
        {
            var document = buffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
            Contract.ThrowIfNull(document);
 
            var newDoc = await FixDocumentAsync(document, context.EnabledFixIds, progress, cancellationToken).ConfigureAwait(true);
            return newDoc.Project.Solution;
        }
    }
 
    private async Task<bool> FixAsync(
        VisualStudioWorkspace workspace,
        Func<IProgress<CodeAnalysisProgress>, CancellationToken, Task<Solution>> applyFixAsync,
        ICodeCleanUpExecutionContext context)
    {
        using (var scope = context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Waiting_for_background_work_to_finish))
        {
            var workspaceStatusService = workspace.Services.GetService<IWorkspaceStatusService>();
            if (workspaceStatusService != null)
                await workspaceStatusService.WaitUntilFullyLoadedAsync(context.OperationContext.UserCancellationToken).ConfigureAwait(true);
        }
 
        using (var scope = context.OperationContext.AddScope(allowCancellation: true, description: EditorFeaturesResources.Applying_changes))
        {
            var cancellationToken = context.OperationContext.UserCancellationToken;
            var progress = scope.GetCodeAnalysisProgress();
 
            var solution = await applyFixAsync(progress, cancellationToken).ConfigureAwait(true);
 
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
 
            return workspace.TryApplyChanges(solution, progress);
        }
    }
 
    private static async Task<Solution> FixProjectsAsync(
        Solution solution,
        ImmutableArray<Project> projects,
        FixIdContainer enabledFixIds,
        IProgress<CodeAnalysisProgress> progressTracker,
        CancellationToken cancellationToken)
    {
        // Add an item for each document in all the projects we're processing.
        progressTracker.AddItems(projects.Sum(static p => p.DocumentIds.Count));
 
        // Run in parallel across all projects.
        var changedRoots = await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync(
            source: projects,
            produceItems: static async (project, callback, args, cancellationToken) =>
            {
                Contract.ThrowIfFalse(project.SupportsCompilation);
 
                var (solution, enabledFixIds, progressTracker) = args;
                cancellationToken.ThrowIfCancellationRequested();
 
                // And for each project, process all the documents in parallel.
                await RoslynParallel.ForEachAsync(
                    source: project.Documents,
                    cancellationToken,
                    async (document, cancellationToken) =>
                    {
                        using var _ = progressTracker.ItemCompletedScope();
 
                        // FixDocumentAsync reports progress within a document, but we only want to report progress at
                        // the document granularity.  So we pass CodeAnalysisProgress.None here so that inner progress
                        // updates don't affect us.
                        var fixedDocument = await FixDocumentAsync(document, enabledFixIds, CodeAnalysisProgress.None, cancellationToken).ConfigureAwait(false);
                        if (fixedDocument == document)
                            return;
 
                        callback((document.Id, await fixedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false)));
                    }).ConfigureAwait(false);
            },
            args: (solution, enabledFixIds, progressTracker),
            cancellationToken).ConfigureAwait(false);
 
        return solution.WithDocumentSyntaxRoots(changedRoots);
    }
 
    private static async Task<Document> FixDocumentAsync(
        Document document,
        FixIdContainer enabledFixIds,
        IProgress<CodeAnalysisProgress> progressTracker,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (document.IsGeneratedCode(cancellationToken))
            return document;
 
        var codeCleanupService = document.GetRequiredLanguageService<ICodeCleanupService>();
 
        var enabledDiagnostics = codeCleanupService.GetAllDiagnostics();
        enabledDiagnostics = AdjustDiagnosticOptions(enabledDiagnostics, enabledFixIds.IsFixIdEnabled);
 
        return await codeCleanupService.CleanupAsync(
            document, enabledDiagnostics, progressTracker, cancellationToken).ConfigureAwait(false);
    }
}