<NME> CSharpDiagnosticWorkerWithAnalyzers.cs
<BEF> ﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.ExternalAccess.OmniSharp.Analyzers;
using Microsoft.CodeAnalysis.ExternalAccess.OmniSharp.ImplementType;
using Microsoft.Extensions.Logging;
using OmniSharp.Helpers;
using OmniSharp.Models.Diagnostics;
using OmniSharp.Models.Events;
using OmniSharp.Options;
using OmniSharp.Roslyn.CSharp.Workers.Diagnostics;
using OmniSharp.Services;

namespace OmniSharp.Roslyn.CSharp.Services.Diagnostics
{
    public class CSharpDiagnosticWorkerWithAnalyzers : ICsDiagnosticWorker, IDisposable
    {
        private readonly AnalyzerWorkQueue _workQueue;
        private readonly SemaphoreSlim _throttler;
        private readonly ILogger<CSharpDiagnosticWorkerWithAnalyzers> _logger;

        private readonly ConcurrentDictionary<DocumentId, DocumentDiagnostics> _currentDiagnosticResultLookup = new();
        private readonly ImmutableArray<ICodeActionProvider> _providers;
        private readonly DiagnosticEventForwarder _forwarder;
        private readonly OmniSharpOptions _options;
        private readonly OmniSharpWorkspace _workspace;

        private const int WorkerWait = 250;

        public CSharpDiagnosticWorkerWithAnalyzers(
            OmniSharpWorkspace workspace,
            [ImportMany] IEnumerable<ICodeActionProvider> providers,
            ILoggerFactory loggerFactory,
            DiagnosticEventForwarder forwarder,
            OmniSharpOptions options)
        {
            _logger = loggerFactory.CreateLogger<CSharpDiagnosticWorkerWithAnalyzers>();
            _providers = providers.ToImmutableArray();
            _workQueue = new AnalyzerWorkQueue(loggerFactory, timeoutForPendingWorkMs: options.RoslynExtensionsOptions.DocumentAnalysisTimeoutMs * 3);
            _throttler = new SemaphoreSlim(options.RoslynExtensionsOptions.DiagnosticWorkersThreadCount);

            _forwarder = forwarder;
            _options = options;
            _workspace = workspace;

            _workspace.WorkspaceChanged += OnWorkspaceChanged;
            _workspace.OnInitialized += OnWorkspaceInitialized;

            Task.Factory.StartNew(() => Worker(AnalyzerWorkType.Foreground), TaskCreationOptions.LongRunning);
            Task.Factory.StartNew(() => Worker(AnalyzerWorkType.Background), TaskCreationOptions.LongRunning);

            OnWorkspaceInitialized(_workspace.Initialized);
        }

        public void OnWorkspaceInitialized(bool isInitialized)
        {
            if (isInitialized)
            {
                var documentIds = QueueDocumentsForDiagnostics();
                _logger.LogInformation($"Solution initialized -> queue all documents for code analysis. Initial document count: {documentIds.Length}.");
            }
        }

        public async Task<ImmutableArray<DocumentDiagnostics>> GetDiagnostics(ImmutableArray<string> documentPaths)
        {
            var documentIds = GetDocumentIdsFromPaths(documentPaths);

            return await GetDiagnosticsByDocumentIds(documentIds, waitForDocuments: true);
        }

        private async Task<ImmutableArray<DocumentDiagnostics>> GetDiagnosticsByDocumentIds(ImmutableArray<DocumentId> documentIds, bool waitForDocuments)
        {
            if (waitForDocuments)
            {
                foreach (var documentId in documentIds)
                {
                    _workQueue.TryPromote(documentId);
                }

                await _workQueue.WaitForegroundWorkComplete();
            }

            return documentIds
                .Where(x => _currentDiagnosticResultLookup.ContainsKey(x))
                .Select(x => _currentDiagnosticResultLookup[x])
                .ToImmutableArray();
        }

        private ImmutableArray<DocumentId> GetDocumentIdsFromPaths(ImmutableArray<string> documentPaths)
        {
            return documentPaths
                .Select(docPath => _workspace.GetDocumentId(docPath))
                .Where(x => x != default)
                .ToImmutableArray();
        }

