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

Add BuildOnlyDiagnosticIds LSP request handler #69475

Merged
merged 3 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -118,4 +118,7 @@
<PublicAPI Include="PublicAPI.Shipped.txt" />
<PublicAPI Include="PublicAPI.Unshipped.txt" />
</ItemGroup>
<ItemGroup>
<Folder Include="LanguageServer\Diagnostics\" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,13 @@ private static RoslynLanguageServer CreateLanguageServer(Stream inputStream, Str
return result;
}

public async Task<ResponseType?> ExecuteRequest0Async<ResponseType>(string methodName, CancellationToken cancellationToken)
{
// If creating the LanguageServer threw we might timeout without this.
var result = await _clientRpc.InvokeWithParameterObjectAsync<ResponseType>(methodName, cancellationToken: cancellationToken).ConfigureAwait(false);
return result;
}

public async Task OpenDocumentAsync(Uri documentUri, string? text = null, string languageId = "")
{
if (text == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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:
Expand Down Expand Up @@ -58,6 +59,7 @@ namespace Microsoft.CodeAnalysis.CSharp.LanguageServer
"CS9177", // ErrorCode.ERR_InterceptorArityNotCompatible
"CS9178" // ErrorCode.ERR_InterceptorCannotBeGeneric
)]
[Shared]
internal sealed class CSharpLspBuildOnlyDiagnostics : ILspBuildOnlyDiagnostics
{
[ImportingConstructor]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer
{
/// <summary>
/// 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.
/// </summary>
internal interface ILspBuildOnlyDiagnostics
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer
/// <inheritdoc cref="ILspBuildOnlyDiagnostics"/>
internal interface ILspBuildOnlyDiagnosticsMetadata
{
string LanguageName { get; }
string[] BuildOnlyDiagnostics { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <inheritdoc cref="ILspBuildOnlyDiagnostics"/>
[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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Lazy<ILspBuildOnlyDiagnostics, ILspBuildOnlyDiagnosticsMetadata>> compilerBuildOnlyDiagnosticsProviders)
: ILspServiceRequestHandler<BuildOnlyDiagnosticIdsResult>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation.

{
public const string BuildOnlyDiagnosticIdsMethodName = "workspace/buildOnlyDiagnosticIds";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a well known lsp method? or a roslyn extension?


private readonly DiagnosticAnalyzerInfoCache.SharedGlobalCache _globalCache = globalCache;
private readonly ImmutableDictionary<string, string[]> _compilerBuildOnlyDiagnosticIds = compilerBuildOnlyDiagnosticsProviders
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: _languageNameToBuildOnlyDiagnosticIds

.ToImmutableDictionary(lazy => lazy.Metadata.LanguageName, lazy => lazy.Metadata.BuildOnlyDiagnostics);

public bool MutatesSolutionState => false;
public bool RequiresLSPSolution => true;

public Task<BuildOnlyDiagnosticIdsResult> HandleRequestAsync(RequestContext context, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(context.Solution);

using var _1 = ArrayBuilder<string>.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()));
}
}
Original file line number Diff line number Diff line change
@@ -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<BuildOnlyDiagnosticIdsResult>(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<BuildOnlyDiagnosticIdsResult>(BuildOnlyDiagnosticIdsHandler.BuildOnlyDiagnosticIdsMethodName,
CancellationToken.None);

var expectedBuildOnlyDiagnosticIds = GetExpectedBuildOnlyDiagnosticIds(LanguageNames.VisualBasic);
AssertEx.SetEqual(expectedBuildOnlyDiagnosticIds, result.Ids);
}

private protected override TestAnalyzerReferenceByLanguage CreateTestAnalyzersReference()
{
var builder = ImmutableDictionary.CreateBuilder<string, ImmutableArray<DiagnosticAnalyzer>>();
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<string>.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<LspBuildOnlyDiagnosticsAttribute>();
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<DiagnosticDescriptor> 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<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(s_descriptor);

public override void Initialize(AnalysisContext context)
{
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@
' 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

Namespace Microsoft.CodeAnalysis.VisualBasic.LanguageServer
' Keep in sync with IsBuildOnlyDiagnostic
' src\Compilers\VisualBasic\Portable\Errors\ErrorFacts.vb
<LspBuildOnlyDiagnostics(
LanguageNames.VisualBasic,
"BC31091", ' ERRID.ERR_TypeRefResolutionError3,
"BC35000", ' ERRID.ERR_MissingRuntimeHelper,
"BC36597", ' ERRID.ERR_CannotGotoNonScopeBlocksWithClosure
"BC37327" ' ERRID.ERR_SymbolDefinedInAssembly
)>
<[Shared]>
Friend NotInheritable Class VisualBasicLspBuildOnlyDiagnostics
Implements ILspBuildOnlyDiagnostics

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<InternalsVisibleTo Include="Microsoft.CodeAnalysis.Features.UnitTests" />
<InternalsVisibleTo Include="Microsoft.CodeAnalysis.EditorFeatures.UnitTests" />
<InternalsVisibleTo Include="Microsoft.CodeAnalysis.EditorFeatures2.UnitTests" />
<InternalsVisibleTo Include="Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests" />
<InternalsVisibleTo Include="Microsoft.CodeAnalysis.VisualBasic.Features.UnitTests" />
<InternalsVisibleTo Include="Microsoft.CodeAnalysis.VisualBasic.EditorFeatures.UnitTests" />
<InternalsVisibleTo Include="Microsoft.CodeAnalysis.Workspaces.UnitTests" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ public ImmutableArray<DiagnosticDescriptor> GetNonCompilationEndDiagnosticDescri
: descriptorInfo.SupportedDescriptors.WhereAsArray(d => !d.IsCompilationEnd());
}

/// <summary>
/// Returns <see cref="DiagnosticAnalyzer.SupportedDiagnostics"/> of given <paramref name="analyzer"/>
/// that are compilation end descriptors.
/// </summary>
public ImmutableArray<DiagnosticDescriptor> GetCompilationEndDiagnosticDescriptors(DiagnosticAnalyzer analyzer)
{
var descriptorInfo = GetOrCreateDescriptorsInfo(analyzer);
return descriptorInfo.HasCompilationEndDescriptor
? descriptorInfo.SupportedDescriptors.WhereAsArray(d => d.IsCompilationEnd())
: ImmutableArray<DiagnosticDescriptor>.Empty;
}

/// <summary>
/// Returns true if given <paramref name="analyzer"/> has a compilation end descriptor
/// that is reported in the Compilation end action.
Expand Down
Loading