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"));