diff --git a/src/Features/Core/Portable/Diagnostics/Analyzers/IDocumentDiagnosticAnalyzer.cs b/src/Features/Core/Portable/Diagnostics/Analyzers/IDocumentDiagnosticAnalyzer.cs index a8bce6846ab56..b1e24468ded7a 100644 --- a/src/Features/Core/Portable/Diagnostics/Analyzers/IDocumentDiagnosticAnalyzer.cs +++ b/src/Features/Core/Portable/Diagnostics/Analyzers/IDocumentDiagnosticAnalyzer.cs @@ -3,7 +3,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.LanguageServices; namespace Microsoft.CodeAnalysis.Diagnostics { @@ -12,10 +11,14 @@ namespace Microsoft.CodeAnalysis.Diagnostics /// internal abstract class DocumentDiagnosticAnalyzer : DiagnosticAnalyzer { + // REVIEW: why DocumentDiagnosticAnalyzer doesn't have span based analysis? public abstract Task AnalyzeSyntaxAsync(Document document, Action addDiagnostic, CancellationToken cancellationToken); public abstract Task AnalyzeSemanticsAsync(Document document, Action addDiagnostic, CancellationToken cancellationToken); - public override void Initialize(AnalysisContext context) + /// + /// it is not allowed one to implement both DocumentDiagnosticAnalzyer and DiagnosticAnalyzer + /// + public sealed override void Initialize(AnalysisContext context) { } } diff --git a/src/Features/Core/Portable/Diagnostics/Analyzers/IProjectDiagnosticAnalyzer.cs b/src/Features/Core/Portable/Diagnostics/Analyzers/IProjectDiagnosticAnalyzer.cs index 13aea173a0651..be0be072e99b5 100644 --- a/src/Features/Core/Portable/Diagnostics/Analyzers/IProjectDiagnosticAnalyzer.cs +++ b/src/Features/Core/Portable/Diagnostics/Analyzers/IProjectDiagnosticAnalyzer.cs @@ -3,7 +3,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.LanguageServices; namespace Microsoft.CodeAnalysis.Diagnostics { @@ -14,7 +13,10 @@ internal abstract class ProjectDiagnosticAnalyzer : DiagnosticAnalyzer { public abstract Task AnalyzeProjectAsync(Project project, Action addDiagnostic, CancellationToken cancellationToken); - public override void Initialize(AnalysisContext context) + /// + /// it is not allowed one to implement both ProjectDiagnosticAnalzyer and DiagnosticAnalyzer + /// + public sealed override void Initialize(AnalysisContext context) { } } diff --git a/src/Features/Core/Portable/Diagnostics/BaseDiagnosticIncrementalAnalyzer.cs b/src/Features/Core/Portable/Diagnostics/BaseDiagnosticIncrementalAnalyzer.cs index c757cada1569f..d05b08dbb1112 100644 --- a/src/Features/Core/Portable/Diagnostics/BaseDiagnosticIncrementalAnalyzer.cs +++ b/src/Features/Core/Portable/Diagnostics/BaseDiagnosticIncrementalAnalyzer.cs @@ -199,22 +199,8 @@ protected BaseDiagnosticIncrementalAnalyzer(DiagnosticAnalyzerService owner, Wor /// It is up to each incremental analyzer how they will merge this information with live diagnostic info. /// /// this API doesn't have cancellationToken since it can't be cancelled. - /// - /// given diagnostics are project wide diagnostics that doesn't contain a source location. - /// - public abstract Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.BatchUpdateToken token, Project project, ImmutableArray diagnostics); - - /// - /// Callback from build listener. - /// - /// Given diagnostics are errors host got from explicit build. - /// It is up to each incremental analyzer how they will merge this information with live diagnostic info - /// - /// this API doesn't have cancellationToken since it can't be cancelled. - /// - /// given diagnostics are ones that has a source location. /// - public abstract Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.BatchUpdateToken token, Document document, ImmutableArray diagnostics); + public abstract Task SynchronizeWithBuildAsync(Workspace workspace, ImmutableDictionary> diagnostics); #endregion internal DiagnosticAnalyzerService Owner { get; } diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_BuildSynchronization.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_BuildSynchronization.cs index 8b17416f83d52..429a6fee8a024 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_BuildSynchronization.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_BuildSynchronization.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Concurrent; using System.Collections.Immutable; using System.Threading.Tasks; using Roslyn.Utilities; @@ -10,81 +8,20 @@ namespace Microsoft.CodeAnalysis.Diagnostics { internal partial class DiagnosticAnalyzerService { - /// - /// Start new Batch build diagnostics update token. - /// - public IDisposable BeginBatchBuildDiagnosticsUpdate(Solution solution) - { - return new BatchUpdateToken(solution); - } - /// /// Synchronize build errors with live error. /// /// no cancellationToken since this can't be cancelled /// - public Task SynchronizeWithBuildAsync(IDisposable batchUpdateToken, Project project, ImmutableArray diagnostics) + public Task SynchronizeWithBuildAsync(Workspace workspace, ImmutableDictionary> diagnostics) { - var token = (BatchUpdateToken)batchUpdateToken; - token.CheckProjectInSnapshot(project); - - BaseDiagnosticIncrementalAnalyzer analyzer; - if (_map.TryGetValue(project.Solution.Workspace, out analyzer)) - { - return analyzer.SynchronizeWithBuildAsync(token, project, diagnostics); - } - - return SpecializedTasks.EmptyTask; - } - - /// - /// Synchronize build errors with live error - /// - /// no cancellationToken since this can't be cancelled - /// - public Task SynchronizeWithBuildAsync(IDisposable batchUpdateToken, Document document, ImmutableArray diagnostics) - { - var token = (BatchUpdateToken)batchUpdateToken; - token.CheckDocumentInSnapshot(document); - BaseDiagnosticIncrementalAnalyzer analyzer; - if (_map.TryGetValue(document.Project.Solution.Workspace, out analyzer)) + if (_map.TryGetValue(workspace, out analyzer)) { - return analyzer.SynchronizeWithBuildAsync(token, document, diagnostics); + return analyzer.SynchronizeWithBuildAsync(workspace, diagnostics); } return SpecializedTasks.EmptyTask; } - - public class BatchUpdateToken : IDisposable - { - public readonly ConcurrentDictionary _cache = new ConcurrentDictionary(concurrencyLevel: 2, capacity: 1); - private readonly Solution _solution; - - public BatchUpdateToken(Solution solution) - { - _solution = solution; - } - - public object GetCache(object key, Func cacheCreator) - { - return _cache.GetOrAdd(key, cacheCreator); - } - - public void CheckDocumentInSnapshot(Document document) - { - Contract.ThrowIfFalse(_solution.GetDocument(document.Id) == document); - } - - public void CheckProjectInSnapshot(Project project) - { - Contract.ThrowIfFalse(_solution.GetProject(project.Id) == project); - } - - public void Dispose() - { - _cache.Clear(); - } - } } } diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_IncrementalAnalyzer.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_IncrementalAnalyzer.cs index cdd6f7767446c..eceed50ab7284 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_IncrementalAnalyzer.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_IncrementalAnalyzer.cs @@ -180,14 +180,9 @@ public override bool ContainsDiagnostics(Workspace workspace, ProjectId projectI #endregion #region build synchronization - public override Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.BatchUpdateToken token, Project project, ImmutableArray diagnostics) + public override Task SynchronizeWithBuildAsync(Workspace workspace, ImmutableDictionary> diagnostics) { - return Analyzer.SynchronizeWithBuildAsync(token, project, diagnostics); - } - - public override Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.BatchUpdateToken token, Document document, ImmutableArray diagnostics) - { - return Analyzer.SynchronizeWithBuildAsync(token, document, diagnostics); + return Analyzer.SynchronizeWithBuildAsync(workspace, diagnostics); } #endregion diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_UpdateSource.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_UpdateSource.cs index fefc437a36b95..7c468582f4700 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_UpdateSource.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerService_UpdateSource.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Immutable; using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.TestHooks; using Roslyn.Utilities; @@ -69,6 +70,22 @@ internal void RaiseBulkDiagnosticsUpdated(Action> } } + internal void RaiseBulkDiagnosticsUpdated(Func, Task> eventActionAsync) + { + // all diagnostics events are serialized. + var ev = _eventMap.GetEventHandlers>(DiagnosticsUpdatedEventName); + if (ev.HasHandlers) + { + // we do this bulk update to reduce number of tasks (with captured data) enqueued. + // we saw some "out of memory" due to us having long list of pending tasks in memory. + // this is to reduce for such case to happen. + Action raiseEvents = args => ev.RaiseEvent(handler => handler(this, args)); + + var asyncToken = Listener.BeginAsyncOperation(nameof(RaiseDiagnosticsUpdated)); + _eventQueue.ScheduleTask(() => eventActionAsync(raiseEvents)).CompletesAsyncOperation(asyncToken); + } + } + bool IDiagnosticUpdateSource.SupportGetDiagnostics { get { return true; } } ImmutableArray IDiagnosticUpdateSource.GetDiagnostics(Workspace workspace, ProjectId projectId, DocumentId documentId, object id, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) diff --git a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticAnalyzerDriver.cs b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticAnalyzerDriver.cs index 050859ae774ec..785c08a8129ab 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticAnalyzerDriver.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticAnalyzerDriver.cs @@ -291,12 +291,15 @@ public async Task> GetProjectDiagnosticsAsync(Diagnos using (var diagnostics = SharedPools.Default>().GetPooledObject()) { - if (_project.SupportsCompilation) + var projectAnalyzer = analyzer as ProjectDiagnosticAnalyzer; + if (projectAnalyzer != null) { - await this.GetCompilationDiagnosticsAsync(analyzer, diagnostics.Object).ConfigureAwait(false); + await this.GetProjectDiagnosticsWorkerAsync(projectAnalyzer, diagnostics.Object).ConfigureAwait(false); + return diagnostics.Object.ToImmutableArray(); } - await this.GetProjectDiagnosticsWorkerAsync(analyzer, diagnostics.Object).ConfigureAwait(false); + Contract.ThrowIfFalse(_project.SupportsCompilation); + await this.GetCompilationDiagnosticsAsync(analyzer, diagnostics.Object).ConfigureAwait(false); return diagnostics.Object.ToImmutableArray(); } @@ -307,29 +310,17 @@ public async Task> GetProjectDiagnosticsAsync(Diagnos } } - private async Task GetProjectDiagnosticsWorkerAsync(DiagnosticAnalyzer analyzer, List diagnostics) + private async Task GetProjectDiagnosticsWorkerAsync(ProjectDiagnosticAnalyzer analyzer, List diagnostics) { + try { - var projectAnalyzer = analyzer as ProjectDiagnosticAnalyzer; - if (projectAnalyzer == null) - { - return; - } - - try - { - await projectAnalyzer.AnalyzeProjectAsync(_project, diagnostics.Add, _cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (!IsCanceled(e, _cancellationToken)) - { - var compilation = await _project.GetCompilationAsync(_cancellationToken).ConfigureAwait(false); - OnAnalyzerException(e, analyzer, compilation); - } + await analyzer.AnalyzeProjectAsync(_project, diagnostics.Add, _cancellationToken).ConfigureAwait(false); } - catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + catch (Exception e) when (!IsCanceled(e, _cancellationToken)) { - throw ExceptionUtilities.Unreachable; + var compilation = await _project.GetCompilationAsync(_cancellationToken).ConfigureAwait(false); + OnAnalyzerException(e, analyzer, compilation); } } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateManager.cs b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateManager.cs index 3756d3202ce04..96ac1e9f691d3 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateManager.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateManager.cs @@ -68,16 +68,6 @@ public IEnumerable GetStateSets(Project project) return GetStateSets(project.Id).Where(s => s.Language == project.Language); } - /// - /// Return s that are added as the given 's AnalyzerReferences. - /// This will never create new but will return ones already created. - /// - public ImmutableArray GetBuildOnlyStateSets(object cache, Project project) - { - var stateSetCache = (IDictionary>)cache; - return stateSetCache.GetOrAdd(project, CreateBuildOnlyProjectStateSet); - } - /// /// Return s for the given . /// This will either return already created s for the specific snapshot of or @@ -126,7 +116,11 @@ public void RemoveStateSet(ProjectId projectId) _projectStates.RemoveStateSet(projectId); } - private ImmutableArray CreateBuildOnlyProjectStateSet(Project project) + /// + /// Return s that are added as the given 's AnalyzerReferences. + /// This will never create new but will return ones already created. + /// + public ImmutableArray CreateBuildOnlyProjectStateSet(Project project) { var referenceIdentities = project.AnalyzerReferences.Select(r => _analyzerManager.GetAnalyzerReferenceIdentity(r)).ToSet(); var stateSetMap = GetStateSets(project).ToDictionary(s => s.Analyzer, s => s); diff --git a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs index 2bba644812bae..eeb049a7703be 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; @@ -14,21 +12,42 @@ namespace Microsoft.CodeAnalysis.Diagnostics.EngineV1 { internal partial class DiagnosticIncrementalAnalyzer { - private readonly static Func s_cacheCreator = _ => new ConcurrentDictionary>(concurrencyLevel: 2, capacity: 10); - - public override async Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.BatchUpdateToken token, Project project, ImmutableArray diagnostics) + public override async Task SynchronizeWithBuildAsync(Workspace workspace, ImmutableDictionary> map) { - if (!PreferBuildErrors(project.Solution.Workspace)) + if (!PreferBuildErrors(workspace)) { // prefer live errors over build errors return; } + var solution = workspace.CurrentSolution; + foreach (var projectEntry in map) + { + var project = solution.GetProject(projectEntry.Key); + if (project == null) + { + continue; + } + + var stateSets = _stateManager.CreateBuildOnlyProjectStateSet(project); + var lookup = projectEntry.Value.ToLookup(d => d.DocumentId); + + // do project one first + await SynchronizeWithBuildAsync(project, stateSets, lookup[null]).ConfigureAwait(false); + + foreach (var document in project.Documents) + { + await SynchronizeWithBuildAsync(document, stateSets, lookup[document.Id]).ConfigureAwait(false); + } + } + } + + private async Task SynchronizeWithBuildAsync(Project project, IEnumerable stateSets, IEnumerable diagnostics) + { using (var poolObject = SharedPools.Default>().GetPooledObject()) { var lookup = CreateDiagnosticIdLookup(diagnostics); - - foreach (var stateSet in _stateManager.GetBuildOnlyStateSets(token.GetCache(_stateManager, s_cacheCreator), project)) + foreach (var stateSet in stateSets) { var descriptors = HostAnalyzerManager.GetDiagnosticDescriptors(stateSet.Analyzer); var liveDiagnostics = ConvertToLiveDiagnostics(lookup, descriptors, poolObject.Object); @@ -47,14 +66,9 @@ public override async Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.B } } - public override async Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.BatchUpdateToken token, Document document, ImmutableArray diagnostics) + private async Task SynchronizeWithBuildAsync(Document document, IEnumerable stateSets, IEnumerable diagnostics) { var workspace = document.Project.Solution.Workspace; - if (!PreferBuildErrors(workspace)) - { - // prefer live errors over build errors - return; - } // check whether, for opened documents, we want to prefer live diagnostics if (PreferLiveErrorsOnOpenedFiles(workspace) && workspace.IsDocumentOpen(document.Id)) @@ -68,7 +82,7 @@ public override async Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.B { var lookup = CreateDiagnosticIdLookup(diagnostics); - foreach (var stateSet in _stateManager.GetBuildOnlyStateSets(token.GetCache(_stateManager, s_cacheCreator), document.Project)) + foreach (var stateSet in stateSets) { // we are using Default so that things like LB can't use cached information var textVersion = VersionStamp.Default; @@ -141,13 +155,8 @@ private ImmutableArray MergeDiagnostics(ImmutableArray.Empty : builder.ToImmutable(); } - private static ILookup CreateDiagnosticIdLookup(ImmutableArray diagnostics) + private static ILookup CreateDiagnosticIdLookup(IEnumerable diagnostics) { - if (diagnostics.Length == 0) - { - return null; - } - return diagnostics.ToLookup(d => d.Id); } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs index dcf6738dab878..80f795ae0a668 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs @@ -236,7 +236,7 @@ protected void AppendDiagnostics(IEnumerable items) } } - protected virtual ImmutableArray GetDiagnosticData() + protected ImmutableArray GetDiagnosticData() { return _builder != null ? _builder.ToImmutableArray() : ImmutableArray.Empty; } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/AnalysisResult.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/AnalysisResult.cs new file mode 100644 index 0000000000000..f6cd1c6e2b6db --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/AnalysisResult.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + /// + /// This holds onto diagnostics for a specific version of project snapshot + /// in a way each kind of diagnostics can be queried fast. + /// + internal struct AnalysisResult + { + public readonly ProjectId ProjectId; + public readonly VersionStamp Version; + + // set of documents that has any kind of diagnostics on it + public readonly ImmutableHashSet DocumentIds; + public readonly bool IsEmpty; + + // map for each kind of diagnostics + // syntax locals and semantic locals are self explanatory. + // non locals means diagnostics that belong to a tree that are produced by analyzing other files. + // others means diagnostics that doesnt have locations. + private readonly ImmutableDictionary> _syntaxLocals; + private readonly ImmutableDictionary> _semanticLocals; + private readonly ImmutableDictionary> _nonLocals; + private readonly ImmutableArray _others; + + public AnalysisResult( + ProjectId projectId, VersionStamp version, ImmutableHashSet documentIds, bool isEmpty) + { + ProjectId = projectId; + Version = version; + DocumentIds = documentIds; + IsEmpty = isEmpty; + + _syntaxLocals = null; + _semanticLocals = null; + _nonLocals = null; + _others = default(ImmutableArray); + } + + public AnalysisResult( + ProjectId projectId, VersionStamp version, + ImmutableHashSet documentIds, + ImmutableDictionary> syntaxLocals, + ImmutableDictionary> semanticLocals, + ImmutableDictionary> nonLocals, + ImmutableArray others) + { + ProjectId = projectId; + Version = version; + DocumentIds = documentIds; + + _syntaxLocals = syntaxLocals; + _semanticLocals = semanticLocals; + _nonLocals = nonLocals; + _others = others; + + IsEmpty = DocumentIds.IsEmpty && _others.IsEmpty; + } + + // aggregated form means it has aggregated information but no actual data. + public bool IsAggregatedForm => _syntaxLocals == null; + + // this shouldn't be called for aggregated form. + public ImmutableDictionary> SyntaxLocals => ReturnIfNotDefalut(_syntaxLocals); + public ImmutableDictionary> SemanticLocals => ReturnIfNotDefalut(_semanticLocals); + public ImmutableDictionary> NonLocals => ReturnIfNotDefalut(_nonLocals); + public ImmutableArray Others => ReturnIfNotDefalut(_others); + + public ImmutableArray GetResultOrEmpty(ImmutableDictionary> map, DocumentId key) + { + // this is just a helper method. + ImmutableArray diagnostics; + if (map.TryGetValue(key, out diagnostics)) + { + return diagnostics; + } + + return ImmutableArray.Empty; + } + + public AnalysisResult ToAggregatedForm() + { + return new AnalysisResult(ProjectId, Version, DocumentIds, IsEmpty); + } + + private T ReturnIfNotDefalut(T value) + { + if (object.Equals(value, default(T))) + { + Contract.Fail("shouldn't be called"); + } + + return value; + } + } +} \ No newline at end of file diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/CompilerDiagnosticExecutor.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/CompilerDiagnosticExecutor.cs index 04acae4363269..e0222a07e02d3 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/CompilerDiagnosticExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/CompilerDiagnosticExecutor.cs @@ -14,11 +14,9 @@ namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 /// internal static class CompilerDiagnosticExecutor { - public static async Task AnalyzeAsync( - this Compilation compilation, ImmutableArray analyzers, CompilationWithAnalyzersOptions options, CancellationToken cancellationToken) + public static async Task> AnalyzeAsync(this CompilationWithAnalyzers analyzerDriver, Project project, CancellationToken cancellationToken) { - // Create driver that holds onto compilation and associated analyzers - var analyzerDriver = compilation.WithAnalyzers(analyzers, options); + var version = await DiagnosticIncrementalAnalyzer.GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); // Run all analyzers at once. // REVIEW: why there are 2 different cancellation token? one that I can give to constructor and one I can give in to each method? @@ -28,10 +26,14 @@ public static async Task AnalyzeAsync( // this is wierd, but now we iterate through each analyzer for each tree to get cached result. // REVIEW: no better way to do this? var noSpanFilter = default(TextSpan?); + var analyzers = analyzerDriver.Analyzers; + var compilation = analyzerDriver.Compilation; - var resultMap = new AnalysisResult.ResultMap(); + var builder = ImmutableDictionary.CreateBuilder(); foreach (var analyzer in analyzers) { + var result = new Builder(project, version); + // REVIEW: more unnecessary allocations just to get diagnostics per analyzer var oneAnalyzers = ImmutableArray.Create(analyzer); @@ -39,49 +41,80 @@ public static async Task AnalyzeAsync( { var model = compilation.GetSemanticModel(tree); - resultMap.AddSyntaxDiagnostics(analyzer, tree, await analyzerDriver.GetAnalyzerSyntaxDiagnosticsAsync(tree, oneAnalyzers, cancellationToken).ConfigureAwait(false)); - resultMap.AddSemanticDiagnostics(analyzer, tree, await analyzerDriver.GetAnalyzerSemanticDiagnosticsAsync(model, noSpanFilter, oneAnalyzers, cancellationToken).ConfigureAwait(false)); + var syntax = await analyzerDriver.GetAnalyzerSyntaxDiagnosticsAsync(tree, oneAnalyzers, cancellationToken).ConfigureAwait(false); + result.AddSyntaxDiagnostics(tree, CompilationWithAnalyzers.GetEffectiveDiagnostics(syntax, compilation)); + + var semantic = await analyzerDriver.GetAnalyzerSemanticDiagnosticsAsync(model, noSpanFilter, oneAnalyzers, cancellationToken).ConfigureAwait(false); + result.AddSemanticDiagnostics(tree, CompilationWithAnalyzers.GetEffectiveDiagnostics(semantic, compilation)); } - resultMap.AddCompilationDiagnostics(analyzer, await analyzerDriver.GetAnalyzerCompilationDiagnosticsAsync(oneAnalyzers, cancellationToken).ConfigureAwait(false)); + var rest = await analyzerDriver.GetAnalyzerCompilationDiagnosticsAsync(oneAnalyzers, cancellationToken).ConfigureAwait(false); + result.AddCompilationDiagnostics(CompilationWithAnalyzers.GetEffectiveDiagnostics(rest, compilation)); + + builder.Add(analyzer, result.ToResult()); } - return new AnalysisResult(resultMap); + return builder.ToImmutable(); } - } - // REVIEW: this will probably go away once we have new API. - // now things run sequencially, so no thread-safety. - internal struct AnalysisResult - { - private readonly ResultMap resultMap; - - public AnalysisResult(ResultMap resultMap) + /// + /// We have this builder to avoid creating collections unnecessarily. + /// Expectation is that, most of time, most of analyzers doesn't have any diagnostics. so no need to actually create any objects. + /// + internal struct Builder { - this.resultMap = resultMap; - } + private readonly Project _project; + private readonly VersionStamp _version; - internal struct ResultMap - { - private Dictionary> _lazySyntaxLocals; - private Dictionary> _lazySemanticLocals; + private HashSet _lazySet; + + private Dictionary> _lazySyntaxLocals; + private Dictionary> _lazySemanticLocals; + private Dictionary> _lazyNonLocals; + + private List _lazyOthers; + + public Builder(Project project, VersionStamp version) + { + _project = project; + _version = version; + + _lazySet = null; + _lazySyntaxLocals = null; + _lazySemanticLocals = null; + _lazyNonLocals = null; + _lazyOthers = null; + } + + public AnalysisResult ToResult() + { + var documentIds = _lazySet == null ? ImmutableHashSet.Empty : _lazySet.ToImmutableHashSet(); + var syntaxLocals = Convert(_lazySyntaxLocals); + var semanticLocals = Convert(_lazySemanticLocals); + var nonLocals = Convert(_lazyNonLocals); + var others = _lazyOthers == null ? ImmutableArray.Empty : _lazyOthers.ToImmutableArray(); + + return new AnalysisResult(_project.Id, _version, documentIds, syntaxLocals, semanticLocals, nonLocals, others); + } - private Dictionary> _lazyNonLocals; - private List _lazyOthers; + private ImmutableDictionary> Convert(Dictionary> map) + { + return map == null ? ImmutableDictionary>.Empty : map.ToImmutableDictionary(kv => kv.Key, kv => kv.Value.ToImmutableArray()); + } - public void AddSyntaxDiagnostics(DiagnosticAnalyzer analyzer, SyntaxTree tree, ImmutableArray diagnostics) + public void AddSyntaxDiagnostics(SyntaxTree tree, IEnumerable diagnostics) { AddDiagnostics(ref _lazySyntaxLocals, tree, diagnostics); } - public void AddSemanticDiagnostics(DiagnosticAnalyzer analyzer, SyntaxTree tree, ImmutableArray diagnostics) + public void AddSemanticDiagnostics(SyntaxTree tree, IEnumerable diagnostics) { AddDiagnostics(ref _lazySemanticLocals, tree, diagnostics); } - public void AddCompilationDiagnostics(DiagnosticAnalyzer analyzer, ImmutableArray diagnostics) + public void AddCompilationDiagnostics(IEnumerable diagnostics) { - Dictionary> dummy = null; + Dictionary> dummy = null; AddDiagnostics(ref dummy, tree: null, diagnostics: diagnostics); // dummy should be always null @@ -89,47 +122,55 @@ public void AddCompilationDiagnostics(DiagnosticAnalyzer analyzer, ImmutableArra } private void AddDiagnostics( - ref Dictionary> _lazyLocals, SyntaxTree tree, ImmutableArray diagnostics) + ref Dictionary> lazyLocals, SyntaxTree tree, IEnumerable diagnostics) { - if (diagnostics.Length == 0) - { - return; - } - - for (var i = 0; i < diagnostics.Length; i++) + foreach (var diagnostic in diagnostics) { - var diagnostic = diagnostics[i]; - // REVIEW: what is our plan for additional locations? switch (diagnostic.Location.Kind) { - case LocationKind.None: case LocationKind.ExternalFile: { - // no location or reported to external files - _lazyOthers = _lazyOthers ?? new List(); - _lazyOthers.Add(diagnostic); + // TODO: currently additional file location is not supported. + break; + } + case LocationKind.None: + { + _lazyOthers = _lazyOthers ?? new List(); + _lazyOthers.Add(DiagnosticData.Create(_project, diagnostic)); break; } case LocationKind.SourceFile: { if (tree != null && diagnostic.Location.SourceTree == tree) { - // local diagnostics to a file - _lazyLocals = _lazyLocals ?? new Dictionary>(); - _lazyLocals.GetOrAdd(diagnostic.Location.SourceTree, _ => new List()).Add(diagnostic); + var document = GetDocument(diagnostic); + if (document != null) + { + // local diagnostics to a file + lazyLocals = lazyLocals ?? new Dictionary>(); + lazyLocals.GetOrAdd(document.Id, _ => new List()).Add(DiagnosticData.Create(document, diagnostic)); + + SetDocument(document); + } } else if (diagnostic.Location.SourceTree != null) { - // non local diagnostics to a file - _lazyNonLocals = _lazyNonLocals ?? new Dictionary>(); - _lazyNonLocals.GetOrAdd(diagnostic.Location.SourceTree, _ => new List()).Add(diagnostic); + var document = _project.GetDocument(diagnostic.Location.SourceTree); + if (document != null) + { + // non local diagnostics to a file + _lazyNonLocals = _lazyNonLocals ?? new Dictionary>(); + _lazyNonLocals.GetOrAdd(document.Id, _ => new List()).Add(DiagnosticData.Create(document, diagnostic)); + + SetDocument(document); + } } else { // non local diagnostics without location - _lazyOthers = _lazyOthers ?? new List(); - _lazyOthers.Add(diagnostic); + _lazyOthers = _lazyOthers ?? new List(); + _lazyOthers.Add(DiagnosticData.Create(_project, diagnostic)); } break; @@ -148,6 +189,17 @@ private void AddDiagnostics( } } } + + private void SetDocument(Document document) + { + _lazySet = _lazySet ?? new HashSet(); + _lazySet.Add(document.Id); + } + + private Document GetDocument(Diagnostic diagnostic) + { + return _project.GetDocument(diagnostic.Location.SourceTree); + } } } } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticDataSerializer.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticDataSerializer.cs new file mode 100644 index 0000000000000..f78c1160c9852 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticDataSerializer.cs @@ -0,0 +1,392 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + /// + /// DiagnosticData serializer + /// + internal struct DiagnosticDataSerializer + { + // version of serialized format + private const int FormatVersion = 1; + + // version of analyzer that produced this data + public readonly VersionStamp AnalyzerVersion; + + // version of project this data belong to + public readonly VersionStamp Version; + + public DiagnosticDataSerializer(VersionStamp analyzerVersion, VersionStamp version) + { + AnalyzerVersion = analyzerVersion; + Version = version; + } + + public async Task SerializeAsync(object documentOrProject, string key, ImmutableArray items, CancellationToken cancellationToken) + { + using (var stream = SerializableBytes.CreateWritableStream()) + { + WriteTo(stream, items, cancellationToken); + + var solution = GetSolution(documentOrProject); + var persistService = solution.Workspace.Services.GetService(); + + using (var storage = persistService.GetStorage(solution)) + { + stream.Position = 0; + return await WriteStreamAsync(storage, documentOrProject, key, stream, cancellationToken).ConfigureAwait(false); + } + } + } + + public async Task> DeserializeAsync(object documentOrProject, string key, CancellationToken cancellationToken) + { + // we have persisted data + var solution = GetSolution(documentOrProject); + var persistService = solution.Workspace.Services.GetService(); + + using (var storage = persistService.GetStorage(solution)) + using (var stream = await ReadStreamAsync(storage, key, documentOrProject, cancellationToken).ConfigureAwait(false)) + { + if (stream == null) + { + return default(ImmutableArray); + } + + return ReadFrom(stream, documentOrProject, cancellationToken); + } + } + + private Task WriteStreamAsync(IPersistentStorage storage, object documentOrProject, string key, Stream stream, CancellationToken cancellationToken) + { + var document = documentOrProject as Document; + if (document != null) + { + return storage.WriteStreamAsync(document, key, stream, cancellationToken); + } + + var project = (Project)documentOrProject; + return storage.WriteStreamAsync(project, key, stream, cancellationToken); + } + + private void WriteTo(Stream stream, ImmutableArray items, CancellationToken cancellationToken) + { + using (var writer = new ObjectWriter(stream, cancellationToken: cancellationToken)) + { + writer.WriteInt32(FormatVersion); + + AnalyzerVersion.WriteTo(writer); + Version.WriteTo(writer); + + writer.WriteInt32(items.Length); + + foreach (var item in items) + { + cancellationToken.ThrowIfCancellationRequested(); + + writer.WriteString(item.Id); + writer.WriteString(item.Category); + + writer.WriteString(item.Message); + writer.WriteString(item.ENUMessageForBingSearch); + writer.WriteString(item.Title); + writer.WriteString(item.Description); + writer.WriteString(item.HelpLink); + writer.WriteInt32((int)item.Severity); + writer.WriteInt32((int)item.DefaultSeverity); + writer.WriteBoolean(item.IsEnabledByDefault); + writer.WriteBoolean(item.IsSuppressed); + writer.WriteInt32(item.WarningLevel); + + if (item.HasTextSpan) + { + // document state + writer.WriteInt32(item.TextSpan.Start); + writer.WriteInt32(item.TextSpan.Length); + } + else + { + // project state + writer.WriteInt32(0); + writer.WriteInt32(0); + } + + WriteTo(writer, item.DataLocation, cancellationToken); + WriteTo(writer, item.AdditionalLocations, cancellationToken); + + writer.WriteInt32(item.CustomTags.Count); + foreach (var tag in item.CustomTags) + { + writer.WriteString(tag); + } + + writer.WriteInt32(item.Properties.Count); + foreach (var property in item.Properties) + { + writer.WriteString(property.Key); + writer.WriteString(property.Value); + } + } + } + } + + private static void WriteTo(ObjectWriter writer, IReadOnlyCollection additionalLocations, CancellationToken cancellationToken) + { + writer.WriteInt32(additionalLocations?.Count ?? 0); + if (additionalLocations != null) + { + foreach (var location in additionalLocations) + { + cancellationToken.ThrowIfCancellationRequested(); + WriteTo(writer, location, cancellationToken); + } + } + } + + private static void WriteTo(ObjectWriter writer, DiagnosticDataLocation item, CancellationToken cancellationToken) + { + if (item == null) + { + writer.WriteBoolean(false); + return; + } + else + { + writer.WriteBoolean(true); + } + + if (item.SourceSpan.HasValue) + { + writer.WriteBoolean(true); + writer.WriteInt32(item.SourceSpan.Value.Start); + writer.WriteInt32(item.SourceSpan.Value.Length); + } + else + { + writer.WriteBoolean(false); + } + + writer.WriteString(item.OriginalFilePath); + writer.WriteInt32(item.OriginalStartLine); + writer.WriteInt32(item.OriginalStartColumn); + writer.WriteInt32(item.OriginalEndLine); + writer.WriteInt32(item.OriginalEndColumn); + + writer.WriteString(item.MappedFilePath); + writer.WriteInt32(item.MappedStartLine); + writer.WriteInt32(item.MappedStartColumn); + writer.WriteInt32(item.MappedEndLine); + writer.WriteInt32(item.MappedEndColumn); + } + + private Task ReadStreamAsync(IPersistentStorage storage, string key, object documentOrProject, CancellationToken cancellationToken) + { + var document = documentOrProject as Document; + if (document != null) + { + return storage.ReadStreamAsync(document, key, cancellationToken); + } + + var project = (Project)documentOrProject; + return storage.ReadStreamAsync(project, key, cancellationToken); + } + + private ImmutableArray ReadFrom(Stream stream, object documentOrProject, CancellationToken cancellationToken) + { + var document = documentOrProject as Document; + if (document != null) + { + return ReadFrom(stream, document.Project, document, cancellationToken); + } + + var project = (Project)documentOrProject; + return ReadFrom(stream, project, null, cancellationToken); + } + + private ImmutableArray ReadFrom(Stream stream, Project project, Document document, CancellationToken cancellationToken) + { + try + { + using (var pooledObject = SharedPools.Default>().GetPooledObject()) + using (var reader = new ObjectReader(stream)) + { + var list = pooledObject.Object; + + var format = reader.ReadInt32(); + if (format != FormatVersion) + { + return default(ImmutableArray); + } + + // saved data is for same analyzer of different version of dll + var analyzerVersion = VersionStamp.ReadFrom(reader); + if (analyzerVersion != AnalyzerVersion) + { + return default(ImmutableArray); + } + + var version = VersionStamp.ReadFrom(reader); + if (version != VersionStamp.Default && version != Version) + { + return default(ImmutableArray); + } + + ReadFrom(reader, project, document, list, cancellationToken); + return list.ToImmutableArray(); + } + } + catch (Exception) + { + return default(ImmutableArray); + } + } + + private static void ReadFrom(ObjectReader reader, Project project, Document document, List list, CancellationToken cancellationToken) + { + var count = reader.ReadInt32(); + + for (var i = 0; i < count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var id = reader.ReadString(); + var category = reader.ReadString(); + + var message = reader.ReadString(); + var messageFormat = reader.ReadString(); + var title = reader.ReadString(); + var description = reader.ReadString(); + var helpLink = reader.ReadString(); + var severity = (DiagnosticSeverity)reader.ReadInt32(); + var defaultSeverity = (DiagnosticSeverity)reader.ReadInt32(); + var isEnabledByDefault = reader.ReadBoolean(); + var isSuppressed = reader.ReadBoolean(); + var warningLevel = reader.ReadInt32(); + + var start = reader.ReadInt32(); + var length = reader.ReadInt32(); + var textSpan = new TextSpan(start, length); + + var location = ReadLocation(project, reader, document); + var additionalLocations = ReadAdditionalLocations(project, reader); + + var customTagsCount = reader.ReadInt32(); + var customTags = GetCustomTags(reader, customTagsCount); + + var propertiesCount = reader.ReadInt32(); + var properties = GetProperties(reader, propertiesCount); + + list.Add(new DiagnosticData( + id, category, message, messageFormat, severity, defaultSeverity, isEnabledByDefault, warningLevel, customTags, properties, + project.Solution.Workspace, project.Id, location, additionalLocations, + title: title, + description: description, + helpLink: helpLink, + isSuppressed: isSuppressed)); + } + } + + private static DiagnosticDataLocation ReadLocation(Project project, ObjectReader reader, Document documentOpt) + { + var exists = reader.ReadBoolean(); + if (!exists) + { + return null; + } + + TextSpan? sourceSpan = null; + if (reader.ReadBoolean()) + { + sourceSpan = new TextSpan(reader.ReadInt32(), reader.ReadInt32()); + } + + var originalFile = reader.ReadString(); + var originalStartLine = reader.ReadInt32(); + var originalStartColumn = reader.ReadInt32(); + var originalEndLine = reader.ReadInt32(); + var originalEndColumn = reader.ReadInt32(); + + var mappedFile = reader.ReadString(); + var mappedStartLine = reader.ReadInt32(); + var mappedStartColumn = reader.ReadInt32(); + var mappedEndLine = reader.ReadInt32(); + var mappedEndColumn = reader.ReadInt32(); + + var documentId = documentOpt != null + ? documentOpt.Id + : project.Documents.FirstOrDefault(d => d.FilePath == originalFile)?.Id; + + return new DiagnosticDataLocation(documentId, sourceSpan, + originalFile, originalStartLine, originalStartColumn, originalEndLine, originalEndColumn, + mappedFile, mappedStartLine, mappedStartColumn, mappedEndLine, mappedEndColumn); + } + + private static IReadOnlyCollection ReadAdditionalLocations(Project project, ObjectReader reader) + { + var count = reader.ReadInt32(); + var result = new List(); + for (var i = 0; i < count; i++) + { + result.Add(ReadLocation(project, reader, documentOpt: null)); + } + + return result; + } + + private static ImmutableDictionary GetProperties(ObjectReader reader, int count) + { + if (count > 0) + { + var properties = ImmutableDictionary.CreateBuilder(); + for (var i = 0; i < count; i++) + { + properties.Add(reader.ReadString(), reader.ReadString()); + } + + return properties.ToImmutable(); + } + + return ImmutableDictionary.Empty; + } + + private static IReadOnlyList GetCustomTags(ObjectReader reader, int count) + { + if (count > 0) + { + var tags = new List(count); + for (var i = 0; i < count; i++) + { + tags.Add(reader.ReadString()); + } + + return new ReadOnlyCollection(tags); + } + + return SpecializedCollections.EmptyReadOnlyList(); + } + + private static Solution GetSolution(object documentOrProject) + { + var document = documentOrProject as Document; + if (document != null) + { + return document.Project.Solution; + } + + var project = (Project)documentOrProject; + return project.Solution; + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ActiveFileState.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ActiveFileState.cs index c8ef7646ab638..d20fdceba5324 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ActiveFileState.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ActiveFileState.cs @@ -1,23 +1,65 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 { - internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer + internal partial class DiagnosticIncrementalAnalyzer { - // TODO: implement active file state - // this should hold onto local syntax/semantic diagnostics for active file in memory. - // this should also hold onto CompilationWithAnalyzer last time used. - // this should use syntax/semantic version for its version + /// + /// state that is responsible to hold onto local diagnostics data regarding active/opened files (depends on host) + /// in memory. + /// private class ActiveFileState { + // file state this is for + public readonly DocumentId DocumentId; + // analysis data for each kind + private DocumentAnalysisData _syntax = DocumentAnalysisData.Empty; + private DocumentAnalysisData _semantic = DocumentAnalysisData.Empty; + + public ActiveFileState(DocumentId documentId) + { + DocumentId = documentId; + } + + public bool IsEmpty => _syntax.Items.IsEmpty && _semantic.Items.IsEmpty; + + public DocumentAnalysisData GetAnalysisData(AnalysisKind kind) + { + switch (kind) + { + case AnalysisKind.Syntax: + return _syntax; + + case AnalysisKind.Semantic: + return _semantic; + + default: + return Contract.FailWithReturn("Shouldn't reach here"); + } + } + + public void Save(AnalysisKind kind, DocumentAnalysisData data) + { + Contract.ThrowIfFalse(data.OldItems.IsDefault); + + switch (kind) + { + case AnalysisKind.Syntax: + _syntax = data; + return; + + case AnalysisKind.Semantic: + _semantic = data; + return; + + default: + Contract.Fail("Shouldn't reach here"); + return; + } + } } } } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.AnalysisData.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.AnalysisData.cs new file mode 100644 index 0000000000000..db144799e240f --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.AnalysisData.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + /// + /// + /// + private struct DocumentAnalysisData + { + public static readonly DocumentAnalysisData Empty = new DocumentAnalysisData(VersionStamp.Default, ImmutableArray.Empty); + + public readonly VersionStamp Version; + public readonly ImmutableArray OldItems; + public readonly ImmutableArray Items; + + public DocumentAnalysisData(VersionStamp version, ImmutableArray items) + { + this.Version = version; + this.Items = items; + } + + public DocumentAnalysisData(VersionStamp version, ImmutableArray oldItems, ImmutableArray newItems) : + this(version, newItems) + { + this.OldItems = oldItems; + } + + public DocumentAnalysisData ToPersistData() + { + return new DocumentAnalysisData(Version, Items); + } + + public bool FromCache + { + get { return this.OldItems.IsDefault; } + } + } + + private struct ProjectAnalysisData + { + public static readonly ProjectAnalysisData Empty = new ProjectAnalysisData( + VersionStamp.Default, ImmutableDictionary.Empty, ImmutableDictionary.Empty); + + public readonly VersionStamp Version; + public readonly ImmutableDictionary OldResult; + public readonly ImmutableDictionary Result; + + + public ProjectAnalysisData(VersionStamp version, ImmutableDictionary result) + { + this.Version = version; + this.Result = result; + + this.OldResult = null; + } + + public ProjectAnalysisData( + VersionStamp version, + ImmutableDictionary oldResult, + ImmutableDictionary newResult) : + this(version, newResult) + { + this.OldResult = oldResult; + } + + public AnalysisResult GetResult(DiagnosticAnalyzer analyzer) + { + return IDictionaryExtensions.GetValueOrDefault(Result, analyzer); + } + + public bool FromCache + { + get { return this.OldResult == null; } + } + + public static async Task CreateAsync(Project project, IEnumerable stateSets, bool avoidLoadingData, CancellationToken cancellationToken) + { + VersionStamp? version = null; + + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var stateSet in stateSets) + { + var state = stateSet.GetProjectState(project.Id); + var result = await state.GetAnalysisDataAsync(project, avoidLoadingData, cancellationToken).ConfigureAwait(false); + + if (!version.HasValue) + { + version = result.Version; + } + else + { + // all version must be same. + Contract.ThrowIfFalse(version == result.Version); + } + + builder.Add(stateSet.Analyzer, result); + } + + if (!version.HasValue) + { + // there is no saved data to return. + return ProjectAnalysisData.Empty; + } + + return new ProjectAnalysisData(version.Value, builder.ToImmutable()); + } + } + } +} \ No newline at end of file diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.AnalysisKind.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.AnalysisKind.cs new file mode 100644 index 0000000000000..1a90fb58d5169 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.AnalysisKind.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + /// + /// enum for each analysis kind. + /// + private enum AnalysisKind + { + Syntax, + Semantic, + NonLocal + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs new file mode 100644 index 0000000000000..254ed34f2fefd --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ErrorReporting; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + /// + /// This cache CompilationWithAnalyzer for active/open files. + /// This will aggressively let go cached compilationWithAnalyzers to not hold them into memory too long. + /// + private class CompilationManager + { + private readonly DiagnosticIncrementalAnalyzer _owner; + private ConditionalWeakTable _map; + + public CompilationManager(DiagnosticIncrementalAnalyzer owner) + { + _owner = owner; + _map = new ConditionalWeakTable(); + } + + /// + /// Return CompilationWithAnalyzer for given project with given stateSets + /// + public async Task GetAnalyzerDriverAsync(Project project, IEnumerable stateSets, CancellationToken cancellationToken) + { + Contract.ThrowIfFalse(project.SupportsCompilation); + + CompilationWithAnalyzers analyzerDriver; + if (_map.TryGetValue(project, out analyzerDriver)) + { + // we have cached one, return that. + AssertAnalyzers(analyzerDriver, stateSets); + return analyzerDriver; + } + + // Create driver that holds onto compilation and associated analyzers + var concurrentAnalysis = false; + var includeSuppressedDiagnostics = true; + var newAnalyzerDriver = await CreateAnalyzerDriverAsync(project, stateSets, concurrentAnalysis, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + + // Add new analyzer driver to the map + analyzerDriver = _map.GetValue(project, _ => newAnalyzerDriver); + + // if somebody has beat us, make sure analyzers are good. + if (analyzerDriver != newAnalyzerDriver) + { + AssertAnalyzers(analyzerDriver, stateSets); + } + + // return driver + return analyzerDriver; + } + + public Task CreateAnalyzerDriverAsync( + Project project, IEnumerable stateSets, bool concurrentAnalysis, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) + { + var analyzers = stateSets.Select(s => s.Analyzer).ToImmutableArrayOrEmpty(); + return CreateAnalyzerDriverAsync(project, analyzers, concurrentAnalysis, includeSuppressedDiagnostics, cancellationToken); + } + + public Task CreateAnalyzerDriverAsync( + Project project, ImmutableArray analyzers, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) + { + var concurrentAnalysis = false; + return CreateAnalyzerDriverAsync(project, analyzers, concurrentAnalysis, includeSuppressedDiagnostics, cancellationToken); + } + + public async Task CreateAnalyzerDriverAsync( + Project project, ImmutableArray analyzers, bool concurrentAnalysis, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) + { + Contract.ThrowIfFalse(project.SupportsCompilation); + + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + + // Create driver that holds onto compilation and associated analyzers + return CreateAnalyzerDriver( + project, compilation, analyzers, concurrentAnalysis: concurrentAnalysis, logAnalyzerExecutionTime: false, reportSuppressedDiagnostics: includeSuppressedDiagnostics); + } + + public CompilationWithAnalyzers CreateAnalyzerDriver( + Project project, + Compilation compilation, + ImmutableArray analyzers, + bool concurrentAnalysis, + bool logAnalyzerExecutionTime, + bool reportSuppressedDiagnostics) + { + Contract.ThrowIfFalse(project.SupportsCompilation); + AssertCompilation(project, compilation); + + var analysisOptions = GetAnalyzerOptions(project, concurrentAnalysis, logAnalyzerExecutionTime, reportSuppressedDiagnostics); + + // Create driver that holds onto compilation and associated analyzers + return compilation.WithAnalyzers(analyzers, analysisOptions); + } + + private CompilationWithAnalyzersOptions GetAnalyzerOptions( + Project project, + bool concurrentAnalysis, + bool logAnalyzerExecutionTime, + bool reportSuppressedDiagnostics) + { + return new CompilationWithAnalyzersOptions( + options: new WorkspaceAnalyzerOptions(project.AnalyzerOptions, project.Solution.Workspace), + onAnalyzerException: GetOnAnalyzerException(project.Id), + analyzerExceptionFilter: GetAnalyzerExceptionFilter(project), + concurrentAnalysis: concurrentAnalysis, + logAnalyzerExecutionTime: logAnalyzerExecutionTime, + reportSuppressedDiagnostics: reportSuppressedDiagnostics); + } + + private Func GetAnalyzerExceptionFilter(Project project) + { + return ex => + { + if (project.Solution.Workspace.Options.GetOption(InternalDiagnosticsOptions.CrashOnAnalyzerException)) + { + // if option is on, crash the host to get crash dump. + FatalError.ReportUnlessCanceled(ex); + } + + return true; + }; + } + + private Action GetOnAnalyzerException(ProjectId projectId) + { + return _owner.Owner.GetOnAnalyzerException(projectId, _owner.DiagnosticLogAggregator); + } + + private void ResetAnalyzerDriverMap() + { + // we basically eagarly clear the cache on some known changes + // to let CompilationWithAnalyzer go. + + // we create new conditional weak table every time, it turns out + // only way to clear ConditionalWeakTable is re-creating it. + // also, conditional weak table has a leak - https://github.com/dotnet/coreclr/issues/665 + _map = new ConditionalWeakTable(); + } + + [Conditional("DEBUG")] + private void AssertAnalyzers(CompilationWithAnalyzers analyzerDriver, IEnumerable stateSets) + { + // make sure analyzers are same. + Contract.ThrowIfFalse(analyzerDriver.Analyzers.SetEquals(stateSets.Select(s => s.Analyzer))); + } + + [Conditional("DEBUG")] + private void AssertCompilation(Project project, Compilation compilation1) + { + // given compilation must be from given project. + Compilation compilation2; + Contract.ThrowIfFalse(project.TryGetCompilation(out compilation2)); + Contract.ThrowIfFalse(compilation1 == compilation2); + } + + #region state changed + public void OnActiveDocumentChanged() + { + ResetAnalyzerDriverMap(); + } + + public void OnDocumentOpened() + { + ResetAnalyzerDriverMap(); + } + + public void OnDocumentClosed() + { + ResetAnalyzerDriverMap(); + } + + public void OnDocumentReset() + { + ResetAnalyzerDriverMap(); + } + + public void OnDocumentRemoved() + { + ResetAnalyzerDriverMap(); + } + + public void OnProjectRemoved() + { + ResetAnalyzerDriverMap(); + } + + public void OnNewSolution() + { + ResetAnalyzerDriverMap(); + } + #endregion + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs new file mode 100644 index 0000000000000..e67526e4765bc --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics.Log; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Shared.Options; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + /// + /// This is responsible for getting diagnostics for given input. + /// It either return one from cache or calculate new one. + /// + private class Executor + { + private readonly DiagnosticIncrementalAnalyzer _owner; + + public Executor(DiagnosticIncrementalAnalyzer owner) + { + _owner = owner; + } + + public IEnumerable ConvertToLocalDiagnostics(Document targetDocument, IEnumerable diagnostics, TextSpan? span = null) + { + var project = targetDocument.Project; + + foreach (var diagnostic in diagnostics) + { + var document = project.GetDocument(diagnostic.Location.SourceTree); + if (document == null || document != targetDocument) + { + continue; + } + + if (span.HasValue && !span.Value.Contains(diagnostic.Location.SourceSpan)) + { + continue; + } + + yield return DiagnosticData.Create(document, diagnostic); + } + } + + public async Task GetDocumentAnalysisDataAsync( + CompilationWithAnalyzers analyzerDriver, Document document, StateSet stateSet, AnalysisKind kind, CancellationToken cancellationToken) + { + try + { + var version = await GetDiagnosticVersionAsync(document.Project, cancellationToken).ConfigureAwait(false); + var state = stateSet.GetActiveFileState(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. + // REVIEW: IsAnalyzerSuppressed call seems can be quite expensive in certain condition. is there any other way to do this? + if (_owner.Owner.IsAnalyzerSuppressed(stateSet.Analyzer, document.Project)) + { + return new DocumentAnalysisData(version, existingData.Items, ImmutableArray.Empty); + } + + var nullFilterSpan = (TextSpan?)null; + var diagnostics = await ComputeDiagnosticsAsync(analyzerDriver, document, stateSet.Analyzer, kind, nullFilterSpan, cancellationToken).ConfigureAwait(false); + + // we only care about local diagnostics + return new DocumentAnalysisData(version, existingData.Items, diagnostics.ToImmutableArrayOrEmpty()); + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + + public async Task GetProjectAnalysisDataAsync(CompilationWithAnalyzers analyzerDriver, Project project, IEnumerable stateSets, CancellationToken cancellationToken) + { + try + { + // PERF: we need to flip this to false when we do actual diffing. + var avoidLoadingData = true; + var version = await GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); + var existingData = await ProjectAnalysisData.CreateAsync(project, stateSets, avoidLoadingData, cancellationToken).ConfigureAwait(false); + + if (existingData.Version == version) + { + return existingData; + } + + // perf optimization. check whether we want to analyze this project or not. + if (!await FullAnalysisEnabledAsync(project, cancellationToken).ConfigureAwait(false)) + { + return new ProjectAnalysisData(version, existingData.Result, ImmutableDictionary.Empty); + } + + var result = await ComputeDiagnosticsAsync(analyzerDriver, project, stateSets, cancellationToken).ConfigureAwait(false); + + return new ProjectAnalysisData(version, existingData.Result, result); + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + + public async Task> ComputeDiagnosticsAsync( + CompilationWithAnalyzers analyzerDriver, Document document, DiagnosticAnalyzer analyzer, AnalysisKind kind, TextSpan? spanOpt, CancellationToken cancellationToken) + { + var documentAnalyzer = analyzer as DocumentDiagnosticAnalyzer; + if (documentAnalyzer != null) + { + var diagnostics = await ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync(document, documentAnalyzer, kind, analyzerDriver.Compilation, cancellationToken).ConfigureAwait(false); + return ConvertToLocalDiagnostics(document, diagnostics); + } + + var documentDiagnostics = await ComputeDiagnosticAnalyzerDiagnosticsAsync(analyzerDriver, document, analyzer, kind, spanOpt, cancellationToken).ConfigureAwait(false); + return ConvertToLocalDiagnostics(document, documentDiagnostics); + } + + public async Task> ComputeDiagnosticsAsync( + CompilationWithAnalyzers analyzerDriver, Project project, IEnumerable stateSets, CancellationToken cancellationToken) + { + // calculate regular diagnostic analyzers diagnostics + var result = await analyzerDriver.AnalyzeAsync(project, cancellationToken).ConfigureAwait(false); + + // record telemetry data + await UpdateAnalyzerTelemetryDataAsync(analyzerDriver, project, cancellationToken).ConfigureAwait(false); + + // check whether there is IDE specific project diagnostic analyzer + return await MergeProjectDiagnosticAnalyzerDiagnosticsAsync(project, stateSets, analyzerDriver.Compilation, result, cancellationToken).ConfigureAwait(false); + } + + private async Task> MergeProjectDiagnosticAnalyzerDiagnosticsAsync( + Project project, IEnumerable stateSets, Compilation compilation, ImmutableDictionary result, CancellationToken cancellationToken) + { + // check whether there is IDE specific project diagnostic analyzer + var projectAnalyzers = stateSets.Select(s => s.Analyzer).OfType().ToImmutableArrayOrEmpty(); + if (projectAnalyzers.Length <= 0) + { + return result; + } + + var version = await GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); + using (var diagnostics = SharedPools.Default>().GetPooledObject()) + { + foreach (var analyzer in projectAnalyzers) + { + // reset pooled list + diagnostics.Object.Clear(); + + try + { + await analyzer.AnalyzeProjectAsync(project, diagnostics.Object.Add, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (!IsCanceled(e, cancellationToken)) + { + OnAnalyzerException(analyzer, project.Id, compilation, e); + continue; + } + + // create result map + var builder = new CompilerDiagnosticExecutor.Builder(project, version); + builder.AddCompilationDiagnostics(diagnostics.Object); + + // merge the result to existing one. + result = result.Add(analyzer, builder.ToResult()); + } + } + + return result; + } + + private async Task> ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync( + Document document, DocumentDiagnosticAnalyzer analyzer, AnalysisKind kind, Compilation compilation, CancellationToken cancellationToken) + { + using (var pooledObject = SharedPools.Default>().GetPooledObject()) + { + var diagnostics = pooledObject.Object; + cancellationToken.ThrowIfCancellationRequested(); + + try + { + switch (kind) + { + case AnalysisKind.Syntax: + await analyzer.AnalyzeSyntaxAsync(document, diagnostics.Add, cancellationToken).ConfigureAwait(false); + return CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, compilation); + case AnalysisKind.Semantic: + await analyzer.AnalyzeSemanticsAsync(document, diagnostics.Add, cancellationToken).ConfigureAwait(false); + return CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, compilation); + default: + return Contract.FailWithReturn>("shouldn't reach here"); + } + } + catch (Exception e) when (!IsCanceled(e, cancellationToken)) + { + OnAnalyzerException(analyzer, document.Project.Id, compilation, e); + + return ImmutableArray.Empty; + } + } + } + + private async Task> ComputeDiagnosticAnalyzerDiagnosticsAsync( + CompilationWithAnalyzers analyzerDriver, Document document, DiagnosticAnalyzer analyzer, AnalysisKind kind, TextSpan? spanOpt, CancellationToken cancellationToken) + { + // quick optimization to reduce allocations. + if (!_owner.SupportAnalysisKind(analyzer, document.Project.Language, kind)) + { + return ImmutableArray.Empty; + } + + // REVIEW: more unnecessary allocations just to get diagnostics per analyzer + var oneAnalyzers = ImmutableArray.Create(analyzer); + + switch (kind) + { + case AnalysisKind.Syntax: + var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var diagnostics = await analyzerDriver.GetAnalyzerSyntaxDiagnosticsAsync(tree, oneAnalyzers, cancellationToken).ConfigureAwait(false); + return CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, analyzerDriver.Compilation); + case AnalysisKind.Semantic: + var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + diagnostics = await analyzerDriver.GetAnalyzerSemanticDiagnosticsAsync(model, spanOpt, oneAnalyzers, cancellationToken).ConfigureAwait(false); + return CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, analyzerDriver.Compilation); + default: + return Contract.FailWithReturn>("shouldn't reach here"); + } + } + + private async Task UpdateAnalyzerTelemetryDataAsync(CompilationWithAnalyzers analyzerDriver, Project project, CancellationToken cancellationToken) + { + foreach (var analyzer in analyzerDriver.Analyzers) + { + await UpdateAnalyzerTelemetryDataAsync(analyzerDriver, analyzer, project, cancellationToken).ConfigureAwait(false); + } + } + + private async Task UpdateAnalyzerTelemetryDataAsync(CompilationWithAnalyzers analyzerDriver, DiagnosticAnalyzer analyzer, Project project, CancellationToken cancellationToken) + { + try + { + var analyzerTelemetryInfo = await analyzerDriver.GetAnalyzerTelemetryInfoAsync(analyzer, cancellationToken).ConfigureAwait(false); + DiagnosticAnalyzerLogger.UpdateAnalyzerTypeCount(analyzer, analyzerTelemetryInfo, project, _owner.DiagnosticLogAggregator); + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + + private static async Task FullAnalysisEnabledAsync(Project project, CancellationToken cancellationToken) + { + var workspace = project.Solution.Workspace; + var language = project.Language; + + if (!workspace.Options.GetOption(ServiceFeatureOnOffOptions.ClosedFileDiagnostic, language) || + !workspace.Options.GetOption(RuntimeOptions.FullSolutionAnalysis)) + { + return false; + } + + return await project.HasSuccessfullyLoadedAsync(cancellationToken).ConfigureAwait(false); + } + + private static bool IsCanceled(Exception ex, CancellationToken cancellationToken) + { + return (ex as OperationCanceledException)?.CancellationToken == cancellationToken; + } + + private void OnAnalyzerException(DiagnosticAnalyzer analyzer, ProjectId projectId, Compilation compilation, Exception ex) + { + var exceptionDiagnostic = AnalyzerHelper.CreateAnalyzerExceptionDiagnostic(analyzer, ex); + + if (compilation != null) + { + exceptionDiagnostic = CompilationWithAnalyzers.GetEffectiveDiagnostics(ImmutableArray.Create(exceptionDiagnostic), compilation).SingleOrDefault(); + } + + var onAnalyzerException = _owner.GetOnAnalyzerException(projectId); + onAnalyzerException(ex, analyzer, exceptionDiagnostic); + } + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InMemoryStorage.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InMemoryStorage.cs new file mode 100644 index 0000000000000..21e3ccd9edddc --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InMemoryStorage.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Concurrent; +using System.Collections.Immutable; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + private static class InMemoryStorage + { + // the reason using nested map rather than having tuple as key is so that I dont have a gigantic map + private readonly static ConcurrentDictionary> s_map = + new ConcurrentDictionary>(concurrencyLevel: 2, capacity: 10); + + public static bool TryGetValue(DiagnosticAnalyzer analyzer, object key, out CacheEntry entry) + { + AssertKey(key); + + entry = default(CacheEntry); + + ConcurrentDictionary analyzerMap; + if (!s_map.TryGetValue(analyzer, out analyzerMap) || + !analyzerMap.TryGetValue(key, out entry)) + { + return false; + } + + return true; + } + + public static void Cache(DiagnosticAnalyzer analyzer, object key, CacheEntry entry) + { + AssertKey(key); + + // add new cache entry + var analyzerMap = s_map.GetOrAdd(analyzer, _ => new ConcurrentDictionary(concurrencyLevel: 2, capacity: 10)); + analyzerMap[key] = entry; + } + + public static void Remove(DiagnosticAnalyzer analyzer, object key) + { + AssertKey(key); + + // remove the entry + ConcurrentDictionary analyzerMap; + if (!s_map.TryGetValue(analyzer, out analyzerMap)) + { + return; + } + + CacheEntry entry; + analyzerMap.TryRemove(key, out entry); + + if (analyzerMap.IsEmpty) + { + s_map.TryRemove(analyzer, out analyzerMap); + } + } + + public static void DropCache(DiagnosticAnalyzer analyzer) + { + // drop any cache related to given analyzer + ConcurrentDictionary analyzerMap; + s_map.TryRemove(analyzer, out analyzerMap); + } + + // make sure key is either documentId or projectId + private static void AssertKey(object key) + { + Contract.ThrowIfFalse(key is DocumentId || key is ProjectId); + } + } + + // in memory cache entry + private struct CacheEntry + { + public readonly VersionStamp Version; + public readonly ImmutableArray Diagnostics; + + public CacheEntry(VersionStamp version, ImmutableArray diagnostics) + { + Version = version; + Diagnostics = diagnostics; + } + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectAnalyzerReferenceChangedEventArgs.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectAnalyzerReferenceChangedEventArgs.cs new file mode 100644 index 0000000000000..f2fb4c375ed14 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectAnalyzerReferenceChangedEventArgs.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + /// + /// EventArgs for + /// + /// this event args contains information such as the has changed + /// and what has changed. + /// + private class ProjectAnalyzerReferenceChangedEventArgs : EventArgs + { + public readonly Project Project; + public readonly ImmutableArray Added; + public readonly ImmutableArray Removed; + + public ProjectAnalyzerReferenceChangedEventArgs(Project project, ImmutableArray added, ImmutableArray removed) + { + Project = project; + Added = added; + Removed = removed; + } + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectState.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectState.cs index f98078b30d726..0c958928ccd42 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectState.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectState.cs @@ -1,26 +1,289 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Collections.Generic; +using System; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 { - internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer + internal partial class DiagnosticIncrementalAnalyzer { - // TODO: implement project state - // this should hold onto information similar to CompilerDiagnsoticExecutor.AnalysisResult - // this should use dependant project version as its version - // this should only cache opened file diagnostics in memory, and all diagnostics in other place. - // we might just use temporary storage rather than peristant storage. but will see. - // now we don't update individual document incrementally. - // but some data might comes from active file state. + /// + /// State for diagnostics that belong to a project at given time. + /// private class ProjectState { + // project id of this state + private readonly StateSet _owner; + // last aggregated analysis result for this project saved + private AnalysisResult _lastResult; + + public ProjectState(StateSet owner, ProjectId projectId) + { + _owner = owner; + _lastResult = new AnalysisResult(projectId, VersionStamp.Default, ImmutableHashSet.Empty, isEmpty: true); + } + + public ImmutableHashSet GetDocumentsWithDiagnostics() + { + return _lastResult.DocumentIds; + } + + public bool IsEmpty() + { + return _lastResult.IsEmpty; + } + + public bool IsEmpty(DocumentId documentId) + { + return !_lastResult.DocumentIds.Contains(documentId); + } + + public async Task GetAnalysisDataAsync(Project project, bool avoidLoadingData, CancellationToken cancellationToken) + { + // make a copy of last result. + var lastResult = _lastResult; + + var version = await GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); + var versionToLoad = GetVersionToLoad(lastResult.Version, version); + + // PERF: avoid loading data if version is not right one. + // avoid loading data flag is there as a strickly perf optimization. + if (avoidLoadingData && versionToLoad != version) + { + return lastResult; + } + + // loading data can be cancelled any time. + var serializer = new DiagnosticDataSerializer(_owner.AnalyzerVersion, versionToLoad); + var builder = new Builder(project.Id, versionToLoad, lastResult.DocumentIds); + + foreach (var documentId in lastResult.DocumentIds) + { + var document = project.GetDocument(documentId); + if (document == null) + { + return lastResult; + } + + if (!await TryDeserializeDocumentAsync(serializer, document, builder, cancellationToken).ConfigureAwait(false)) + { + return lastResult; + } + } + + if (!await TryDeserializeAsync(serializer, project, project.Id, _owner.NonLocalStateName, builder.AddOthers, cancellationToken).ConfigureAwait(false)) + { + return lastResult; + } + + return builder.ToResult(); + } + + public async Task GetAnalysisDataAsync(Document document, bool avoidLoadingData, CancellationToken cancellationToken) + { + // make a copy of last result. + var lastResult = _lastResult; + + var version = await GetDiagnosticVersionAsync(document.Project, cancellationToken).ConfigureAwait(false); + var versionToLoad = GetVersionToLoad(lastResult.Version, version); + + if (avoidLoadingData && versionToLoad != version) + { + return lastResult; + } + + // loading data can be cancelled any time. + var serializer = new DiagnosticDataSerializer(_owner.AnalyzerVersion, versionToLoad); + var builder = new Builder(document.Project.Id, versionToLoad, lastResult.DocumentIds); + + if (!await TryDeserializeDocumentAsync(serializer, document, builder, cancellationToken).ConfigureAwait(false)) + { + return lastResult; + } + + return builder.ToResult(); + } + + public async Task GetProjectAnalysisDataAsync(Project project, bool avoidLoadingData, CancellationToken cancellationToken) + { + // make a copy of last result. + var lastResult = _lastResult; + + var version = await GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); + var versionToLoad = GetVersionToLoad(lastResult.Version, version); + + if (avoidLoadingData && versionToLoad != version) + { + return lastResult; + } + + // loading data can be cancelled any time. + var serializer = new DiagnosticDataSerializer(_owner.AnalyzerVersion, versionToLoad); + var builder = new Builder(project.Id, versionToLoad, lastResult.DocumentIds); + + if (!await TryDeserializeAsync(serializer, project, project.Id, _owner.NonLocalStateName, builder.AddOthers, cancellationToken).ConfigureAwait(false)) + { + return lastResult; + } + + return builder.ToResult(); + } + + public async Task SaveAsync(Project project, AnalysisResult result) + { + // save last aggregated form of analysis result + _lastResult = result.ToAggregatedForm(); + + // serialization can't be cancelled. + var serializer = new DiagnosticDataSerializer(_owner.AnalyzerVersion, result.Version); + foreach (var documentId in result.DocumentIds) + { + var document = project.GetDocument(documentId); + Contract.ThrowIfNull(document); + + await SerializeAsync(serializer, document, document.Id, _owner.SyntaxStateName, GetResult(result, AnalysisKind.Syntax, document.Id)).ConfigureAwait(false); + await SerializeAsync(serializer, document, document.Id, _owner.SemanticStateName, GetResult(result, AnalysisKind.Semantic, document.Id)).ConfigureAwait(false); + await SerializeAsync(serializer, document, document.Id, _owner.NonLocalStateName, GetResult(result, AnalysisKind.NonLocal, document.Id)).ConfigureAwait(false); + } + + await SerializeAsync(serializer, project, result.ProjectId, _owner.NonLocalStateName, result.Others).ConfigureAwait(false); + } + + public bool OnDocumentRemoved(DocumentId id) + { + RemoveInMemoryCacheEntry(id); + + return !IsEmpty(id); + } + + public bool OnProjectRemoved(ProjectId id) + { + RemoveInMemoryCacheEntry(id); + + return !IsEmpty(); + } + + private async Task SerializeAsync(DiagnosticDataSerializer serializer, object documentOrProject, object key, string stateKey, ImmutableArray diagnostics) + { + // try to serialize it + if (await serializer.SerializeAsync(documentOrProject, stateKey, diagnostics, CancellationToken.None).ConfigureAwait(false)) + { + // we succeeded saving it to persistent storage. remove it from in memory cache if it exists + RemoveInMemoryCacheEntry(key); + return; + } + + // if serialization fail, hold it in the memory + InMemoryStorage.Cache(_owner.Analyzer, key, new CacheEntry(serializer.Version, diagnostics)); + } + + private async Task TryDeserializeDocumentAsync(DiagnosticDataSerializer serializer, Document document, Builder builder, CancellationToken cancellationToken) + { + return await TryDeserializeAsync(serializer, document, document.Id, _owner.SyntaxStateName, builder.AddSyntaxLocals, cancellationToken).ConfigureAwait(false) && + await TryDeserializeAsync(serializer, document, document.Id, _owner.SemanticStateName, builder.AddSemanticLocals, cancellationToken).ConfigureAwait(false) && + await TryDeserializeAsync(serializer, document, document.Id, _owner.NonLocalStateName, builder.AddNonLocals, cancellationToken).ConfigureAwait(false); + } + + private async Task TryDeserializeAsync( + DiagnosticDataSerializer serializer, + object documentOrProject, T key, string stateKey, + Action> add, + CancellationToken cancellationToken) where T : class + { + var diagnostics = await DeserializeAsync(serializer, documentOrProject, key, stateKey, cancellationToken).ConfigureAwait(false); + if (diagnostics.IsDefault) + { + return false; + } + + add(key, diagnostics); + return true; + } + + private async Task> DeserializeAsync(DiagnosticDataSerializer serializer, object documentOrProject, object key, string stateKey, CancellationToken cancellationToken) + { + // check cache first + CacheEntry entry; + if (InMemoryStorage.TryGetValue(_owner.Analyzer, key, out entry) && serializer.Version == entry.Version) + { + return entry.Diagnostics; + } + + // try to deserialize it + return await serializer.DeserializeAsync(documentOrProject, stateKey, cancellationToken).ConfigureAwait(false); + } + + private void RemoveInMemoryCacheEntry(object key) + { + // remove in memory cache if entry exist + InMemoryStorage.Remove(_owner.Analyzer, key); + } + + private static VersionStamp GetVersionToLoad(VersionStamp savedVersion, VersionStamp currentVersion) + { + // if we don't have saved version, use currrent version. + // this will let us deal with case where we want to load saved data from last vs session. + return savedVersion == VersionStamp.Default ? currentVersion : savedVersion; + } + + // we have this builder to avoid allocating collections unnecessarily. + private class Builder + { + private readonly ProjectId _projectId; + private readonly VersionStamp _version; + private readonly ImmutableHashSet _documentIds; + + private ImmutableDictionary>.Builder _syntaxLocals; + private ImmutableDictionary>.Builder _semanticLocals; + private ImmutableDictionary>.Builder _nonLocals; + private ImmutableArray _others; + + public Builder(ProjectId projectId, VersionStamp version, ImmutableHashSet documentIds) + { + _projectId = projectId; + _version = version; + _documentIds = documentIds; + } + + public void AddSyntaxLocals(DocumentId documentId, ImmutableArray diagnostics) + { + Add(ref _syntaxLocals, documentId, diagnostics); + } + + public void AddSemanticLocals(DocumentId documentId, ImmutableArray diagnostics) + { + Add(ref _semanticLocals, documentId, diagnostics); + } + + public void AddNonLocals(DocumentId documentId, ImmutableArray diagnostics) + { + Add(ref _nonLocals, documentId, diagnostics); + } + + public void AddOthers(ProjectId unused, ImmutableArray diagnostics) + { + _others = diagnostics; + } + + private void Add(ref ImmutableDictionary>.Builder locals, DocumentId documentId, ImmutableArray diagnostics) + { + locals = locals ?? ImmutableDictionary.CreateBuilder>(); + locals.Add(documentId, diagnostics); + } + + public AnalysisResult ToResult() + { + return new AnalysisResult(_projectId, _version, _documentIds, + _syntaxLocals?.ToImmutable() ?? ImmutableDictionary>.Empty, + _semanticLocals?.ToImmutable() ?? ImmutableDictionary>.Empty, + _nonLocals?.ToImmutable() ?? ImmutableDictionary>.Empty, + _others.IsDefault ? ImmutableArray.Empty : _others); + } + } } } } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.HostStates.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.HostStates.cs new file mode 100644 index 0000000000000..6189620ee1e04 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.HostStates.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + private partial class StateManager + { + /// + /// This class is responsible for anything related to for host level s. + /// + private class HostStates + { + private readonly StateManager _owner; + + private ImmutableDictionary _stateMap; + + public HostStates(StateManager owner) + { + _owner = owner; + _stateMap = ImmutableDictionary.Empty; + } + + public IEnumerable GetStateSets() + { + return _stateMap.Values.SelectMany(v => v.GetStateSets()); + } + + public IEnumerable GetOrCreateStateSets(string language) + { + return GetAnalyzerMap(language).GetStateSets(); + } + + public IEnumerable GetAnalyzers(string language) + { + var map = GetAnalyzerMap(language); + return map.GetAnalyzers(); + } + + public StateSet GetOrCreateStateSet(string language, DiagnosticAnalyzer analyzer) + { + return GetAnalyzerMap(language).GetStateSet(analyzer); + } + + private DiagnosticAnalyzerMap GetAnalyzerMap(string language) + { + return ImmutableInterlocked.GetOrAdd(ref _stateMap, language, CreateLanguageSpecificAnalyzerMap, this); + } + + private DiagnosticAnalyzerMap CreateLanguageSpecificAnalyzerMap(string language, HostStates @this) + { + var analyzersPerReference = _owner.AnalyzerManager.GetHostDiagnosticAnalyzersPerReference(language); + + var analyzerMap = CreateAnalyzerMap(_owner.AnalyzerManager, language, analyzersPerReference.Values); + VerifyDiagnosticStates(analyzerMap.Values); + + return new DiagnosticAnalyzerMap(_owner.AnalyzerManager, language, analyzerMap); + } + + private class DiagnosticAnalyzerMap + { + private readonly DiagnosticAnalyzer _compilerAnalyzer; + private readonly StateSet _compilerStateSet; + + private readonly ImmutableDictionary _map; + + public DiagnosticAnalyzerMap(HostAnalyzerManager analyzerManager, string language, ImmutableDictionary analyzerMap) + { + // hold directly on to compiler analyzer + _compilerAnalyzer = analyzerManager.GetCompilerDiagnosticAnalyzer(language); + + // in test case, we might not have the compiler analyzer. + if (_compilerAnalyzer == null) + { + _map = analyzerMap; + return; + } + + _compilerStateSet = analyzerMap[_compilerAnalyzer]; + + // hold rest of analyzers + _map = analyzerMap.Remove(_compilerAnalyzer); + } + + public IEnumerable GetAnalyzers() + { + // always return compiler one first if it exists. + // it might not exist in test environment. + if (_compilerAnalyzer != null) + { + yield return _compilerAnalyzer; + } + + foreach (var analyzer in _map.Keys) + { + yield return analyzer; + } + } + + public IEnumerable GetStateSets() + { + // always return compiler one first if it exists. + // it might not exist in test environment. + if (_compilerAnalyzer != null) + { + yield return _compilerStateSet; + } + + // TODO: for now, this is static, but in future, we might consider making this a dynamic so that we process cheaper analyzer first. + foreach (var set in _map.Values) + { + yield return set; + } + } + + public StateSet GetStateSet(DiagnosticAnalyzer analyzer) + { + if (_compilerAnalyzer == analyzer) + { + return _compilerStateSet; + } + + StateSet set; + if (_map.TryGetValue(analyzer, out set)) + { + return set; + } + + return null; + } + } + } + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.ProjectStates.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.ProjectStates.cs new file mode 100644 index 0000000000000..652535185a8c3 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.ProjectStates.cs @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + private partial class StateManager + { + /// + /// This class is responsible for anything related to for project level s. + /// + private class ProjectStates + { + private readonly StateManager _owner; + private readonly ConcurrentDictionary _stateMap; + + public ProjectStates(StateManager owner) + { + _owner = owner; + _stateMap = new ConcurrentDictionary(concurrencyLevel: 2, capacity: 10); + } + + public IEnumerable GetStateSets(ProjectId projectId) + { + var map = GetCachedAnalyzerMap(projectId); + return map.Values; + } + + public IEnumerable GetOrCreateAnalyzers(Project project) + { + var map = GetOrCreateAnalyzerMap(project); + return map.Keys; + } + + public IEnumerable GetOrUpdateStateSets(Project project) + { + var map = GetOrUpdateAnalyzerMap(project); + return map.Values; + } + + public IEnumerable GetOrCreateStateSets(Project project) + { + var map = GetOrCreateAnalyzerMap(project); + return map.Values; + } + + public StateSet GetOrCreateStateSet(Project project, DiagnosticAnalyzer analyzer) + { + var map = GetOrCreateAnalyzerMap(project); + + StateSet set; + if (map.TryGetValue(analyzer, out set)) + { + return set; + } + + return null; + } + + public void RemoveStateSet(ProjectId projectId) + { + if (projectId == null) + { + return; + } + + Entry unused; + _stateMap.TryRemove(projectId, out unused); + } + + private ImmutableDictionary GetOrUpdateAnalyzerMap(Project project) + { + var map = GetAnalyzerMap(project); + if (map != null) + { + return map; + } + + var newAnalyzersPerReference = _owner.AnalyzerManager.CreateProjectDiagnosticAnalyzersPerReference(project); + var newMap = StateManager.CreateAnalyzerMap(_owner.AnalyzerManager, project.Language, newAnalyzersPerReference.Values); + + RaiseProjectAnalyzerReferenceChangedIfNeeded(project, newAnalyzersPerReference, newMap); + + // update cache. + // add and update is same since this method will not be called concurrently. + var entry = _stateMap.AddOrUpdate(project.Id, + _ => new Entry(project.AnalyzerReferences, newAnalyzersPerReference, newMap), (_1, _2) => new Entry(project.AnalyzerReferences, newAnalyzersPerReference, newMap)); + + VerifyDiagnosticStates(entry.AnalyzerMap.Values); + + return entry.AnalyzerMap; + } + + private ImmutableDictionary GetCachedAnalyzerMap(ProjectId projectId) + { + Entry entry; + if (_stateMap.TryGetValue(projectId, out entry)) + { + return entry.AnalyzerMap; + } + + return ImmutableDictionary.Empty; + } + + private ImmutableDictionary GetOrCreateAnalyzerMap(Project project) + { + // if we can't use cached one, we will create a new analyzer map. which is a bit of waste since + // we will create new StateSet for all analyzers. but since this only happens when project analyzer references + // are changed, I believe it is acceptable to have a bit of waste for simplicity. + return GetAnalyzerMap(project) ?? CreateAnalyzerMap(project); + } + + private ImmutableDictionary GetAnalyzerMap(Project project) + { + Entry entry; + if (_stateMap.TryGetValue(project.Id, out entry) && entry.AnalyzerReferences.Equals(project.AnalyzerReferences)) + { + return entry.AnalyzerMap; + } + + return null; + } + + private ImmutableDictionary CreateAnalyzerMap(Project project) + { + if (project.AnalyzerReferences.Count == 0) + { + return ImmutableDictionary.Empty; + } + + var analyzersPerReference = _owner.AnalyzerManager.CreateProjectDiagnosticAnalyzersPerReference(project); + if (analyzersPerReference.Count == 0) + { + return ImmutableDictionary.Empty; + } + + return StateManager.CreateAnalyzerMap(_owner.AnalyzerManager, project.Language, analyzersPerReference.Values); + } + + private void RaiseProjectAnalyzerReferenceChangedIfNeeded( + Project project, + ImmutableDictionary> newMapPerReference, + ImmutableDictionary newMap) + { + Entry entry; + if (!_stateMap.TryGetValue(project.Id, out entry)) + { + // no previous references and we still don't have any references + if (newMap.Count == 0) + { + return; + } + + // new reference added + _owner.RaiseProjectAnalyzerReferenceChanged( + new ProjectAnalyzerReferenceChangedEventArgs(project, newMap.Values.ToImmutableArrayOrEmpty(), ImmutableArray.Empty)); + return; + } + + Contract.Requires(!entry.AnalyzerReferences.Equals(project.AnalyzerReferences)); + + // there has been change. find out what has changed + var addedStates = DiffStateSets(project.AnalyzerReferences.Except(entry.AnalyzerReferences), newMapPerReference, newMap); + var removedStates = DiffStateSets(entry.AnalyzerReferences.Except(project.AnalyzerReferences), entry.MapPerReferences, entry.AnalyzerMap); + + // nothing has changed + if (addedStates.Length == 0 && removedStates.Length == 0) + { + return; + } + + _owner.RaiseProjectAnalyzerReferenceChanged( + new ProjectAnalyzerReferenceChangedEventArgs(project, addedStates, removedStates)); + } + + private ImmutableArray DiffStateSets( + IEnumerable references, + ImmutableDictionary> mapPerReference, + ImmutableDictionary map) + { + if (mapPerReference.Count == 0 || map.Count == 0) + { + // nothing to diff + return ImmutableArray.Empty; + } + + var builder = ImmutableArray.CreateBuilder(); + foreach (var reference in references) + { + var referenceIdentity = _owner.AnalyzerManager.GetAnalyzerReferenceIdentity(reference); + + // check duplication + ImmutableArray analyzers; + if (!mapPerReference.TryGetValue(referenceIdentity, out analyzers)) + { + continue; + } + + // okay, this is real reference. get stateset + foreach (var analyzer in analyzers) + { + StateSet set; + if (!map.TryGetValue(analyzer, out set)) + { + continue; + } + + builder.Add(set); + } + } + + return builder.ToImmutable(); + } + + [Conditional("DEBUG")] + private void VerifyDiagnosticStates(IEnumerable stateSets) + { + // We do not de-duplicate analyzer instances across host and project analyzers. + var projectAnalyzers = stateSets.Select(state => state.Analyzer).ToImmutableHashSet(); + + var hostStates = _owner._hostStates.GetStateSets() + .Where(state => !projectAnalyzers.Contains(state.Analyzer)); + + StateManager.VerifyDiagnosticStates(hostStates.Concat(stateSets)); + } + + private struct Entry + { + public readonly IReadOnlyList AnalyzerReferences; + public readonly ImmutableDictionary> MapPerReferences; + public readonly ImmutableDictionary AnalyzerMap; + + public Entry( + IReadOnlyList analyzerReferences, + ImmutableDictionary> mapPerReferences, + ImmutableDictionary analyzerMap) + { + Contract.ThrowIfNull(analyzerReferences); + Contract.ThrowIfNull(mapPerReferences); + Contract.ThrowIfNull(analyzerMap); + + AnalyzerReferences = analyzerReferences; + MapPerReferences = mapPerReferences; + AnalyzerMap = analyzerMap; + } + } + } + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.cs new file mode 100644 index 0000000000000..5149a6a0e851e --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateManager.cs @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + private const string RoslynLanguageServices = "Roslyn Language Services"; + + /// + /// This is in charge of anything related to + /// + private partial class StateManager + { + private readonly HostAnalyzerManager _analyzerManager; + + private readonly HostStates _hostStates; + private readonly ProjectStates _projectStates; + + public StateManager(HostAnalyzerManager analyzerManager) + { + _analyzerManager = analyzerManager; + + _hostStates = new HostStates(this); + _projectStates = new ProjectStates(this); + } + + private HostAnalyzerManager AnalyzerManager { get { return _analyzerManager; } } + + /// + /// This will be raised whenever finds change + /// + public event EventHandler ProjectAnalyzerReferenceChanged; + + /// + /// Return existing or new s for the given . + /// + public IEnumerable GetOrCreateAnalyzers(Project project) + { + return _hostStates.GetAnalyzers(project.Language).Concat(_projectStates.GetOrCreateAnalyzers(project)); + } + + /// + /// Return s for the given . + /// This will never create new but will return ones already created. + /// + public IEnumerable GetStateSets(ProjectId projectId) + { + return _hostStates.GetStateSets().Concat(_projectStates.GetStateSets(projectId)); + } + + /// + /// Return s for the given . + /// This will never create new but will return ones already created. + /// Difference with is that + /// this will only return s that have same language as . + /// + public IEnumerable GetStateSets(Project project) + { + return GetStateSets(project.Id).Where(s => s.Language == project.Language); + } + + /// + /// Return s for the given . + /// This will either return already created s for the specific snapshot of or + /// It will create new s for the and update internal state. + /// + /// since this has a side-effect, this should never be called concurrently. and incremental analyzer (solution crawler) should guarantee that. + /// + public IEnumerable GetOrUpdateStateSets(Project project) + { + return _hostStates.GetOrCreateStateSets(project.Language).Concat(_projectStates.GetOrUpdateStateSets(project)); + } + + /// + /// Return s for the given . + /// This will either return already created s for the specific snapshot of or + /// It will create new s for the . + /// Unlike , this has no side effect. + /// + public IEnumerable GetOrCreateStateSets(Project project) + { + return _hostStates.GetOrCreateStateSets(project.Language).Concat(_projectStates.GetOrCreateStateSets(project)); + } + + /// + /// Return for the given in the context of . + /// This will either return already created for the specific snapshot of or + /// It will create new for the . + /// This will not have any side effect. + /// + public StateSet GetOrCreateStateSet(Project project, DiagnosticAnalyzer analyzer) + { + var stateSet = _hostStates.GetOrCreateStateSet(project.Language, analyzer); + if (stateSet != null) + { + return stateSet; + } + + return _projectStates.GetOrCreateStateSet(project, analyzer); + } + + /// + /// Return s that are added as the given 's AnalyzerReferences. + /// This will never create new but will return ones already created. + /// + public ImmutableArray CreateBuildOnlyProjectStateSet(Project project) + { + var referenceIdentities = project.AnalyzerReferences.Select(r => _analyzerManager.GetAnalyzerReferenceIdentity(r)).ToSet(); + var stateSetMap = GetStateSets(project).ToDictionary(s => s.Analyzer, s => s); + + var stateSets = ImmutableArray.CreateBuilder(); + + // we always include compiler analyzer in build only state + var compilerAnalyzer = _analyzerManager.GetCompilerDiagnosticAnalyzer(project.Language); + StateSet compilerStateSet; + if (stateSetMap.TryGetValue(compilerAnalyzer, out compilerStateSet)) + { + stateSets.Add(compilerStateSet); + } + + var analyzerMap = _analyzerManager.GetHostDiagnosticAnalyzersPerReference(project.Language); + foreach (var kv in analyzerMap) + { + var identity = kv.Key; + if (!referenceIdentities.Contains(identity)) + { + // it is from host analyzer package rather than project analyzer reference + // which build doesn't have + continue; + } + + // if same analyzer exists both in host (vsix) and in analyzer reference, + // we include it in build only analyzer. + foreach (var analyzer in kv.Value) + { + StateSet stateSet; + if (stateSetMap.TryGetValue(analyzer, out stateSet) && stateSet != compilerStateSet) + { + stateSets.Add(stateSet); + } + } + } + + return stateSets.ToImmutable(); + } + + public bool OnDocumentReset(IEnumerable stateSets, DocumentId documentId) + { + var removed = false; + foreach (var stateSet in stateSets) + { + removed |= stateSet.OnDocumentReset(documentId); + } + + return removed; + } + + public bool OnDocumentClosed(IEnumerable stateSets, DocumentId documentId) + { + var removed = false; + foreach (var stateSet in stateSets) + { + removed |= stateSet.OnDocumentClosed(documentId); + } + + return removed; + } + + public bool OnDocumentRemoved(IEnumerable stateSets, DocumentId documentId) + { + var removed = false; + foreach (var stateSet in stateSets) + { + removed |= stateSet.OnDocumentRemoved(documentId); + } + + return removed; + } + + public bool OnProjectRemoved(IEnumerable stateSets, ProjectId projectId) + { + var removed = false; + foreach (var stateSet in stateSets) + { + removed |= stateSet.OnProjectRemoved(projectId); + } + + _projectStates.RemoveStateSet(projectId); + return removed; + } + + private void RaiseProjectAnalyzerReferenceChanged(ProjectAnalyzerReferenceChangedEventArgs args) + { + ProjectAnalyzerReferenceChanged?.Invoke(this, args); + } + + private static ImmutableDictionary CreateAnalyzerMap( + HostAnalyzerManager analyzerManager, string language, IEnumerable> analyzerCollection) + { + var compilerAnalyzer = analyzerManager.GetCompilerDiagnosticAnalyzer(language); + + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var analyzers in analyzerCollection) + { + foreach (var analyzer in analyzers) + { + // TODO: + // #1, all de -duplication should move to HostAnalyzerManager + // #2, not sure whether de-duplication of analyzer itself makes sense. this can only happen + // if user deliberately put same analyzer twice. + if (builder.ContainsKey(analyzer)) + { + continue; + } + + var buildToolName = analyzer == compilerAnalyzer ? + PredefinedBuildTools.Live : GetBuildToolName(analyzerManager, language, analyzer); + + builder.Add(analyzer, new StateSet(language, analyzer, buildToolName)); + } + } + + return builder.ToImmutable(); + } + + private static string GetBuildToolName(HostAnalyzerManager analyzerManager, string language, DiagnosticAnalyzer analyzer) + { + var packageName = analyzerManager.GetDiagnosticAnalyzerPackageName(language, analyzer); + if (packageName == null) + { + return null; + } + + if (packageName == RoslynLanguageServices) + { + return PredefinedBuildTools.Live; + } + + return $"{analyzer.GetAnalyzerAssemblyName()} [{packageName}]"; + } + + [Conditional("DEBUG")] + private static void VerifyDiagnosticStates(IEnumerable stateSets) + { + // Ensure diagnostic state name is indeed unique. + var set = new HashSet>(); + + foreach (var stateSet in stateSets) + { + if (!(set.Add(ValueTuple.Create(stateSet.Language, stateSet.StateName)))) + { + Contract.Fail(); + } + } + } + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs new file mode 100644 index 0000000000000..4ee5c9c8ec9ca --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.StateSet.cs @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + /// + /// this contains all states regarding a + /// + private class StateSet + { + private const string UserDiagnosticsPrefixTableName = ""; + + private readonly string _language; + private readonly DiagnosticAnalyzer _analyzer; + private readonly string _errorSourceName; + + // analyzer version this state belong to + private readonly VersionStamp _analyzerVersion; + + // name of each analysis kind persistent storage + private readonly string _stateName; + private readonly string _syntaxStateName; + private readonly string _semanticStateName; + private readonly string _nonLocalStateName; + + private readonly ConcurrentDictionary _activeFileStates; + private readonly ConcurrentDictionary _projectStates; + + public StateSet(string language, DiagnosticAnalyzer analyzer, string errorSourceName) + { + _language = language; + _analyzer = analyzer; + _errorSourceName = errorSourceName; + + var nameAndVersion = GetNameAndVersion(_analyzer); + _analyzerVersion = nameAndVersion.Item2; + + _stateName = nameAndVersion.Item1; + + _syntaxStateName = _stateName + ".Syntax"; + _semanticStateName = _stateName + ".Semantic"; + _nonLocalStateName = _stateName + ".NonLocal"; + + _activeFileStates = new ConcurrentDictionary(concurrencyLevel: 2, capacity: 10); + _projectStates = new ConcurrentDictionary(concurrencyLevel: 2, capacity: 1); + } + + public string StateName => _stateName; + public string SyntaxStateName => _syntaxStateName; + public string SemanticStateName => _semanticStateName; + public string NonLocalStateName => _nonLocalStateName; + + public string Language => _language; + public string ErrorSourceName => _errorSourceName; + + public DiagnosticAnalyzer Analyzer => _analyzer; + public VersionStamp AnalyzerVersion => _analyzerVersion; + + public bool ContainsAnyDocumentOrProjectDiagnostics(ProjectId projectId) + { + foreach (var state in GetActiveFileStates(projectId)) + { + if (!state.IsEmpty) + { + return true; + } + } + + ProjectState projectState; + if (!_projectStates.TryGetValue(projectId, out projectState)) + { + return false; + } + + return !projectState.IsEmpty(); + } + + public IEnumerable GetDocumentsWithDiagnostics(ProjectId projectId) + { + HashSet set = null; + foreach (var state in GetActiveFileStates(projectId)) + { + set = set ?? new HashSet(); + set.Add(state.DocumentId); + } + + ProjectState projectState; + if (!_projectStates.TryGetValue(projectId, out projectState) || projectState.IsEmpty()) + { + return set ?? SpecializedCollections.EmptyEnumerable(); + } + + set = set ?? new HashSet(); + set.UnionWith(projectState.GetDocumentsWithDiagnostics()); + + return set; + } + + private IEnumerable GetActiveFileStates(ProjectId projectId) + { + return _activeFileStates.Where(kv => kv.Key.ProjectId == projectId).Select(kv => kv.Value); + } + + public bool IsActiveFile(DocumentId documentId) + { + return _activeFileStates.ContainsKey(documentId); + } + + public bool TryGetActiveFileState(DocumentId documentId, out ActiveFileState state) + { + return _activeFileStates.TryGetValue(documentId, out state); + } + + public bool TryGetProjectState(ProjectId projectId, out ProjectState state) + { + return _projectStates.TryGetValue(projectId, out state); + } + + public ActiveFileState GetActiveFileState(DocumentId documentId) + { + return _activeFileStates.GetOrAdd(documentId, id => new ActiveFileState(id)); + } + + public ProjectState GetProjectState(ProjectId projectId) + { + return _projectStates.GetOrAdd(projectId, id => new ProjectState(this, id)); + } + + public bool OnDocumentClosed(DocumentId id) + { + return OnDocumentReset(id); + } + + public bool OnDocumentReset(DocumentId id) + { + // remove active file state + ActiveFileState state; + if (_activeFileStates.TryRemove(id, out state)) + { + return !state.IsEmpty; + } + + return false; + } + + public bool OnDocumentRemoved(DocumentId id) + { + // remove active file state for removed document + var removed = OnDocumentReset(id); + + // remove state for the file that got removed. + ProjectState state; + if (_projectStates.TryGetValue(id.ProjectId, out state)) + { + removed |= state.OnDocumentRemoved(id); + } + + return removed; + } + + public bool OnProjectRemoved(ProjectId id) + { + // remove state for project that got removed. + ProjectState state; + if (_projectStates.TryRemove(id, out state)) + { + return state.OnProjectRemoved(id); + } + + return false; + } + + public void OnRemoved() + { + // ths stateset is being removed. + // TODO: we do this since InMemoryCache is static type. we might consider making it instance object + // of something. + InMemoryStorage.DropCache(Analyzer); + } + + /// + /// Get the unique state name for the given analyzer. + /// Note that this name is used by the underlying persistence stream of the corresponding to Read/Write diagnostic data into the stream. + /// If any two distinct analyzer have the same diagnostic state name, we will end up sharing the persistence stream between them, leading to duplicate/missing/incorrect diagnostic data. + /// + private static ValueTuple GetNameAndVersion(DiagnosticAnalyzer analyzer) + { + Contract.ThrowIfNull(analyzer); + + // Get the unique ID for given diagnostic analyzer. + // note that we also put version stamp so that we can detect changed analyzer. + var tuple = analyzer.GetAnalyzerIdAndVersion(); + return ValueTuple.Create(UserDiagnosticsPrefixTableName + "_" + tuple.Item1, tuple.Item2); + } + } + } +} diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs index 50bbf3ab086d5..d3f9bb9616f12 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs @@ -1,23 +1,27 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Shared.Options; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 { - // TODO: implement correct events and etc. + /// + /// Diagnostic Analyzer Engine V2 + /// + /// This one follows pattern compiler has set for diagnostic analyzer. + /// internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer { private readonly int _correlationId; + private readonly StateManager _stateManager; + private readonly Executor _executor; + private readonly CompilationManager _compilationManager; + public DiagnosticIncrementalAnalyzer( DiagnosticAnalyzerService owner, int correlationId, @@ -27,86 +31,157 @@ public DiagnosticIncrementalAnalyzer( : base(owner, workspace, hostAnalyzerManager, hostDiagnosticUpdateSource) { _correlationId = correlationId; - } - private static bool AnalysisEnabled(Document document) - { - // change it to check active file (or visible files), not open files if active file tracking is enabled. - // otherwise, use open file. - return document.IsOpen(); + _stateManager = new StateManager(hostAnalyzerManager); + _stateManager.ProjectAnalyzerReferenceChanged += OnProjectAnalyzerReferenceChanged; + + _executor = new Executor(this); + _compilationManager = new CompilationManager(this); } - private bool FullAnalysisEnabled(Workspace workspace, string language) + public override bool ContainsDiagnostics(Workspace workspace, ProjectId projectId) { - return workspace.Options.GetOption(ServiceFeatureOnOffOptions.ClosedFileDiagnostic, language) && - workspace.Options.GetOption(RuntimeOptions.FullSolutionAnalysis); + foreach (var stateSet in _stateManager.GetStateSets(projectId)) + { + if (stateSet.ContainsAnyDocumentOrProjectDiagnostics(projectId)) + { + return true; + } + } + + return false; } - private async Task> GetProjectDiagnosticsAsync(Project project, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) + private bool SupportAnalysisKind(DiagnosticAnalyzer analyzer, string language, AnalysisKind kind) { - if (project == null) + // compiler diagnostic analyzer always support all kinds + if (HostAnalyzerManager.IsCompilerDiagnosticAnalyzer(language, analyzer)) { - return ImmutableArray.Empty; + return true; } - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + switch (kind) + { + case AnalysisKind.Syntax: + return analyzer.SupportsSyntaxDiagnosticAnalysis(); + case AnalysisKind.Semantic: + return analyzer.SupportsSemanticDiagnosticAnalysis(); + default: + return Contract.FailWithReturn("shouldn't reach here"); + } + } - var analyzers = HostAnalyzerManager.CreateDiagnosticAnalyzers(project); + private void OnProjectAnalyzerReferenceChanged(object sender, ProjectAnalyzerReferenceChangedEventArgs e) + { + if (e.Removed.Length == 0) + { + // nothing to refresh + return; + } - var compilationWithAnalyzer = compilation.WithAnalyzers(analyzers, project.AnalyzerOptions, cancellationToken); + // events will be automatically serialized. + var project = e.Project; + var stateSets = e.Removed; - // REVIEW: this API is a bit strange. - // if getting diagnostic is cancelled, it has to create new compilation and do everything from scratch again? - var dxs = GetDiagnosticData(project, await compilationWithAnalyzer.GetAnalyzerDiagnosticsAsync().ConfigureAwait(false)).ToImmutableArrayOrEmpty(); + // make sure we drop cache related to the analyzers + foreach (var stateSet in stateSets) + { + stateSet.OnRemoved(); + } - return dxs; + ClearAllDiagnostics(stateSets, project.Id); } - private IEnumerable GetDiagnosticData(Project project, ImmutableArray diagnostics) + private void ClearAllDiagnostics(ImmutableArray stateSets, ProjectId projectId) { - foreach (var diagnostic in diagnostics) + Owner.RaiseBulkDiagnosticsUpdated(raiseEvents => { - if (diagnostic.Location == Location.None) + var handleActiveFile = true; + foreach (var stateSet in stateSets) { - yield return DiagnosticData.Create(project, diagnostic); - continue; - } + // PERF: don't fire events for ones that we dont have any diagnostics on + if (!stateSet.ContainsAnyDocumentOrProjectDiagnostics(projectId)) + { + continue; + } - var document = project.GetDocument(diagnostic.Location.SourceTree); - if (document == null) - { - continue; + RaiseProjectDiagnosticsRemoved(stateSet, projectId, stateSet.GetDocumentsWithDiagnostics(projectId), handleActiveFile, raiseEvents); } + }); + } - yield return DiagnosticData.Create(document, diagnostic); - } + private void RaiseDiagnosticsCreated( + Project project, StateSet stateSet, ImmutableArray items, Action raiseEvents) + { + Contract.ThrowIfFalse(project.Solution.Workspace == Workspace); + + raiseEvents(DiagnosticsUpdatedArgs.DiagnosticsCreated( + CreateId(stateSet.Analyzer, project.Id, AnalysisKind.NonLocal, stateSet.ErrorSourceName), + project.Solution.Workspace, + project.Solution, + project.Id, + documentId: null, + diagnostics: items)); } - private void RaiseEvents(Project project, ImmutableArray diagnostics) + private void RaiseDiagnosticsRemoved( + ProjectId projectId, Solution solution, StateSet stateSet, Action raiseEvents) { - var groups = diagnostics.GroupBy(d => d.DocumentId); + Contract.ThrowIfFalse(solution == null || solution.Workspace == Workspace); + + raiseEvents(DiagnosticsUpdatedArgs.DiagnosticsRemoved( + CreateId(stateSet.Analyzer, projectId, AnalysisKind.NonLocal, stateSet.ErrorSourceName), + Workspace, + solution, + projectId, + documentId: null)); + } - var solution = project.Solution; - var workspace = solution.Workspace; + private void RaiseDiagnosticsCreated( + Document document, StateSet stateSet, AnalysisKind kind, ImmutableArray items, Action raiseEvents) + { + Contract.ThrowIfFalse(document.Project.Solution.Workspace == Workspace); + + raiseEvents(DiagnosticsUpdatedArgs.DiagnosticsCreated( + CreateId(stateSet.Analyzer, document.Id, kind, stateSet.ErrorSourceName), + document.Project.Solution.Workspace, + document.Project.Solution, + document.Project.Id, + document.Id, + items)); + } - foreach (var kv in groups) - { - if (kv.Key == null) - { - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsCreated( - ValueTuple.Create(this, project.Id), workspace, solution, project.Id, null, kv.ToImmutableArrayOrEmpty())); - continue; - } + private void RaiseDiagnosticsRemoved( + DocumentId documentId, Solution solution, StateSet stateSet, AnalysisKind kind, Action raiseEvents) + { + Contract.ThrowIfFalse(solution == null || solution.Workspace == Workspace); + + raiseEvents(DiagnosticsUpdatedArgs.DiagnosticsRemoved( + CreateId(stateSet.Analyzer, documentId, kind, stateSet.ErrorSourceName), + Workspace, + solution, + documentId.ProjectId, + documentId)); + } - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsCreated( - ValueTuple.Create(this, kv.Key), workspace, solution, project.Id, kv.Key, kv.ToImmutableArrayOrEmpty())); - } + private object CreateId(DiagnosticAnalyzer analyzer, DocumentId key, AnalysisKind kind, string errorSourceName) + { + return CreateIdInternal(analyzer, key, kind, errorSourceName); } - public override bool ContainsDiagnostics(Workspace workspace, ProjectId projectId) + private object CreateId(DiagnosticAnalyzer analyzer, ProjectId key, AnalysisKind kind, string errorSourceName) { - // for now, it always return false; - return false; + return CreateIdInternal(analyzer, key, kind, errorSourceName); + } + + private static object CreateIdInternal(DiagnosticAnalyzer analyzer, object key, AnalysisKind kind, string errorSourceName) + { + return new LiveDiagnosticUpdateArgsId(analyzer, key, (int)kind, errorSourceName); + } + + public static Task GetDiagnosticVersionAsync(Project project, CancellationToken cancellationToken) + { + return project.GetDependentVersionAsync(cancellationToken); } } } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs index aa378e8c3aab4..0755cc990e6ba 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs @@ -1,6 +1,10 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Roslyn.Utilities; @@ -8,18 +12,183 @@ namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 { internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer { - // TODO: this API will be changed to 1 API that gives all diagnostics under a project - // and that will simply replace project state - public override Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.BatchUpdateToken token, Project project, ImmutableArray diagnostics) + public override async Task SynchronizeWithBuildAsync(Workspace workspace, ImmutableDictionary> map) { - // TODO: for now, we dont do anything. - return SpecializedTasks.EmptyTask; + if (!PreferBuildErrors(workspace)) + { + // prefer live errors over build errors + return; + } + + var solution = workspace.CurrentSolution; + foreach (var projectEntry in map) + { + var project = solution.GetProject(projectEntry.Key); + if (project == null) + { + continue; + } + + // REVIEW: is build diagnostic contains suppressed diagnostics? + var stateSets = _stateManager.CreateBuildOnlyProjectStateSet(project); + var result = await CreateProjectAnalysisDataAsync(project, stateSets, projectEntry.Value).ConfigureAwait(false); + + foreach (var stateSet in stateSets) + { + var state = stateSet.GetProjectState(project.Id); + await state.SaveAsync(project, result.GetResult(stateSet.Analyzer)).ConfigureAwait(false); + } + + // REVIEW: this won't handle active files. might need to tweak it later. + RaiseProjectDiagnosticsIfNeeded(project, stateSets, result.OldResult, result.Result); + } + + if (PreferLiveErrorsOnOpenedFiles(workspace)) + { + // enqueue re-analysis of open documents. + this.Owner.Reanalyze(workspace, documentIds: workspace.GetOpenDocumentIds(), highPriority: true); + } + } + + private async Task CreateProjectAnalysisDataAsync(Project project, ImmutableArray stateSets, ImmutableArray diagnostics) + { + // we always load data sicne we don't know right version. + var avoidLoadingData = false; + var oldAnalysisData = await ProjectAnalysisData.CreateAsync(project, stateSets, avoidLoadingData, CancellationToken.None).ConfigureAwait(false); + var newResult = CreateAnalysisResults(project, stateSets, oldAnalysisData, diagnostics); + + return new ProjectAnalysisData(VersionStamp.Default, oldAnalysisData.Result, newResult); + } + + private ImmutableDictionary CreateAnalysisResults( + Project project, ImmutableArray stateSets, ProjectAnalysisData oldAnalysisData, ImmutableArray diagnostics) + { + using (var poolObject = SharedPools.Default>().GetPooledObject()) + { + // we can't distinguish locals and non locals from build diagnostics nor determine right snapshot version for the build. + // so we put everything in as semantic local with default version. this lets us to replace those to live diagnostics when needed easily. + var version = VersionStamp.Default; + var lookup = diagnostics.ToLookup(d => d.Id); + + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var stateSet in stateSets) + { + var descriptors = HostAnalyzerManager.GetDiagnosticDescriptors(stateSet.Analyzer); + var liveDiagnostics = MergeDiagnostics(ConvertToLiveDiagnostics(lookup, descriptors, poolObject.Object), GetDiagnostics(oldAnalysisData.GetResult(stateSet.Analyzer))); + + var group = liveDiagnostics.GroupBy(d => d.DocumentId); + var result = new AnalysisResult( + project.Id, + version, + documentIds: group.Where(g => g.Key != null).Select(g => g.Key).ToImmutableHashSet(), + syntaxLocals: ImmutableDictionary>.Empty, + semanticLocals: group.Where(g => g.Key != null).ToImmutableDictionary(g => g.Key, g => g.ToImmutableArray()), + nonLocals: ImmutableDictionary>.Empty, + others: ImmutableArray.Empty); + + builder.Add(stateSet.Analyzer, result); + } + + return builder.ToImmutable(); + } + } + + private ImmutableArray GetDiagnostics(AnalysisResult result) + { + // PERF: don't allocation anything if not needed + if (result.IsAggregatedForm || result.IsEmpty) + { + return ImmutableArray.Empty; + } + + return result.SyntaxLocals.Values.SelectMany(v => v).Concat( + result.SemanticLocals.Values.SelectMany(v => v)).Concat( + result.NonLocals.Values.SelectMany(v => v)).Concat( + result.Others).ToImmutableArray(); + } + + private bool PreferBuildErrors(Workspace workspace) + { + return workspace.Options.GetOption(InternalDiagnosticsOptions.BuildErrorIsTheGod) || workspace.Options.GetOption(InternalDiagnosticsOptions.PreferBuildErrorsOverLiveErrors); + } + + private bool PreferLiveErrorsOnOpenedFiles(Workspace workspace) + { + return !workspace.Options.GetOption(InternalDiagnosticsOptions.BuildErrorIsTheGod) && workspace.Options.GetOption(InternalDiagnosticsOptions.PreferLiveErrorsOnOpenedFiles); + } + + private ImmutableArray MergeDiagnostics(ImmutableArray newDiagnostics, ImmutableArray existingDiagnostics) + { + ImmutableArray.Builder builder = null; + + if (newDiagnostics.Length > 0) + { + builder = ImmutableArray.CreateBuilder(); + builder.AddRange(newDiagnostics); + } + + if (existingDiagnostics.Length > 0) + { + // retain hidden live diagnostics since it won't be comes from build. + builder = builder ?? ImmutableArray.CreateBuilder(); + builder.AddRange(existingDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Hidden)); + } + + return builder == null ? ImmutableArray.Empty : builder.ToImmutable(); + } + + private ImmutableArray ConvertToLiveDiagnostics( + ILookup lookup, ImmutableArray descriptors, HashSet seen) + { + if (lookup == null) + { + return ImmutableArray.Empty; + } + + ImmutableArray.Builder builder = null; + foreach (var descriptor in descriptors) + { + // make sure we don't report same id to multiple different analyzers + if (!seen.Add(descriptor.Id)) + { + // TODO: once we have information where diagnostic came from, we probably don't need this. + continue; + } + + var items = lookup[descriptor.Id]; + if (items == null) + { + continue; + } + + builder = builder ?? ImmutableArray.CreateBuilder(); + builder.AddRange(items.Select(d => CreateLiveDiagnostic(descriptor, d))); + } + + return builder == null ? ImmutableArray.Empty : builder.ToImmutable(); } - public override Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.BatchUpdateToken token, Document document, ImmutableArray diagnostics) + private static DiagnosticData CreateLiveDiagnostic(DiagnosticDescriptor descriptor, DiagnosticData diagnostic) { - // TODO: for now, we dont do anything. - return SpecializedTasks.EmptyTask; + return new DiagnosticData( + descriptor.Id, + descriptor.Category, + diagnostic.Message, + descriptor.GetBingHelpMessage(), + diagnostic.Severity, + descriptor.DefaultSeverity, + descriptor.IsEnabledByDefault, + diagnostic.WarningLevel, + descriptor.CustomTags.ToImmutableArray(), + diagnostic.Properties, + diagnostic.Workspace, + diagnostic.ProjectId, + diagnostic.DataLocation, + diagnostic.AdditionalLocations, + descriptor.Title.ToString(CultureInfo.CurrentUICulture), + descriptor.Description.ToString(CultureInfo.CurrentUICulture), + descriptor.HelpLinkUri, + isSuppressed: diagnostic.IsSuppressed); } } } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs index 093213a776ae3..2da4187726011 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; @@ -8,69 +9,466 @@ namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 { - // TODO: need to mimic what V1 does. use cache if possible, otherwise, calculate at the spot. - internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer + internal partial class DiagnosticIncrementalAnalyzer { public override Task> GetSpecificCachedDiagnosticsAsync(Solution solution, object id, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) { - return GetSpecificDiagnosticsAsync(solution, id, includeSuppressedDiagnostics, cancellationToken); + return new IDECachedDiagnosticGetter(this, solution, id, includeSuppressedDiagnostics).GetSpecificDiagnosticsAsync(cancellationToken); } - public override Task> GetCachedDiagnosticsAsync(Solution solution, ProjectId projectId = null, DocumentId documentId = null, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + public override Task> GetCachedDiagnosticsAsync(Solution solution, ProjectId projectId, DocumentId documentId, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) { - return GetDiagnosticsAsync(solution, projectId, documentId, includeSuppressedDiagnostics, cancellationToken); + return new IDECachedDiagnosticGetter(this, solution, projectId, documentId, includeSuppressedDiagnostics).GetDiagnosticsAsync(cancellationToken); } - public override async Task> GetSpecificDiagnosticsAsync(Solution solution, object id, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + public override Task> GetSpecificDiagnosticsAsync(Solution solution, object id, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) { - if (id is ValueTuple) + return new IDELatestDiagnosticGetter(this, solution, id, includeSuppressedDiagnostics).GetSpecificDiagnosticsAsync(cancellationToken); + } + + public override Task> GetDiagnosticsAsync(Solution solution, ProjectId projectId, DocumentId documentId, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + { + return new IDELatestDiagnosticGetter(this, solution, projectId, documentId, includeSuppressedDiagnostics).GetDiagnosticsAsync(cancellationToken); + } + + public override Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId projectId, DocumentId documentId, ImmutableHashSet diagnosticIds, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + { + return new IDELatestDiagnosticGetter(this, diagnosticIds, solution, projectId, documentId, includeSuppressedDiagnostics).GetDiagnosticsAsync(cancellationToken); + } + + public override Task> GetProjectDiagnosticsForIdsAsync(Solution solution, ProjectId projectId, ImmutableHashSet diagnosticIds, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + { + return new IDELatestDiagnosticGetter(this, diagnosticIds, solution, projectId, includeSuppressedDiagnostics).GetProjectDiagnosticsAsync(cancellationToken); + } + + private abstract class DiagnosticGetter + { + protected readonly DiagnosticIncrementalAnalyzer Owner; + + protected readonly Solution Solution; + protected readonly ProjectId ProjectId; + protected readonly DocumentId DocumentId; + protected readonly object Id; + protected readonly bool IncludeSuppressedDiagnostics; + + private ImmutableArray.Builder _builder; + + public DiagnosticGetter( + DiagnosticIncrementalAnalyzer owner, + Solution solution, + ProjectId projectId, + DocumentId documentId, + object id, + bool includeSuppressedDiagnostics) { - var key = (ValueTuple)id; - return await GetDiagnosticsAsync(solution, key.Item2.ProjectId, key.Item2, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + Owner = owner; + Solution = solution; + + DocumentId = documentId; + ProjectId = projectId; + + Id = id; + IncludeSuppressedDiagnostics = includeSuppressedDiagnostics; + + // try to retrieve projectId/documentId from id if possible. + var argsId = id as LiveDiagnosticUpdateArgsId; + if (argsId != null) + { + DocumentId = DocumentId ?? argsId.Key as DocumentId; + ProjectId = ProjectId ?? (argsId.Key as ProjectId) ?? DocumentId.ProjectId; + } + + _builder = null; } - if (id is ValueTuple) + protected StateManager StateManager => this.Owner._stateManager; + + protected Project Project => Solution.GetProject(ProjectId); + protected Document Document => Solution.GetDocument(DocumentId); + + protected virtual bool ShouldIncludeDiagnostic(DiagnosticData diagnostic) => true; + + protected ImmutableArray GetDiagnosticData() { - var key = (ValueTuple)id; - var diagnostics = await GetDiagnosticsAsync(solution, key.Item2, null, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return diagnostics.Where(d => d.DocumentId == null).ToImmutableArray(); + return _builder != null ? _builder.ToImmutableArray() : ImmutableArray.Empty; } - return ImmutableArray.Empty; - } + protected abstract Task?> GetDiagnosticsAsync(StateSet stateSet, Project project, DocumentId documentId, AnalysisKind kind, CancellationToken cancellationToken); + protected abstract Task AppendDiagnosticsAsync(Project project, DocumentId targetDocumentId, IEnumerable documentIds, CancellationToken cancellationToken); - public override async Task> GetDiagnosticsAsync(Solution solution, ProjectId projectId = null, DocumentId documentId = null, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) - { - if (documentId != null) + public async Task> GetSpecificDiagnosticsAsync(CancellationToken cancellationToken) + { + if (Solution == null) + { + return ImmutableArray.Empty; + } + + var argsId = Id as LiveDiagnosticUpdateArgsId; + if (argsId == null) + { + return ImmutableArray.Empty; + } + + 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 = this.StateManager.GetOrCreateStateSet(Project, argsId.Analyzer); + if (stateSet == null) + { + return ImmutableArray.Empty; + } + + var diagnostics = await GetDiagnosticsAsync(stateSet, Project, DocumentId, (AnalysisKind)argsId.Kind, cancellationToken).ConfigureAwait(false); + if (diagnostics == null) + { + // Document or project might have been removed from the solution. + return ImmutableArray.Empty; + } + + return FilterSuppressedDiagnostics(diagnostics.Value); + } + + public async Task> GetDiagnosticsAsync(CancellationToken cancellationToken) { - var diagnostics = await GetProjectDiagnosticsAsync(solution.GetProject(projectId), includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return diagnostics.Where(d => d.DocumentId == documentId).ToImmutableArrayOrEmpty(); + if (Solution == null) + { + return ImmutableArray.Empty; + } + + if (ProjectId != null) + { + if (Project == null) + { + return GetDiagnosticData(); + } + + var documentIds = DocumentId != null ? SpecializedCollections.SingletonEnumerable(DocumentId) : Project.DocumentIds; + + // return diagnostics specific to one project or document + await AppendDiagnosticsAsync(Project, DocumentId, documentIds, cancellationToken).ConfigureAwait(false); + return GetDiagnosticData(); + } + + await AppendDiagnosticsAsync(Solution, cancellationToken).ConfigureAwait(false); + return GetDiagnosticData(); } - if (projectId != null) + protected async Task AppendDiagnosticsAsync(Solution solution, CancellationToken cancellationToken) { - return await GetProjectDiagnosticsAsync(solution.GetProject(projectId), includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + // when we return cached result, make sure we at least return something that exist in current solution + if (Project == null) + { + return; + } + + // PERF: should run this concurrently? analyzer driver itself is already running concurrently. + DocumentId nullTargetDocumentId = null; + foreach (var project in Solution.Projects) + { + await AppendDiagnosticsAsync(project, nullTargetDocumentId, project.DocumentIds, cancellationToken: cancellationToken).ConfigureAwait(false); + } } - var builder = ImmutableArray.CreateBuilder(); - foreach (var project in solution.Projects) + protected void AppendDiagnostics(IEnumerable items) { - builder.AddRange(await GetProjectDiagnosticsAsync(project, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false)); + if (items == null) + { + return; + } + + if (_builder == null) + { + Interlocked.CompareExchange(ref _builder, ImmutableArray.CreateBuilder(), null); + } + + lock (_builder) + { + _builder.AddRange(items.Where(ShouldIncludeSuppressedDiagnostic).Where(ShouldIncludeDiagnostic)); + } } - return builder.ToImmutable(); + private bool ShouldIncludeSuppressedDiagnostic(DiagnosticData diagnostic) + { + return IncludeSuppressedDiagnostics || !diagnostic.IsSuppressed; + } + + private ImmutableArray FilterSuppressedDiagnostics(ImmutableArray diagnostics) + { + if (IncludeSuppressedDiagnostics || diagnostics.IsDefaultOrEmpty) + { + return diagnostics; + } + + // create builder only if there is suppressed diagnostics + ImmutableArray.Builder builder = null; + for (int i = 0; i < diagnostics.Length; i++) + { + var diagnostic = diagnostics[i]; + if (diagnostic.IsSuppressed) + { + if (builder == null) + { + builder = ImmutableArray.CreateBuilder(); + for (int j = 0; j < i; j++) + { + builder.Add(diagnostics[j]); + } + } + } + else if (builder != null) + { + builder.Add(diagnostic); + } + } + + return builder != null ? builder.ToImmutable() : diagnostics; + } } - public override async Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId projectId = null, DocumentId documentId = null, ImmutableHashSet diagnosticIds = null, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + private class IDECachedDiagnosticGetter : DiagnosticGetter { - var diagnostics = await GetDiagnosticsAsync(solution, projectId, documentId, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return diagnostics.Where(d => diagnosticIds.Contains(d.Id)).ToImmutableArrayOrEmpty(); + public IDECachedDiagnosticGetter(DiagnosticIncrementalAnalyzer owner, Solution solution, object id, bool includeSuppressedDiagnostics) : + base(owner, solution, projectId: null, documentId: null, id: id, includeSuppressedDiagnostics: includeSuppressedDiagnostics) + { + } + + public IDECachedDiagnosticGetter(DiagnosticIncrementalAnalyzer owner, Solution solution, ProjectId projectId, DocumentId documentId, bool includeSuppressedDiagnostics) : + base(owner, solution, projectId, documentId, id: null, includeSuppressedDiagnostics: includeSuppressedDiagnostics) + { + } + + protected override async Task AppendDiagnosticsAsync(Project project, DocumentId targetDocumentId, IEnumerable documentIds, CancellationToken cancellationToken) + { + // when we return cached result, make sure we at least return something that exist in current solution + if (Project == null) + { + return; + } + + foreach (var stateSet in StateManager.GetOrCreateStateSets(project)) + { + foreach (var documentId in documentIds) + { + AppendDiagnostics(await GetDiagnosticsAsync(stateSet, project, documentId, AnalysisKind.Syntax, cancellationToken).ConfigureAwait(false)); + AppendDiagnostics(await GetDiagnosticsAsync(stateSet, project, documentId, AnalysisKind.Semantic, cancellationToken).ConfigureAwait(false)); + AppendDiagnostics(await GetDiagnosticsAsync(stateSet, project, documentId, AnalysisKind.NonLocal, cancellationToken).ConfigureAwait(false)); + } + + if (targetDocumentId == null) + { + // include project diagnostics if there is no target document + AppendDiagnostics(await GetProjectDiagnosticsAsync(stateSet, project, targetDocumentId, AnalysisKind.NonLocal, cancellationToken).ConfigureAwait(false)); + } + } + } + + protected override async Task?> GetDiagnosticsAsync(StateSet stateSet, Project project, DocumentId documentId, AnalysisKind kind, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var activeFileDiagnostics = GetActiveFileDiagnostics(stateSet, documentId, kind); + if (activeFileDiagnostics.HasValue) + { + return activeFileDiagnostics.Value; + } + + var projectDiagnostics = await GetProjectDiagnosticsAsync(stateSet, project, documentId, kind, cancellationToken).ConfigureAwait(false); + if (projectDiagnostics.HasValue) + { + return projectDiagnostics.Value; + } + + return null; + } + + private ImmutableArray? GetActiveFileDiagnostics(StateSet stateSet, DocumentId documentId, AnalysisKind kind) + { + if (documentId == null) + { + return null; + } + + ActiveFileState state; + if (!stateSet.TryGetActiveFileState(documentId, out state)) + { + return null; + } + + return state.GetAnalysisData(kind).Items; + } + + private async Task?> GetProjectDiagnosticsAsync( + StateSet stateSet, Project project, DocumentId documentId, AnalysisKind kind, CancellationToken cancellationToken) + { + ProjectState state; + if (!stateSet.TryGetProjectState(project.Id, out state)) + { + // never analyzed this project yet. + return null; + } + + if (documentId != null) + { + // file doesn't exist in current solution + var document = Solution.GetDocument(documentId); + if (document == null) + { + return null; + } + + var result = await state.GetAnalysisDataAsync(document, avoidLoadingData: false, cancellationToken: cancellationToken).ConfigureAwait(false); + return GetResult(result, kind, documentId); + } + + Contract.ThrowIfFalse(kind == AnalysisKind.NonLocal); + var nonLocalResult = await state.GetProjectAnalysisDataAsync(project, avoidLoadingData: false, cancellationToken: cancellationToken).ConfigureAwait(false); + return nonLocalResult.Others; + } } - public override async Task> GetProjectDiagnosticsForIdsAsync(Solution solution, ProjectId projectId = null, ImmutableHashSet diagnosticIds = null, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + private class IDELatestDiagnosticGetter : DiagnosticGetter { - var diagnostics = await GetDiagnosticsForIdsAsync(solution, projectId, null, diagnosticIds, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return diagnostics.Where(d => d.DocumentId == null).ToImmutableArray(); + private readonly ImmutableHashSet _diagnosticIds; + + public IDELatestDiagnosticGetter(DiagnosticIncrementalAnalyzer owner, Solution solution, object id, bool includeSuppressedDiagnostics) : + base(owner, solution, projectId: null, documentId: null, id: id, includeSuppressedDiagnostics: includeSuppressedDiagnostics) + { + _diagnosticIds = null; + } + + public IDELatestDiagnosticGetter(DiagnosticIncrementalAnalyzer owner, Solution solution, ProjectId projectId, DocumentId documentId, bool includeSuppressedDiagnostics) : + base(owner, solution, projectId, documentId, id: null, includeSuppressedDiagnostics: includeSuppressedDiagnostics) + { + _diagnosticIds = null; + } + + public IDELatestDiagnosticGetter(DiagnosticIncrementalAnalyzer owner, ImmutableHashSet diagnosticIds, Solution solution, ProjectId projectId, bool includeSuppressedDiagnostics) : + this(owner, diagnosticIds, solution, projectId, documentId: null, includeSuppressedDiagnostics: includeSuppressedDiagnostics) + { + } + + public IDELatestDiagnosticGetter(DiagnosticIncrementalAnalyzer owner, ImmutableHashSet diagnosticIds, Solution solution, ProjectId projectId, DocumentId documentId, bool includeSuppressedDiagnostics) : + base(owner, solution, projectId, documentId, id: null, includeSuppressedDiagnostics: includeSuppressedDiagnostics) + { + _diagnosticIds = diagnosticIds; + } + + public async Task> GetProjectDiagnosticsAsync(CancellationToken cancellationToken) + { + if (Solution == null) + { + return GetDiagnosticData(); + } + + DocumentId nullTargetDocumentId = null; + + if (ProjectId != null) + { + await AppendDiagnosticsAsync(Project, nullTargetDocumentId, SpecializedCollections.EmptyEnumerable(), cancellationToken).ConfigureAwait(false); + return GetDiagnosticData(); + } + + await AppendDiagnosticsAsync(Solution, cancellationToken).ConfigureAwait(false); + return GetDiagnosticData(); + } + + protected override bool ShouldIncludeDiagnostic(DiagnosticData diagnostic) + { + return _diagnosticIds == null || _diagnosticIds.Contains(diagnostic.Id); + } + + protected override async Task AppendDiagnosticsAsync(Project project, DocumentId targetDocumentId, IEnumerable documentIds, CancellationToken cancellationToken) + { + // when we return cached result, make sure we at least return something that exist in current solution + if (Project == null) + { + return; + } + + // get analyzers that are not suppressed. + // REVIEW: IsAnalyzerSuppressed call seems can be quite expensive in certain condition. is there any other way to do this? + var stateSets = StateManager.GetOrCreateStateSets(project).Where(s => ShouldIncludeStateSet(project, s)).ToImmutableArrayOrEmpty(); + + var concurrentAnalysis = true; + var analyzerDriver = await Owner._compilationManager.CreateAnalyzerDriverAsync(project, stateSets, concurrentAnalysis, IncludeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + + var result = await Owner._executor.GetProjectAnalysisDataAsync(analyzerDriver, project, stateSets, cancellationToken).ConfigureAwait(false); + + foreach (var stateSet in stateSets) + { + var analysisResult = result.GetResult(stateSet.Analyzer); + + foreach (var documentId in documentIds) + { + AppendDiagnostics(GetResult(analysisResult, AnalysisKind.Syntax, documentId)); + AppendDiagnostics(GetResult(analysisResult, AnalysisKind.Semantic, documentId)); + AppendDiagnostics(GetResult(analysisResult, AnalysisKind.NonLocal, documentId)); + } + + if (targetDocumentId == null) + { + // include project diagnostics if there is no target document + AppendDiagnostics(analysisResult.Others); + } + } + } + + private bool ShouldIncludeStateSet(Project project, StateSet stateSet) + { + // REVIEW: this can be expensive. any way to do this cheaper? + var diagnosticService = Owner.Owner; + if (diagnosticService.IsAnalyzerSuppressed(stateSet.Analyzer, project)) + { + return false; + } + + if (_diagnosticIds != null && diagnosticService.GetDiagnosticDescriptors(stateSet.Analyzer).All(d => !_diagnosticIds.Contains(d.Id))) + { + return false; + } + + return true; + } + + protected override async Task?> GetDiagnosticsAsync(StateSet stateSet, Project project, DocumentId documentId, AnalysisKind kind, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var concurrentAnalysis = true; + var stateSets = SpecializedCollections.SingletonCollection(stateSet); + var analyzerDriver = await Owner._compilationManager.CreateAnalyzerDriverAsync(project, stateSets, concurrentAnalysis, IncludeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + + if (documentId != null) + { + var document = Solution.GetDocument(documentId); + Contract.ThrowIfNull(document); + + switch (kind) + { + case AnalysisKind.Syntax: + case AnalysisKind.Semantic: + { + var result = await Owner._executor.GetDocumentAnalysisDataAsync(analyzerDriver, document, stateSet, kind, cancellationToken).ConfigureAwait(false); + return result.Items; + } + case AnalysisKind.NonLocal: + { + var nonLocalDocumentResult = await Owner._executor.GetProjectAnalysisDataAsync(analyzerDriver, project, stateSets, cancellationToken).ConfigureAwait(false); + var analysisResult = nonLocalDocumentResult.GetResult(stateSet.Analyzer); + return GetResult(analysisResult, AnalysisKind.NonLocal, documentId); + } + default: + return Contract.FailWithReturn?>("shouldn't reach here"); + } + } + + Contract.ThrowIfFalse(kind == AnalysisKind.NonLocal); + var projectResult = await Owner._executor.GetProjectAnalysisDataAsync(analyzerDriver, project, stateSets, cancellationToken).ConfigureAwait(false); + return projectResult.GetResult(stateSet.Analyzer).Others; + } } } } \ No newline at end of file diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs index e18b7d8c9651f..9b2daea2af9d5 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs @@ -1,27 +1,426 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +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.LanguageServices; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 { - // TODO: we need to do what V1 does for LB for span. internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer { public override async Task TryAppendDiagnosticsForSpanAsync(Document document, TextSpan range, List result, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) { - result.AddRange(await GetDiagnosticsForSpanAsync(document, range, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false)); - return true; + var blockForData = false; + var getter = await LatestDiagnosticsForSpanGetter.CreateAsync(this, document, range, blockForData, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + return await getter.TryGetAsync(result, cancellationToken).ConfigureAwait(false); } public override async Task> GetDiagnosticsForSpanAsync(Document document, TextSpan range, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) { - var diagnostics = await GetDiagnosticsAsync(document.Project.Solution, document.Project.Id, document.Id, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return diagnostics.Where(d => range.IntersectsWith(d.TextSpan)); + var blockForData = true; + var getter = await LatestDiagnosticsForSpanGetter.CreateAsync(this, document, range, blockForData, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + + var list = new List(); + var result = await getter.TryGetAsync(list, cancellationToken).ConfigureAwait(false); + Contract.Requires(result); + + return list; + } + + /// + /// Get diagnostics for given span either by using cache or calculating it on the spot. + /// + private class LatestDiagnosticsForSpanGetter + { + private readonly DiagnosticIncrementalAnalyzer _owner; + private readonly Project _project; + private readonly Document _document; + + private readonly IEnumerable _stateSets; + private readonly CompilationWithAnalyzers _analyzerDriver; + private readonly DiagnosticAnalyzer _compilerAnalyzer; + + private readonly TextSpan _range; + private readonly bool _blockForData; + private readonly bool _includeSuppressedDiagnostics; + + // cache of project result + private ImmutableDictionary _projectResultCache; + + private delegate Task> DiagnosticsGetterAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken); + + public static async Task CreateAsync( + DiagnosticIncrementalAnalyzer owner, Document document, TextSpan range, bool blockForData, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) + { + var concurrentAnalysis = false; + var stateSets = owner._stateManager.GetOrCreateStateSets(document.Project); + var analyzerDriver = await owner._compilationManager.CreateAnalyzerDriverAsync(document.Project, stateSets, concurrentAnalysis, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + + return new LatestDiagnosticsForSpanGetter(owner, document, stateSets, analyzerDriver, range, blockForData, includeSuppressedDiagnostics); + } + + private LatestDiagnosticsForSpanGetter( + DiagnosticIncrementalAnalyzer owner, + Document document, + IEnumerable stateSets, + CompilationWithAnalyzers analyzerDriver, + TextSpan range, bool blockForData, bool includeSuppressedDiagnostics) + { + _owner = owner; + + _project = document.Project; + _document = document; + + _stateSets = stateSets; + _analyzerDriver = analyzerDriver; + _compilerAnalyzer = _owner.HostAnalyzerManager.GetCompilerDiagnosticAnalyzer(_document.Project.Language); + + _range = range; + _blockForData = blockForData; + _includeSuppressedDiagnostics = includeSuppressedDiagnostics; + } + + public async Task TryGetAsync(List list, CancellationToken cancellationToken) + { + try + { + var containsFullResult = true; + foreach (var stateSet in _stateSets) + { + containsFullResult &= await TryGetSyntaxAndSemanticDiagnosticsAsync(stateSet, list, cancellationToken).ConfigureAwait(false); + + // check whether compilation end code fix is enabled + if (!_document.Project.Solution.Workspace.Options.GetOption(InternalDiagnosticsOptions.CompilationEndCodeFix)) + { + continue; + } + + // check whether heuristic is enabled + if (_blockForData && _document.Project.Solution.Workspace.Options.GetOption(InternalDiagnosticsOptions.UseCompilationEndCodeFixHeuristic)) + { + var avoidLoadingData = true; + var state = stateSet.GetProjectState(_project.Id); + var result = await state.GetAnalysisDataAsync(_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; + } + } + + containsFullResult &= await TryGetProjectDiagnosticsAsync(stateSet, GetProjectDiagnosticsAsync, list, cancellationToken).ConfigureAwait(false); + } + + // if we are blocked for data, then we should always have full result. + Contract.Requires(!_blockForData || containsFullResult); + return containsFullResult; + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + + private async Task TryGetSyntaxAndSemanticDiagnosticsAsync(StateSet stateSet, List 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 == _compilerAnalyzer) + { + return await TryGetSyntaxAndSemanticCompilerDiagnostics(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 TryGetSyntaxAndSemanticCompilerDiagnostics(StateSet stateSet, List 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); + var diagnostics = root.GetDiagnostics(); + + return _owner._executor.ConvertToLocalDiagnostics(_document, diagnostics, _range); + } + + private async Task> GetCompilerSemanticDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) + { + var model = await _document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + VerifyDiagnostics(model); + + var root = await _document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var adjustedSpan = AdjustSpan(_document, root, _range); + var diagnostics = model.GetDeclarationDiagnostics(adjustedSpan, cancellationToken).Concat(model.GetMethodBodyDiagnostics(adjustedSpan, cancellationToken)); + + return _owner._executor.ConvertToLocalDiagnostics(_document, diagnostics, _range); + } + + private Task> GetSyntaxDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) + { + return _owner._executor.ComputeDiagnosticsAsync(_analyzerDriver, _document, analyzer, AnalysisKind.Syntax, _range, cancellationToken); + } + + private Task> GetSemanticDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) + { + var supportsSemanticInSpan = analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis(); + + var analysisSpan = supportsSemanticInSpan ? (TextSpan?)_range : null; + return _owner._executor.ComputeDiagnosticsAsync(_analyzerDriver, _document, analyzer, AnalysisKind.Semantic, analysisSpan, cancellationToken); + } + + private async Task> GetProjectDiagnosticsAsync(DiagnosticAnalyzer analyzer, CancellationToken cancellationToken) + { + if (_projectResultCache == null) + { + // execute whole project as one shot and cache the result. + _projectResultCache = await _owner._executor.ComputeDiagnosticsAsync(_analyzerDriver, _project, _stateSets, cancellationToken).ConfigureAwait(false); + } + + AnalysisResult result; + if (!_projectResultCache.TryGetValue(analyzer, out result)) + { + return ImmutableArray.Empty; + } + + return GetResult(result, AnalysisKind.NonLocal, _document.Id); + } + + [Conditional("DEBUG")] + private void VerifyDiagnostics(SemanticModel model) + { +#if DEBUG + // Exclude unused import diagnostics since they are never reported when a span is passed. + // (See CSharp/VisualBasicCompilation.GetDiagnosticsForMethodBodiesInTree.) + Func shouldInclude = 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); + } +#endif + } + + private static bool IsUnusedImportDiagnostic(Diagnostic d) + { + switch (d.Id) + { + case "CS8019": + case "BC50000": + case "BC50001": + return true; + default: + return false; + } + } + + 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.GetLanguageService(); + 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( + StateSet stateSet, + AnalysisKind kind, + DiagnosticsGetterAsync diagnosticGetterAsync, + List list, + CancellationToken cancellationToken) + { + // REVIEW: IsAnalyzerSuppressed can be quite expensive in some cases. try to find a way to make it cheaper + if (!_owner.SupportAnalysisKind(stateSet.Analyzer, stateSet.Language, kind) || + _owner.Owner.IsAnalyzerSuppressed(stateSet.Analyzer, _document.Project)) + { + return true; + } + + // 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.GetActiveFileState(_document.Id); + + // see whether we can use existing info + var existingData = state.GetAnalysisData(kind); + var version = await GetDiagnosticVersionAsync(_document.Project, cancellationToken).ConfigureAwait(false); + if (existingData.Version == version) + { + if (existingData.Items.IsEmpty) + { + return true; + } + + list.AddRange(existingData.Items.Where(ShouldInclude)); + return true; + } + + // 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; + } + + private async Task TryGetProjectDiagnosticsAsync( + StateSet stateSet, + DiagnosticsGetterAsync diagnosticGetterAsync, + List list, + CancellationToken cancellationToken) + { + // REVIEW: IsAnalyzerSuppressed can be quite expensive in some cases. try to find a way to make it cheaper + if (!stateSet.Analyzer.SupportsProjectDiagnosticAnalysis() || + _owner.Owner.IsAnalyzerSuppressed(stateSet.Analyzer, _document.Project)) + { + return true; + } + + // 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.GetProjectState(_document.Project.Id); + + // see whether we can use existing info + var result = await state.GetAnalysisDataAsync(_document, avoidLoadingData: true, cancellationToken: cancellationToken).ConfigureAwait(false); + var version = await GetDiagnosticVersionAsync(_document.Project, cancellationToken).ConfigureAwait(false); + if (result.Version == version) + { + var existingData = GetResult(result, AnalysisKind.NonLocal, _document.Id); + if (existingData.IsEmpty) + { + return true; + } + + list.AddRange(existingData.Where(ShouldInclude)); + return true; + } + + // 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 diagnosticGetterAsync(stateSet.Analyzer, cancellationToken).ConfigureAwait(false); + if (dx != null) + { + // no state yet + list.AddRange(dx.Where(ShouldInclude)); + } + + return true; + } + + private bool ShouldInclude(DiagnosticData diagnostic) + { + return diagnostic.DocumentId == _document.Id && _range.IntersectsWith(diagnostic.TextSpan) && (_includeSuppressedDiagnostics || !diagnostic.IsSuppressed); + } + + 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) + { + return x.Id == y.Id && x.Location == y.Location; + } + + public int GetHashCode(Diagnostic obj) + { + 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 4ab5d95895d67..6e2eb81973a55 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 @@ -12,139 +15,403 @@ namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 // TODO: make it to use cache internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer { - public async override Task AnalyzeSyntaxAsync(Document document, CancellationToken cancellationToken) + public override Task AnalyzeSyntaxAsync(Document document, CancellationToken cancellationToken) { - if (!AnalysisEnabled(document)) - { - return; - } + return AnalyzeDocumentForKindAsync(document, AnalysisKind.Syntax, cancellationToken); + } - // TODO: make active file state to cache compilationWithAnalyzer - // REVIEW: this is really wierd that we need compilation for syntax diagnostics which basically defeat any reason - // we have syntax diagnostics. - var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + public override Task AnalyzeDocumentAsync(Document document, SyntaxNode bodyOpt, CancellationToken cancellationToken) + { + return AnalyzeDocumentForKindAsync(document, AnalysisKind.Semantic, cancellationToken); + } - // TODO: make it to use state manager - var analyzers = HostAnalyzerManager.CreateDiagnosticAnalyzers(document.Project); + private async Task AnalyzeDocumentForKindAsync(Document document, AnalysisKind kind, CancellationToken cancellationToken) + { + try + { + if (!AnalysisEnabled(document)) + { + // to reduce allocations, here, we don't clear existing diagnostics since it is dealt by other entry point such as + // DocumentReset or DocumentClosed. + return; + } - // Create driver that holds onto compilation and associated analyzers - // TODO: use CompilationWithAnalyzerOption instead of AnalyzerOption so that we can have exception filter and etc - var analyzerDriver = compilation.WithAnalyzers(analyzers, document.Project.AnalyzerOptions, cancellationToken); + var stateSets = _stateManager.GetOrUpdateStateSets(document.Project); + var analyzerDriver = await _compilationManager.GetAnalyzerDriverAsync(document.Project, stateSets, cancellationToken).ConfigureAwait(false); - foreach (var analyzer in analyzers) - { - // TODO: implement perf optimization not to run analyzers that are not needed. - // REVIEW: more unnecessary allocations just to get diagnostics per analyzer - var oneAnalyzers = ImmutableArray.Create(analyzer); + foreach (var stateSet in stateSets) + { + var analyzer = stateSet.Analyzer; - // TODO: use cache for perf optimization - var diagnostics = await analyzerDriver.GetAnalyzerSyntaxDiagnosticsAsync(tree, oneAnalyzers, cancellationToken).ConfigureAwait(false); + var result = await _executor.GetDocumentAnalysisDataAsync(analyzerDriver, document, stateSet, kind, cancellationToken).ConfigureAwait(false); + if (result.FromCache) + { + RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, result.Items); + continue; + } - // we only care about local diagnostics - var diagnosticData = GetDiagnosticData(document.Project, diagnostics).Where(d => d.DocumentId == document.Id); + // no cancellation after this point. + var state = stateSet.GetActiveFileState(document.Id); + state.Save(kind, result.ToPersistData()); - // TODO: update using right arguments - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsCreated( - ValueTuple.Create(this, "Syntax", document.Id), document.Project.Solution.Workspace, document.Project.Solution, document.Project.Id, document.Id, diagnosticData.ToImmutableArrayOrEmpty())); + RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, result.OldItems, result.Items); + } + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; } } - public override async Task AnalyzeDocumentAsync(Document document, SyntaxNode bodyOpt, CancellationToken cancellationToken) + public override async Task AnalyzeProjectAsync(Project project, bool semanticsChanged, CancellationToken cancellationToken) { - if (!AnalysisEnabled(document)) + try { - return; - } + var stateSets = _stateManager.GetOrUpdateStateSets(project); - // TODO: make active file state to cache compilationWithAnalyzer - var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + // get analyzers that are not suppressed. + // REVIEW: IsAnalyzerSuppressed call seems can be quite expensive in certain condition. is there any other way to do this? + var activeAnalyzers = stateSets.Select(s => s.Analyzer).Where(a => !Owner.IsAnalyzerSuppressed(a, project)).ToImmutableArrayOrEmpty(); - // TODO: make it to use state manager - var analyzers = HostAnalyzerManager.CreateDiagnosticAnalyzers(document.Project); + // get driver only with active analyzers. + var includeSuppressedDiagnostics = true; + var analyzerDriver = await _compilationManager.CreateAnalyzerDriverAsync(project, activeAnalyzers, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - // Create driver that holds onto compilation and associated analyzers - // TODO: use CompilationWithAnalyzerOption instead of AnalyzerOption so that we can have exception filter and etc - var analyzerDriver = compilation.WithAnalyzers(analyzers, document.Project.AnalyzerOptions, cancellationToken); + var result = await _executor.GetProjectAnalysisDataAsync(analyzerDriver, project, stateSets, cancellationToken).ConfigureAwait(false); + if (result.FromCache) + { + RaiseProjectDiagnosticsIfNeeded(project, stateSets, result.Result); + return; + } - var noSpanFilter = default(TextSpan?); - foreach (var analyzer in analyzers) - { - // REVIEW: more unnecessary allocations just to get diagnostics per analyzer - var oneAnalyzers = ImmutableArray.Create(analyzer); + // no cancellation after this point. + foreach (var stateSet in stateSets) + { + var state = stateSet.GetProjectState(project.Id); + await state.SaveAsync(project, result.GetResult(stateSet.Analyzer)).ConfigureAwait(false); + } - // TODO: use cache for perf optimization - // REVIEW: I think we don't even need member tracking optimization - var diagnostics = await analyzerDriver.GetAnalyzerSemanticDiagnosticsAsync(model, noSpanFilter, oneAnalyzers, cancellationToken).ConfigureAwait(false); + RaiseProjectDiagnosticsIfNeeded(project, stateSets, result.OldResult, result.Result); + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } - var diagnosticData = GetDiagnosticData(document.Project, diagnostics).Where(d => d.DocumentId == document.Id); + public override Task DocumentOpenAsync(Document document, CancellationToken cancellationToken) + { + // let other component knows about this event + _compilationManager.OnDocumentOpened(); - // TODO: update using right arguments - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsCreated( - ValueTuple.Create(this, "Semantic", document.Id), document.Project.Solution.Workspace, document.Project.Solution, document.Project.Id, document.Id, diagnosticData.ToImmutableArrayOrEmpty())); - } + // here we dont need to raise any event, it will be taken cared by analyze methods. + return SpecializedTasks.EmptyTask; } public override Task DocumentCloseAsync(Document document, CancellationToken cancellationToken) { - // TODO: at this event, if file being closed is active file (the one in ActiveFileState), we should put that data into - // ProjectState + var stateSets = _stateManager.GetStateSets(document.Project); + + // let other components knows about this event + _compilationManager.OnDocumentClosed(); + var changed = _stateManager.OnDocumentClosed(stateSets, document.Id); + + // replace diagnostics from project state over active file state + RaiseLocalDocumentEventsFromProjectOverActiveFile(stateSets, document, changed); + return SpecializedTasks.EmptyTask; } public override Task DocumentResetAsync(Document document, CancellationToken cancellationToken) { - // REVIEW: this should reset both active file and project state the document belong to. + var stateSets = _stateManager.GetStateSets(document.Project); + + // let other components knows about this event + _compilationManager.OnDocumentReset(); + var changed = _stateManager.OnDocumentReset(stateSets, document.Id); + + // replace diagnostics from project state over active file state + RaiseLocalDocumentEventsFromProjectOverActiveFile(stateSets, document, changed); + return SpecializedTasks.EmptyTask; } public override void RemoveDocument(DocumentId documentId) { - // TODO: do proper eventing - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsRemoved( - ValueTuple.Create(this, "Syntax", documentId), Workspace, null, null, null)); + var stateSets = _stateManager.GetStateSets(documentId.ProjectId); - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsRemoved( - ValueTuple.Create(this, "Semantic", documentId), Workspace, null, null, null)); + // let other components knows about this event + _compilationManager.OnDocumentRemoved(); + var changed = _stateManager.OnDocumentRemoved(stateSets, documentId); - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsRemoved( - ValueTuple.Create(this, documentId), Workspace, null, null, null)); + // if there was no diagnostic reported for this document, nothing to clean up + if (!changed) + { + // this is Perf to reduce raising events unnecessarily. + return; + } + + // remove all diagnostics for the document + Owner.RaiseBulkDiagnosticsUpdated(raiseEvents => + { + Solution nullSolution = null; + foreach (var stateSet in stateSets) + { + // clear all doucment diagnostics + RaiseDiagnosticsRemoved(documentId, nullSolution, stateSet, AnalysisKind.Syntax, raiseEvents); + RaiseDiagnosticsRemoved(documentId, nullSolution, stateSet, AnalysisKind.Semantic, raiseEvents); + RaiseDiagnosticsRemoved(documentId, nullSolution, stateSet, AnalysisKind.NonLocal, raiseEvents); + } + }); } - public override async Task AnalyzeProjectAsync(Project project, bool semanticsChanged, CancellationToken cancellationToken) + public override void RemoveProject(ProjectId projectId) { - if (!FullAnalysisEnabled(project.Solution.Workspace, project.Language)) + var stateSets = _stateManager.GetStateSets(projectId); + + // let other components knows about this event + _compilationManager.OnProjectRemoved(); + var changed = _stateManager.OnProjectRemoved(stateSets, projectId); + + // if there was no diagnostic reported for this project, nothing to clean up + if (!changed) { - // TODO: check whether there is existing state, if so, raise events to remove them all. + // this is Perf to reduce raising events unnecessarily. return; } - // TODO: make this to use cache. - // TODO: use CompilerDiagnosticExecutor - var diagnostics = await GetDiagnosticsAsync(project.Solution, project.Id, null, includeSuppressedDiagnostics: true, cancellationToken: cancellationToken).ConfigureAwait(false); + // remove all diagnostics for the project + Owner.RaiseBulkDiagnosticsUpdated(raiseEvents => + { + Solution nullSolution = null; + foreach (var stateSet in stateSets) + { + // clear all project diagnostics + RaiseDiagnosticsRemoved(projectId, nullSolution, stateSet, raiseEvents); + } + }); + } + + public override Task NewSolutionSnapshotAsync(Solution solution, CancellationToken cancellationToken) + { + // let other components knows about this event + _compilationManager.OnNewSolution(); - // TODO: do proper event - RaiseEvents(project, diagnostics); + return SpecializedTasks.EmptyTask; } - public override void RemoveProject(ProjectId projectId) + private static bool AnalysisEnabled(Document document) { - // TODO: do proper event - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsRemoved( - ValueTuple.Create(this, projectId), Workspace, null, null, null)); + // change it to check active file (or visible files), not open files if active file tracking is enabled. + // otherwise, use open file. + return document.IsOpen(); } - public override Task DocumentOpenAsync(Document document, CancellationToken cancellationToken) + private static ImmutableArray GetResult(AnalysisResult result, AnalysisKind kind, DocumentId id) { - // Review: I think we don't need to care about it - return SpecializedTasks.EmptyTask; + switch (kind) + { + case AnalysisKind.Syntax: + return result.GetResultOrEmpty(result.SyntaxLocals, id); + case AnalysisKind.Semantic: + return result.GetResultOrEmpty(result.SemanticLocals, id); + case AnalysisKind.NonLocal: + return result.GetResultOrEmpty(result.NonLocals, id); + default: + return Contract.FailWithReturn>("shouldn't reach here"); + } } - public override Task NewSolutionSnapshotAsync(Solution solution, CancellationToken cancellationToken) + private void RaiseLocalDocumentEventsFromProjectOverActiveFile(IEnumerable stateSets, Document document, bool activeFileDiagnosticExist) { - // we don't use this one. - return SpecializedTasks.EmptyTask; + // PERF: activeFileDiagnosticExist is perf optimization to reduce raising events unnecessarily. + + // this removes diagnostic reported by active file and replace those with ones from project. + Owner.RaiseBulkDiagnosticsUpdated(async raiseEvents => + { + // this basically means always load data + var avoidLoadingData = false; + + foreach (var stateSet in stateSets) + { + // get project state + var state = stateSet.GetProjectState(document.Project.Id); + + // this is perf optimization to reduce events; + if (!activeFileDiagnosticExist && state.IsEmpty(document.Id)) + { + // there is nothing reported before. we don't need to do anything. + continue; + } + + // no cancellation since event can't be cancelled. + // now get diagnostic information from project + var result = await state.GetAnalysisDataAsync(document, avoidLoadingData, CancellationToken.None).ConfigureAwait(false); + if (result.IsAggregatedForm) + { + // something made loading data failed. + // clear all existing diagnostics + RaiseDiagnosticsRemoved(document.Id, document.Project.Solution, stateSet, AnalysisKind.Syntax, raiseEvents); + RaiseDiagnosticsRemoved(document.Id, document.Project.Solution, stateSet, AnalysisKind.Semantic, raiseEvents); + continue; + } + + // we have data, do actual event raise that will replace diagnostics from active file + var syntaxItems = GetResult(result, AnalysisKind.Syntax, document.Id); + RaiseDiagnosticsCreated(document, stateSet, AnalysisKind.Syntax, syntaxItems, raiseEvents); + + var semanticItems = GetResult(result, AnalysisKind.Semantic, document.Id); + RaiseDiagnosticsCreated(document, stateSet, AnalysisKind.Semantic, semanticItems, raiseEvents); + } + }); + } + + private void RaiseProjectDiagnosticsIfNeeded( + Project project, + IEnumerable stateSets, + ImmutableDictionary result) + { + RaiseProjectDiagnosticsIfNeeded(project, stateSets, ImmutableDictionary.Empty, result); + } + + private void RaiseProjectDiagnosticsIfNeeded( + Project project, + IEnumerable stateSets, + ImmutableDictionary oldResult, + ImmutableDictionary newResult) + { + if (oldResult.Count == 0 && newResult.Count == 0) + { + // there is nothing to update + return; + } + + Owner.RaiseBulkDiagnosticsUpdated(raiseEvents => + { + foreach (var stateSet in stateSets) + { + var analyzer = stateSet.Analyzer; + + var oldAnalysisResult = ImmutableDictionary.GetValueOrDefault(oldResult, analyzer); + var newAnalysisResult = ImmutableDictionary.GetValueOrDefault(newResult, analyzer); + + // Perf - 4 different cases. + // upper 3 cases can be removed and it will still work. but this is hot path so if we can bail out + // without any allocations, that's better. + if (oldAnalysisResult.IsEmpty && newAnalysisResult.IsEmpty) + { + // nothing to do + continue; + } + + if (!oldAnalysisResult.IsEmpty && newAnalysisResult.IsEmpty) + { + // remove old diagnostics + RaiseProjectDiagnosticsRemoved(stateSet, oldAnalysisResult.ProjectId, oldAnalysisResult.DocumentIds, raiseEvents); + continue; + } + + if (oldAnalysisResult.IsEmpty && !newAnalysisResult.IsEmpty) + { + // add new diagnostics + RaiseProjectDiagnosticsCreated(project, stateSet, oldAnalysisResult, newAnalysisResult, raiseEvents); + continue; + } + + // both old and new has items in them. update existing items + + // first remove ones no longer needed. + var documentsToRemove = oldAnalysisResult.DocumentIds.Except(newAnalysisResult.DocumentIds); + RaiseProjectDiagnosticsRemoved(stateSet, oldAnalysisResult.ProjectId, documentsToRemove, raiseEvents); + + // next update or create new ones + RaiseProjectDiagnosticsCreated(project, stateSet, oldAnalysisResult, newAnalysisResult, raiseEvents); + } + }); + } + + private void RaiseDocumentDiagnosticsIfNeeded(Document document, StateSet stateSet, AnalysisKind kind, ImmutableArray items) + { + RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, ImmutableArray.Empty, items); + } + + private void RaiseDocumentDiagnosticsIfNeeded( + Document document, StateSet stateSet, AnalysisKind kind, ImmutableArray oldItems, ImmutableArray newItems) + { + RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, oldItems, newItems, Owner.RaiseDiagnosticsUpdated); + } + + private void RaiseDocumentDiagnosticsIfNeeded( + Document document, StateSet stateSet, AnalysisKind kind, + AnalysisResult oldResult, AnalysisResult newResult, + Action raiseEvents) + { + var oldItems = GetResult(oldResult, kind, document.Id); + var newItems = GetResult(newResult, kind, document.Id); + + RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, oldItems, newItems, raiseEvents); + } + + private void RaiseDocumentDiagnosticsIfNeeded( + Document document, StateSet stateSet, AnalysisKind kind, + ImmutableArray oldItems, ImmutableArray newItems, + Action raiseEvents) + { + if (oldItems.IsEmpty && newItems.IsEmpty) + { + // there is nothing to update + return; + } + + RaiseDiagnosticsCreated(document, stateSet, kind, newItems, raiseEvents); + } + + private void RaiseProjectDiagnosticsCreated(Project project, StateSet stateSet, AnalysisResult oldAnalysisResult, AnalysisResult newAnalysisResult, Action raiseEvents) + { + foreach (var documentId in newAnalysisResult.DocumentIds) + { + var document = project.GetDocument(documentId); + Contract.ThrowIfNull(document); + + RaiseDocumentDiagnosticsIfNeeded(document, stateSet, AnalysisKind.NonLocal, oldAnalysisResult, newAnalysisResult, raiseEvents); + + // we don't raise events for active file. it will be taken cared by active file analysis + if (stateSet.IsActiveFile(documentId)) + { + continue; + } + + RaiseDocumentDiagnosticsIfNeeded(document, stateSet, AnalysisKind.Syntax, oldAnalysisResult, newAnalysisResult, raiseEvents); + RaiseDocumentDiagnosticsIfNeeded(document, stateSet, AnalysisKind.Semantic, oldAnalysisResult, newAnalysisResult, raiseEvents); + } + + RaiseDiagnosticsCreated(project, stateSet, newAnalysisResult.Others, raiseEvents); + } + + private void RaiseProjectDiagnosticsRemoved(StateSet stateSet, ProjectId projectId, IEnumerable documentIds, Action raiseEvents) + { + var handleActiveFile = false; + RaiseProjectDiagnosticsRemoved(stateSet, projectId, documentIds, handleActiveFile, raiseEvents); + } + + private void RaiseProjectDiagnosticsRemoved(StateSet stateSet, ProjectId projectId, IEnumerable documentIds, bool handleActiveFile, Action raiseEvents) + { + Solution nullSolution = null; + foreach (var documentId in documentIds) + { + RaiseDiagnosticsRemoved(documentId, nullSolution, stateSet, AnalysisKind.NonLocal, raiseEvents); + + // we don't raise events for active file. it will be taken cared by active file analysis + if (!handleActiveFile && stateSet.IsActiveFile(documentId)) + { + continue; + } + + RaiseDiagnosticsRemoved(documentId, nullSolution, stateSet, AnalysisKind.Syntax, raiseEvents); + RaiseDiagnosticsRemoved(documentId, nullSolution, stateSet, AnalysisKind.Semantic, raiseEvents); + } + + RaiseDiagnosticsRemoved(projectId, nullSolution, stateSet, raiseEvents); } } } diff --git a/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.cs b/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.cs index 150a54ddf9e6c..354163b2ef1c0 100644 --- a/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.cs +++ b/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.cs @@ -11,14 +11,11 @@ internal class LiveDiagnosticUpdateArgsId : AnalyzerUpdateArgsId public readonly object Key; public readonly int Kind; - public LiveDiagnosticUpdateArgsId(DiagnosticAnalyzer analyzer, object key, int kind) : - this(analyzer, key, kind, analyzerPackageName: null) - { - } - public LiveDiagnosticUpdateArgsId(DiagnosticAnalyzer analyzer, object key, int kind, string analyzerPackageName) : base(analyzer) { + Contract.ThrowIfNull(key); + Key = key; Kind = kind; diff --git a/src/Features/Core/Portable/Features.csproj b/src/Features/Core/Portable/Features.csproj index 4329b0d062201..eaaf2ff3737da 100644 --- a/src/Features/Core/Portable/Features.csproj +++ b/src/Features/Core/Portable/Features.csproj @@ -197,6 +197,18 @@ + + + + + + + + + + + + diff --git a/src/VisualStudio/Core/Def/Implementation/TaskList/ExternalErrorDiagnosticUpdateSource.cs b/src/VisualStudio/Core/Def/Implementation/TaskList/ExternalErrorDiagnosticUpdateSource.cs index 1d73e1507af9e..2c087d11ad81d 100644 --- a/src/VisualStudio/Core/Def/Implementation/TaskList/ExternalErrorDiagnosticUpdateSource.cs +++ b/src/VisualStudio/Core/Def/Implementation/TaskList/ExternalErrorDiagnosticUpdateSource.cs @@ -203,13 +203,8 @@ internal void OnSolutionBuild(object sender, UIContextChangedEventArgs e) var diagnosticService = _diagnosticService as DiagnosticAnalyzerService; if (diagnosticService != null) { - using (var batchUpdateToken = diagnosticService.BeginBatchBuildDiagnosticsUpdate(solution)) - { - await CleanupAllLiveErrorsIfNeededAsync(diagnosticService, batchUpdateToken, solution, inprogressState).ConfigureAwait(false); - - await SyncBuildErrorsAndReportAsync(diagnosticService, batchUpdateToken, solution, liveDiagnosticChecker, inprogressState.GetDocumentAndErrors(solution)).ConfigureAwait(false); - await SyncBuildErrorsAndReportAsync(diagnosticService, batchUpdateToken, solution, liveDiagnosticChecker, inprogressState.GetProjectAndErrors(solution)).ConfigureAwait(false); - } + await CleanupAllLiveErrorsIfNeededAsync(diagnosticService, solution, inprogressState).ConfigureAwait(false); + await SyncBuildErrorsAndReportAsync(diagnosticService, inprogressState.GetLiveDiagnosticsPerProject(liveDiagnosticChecker)).ConfigureAwait(false); } inprogressState.Done(); @@ -217,90 +212,66 @@ internal void OnSolutionBuild(object sender, UIContextChangedEventArgs e) }).CompletesAsyncOperation(asyncToken); } - private async System.Threading.Tasks.Task CleanupAllLiveErrorsIfNeededAsync( - DiagnosticAnalyzerService diagnosticService, IDisposable batchUpdateToken, - Solution solution, InprogressState state) + private async System.Threading.Tasks.Task CleanupAllLiveErrorsIfNeededAsync(DiagnosticAnalyzerService diagnosticService, Solution solution, InprogressState state) { if (_workspace.Options.GetOption(InternalDiagnosticsOptions.BuildErrorIsTheGod)) { - await CleanupAllLiveErrors(diagnosticService, batchUpdateToken, solution, state, solution.Projects).ConfigureAwait(false); + await CleanupAllLiveErrors(diagnosticService, solution.ProjectIds).ConfigureAwait(false); return; } if (_workspace.Options.GetOption(InternalDiagnosticsOptions.ClearLiveErrorsForProjectBuilt)) { - await CleanupAllLiveErrors(diagnosticService, batchUpdateToken, solution, state, state.GetProjectsBuilt(solution)).ConfigureAwait(false); + await CleanupAllLiveErrors(diagnosticService, state.GetProjectsBuilt(solution)).ConfigureAwait(false); return; } - await CleanupAllLiveErrors(diagnosticService, batchUpdateToken, solution, state, state.GetProjectsWithoutErrors(solution)).ConfigureAwait(false); + await CleanupAllLiveErrors(diagnosticService, state.GetProjectsWithoutErrors(solution)).ConfigureAwait(false); return; } - private static async System.Threading.Tasks.Task CleanupAllLiveErrors( - DiagnosticAnalyzerService diagnosticService, IDisposable batchUpdateToken, - Solution solution, InprogressState state, IEnumerable projects) + private System.Threading.Tasks.Task CleanupAllLiveErrors(DiagnosticAnalyzerService diagnosticService, IEnumerable projects) { - foreach (var project in projects) - { - foreach (var document in project.Documents) - { - await SynchronizeWithBuildAsync(diagnosticService, batchUpdateToken, document, ImmutableArray.Empty).ConfigureAwait(false); - } - - await SynchronizeWithBuildAsync(diagnosticService, batchUpdateToken, project, ImmutableArray.Empty).ConfigureAwait(false); - } + var map = projects.ToImmutableDictionary(p => p, _ => ImmutableArray.Empty); + return diagnosticService.SynchronizeWithBuildAsync(_workspace, map); } - private async System.Threading.Tasks.Task SyncBuildErrorsAndReportAsync( - DiagnosticAnalyzerService diagnosticService, IDisposable batchUpdateToken, Solution solution, - Func liveDiagnosticChecker, IEnumerable>> items) + private async System.Threading.Tasks.Task SyncBuildErrorsAndReportAsync(DiagnosticAnalyzerService diagnosticService, ImmutableDictionary> map) { - foreach (var kv in items) - { - // get errors that can be reported by live diagnostic analyzer - var liveErrors = kv.Value.Where(liveDiagnosticChecker).ToImmutableArray(); - - // make those errors live errors - await SynchronizeWithBuildAsync(diagnosticService, batchUpdateToken, kv.Key, liveErrors).ConfigureAwait(false); + // make those errors live errors + await diagnosticService.SynchronizeWithBuildAsync(_workspace, map).ConfigureAwait(false); - // raise events for ones left-out - if (liveErrors.Length != kv.Value.Count) + // raise events for ones left-out + var buildErrors = GetBuildErrors().Except(map.Values.SelectMany(v => v)).GroupBy(k => k.DocumentId); + foreach (var group in buildErrors) + { + if (group.Key == null) { - var buildErrors = kv.Value.Except(liveErrors).ToImmutableArray(); - ReportBuildErrors(kv.Key, buildErrors); + foreach (var projectGroup in group.GroupBy(g => g.ProjectId)) + { + Contract.ThrowIfNull(projectGroup.Key); + ReportBuildErrors(projectGroup.Key, projectGroup.ToImmutableArray()); + } + + continue; } - } - } - private static async System.Threading.Tasks.Task SynchronizeWithBuildAsync( - DiagnosticAnalyzerService diagnosticService, IDisposable batchUpdateToken, - T item, ImmutableArray liveErrors) - { - var project = item as Project; - if (project != null) - { - await diagnosticService.SynchronizeWithBuildAsync(batchUpdateToken, project, liveErrors).ConfigureAwait(false); - return; + ReportBuildErrors(group.Key, group.ToImmutableArray()); } - - // must be not null - var document = item as Document; - await diagnosticService.SynchronizeWithBuildAsync(batchUpdateToken, document, liveErrors).ConfigureAwait(false); } private void ReportBuildErrors(T item, ImmutableArray buildErrors) { - var project = item as Project; - if (project != null) + var projectId = item as ProjectId; + if (projectId != null) { - RaiseDiagnosticsCreated(project.Id, project.Id, null, buildErrors); + RaiseDiagnosticsCreated(projectId, projectId, null, buildErrors); return; } // must be not null - var document = item as Document; - RaiseDiagnosticsCreated(document.Id, document.Project.Id, document.Id, buildErrors); + var documentId = item as DocumentId; + RaiseDiagnosticsCreated(documentId, documentId.ProjectId, documentId, buildErrors); } private Dictionary> GetSupportedLiveDiagnosticId(Solution solution, InprogressState state) @@ -308,8 +279,14 @@ private Dictionary> GetSupportedLiveDiagnosticId(Solu var map = new Dictionary>(); // here, we don't care about perf that much since build is already expensive work - foreach (var project in state.GetProjectsWithErrors(solution)) + foreach (var projectId in state.GetProjectsWithErrors(solution)) { + var project = solution.GetProject(projectId); + if (project == null) + { + continue; + } + var descriptorMap = _diagnosticService.GetDiagnosticDescriptors(project); map.Add(project.Id, new HashSet(descriptorMap.Values.SelectMany(v => v.Select(d => d.Id)))); } @@ -442,56 +419,32 @@ public void Built(ProjectId projectId) _builtProjects.Add(projectId); } - public IEnumerable GetProjectsBuilt(Solution solution) + public IEnumerable GetProjectsBuilt(Solution solution) { - return solution.Projects.Where(p => _builtProjects.Contains(p.Id)); + return solution.ProjectIds.Where(p => _builtProjects.Contains(p)); } - public IEnumerable GetProjectsWithErrors(Solution solution) + public IEnumerable GetProjectsWithErrors(Solution solution) { - foreach (var projectId in _documentMap.Keys.Select(k => k.ProjectId).Concat(_projectMap.Keys).Distinct()) - { - var project = solution.GetProject(projectId); - if (project == null) - { - continue; - } - - yield return project; - } + return _documentMap.Keys.Select(k => k.ProjectId).Concat(_projectMap.Keys).Distinct(); } - public IEnumerable GetProjectsWithoutErrors(Solution solution) + public IEnumerable GetProjectsWithoutErrors(Solution solution) { return GetProjectsBuilt(solution).Except(GetProjectsWithErrors(solution)); } - public IEnumerable>> GetDocumentAndErrors(Solution solution) + public ImmutableDictionary> GetLiveDiagnosticsPerProject(Func liveDiagnosticChecker) { - foreach (var kv in _documentMap) + var builder = ImmutableDictionary.CreateBuilder>(); + foreach (var projectKv in _projectMap) { - var document = solution.GetDocument(kv.Key); - if (document == null) - { - continue; - } - - yield return KeyValuePair.Create(document, kv.Value); + // get errors that can be reported by live diagnostic analyzer + var diagnostics = ImmutableArray.CreateRange(projectKv.Value.Concat(_documentMap.Where(kv => kv.Key.ProjectId == projectKv.Key).SelectMany(kv => kv.Value)).Where(liveDiagnosticChecker)); + builder.Add(projectKv.Key, diagnostics); } - } - - public IEnumerable>> GetProjectAndErrors(Solution solution) - { - foreach (var kv in _projectMap) - { - var project = solution.GetProject(kv.Key); - if (project == null) - { - continue; - } - yield return KeyValuePair.Create(project, kv.Value); - } + return builder.ToImmutable(); } public void AddErrors(DocumentId key, HashSet diagnostics) @@ -511,17 +464,17 @@ public void AddError(DocumentId key, DiagnosticData diagnostic) private void AddErrors(Dictionary> map, T key, HashSet diagnostics) { - var errors = GetErrors(map, key); + var errors = GetErrorSet(map, key); errors.UnionWith(diagnostics); } private void AddError(Dictionary> map, T key, DiagnosticData diagnostic) { - var errors = GetErrors(map, key); + var errors = GetErrorSet(map, key); errors.Add(diagnostic); } - private HashSet GetErrors(Dictionary> map, T key) + private HashSet GetErrorSet(Dictionary> map, T key) { return map.GetOrAdd(key, _ => new HashSet(DiagnosticDataComparer.Instance)); }