        private async Task Worker(AnalyzerWorkType workType)
        {
            while (true)
            {
                try
                {
                    var solution = _workspace.CurrentSolution;

                    var documents = _workQueue
                        .TakeWork(workType)
                        .Select(documentId => (projectId: solution.GetDocument(documentId)?.Project?.Id, documentId))
                        .Where(x => x.projectId != null)
                        .ToImmutableArray();

                    if (documents.IsEmpty)
                    {
                        _workQueue.WorkComplete(workType);

                        await Task.Delay(WorkerWait);

                        continue;
                    }

                    var documentCount = documents.Length;
                    var documentCountRemaining = documentCount;

                    // event every percentage increase, or every 10th if there are fewer than 1000
                    var eventEvery = Math.Max(10, documentCount / 100);

                    var documentsGroupedByProjects = documents
                        .GroupBy(x => x.projectId, x => x.documentId)
                        .ToImmutableArray();
                    var projectCount = documentsGroupedByProjects.Length;

                    EventIfBackgroundWork(workType, BackgroundDiagnosticStatus.Started, projectCount, documentCount, documentCountRemaining);

                    void decrementDocumentCountRemaining()
                    {
                        var remaining = Interlocked.Decrement(ref documentCountRemaining);
                        var done = documentCount - remaining;
                        if (done % eventEvery == 0)
                        {
                            EventIfBackgroundWork(workType, BackgroundDiagnosticStatus.Progress, projectCount, documentCount, remaining);
                        }
                    }

                    try
                    {
                        var projectAnalyzerTasks =
                            documentsGroupedByProjects
                                .Select(projectGroup => Task.Run(async () =>
                                {
                                    var projectPath = solution.GetProject(projectGroup.Key).FilePath;
                                    await AnalyzeProject(solution, projectGroup, decrementDocumentCountRemaining);
                                }))
                                .ToImmutableArray();

                        await Task.WhenAll(projectAnalyzerTasks);
                    }
                    finally
                    {
                        EventIfBackgroundWork(workType, BackgroundDiagnosticStatus.Finished, projectCount, documentCount, documentCountRemaining);
                    }

                    _workQueue.WorkComplete(workType);

                    await Task.Delay(WorkerWait);
                }
                catch (Exception ex)
                {
                    _logger.LogError($"Analyzer worker failed: {ex}");
                }
            }
        }

        private void EventIfBackgroundWork(AnalyzerWorkType workType, BackgroundDiagnosticStatus status, int numberProjects, int numberFiles, int numberFilesRemaining)
        {
            if (workType == AnalyzerWorkType.Background)
                _forwarder.BackgroundDiagnosticsStatus(status, numberProjects, numberFiles, numberFilesRemaining);
        }

        private void QueueForAnalysis(ImmutableArray<DocumentId> documentIds, AnalyzerWorkType workType)
        {
            _workQueue.PutWork(documentIds, workType);
        }

