diff --git a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj
index 111fb9d55aa3a..62f80f3ff769f 100644
--- a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj
+++ b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj
@@ -118,4 +118,7 @@
+
+
+
\ No newline at end of file
diff --git a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs b/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs
index ac56a720a1d32..179513c9f44a9 100644
--- a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs
+++ b/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs
@@ -639,6 +639,13 @@ private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Str
return result;
}
+ public async Task ExecuteRequest0Async(string methodName, CancellationToken cancellationToken)
+ {
+ // If creating the LanguageServer threw we might timeout without this.
+ var result = await _clientRpc.InvokeWithParameterObjectAsync(methodName, cancellationToken: cancellationToken).ConfigureAwait(false);
+ return result;
+ }
+
public async Task OpenDocumentAsync(Uri documentUri, string? text = null, string languageId = "")
{
if (text == null)
diff --git a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs b/src/Features/CSharp/Portable/Diagnostics/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs
similarity index 98%
rename from src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs
rename to src/Features/CSharp/Portable/Diagnostics/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs
index 2276570b8eca8..f814b068dbf36 100644
--- a/src/EditorFeatures/CSharp/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs
+++ b/src/Features/CSharp/Portable/Diagnostics/LanguageServer/CSharpLspBuildOnlyDiagnostics.cs
@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
-using System.ComponentModel.Composition;
+using System.Composition;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer;
@@ -12,6 +12,7 @@ namespace Microsoft.CodeAnalysis.CSharp.LanguageServer
// Keep in sync with IsBuildOnlyDiagnostic
// src\Compilers\CSharp\Portable\Errors\ErrorFacts.cs
[LspBuildOnlyDiagnostics(
+ LanguageNames.CSharp,
"CS1607", // ErrorCode.WRN_ALinkWarn:
"CS0169", // ErrorCode.WRN_UnreferencedField:
"CS0414", // ErrorCode.WRN_UnreferencedFieldAssg:
@@ -58,6 +59,7 @@ namespace Microsoft.CodeAnalysis.CSharp.LanguageServer
"CS9177", // ErrorCode.ERR_InterceptorArityNotCompatible
"CS9178" // ErrorCode.ERR_InterceptorCannotBeGeneric
)]
+ [Shared]
internal sealed class CSharpLspBuildOnlyDiagnostics : ILspBuildOnlyDiagnostics
{
[ImportingConstructor]
diff --git a/src/EditorFeatures/Core/LanguageServer/Diagnostics/ILspBuildOnlyDiagnostics.cs b/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnostics.cs
similarity index 82%
rename from src/EditorFeatures/Core/LanguageServer/Diagnostics/ILspBuildOnlyDiagnostics.cs
rename to src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnostics.cs
index 2cefdb5bc48be..a138b8b294353 100644
--- a/src/EditorFeatures/Core/LanguageServer/Diagnostics/ILspBuildOnlyDiagnostics.cs
+++ b/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnostics.cs
@@ -6,7 +6,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer
{
///
/// Marker interface for individual Roslyn languages to expose what diagnostics IDs they have are 'build only'. This
- /// affects how the VS LSP client will handle and dedupe related diagnostics produced by Roslyn for live diagnostics
+ /// affects how the LSP client will handle and dedupe related diagnostics produced by Roslyn for live diagnostics
/// against the diagnostics produced by CPS when a build is performed.
///
internal interface ILspBuildOnlyDiagnostics
diff --git a/src/EditorFeatures/Core/LanguageServer/Diagnostics/ILspBuildOnlyDiagnosticsMetadata.cs b/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnosticsMetadata.cs
similarity index 92%
rename from src/EditorFeatures/Core/LanguageServer/Diagnostics/ILspBuildOnlyDiagnosticsMetadata.cs
rename to src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnosticsMetadata.cs
index cd8bba7435b82..d9e5226e3886b 100644
--- a/src/EditorFeatures/Core/LanguageServer/Diagnostics/ILspBuildOnlyDiagnosticsMetadata.cs
+++ b/src/Features/Core/Portable/Diagnostics/LanguageServer/ILspBuildOnlyDiagnosticsMetadata.cs
@@ -7,6 +7,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer
///
internal interface ILspBuildOnlyDiagnosticsMetadata
{
+ string LanguageName { get; }
string[] BuildOnlyDiagnostics { get; }
}
}
diff --git a/src/EditorFeatures/Core/LanguageServer/Diagnostics/LspBuildOnlyDiagnosticsAttribute.cs b/src/Features/Core/Portable/Diagnostics/LanguageServer/LspBuildOnlyDiagnosticsAttribute.cs
similarity index 63%
rename from src/EditorFeatures/Core/LanguageServer/Diagnostics/LspBuildOnlyDiagnosticsAttribute.cs
rename to src/Features/Core/Portable/Diagnostics/LanguageServer/LspBuildOnlyDiagnosticsAttribute.cs
index c1d31db249fff..ddbd266652e6b 100644
--- a/src/EditorFeatures/Core/LanguageServer/Diagnostics/LspBuildOnlyDiagnosticsAttribute.cs
+++ b/src/Features/Core/Portable/Diagnostics/LanguageServer/LspBuildOnlyDiagnosticsAttribute.cs
@@ -3,15 +3,16 @@
// See the LICENSE file in the project root for more information.
using System;
-using System.ComponentModel.Composition;
+using System.Composition;
namespace Microsoft.CodeAnalysis.LanguageServer
{
///
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class)]
- internal class LspBuildOnlyDiagnosticsAttribute(params string[] buildOnlyDiagnostics) : ExportAttribute(typeof(ILspBuildOnlyDiagnostics)), ILspBuildOnlyDiagnosticsMetadata
+ internal class LspBuildOnlyDiagnosticsAttribute(string languageName, params string[] buildOnlyDiagnostics) : ExportAttribute(typeof(ILspBuildOnlyDiagnostics)), ILspBuildOnlyDiagnosticsMetadata
{
+ public string LanguageName { get; } = languageName;
public string[] BuildOnlyDiagnostics { get; } = buildOnlyDiagnostics;
}
}
diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/BuildOnlyDiagnosticIdsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/BuildOnlyDiagnosticIdsHandler.cs
new file mode 100644
index 0000000000000..47f2c0586c2d8
--- /dev/null
+++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/BuildOnlyDiagnosticIdsHandler.cs
@@ -0,0 +1,80 @@
+// 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.Composition;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Host.Mef;
+using Microsoft.CodeAnalysis.PooledObjects;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.LanguageServer.Handler;
+
+[DataContract]
+internal record class BuildOnlyDiagnosticIdsResult([property: DataMember(Name = "ids")] string[] Ids);
+
+[ExportCSharpVisualBasicStatelessLspService(typeof(BuildOnlyDiagnosticIdsHandler)), Shared]
+[Method(BuildOnlyDiagnosticIdsMethodName)]
+[method: ImportingConstructor]
+[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
+internal sealed class BuildOnlyDiagnosticIdsHandler(
+ DiagnosticAnalyzerInfoCache.SharedGlobalCache globalCache,
+ [ImportMany] IEnumerable> compilerBuildOnlyDiagnosticsProviders)
+ : ILspServiceRequestHandler
+{
+ public const string BuildOnlyDiagnosticIdsMethodName = "workspace/buildOnlyDiagnosticIds";
+
+ private readonly DiagnosticAnalyzerInfoCache.SharedGlobalCache _globalCache = globalCache;
+ private readonly ImmutableDictionary _compilerBuildOnlyDiagnosticIds = compilerBuildOnlyDiagnosticsProviders
+ .ToImmutableDictionary(lazy => lazy.Metadata.LanguageName, lazy => lazy.Metadata.BuildOnlyDiagnostics);
+
+ public bool MutatesSolutionState => false;
+ public bool RequiresLSPSolution => true;
+
+ public Task HandleRequestAsync(RequestContext context, CancellationToken cancellationToken)
+ {
+ Contract.ThrowIfNull(context.Solution);
+
+ using var _1 = ArrayBuilder.GetInstance(out var builder);
+ foreach (var languageName in context.Solution.Projects.Select(p => p.Language).Distinct())
+ {
+ if (_compilerBuildOnlyDiagnosticIds.TryGetValue(languageName, out var compilerBuildOnlyDiagnosticIds))
+ {
+ builder.AddRange(compilerBuildOnlyDiagnosticIds);
+ }
+ }
+
+ using var _2 = PooledHashSet<(object Reference, string Language)>.GetInstance(out var seenAnalyzerReferencesByLanguage);
+
+ foreach (var project in context.Solution.Projects)
+ {
+ var analyzersPerReferenceMap = context.Solution.State.Analyzers.CreateDiagnosticAnalyzersPerReference(project);
+ foreach (var (analyzerReference, analyzers) in analyzersPerReferenceMap)
+ {
+ if (!seenAnalyzerReferencesByLanguage.Add((analyzerReference, project.Language)))
+ continue;
+
+ foreach (var analyzer in analyzers)
+ {
+ // We have already added the compiler build-only diagnostics upfront.
+ if (analyzer.IsCompilerAnalyzer())
+ continue;
+
+ foreach (var buildOnlyDescriptor in _globalCache.AnalyzerInfoCache.GetCompilationEndDiagnosticDescriptors(analyzer))
+ {
+ builder.Add(buildOnlyDescriptor.Id);
+ }
+ }
+ }
+ }
+
+ return Task.FromResult(new BuildOnlyDiagnosticIdsResult(builder.ToArray()));
+ }
+}
diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/BuildOnlyDiagnosticIdsHandlerTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/BuildOnlyDiagnosticIdsHandlerTests.cs
new file mode 100644
index 0000000000000..23ad8255dd37d
--- /dev/null
+++ b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/BuildOnlyDiagnosticIdsHandlerTests.cs
@@ -0,0 +1,112 @@
+// 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.
+
+#nullable disable
+
+using System.Collections.Immutable;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.LanguageServer.Handler;
+using Microsoft.CodeAnalysis.PooledObjects;
+using Roslyn.Test.Utilities;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Completion
+{
+ public class BuildOnlyDiagnosticIdsHandlerTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper)
+ {
+ [Theory, CombinatorialData]
+ [WorkItem("https://github.com/dotnet/vscode-csharp/issues/5728")]
+ public async Task TestCSharpBuildOnlyDiagnosticIdsAsync(bool mutatingLspWorkspace)
+ {
+ await using var testLspServer = await CreateTestLspServerAsync("class C { }", mutatingLspWorkspace);
+
+ var result = await testLspServer.ExecuteRequest0Async(BuildOnlyDiagnosticIdsHandler.BuildOnlyDiagnosticIdsMethodName,
+ CancellationToken.None);
+
+ var expectedBuildOnlyDiagnosticIds = GetExpectedBuildOnlyDiagnosticIds(LanguageNames.CSharp);
+ AssertEx.SetEqual(expectedBuildOnlyDiagnosticIds, result.Ids);
+ }
+
+ [Theory, CombinatorialData]
+ [WorkItem("https://github.com/dotnet/vscode-csharp/issues/5728")]
+ public async Task TestVisualBasicBuildOnlyDiagnosticIdsAsync(bool mutatingLspWorkspace)
+ {
+ await using var testLspServer = await CreateVisualBasicTestLspServerAsync(markup: @"
+Class C
+End Class", mutatingLspWorkspace);
+
+ var result = await testLspServer.ExecuteRequest0Async(BuildOnlyDiagnosticIdsHandler.BuildOnlyDiagnosticIdsMethodName,
+ CancellationToken.None);
+
+ var expectedBuildOnlyDiagnosticIds = GetExpectedBuildOnlyDiagnosticIds(LanguageNames.VisualBasic);
+ AssertEx.SetEqual(expectedBuildOnlyDiagnosticIds, result.Ids);
+ }
+
+ private protected override TestAnalyzerReferenceByLanguage CreateTestAnalyzersReference()
+ {
+ var builder = ImmutableDictionary.CreateBuilder>();
+ builder.Add(LanguageNames.CSharp, ImmutableArray.Create(
+ DiagnosticExtensions.GetCompilerDiagnosticAnalyzer(LanguageNames.CSharp),
+ new BuildOnlyAnalyzer(),
+ new LiveAnalyzer()));
+ builder.Add(LanguageNames.VisualBasic, ImmutableArray.Create(
+ DiagnosticExtensions.GetCompilerDiagnosticAnalyzer(LanguageNames.VisualBasic),
+ new BuildOnlyAnalyzer(),
+ new LiveAnalyzer()));
+ return new(builder.ToImmutableDictionary());
+ }
+
+ private static string[] GetExpectedBuildOnlyDiagnosticIds(string languageName)
+ {
+ using var _ = ArrayBuilder.GetInstance(out var builder);
+
+ // NOTE: 'CSharpLspBuildOnlyDiagnosticsTests' and 'VisualBasicLspBuildOnlyDiagnosticsTests' already verify that
+ // the corresponding build-only diagnostic providers return expected compiler build-only diagnostic IDs.
+ // So, here we just directly append 'attribute.BuildOnlyDiagnostics' from these providers to our expected build-only diagnostic IDs.
+ var compilerBuildOnlyDiagnosticsType = languageName switch
+ {
+ LanguageNames.CSharp => typeof(CSharp.LanguageServer.CSharpLspBuildOnlyDiagnostics),
+ LanguageNames.VisualBasic => typeof(VisualBasic.LanguageServer.VisualBasicLspBuildOnlyDiagnostics),
+ _ => null
+ };
+
+ if (compilerBuildOnlyDiagnosticsType != null)
+ {
+ var attribute = compilerBuildOnlyDiagnosticsType.GetCustomAttribute();
+ builder.AddRange(attribute.BuildOnlyDiagnostics);
+ }
+
+ builder.Add(BuildOnlyAnalyzer.Id);
+ return builder.ToArray();
+ }
+
+ [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
+ private sealed class BuildOnlyAnalyzer : DiagnosticAnalyzer
+ {
+ public const string Id = "BuildOnly0001";
+ private static readonly DiagnosticDescriptor s_descriptor = new(Id, "Title", "Message", "Category", DiagnosticSeverity.Warning, isEnabledByDefault: true, customTags: new[] { WellKnownDiagnosticTags.CompilationEnd });
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(s_descriptor);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ }
+ }
+
+ [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
+ private sealed class LiveAnalyzer : DiagnosticAnalyzer
+ {
+ public const string Id = "Live0001";
+ private static readonly DiagnosticDescriptor s_descriptor = new(Id, "Title", "Message", "Category", DiagnosticSeverity.Warning, isEnabledByDefault: true);
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(s_descriptor);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ }
+ }
+ }
+}
diff --git a/src/EditorFeatures/VisualBasic/LanguageServer/VisualBasicLspBuildOnlyDiagnostics.vb b/src/Features/VisualBasic/Portable/Diagnostics/LanguageServer/VisualBasicLspBuildOnlyDiagnostics.vb
similarity index 92%
rename from src/EditorFeatures/VisualBasic/LanguageServer/VisualBasicLspBuildOnlyDiagnostics.vb
rename to src/Features/VisualBasic/Portable/Diagnostics/LanguageServer/VisualBasicLspBuildOnlyDiagnostics.vb
index 9e2b56191937b..1dd7ad15b870c 100644
--- a/src/EditorFeatures/VisualBasic/LanguageServer/VisualBasicLspBuildOnlyDiagnostics.vb
+++ b/src/Features/VisualBasic/Portable/Diagnostics/LanguageServer/VisualBasicLspBuildOnlyDiagnostics.vb
@@ -2,7 +2,7 @@
' The .NET Foundation licenses this file to you under the MIT license.
' See the LICENSE file in the project root for more information.
-Imports System.ComponentModel.Composition
+Imports System.Composition
Imports Microsoft.CodeAnalysis.Host.Mef
Imports Microsoft.CodeAnalysis.LanguageServer
@@ -10,11 +10,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageServer
' Keep in sync with IsBuildOnlyDiagnostic
' src\Compilers\VisualBasic\Portable\Errors\ErrorFacts.vb
+ <[Shared]>
Friend NotInheritable Class VisualBasicLspBuildOnlyDiagnostics
Implements ILspBuildOnlyDiagnostics
diff --git a/src/Features/VisualBasic/Portable/Microsoft.CodeAnalysis.VisualBasic.Features.vbproj b/src/Features/VisualBasic/Portable/Microsoft.CodeAnalysis.VisualBasic.Features.vbproj
index d6ed862178b3c..7e0591b5055af 100644
--- a/src/Features/VisualBasic/Portable/Microsoft.CodeAnalysis.VisualBasic.Features.vbproj
+++ b/src/Features/VisualBasic/Portable/Microsoft.CodeAnalysis.VisualBasic.Features.vbproj
@@ -34,6 +34,7 @@
+
diff --git a/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalyzerInfoCache.cs b/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalyzerInfoCache.cs
index 05642128a7716..888b8c259a426 100644
--- a/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalyzerInfoCache.cs
+++ b/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalyzerInfoCache.cs
@@ -111,6 +111,18 @@ public ImmutableArray GetNonCompilationEndDiagnosticDescri
: descriptorInfo.SupportedDescriptors.WhereAsArray(d => !d.IsCompilationEnd());
}
+ ///
+ /// Returns of given
+ /// that are compilation end descriptors.
+ ///
+ public ImmutableArray GetCompilationEndDiagnosticDescriptors(DiagnosticAnalyzer analyzer)
+ {
+ var descriptorInfo = GetOrCreateDescriptorsInfo(analyzer);
+ return descriptorInfo.HasCompilationEndDescriptor
+ ? descriptorInfo.SupportedDescriptors.WhereAsArray(d => d.IsCompilationEnd())
+ : ImmutableArray.Empty;
+ }
+
///
/// Returns true if given has a compilation end descriptor
/// that is reported in the Compilation end action.