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.