提交 c9215386 编写于 作者: M Manish Vasani

Refactor IDE open file diagnostic analysis + some perf improvements

First step towards #44408

1. Refactor IDE open file analysis so that we have a single place where we execute the analyzers for document analysis. I have preserved the existing IDE layer diagnostic caching in this PR, but it should hopefully go away in future when we move the diagnostic computation to OOP.
2. Perf improvement: Currently, we execute open file analyzers one at a time by making separate calls into CompilationWithAnalyzers. This identified overhead where we are recomputing lot of stuff with each call and missing semantic model/bound node cache hits. This PR simplifies the approach by invoking a single syntax/semantic call into CompilationWithAnalyzers with all relevant analyzers. This required adding a new public API to CompilationWithAnalyzers to return syntax/semantic diagnostics for a tree/model grouped by analyzers, which has been split into a separate commit.
3. Remove code for code fix support for CompilationEndAction diagnostics - these actions are not executed in the IDE live analysis, so the code was not executing. Additionally, we have identified that we never want to run these actions in IDE live analysis due to associated perf concerns.

Future steps:
1. Move open file diagnostic analyzer execution to OOP
2. Remove IDE layer diagnostic caching, and rely on caching at OOP side
3. Investigate moving even compiler diagnostic computation to OOP - need to ensure responsiveness of refreshing compiler diagnostics on typing is not affected
上级 7087a176
......@@ -205,14 +205,14 @@ public void DiagnosticEquivalence()
Assert.NotSame(diagnostics1[0], diagnostics2[0]);
Assert.NotSame(diagnostics1[1], diagnostics2[1]);
Assert.Equal(diagnostics1, diagnostics2);
Assert.True(DiagnosticIncrementalAnalyzer.AreEquivalent(diagnostics1, diagnostics2));
Assert.True(AnalyzerHelper.AreEquivalent(diagnostics1, diagnostics2));
// Verify that not all collections are treated as equivalent.
diagnostics1 = new[] { diagnostics1[0] };
diagnostics2 = new[] { diagnostics2[1] };
Assert.NotEqual(diagnostics1, diagnostics2);
Assert.False(DiagnosticIncrementalAnalyzer.AreEquivalent(diagnostics1, diagnostics2));
Assert.False(AnalyzerHelper.AreEquivalent(diagnostics1, diagnostics2));
#endif
}
......
......@@ -799,10 +799,6 @@ class AnonymousFunctions
Using workspace = TestWorkspace.CreateWorkspace(test)
Dim project = workspace.CurrentSolution.Projects.Single()
' turn off heuristic
workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(workspace.Options _
.WithChangedOption(InternalDiagnosticsOptions.UseCompilationEndCodeFixHeuristic, False)))
Dim solution = workspace.CurrentSolution
Dim analyzer = New CompilationEndedAnalyzer
Dim analyzerReference = New AnalyzerImageReference(ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer))
......@@ -815,22 +811,21 @@ class AnonymousFunctions
Dim descriptorsMap = solution.State.Analyzers.GetDiagnosticDescriptorsPerReference(diagnosticService.AnalyzerInfoCache, project)
Assert.Equal(1, descriptorsMap.Count)
' Ask for document diagnostics multiple times, and verify compilation diagnostics are reported.
' Test "GetDiagnosticsForSpanAsync" used from CodeFixService does not force computation of compilation end diagnostics.
' Ask for document diagnostics for multiple times, and verify compilation end diagnostics are not reported.
Dim document = project.Documents.Single()
Dim fullSpan = document.GetSyntaxRootAsync().WaitAndGetResult(CancellationToken.None).FullSpan
Dim diagnostics = diagnosticService.GetDiagnosticsForSpanAsync(document, fullSpan).WaitAndGetResult(CancellationToken.None)
Assert.Equal(1, diagnostics.Count())
Assert.Equal(document.Id, diagnostics.First().DocumentId)
Assert.Empty(diagnostics)
diagnostics = diagnosticService.GetDiagnosticsForSpanAsync(document, fullSpan).WaitAndGetResult(CancellationToken.None)
Assert.Equal(1, diagnostics.Count())
Assert.Equal(document.Id, diagnostics.First().DocumentId)
Assert.Empty(diagnostics)
diagnostics = diagnosticService.GetDiagnosticsForSpanAsync(document, fullSpan).WaitAndGetResult(CancellationToken.None)
Assert.Equal(1, diagnostics.Count())
Assert.Equal(document.Id, diagnostics.First().DocumentId)
Assert.Empty(diagnostics)
' Test "GetDiagnosticsForIdsAsync" does force computation of compilation end diagnostics.
' Verify compilation diagnostics are reported with correct location info when asked for project diagnostics.
Dim projectDiagnostics = diagnosticService.GetDiagnosticsForIdsAsync(project.Solution, project.Id).WaitAndGetResult(CancellationToken.None)
Assert.Equal(2, projectDiagnostics.Count())
......
......@@ -298,115 +298,6 @@ private static void AssertCompilation(Project project, Compilation compilation1)
Contract.ThrowIfFalse(compilation1 == compilation2);
}
/// <summary>
/// Return all local diagnostics (syntax, semantic) that belong to given document for the given StateSet (analyzer) by calculating them
/// </summary>
public static async Task<IEnumerable<DiagnosticData>> ComputeDiagnosticsAsync(
DiagnosticAnalyzer analyzer,
Document document,
AnalysisKind kind,
DiagnosticAnalyzerInfoCache analyzerInfoCache,
CompilationWithAnalyzers? compilationWithAnalyzers,
TextSpan? span,
CancellationToken cancellationToken)
{
var loadDiagnostic = await document.State.GetLoadDiagnosticAsync(cancellationToken).ConfigureAwait(false);
if (analyzer == FileContentLoadAnalyzer.Instance)
{
return loadDiagnostic != null ?
SpecializedCollections.SingletonEnumerable(DiagnosticData.Create(loadDiagnostic, document)) :
SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
if (loadDiagnostic != null)
{
return SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
if (analyzer is DocumentDiagnosticAnalyzer documentAnalyzer)
{
var diagnostics = await ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync(
documentAnalyzer, document, kind, compilationWithAnalyzers?.Compilation, cancellationToken).ConfigureAwait(false);
return diagnostics.ConvertToLocalDiagnostics(document);
}
// quick optimization to reduce allocations.
if (compilationWithAnalyzers == null || !analyzer.SupportAnalysisKind(kind))
{
if (kind == AnalysisKind.Syntax)
{
Logger.Log(FunctionId.Diagnostics_SyntaxDiagnostic,
(r, d, a, k) => $"Driver: {r != null}, {d.Id}, {d.Project.Id}, {a}, {k}", compilationWithAnalyzers, document, analyzer, kind);
}
return SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
// if project is not loaded successfully then, we disable semantic errors for compiler analyzers
if (kind != AnalysisKind.Syntax && analyzer.IsCompilerAnalyzer())
{
var isEnabled = await document.Project.HasSuccessfullyLoadedAsync(cancellationToken).ConfigureAwait(false);
Logger.Log(FunctionId.Diagnostics_SemanticDiagnostic, (a, d, e) => $"{a}, ({d.Id}, {d.Project.Id}), Enabled:{e}", analyzer, document, isEnabled);
if (!isEnabled)
{
return SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
}
// REVIEW: more unnecessary allocations just to get diagnostics per analyzer
var singleAnalyzer = ImmutableArray.Create(analyzer);
var skippedAnalyzerInfo = document.Project.GetSkippedAnalyzersInfo(analyzerInfoCache);
ImmutableArray<string> filteredIds;
switch (kind)
{
case AnalysisKind.Syntax:
var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
if (tree == null)
{
return SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
var diagnostics = await compilationWithAnalyzers.GetAnalyzerSyntaxDiagnosticsAsync(tree, singleAnalyzer, cancellationToken).ConfigureAwait(false);
if (diagnostics.IsDefaultOrEmpty)
{
Logger.Log(FunctionId.Diagnostics_SyntaxDiagnostic, (d, a, t) => $"{d.Id}, {d.Project.Id}, {a}, {t.Length}", document, analyzer, tree);
}
else if (skippedAnalyzerInfo.FilteredDiagnosticIdsForAnalyzers.TryGetValue(analyzer, out filteredIds))
{
diagnostics = diagnostics.Filter(filteredIds);
}
Debug.Assert(diagnostics.Length == CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, compilationWithAnalyzers.Compilation).Count());
return diagnostics.ConvertToLocalDiagnostics(document);
case AnalysisKind.Semantic:
var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
if (model == null)
{
return SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
diagnostics = await compilationWithAnalyzers.GetAnalyzerSemanticDiagnosticsAsync(model, span, singleAnalyzer, cancellationToken).ConfigureAwait(false);
if (skippedAnalyzerInfo.FilteredDiagnosticIdsForAnalyzers.TryGetValue(analyzer, out filteredIds))
{
diagnostics = diagnostics.Filter(filteredIds);
}
Debug.Assert(diagnostics.Length == CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, compilationWithAnalyzers.Compilation).Count());
return diagnostics.ConvertToLocalDiagnostics(document);
default:
throw ExceptionUtilities.UnexpectedValue(kind);
}
}
public static async Task<ImmutableArray<Diagnostic>> ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync(
DocumentDiagnosticAnalyzer analyzer,
Document document,
......@@ -624,6 +515,37 @@ IEnumerable<DiagnosticData> ConvertToLocalDiagnosticsWithCompilation()
}
}
#if DEBUG
internal static bool AreEquivalent(Diagnostic[] diagnosticsA, Diagnostic[] diagnosticsB)
{
var set = new HashSet<Diagnostic>(diagnosticsA, DiagnosticComparer.Instance);
return set.SetEquals(diagnosticsB);
}
private sealed class DiagnosticComparer : IEqualityComparer<Diagnostic?>
{
internal static readonly DiagnosticComparer Instance = new DiagnosticComparer();
public bool Equals(Diagnostic? x, Diagnostic? y)
{
if (x is null)
return y is null;
else if (y is null)
return false;
return x.Id == y.Id && x.Location == y.Location;
}
public int GetHashCode(Diagnostic? obj)
{
if (obj is null)
return 0;
return Hash.Combine(obj.Id.GetHashCode(), obj.Location.GetHashCode());
}
}
#endif
public static bool? IsCompilationEndAnalyzer(this DiagnosticAnalyzer analyzer, Project project, Compilation? compilation)
{
if (!project.SupportsCompilation)
......
......@@ -159,12 +159,12 @@ private async Task AnalyzeForKindAsync(Document document, AnalysisKind kind, Can
var compilationWithAnalyzers = await AnalyzerHelper.CreateCompilationWithAnalyzersAsync(
project, analyzers, includeSuppressedDiagnostics: false, cancellationToken).ConfigureAwait(false);
var executor = new DocumentAnalysisExecutor(document, span: null, kind, compilationWithAnalyzers, _service._analyzerInfoCache);
var builder = ArrayBuilder<DiagnosticData>.GetInstance();
foreach (var analyzer in analyzers)
{
builder.AddRange(await AnalyzerHelper.ComputeDiagnosticsAsync(analyzer,
document, kind, _service._analyzerInfoCache, compilationWithAnalyzers, span: null, cancellationToken).ConfigureAwait(false));
builder.AddRange(await executor.ComputeDiagnosticsAsync(analyzer, cancellationToken).ConfigureAwait(false));
}
return builder.ToImmutableAndFree();
......
// 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 enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageServices;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Diagnostics
{
/// <summary>
/// Executes analyzers on a document for computing local syntax/semantic diagnostics.
/// </summary>
internal sealed class DocumentAnalysisExecutor
{
private ImmutableDictionary<DiagnosticAnalyzer, ImmutableArray<Diagnostic>>? _lazySyntaxDiagnostics;
private ImmutableDictionary<DiagnosticAnalyzer, ImmutableArray<Diagnostic>>? _lazySemanticDiagnostics;
public DocumentAnalysisExecutor(
Document document,
TextSpan? span,
AnalysisKind kind,
CompilationWithAnalyzers? compilationWithAnalyzers,
DiagnosticAnalyzerInfoCache analyzerInfoCache)
{
Debug.Assert(kind == AnalysisKind.Syntax || kind == AnalysisKind.Semantic);
Document = document;
Span = span;
Kind = kind;
CompilationWithAnalyzers = compilationWithAnalyzers;
AnalyzerInfoCache = analyzerInfoCache;
}
public Document Document { get; }
public TextSpan? Span { get; }
public AnalysisKind Kind { get; }
public CompilationWithAnalyzers? CompilationWithAnalyzers { get; }
public DiagnosticAnalyzerInfoCache AnalyzerInfoCache { get; }
/// <summary>
/// Return all local diagnostics (syntax, semantic) that belong to given document for the given StateSet (analyzer) by calculating them
/// </summary>
public async Task<IEnumerable<DiagnosticData>> ComputeDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken)
{
var loadDiagnostic = await Document.State.GetLoadDiagnosticAsync(cancellationToken).ConfigureAwait(false);
if (analyzer == FileContentLoadAnalyzer.Instance)
{
return loadDiagnostic != null ?
SpecializedCollections.SingletonEnumerable(DiagnosticData.Create(loadDiagnostic, Document)) :
SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
if (loadDiagnostic != null)
{
return SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
if (analyzer is DocumentDiagnosticAnalyzer documentAnalyzer)
{
var diagnostics = await AnalyzerHelper.ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync(
documentAnalyzer, Document, Kind, CompilationWithAnalyzers?.Compilation, cancellationToken).ConfigureAwait(false);
return diagnostics.ConvertToLocalDiagnostics(Document, Span);
}
// quick optimization to reduce allocations.
if (CompilationWithAnalyzers == null || !analyzer.SupportAnalysisKind(Kind))
{
if (Kind == AnalysisKind.Syntax)
{
Logger.Log(FunctionId.Diagnostics_SyntaxDiagnostic,
(r, d, a, k) => $"Driver: {r != null}, {d.Id}, {d.Project.Id}, {a}, {k}", CompilationWithAnalyzers, Document, analyzer, Kind);
}
return SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
// if project is not loaded successfully then, we disable semantic errors for compiler analyzers
var isCompilerAnalyzer = analyzer.IsCompilerAnalyzer();
if (Kind != AnalysisKind.Syntax && isCompilerAnalyzer)
{
var isEnabled = await Document.Project.HasSuccessfullyLoadedAsync(cancellationToken).ConfigureAwait(false);
Logger.Log(FunctionId.Diagnostics_SemanticDiagnostic, (a, d, e) => $"{a}, ({d.Id}, {d.Project.Id}), Enabled:{e}", analyzer, Document, isEnabled);
if (!isEnabled)
{
return SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
}
var skippedAnalyzerInfo = Document.Project.GetSkippedAnalyzersInfo(AnalyzerInfoCache);
ImmutableArray<string> filteredIds;
switch (Kind)
{
case AnalysisKind.Syntax:
var tree = await Document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
if (tree == null)
{
return SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
var diagnostics = await GetSyntaxDiagnosticsAsync(tree, analyzer, isCompilerAnalyzer, cancellationToken).ConfigureAwait(false);
if (diagnostics.IsDefaultOrEmpty)
{
Logger.Log(FunctionId.Diagnostics_SyntaxDiagnostic, (d, a, t) => $"{d.Id}, {d.Project.Id}, {a}, {t.Length}", Document, analyzer, tree);
}
else if (skippedAnalyzerInfo.FilteredDiagnosticIdsForAnalyzers.TryGetValue(analyzer, out filteredIds))
{
diagnostics = diagnostics.Filter(filteredIds);
}
Debug.Assert(diagnostics.Length == CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, CompilationWithAnalyzers.Compilation).Count());
return diagnostics.ConvertToLocalDiagnostics(Document, Span);
case AnalysisKind.Semantic:
var model = await Document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
if (model == null)
{
return SpecializedCollections.EmptyEnumerable<DiagnosticData>();
}
diagnostics = await GetSemanticDiagnosticsAsync(model, analyzer, isCompilerAnalyzer, cancellationToken).ConfigureAwait(false);
if (skippedAnalyzerInfo.FilteredDiagnosticIdsForAnalyzers.TryGetValue(analyzer, out filteredIds))
{
diagnostics = diagnostics.Filter(filteredIds);
}
Debug.Assert(diagnostics.Length == CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, CompilationWithAnalyzers.Compilation).Count());
return diagnostics.ConvertToLocalDiagnostics(Document, Span);
default:
throw ExceptionUtilities.UnexpectedValue(Kind);
}
}
private async Task<ImmutableArray<Diagnostic>> GetSyntaxDiagnosticsAsync(SyntaxTree tree, DiagnosticAnalyzer analyzer, bool isCompilerAnalyzer, CancellationToken cancellationToken)
{
// PERF:
// 1. Compute diagnostics for all analyzers with a single invocation into CompilationWithAnalyzers.
// This is critical for better analyzer execution performance.
// 2. Ensure that the compiler analyzer is treated specially and does not block on diagnostic computation
// for rest of the analyzers. This is needed to ensure faster refresh for compiler diagnostics while typing.
RoslynDebug.Assert(CompilationWithAnalyzers != null);
if (isCompilerAnalyzer)
{
return await CompilationWithAnalyzers.GetAnalyzerSyntaxDiagnosticsAsync(tree, ImmutableArray.Create(analyzer), cancellationToken).ConfigureAwait(false);
}
if (_lazySyntaxDiagnostics == null)
{
// TODO: Move this invocation to OOP
var treeDiagnostics = await CompilationWithAnalyzers.GetCategorizedAnalyzerSyntaxDiagnosticsAsync(tree, cancellationToken).ConfigureAwait(false);
Interlocked.CompareExchange(ref _lazySyntaxDiagnostics, treeDiagnostics, null);
}
return _lazySyntaxDiagnostics.TryGetValue(analyzer, out var diagnostics) ?
diagnostics :
ImmutableArray<Diagnostic>.Empty;
}
private async Task<ImmutableArray<Diagnostic>> GetSemanticDiagnosticsAsync(SemanticModel model, DiagnosticAnalyzer analyzer, bool isCompilerAnalyzer, CancellationToken cancellationToken)
{
// PERF:
// 1. Compute diagnostics for all analyzers with a single invocation into CompilationWithAnalyzers.
// This is critical for better analyzer execution performance through re-use of bound node cache.
// 2. Ensure that the compiler analyzer is treated specially and does not block on diagnostic computation
// for rest of the analyzers. This is needed to ensure faster refresh for compiler diagnostics while typing.
RoslynDebug.Assert(CompilationWithAnalyzers != null);
if (isCompilerAnalyzer)
{
#if DEBUG
VerifySpanBasedCompilerDiagnostics(model);
#endif
var adjustedSpan = await GetAdjustedSpanForCompilerAnalyzerAsync().ConfigureAwait(false);
return await CompilationWithAnalyzers.GetAnalyzerSemanticDiagnosticsAsync(model, adjustedSpan, ImmutableArray.Create(analyzer), cancellationToken).ConfigureAwait(false);
}
if (_lazySemanticDiagnostics == null)
{
// TODO: Move this invocation to OOP
var treeDiagnostics = await CompilationWithAnalyzers.GetCategorizedAnalyzerSemanticDiagnosticsAsync(model, Span, cancellationToken).ConfigureAwait(false);
Interlocked.CompareExchange(ref _lazySemanticDiagnostics, treeDiagnostics, null);
}
return _lazySemanticDiagnostics.TryGetValue(analyzer, out var diagnostics) ?
diagnostics :
ImmutableArray<Diagnostic>.Empty;
async Task<TextSpan?> GetAdjustedSpanForCompilerAnalyzerAsync()
{
// This method is to workaround a bug (https://github.com/dotnet/roslyn/issues/1557)
// once that bug is fixed, we should be able to use given span as it is.
Debug.Assert(isCompilerAnalyzer);
if (!Span.HasValue)
{
return null;
}
var service = Document.GetRequiredLanguageService<ISyntaxFactsService>();
var root = await model.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
var startNode = service.GetContainingMemberDeclaration(root, Span.Value.Start);
var endNode = service.GetContainingMemberDeclaration(root, Span.Value.End);
if (startNode == endNode)
{
// use full member span
if (service.IsMethodLevelMember(startNode))
{
return startNode.FullSpan;
}
// use span as it is
return Span;
}
var startSpan = service.IsMethodLevelMember(startNode) ? startNode.FullSpan : Span.Value;
var endSpan = service.IsMethodLevelMember(endNode) ? endNode.FullSpan : Span.Value;
return TextSpan.FromBounds(Math.Min(startSpan.Start, endSpan.Start), Math.Max(startSpan.End, endSpan.End));
}
#if DEBUG
void VerifySpanBasedCompilerDiagnostics(SemanticModel model)
{
if (!Span.HasValue)
{
return;
}
// make sure what we got from range is same as what we got from whole diagnostics
var rangeDeclaractionDiagnostics = model.GetDeclarationDiagnostics(Span.Value).ToArray();
var rangeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics(Span.Value).ToArray();
var rangeDiagnostics = rangeDeclaractionDiagnostics.Concat(rangeMethodBodyDiagnostics).Where(shouldInclude).ToArray();
var wholeDeclarationDiagnostics = model.GetDeclarationDiagnostics().ToArray();
var wholeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics().ToArray();
var wholeDiagnostics = wholeDeclarationDiagnostics.Concat(wholeMethodBodyDiagnostics).Where(shouldInclude).ToArray();
if (!AnalyzerHelper.AreEquivalent(rangeDiagnostics, wholeDiagnostics))
{
// otherwise, report non-fatal watson so that we can fix those cases
FatalError.ReportWithoutCrash(new Exception("Bug in GetDiagnostics"));
// make sure we hold onto these for debugging.
GC.KeepAlive(rangeDeclaractionDiagnostics);
GC.KeepAlive(rangeMethodBodyDiagnostics);
GC.KeepAlive(rangeDiagnostics);
GC.KeepAlive(wholeDeclarationDiagnostics);
GC.KeepAlive(wholeMethodBodyDiagnostics);
GC.KeepAlive(wholeDiagnostics);
}
return;
static bool IsUnusedImportDiagnostic(Diagnostic d)
{
switch (d.Id)
{
case "CS8019":
case "BC50000":
case "BC50001":
return true;
default:
return false;
}
}
// Exclude unused import diagnostics since they are never reported when a span is passed.
// (See CSharp/VisualBasicCompilation.GetDiagnosticsForMethodBodiesInTree.)
bool shouldInclude(Diagnostic d) => Span.Value.IntersectsWith(d.Location.SourceSpan) && !IsUnusedImportDiagnostic(d);
}
#endif
}
}
}
......@@ -23,11 +23,47 @@ namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2
internal partial class DiagnosticIncrementalAnalyzer
{
/// <summary>
/// Return all local diagnostics (syntax, semantic) that belong to given document for the given StateSet (analyzer) either from cache or by calculating them
/// Return all cached local diagnostics (syntax, semantic) that belong to given document for the given StateSet (analyzer).
/// Also returns empty diagnostics for suppressed analyzer.
/// Returns null if the diagnostics need to be computed.
/// </summary>
private async Task<DocumentAnalysisData> GetDocumentAnalysisDataAsync(
CompilationWithAnalyzers? compilation, Document document, StateSet stateSet, AnalysisKind kind, CancellationToken cancellationToken)
private async Task<DocumentAnalysisData?> TryGetCachedDocumentAnalysisDataAsync(
Document document, StateSet stateSet, AnalysisKind kind, CancellationToken cancellationToken)
{
try
{
var version = await GetDiagnosticVersionAsync(document.Project, cancellationToken).ConfigureAwait(false);
var state = stateSet.GetOrCreateActiveFileState(document.Id);
var existingData = state.GetAnalysisData(kind);
if (existingData.Version == version)
{
return existingData;
}
// Perf optimization: Check whether analyzer is suppressed and avoid getting diagnostics if suppressed.
if (DiagnosticAnalyzerInfoCache.IsAnalyzerSuppressed(stateSet.Analyzer, document.Project))
{
return new DocumentAnalysisData(version, existingData.Items, ImmutableArray<DiagnosticData>.Empty);
}
return null;
}
catch (Exception e) when (FatalError.ReportUnlessCanceled(e))
{
throw ExceptionUtilities.Unreachable;
}
}
/// <summary>
/// Computes all local diagnostics (syntax, semantic) that belong to given document for the given StateSet (analyzer).
/// </summary>
private static async Task<DocumentAnalysisData> ComputeDocumentAnalysisDataAsync(
DocumentAnalysisExecutor executor, StateSet stateSet, CancellationToken cancellationToken)
{
var kind = executor.Kind;
var document = executor.Document;
// get log title and functionId
GetLogFunctionIdAndTitle(kind, out var functionId, out var title);
......@@ -35,27 +71,16 @@ internal partial class DiagnosticIncrementalAnalyzer
{
try
{
var version = await GetDiagnosticVersionAsync(document.Project, cancellationToken).ConfigureAwait(false);
var state = stateSet.GetOrCreateActiveFileState(document.Id);
var existingData = state.GetAnalysisData(kind);
if (existingData.Version == version)
{
return existingData;
}
// perf optimization. check whether analyzer is suppressed and avoid getting diagnostics if suppressed.
if (DiagnosticAnalyzerInfoCache.IsAnalyzerSuppressed(stateSet.Analyzer, document.Project))
{
return new DocumentAnalysisData(version, existingData.Items, ImmutableArray<DiagnosticData>.Empty);
}
var diagnostics = await AnalyzerHelper.ComputeDiagnosticsAsync(stateSet.Analyzer, document, kind, DiagnosticAnalyzerInfoCache, compilation, span: null, cancellationToken).ConfigureAwait(false);
var diagnostics = await executor.ComputeDiagnosticsAsync(stateSet.Analyzer, cancellationToken).ConfigureAwait(false);
// this is no-op in product. only run in test environment
Logger.Log(functionId, (t, d, a, ds) => $"{GetDocumentLogMessage(t, d, a)}, {string.Join(Environment.NewLine, ds)}",
title, document, stateSet.Analyzer, diagnostics);
var version = await GetDiagnosticVersionAsync(document.Project, cancellationToken).ConfigureAwait(false);
var state = stateSet.GetOrCreateActiveFileState(document.Id);
var existingData = state.GetAnalysisData(kind);
// we only care about local diagnostics
return new DocumentAnalysisData(version, existingData.Items, diagnostics.ToImmutableArrayOrEmpty());
}
......
......@@ -73,29 +73,8 @@ private abstract class DiagnosticGetter
protected ImmutableArray<DiagnosticData> GetDiagnosticData()
=> (_lazyDataBuilder != null) ? _lazyDataBuilder.ToImmutableArray() : ImmutableArray<DiagnosticData>.Empty;
protected abstract Task<ImmutableArray<DiagnosticData>> GetDiagnosticsAsync(StateSet stateSet, Project project, DocumentId? documentId, AnalysisKind kind, CancellationToken cancellationToken);
protected abstract Task AppendDiagnosticsAsync(Project project, IEnumerable<DocumentId> documentIds, bool includeProjectNonLocalResult, CancellationToken cancellationToken);
public async Task<ImmutableArray<DiagnosticData>> GetSpecificDiagnosticsAsync(DiagnosticAnalyzer analyzer, AnalysisKind analysisKind, CancellationToken cancellationToken)
{
var project = Solution.GetProject(ProjectId);
if (project == null)
{
// when we return cached result, make sure we at least return something that exist in current solution
return ImmutableArray<DiagnosticData>.Empty;
}
var stateSet = StateManager.GetOrCreateStateSet(project, analyzer);
if (stateSet == null)
{
return ImmutableArray<DiagnosticData>.Empty;
}
var diagnostics = await GetDiagnosticsAsync(stateSet, project, DocumentId, analysisKind, cancellationToken).ConfigureAwait(false);
return IncludeSuppressedDiagnostics ? diagnostics : diagnostics.WhereAsArray(d => !d.IsSuppressed);
}
public async Task<ImmutableArray<DiagnosticData>> GetDiagnosticsAsync(CancellationToken cancellationToken)
{
if (ProjectId != null)
......@@ -182,7 +161,27 @@ protected override async Task AppendDiagnosticsAsync(Project project, IEnumerabl
}
}
protected override async Task<ImmutableArray<DiagnosticData>> GetDiagnosticsAsync(StateSet stateSet, Project project, DocumentId? documentId, AnalysisKind kind, CancellationToken cancellationToken)
public async Task<ImmutableArray<DiagnosticData>> GetSpecificDiagnosticsAsync(DiagnosticAnalyzer analyzer, AnalysisKind analysisKind, CancellationToken cancellationToken)
{
var project = Solution.GetProject(ProjectId);
if (project == null)
{
// when we return cached result, make sure we at least return something that exist in current solution
return ImmutableArray<DiagnosticData>.Empty;
}
var stateSet = StateManager.GetOrCreateStateSet(project, analyzer);
if (stateSet == null)
{
return ImmutableArray<DiagnosticData>.Empty;
}
var diagnostics = await GetDiagnosticsAsync(stateSet, project, DocumentId, analysisKind, cancellationToken).ConfigureAwait(false);
return IncludeSuppressedDiagnostics ? diagnostics : diagnostics.WhereAsArray(d => !d.IsSuppressed);
}
private async Task<ImmutableArray<DiagnosticData>> GetDiagnosticsAsync(StateSet stateSet, Project project, DocumentId? documentId, AnalysisKind kind, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
......@@ -297,42 +296,6 @@ private bool ShouldIncludeStateSet(Project project, StateSet stateSet)
return true;
}
protected override async Task<ImmutableArray<DiagnosticData>> GetDiagnosticsAsync(StateSet stateSet, Project project, DocumentId? documentId, AnalysisKind kind, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var stateSets = SpecializedCollections.SingletonCollection(stateSet);
// Here, we don't care what kind of analyzer (StateSet) is given.
var forceAnalyzerRun = true;
var compilation = await CreateCompilationWithAnalyzersAsync(project, stateSets, IncludeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false);
if (documentId != null)
{
var document = project.Solution.GetDocument(documentId);
Contract.ThrowIfNull(document);
switch (kind)
{
case AnalysisKind.Syntax:
case AnalysisKind.Semantic:
return (await Owner.GetDocumentAnalysisDataAsync(compilation, document, stateSet, kind, cancellationToken).ConfigureAwait(false)).Items;
case AnalysisKind.NonLocal:
var nonLocalDocumentResult = await Owner.GetProjectAnalysisDataAsync(compilation, project, stateSets, forceAnalyzerRun, cancellationToken).ConfigureAwait(false);
var analysisResult = nonLocalDocumentResult.GetResult(stateSet.Analyzer);
return analysisResult.GetDocumentDiagnostics(documentId, AnalysisKind.NonLocal);
default:
throw ExceptionUtilities.UnexpectedValue(kind);
}
}
Contract.ThrowIfFalse(kind == AnalysisKind.NonLocal);
var projectResult = await Owner.GetProjectAnalysisDataAsync(compilation, project, stateSets, forceAnalyzerRun, cancellationToken).ConfigureAwait(false);
return projectResult.GetResult(stateSet.Analyzer).GetOtherDiagnostics();
}
}
}
}
......@@ -14,6 +14,7 @@
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Remote;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Options;
......@@ -43,34 +44,57 @@ private async Task AnalyzeDocumentForKindAsync(Document document, AnalysisKind k
return;
}
// First attempt to fetch diagnostics from the cache, while computing the state sets for analyzers that are not cached.
var stateSets = _stateManager.GetOrUpdateStateSets(document.Project);
var compilation = await GetOrCreateCompilationWithAnalyzersAsync(document.Project, stateSets, cancellationToken).ConfigureAwait(false);
using var _ = ArrayBuilder<StateSet>.GetInstance(out var nonCachedStateSets);
foreach (var stateSet in stateSets)
{
var analyzer = stateSet.Analyzer;
var result = await GetDocumentAnalysisDataAsync(compilation, document, stateSet, kind, cancellationToken).ConfigureAwait(false);
if (result.FromCache)
var data = await TryGetCachedDocumentAnalysisDataAsync(document, stateSet, kind, cancellationToken).ConfigureAwait(false);
if (data.HasValue)
{
RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, result.Items);
continue;
// We need to persist and raise diagnostics for suppressed analyzer.
PersistAndRaiseDiagnosticsIfNeeded(data.Value, stateSet);
}
else
{
nonCachedStateSets.Add(stateSet);
}
}
// no cancellation after this point.
var state = stateSet.GetOrCreateActiveFileState(document.Id);
state.Save(kind, result.ToPersistData());
// Then, compute the diagnostics for non-cached state sets.
if (nonCachedStateSets.Count > 0)
{
var compilationWithAnalyzers = await GetOrCreateCompilationWithAnalyzersAsync(document.Project, nonCachedStateSets, cancellationToken).ConfigureAwait(false);
var executor = new DocumentAnalysisExecutor(document, span: null, kind, compilationWithAnalyzers, DiagnosticAnalyzerInfoCache);
foreach (var stateSet in nonCachedStateSets)
{
var computedData = await ComputeDocumentAnalysisDataAsync(executor, stateSet, cancellationToken).ConfigureAwait(false);
PersistAndRaiseDiagnosticsIfNeeded(computedData, stateSet);
}
RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, result.OldItems, result.Items);
var asyncToken = AnalyzerService.Listener.BeginAsyncOperation(nameof(AnalyzeDocumentForKindAsync));
var _2 = ReportAnalyzerPerformanceAsync(document, compilationWithAnalyzers, cancellationToken).CompletesAsyncOperation(asyncToken);
}
var asyncToken = AnalyzerService.Listener.BeginAsyncOperation(nameof(AnalyzeDocumentForKindAsync));
var _ = ReportAnalyzerPerformanceAsync(document, compilation, cancellationToken).CompletesAsyncOperation(asyncToken);
}
catch (Exception e) when (FatalError.ReportUnlessCanceled(e))
{
throw ExceptionUtilities.Unreachable;
}
void PersistAndRaiseDiagnosticsIfNeeded(DocumentAnalysisData result, StateSet stateSet)
{
if (result.FromCache == true)
{
RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, result.Items);
return;
}
// no cancellation after this point.
var state = stateSet.GetOrCreateActiveFileState(document.Id);
state.Save(kind, result.ToPersistData());
RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, result.OldItems, result.Items);
}
}
public async Task AnalyzeProjectAsync(Project project, bool semanticsChanged, InvocationReasons reasons, CancellationToken cancellationToken)
......
......@@ -21,8 +21,6 @@ public InternalDiagnosticsOptionsProvider()
}
public ImmutableArray<IOption> Options { get; } = ImmutableArray.Create<IOption>(
InternalDiagnosticsOptions.CompilationEndCodeFix,
InternalDiagnosticsOptions.UseCompilationEndCodeFixHeuristic,
InternalDiagnosticsOptions.PreferLiveErrorsOnOpenedFiles,
InternalDiagnosticsOptions.PreferBuildErrorsOverLiveErrors);
}
......
......@@ -10,12 +10,6 @@ internal static class InternalDiagnosticsOptions
{
private const string LocalRegistryPath = @"Roslyn\Internal\Diagnostics\";
public static readonly Option2<bool> CompilationEndCodeFix = new Option2<bool>(nameof(InternalDiagnosticsOptions), "Enable Compilation End Code Fix", defaultValue: true,
storageLocations: new LocalUserProfileStorageLocation(LocalRegistryPath + "Enable Compilation End Code Fix"));
public static readonly Option2<bool> UseCompilationEndCodeFixHeuristic = new Option2<bool>(nameof(InternalDiagnosticsOptions), "Enable Compilation End Code Fix With Heuristic", defaultValue: true,
storageLocations: new LocalUserProfileStorageLocation(LocalRegistryPath + "Enable Compilation End Code Fix With Heuristic"));
public static readonly Option2<bool> PreferLiveErrorsOnOpenedFiles = new Option2<bool>(nameof(InternalDiagnosticsOptions), "Live errors will be preferred over errors from build on opened files from same analyzer", defaultValue: true,
storageLocations: new LocalUserProfileStorageLocation(LocalRegistryPath + "Live errors will be preferred over errors from build on opened files from same analyzer"));
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册