File: Diagnostics\VisualStudioDiagnosticAnalyzerService.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.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.ComponentModel.Design;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.SolutionCrawler;
using Microsoft.VisualStudio.LanguageServices.EditorConfigSettings;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities;
using Microsoft.VisualStudio.LanguageServices.Setup;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
using Task = System.Threading.Tasks.Task;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics;
 
[Export(typeof(IVisualStudioDiagnosticAnalyzerService))]
internal partial class VisualStudioDiagnosticAnalyzerService : IVisualStudioDiagnosticAnalyzerService
{
    // "Run Code Analysis on <%ProjectName%>" command for Top level "Build" and "Analyze" menus.
    // The below ID is actually defined as "ECMD_RUNFXCOPSEL" in stdidcmd.h, we're just referencing it here.
    private const int RunCodeAnalysisForSelectedProjectCommandId = 1647;
 
    private readonly VisualStudioWorkspace _workspace;
    private readonly IVsService<IVsStatusbar> _statusbar;
    private readonly DiagnosticAnalyzerInfoCache _diagnosticAnalyzerInfoCache;
    private readonly IThreadingContext _threadingContext;
    private readonly IVsHierarchyItemManager _vsHierarchyItemManager;
    private readonly IAsynchronousOperationListener _listener;
    private readonly IGlobalOptionService _globalOptions;
    private readonly ICodeAnalysisDiagnosticAnalyzerService _codeAnalysisService;
 
    private IServiceProvider? _serviceProvider;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public VisualStudioDiagnosticAnalyzerService(
        VisualStudioWorkspace workspace,
        IVsService<SVsStatusbar, IVsStatusbar> statusbar,
        DiagnosticAnalyzerInfoCache.SharedGlobalCache diagnosticAnalyzerInfoCache,
        IThreadingContext threadingContext,
        IVsHierarchyItemManager vsHierarchyItemManager,
        IAsynchronousOperationListenerProvider listenerProvider,
        IGlobalOptionService globalOptions)
    {
        _workspace = workspace;
        _statusbar = statusbar;
        _diagnosticAnalyzerInfoCache = diagnosticAnalyzerInfoCache.AnalyzerInfoCache;
        _threadingContext = threadingContext;
        _vsHierarchyItemManager = vsHierarchyItemManager;
        _listener = listenerProvider.GetListener(FeatureAttribute.DiagnosticService);
        _globalOptions = globalOptions;
        _codeAnalysisService = workspace.Services.GetRequiredService<ICodeAnalysisDiagnosticAnalyzerService>();
    }
 
