Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable LSP pull model diagnostic for XAML. #49145

Merged
merged 3 commits into from
Nov 9, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ private static LSP.DiagnosticSeverity ConvertDiagnosticSeverity(DiagnosticSeveri
_ => throw ExceptionUtilities.UnexpectedValue(severity),
};

/// <summary>
/// If you make change in this method, please also update the corresponding file in
/// src\VisualStudio\Xaml\Impl\Implementation\LanguageServer\Handler\Diagnostics\AbstractPullDiagnosticHandler.cs
/// </summary>
private static DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData)
{
using var _ = ArrayBuilder<DiagnosticTag>.GetInstance(out var result);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Host;

namespace Microsoft.VisualStudio.LanguageServices.Xaml.Features.Diagnostics
{
internal interface IXamlPullDiagnosticService : ILanguageService
{
/// <summary>
/// Get diagnostic report for the given TextDocument.
/// </summary>
/// <param name="document">The TextDocument to get diagnostic report from. Should not be null.</param>
/// <param name="previousResultId">Previous ResultId we get from the Pull Diagnostic request. This can null when we don't see a corresponding previousResultId for this document from the request.</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns>A XamlDiagnosticReport which will be used as the response to the Pull Diagnostic request.</returns>
Task<XamlDiagnosticReport> GetDiagnosticReportAsync(TextDocument document, string? previousResultId, CancellationToken cancellationToken);
}
}
18 changes: 18 additions & 0 deletions src/VisualStudio/Xaml/Impl/Features/Diagnostics/XamlDiagnostic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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.

namespace Microsoft.VisualStudio.LanguageServices.Xaml.Features.Diagnostics
{
internal class XamlDiagnostic
{
public string? Code { get; set; }
public string? Message { get; set; }
public XamlDiagnosticSeverity Severity { get; set; }
public int Offset { get; set; }
public int Length { get; set; }
public string? Tool { get; set; }
public string? ExtendedMessage { get; set; }
public string[]? CustomTags { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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.Collections.Immutable;

namespace Microsoft.VisualStudio.LanguageServices.Xaml.Features.Diagnostics
{
internal class XamlDiagnosticReport
LinglingTong marked this conversation as resolved.
Show resolved Hide resolved
{
public string? ResultId { get; set; }
public ImmutableArray<XamlDiagnostic>? Diagnostics { get; set; }

public XamlDiagnosticReport(string? resultId = null, ImmutableArray<XamlDiagnostic>? diagnostics = null)
{
this.ResultId = resultId;
this.Diagnostics = diagnostics;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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.

namespace Microsoft.VisualStudio.LanguageServices.Xaml.Features.Diagnostics
{
internal enum XamlDiagnosticSeverity
{
/// <summary>
/// Represents an error.
/// </summary>
Error,

/// <summary>
/// Represent a warning.
/// </summary>
Warning,

/// <summary>
/// Represents an informational note.
/// </summary>
Message,

/// <summary>
/// Represents a hidden note.
/// </summary>
Hidden
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ protected internal override VSServerCapabilities GetCapabilities()
{
Change = TextDocumentSyncKind.None
},
SupportsDiagnosticRequests = true,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Xaml;

namespace Microsoft.VisualStudio.LanguageServices.Xaml.Implementation.LanguageServer.Extensions
{
internal static class SolutionExtensions
{
public static IEnumerable<Project> GetXamlProjects(this Solution solution)
=> solution.Projects.Where(p => p.Language == StringConstants.XamlLanguageName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// 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 Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.LanguageServices.Xaml.Features.Diagnostics;
using Roslyn.Utilities;
using LSP = Microsoft.VisualStudio.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> : IRequestHandler<TDiagnosticsParams, TReport[]?>
where TReport : DiagnosticReport
{
private readonly ILspSolutionProvider _solutionProvider;
private readonly IXamlPullDiagnosticService _xamlDiagnosticService;

public abstract TextDocumentIdentifier? GetTextDocumentIdentifier(TDiagnosticsParams request);

/// <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 DiagnosticParams[]? 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="DiagnosticReport"/> 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(ILspSolutionProvider solutionProvider, IXamlPullDiagnosticService xamlDiagnosticService)
{
_solutionProvider = solutionProvider;
_xamlDiagnosticService = xamlDiagnosticService;
}

public async Task<TReport[]?> HandleRequestAsync(TDiagnosticsParams diagnosticsParams, RequestContext context, CancellationToken cancellationToken)
{
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 = _solutionProvider.GetDocument(previousResult.TextDocument);
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.GetTextAsync(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.GetValues();
}

/// <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,
Projects = new[]
{
new ProjectAndContext
{
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.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\Features\LanguageServer\Protocol\Handler\Diagnostics\AbstractPullDiagnosticHandler.cs
/// </summary>
private static DiagnosticTag[] ConvertTags(XamlDiagnostic diagnostic)
LinglingTong marked this conversation as resolved.
Show resolved Hide resolved
{
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
{
result.Add(VSDiagnosticTags.VisibleInErrorList);
}

if (diagnostic.CustomTags?.Contains(WellKnownDiagnosticTags.Unnecessary) == true)
result.Add(DiagnosticTag.Unnecessary);

return result.ToArray();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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.Composition;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Xaml;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.LanguageServices.Xaml.Features.Diagnostics;

namespace Microsoft.VisualStudio.LanguageServices.Xaml.Implementation.LanguageServer.Handler.Diagnostics
{
[ExportLspMethod(MSLSPMethods.DocumentPullDiagnosticName, mutatesSolutionState: false, StringConstants.XamlLanguageName), Shared]
internal class DocumentPullDiagnosticHandler : AbstractPullDiagnosticHandler<DocumentDiagnosticsParams, DiagnosticReport>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public DocumentPullDiagnosticHandler(
ILspSolutionProvider solutionProvider,
IXamlPullDiagnosticService xamlPullDiagnosticService)
: base(solutionProvider, xamlPullDiagnosticService)
{ }

public override TextDocumentIdentifier? GetTextDocumentIdentifier(DocumentDiagnosticsParams request)
=> request.TextDocument;

protected override DiagnosticReport CreateReport(TextDocumentIdentifier? identifier, VSDiagnostic[]? diagnostics, string? resultId)
=> new DiagnosticReport { Diagnostics = diagnostics, ResultId = resultId };

protected override ImmutableArray<Document> GetDocuments(RequestContext context)
{
// For the single document case, that is the only doc we want to process.
//
// Note: context.Document may be null in the case where the client is asking about a document that we have
// since removed from the workspace. In this case, we don't really have anything to process.
// GetPreviousResults will be used to properly realize this and notify the client that the doc is gone.
return context.Document == null ? ImmutableArray<Document>.Empty : ImmutableArray.Create(context.Document);
}

protected override DiagnosticParams[]? GetPreviousResults(DocumentDiagnosticsParams diagnosticsParams)
=> new[] { diagnosticsParams };

protected override IProgress<DiagnosticReport[]>? GetProgress(DocumentDiagnosticsParams diagnosticsParams)
=> diagnosticsParams.PartialResultToken;
}
}
Loading