        private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs changeEvent)
        {
            switch (changeEvent.Kind)
            {
                case WorkspaceChangeKind.DocumentChanged:
                case WorkspaceChangeKind.DocumentAdded:
                case WorkspaceChangeKind.DocumentReloaded:
                case WorkspaceChangeKind.DocumentInfoChanged:
                    QueueForAnalysis(ImmutableArray.Create(changeEvent.DocumentId), AnalyzerWorkType.Foreground);

            var compilation = await project.GetCompilationAsync();
            var workspaceAnalyzerOptions = (AnalyzerOptions)_workspaceAnalyzerOptionsConstructor.Invoke(new object[] { project.AnalyzerOptions, project.Solution });
            var filteredAnalyzers = await FilterSuppressedAnalyzersAsync(allAnalyzers, document, workspaceAnalyzerOptions, compilation, cancellationToken);

            cancellationToken.ThrowIfCancellationRequested();
            return await AnalyzeDocumentAsync(project, filteredAnalyzers, compilation, workspaceAnalyzerOptions, document);
                    _logger.LogDebug($"Analyzer config document {changeEvent.DocumentId} changed, which triggered re-analysis of project {changeEvent.ProjectId}.");
                    QueueForAnalysis(_workspace.CurrentSolution.GetProject(changeEvent.ProjectId).Documents.Select(x => x.Id).ToImmutableArray(), AnalyzerWorkType.Background);
                    break;
                case WorkspaceChangeKind.ProjectAdded:
                case WorkspaceChangeKind.ProjectChanged:
                case WorkspaceChangeKind.ProjectReloaded:
                    _logger.LogDebug($"Project {changeEvent.ProjectId} updated, reanalyzing its diagnostics.");
                    QueueForAnalysis(_workspace.CurrentSolution.GetProject(changeEvent.ProjectId).Documents.Select(x => x.Id).ToImmutableArray(), AnalyzerWorkType.Background);
                    break;

            var compilation = await project.GetCompilationAsync();
            var workspaceAnalyzerOptions = (AnalyzerOptions)_workspaceAnalyzerOptionsConstructor.Invoke(new object[] { project.AnalyzerOptions, project.Solution });
            var filteredAnalyzers = await FilterSuppressedAnalyzersAsync(allAnalyzers, project, compilation, cancellationToken);

            return await AnalyzeProjectAsync(project, filteredAnalyzers, compilation, workspaceAnalyzerOptions);
        }

        public static async Task<ImmutableArray<DiagnosticAnalyzer>> FilterSuppressedAnalyzersAsync(ImmutableArray<DiagnosticAnalyzer> allAnalyzers, Document document, AnalyzerOptions analyzerOptions, Compilation compilation, CancellationToken cancellationToken = default)
        {
            var documentOptions = await document.GetOptionsAsync(cancellationToken);
            var filteredAnalyzersBuilder = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();
            Project project = document.Project;
            var allAnalyzers = GetAnalyzersForProject(project);
            var compilation = await project.GetCompilationAsync(cancellationToken);

            cancellationToken.ThrowIfCancellationRequested();
                    filteredAnalyzersBuilder.Add(analyzer);
                }

                var severity = analyzer.GetSeverity(document, analyzerOptions, documentOptions, compilation);
                if (severity != ReportDiagnostic.Suppress)
                {
                    filteredAnalyzersBuilder.Add(analyzer);
                }
            var documentAnalyzerTasks = new List<Task>();
            var diagnostics = ImmutableList<Diagnostic>.Empty;
            return filteredAnalyzersBuilder.ToImmutable();
        }

        public static async Task<ImmutableArray<DiagnosticAnalyzer>> FilterSuppressedAnalyzersAsync(ImmutableArray<DiagnosticAnalyzer> allAnalyzers, Project project, Compilation compilation, CancellationToken cancellationToken = default)
        {
            var filteredAnalyzersBuilder = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();

                    try
                    {
                        var documentDiagnostics = await AnalyzeDocument(project, allAnalyzers, compilation, workspaceAnalyzerOptions, document);
                        ImmutableInterlocked.Update(ref diagnostics, currentDiagnostics => currentDiagnostics.AddRange(documentDiagnostics));
                    filteredAnalyzersBuilder.Add(analyzer);
                }

                var severity = await analyzer.GetSeverityAsync(project, compilation, cancellationToken);
                if (severity != ReportDiagnostic.Suppress)
                {
                    filteredAnalyzersBuilder.Add(analyzer);
                }
            await Task.WhenAll(documentAnalyzerTasks);

            return diagnostics;
        }