    public async Task InitializeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken)
    {
        _serviceProvider = (IServiceProvider)serviceProvider;
 
        // Hook up the "Run Code Analysis" menu command for CPS based managed projects.
        var menuCommandService = await serviceProvider.GetServiceAsync<IMenuCommandService, IMenuCommandService>(_threadingContext.JoinableTaskFactory, throwOnFailure: false).ConfigureAwait(false);
        if (menuCommandService != null)
        {
            await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
            VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, RunCodeAnalysisForSelectedProjectCommandId, VSConstants.VSStd2K, OnRunCodeAnalysisForSelectedProject, OnRunCodeAnalysisForSelectedProjectStatus);
            VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, ID.RoslynCommands.RunCodeAnalysisForProject, Guids.RoslynGroupId, OnRunCodeAnalysisForSelectedProject, OnRunCodeAnalysisForSelectedProjectStatus);
            VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, ID.RoslynCommands.AnalysisScopeDefault, Guids.RoslynGroupId, OnSetAnalysisScopeDefault, OnSetAnalysisScopeDefaultStatus);
            VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, ID.RoslynCommands.AnalysisScopeCurrentDocument, Guids.RoslynGroupId, OnSetAnalysisScopeCurrentDocument, OnSetAnalysisScopeCurrentDocumentStatus);
            VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, ID.RoslynCommands.AnalysisScopeOpenDocuments, Guids.RoslynGroupId, OnSetAnalysisScopeOpenDocuments, OnSetAnalysisScopeOpenDocumentsStatus);
            VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, ID.RoslynCommands.AnalysisScopeEntireSolution, Guids.RoslynGroupId, OnSetAnalysisScopeEntireSolution, OnSetAnalysisScopeEntireSolutionStatus);
            VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, ID.RoslynCommands.AnalysisScopeNone, Guids.RoslynGroupId, OnSetAnalysisScopeNone, OnSetAnalysisScopeNoneStatus);
        }
    }
 
    public IReadOnlyDictionary<string, IEnumerable<DiagnosticDescriptor>> GetAllDiagnosticDescriptors(IVsHierarchy? hierarchy)
    {
        var currentSolution = _workspace.CurrentSolution;
        var hostAnalyzers = currentSolution.SolutionState.Analyzers;
 
        if (hierarchy == null)
        {
            return Transform(hostAnalyzers.GetDiagnosticDescriptorsPerReference(_diagnosticAnalyzerInfoCache));
        }
 
        // Analyzers are only supported for C# and VB currently.
        var projectsWithHierarchy = currentSolution.Projects
            .Where(p => p.Language is LanguageNames.CSharp or LanguageNames.VisualBasic)
            .Where(p => _workspace.GetHierarchy(p.Id) == hierarchy);
 
        if (projectsWithHierarchy.Count() <= 1)
        {
            var project = projectsWithHierarchy.FirstOrDefault();
            if (project == null)
            {
                return Transform(hostAnalyzers.GetDiagnosticDescriptorsPerReference(_diagnosticAnalyzerInfoCache));
            }
            else
            {
                return Transform(hostAnalyzers.GetDiagnosticDescriptorsPerReference(_diagnosticAnalyzerInfoCache, project));
            }
        }
        else
        {
            // Multiple workspace projects map to the same hierarchy, return a union of descriptors for all projects.
            // For example, this can happen for web projects where we create on the fly projects for aspx files.
            var descriptorsMap = ImmutableDictionary.CreateBuilder<string, IEnumerable<DiagnosticDescriptor>>();
            foreach (var project in projectsWithHierarchy)
            {
                var descriptorsPerReference = hostAnalyzers.GetDiagnosticDescriptorsPerReference(_diagnosticAnalyzerInfoCache, project);
                foreach (var (displayName, descriptors) in descriptorsPerReference)
                {
                    if (descriptorsMap.TryGetValue(displayName, out var existingDescriptors))
                    {
                        descriptorsMap[displayName] = existingDescriptors.Concat(descriptors).Distinct();
                    }
                    else
                    {
                        descriptorsMap[displayName] = descriptors;
                    }
                }
            }
 
            return descriptorsMap.ToImmutable();
        }
    }
 
    private static IReadOnlyDictionary<string, IEnumerable<DiagnosticDescriptor>> Transform(
        ImmutableDictionary<string, ImmutableArray<DiagnosticDescriptor>> map)
    {
        // unfortunately, we had to do this since ruleset editor and us are set to use this signature
        return map.ToDictionary(kv => kv.Key, kv => (IEnumerable<DiagnosticDescriptor>)kv.Value);
    }
 
    private void OnSetAnalysisScopeDefaultStatus(object sender, EventArgs e)
        => OnSetAnalysisScopeStatus((OleMenuCommand)sender, scope: null);
 
    private void OnSetAnalysisScopeCurrentDocumentStatus(object sender, EventArgs e)
        => OnSetAnalysisScopeStatus((OleMenuCommand)sender, BackgroundAnalysisScope.VisibleFilesAndOpenFilesWithPreviouslyReportedDiagnostics);
 
    private void OnSetAnalysisScopeOpenDocumentsStatus(object sender, EventArgs e)
        => OnSetAnalysisScopeStatus((OleMenuCommand)sender, BackgroundAnalysisScope.OpenFiles);
 
    private void OnSetAnalysisScopeEntireSolutionStatus(object sender, EventArgs e)
        => OnSetAnalysisScopeStatus((OleMenuCommand)sender, BackgroundAnalysisScope.FullSolution);
 
    private void OnSetAnalysisScopeNoneStatus(object sender, EventArgs e)
        => OnSetAnalysisScopeStatus((OleMenuCommand)sender, BackgroundAnalysisScope.None);
 
    private void OnSetAnalysisScopeStatus(OleMenuCommand command, BackgroundAnalysisScope? scope)
    {
        // The command is enabled as long as we have a service provider
        if (_serviceProvider is null)
        {
            // Not yet initialized
            command.Enabled = false;
            return;
        }
 
        command.Enabled = true;
 
        // The command is checked if RoslynPackage is loaded and the analysis scope for this command matches the
        // value saved for the solution.
        var roslynPackage = _threadingContext.JoinableTaskFactory.Run(() =>
        {
            return RoslynPackage.GetOrLoadAsync(_threadingContext, (IAsyncServiceProvider)_serviceProvider, _threadingContext.DisposalToken).AsTask();
        });
 
        if (roslynPackage is not null)
        {
            command.Checked = roslynPackage.AnalysisScope == scope;
        }
 
        // For the specific case of the default analysis scope command, update the command text to show the
        // current effective default in the context of the language(s) used in the solution.
        if (scope is null)
        {
            command.Text = GetBackgroundAnalysisScope(_workspace.CurrentSolution, _globalOptions) switch
            {
                BackgroundAnalysisScope.VisibleFilesAndOpenFilesWithPreviouslyReportedDiagnostics => ServicesVSResources.Default_Current_Document,
                BackgroundAnalysisScope.OpenFiles => ServicesVSResources.Default_Open_Documents,
                BackgroundAnalysisScope.FullSolution => ServicesVSResources.Default_Entire_Solution,
                BackgroundAnalysisScope.None => ServicesVSResources.Default_None,
                _ => ServicesVSResources.Default_,
            };
        }
 
        return;
 
        // Local functions
        static BackgroundAnalysisScope? GetBackgroundAnalysisScope(Solution solution, IGlobalOptionService globalOptions)
        {
            var csharpAnalysisScope = globalOptions.GetOption(SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption, LanguageNames.CSharp);
            var visualBasicAnalysisScope = globalOptions.GetOption(SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption, LanguageNames.VisualBasic);
 
            var containsCSharpProject = solution.Projects.Any(static project => project.Language == LanguageNames.CSharp);
            var containsVisualBasicProject = solution.Projects.Any(static project => project.Language == LanguageNames.VisualBasic);
            if (containsCSharpProject && containsVisualBasicProject)
            {
                if (csharpAnalysisScope == visualBasicAnalysisScope)
                    return csharpAnalysisScope;
                else
                    return null;
            }
            else if (containsVisualBasicProject)
            {
                return visualBasicAnalysisScope;
            }
            else
            {
                return csharpAnalysisScope;
            }
        }
    }
 
    private void OnSetAnalysisScopeDefault(object sender, EventArgs args)
        => OnSetAnalysisScope(scope: null);
 
    private void OnSetAnalysisScopeCurrentDocument(object sender, EventArgs args)
        => OnSetAnalysisScope(BackgroundAnalysisScope.VisibleFilesAndOpenFilesWithPreviouslyReportedDiagnostics);
 
    private void OnSetAnalysisScopeOpenDocuments(object sender, EventArgs args)
        => OnSetAnalysisScope(BackgroundAnalysisScope.OpenFiles);
 
    private void OnSetAnalysisScopeEntireSolution(object sender, EventArgs args)
        => OnSetAnalysisScope(BackgroundAnalysisScope.FullSolution);
 
    private void OnSetAnalysisScopeNone(object sender, EventArgs args)
        => OnSetAnalysisScope(BackgroundAnalysisScope.None);
 
    private void OnSetAnalysisScope(BackgroundAnalysisScope? scope)
    {
        if (_serviceProvider is null
            || !_serviceProvider.TryGetService<SVsShell, IVsShell>(_threadingContext.JoinableTaskFactory, out var shell))
        {
            return;
        }
 
        var roslynPackage = _threadingContext.JoinableTaskFactory.Run(() =>
        {
            return RoslynPackage.GetOrLoadAsync(_threadingContext, (IAsyncServiceProvider)_serviceProvider, _threadingContext.DisposalToken).AsTask();
        });
 
        Assumes.Present(roslynPackage);
 
        roslynPackage.AnalysisScope = scope;
    }
 
    private void OnRunCodeAnalysisForSelectedProjectStatus(object sender, EventArgs e)
    {
        var command = (OleMenuCommand)sender;
 
        // We hook up the "Run Code Analysis" menu commands for CPS based managed projects.
        // These commands are already hooked up for csproj based projects in StanCore, but those will eventually go away.
        var visible = VisualStudioCommandHandlerHelpers.TryGetSelectedProjectHierarchy(_serviceProvider, out var hierarchy) &&
            hierarchy.IsCapabilityMatch("CPS") &&
            hierarchy.IsCapabilityMatch(".NET");
        var enabled = false;
 
        if (visible)
        {
            if (command.CommandID.ID == RunCodeAnalysisForSelectedProjectCommandId &&
                hierarchy!.TryGetProject(out var project))
            {
                // Change to show the name of the project as part of the menu item display text.
                command.Text = string.Format(ServicesVSResources.Run_Code_Analysis_on_0, project.Name);
            }
 
            enabled = !VisualStudioCommandHandlerHelpers.IsBuildActive();
        }
 
        if (command.Visible != visible)
        {
            command.Visible = visible;
        }
 
        if (command.Enabled != enabled)
        {
            command.Enabled = enabled;
        }
    }
 
    private void OnRunCodeAnalysisForSelectedProject(object sender, EventArgs args)
    {
        if (VisualStudioCommandHandlerHelpers.TryGetSelectedProjectHierarchy(_serviceProvider, out var hierarchy))
        {
            RunAnalyzers(hierarchy);
        }
    }
 
    public void RunAnalyzers(IVsHierarchy? hierarchy)
    {
        var project = GetProject(hierarchy);
        var solution = _workspace.CurrentSolution;
        var projectOrSolutionName = project?.Name ?? PathUtilities.GetFileName(solution.FilePath);
 
        // Handle multi-tfm projects - we want to run code analysis for all tfm flavors of the project.
        ImmutableArray<Project> otherProjectsForMultiTfmProject;
        if (project != null)
        {
            otherProjectsForMultiTfmProject = solution.Projects.Where(
                p => p != project && p.FilePath == project.FilePath && p.State.NameAndFlavor.name == project.State.NameAndFlavor.name).ToImmutableArray();
            if (!otherProjectsForMultiTfmProject.IsEmpty)
                projectOrSolutionName = project.State.NameAndFlavor.name;
        }
        else
        {
            otherProjectsForMultiTfmProject = ImmutableArray<Project>.Empty;
        }
 
        // Force complete analyzer execution in background.
        _threadingContext.JoinableTaskFactory.RunAsync(async () =>
        {
            using var asyncToken = _listener.BeginAsyncOperation($"{nameof(VisualStudioDiagnosticAnalyzerService)}_{nameof(RunAnalyzers)}");
 
            // Add a message to VS status bar that we are running code analysis.
            var statusBar = await _statusbar.GetValueOrNullAsync().ConfigureAwait(true);
            var totalProjectCount = project != null ? (1 + otherProjectsForMultiTfmProject.Length) : solution.ProjectIds.Count;
            using var statusBarUpdater = statusBar != null
                ? new StatusBarUpdater(statusBar, _threadingContext, projectOrSolutionName, (uint)totalProjectCount)
                : null;
 
            await TaskScheduler.Default;
 
            var onAfterProjectAnalyzed = statusBarUpdater != null ? statusBarUpdater.OnAfterProjectAnalyzed : (Action<Project>)((Project _) => { });
            await _codeAnalysisService.RunAnalysisAsync(solution, project?.Id, onAfterProjectAnalyzed, CancellationToken.None).ConfigureAwait(false);
 
            foreach (var otherProject in otherProjectsForMultiTfmProject)
                await _codeAnalysisService.RunAnalysisAsync(solution, otherProject.Id, onAfterProjectAnalyzed, CancellationToken.None).ConfigureAwait(false);
        });
    }
 
    private Project? GetProject(IVsHierarchy? hierarchy)
    {
        if (hierarchy != null)
        {
            var projectMap = _workspace.Services.GetRequiredService<IHierarchyItemToProjectIdMap>();
            var projectHierarchyItem = _vsHierarchyItemManager.GetHierarchyItem(hierarchy, VSConstants.VSITEMID_ROOT);
            if (projectMap.TryGetProjectId(projectHierarchyItem, targetFrameworkMoniker: null, out var projectId))
            {
                return _workspace.CurrentSolution.GetProject(projectId);
            }
        }
 
        return null;
    }
 
    private sealed class StatusBarUpdater : IDisposable
    {
        private readonly IVsStatusbar _statusBar;
        private readonly IThreadingContext _threadingContext;
        private readonly uint _totalProjectCount;
        private readonly string _statusMessageWhileRunning;
        private readonly string _statusMesageOnCompleted;
        private readonly string _statusMesageOnTerminated;
        private readonly Timer _timer;
 
        private int _analyzedProjectCount;
        private bool _disposed;
        private uint _statusBarCookie;
 
        public StatusBarUpdater(IVsStatusbar statusBar, IThreadingContext threadingContext, string? projectOrSolutionName, uint totalProjectCount)
        {
            threadingContext.ThrowIfNotOnUIThread();
            _statusBar = statusBar;
            _threadingContext = threadingContext;
            _totalProjectCount = totalProjectCount;
 
            _statusMessageWhileRunning = projectOrSolutionName != null
                ? string.Format(ServicesVSResources.Running_code_analysis_for_0, projectOrSolutionName)
                : ServicesVSResources.Running_code_analysis_for_Solution;
            _statusMesageOnCompleted = projectOrSolutionName != null
                ? string.Format(ServicesVSResources.Code_analysis_completed_for_0, projectOrSolutionName)
                : ServicesVSResources.Code_analysis_completed_for_Solution;
            _statusMesageOnTerminated = projectOrSolutionName != null
                ? string.Format(ServicesVSResources.Code_analysis_terminated_before_completion_for_0, projectOrSolutionName)
                : ServicesVSResources.Code_analysis_terminated_before_completion_for_Solution;
 
            // Set the initial status bar progress and text.
            _statusBar.Progress(ref _statusBarCookie, fInProgress: 1, _statusMessageWhileRunning, nComplete: 0, nTotal: totalProjectCount);
            _statusBar.SetText(_statusMessageWhileRunning);
 
            // Create a timer to periodically update the status message while running analysis.
            _timer = new Timer(new TimerCallback(UpdateStatusOnTimer), new AutoResetEvent(false),
                dueTime: TimeSpan.FromSeconds(5), period: TimeSpan.FromSeconds(5));
        }
 
        internal void OnAfterProjectAnalyzed(Project _)
        {
            Interlocked.Increment(ref _analyzedProjectCount);
            UpdateStatusCore();
        }
 
        // Add a message to VS status bar that we are running code analysis.
        private void UpdateStatusOnTimer(object state)
            => UpdateStatusCore();
 
        public void Dispose()
        {
            _timer.Dispose();
            _disposed = true;
            UpdateStatusCore();
        }
 
        private void UpdateStatusCore()
        {
            _threadingContext.JoinableTaskFactory.RunAsync(async () =>
            {
                await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();
 
                string message;
                int fInProgress;
                var analyzedProjectCount = (uint)_analyzedProjectCount;
                if (analyzedProjectCount == _totalProjectCount)
                {
                    message = _statusMesageOnCompleted;
                    fInProgress = 0;
                }
                else if (_disposed)
                {
                    message = _statusMesageOnTerminated;
                    fInProgress = 0;
                }
                else
                {
                    message = _statusMessageWhileRunning;
                    fInProgress = 1;
                }
 
                // Update the status bar progress and text.
                _statusBar.Progress(ref _statusBarCookie, fInProgress, message, analyzedProjectCount, _totalProjectCount);
                _statusBar.SetText(message);
            });
        }
    }
}