|
// 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.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Diagnostics;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics;
internal abstract class AbstractWorkspaceDocumentDiagnosticSource(TextDocument document) : AbstractDocumentDiagnosticSource<TextDocument>(document)
{
public static AbstractWorkspaceDocumentDiagnosticSource CreateForFullSolutionAnalysisDiagnostics(TextDocument document, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func<DiagnosticAnalyzer, bool>? shouldIncludeAnalyzer)
=> new FullSolutionAnalysisDiagnosticSource(document, diagnosticAnalyzerService, shouldIncludeAnalyzer);
public static AbstractWorkspaceDocumentDiagnosticSource CreateForCodeAnalysisDiagnostics(TextDocument document, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService)
=> new CodeAnalysisDiagnosticSource(document, codeAnalysisService);
private sealed class FullSolutionAnalysisDiagnosticSource(TextDocument document, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func<DiagnosticAnalyzer, bool>? shouldIncludeAnalyzer)
: AbstractWorkspaceDocumentDiagnosticSource(document)
{
/// <summary>
/// Cached mapping between a project instance and all the diagnostics computed for it. This is used so that
/// once we compute the diagnostics once for a particular project, we don't need to recompute them again as we
/// walk every document within it.
/// </summary>
private static readonly ConditionalWeakTable<Project, AsyncLazy<ILookup<DocumentId, DiagnosticData>>> s_projectToDiagnostics = new();
/// <summary>
/// This is a normal document source that represents live/fresh diagnostics that should supersede everything else.
/// </summary>
public override bool IsLiveSource()
=> true;
public override async Task<ImmutableArray<DiagnosticData>> GetDiagnosticsAsync(
RequestContext context,
CancellationToken cancellationToken)
{
if (Document is SourceGeneratedDocument sourceGeneratedDocument)
{
// Unfortunately GetDiagnosticsForIdsAsync returns nothing for source generated documents.
var documentDiagnostics = await diagnosticAnalyzerService.GetDiagnosticsForSpanAsync(
sourceGeneratedDocument, range: null, DiagnosticKind.All, cancellationToken).ConfigureAwait(false);
documentDiagnostics = documentDiagnostics.WhereAsArray(d => !d.IsSuppressed);
return documentDiagnostics;
}
else
{
var projectDiagnostics = await GetProjectDiagnosticsAsync(diagnosticAnalyzerService, cancellationToken).ConfigureAwait(false);
return projectDiagnostics.WhereAsArray(d => d.DocumentId == Document.Id);
}
}
private async ValueTask<ImmutableArray<DiagnosticData>> GetProjectDiagnosticsAsync(
IDiagnosticAnalyzerService diagnosticAnalyzerService, CancellationToken cancellationToken)
{
if (!s_projectToDiagnostics.TryGetValue(Document.Project, out var lazyDiagnostics))
{
// Extracted into local to prevent captures.
lazyDiagnostics = GetLazyDiagnostics();
}
var result = await lazyDiagnostics.GetValueAsync(cancellationToken).ConfigureAwait(false);
return [.. result[Document.Id]];
AsyncLazy<ILookup<DocumentId, DiagnosticData>> GetLazyDiagnostics()
{
return s_projectToDiagnostics.GetValue(
Document.Project,
_ => AsyncLazy.Create(
async cancellationToken =>
{
var allDiagnostics = await diagnosticAnalyzerService.GetDiagnosticsForIdsAsync(
Document.Project, documentId: null, diagnosticIds: null, shouldIncludeAnalyzer,
includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics: true, cancellationToken).ConfigureAwait(false);
// TODO(cyrusn): Should we be filtering out suppressed diagnostics here? This is how the
// code has always worked, but it isn't clear if that is correct.
return allDiagnostics.Where(d => !d.IsSuppressed && d.DocumentId != null).ToLookup(d => d.DocumentId!);
}));
}
}
}
private sealed class CodeAnalysisDiagnosticSource(TextDocument document, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService)
: AbstractWorkspaceDocumentDiagnosticSource(document)
{
/// <summary>
/// This source provides the results of the *last* explicitly kicked off "run code analysis" command from the
/// user. As such, it is definitely not "live" data, and it should be overridden by any subsequent fresh data
/// that has been produced.
/// </summary>
public override bool IsLiveSource()
=> false;
public override Task<ImmutableArray<DiagnosticData>> GetDiagnosticsAsync(
RequestContext context,
CancellationToken cancellationToken)
{
return Task.FromResult(codeAnalysisService.GetLastComputedDocumentDiagnostics(Document.Id));
}
}
}
|