File: Implementation\LanguageServer\Handler\Diagnostics\AbstractPullDiagnosticHandler.cs
Web Access
Project: src\src\VisualStudio\Xaml\Impl\Microsoft.VisualStudio.LanguageServices.Xaml.csproj (Microsoft.VisualStudio.LanguageServices.Xaml)
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.LanguageServer.Protocol;
using Microsoft.VisualStudio.LanguageServices.Xaml.Features.Diagnostics;
using Roslyn.Utilities;
using LSP = Roslyn.LanguageServer.Protocol;
 
namespace Microsoft.VisualStudio.LanguageServices.Xaml.Implementation.LanguageServer.Handler.Diagnostics
{
    /// <summary>
    /// Root type for both document and workspace diagnostic pull requests.
    /// </summary>
    internal abstract class AbstractPullDiagnosticHandler<TDiagnosticsParams, TReport> : ILspServiceRequestHandler<TDiagnosticsParams, TReport[]?>
        where TReport : VSInternalDiagnosticReport
    {
        private readonly IXamlPullDiagnosticService _xamlDiagnosticService;
 
        public bool MutatesSolutionState => false;
        public bool RequiresLSPSolution => true;
 
        /// <summary>
        /// Gets the progress object to stream results to.
        /// </summary>
        protected abstract IProgress<TReport[]>? GetProgress(TDiagnosticsParams diagnosticsParams);
 
        /// <summary>
        /// Retrieve the previous results we reported.
        /// </summary>
        protected abstract VSInternalDiagnosticParams[]? GetPreviousResults(TDiagnosticsParams diagnosticsParams);
 
        /// <summary>
        /// Returns all the documents that should be processed.
        /// </summary>
        protected abstract ImmutableArray<Document> GetDocuments(RequestContext context);
 
        /// <summary>
        /// Creates the <see cref="VSInternalDiagnosticReport"/> instance we'll report back to clients to let them know our
        /// progress. 
        /// </summary>
        protected abstract TReport CreateReport(TextDocumentIdentifier? identifier, VSDiagnostic[]? diagnostics, string? resultId);
 
        protected AbstractPullDiagnosticHandler(IXamlPullDiagnosticService xamlDiagnosticService)
        {
            _xamlDiagnosticService = xamlDiagnosticService;
        }
 
        public async Task<TReport[]?> HandleRequestAsync(TDiagnosticsParams diagnosticsParams, RequestContext context, CancellationToken cancellationToken)
        {
            Contract.ThrowIfNull(context.Solution);
 
            using var progress = BufferedProgress.Create(GetProgress(diagnosticsParams));
 
            // Get the set of results the request said were previously reported.
            var previousResults = GetPreviousResults(diagnosticsParams);
 
            var documentToPreviousResultId = new Dictionary<Document, string?>();
            if (previousResults != null)
            {
                // Go through the previousResults and check if we need to remove diagnostic information for any documents
                foreach (var previousResult in previousResults)
                {
                    if (previousResult.TextDocument != null)
                    {
                        var document = await context.Solution.GetDocumentAsync(previousResult.TextDocument, cancellationToken).ConfigureAwait(false);
                        if (document == null)
                        {
                            // We can no longer get this document, return null for both diagnostics and resultId
                            progress.Report(CreateReport(previousResult.TextDocument, diagnostics: null, resultId: null));
                        }
                        else
                        {
                            // Cache the document to previousResultId mapping so we can easily retrieve the resultId later.
                            documentToPreviousResultId[document] = previousResult.PreviousResultId;
                        }
                    }
                }
            }
 
            // Go through the documents that we need to process and call XamlPullDiagnosticService to get the diagnostic report
            foreach (var document in GetDocuments(context))
            {
                var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
                var documentId = ProtocolConversions.DocumentToTextDocumentIdentifier(document);
 
                // If we can get a previousId of the document, use it, 
                // otherwise use null as the previousId to pass into the XamlPullDiagnosticService
                var previousResultId = documentToPreviousResultId.TryGetValue(document, out var id) ? id : null;
 
                // Call XamlPullDiagnosticService to get the diagnostic report for this document.
                // We will compute what to report inside XamlPullDiagnosticService, for example, whether we should keep using the previousId or use a new resultId,
                // and the handler here just return the result get from XamlPullDiagnosticService.
                var diagnosticReport = await _xamlDiagnosticService.GetDiagnosticReportAsync(document, previousResultId, cancellationToken).ConfigureAwait(false);
                progress.Report(CreateReport(
                            documentId,
                            ConvertToVSDiagnostics(diagnosticReport.Diagnostics, document, text),
                            diagnosticReport.ResultId));
            }
 
            return progress.GetFlattenedValues();
        }
 
        /// <summary>
        /// Convert XamlDiagnostics to VSDiagnostics
        /// </summary>
        private static VSDiagnostic[]? ConvertToVSDiagnostics(ImmutableArray<XamlDiagnostic>? xamlDiagnostics, Document document, SourceText text)
        {
            if (xamlDiagnostics == null)
            {
                return null;
            }
 
            var project = document.Project;
            return xamlDiagnostics.Value.Select(d => new VSDiagnostic()
            {
                Code = d.Code,
                Message = d.Message ?? string.Empty,
                ExpandedMessage = d.ExtendedMessage,
                Severity = ConvertDiagnosticSeverity(d.Severity),
                Range = ProtocolConversions.TextSpanToRange(new TextSpan(d.Offset, d.Length), text),
                Tags = ConvertTags(d),
                Source = d.Tool,
                CodeDescription = ProtocolConversions.HelpLinkToCodeDescription(d.GetHelpLinkUri()),
                Projects =
                [
                    new VSDiagnosticProjectInformation
                    {
                        ProjectIdentifier = project.Id.Id.ToString(),
                        ProjectName = project.Name,
                    },
                ],
            }).ToArray();
        }
 
        private static LSP.DiagnosticSeverity ConvertDiagnosticSeverity(XamlDiagnosticSeverity severity)
            => severity switch
            {
                // Hidden is translated in ConvertTags to pass along appropriate _ms tags
                // that will hide the item in a client that knows about those tags.
                XamlDiagnosticSeverity.Hidden => LSP.DiagnosticSeverity.Hint,
                XamlDiagnosticSeverity.HintedSuggestion => LSP.DiagnosticSeverity.Hint,
                XamlDiagnosticSeverity.Message => LSP.DiagnosticSeverity.Information,
                XamlDiagnosticSeverity.Warning => LSP.DiagnosticSeverity.Warning,
                XamlDiagnosticSeverity.Error => LSP.DiagnosticSeverity.Error,
                _ => throw ExceptionUtilities.UnexpectedValue(severity),
            };
 
        /// <summary>
        /// If you make change in this method, please also update the corresponding file in
        /// src\\LanguageServer\Protocol\Extensions\ProtocolConversions.Diagnostics.cs
        /// </summary>
        private static DiagnosticTag[] ConvertTags(XamlDiagnostic diagnostic)
        {
            using var _ = ArrayBuilder<DiagnosticTag>.GetInstance(out var result);
 
            result.Add(VSDiagnosticTags.IntellisenseError);
 
            if (diagnostic.Severity == XamlDiagnosticSeverity.Hidden)
            {
                result.Add(VSDiagnosticTags.HiddenInEditor);
                result.Add(VSDiagnosticTags.HiddenInErrorList);
                result.Add(VSDiagnosticTags.SuppressEditorToolTip);
            }
            else if (diagnostic.Severity == XamlDiagnosticSeverity.HintedSuggestion)
            {
                result.Add(VSDiagnosticTags.HiddenInErrorList);
            }
            else
            {
                result.Add(VSDiagnosticTags.VisibleInErrorList);
            }
 
            if (diagnostic.CustomTags?.Contains(WellKnownDiagnosticTags.Unnecessary) == true)
                result.Add(DiagnosticTag.Unnecessary);
 
            return result.ToArray();
        }
    }
}