Skip to content

Commit c0bd75d

Browse files
authored
Move VS Code To Pull Diagnostics (#11602)
After a few reports like microsoft/vscode-dotnettools#1746 and microsoft/vscode-dotnettools#1740 diagnostics are causing users some headaches (and a few other issues I can't find) In VS we avoid this by using pull diagnostics. In VS Code we publish all diagnostics in the workspace. This PR simply makes it so VS Code now also uses pull diagnostics! There are a few caveats to this: 1. HTML diagnostics aren't supported. As it turns out, they weren't before anyways. VS Code has no way to ask for them in a generic manner. This isn't a regression but I'll call it out 2. Workspace diagnostics aren't supported (yet). Users may notice that diagnostics aren't reported until a razor file is open. The generated document will still get C# diagnostics through Roslyn workspace diagnostics (I think? Assuming dynamic files aren't filtered...). This matches our VS behavior 3. This duplicates some code adn tests. VS had it's own pull diagnostics prior to it being in the spec. I think we can merge the two but I'm leaving that out of the scope of this PR. This will require a PR in VS Code to adapt to the new data sent over our custom LSP method.
1 parent b102290 commit c0bd75d

20 files changed

+377
-1033
lines changed

src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDiagnosticsBenchmark.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Razor.Microbenchmarks.LanguageServer;
3131
[ShortRunJob]
3232
public class RazorDiagnosticsBenchmark : RazorLanguageServerBenchmarkBase
3333
{
34-
private DocumentPullDiagnosticsEndpoint? DocumentPullDiagnosticsEndpoint { get; set; }
34+
private VSDocumentDiagnosticsEndpoint? DocumentPullDiagnosticsEndpoint { get; set; }
3535
private RazorRequestContext RazorRequestContext { get; set; }
3636
private RazorCodeDocument? RazorCodeDocument { get; set; }
3737
private SourceText? SourceText { get; set; }
@@ -94,7 +94,7 @@ public void Setup()
9494

9595
var optionsMonitor = Mock.Of<RazorLSPOptionsMonitor>(MockBehavior.Strict);
9696
var translateDiagnosticsService = new RazorTranslateDiagnosticsService(documentMappingService, loggerFactory);
97-
DocumentPullDiagnosticsEndpoint = new DocumentPullDiagnosticsEndpoint(languageServerFeatureOptions, translateDiagnosticsService, optionsMonitor, languageServer, telemetryReporter: null);
97+
DocumentPullDiagnosticsEndpoint = new VSDocumentDiagnosticsEndpoint(languageServerFeatureOptions, translateDiagnosticsService, optionsMonitor, languageServer, telemetryReporter: null);
9898
}
9999

100100
private object BuildDiagnostics()
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Razor.Language;
8+
using Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;
9+
using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts;
10+
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
11+
using Microsoft.AspNetCore.Razor.PooledObjects;
12+
using Microsoft.AspNetCore.Razor.ProjectSystem;
13+
using Microsoft.AspNetCore.Razor.Telemetry;
14+
using Microsoft.CodeAnalysis.Razor.Diagnostics;
15+
using Microsoft.CodeAnalysis.Razor.Protocol;
16+
using Microsoft.CodeAnalysis.Razor.Workspaces.Telemetry;
17+
using Microsoft.VisualStudio.LanguageServer.Protocol;
18+
19+
namespace Microsoft.AspNetCore.Razor.LanguageServer.Hosting.Diagnostics;
20+
21+
[RazorLanguageServerEndpoint(Methods.TextDocumentDiagnosticName)]
22+
internal sealed class DocumentDiagnosticsEndpoint(
23+
RazorTranslateDiagnosticsService translateDiagnosticsService,
24+
IClientConnection clientConnection,
25+
ITelemetryReporter? telemetryReporter)
26+
: IRazorRequestHandler<DocumentDiagnosticParams, FullDocumentDiagnosticReport?>, ICapabilitiesProvider
27+
{
28+
private readonly RazorTranslateDiagnosticsService _translateDiagnosticsService = translateDiagnosticsService;
29+
private readonly IClientConnection _clientConnection = clientConnection;
30+
private readonly ITelemetryReporter? _telemetryReporter = telemetryReporter;
31+
private readonly MissingTagHelperTelemetryReporter? _missingTagHelperTelemetryReporter = telemetryReporter is null ? null : new(telemetryReporter);
32+
33+
public bool MutatesSolutionState => false;
34+
35+
public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentDiagnosticParams request)
36+
=> request.TextDocument;
37+
38+
public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities)
39+
{
40+
serverCapabilities.SupportsDiagnosticRequests = true;
41+
serverCapabilities.DiagnosticOptions = new()
42+
{
43+
InterFileDependencies = false,
44+
WorkspaceDiagnostics = false,
45+
WorkDoneProgress = false
46+
};
47+
}
48+
49+
public async Task<FullDocumentDiagnosticReport?> HandleRequestAsync(DocumentDiagnosticParams request, RazorRequestContext context, CancellationToken cancellationToken)
50+
{
51+
var documentContext = context.DocumentContext;
52+
if (documentContext is null)
53+
{
54+
return null;
55+
}
56+
57+
var correlationId = Guid.NewGuid();
58+
using var __ = _telemetryReporter?.TrackLspRequest(Methods.TextDocumentDiagnosticName, LanguageServerConstants.RazorLanguageServerName, TelemetryThresholds.DiagnosticsRazorTelemetryThreshold, correlationId);
59+
60+
var documentSnapshot = documentContext.Snapshot;
61+
var razorDiagnostics = await RazorDiagnosticHelper.GetRazorDiagnosticsAsync(documentSnapshot, cancellationToken).ConfigureAwait(false);
62+
var csharpDiagnostics = await GetCSharpDiagnosticsAsync(documentSnapshot, request.TextDocument, correlationId, cancellationToken).ConfigureAwait(false);
63+
64+
var diagnosticCount =
65+
(razorDiagnostics?.Length ?? 0) +
66+
(csharpDiagnostics?.Length ?? 0);
67+
68+
using var _ = ListPool<Diagnostic>.GetPooledObject(out var allDiagnostics);
69+
allDiagnostics.SetCapacityIfLarger(diagnosticCount);
70+
71+
if (razorDiagnostics is not null)
72+
{
73+
// No extra work to do for Razor diagnostics
74+
allDiagnostics.AddRange(razorDiagnostics);
75+
76+
if (_missingTagHelperTelemetryReporter is not null)
77+
{
78+
await _missingTagHelperTelemetryReporter.ReportRZ10012TelemetryAsync(documentContext, razorDiagnostics, cancellationToken).ConfigureAwait(false);
79+
}
80+
}
81+
82+
if (csharpDiagnostics is not null)
83+
{
84+
var mappedDiagnostics = await _translateDiagnosticsService
85+
.TranslateAsync(RazorLanguageKind.CSharp, csharpDiagnostics, documentSnapshot, cancellationToken)
86+
.ConfigureAwait(false);
87+
allDiagnostics.AddRange(mappedDiagnostics);
88+
}
89+
90+
return new()
91+
{
92+
Items = [.. allDiagnostics]
93+
};
94+
}
95+
96+
private async Task<Diagnostic[]?> GetCSharpDiagnosticsAsync(IDocumentSnapshot documentSnapshot, TextDocumentIdentifier razorDocumentIdentifier, Guid correlationId, CancellationToken cancellationToken)
97+
{
98+
var delegatedParams = new DelegatedDiagnosticParams(
99+
new(razorDocumentIdentifier, documentSnapshot.Version),
100+
correlationId
101+
);
102+
103+
var delegatedResponse = await _clientConnection
104+
.SendRequestAsync<DelegatedDiagnosticParams, SumType<FullDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport>?>(
105+
CustomMessageNames.RazorCSharpPullDiagnosticsEndpointName,
106+
delegatedParams,
107+
cancellationToken)
108+
.ConfigureAwait(false);
109+
110+
return delegatedResponse.HasValue
111+
? delegatedResponse.Value.TryGetFirst(out var fullReport)
112+
? fullReport.Items
113+
: null
114+
: null;
115+
}
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.Immutable;
7+
using System.Linq;
8+
using System.Text;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.AspNetCore.Razor.Language.Components;
12+
using Microsoft.AspNetCore.Razor.ProjectSystem;
13+
using Microsoft.AspNetCore.Razor.Telemetry;
14+
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
15+
using Microsoft.VisualStudio.LanguageServer.Protocol;
16+
17+
namespace Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics;
18+
19+
internal sealed class MissingTagHelperTelemetryReporter(ITelemetryReporter telemetryReporter)
20+
{
21+
private readonly ITelemetryReporter _telemetryReporter = telemetryReporter;
22+
private ImmutableDictionary<ProjectKey, int> _lastReportedProjectTagHelperCount = ImmutableDictionary<ProjectKey, int>.Empty;
23+
24+
/// <summary>
25+
/// Reports telemetry for RZ10012 "Found markup element with unexpected name" to help track down potential issues
26+
/// with taghelpers being discovered (or lack thereof)
27+
/// </summary>
28+
public async ValueTask ReportRZ10012TelemetryAsync(DocumentContext documentContext, IEnumerable<Diagnostic> razorDiagnostics, CancellationToken cancellationToken)
29+
{
30+
var relevantDiagnosticsCount = razorDiagnostics.Count(d => d.Code == ComponentDiagnosticFactory.UnexpectedMarkupElement.Id);
31+
if (relevantDiagnosticsCount == 0)
32+
{
33+
return;
34+
}
35+
36+
var tagHelpers = await documentContext.Project.GetTagHelpersAsync(cancellationToken).ConfigureAwait(false);
37+
var tagHelperCount = tagHelpers.Length;
38+
var shouldReport = false;
39+
40+
ImmutableInterlocked.AddOrUpdate(
41+
ref _lastReportedProjectTagHelperCount,
42+
documentContext.Project.Key,
43+
(k) =>
44+
{
45+
shouldReport = true;
46+
return tagHelperCount;
47+
},
48+
(k, currentValue) =>
49+
{
50+
shouldReport = currentValue != tagHelperCount;
51+
return tagHelperCount;
52+
});
53+
54+
if (shouldReport)
55+
{
56+
_telemetryReporter.ReportEvent(
57+
"RZ10012",
58+
Severity.Low,
59+
new("tagHelpers", tagHelperCount),
60+
new("RZ10012errors", relevantDiagnosticsCount),
61+
new("project", documentContext.Project.Key.Id));
62+
}
63+
}
64+
}

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.Comparer.cs

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.TestAccessor.cs

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)