Skip to content

Commit 3802c7c

Browse files
authored
Merge pull request #6543 from beccamc/u/beccam/dedupeDiagnostics
Dedupe Build and Live Diagnostics
2 parents 98b73f1 + db5d8af commit 3802c7c

File tree

8 files changed

+385
-1
lines changed

8 files changed

+385
-1
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
"name": "Microsoft.CodeAnalysis.LanguageClient.SolutionSnapshotProvider",
5252
"version": "0.1"
5353
}
54+
},
55+
{
56+
"moniker": {
57+
"name": "Microsoft.VisualStudio.CSharpExtension.BuildResultService",
58+
"version": "0.1"
59+
}
5460
}
5561
],
5662
"scripts": {
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import * as vscode from 'vscode';
6+
import { languageServerOptions } from '../shared/options';
7+
8+
export enum AnalysisSetting {
9+
FullSolution = 'fullSolution',
10+
OpenFiles = 'openFiles',
11+
None = 'none',
12+
}
13+
14+
export class BuildDiagnosticsService {
15+
/** All the build results sent by the DevKit extension. */
16+
private _allBuildDiagnostics: { [uri: string]: vscode.Diagnostic[] } = {};
17+
18+
/** The diagnostic results from build displayed by VS Code. When live diagnostics are available for a file, these are errors that only build knows about.
19+
* When live diagnostics aren't loaded for a file, then these are all of the diagnostics reported by the build.*/
20+
private _diagnosticsReportedByBuild: vscode.DiagnosticCollection;
21+
22+
constructor(buildDiagnostics: vscode.DiagnosticCollection) {
23+
this._diagnosticsReportedByBuild = buildDiagnostics;
24+
}
25+
26+
public clearDiagnostics() {
27+
this._diagnosticsReportedByBuild.clear();
28+
}
29+
30+
public async setBuildDiagnostics(buildDiagnostics: { [uri: string]: vscode.Diagnostic[] }, buildOnlyIds: string[]) {
31+
this._allBuildDiagnostics = buildDiagnostics;
32+
const displayedBuildDiagnostics = new Array<[vscode.Uri, vscode.Diagnostic[]]>();
33+
const allDocuments = vscode.workspace.textDocuments;
34+
35+
for (const [uriPath, diagnosticList] of Object.entries(this._allBuildDiagnostics)) {
36+
// Check if the document is open
37+
const uri = vscode.Uri.file(uriPath);
38+
const document = allDocuments.find((d) => this.compareUri(d.uri, uri));
39+
const isDocumentOpen = document !== undefined ? !document.isClosed : false;
40+
41+
// Show the build-only diagnostics
42+
displayedBuildDiagnostics.push([
43+
uri,
44+
BuildDiagnosticsService.filterDiagnosticsFromBuild(diagnosticList, buildOnlyIds, isDocumentOpen),
45+
]);
46+
}
47+
48+
this._diagnosticsReportedByBuild.set(displayedBuildDiagnostics);
49+
}
50+
51+
private compareUri(a: vscode.Uri, b: vscode.Uri): boolean {
52+
return a.fsPath.localeCompare(b.fsPath) === 0;
53+
}
54+
55+
public async _onFileOpened(document: vscode.TextDocument, buildOnlyIds: string[]) {
56+
const uri = document.uri;
57+
const currentFileBuildDiagnostics = this._allBuildDiagnostics[uri.fsPath];
58+
59+
// The document is now open in the editor and live diagnostics are being shown. Filter diagnostics
60+
// reported by the build to show build-only problems.
61+
if (currentFileBuildDiagnostics) {
62+
const buildDiagnostics = BuildDiagnosticsService.filterDiagnosticsFromBuild(
63+
currentFileBuildDiagnostics,
64+
buildOnlyIds,
65+
true
66+
);
67+
this._diagnosticsReportedByBuild.set(uri, buildDiagnostics);
68+
}
69+
}
70+
71+
public static filterDiagnosticsFromBuild(
72+
diagnosticList: vscode.Diagnostic[],
73+
buildOnlyIds: string[],
74+
isDocumentOpen: boolean
75+
): vscode.Diagnostic[] {
76+
const analyzerDiagnosticScope = languageServerOptions.analyzerDiagnosticScope as AnalysisSetting;
77+
const compilerDiagnosticScope = languageServerOptions.compilerDiagnosticScope as AnalysisSetting;
78+
79+
// If compiler and analyzer diagnostics are set to "none", show everything reported by the build
80+
if (analyzerDiagnosticScope === AnalysisSetting.None && compilerDiagnosticScope === AnalysisSetting.None) {
81+
return diagnosticList;
82+
}
83+
84+
// Filter the diagnostics reported by the build. Some may already be shown by live diagnostics.
85+
const buildDiagnosticsToDisplay: vscode.Diagnostic[] = [];
86+
87+
// If it is a project system diagnostic (e.g. "Target framework out of support")
88+
// then always show it. It cannot be reported by live.
89+
const projectSystemDiagnostics = diagnosticList.filter((d) =>
90+
BuildDiagnosticsService.isProjectSystemDiagnostic(d)
91+
);
92+
buildDiagnosticsToDisplay.push(...projectSystemDiagnostics);
93+
94+
// If it is a "build-only"diagnostics (i.e. it can only be found by building)
95+
// then always show it. It cannot be reported by live.
96+
const buildOnlyDiagnostics = diagnosticList.filter((d) =>
97+
BuildDiagnosticsService.isBuildOnlyDiagnostic(buildOnlyIds, d)
98+
);
99+
buildDiagnosticsToDisplay.push(...buildOnlyDiagnostics);
100+
101+
// Check the analyzer diagnostic setting. If the setting is "none" or if the file is closed,
102+
// then no live analyzers are being shown and bulid analyzers should be added.
103+
// If FSA is on, then this is a no-op as FSA will report all analyzer diagnostics
104+
if (
105+
analyzerDiagnosticScope === AnalysisSetting.None ||
106+
(analyzerDiagnosticScope === AnalysisSetting.OpenFiles && !isDocumentOpen)
107+
) {
108+
const analyzerDiagnostics = diagnosticList.filter(
109+
// Needs to be analyzer diagnostics and not already reported as "build only"
110+
(d) => BuildDiagnosticsService.isAnalyzerDiagnostic(d) && !this.isBuildOnlyDiagnostic(buildOnlyIds, d)
111+
);
112+
buildDiagnosticsToDisplay.push(...analyzerDiagnostics);
113+
}
114+
115+
// Check the compiler diagnostic setting. If the setting is "none" or if the file is closed,
116+
// then no live compiler diagnostics are being shown and bulid compiler diagnostics should be added.
117+
// If FSA is on, then this is a no-op as FSA will report all compiler diagnostics
118+
if (
119+
compilerDiagnosticScope === AnalysisSetting.None ||
120+
(compilerDiagnosticScope === AnalysisSetting.OpenFiles && !isDocumentOpen)
121+
) {
122+
const compilerDiagnostics = diagnosticList.filter(
123+
// Needs to be analyzer diagnostics and not already reported as "build only"
124+
(d) => BuildDiagnosticsService.isCompilerDiagnostic(d) && !this.isBuildOnlyDiagnostic(buildOnlyIds, d)
125+
);
126+
buildDiagnosticsToDisplay.push(...compilerDiagnostics);
127+
}
128+
129+
return buildDiagnosticsToDisplay;
130+
}
131+
132+
private static isBuildOnlyDiagnostic(buildOnlyIds: string[], d: vscode.Diagnostic): boolean {
133+
return buildOnlyIds.find((b_id) => b_id === d.code) !== undefined;
134+
}
135+
136+
private static isCompilerDiagnostic(d: vscode.Diagnostic): boolean {
137+
const regex = '[cC][sS][0-9]{4}';
138+
return d.code ? d.code.toString().match(regex) !== null : false;
139+
}
140+
141+
private static isAnalyzerDiagnostic(d: vscode.Diagnostic): boolean {
142+
return d.code ? !this.isCompilerDiagnostic(d) : false;
143+
}
144+
145+
private static isProjectSystemDiagnostic(d: vscode.Diagnostic): boolean {
146+
return d.code ? d.code.toString().startsWith('NETSDK') : false;
147+
}
148+
}

src/lsptoolshost/roslynLanguageServer.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
SocketMessageReader,
2727
MessageTransports,
2828
RAL,
29+
CancellationToken,
2930
} from 'vscode-languageclient/node';
3031
import { PlatformInformation } from '../shared/platform';
3132
import { readConfigurations } from './configurationMiddleware';
@@ -57,6 +58,7 @@ import { commonOptions, languageServerOptions, omnisharpOptions } from '../share
5758
import { NamedPipeInformation } from './roslynProtocol';
5859
import { IDisposable } from '../disposable';
5960
import { registerRestoreCommands } from './restore';
61+
import { BuildDiagnosticsService } from './buildDiagnosticsService';
6062

