File: Diagnostics\CodeAnalysisDiagnosticAnalyzerService.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Concurrent;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Diagnostics;
 
[ExportWorkspaceServiceFactory(typeof(ICodeAnalysisDiagnosticAnalyzerService)), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class CodeAnalysisDiagnosticAnalyzerServiceFactory() : IWorkspaceServiceFactory
{
    public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)
    {
        var diagnosticAnalyzerService = workspaceServices.SolutionServices.ExportProvider.GetExports<IDiagnosticAnalyzerService>().Single().Value;
        return new CodeAnalysisDiagnosticAnalyzerService(diagnosticAnalyzerService, workspaceServices.Workspace);
    }
 
    private sealed class CodeAnalysisDiagnosticAnalyzerService : ICodeAnalysisDiagnosticAnalyzerService
    {
        private readonly IDiagnosticAnalyzerService _diagnosticAnalyzerService;
        private readonly Workspace _workspace;
 
        /// <summary>
        /// Mapping of projects to the diagnostics for the projects that we've finished running "run code analysis" on.
        /// Cached results can now be returned for these through <see cref="GetLastComputedDocumentDiagnostics"/>
        /// and <see cref="GetLastComputedProjectDiagnostics"/>.
        /// </summary>
        private readonly ConcurrentDictionary<ProjectId, ImmutableArray<DiagnosticData>> _analyzedProjectToDiagnostics = [];
 
        /// <summary>
        /// Previously analyzed projects that we no longer want to report results for.  This happens when an explicit
        /// build is kicked off.  At that point, we want the build results to win out for a particular project.  We mark
        /// this project (as opposed to removing from <see cref="_analyzedProjectToDiagnostics"/>) as we want our LSP
        /// handler to still think it should process it, as that will the cause the diagnostics to be removed when they
        /// now transition to an empty list returned from this type.
        /// </summary>
        private readonly ConcurrentSet<ProjectId> _clearedProjectIds = [];
 
        public CodeAnalysisDiagnosticAnalyzerService(
            IDiagnosticAnalyzerService diagnosticAnalyzerService,
            Workspace workspace)
        {
            _diagnosticAnalyzerService = diagnosticAnalyzerService;
            _workspace = workspace;
 
            _workspace.WorkspaceChanged += OnWorkspaceChanged;
        }
 
        private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e)
        {
            switch (e.Kind)
            {
                case WorkspaceChangeKind.SolutionAdded:
                case WorkspaceChangeKind.SolutionCleared:
                case WorkspaceChangeKind.SolutionReloaded:
                case WorkspaceChangeKind.SolutionRemoved:
 
                    _analyzedProjectToDiagnostics.Clear();
                    _clearedProjectIds.Clear();
 
                    // Let LSP know so that it requests up to date info, and will see our cached info disappear.
                    _diagnosticAnalyzerService.RequestDiagnosticRefresh();
                    break;
            }
        }
 
        public void Clear()
        {
            // Clear the list of analyzed projects.
            _clearedProjectIds.AddRange(_analyzedProjectToDiagnostics.Keys);
 
            // Let LSP know so that it requests up to date info, and will see our cached info disappear.
            _diagnosticAnalyzerService.RequestDiagnosticRefresh();
        }
 
        public bool HasProjectBeenAnalyzed(ProjectId projectId)
            => _analyzedProjectToDiagnostics.ContainsKey(projectId);
 
        public async Task RunAnalysisAsync(Solution solution, ProjectId? projectId, Action<Project> onAfterProjectAnalyzed, CancellationToken cancellationToken)
        {
            Contract.ThrowIfFalse(solution.Workspace == _workspace);
 
            if (projectId != null)
            {
                var project = solution.GetProject(projectId);
                if (project != null)
                {
                    await AnalyzeProjectCoreAsync(project, onAfterProjectAnalyzed, cancellationToken).ConfigureAwait(false);
                }
            }
            else
            {
                // We run analysis for all the projects concurrently as this is a user invoked operation.
                await RoslynParallel.ForEachAsync(
                    source: solution.Projects,
                    cancellationToken,
                    (project, cancellationToken) => AnalyzeProjectCoreAsync(project, onAfterProjectAnalyzed, cancellationToken)).ConfigureAwait(false);
            }
        }
 
        private async ValueTask AnalyzeProjectCoreAsync(Project project, Action<Project> onAfterProjectAnalyzed, CancellationToken cancellationToken)
        {
            // Execute force analysis for the project.
            var diagnostics = await _diagnosticAnalyzerService.ForceAnalyzeProjectAsync(project, cancellationToken).ConfigureAwait(false);
 
            // Add the given project to the analyzed projects list **after** analysis has completed.
            // We need this ordering to ensure that 'HasProjectBeenAnalyzed' call above functions correctly.
            _analyzedProjectToDiagnostics[project.Id] = diagnostics;
 
            // Remove from the cleared list now that we've run a more recent "run code analysis" on this project.
            _clearedProjectIds.Remove(project.Id);
 
            // Now raise the callback into our caller to indicate this project has been analyzed.
            onAfterProjectAnalyzed(project);
 
            // Finally, invoke a workspace refresh request for LSP client to pull onto these diagnostics.
            //
            // TODO: Below call will eventually be replaced with a special workspace refresh request that skips pulling
            // document diagnostics and also does not add any delay for pulling workspace diagnostics.
            _diagnosticAnalyzerService.RequestDiagnosticRefresh();
        }
 
        /// <summary>
        /// Running code analysis on the project force computes and caches the diagnostics in <see
        /// cref="_analyzedProjectToDiagnostics"/>. We return these cached document diagnostics here, including both
        /// local and non-local document diagnostics.
        /// </summary>
        /// <remarks>
        /// Only returns non-suppressed diagnostics.
        /// </remarks>
        public ImmutableArray<DiagnosticData> GetLastComputedDocumentDiagnostics(DocumentId documentId)
        {
            if (_clearedProjectIds.Contains(documentId.ProjectId))
                return [];
 
            if (!_analyzedProjectToDiagnostics.TryGetValue(documentId.ProjectId, out var diagnostics))
                return [];
 
            return diagnostics.WhereAsArray(static (d, documentId) =>
                !d.IsSuppressed && d.DataLocation.DocumentId == documentId, documentId);
        }
 
        /// <summary>
        /// Running code analysis on the project force computes and caches the diagnostics in <see
        /// cref="_analyzedProjectToDiagnostics"/>. We return these cached project diagnostics here, i.e. diagnostics
        /// with no location, by excluding all local and non-local document diagnostics.
        /// </summary>
        /// <remarks>
        /// Only returns non-suppressed diagnostics.
        /// </remarks>
        public ImmutableArray<DiagnosticData> GetLastComputedProjectDiagnostics(ProjectId projectId)
        {
            if (_clearedProjectIds.Contains(projectId))
                return [];
 
            if (!_analyzedProjectToDiagnostics.TryGetValue(projectId, out var diagnostics))
                return [];
 
            return diagnostics.WhereAsArray(static (d, projectId) =>
                !d.IsSuppressed && d.ProjectId == projectId && d.DataLocation.DocumentId == null, projectId);
        }
    }
}