diff --git a/src/EditorFeatures/Test/Diagnostics/DiagnosticDataSerializerTests.cs b/src/EditorFeatures/Test/Diagnostics/DiagnosticDataSerializerTests.cs index d069e87302c9465c4ed3b9913aaae87de10301ac..8324d3256b4cc8e2bee0d19fee639ab36926c747 100644 --- a/src/EditorFeatures/Test/Diagnostics/DiagnosticDataSerializerTests.cs +++ b/src/EditorFeatures/Test/Diagnostics/DiagnosticDataSerializerTests.cs @@ -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 } diff --git a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb index bab9d25eb39a71de8bae46ac941764247df15b7f..adb6a65fc9ed67b28c4897e6460f9b0d3b84d915 100644 --- a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb +++ b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb @@ -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()) diff --git a/src/Features/Core/Portable/Diagnostics/AnalyzerHelper.cs b/src/Features/Core/Portable/Diagnostics/AnalyzerHelper.cs index c7237894f9eded1b5648a48d8449a1508885e377..63c3744e80e128170e7d8ce496c769527cc50f83 100644 --- a/src/Features/Core/Portable/Diagnostics/AnalyzerHelper.cs +++ b/src/Features/Core/Portable/Diagnostics/AnalyzerHelper.cs @@ -298,115 +298,6 @@ private static void AssertCompilation(Project project, Compilation compilation1) Contract.ThrowIfFalse(compilation1 == compilation2); } - /// - /// Return all local diagnostics (syntax, semantic) that belong to given document for the given StateSet (analyzer) by calculating them - /// - public static async Task> 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(); - } - - if (loadDiagnostic != null) - { - return SpecializedCollections.EmptyEnumerable(); - } - - 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(); - } - - // 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(); - } - } - - // REVIEW: more unnecessary allocations just to get diagnostics per analyzer - var singleAnalyzer = ImmutableArray.Create(analyzer); - var skippedAnalyzerInfo = document.Project.GetSkippedAnalyzersInfo(analyzerInfoCache); - ImmutableArray filteredIds; - - switch (kind) - { - case AnalysisKind.Syntax: - var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - if (tree == null) - { - return SpecializedCollections.EmptyEnumerable(); - } - - 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(); - } - - 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> ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync( DocumentDiagnosticAnalyzer analyzer, Document document, @@ -624,6 +515,37 @@ IEnumerable ConvertToLocalDiagnosticsWithCompilation() } } +#if DEBUG + internal static bool AreEquivalent(Diagnostic[] diagnosticsA, Diagnostic[] diagnosticsB) + { + var set = new HashSet(diagnosticsA, DiagnosticComparer.Instance); + return set.SetEquals(diagnosticsB); + } + + private sealed class DiagnosticComparer : IEqualityComparer + { + 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) diff --git a/src/Features/Core/Portable/Diagnostics/DefaultDiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/DefaultDiagnosticAnalyzerService.cs index 127f4d84c5e108995d7f6692e1849fd32c881310..efa3bc0542ec01fa7124653b255d0839d1cadb77 100644 --- a/src/Features/Core/Portable/Diagnostics/DefaultDiagnosticAnalyzerService.cs +++ b/src/Features/Core/Portable/Diagnostics/DefaultDiagnosticAnalyzerService.cs @@ -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.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(); diff --git a/src/Features/Core/Portable/Diagnostics/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/DocumentAnalysisExecutor.cs new file mode 100644 index 0000000000000000000000000000000000000000..1a799c076106fcd19dbcbf7b26a46aa42b2e35fc --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/DocumentAnalysisExecutor.cs @@ -0,0 +1,300 @@ +// 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 +{ + /// + /// Executes analyzers on a document for computing local syntax/semantic diagnostics. + /// + internal sealed class DocumentAnalysisExecutor + { + private ImmutableDictionary>? _lazySyntaxDiagnostics; + private ImmutableDictionary>? _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; } + + /// + /// Return all local diagnostics (syntax, semantic) that belong to given document for the given StateSet (analyzer) by calculating them + /// + public async Task> 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(); + } + + if (loadDiagnostic != null) + { + return SpecializedCollections.EmptyEnumerable(); + } + + 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(); + } + + // 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(); + } + } + + var skippedAnalyzerInfo = Document.Project.GetSkippedAnalyzersInfo(AnalyzerInfoCache); + ImmutableArray filteredIds; + + switch (Kind) + { + case AnalysisKind.Syntax: + var tree = await Document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + if (tree == null) + { + return SpecializedCollections.EmptyEnumerable(); + } + + 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(); + } + + 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> 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.Empty; + } + + private async Task> 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.Empty; + + async Task 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(); + 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 + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs index c308fd4b66447b9ec506300f9e2acdbdfdf6964a..3127af80f4075c22333df6eaddfe03fc5cb92317 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs @@ -23,11 +23,47 @@ namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 internal partial class DiagnosticIncrementalAnalyzer { /// - /// 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. /// - private async Task GetDocumentAnalysisDataAsync( - CompilationWithAnalyzers? compilation, Document document, StateSet stateSet, AnalysisKind kind, CancellationToken cancellationToken) + private async Task 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.Empty); + } + + return null; + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + + /// + /// Computes all local diagnostics (syntax, semantic) that belong to given document for the given StateSet (analyzer). + /// + private static async Task 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.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()); } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs index 77d744cf1f8483a4896b1f267fffe2a6bce59508..095c10ea8ade1eb2e0a9243467f5166775090a37 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs @@ -73,29 +73,8 @@ private abstract class DiagnosticGetter protected ImmutableArray GetDiagnosticData() => (_lazyDataBuilder != null) ? _lazyDataBuilder.ToImmutableArray() : ImmutableArray.Empty; - protected abstract Task> GetDiagnosticsAsync(StateSet stateSet, Project project, DocumentId? documentId, AnalysisKind kind, CancellationToken cancellationToken); protected abstract Task AppendDiagnosticsAsync(Project project, IEnumerable documentIds, bool includeProjectNonLocalResult, CancellationToken cancellationToken); - public async Task> 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.Empty; - } - - var stateSet = StateManager.GetOrCreateStateSet(project, analyzer); - if (stateSet == null) - { - return ImmutableArray.Empty; - } - - var diagnostics = await GetDiagnosticsAsync(stateSet, project, DocumentId, analysisKind, cancellationToken).ConfigureAwait(false); - - return IncludeSuppressedDiagnostics ? diagnostics : diagnostics.WhereAsArray(d => !d.IsSuppressed); - } - public async Task> GetDiagnosticsAsync(CancellationToken cancellationToken) { if (ProjectId != null) @@ -182,7 +161,27 @@ protected override async Task AppendDiagnosticsAsync(Project project, IEnumerabl } } - protected override async Task> GetDiagnosticsAsync(StateSet stateSet, Project project, DocumentId? documentId, AnalysisKind kind, CancellationToken cancellationToken) + public async Task> 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.Empty; + } + + var stateSet = StateManager.GetOrCreateStateSet(project, analyzer); + if (stateSet == null) + { + return ImmutableArray.Empty; + } + + var diagnostics = await GetDiagnosticsAsync(stateSet, project, DocumentId, analysisKind, cancellationToken).ConfigureAwait(false); + + return IncludeSuppressedDiagnostics ? diagnostics : diagnostics.WhereAsArray(d => !d.IsSuppressed); + } + + private async Task> 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> 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(); - } } } } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs index 37dc29c65ef0c905943aea3ff29240fc22b45692..15d985918143be312b1deebf69c46945783fd034 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs @@ -17,7 +17,6 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.Workspaces.Diagnostics; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 @@ -26,8 +25,8 @@ internal partial class DiagnosticIncrementalAnalyzer { public async Task TryAppendDiagnosticsForSpanAsync(Document document, TextSpan range, ArrayBuilder result, string? diagnosticId, bool includeSuppressedDiagnostics, bool blockForData, Func? addOperationScope, CancellationToken cancellationToken) { - var getter = await LatestDiagnosticsForSpanGetter.CreateAsync(this, document, range, blockForData, includeSuppressedDiagnostics, diagnosticId, cancellationToken).ConfigureAwait(false); - return await getter.TryGetAsync(result, addOperationScope, cancellationToken).ConfigureAwait(false); + var getter = LatestDiagnosticsForSpanGetter.Create(this, document, range, blockForData, addOperationScope, includeSuppressedDiagnostics, diagnosticId); + return await getter.TryGetAsync(result, cancellationToken).ConfigureAwait(false); } public async Task> GetDiagnosticsForSpanAsync(Document document, TextSpan range, string? diagnosticId, bool includeSuppressedDiagnostics, bool blockForData, Func? addOperationScope, CancellationToken cancellationToken) @@ -44,30 +43,26 @@ public async Task> GetDiagnosticsForSpanAsync(Doc private sealed class LatestDiagnosticsForSpanGetter { private readonly DiagnosticIncrementalAnalyzer _owner; - private readonly Project _project; private readonly Document _document; private readonly IEnumerable _stateSets; - private readonly CompilationWithAnalyzers? _compilation; private readonly TextSpan _range; private readonly bool _blockForData; private readonly bool _includeSuppressedDiagnostics; private readonly string? _diagnosticId; + private readonly Func? _addOperationScope; - // cache of project result - private ImmutableDictionary? _lazyProjectResultCache; + private delegate Task> DiagnosticsGetterAsync(DiagnosticAnalyzer analyzer, DocumentAnalysisExecutor executor, CancellationToken cancellationToken); - private delegate Task> DiagnosticsGetterAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken); - - public static async Task CreateAsync( + public static LatestDiagnosticsForSpanGetter Create( DiagnosticIncrementalAnalyzer owner, Document document, TextSpan range, bool blockForData, - bool includeSuppressedDiagnostics = false, - string? diagnosticId = null, - CancellationToken cancellationToken = default) + Func? addOperationScope, + bool includeSuppressedDiagnostics, + string? diagnosticId) { var stateSets = owner._stateManager .GetOrCreateStateSets(document.Project).Where(s => !owner.DiagnosticAnalyzerInfoCache.IsAnalyzerSuppressed(s.Analyzer, document.Project)); @@ -78,74 +73,68 @@ private sealed class LatestDiagnosticsForSpanGetter stateSets = stateSets.Where(s => owner.DiagnosticAnalyzerInfoCache.GetDiagnosticDescriptors(s.Analyzer).Any(d => d.Id == diagnosticId)).ToList(); } - var compilation = await CreateCompilationWithAnalyzersAsync(document.Project, stateSets, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - - return new LatestDiagnosticsForSpanGetter(owner, compilation, document, stateSets, diagnosticId, range, blockForData, includeSuppressedDiagnostics); + return new LatestDiagnosticsForSpanGetter(owner, document, stateSets, diagnosticId, range, blockForData, addOperationScope, includeSuppressedDiagnostics); } private LatestDiagnosticsForSpanGetter( DiagnosticIncrementalAnalyzer owner, - CompilationWithAnalyzers? compilation, Document document, IEnumerable stateSets, string? diagnosticId, - TextSpan range, bool blockForData, bool includeSuppressedDiagnostics) + TextSpan range, + bool blockForData, + Func? addOperationScope, + bool includeSuppressedDiagnostics) { _owner = owner; - - _project = document.Project; _document = document; - _stateSets = stateSets; _diagnosticId = diagnosticId; - _compilation = compilation; - _range = range; _blockForData = blockForData; + _addOperationScope = addOperationScope; _includeSuppressedDiagnostics = includeSuppressedDiagnostics; } - public async Task TryGetAsync(ArrayBuilder list, Func? addOperationScope, CancellationToken cancellationToken) + public async Task TryGetAsync(ArrayBuilder list, CancellationToken cancellationToken) { try { var containsFullResult = true; + + // Try to get cached diagnostics, and also compute non-cached state sets that need diagnostic computation. + using var _1 = ArrayBuilder.GetInstance(out var syntaxStateSets); + using var _2 = ArrayBuilder.GetInstance(out var semanticSpanBasedStateSets); + using var _3 = ArrayBuilder.GetInstance(out var semanticDocumentBasedStateSets); foreach (var stateSet in _stateSets) { - var analyzerTypeName = stateSet.Analyzer.GetType().Name; - using (addOperationScope?.Invoke(analyzerTypeName)) - using (addOperationScope is object ? RoslynEventSource.LogInformationalBlock(FunctionId.DiagnosticAnalyzerService_GetDiagnosticsForSpanAsync, analyzerTypeName, cancellationToken) : default) + if (!await TryGetCachedDocumentDiagnosticsAsync(stateSet, AnalysisKind.Syntax, list, cancellationToken).ConfigureAwait(false)) { - cancellationToken.ThrowIfCancellationRequested(); - - containsFullResult &= await TryGetSyntaxAndSemanticDiagnosticsAsync(stateSet, list, cancellationToken).ConfigureAwait(false); + syntaxStateSets.Add(stateSet); + } - // check whether compilation end code fix is enabled - if (!_document.Project.Solution.Workspace.Options.GetOption(InternalDiagnosticsOptions.CompilationEndCodeFix)) + if (!await TryGetCachedDocumentDiagnosticsAsync(stateSet, AnalysisKind.Semantic, list, cancellationToken).ConfigureAwait(false)) + { + // Check whether we want up-to-date document wide semantic diagnostics + var spanBased = stateSet.Analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis(); + if (!_blockForData && !spanBased) { - continue; + containsFullResult = false; } - - // check whether heuristic is enabled - if (_blockForData && _document.Project.Solution.Workspace.Options.GetOption(InternalDiagnosticsOptions.UseCompilationEndCodeFixHeuristic)) + else { - var avoidLoadingData = true; - var state = stateSet.GetOrCreateProjectState(_project.Id); - var result = await state.GetAnalysisDataAsync(_owner.PersistentStorageService, _document, avoidLoadingData, cancellationToken).ConfigureAwait(false); - - // no previous compilation end diagnostics in this file. - var version = await GetDiagnosticVersionAsync(_project, cancellationToken).ConfigureAwait(false); - if (state.IsEmpty(_document.Id) || result.Version != version) - { - continue; - } + var stateSets = spanBased ? semanticSpanBasedStateSets : semanticDocumentBasedStateSets; + stateSets.Add(stateSet); } - - containsFullResult &= await TryGetProjectDiagnosticsAsync(stateSet, list, cancellationToken).ConfigureAwait(false); } } - // if we are blocked for data, then we should always have full result. + // Compute diagnostics for non-cached state sets. + await ComputeDocumentDiagnosticsAsync(syntaxStateSets, AnalysisKind.Syntax, _range, list, cancellationToken).ConfigureAwait(false); + await ComputeDocumentDiagnosticsAsync(semanticSpanBasedStateSets, AnalysisKind.Semantic, _range, list, cancellationToken).ConfigureAwait(false); + await ComputeDocumentDiagnosticsAsync(semanticDocumentBasedStateSets, AnalysisKind.Semantic, span: null, list, cancellationToken).ConfigureAwait(false); + + // If we are blocked for data, then we should always have full result. Debug.Assert(!_blockForData || containsFullResult); return containsFullResult; } @@ -155,171 +144,9 @@ public async Task TryGetAsync(ArrayBuilder list, Func TryGetSyntaxAndSemanticDiagnosticsAsync(StateSet stateSet, ArrayBuilder list, CancellationToken cancellationToken) - { - // unfortunately, we need to special case compiler diagnostic analyzer so that - // we can do span based analysis even though we implemented it as semantic model analysis - if (stateSet.Analyzer.IsCompilerAnalyzer()) - { - return await TryGetSyntaxAndSemanticCompilerDiagnosticsAsync(stateSet, list, cancellationToken).ConfigureAwait(false); - } - - var fullResult = true; - fullResult &= await TryGetDocumentDiagnosticsAsync(stateSet, AnalysisKind.Syntax, GetSyntaxDiagnosticsAsync, list, cancellationToken).ConfigureAwait(false); - fullResult &= await TryGetDocumentDiagnosticsAsync(stateSet, AnalysisKind.Semantic, GetSemanticDiagnosticsAsync, list, cancellationToken).ConfigureAwait(false); - - return fullResult; - } - - private async Task TryGetSyntaxAndSemanticCompilerDiagnosticsAsync(StateSet stateSet, ArrayBuilder list, CancellationToken cancellationToken) - { - // First, get syntax errors and semantic errors - var fullResult = true; - fullResult &= await TryGetDocumentDiagnosticsAsync(stateSet, AnalysisKind.Syntax, GetCompilerSyntaxDiagnosticsAsync, list, cancellationToken).ConfigureAwait(false); - fullResult &= await TryGetDocumentDiagnosticsAsync(stateSet, AnalysisKind.Semantic, GetCompilerSemanticDiagnosticsAsync, list, cancellationToken).ConfigureAwait(false); - - return fullResult; - } - - private async Task> GetCompilerSyntaxDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) - { - var root = await _document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root == null) - { - return SpecializedCollections.EmptyEnumerable(); - } - - return root.GetDiagnostics().ConvertToLocalDiagnostics(_document, _range); - } - - private async Task> GetCompilerSemanticDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) - { - var model = await _document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - if (model == null) - { - return SpecializedCollections.EmptyEnumerable(); - } - -#if DEBUG - VerifyDiagnostics(model); -#endif - - var root = await _document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - if (root == null) - { - return SpecializedCollections.EmptyEnumerable(); - } - - var adjustedSpan = AdjustSpan(_document, root, _range); - var diagnostics = model.GetDeclarationDiagnostics(adjustedSpan, cancellationToken).Concat(model.GetMethodBodyDiagnostics(adjustedSpan, cancellationToken)); - - return diagnostics.ConvertToLocalDiagnostics(_document, _range); - } - - private Task> GetSyntaxDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) - => AnalyzerHelper.ComputeDiagnosticsAsync(analyzer, _document, AnalysisKind.Syntax, _owner.DiagnosticAnalyzerInfoCache, _compilation, _range, cancellationToken); - - private Task> GetSemanticDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) - { - var supportsSemanticInSpan = analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis(); - - var analysisSpan = supportsSemanticInSpan ? (TextSpan?)_range : null; - return AnalyzerHelper.ComputeDiagnosticsAsync(analyzer, _document, AnalysisKind.Semantic, _owner.DiagnosticAnalyzerInfoCache, _compilation, analysisSpan, cancellationToken); - } - - private async Task> GetProjectDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) - { - if (_lazyProjectResultCache == null) - { - // execute whole project as one shot and cache the result. - var analysisResult = await _owner.GetProjectAnalysisDataAsync(_compilation, _project, _stateSets, forceAnalyzerRun: true, cancellationToken).ConfigureAwait(false); - _lazyProjectResultCache = analysisResult.Result; - } - - if (_lazyProjectResultCache.TryGetValue(analyzer, out var result)) - { - return result.GetDocumentDiagnostics(_document.Id, AnalysisKind.NonLocal); - } - - return ImmutableArray.Empty; - } - -#if DEBUG - private void VerifyDiagnostics(SemanticModel model) - { - // Exclude unused import diagnostics since they are never reported when a span is passed. - // (See CSharp/VisualBasicCompilation.GetDiagnosticsForMethodBodiesInTree.) - bool shouldInclude(Diagnostic d) => _range.IntersectsWith(d.Location.SourceSpan) && !IsUnusedImportDiagnostic(d); - - // make sure what we got from range is same as what we got from whole diagnostics - var rangeDeclaractionDiagnostics = model.GetDeclarationDiagnostics(_range).ToArray(); - var rangeMethodBodyDiagnostics = model.GetMethodBodyDiagnostics(_range).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 (!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); - } - - static bool IsUnusedImportDiagnostic(Diagnostic d) - { - switch (d.Id) - { - case "CS8019": - case "BC50000": - case "BC50001": - return true; - default: - return false; - } - } - } -#endif - - private static TextSpan AdjustSpan(Document document, SyntaxNode root, TextSpan span) - { - // this 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. - - var service = document.GetRequiredLanguageService(); - var startNode = service.GetContainingMemberDeclaration(root, span.Start); - var endNode = service.GetContainingMemberDeclaration(root, span.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; - var endSpan = service.IsMethodLevelMember(endNode) ? endNode.FullSpan : span; - - return TextSpan.FromBounds(Math.Min(startSpan.Start, endSpan.Start), Math.Max(startSpan.End, endSpan.End)); - } - - private async Task TryGetDocumentDiagnosticsAsync( + private async Task TryGetCachedDocumentDiagnosticsAsync( StateSet stateSet, AnalysisKind kind, - DiagnosticsGetterAsync diagnosticGetterAsync, ArrayBuilder list, CancellationToken cancellationToken) { @@ -337,79 +164,47 @@ private static TextSpan AdjustSpan(Document document, SyntaxNode root, TextSpan var version = await GetDiagnosticVersionAsync(_document.Project, cancellationToken).ConfigureAwait(false); if (existingData.Version == version) { - if (existingData.Items.IsEmpty) + if (!existingData.Items.IsEmpty) { - return true; + list.AddRange(existingData.Items.Where(ShouldInclude)); } - list.AddRange(existingData.Items.Where(ShouldInclude)); return true; } - cancellationToken.ThrowIfCancellationRequested(); - - // check whether we want up-to-date document wide diagnostics - var supportsSemanticInSpan = stateSet.Analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis(); - if (!BlockForData(kind, supportsSemanticInSpan)) - { - return false; - } - - var dx = await diagnosticGetterAsync(stateSet.Analyzer, cancellationToken).ConfigureAwait(false); - if (dx != null) - { - // no state yet - list.AddRange(dx.Where(ShouldInclude)); - } - - return true; + return false; } - private async Task TryGetProjectDiagnosticsAsync( - StateSet stateSet, + private async Task ComputeDocumentDiagnosticsAsync( + IReadOnlyList stateSets, + AnalysisKind kind, + TextSpan? span, ArrayBuilder list, CancellationToken cancellationToken) { - if (!stateSet.Analyzer.SupportsProjectDiagnosticAnalysis()) + if (stateSets.Count == 0) { - return true; + return; } - // make sure we get state even when none of our analyzer has ran yet. - // but this shouldn't create analyzer that doesn't belong to this project (language) - var state = stateSet.GetOrCreateProjectState(_document.Project.Id); - - // see whether we can use existing info - var result = await state.GetAnalysisDataAsync(_owner.PersistentStorageService, _document, avoidLoadingData: true, cancellationToken: cancellationToken).ConfigureAwait(false); - var version = await GetDiagnosticVersionAsync(_document.Project, cancellationToken).ConfigureAwait(false); - if (result.Version == version) + var compilationWithAnalyzers = await CreateCompilationWithAnalyzersAsync(_document.Project, stateSets, _includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + var executor = new DocumentAnalysisExecutor(_document, span, kind, compilationWithAnalyzers, _owner.DiagnosticAnalyzerInfoCache); + foreach (var stateSet in stateSets) { - var existingData = result.GetDocumentDiagnostics(_document.Id, AnalysisKind.NonLocal); - if (!existingData.IsEmpty) + cancellationToken.ThrowIfCancellationRequested(); + + var analyzerTypeName = stateSet.Analyzer.GetType().Name; + using (_addOperationScope?.Invoke(analyzerTypeName)) + using (_addOperationScope is object ? RoslynEventSource.LogInformationalBlock(FunctionId.DiagnosticAnalyzerService_GetDiagnosticsForSpanAsync, analyzerTypeName, cancellationToken) : default) { - list.AddRange(existingData.Where(ShouldInclude)); + var dx = await executor.ComputeDiagnosticsAsync(stateSet.Analyzer, cancellationToken).ConfigureAwait(false); + if (dx != null) + { + // no state yet + list.AddRange(dx.Where(ShouldInclude)); + } } - - return true; - } - - cancellationToken.ThrowIfCancellationRequested(); - - // check whether we want up-to-date document wide diagnostics - var supportsSemanticInSpan = stateSet.Analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis(); - if (!BlockForData(AnalysisKind.NonLocal, supportsSemanticInSpan)) - { - return false; - } - - var dx = await GetProjectDiagnosticsAsync(stateSet.Analyzer, cancellationToken).ConfigureAwait(false); - if (!dx.IsEmpty) - { - // no state yet - list.AddRange(dx.Where(ShouldInclude)); } - - return true; } private bool ShouldInclude(DiagnosticData diagnostic) @@ -418,52 +213,6 @@ private bool ShouldInclude(DiagnosticData diagnostic) && (_includeSuppressedDiagnostics || !diagnostic.IsSuppressed) && (_diagnosticId == null || _diagnosticId == diagnostic.Id); } - - private bool BlockForData(AnalysisKind kind, bool supportsSemanticInSpan) - { - if (kind == AnalysisKind.Semantic && !supportsSemanticInSpan && !_blockForData) - { - return false; - } - - if (kind == AnalysisKind.NonLocal && !_blockForData) - { - return false; - } - - return true; - } - } - -#if DEBUG - internal static bool AreEquivalent(Diagnostic[] diagnosticsA, Diagnostic[] diagnosticsB) - { - var set = new HashSet(diagnosticsA, DiagnosticComparer.Instance); - return set.SetEquals(diagnosticsB); - } - - private sealed class DiagnosticComparer : IEqualityComparer - { - 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 } } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs index cb6b5f0be52a20621736038b192b8a0d27f16c37..6f3c527bb3eaa05bcfb7ecf86f5b7db45c20679f 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs @@ -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.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) diff --git a/src/Features/Core/Portable/Diagnostics/InternalDiagnosticsOptionsProvider.cs b/src/Features/Core/Portable/Diagnostics/InternalDiagnosticsOptionsProvider.cs index 302d5d8252032d0f89bc289446fd6c362302aea3..ab84a76bf747b47255d107c2b8a10fd05803e63e 100644 --- a/src/Features/Core/Portable/Diagnostics/InternalDiagnosticsOptionsProvider.cs +++ b/src/Features/Core/Portable/Diagnostics/InternalDiagnosticsOptionsProvider.cs @@ -21,8 +21,6 @@ public InternalDiagnosticsOptionsProvider() } public ImmutableArray Options { get; } = ImmutableArray.Create( - InternalDiagnosticsOptions.CompilationEndCodeFix, - InternalDiagnosticsOptions.UseCompilationEndCodeFixHeuristic, InternalDiagnosticsOptions.PreferLiveErrorsOnOpenedFiles, InternalDiagnosticsOptions.PreferBuildErrorsOverLiveErrors); } diff --git a/src/Workspaces/Core/Portable/Diagnostics/InternalDiagnosticsOptions.cs b/src/Workspaces/Core/Portable/Diagnostics/InternalDiagnosticsOptions.cs index b16b70e74eeed5f1f4ce514b1dfd094b891d51e5..d516a4ee4257d10df50811499260f3e58a6f29d4 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/InternalDiagnosticsOptions.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/InternalDiagnosticsOptions.cs @@ -10,12 +10,6 @@ internal static class InternalDiagnosticsOptions { private const string LocalRegistryPath = @"Roslyn\Internal\Diagnostics\"; - public static readonly Option2 CompilationEndCodeFix = new Option2(nameof(InternalDiagnosticsOptions), "Enable Compilation End Code Fix", defaultValue: true, - storageLocations: new LocalUserProfileStorageLocation(LocalRegistryPath + "Enable Compilation End Code Fix")); - - public static readonly Option2 UseCompilationEndCodeFixHeuristic = new Option2(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 PreferLiveErrorsOnOpenedFiles = new Option2(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"));