        private async Task AnalyzeProject(Solution solution, IGrouping<ProjectId, DocumentId> documentsGroupedByProject, Action decrementRemaining)
        {
            try
            {
                var project = solution.GetProject(documentsGroupedByProject.Key);
                var allAnalyzers = GetAnalyzersForProject(project);
                var compilation = await project.GetCompilationAsync();
                var workspaceAnalyzerOptions = CreateAnalyzerOptions(project);
                var documentAnalyzerTasks = new List<Task>();


                var compilation = await project.GetCompilationAsync();
                var workspaceAnalyzerOptions = (AnalyzerOptions)_workspaceAnalyzerOptionsConstructor.Invoke(new object[] { project.AnalyzerOptions, project.Solution });
                var filteredAnalyzers = await FilterSuppressedAnalyzersAsync(allAnalyzers, project, compilation);

                var diagnostics = await AnalyzeProjectAsync(project, filteredAnalyzers, compilation, workspaceAnalyzerOptions);
                var diagnosticsByFilePath = diagnostics
                            var document = project.GetDocument(documentId);
                            var diagnostics = await AnalyzeDocument(project, allAnalyzers, compilation, workspaceAnalyzerOptions, document);
                            UpdateCurrentDiagnostics(project, document, diagnostics);
                            decrementRemaining();
                        }
                        finally
                        {
                            _throttler.Release();
                        }
                    }));
                }

                await Task.WhenAll(documentAnalyzerTasks);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Analysis of project {documentsGroupedByProject.Key} failed, underlaying error: {ex}");
            }
        }

        private async Task<ImmutableArray<Diagnostic>> AnalyzeDocument(Project project, ImmutableArray<DiagnosticAnalyzer> allAnalyzers, Compilation compilation, AnalyzerOptions workspaceAnalyzerOptions, Document document)
        {
            try
            {
                // There's real possibility that bug in analyzer causes analysis hang at document.
                var perDocumentTimeout =
                    new CancellationTokenSource(_options.RoslynExtensionsOptions.DocumentAnalysisTimeoutMs);

                var documentSemanticModel = await document.GetSemanticModelAsync(perDocumentTimeout.Token);

                // Analyzers cannot be called with empty analyzer list.
                var canDoFullAnalysis = allAnalyzers.Length > 0
                    && (!_options.RoslynExtensionsOptions.AnalyzeOpenDocumentsOnly
                        || _workspace.IsDocumentOpen(document.Id));

                // Only basic syntax check is available if file is miscellanous like orphan .cs file.
                // Those projects are on hard coded virtual project
                if (project.Name == $"{Configuration.OmniSharpMiscProjectName}.csproj")
                {
                    var syntaxTree = await document.GetSyntaxTreeAsync();
                    return syntaxTree.GetDiagnostics().ToImmutableArray();
                }
                else if (canDoFullAnalysis)
                {
                    var compilationWithAnalyzers = compilation.WithAnalyzers(allAnalyzers, new CompilationWithAnalyzersOptions(
                        workspaceAnalyzerOptions,
                        onAnalyzerException: OnAnalyzerException,
                        concurrentAnalysis: false,
                        logAnalyzerExecutionTime: false,
                        reportSuppressedDiagnostics: false));

                    var semanticDiagnosticsWithAnalyzers = await compilationWithAnalyzers
                        .GetAnalyzerSemanticDiagnosticsAsync(documentSemanticModel, filterSpan: null, perDocumentTimeout.Token);

                    var syntaxDiagnosticsWithAnalyzers = await compilationWithAnalyzers
                        .GetAnalyzerSyntaxDiagnosticsAsync(documentSemanticModel.SyntaxTree, perDocumentTimeout.Token);

                    return semanticDiagnosticsWithAnalyzers
                        .Concat(syntaxDiagnosticsWithAnalyzers)
                        .Where(d => !d.IsSuppressed)
                        .Concat(documentSemanticModel.GetDiagnostics())
                        .ToImmutableArray();
                }
                else
                {
                    return documentSemanticModel.GetDiagnostics();
                }
            }
            catch (Exception ex)
            {
                _logger.LogError($"Analysis of document {document.Name} failed or cancelled by timeout: {ex.Message}, analysers: {string.Join(", ", allAnalyzers)}");
                return ImmutableArray<Diagnostic>.Empty;
            }
        }

        private ImmutableArray<DiagnosticAnalyzer> GetAnalyzersForProject(Project project)
        {
            return _providers
                .SelectMany(x => x.CodeDiagnosticAnalyzerProviders)
                .Concat(project.AnalyzerReferences.SelectMany(x => x.GetAnalyzers(project.Language)))
                .ToImmutableArray();
        }

        private void OnAnalyzerException(Exception ex, DiagnosticAnalyzer analyzer, Diagnostic diagnostic)
        {
            _logger.LogDebug("Exception in diagnostic analyzer." +
                $"\n            analyzer: {analyzer}" +
                $"\n            diagnostic: {diagnostic}" +
                $"\n            exception: {ex.Message}");
        }

        private void UpdateCurrentDiagnostics(Project project, Document document, ImmutableArray<Diagnostic> diagnostics)
        {
            var documentDiagnostics = new DocumentDiagnostics(document.Id, document.FilePath, project.Id, project.Name, diagnostics);
            _currentDiagnosticResultLookup[document.Id] = documentDiagnostics;
            EmitDiagnostics(documentDiagnostics);
        }

        private void EmitDiagnostics(DocumentDiagnostics results)
        {
            _forwarder.Forward(new DiagnosticMessage
            {
                Results = new[]
                {
                    new DiagnosticResult
                    {
                        FileName = results.DocumentPath, QuickFixes = results.Diagnostics
                            .Select(x => x.ToDiagnosticLocation())
                            .ToList()
                    }
                }
            });
        }

        public ImmutableArray<DocumentId> QueueDocumentsForDiagnostics()
        {
            var documentIds = _workspace.CurrentSolution.Projects.SelectMany(x => x.DocumentIds).ToImmutableArray();
            QueueForAnalysis(documentIds, AnalyzerWorkType.Background);
            return documentIds;
        }

        public async Task<ImmutableArray<DocumentDiagnostics>> GetAllDiagnosticsAsync()
        {
            var allDocumentsIds = _workspace.CurrentSolution.Projects.SelectMany(x => x.DocumentIds).ToImmutableArray();
            return await GetDiagnosticsByDocumentIds(allDocumentsIds, waitForDocuments: false);
        }

        public ImmutableArray<DocumentId> QueueDocumentsForDiagnostics(ImmutableArray<ProjectId> projectIds)
        {
            var documentIds = projectIds
                .SelectMany(projectId => _workspace.CurrentSolution.GetProject(projectId).Documents.Select(x => x.Id))
                .ToImmutableArray();
            QueueForAnalysis(documentIds, AnalyzerWorkType.Background);
            return documentIds;
        }

        public void Dispose()
        {
            _workspace.WorkspaceChanged -= OnWorkspaceChanged;
            _workspace.OnInitialized -= OnWorkspaceInitialized;
        }
    }
}