6163
let _channel: vscode.OutputChannel;
6264
let _traceChannel: vscode.OutputChannel;
@@ -95,6 +97,8 @@ export class RoslynLanguageServer {
9597
/** The project files previously opened; we hold onto this for the same reason as _solutionFile. */
9698
private _projectFiles: vscode.Uri[] = new Array<vscode.Uri>();
9799

100+
public _buildDiagnosticService: BuildDiagnosticsService;
101+
98102
constructor(
99103
private _languageClient: RoslynLanguageClient,
100104
private _platformInfo: PlatformInformation,
@@ -109,6 +113,10 @@ export class RoslynLanguageServer {
109113
this.registerExtensionsChanged();
110114
this.registerTelemetryChanged();
111115

116+
const diagnosticsReportedByBuild = vscode.languages.createDiagnosticCollection('csharp-build');
117+
this._buildDiagnosticService = new BuildDiagnosticsService(diagnosticsReportedByBuild);
118+
this.registerDocumentOpenForDiagnostics();
119+
112120
// Register Razor dynamic file info handling
113121
this.registerDynamicFileInfo();
114122

@@ -570,7 +578,7 @@ export class RoslynLanguageServer {
570578
});
571579
});
572580

573-
// The server process will create the named pipe used for communcation. Wait for it to be created,
581+
// The server process will create the named pipe used for communication. Wait for it to be created,
574582
// and listen for the server to pass back the connection information via stdout.
575583
const namedPipeConnectionPromise = new Promise<NamedPipeInformation>((resolve) => {
576584
_channel.appendLine('waiting for named pipe information from server...');
@@ -696,6 +704,16 @@ export class RoslynLanguageServer {
696704
);
697705
}
698706

707+
private registerDocumentOpenForDiagnostics() {
708+
// When a file is opened process any build diagnostics that may be shown
709+
this._languageClient.addDisposable(
710+
vscode.workspace.onDidOpenTextDocument(async (event) => {
711+
const buildIds = await this.getBuildOnlyDiagnosticIds(CancellationToken.None);
712+
this._buildDiagnosticService._onFileOpened(event, buildIds);
713+
})
714+
);
715+
}
716+
699717
private registerExtensionsChanged() {
700718
// subscribe to extension change events so that we can get notified if C# Dev Kit is added/removed later.
701719
this._languageClient.addDisposable(
@@ -817,6 +835,11 @@ export class RoslynLanguageServer {
817835
}
818836

819837
public async getBuildOnlyDiagnosticIds(token: vscode.CancellationToken): Promise<string[]> {
838+
// If the server isn't running, no build diagnostics to get
839+
if (!this.isRunning()) {
840+
return [];
841+
}
842+
820843
const response = await this.sendRequest0(RoslynProtocol.BuildOnlyDiagnosticIdsRequest.type, token);
821844
if (response) {
822845
return response.ids;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import { RoslynLanguageServer } from '../roslynLanguageServer';
6+
import { CancellationToken } from 'vscode-jsonrpc';
7+
import * as vscode from 'vscode';
8+
9+
interface IBuildResultDiagnostics {
10+
buildStarted(cancellationToken?: CancellationToken): Promise<void>;
11+
reportBuildResult(
12+
buildDiagnostics: { [uri: string]: vscode.Diagnostic[] },
13+
cancellationToken?: CancellationToken
14+
): Promise<void>;
15+
}
16+
17+
export class BuildResultDiagnostics implements IBuildResultDiagnostics {
18+
constructor(private _languageServerPromise: Promise<RoslynLanguageServer>) {}
19+
20+
public async buildStarted(): Promise<void> {
21+
const langServer: RoslynLanguageServer = await this._languageServerPromise;
22+
langServer._buildDiagnosticService.clearDiagnostics();
23+
}
24+
25+
public async reportBuildResult(buildDiagnostics: { [uri: string]: vscode.Diagnostic[] }): Promise<void> {
26+
const langServer: RoslynLanguageServer = await this._languageServerPromise;
27+
const buildOnlyIds: string[] = await langServer.getBuildOnlyDiagnosticIds(CancellationToken.None);
28+
langServer._buildDiagnosticService.setBuildDiagnostics(buildDiagnostics, buildOnlyIds);
29+
}
30+
}

src/lsptoolshost/services/descriptors.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,13 @@ export default class Descriptors {
3232
protocolMajorVersion: 3,
3333
}
3434
);
35+
36+
static readonly csharpExtensionBuildResultService: ServiceRpcDescriptor = new ServiceJsonRpcDescriptor(
37+
ServiceMoniker.create('Microsoft.VisualStudio.CSharpExtension.BuildResultService', '0.1'),
38+
Formatters.MessagePack,
39+
MessageDelimiters.BigEndianInt32LengthHeader,
40+
{
41+
protocolMajorVersion: 3,
42+
}
43+
);
3544
}

src/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { ServerStateChange } from './lsptoolshost/serverStateChange';
5757
import { SolutionSnapshotProvider } from './lsptoolshost/services/solutionSnapshotProvider';
5858
import { RazorTelemetryDownloader } from './razor/razorTelemetryDownloader';
5959
import { commonOptions, omnisharpOptions, razorOptions } from './shared/options';
60+
import { BuildResultDiagnostics } from './lsptoolshost/services/buildResultReporterService';
6061
import { debugSessionTracker } from './coreclrDebug/provisionalDebugSessionTracker';
6162

6263
export async function activate(
@@ -431,6 +432,10 @@ function profferBrokeredServices(
431432
serviceContainer.profferServiceFactory(
432433
Descriptors.solutionSnapshotProviderRegistration,
433434
(_mk, _op, _sb) => new SolutionSnapshotProvider(languageServerPromise)
435+
),
436+
serviceContainer.profferServiceFactory(
437+
Descriptors.csharpExtensionBuildResultService,
438+
(_mk, _op, _sb) => new BuildResultDiagnostics(languageServerPromise)
434439
)
435440
);
436441
}

src/shared/options.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ export interface LanguageServerOptions {
7575
readonly preferCSharpExtension: boolean;
7676
readonly startTimeout: number;
7777
readonly crashDumpPath: string | undefined;
78+
readonly analyzerDiagnosticScope: string;
79+
readonly compilerDiagnosticScope: string;
7880
}
7981

8082
export interface RazorOptions {
@@ -389,6 +391,12 @@ class LanguageServerOptionsImpl implements LanguageServerOptions {
389391
public get crashDumpPath() {
390392
return readOption<string | undefined>('dotnet.server.crashDumpPath', undefined);
391393
}
394+
public get analyzerDiagnosticScope() {
395+
return readOption<string>('dotnet.backgroundAnalysis.analyzerDiagnosticsScope', 'openFiles');
396+
}
397+
public get compilerDiagnosticScope() {
398+
return readOption<string>('dotnet.backgroundAnalysis.compilerDiagnosticsScope', 'openFiles');
399+
}
392400
}
393401

394402
class RazorOptionsImpl implements RazorOptions {

0 commit comments

Comments
 (0)