<MSG> When filtering to not calculate final diagnostic severity

<DFF> @@ -200,7 +200,8 @@ public async Task<IEnumerable<Diagnostic>> AnalyzeDocumentAsync(Document documen
 
             var compilation = await project.GetCompilationAsync();
             var workspaceAnalyzerOptions = (AnalyzerOptions)_workspaceAnalyzerOptionsConstructor.Invoke(new object[] { project.AnalyzerOptions, project.Solution });
-            var filteredAnalyzers = await FilterSuppressedAnalyzersAsync(allAnalyzers, document, workspaceAnalyzerOptions, compilation, cancellationToken);
+            // Filter out suppressed analyzers by requiring a minimum diagnostic severity of Hidden
+            var filteredAnalyzers = await FilterAnalyzersBySeverityAsync(allAnalyzers, document, workspaceAnalyzerOptions, compilation, ReportDiagnostic.Hidden, cancellationToken);
 
             cancellationToken.ThrowIfCancellationRequested();
             return await AnalyzeDocumentAsync(project, filteredAnalyzers, compilation, workspaceAnalyzerOptions, document);
@@ -216,12 +217,13 @@ public async Task<IEnumerable<Diagnostic>> AnalyzeProjectAsync(Project project,
 
             var compilation = await project.GetCompilationAsync();
             var workspaceAnalyzerOptions = (AnalyzerOptions)_workspaceAnalyzerOptionsConstructor.Invoke(new object[] { project.AnalyzerOptions, project.Solution });
-            var filteredAnalyzers = await FilterSuppressedAnalyzersAsync(allAnalyzers, project, compilation, cancellationToken);
+            // Filter out suppressed analyzers by requiring a minimum diagnostic severity of Hidden
+            var filteredAnalyzers = await FilterAnalyzersBySeverityAsync(allAnalyzers, project, compilation, ReportDiagnostic.Hidden, cancellationToken);
 
             return await AnalyzeProjectAsync(project, filteredAnalyzers, compilation, workspaceAnalyzerOptions);
         }
 
-        public static async Task<ImmutableArray<DiagnosticAnalyzer>> FilterSuppressedAnalyzersAsync(ImmutableArray<DiagnosticAnalyzer> allAnalyzers, Document document, AnalyzerOptions analyzerOptions, Compilation compilation, CancellationToken cancellationToken = default)
+        public static async Task<ImmutableArray<DiagnosticAnalyzer>> FilterAnalyzersBySeverityAsync(ImmutableArray<DiagnosticAnalyzer> allAnalyzers, Document document, AnalyzerOptions analyzerOptions, Compilation compilation, ReportDiagnostic minimumSeverity, CancellationToken cancellationToken = default)
         {
             var documentOptions = await document.GetOptionsAsync(cancellationToken);
             var filteredAnalyzersBuilder = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();
@@ -233,8 +235,8 @@ public static async Task<ImmutableArray<DiagnosticAnalyzer>> FilterSuppressedAna
                     filteredAnalyzersBuilder.Add(analyzer);
                 }
 
-                var severity = analyzer.GetSeverity(document, analyzerOptions, documentOptions, compilation);
-                if (severity != ReportDiagnostic.Suppress)
+                var hasMinimumSeverity = analyzer.HasMinimumSeverity(document, analyzerOptions, documentOptions, minimumSeverity, compilation);
+                if (hasMinimumSeverity)
                 {
                     filteredAnalyzersBuilder.Add(analyzer);
                 }
@@ -243,7 +245,7 @@ public static async Task<ImmutableArray<DiagnosticAnalyzer>> FilterSuppressedAna
             return filteredAnalyzersBuilder.ToImmutable();
         }
 
-        public static async Task<ImmutableArray<DiagnosticAnalyzer>> FilterSuppressedAnalyzersAsync(ImmutableArray<DiagnosticAnalyzer> allAnalyzers, Project project, Compilation compilation, CancellationToken cancellationToken = default)
+        public static async Task<ImmutableArray<DiagnosticAnalyzer>> FilterAnalyzersBySeverityAsync(ImmutableArray<DiagnosticAnalyzer> allAnalyzers, Project project, Compilation compilation, ReportDiagnostic minimumSeverity, CancellationToken cancellationToken = default)
         {
             var filteredAnalyzersBuilder = ImmutableArray.CreateBuilder<DiagnosticAnalyzer>();
 
@@ -254,8 +256,8 @@ public static async Task<ImmutableArray<DiagnosticAnalyzer>> FilterSuppressedAna
                     filteredAnalyzersBuilder.Add(analyzer);
                 }
 
-                var severity = await analyzer.GetSeverityAsync(project, compilation, cancellationToken);
-                if (severity != ReportDiagnostic.Suppress)
+                var hasMinimumSeverity = await analyzer.HasMinimumSeverityAsync(project, compilation, minimumSeverity, cancellationToken);
+                if (hasMinimumSeverity)
                 {
                     filteredAnalyzersBuilder.Add(analyzer);
                 }
@@ -277,7 +279,8 @@ private async Task AnalyzeAndReport(Solution solution, IGrouping<ProjectId, Docu
 
                 var compilation = await project.GetCompilationAsync();
                 var workspaceAnalyzerOptions = (AnalyzerOptions)_workspaceAnalyzerOptionsConstructor.Invoke(new object[] { project.AnalyzerOptions, project.Solution });
-                var filteredAnalyzers = await FilterSuppressedAnalyzersAsync(allAnalyzers, project, compilation);
+                // Filter out suppressed analyzers by requiring a minimum diagnostic severity of Hidden
+                var filteredAnalyzers = await FilterAnalyzersBySeverityAsync(allAnalyzers, project, compilation, ReportDiagnostic.Hidden);
 
                 var diagnostics = await AnalyzeProjectAsync(project, filteredAnalyzers, compilation, workspaceAnalyzerOptions);
                 var diagnosticsByFilePath = diagnostics

{'addition_count': 12, 'commit_subject': 'When filtering to not calculate final diagnostic severity', 'deletion_count': 9, 'file_extension': '.cs', 'lang': 'Roslyn', 'license': 'mit', 'repo_name': 'OmniSharp/omnisharp-roslyn'}