From 79f3481d9a3d0371c273a02630cc35d96caa012c Mon Sep 17 00:00:00 2001 From: Heejae Chang Date: Thu, 19 Mar 2015 06:27:28 -0700 Subject: [PATCH] change the way build and live diagnostics are merged previously we made system to prefer live diagnostic over build diagnostic. also when we merge those two, we used very detail info to find out build and live diagnostics are same or not. but the issue was diagnostics from build didn't have all information live one has, also diagnostics from build is static when live one is changing as user type in IDE. which lead to race where build diagnostic might be already out-dated on arrival. new system is that we prefer diagnostic from build over live. and don't distinguish them by detail info but whether live can produce same diagnostics build produced. as user change file, we will replace build diagnostics in affected files to live diagnostics. this should remove chance where build diagnostic got stuck in the system until next build. and as user change files, live diagnostic over take build diagnostics. --- .../CSharpCompilerDiagnosticAnalyzer.cs | 4 + .../CompilerDiagnosticAnalyzer.cs | 2 - .../BaseDiagnosticIncrementalAnalyzer.cs | 26 + .../Diagnostics/DiagnosticAnalyzerService.cs | 5 + ...ticAnalyzerService_BuildSynchronization.cs | 43 ++ ...sticAnalyzerService_IncrementalAnalyzer.cs | 14 +- ...ticIncrementalAnalyzer.AnalyzerExecutor.cs | 1 - ...sticIncrementalAnalyzer.DiagnosticState.cs | 2 +- ...ncrementalAnalyzer_BuildSynchronization.cs | 191 +++++++ .../EngineV2/DiagnosticIncrementalAnalyzer.cs | 18 +- .../Core/Diagnostics/HostAnalyzerManager.cs | 54 +- .../Diagnostics/IDiagnosticAnalyzerService.cs | 5 + .../Diagnostics/InternalDiagnosticsOptions.cs | 6 + src/Features/Core/Features.csproj | 2 + .../ExternalErrorDiagnosticUpdateSource.cs | 471 +++++++++--------- .../ExternalDiagnosticUpdateSourceTests.vb | 64 +-- .../EditAndContinueTestHelper.vb | 4 + 17 files changed, 618 insertions(+), 294 deletions(-) create mode 100644 src/Features/Core/Diagnostics/DiagnosticAnalyzerService_BuildSynchronization.cs create mode 100644 src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilerDiagnosticAnalyzer.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilerDiagnosticAnalyzer.cs index e43de782a38e3..35314fe4b91c6 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilerDiagnosticAnalyzer.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilerDiagnosticAnalyzer.cs @@ -36,6 +36,10 @@ internal override ImmutableArray GetSupportedErrorCodes() // We don't support configuring WRN_ALinkWarn. See comments in method "CSharpDiagnosticFilter.Filter" for more details. continue; + case (int)ErrorCode.WRN_UnreferencedField: + // unused field. current live error doesn't support this. + continue; + default: builder.Add(errorCode); break; diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilerDiagnosticAnalyzer.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilerDiagnosticAnalyzer.cs index 685e985642b6a..1ec4f13747935 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilerDiagnosticAnalyzer.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilerDiagnosticAnalyzer.cs @@ -40,8 +40,6 @@ public sealed override void Initialize(AnalysisContext context) c.RegisterSyntaxTreeAction(analyzer.AnalyzeSyntaxTree); c.RegisterSemanticModelAction(CompilationAnalyzer.AnalyzeSemanticModel); }); - - context.RegisterCompilationAction(CompilationAnalyzer.AnalyzeCompilation); } } } diff --git a/src/Features/Core/Diagnostics/BaseDiagnosticIncrementalAnalyzer.cs b/src/Features/Core/Diagnostics/BaseDiagnosticIncrementalAnalyzer.cs index 9b2e0f70898ec..a04f57a20798c 100644 --- a/src/Features/Core/Diagnostics/BaseDiagnosticIncrementalAnalyzer.cs +++ b/src/Features/Core/Diagnostics/BaseDiagnosticIncrementalAnalyzer.cs @@ -166,6 +166,32 @@ protected BaseDiagnosticIncrementalAnalyzer(DiagnosticAnalyzerService owner, Wor public abstract Task> GetDiagnosticsForSpanAsync(Document document, TextSpan range, CancellationToken cancellationToken); #endregion + #region build error synchronization + /// + /// 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 project wide diagnsotics that doesn't contain a source location. + /// + public abstract Task SynchronizeWithBuildAsync(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(Document document, ImmutableArray diagnostics); + #endregion + internal DiagnosticAnalyzerService Owner { get; } internal Workspace Workspace { get; } internal AbstractHostDiagnosticUpdateSource HostDiagnosticUpdateSource { get; } diff --git a/src/Features/Core/Diagnostics/DiagnosticAnalyzerService.cs b/src/Features/Core/Diagnostics/DiagnosticAnalyzerService.cs index 9fc579677139a..cc31400f8930d 100644 --- a/src/Features/Core/Diagnostics/DiagnosticAnalyzerService.cs +++ b/src/Features/Core/Diagnostics/DiagnosticAnalyzerService.cs @@ -169,6 +169,11 @@ public Task> GetProjectDiagnosticsForIdsAsync( return SpecializedTasks.EmptyImmutableArray(); } + public bool IsCompilerDiagnostic(string language, DiagnosticData diagnostic) + { + return _hostAnalyzerManager.IsCompilerDiagnostic(language, diagnostic); + } + // virtual for testing purposes. internal virtual Action GetOnAnalyzerException(ProjectId projectId, DiagnosticLogAggregator diagnosticLogAggregator) { diff --git a/src/Features/Core/Diagnostics/DiagnosticAnalyzerService_BuildSynchronization.cs b/src/Features/Core/Diagnostics/DiagnosticAnalyzerService_BuildSynchronization.cs new file mode 100644 index 0000000000000..83df151081666 --- /dev/null +++ b/src/Features/Core/Diagnostics/DiagnosticAnalyzerService_BuildSynchronization.cs @@ -0,0 +1,43 @@ +// 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 System.Threading.Tasks; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics +{ + internal partial class DiagnosticAnalyzerService + { + /// + /// Synchronize build errors with live error. + /// + /// no cancellationToken since this can't be cancelled + /// + public Task SynchronizeWithBuildAsync(Project project, ImmutableArray diagnostics) + { + BaseDiagnosticIncrementalAnalyzer analyzer; + if (_map.TryGetValue(project.Solution.Workspace, out analyzer)) + { + return analyzer.SynchronizeWithBuildAsync(project, diagnostics); + } + + return SpecializedTasks.EmptyTask; + } + + /// + /// Synchronize build errors with live error + /// + /// no cancellationToken since this can't be cancelled + /// + public Task SynchronizeWithBuildAsync(Document document, ImmutableArray diagnostics) + { + BaseDiagnosticIncrementalAnalyzer analyzer; + if (_map.TryGetValue(document.Project.Solution.Workspace, out analyzer)) + { + return analyzer.SynchronizeWithBuildAsync(document, diagnostics); + } + + return SpecializedTasks.EmptyTask; + } + } +} diff --git a/src/Features/Core/Diagnostics/DiagnosticAnalyzerService_IncrementalAnalyzer.cs b/src/Features/Core/Diagnostics/DiagnosticAnalyzerService_IncrementalAnalyzer.cs index 895572a180823..e95afdd1c40cc 100644 --- a/src/Features/Core/Diagnostics/DiagnosticAnalyzerService_IncrementalAnalyzer.cs +++ b/src/Features/Core/Diagnostics/DiagnosticAnalyzerService_IncrementalAnalyzer.cs @@ -1,12 +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; using System.Collections.Generic; using System.Collections.Immutable; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Diagnostics.Log; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Options; @@ -170,6 +168,18 @@ public override Task> GetDiagnosticsForSpanAsync(Doc } #endregion + #region build synchronization + public override Task SynchronizeWithBuildAsync(Project project, ImmutableArray diagnostics) + { + return Analyzer.SynchronizeWithBuildAsync(project, diagnostics); + } + + public override Task SynchronizeWithBuildAsync(Document document, ImmutableArray diagnostics) + { + return Analyzer.SynchronizeWithBuildAsync(document, diagnostics); + } + #endregion + public void TurnOff(bool useV2) { var turnedOffAnalyzer = GetAnalyzer(!useV2); diff --git a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.AnalyzerExecutor.cs b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.AnalyzerExecutor.cs index 317a54114c44d..5f3bd503d61c3 100644 --- a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.AnalyzerExecutor.cs +++ b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.AnalyzerExecutor.cs @@ -188,7 +188,6 @@ private async Task GetExistingProjectAnalysisDataAsync(Project pro return null; } - Contract.Requires(textVersion != VersionStamp.Default); return new AnalysisData(textVersion, dataVersion, builder.ToImmutable()); } diff --git a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.DiagnosticState.cs b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.DiagnosticState.cs index bae050fad4c91..c447733a7efc6 100644 --- a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.DiagnosticState.cs +++ b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.DiagnosticState.cs @@ -128,7 +128,7 @@ private AnalysisData TryGetExistingData(Stream stream, Project project, Document var textVersion = VersionStamp.ReadFrom(reader); var dataVersion = VersionStamp.ReadFrom(reader); - if (textVersion == VersionStamp.Default || dataVersion == VersionStamp.Default) + if (dataVersion == VersionStamp.Default) { return null; } diff --git a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs new file mode 100644 index 0000000000000..2da7f253487c6 --- /dev/null +++ b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs @@ -0,0 +1,191 @@ +// 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 Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV1 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + public override async Task SynchronizeWithBuildAsync(Project project, ImmutableArray diagnostics) + { + if (!PreferBuildErrors(project.Solution.Workspace)) + { + // prefer live errors over build errors + return; + } + + using (var poolObject = SharedPools.Default>().GetPooledObject()) + { + var lookup = CreateDiagnosticIdLookup(diagnostics); + + foreach (var stateSet in _stateManger.GetStateSets(project)) + { + var descriptors = HostAnalyzerManager.GetDiagnosticDescriptors(stateSet.Analyzer); + var liveDiagnostics = ConvertToLiveDiagnostics(lookup, descriptors, poolObject.Object); + + // we are using Default so that things like LB can't use cached information + var projectTextVersion = VersionStamp.Default; + var semanticVersion = await project.GetDependentSemanticVersionAsync(CancellationToken.None).ConfigureAwait(false); + + var state = stateSet.GetState(StateType.Project); + var existingDiagnostics = await state.TryGetExistingDataAsync(project, CancellationToken.None).ConfigureAwait(false); + + var mergedDiagnostics = MergeDiagnostics(liveDiagnostics, GetExistingDiagnostics(existingDiagnostics)); + await state.PersistAsync(project, new AnalysisData(projectTextVersion, semanticVersion, mergedDiagnostics), CancellationToken.None).ConfigureAwait(false); + RaiseDiagnosticsUpdated(StateType.Project, project.Id, stateSet.Analyzer, new SolutionArgument(project), mergedDiagnostics); + } + } + } + + public override async Task SynchronizeWithBuildAsync(Document document, ImmutableArray diagnostics) + { + if (!PreferBuildErrors(document.Project.Solution.Workspace)) + { + // prefer live errors over build errors + return; + } + + using (var poolObject = SharedPools.Default>().GetPooledObject()) + { + var lookup = CreateDiagnosticIdLookup(diagnostics); + + foreach (var stateSet in _stateManger.GetStateSets(document.Project)) + { + // we are using Default so that things like LB can't use cached information + var textVersion = VersionStamp.Default; + var semanticVersion = await document.Project.GetDependentSemanticVersionAsync(CancellationToken.None).ConfigureAwait(false); + + // clear document and project live errors + await PersistAndReportAsync(stateSet, StateType.Project, document, textVersion, semanticVersion, ImmutableArray.Empty).ConfigureAwait(false); + await PersistAndReportAsync(stateSet, StateType.Syntax, document, textVersion, semanticVersion, ImmutableArray.Empty).ConfigureAwait(false); + + var descriptors = HostAnalyzerManager.GetDiagnosticDescriptors(stateSet.Analyzer); + var liveDiagnostics = ConvertToLiveDiagnostics(lookup, descriptors, poolObject.Object); + + // REVIEW: for now, we are putting build error in document state rather than creating its own state. + // reason is so that live error can take over it as soon as possible + // this also means there can be slight race where it will clean up eventually. if live analysis runs for syntax but didn't run + // for document yet, then we can have duplicated entries in the error list until live analysis catch. + await PersistAndReportAsync(stateSet, StateType.Document, document, textVersion, semanticVersion, liveDiagnostics).ConfigureAwait(false); + } + } + } + + private bool PreferBuildErrors(Workspace workspace) + { + return workspace.Options.GetOption(InternalDiagnosticsOptions.BuildErrorIsTheGod) || workspace.Options.GetOption(InternalDiagnosticsOptions.BuildErrorWinLiveError); + } + + private async Task PersistAndReportAsync( + StateSet stateSet, StateType stateType, Document document, VersionStamp textVersion, VersionStamp semanticVersion, ImmutableArray diagnostics) + { + var state = stateSet.GetState(stateType); + var existingDiagnostics = await state.TryGetExistingDataAsync(document, CancellationToken.None).ConfigureAwait(false); + + var mergedDiagnostics = MergeDiagnostics(diagnostics, GetExistingDiagnostics(existingDiagnostics)); + await state.PersistAsync(document, new AnalysisData(textVersion, semanticVersion, mergedDiagnostics), CancellationToken.None).ConfigureAwait(false); + RaiseDiagnosticsUpdated(stateType, document.Id, stateSet.Analyzer, new SolutionArgument(document), mergedDiagnostics); + } + + private static ImmutableArray GetExistingDiagnostics(AnalysisData analysisData) + { + if (analysisData == null) + { + return ImmutableArray.Empty; + } + + return analysisData.Items; + } + + private ImmutableArray MergeDiagnostics(ImmutableArray liveDiagnostics, ImmutableArray existingDiagnostics) + { + ImmutableArray.Builder builder = null; + + if (liveDiagnostics.Length > 0) + { + builder = ImmutableArray.CreateBuilder(); + builder.AddRange(liveDiagnostics); + } + + if (existingDiagnostics.Length > 0) + { + builder = builder ?? ImmutableArray.CreateBuilder(); + builder.AddRange(existingDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Hidden)); + } + + return builder == null ? ImmutableArray.Empty : builder.ToImmutable(); + } + + private static ILookup CreateDiagnosticIdLookup(ImmutableArray diagnostics) + { + if (diagnostics.Length == 0) + { + return null; + } + + return diagnostics.ToLookup(d => d.Id); + } + + 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 doesnt 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(); + } + + private static DiagnosticData CreateLiveDiagnostic(DiagnosticDescriptor descriptor, DiagnosticData diagnostic) + { + return new DiagnosticData( + descriptor.Id, + descriptor.Category, + diagnostic.Message, + descriptor.MessageFormat.ToString(DiagnosticData.USCultureInfo), + diagnostic.Severity, + descriptor.DefaultSeverity, + descriptor.IsEnabledByDefault, + diagnostic.WarningLevel, + descriptor.CustomTags.ToImmutableArray(), + diagnostic.Properties, + diagnostic.Workspace, + diagnostic.ProjectId, + diagnostic.DocumentId, + diagnostic.HasTextSpan ? diagnostic.TextSpan : (TextSpan?)null, + diagnostic.MappedFilePath, diagnostic.MappedStartLine, diagnostic.MappedStartColumn, diagnostic.MappedEndLine, diagnostic.MappedEndColumn, + diagnostic.OriginalFilePath, diagnostic.OriginalStartLine, diagnostic.OriginalStartColumn, diagnostic.OriginalEndLine, diagnostic.OriginalEndColumn, + descriptor.Title.ToString(CultureInfo.CurrentUICulture), + descriptor.Description.ToString(CultureInfo.CurrentUICulture), + descriptor.HelpLinkUri); + } + } +} diff --git a/src/Features/Core/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs b/src/Features/Core/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs index cbe0261c3ff2e..7f6d9e8ae1c8d 100644 --- a/src/Features/Core/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs +++ b/src/Features/Core/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs @@ -14,7 +14,7 @@ namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 internal class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer { private readonly int _correlationId; - + public DiagnosticIncrementalAnalyzer(DiagnosticAnalyzerService owner, int correlationId, Workspace workspace, HostAnalyzerManager hostAnalyzerManager, AbstractHostDiagnosticUpdateSource hostDiagnosticUpdateSource) : base(owner, workspace, hostAnalyzerManager, hostDiagnosticUpdateSource) { @@ -181,6 +181,22 @@ private IEnumerable GetDiagnosticData(Project project, Immutable } } + public override Task SynchronizeWithBuildAsync(Project project, ImmutableArray diagnostics) + { + // V2 engine doesn't do anything. + // it means live error always win over build errors. build errors that can't be reported by live analyzer + // are already taken cared by engine + return SpecializedTasks.EmptyTask; + } + + public override Task SynchronizeWithBuildAsync(Document document, ImmutableArray diagnostics) + { + // V2 engine doesn't do anything. + // it means live error always win over build errors. build errors that can't be reported by live analyzer + // are already taken cared by engine + return SpecializedTasks.EmptyTask; + } + private void RaiseEvents(Project project, ImmutableArray diagnostics) { var groups = diagnostics.GroupBy(d => d.DocumentId); diff --git a/src/Features/Core/Diagnostics/HostAnalyzerManager.cs b/src/Features/Core/Diagnostics/HostAnalyzerManager.cs index a6791e0c2b1a0..c7841d17a5244 100644 --- a/src/Features/Core/Diagnostics/HostAnalyzerManager.cs +++ b/src/Features/Core/Diagnostics/HostAnalyzerManager.cs @@ -53,6 +53,16 @@ internal sealed partial class HostAnalyzerManager /// private readonly AbstractHostDiagnosticUpdateSource _hostDiagnosticUpdateSource; + /// + /// map to compiler diagnostic analyzer. + /// + private ImmutableDictionary _compilerDiagnosticAnalyzerMap; + + /// + /// map to compiler diagnostic analyzer descriptor. + /// + private ImmutableDictionary> _compilerDiagnosticAnalyzerDescriptorMap; + public HostAnalyzerManager(IEnumerable hostAnalyzerAssemblies, AbstractHostDiagnosticUpdateSource hostDiagnosticUpdateSource) : this(CreateAnalyzerReferencesFromAssemblies(hostAnalyzerAssemblies), hostDiagnosticUpdateSource) { @@ -66,6 +76,9 @@ public HostAnalyzerManager(ImmutableArray hostAnalyzerReferen _hostDiagnosticAnalyzersPerLanguageMap = new ConcurrentDictionary>>(concurrencyLevel: 2, capacity: 2); _lazyHostDiagnosticAnalyzersPerReferenceMap = new Lazy>>(() => CreateDiagnosticAnalyzersPerReferenceMap(_hostAnalyzerReferencesMap), isThreadSafe: true); + _compilerDiagnosticAnalyzerMap = ImmutableDictionary.Empty; + _compilerDiagnosticAnalyzerDescriptorMap = ImmutableDictionary>.Empty; + DiagnosticAnalyzerLogger.LogWorkspaceAnalyzers(hostAnalyzerReferences); } @@ -140,6 +153,25 @@ public ImmutableArray CreateDiagnosticAnalyzers(Project proj return analyzersPerReferences.SelectMany(kv => kv.Value).ToImmutableArray(); } + /// + /// Check whether given belong to compiler diagnostic analyzer + /// + public bool IsCompilerDiagnostic(string language, DiagnosticData diagnostic) + { + var map = GetHostDiagnosticAnalyzersPerReference(language); + + HashSet idMap; + DiagnosticAnalyzer compilerAnalyzer; + if (_compilerDiagnosticAnalyzerMap.TryGetValue(language, out compilerAnalyzer) && + _compilerDiagnosticAnalyzerDescriptorMap.TryGetValue(compilerAnalyzer, out idMap) && + idMap.Contains(diagnostic.Id)) + { + return true; + } + + return false; + } + private ImmutableDictionary> CreateDiagnosticDescriptorsPerReference( ImmutableDictionary> analyzersMap) { @@ -179,13 +211,33 @@ private ImmutableDictionary> CreateHo continue; } + UpdateCompilerAnalyzerMapIfNeeded(language, analyzers); + // there can't be duplication since _hostAnalyzerReferenceMap is already de-duplicated. - builder.Add(referenceIdenity, reference.GetAnalyzers(language)); + builder.Add(referenceIdenity, analyzers); } return builder.ToImmutable(); } + private void UpdateCompilerAnalyzerMapIfNeeded(string language, ImmutableArray analyzers) + { + if (_compilerDiagnosticAnalyzerMap.ContainsKey(language)) + { + return; + } + + foreach (var analyzer in analyzers) + { + if (analyzer.IsCompilerAnalyzer()) + { + ImmutableInterlocked.GetOrAdd(ref _compilerDiagnosticAnalyzerDescriptorMap, analyzer, a => new HashSet(GetDiagnosticDescriptors(a).Select(d => d.Id))); + ImmutableInterlocked.GetOrAdd(ref _compilerDiagnosticAnalyzerMap, language, _ => analyzer); + return; + } + } + } + private static string GetAnalyzerReferenceId(AnalyzerReference reference) { return reference.Display ?? FeaturesResources.Unknown; diff --git a/src/Features/Core/Diagnostics/IDiagnosticAnalyzerService.cs b/src/Features/Core/Diagnostics/IDiagnosticAnalyzerService.cs index 47b3550806785..f11488041b68c 100644 --- a/src/Features/Core/Diagnostics/IDiagnosticAnalyzerService.cs +++ b/src/Features/Core/Diagnostics/IDiagnosticAnalyzerService.cs @@ -76,5 +76,10 @@ internal interface IDiagnosticAnalyzerService /// /// A list of the diagnostics produced by the given analyzer ImmutableArray GetDiagnosticDescriptors(DiagnosticAnalyzer analyzer); + + /// + /// Check whether given diagnostic is compiler diagnostic or not + /// + bool IsCompilerDiagnostic(string language, DiagnosticData diagnostic); } } diff --git a/src/Features/Core/Diagnostics/InternalDiagnosticsOptions.cs b/src/Features/Core/Diagnostics/InternalDiagnosticsOptions.cs index e19941c65865e..10cc5f41a98cd 100644 --- a/src/Features/Core/Diagnostics/InternalDiagnosticsOptions.cs +++ b/src/Features/Core/Diagnostics/InternalDiagnosticsOptions.cs @@ -19,5 +19,11 @@ internal static class InternalDiagnosticsOptions [ExportOption] public static readonly Option UseCompilationEndCodeFixHeuristic = new Option(OptionName, "Enable Compilation End Code Fix With Heuristic", defaultValue: true); + + [ExportOption] + public static readonly Option BuildErrorIsTheGod = new Option(OptionName, "Make build errors to take over everything", defaultValue: false); + + [ExportOption] + public static readonly Option BuildErrorWinLiveError = new Option(OptionName, "Errors from build will win live errors from same analyzer", defaultValue: true); } } diff --git a/src/Features/Core/Features.csproj b/src/Features/Core/Features.csproj index e1559bc121490..d8b86477a0957 100644 --- a/src/Features/Core/Features.csproj +++ b/src/Features/Core/Features.csproj @@ -179,11 +179,13 @@ + + diff --git a/src/VisualStudio/Core/Def/Implementation/TaskList/ExternalErrorDiagnosticUpdateSource.cs b/src/VisualStudio/Core/Def/Implementation/TaskList/ExternalErrorDiagnosticUpdateSource.cs index e1e1fbf900053..e6eb38f9efa8c 100644 --- a/src/VisualStudio/Core/Def/Implementation/TaskList/ExternalErrorDiagnosticUpdateSource.cs +++ b/src/VisualStudio/Core/Def/Implementation/TaskList/ExternalErrorDiagnosticUpdateSource.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Notification; +using Microsoft.CodeAnalysis.Shared.Options; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; using Microsoft.VisualStudio.LanguageServices.Implementation.Venus; @@ -22,16 +24,12 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.TaskList internal class ExternalErrorDiagnosticUpdateSource : IDiagnosticUpdateSource { private readonly Workspace _workspace; - private readonly IDiagnosticUpdateSource _diagnosticService; + private readonly IDiagnosticAnalyzerService _diagnosticService; private readonly SimpleTaskQueue _taskQueue; private readonly IAsynchronousOperationListener _listener; - private readonly object _gate = new object(); - - // errors reported by build that is not reported by live errors - private readonly Dictionary> _projectToDiagnosticsMap = new Dictionary>(); - private readonly Dictionary> _documentToDiagnosticsMap = new Dictionary>(); + private InprogressState _state = null; [ImportingConstructor] public ExternalErrorDiagnosticUpdateSource( @@ -45,9 +43,9 @@ public ExternalErrorDiagnosticUpdateSource( } /// - /// Test Only + /// internal for testing /// - public ExternalErrorDiagnosticUpdateSource( + internal ExternalErrorDiagnosticUpdateSource( Workspace workspace, IDiagnosticAnalyzerService diagnosticService, IAsynchronousOperationListener listener) @@ -59,108 +57,11 @@ public ExternalErrorDiagnosticUpdateSource( _workspace = workspace; _workspace.WorkspaceChanged += OnWorkspaceChanged; - _diagnosticService = (IDiagnosticUpdateSource)diagnosticService; - _diagnosticService.DiagnosticsUpdated += OnDiagnosticUpdated; + _diagnosticService = diagnosticService; } public event EventHandler DiagnosticsUpdated; - public bool SupportGetDiagnostics { get { return true; } } - - public ImmutableArray GetDiagnostics( - Workspace workspace, ProjectId projectId, DocumentId documentId, object id, CancellationToken cancellationToken) - { - if (workspace != _workspace) - { - return ImmutableArray.Empty; - } - - if (id != null) - { - return GetSpecificDiagnostics(projectId, documentId, id); - } - - if (documentId != null) - { - return GetSpecificDiagnostics(_documentToDiagnosticsMap, documentId); - } - - if (projectId != null) - { - return GetProjectDiagnostics(projectId, cancellationToken); - } - - return GetSolutionDiagnostics(workspace.CurrentSolution, cancellationToken); - } - - private ImmutableArray GetSolutionDiagnostics(Solution solution, CancellationToken cancellationToken) - { - var builder = ImmutableArray.CreateBuilder(); - foreach (var projectId in solution.ProjectIds) - { - builder.AddRange(GetProjectDiagnostics(projectId, cancellationToken)); - } - - return builder.ToImmutable(); - } - - private ImmutableArray GetProjectDiagnostics(ProjectId projectId, CancellationToken cancellationToken) - { - List documentIds; - lock (_gate) - { - documentIds = _documentToDiagnosticsMap.Keys.Where(d => d.ProjectId == projectId).ToList(); - } - - var builder = ImmutableArray.CreateBuilder(documentIds.Count + 1); - builder.AddRange(GetSpecificDiagnostics(_projectToDiagnosticsMap, projectId)); - - foreach (var documentId in documentIds) - { - cancellationToken.ThrowIfCancellationRequested(); - builder.AddRange(GetSpecificDiagnostics(_documentToDiagnosticsMap, documentId)); - } - - return builder.ToImmutable(); - } - - private ImmutableArray GetSpecificDiagnostics(ProjectId projectId, DocumentId documentId, object id) - { - var key = id as ArgumentKey; - if (key == null) - { - return ImmutableArray.Empty; - } - - Contract.Requires(documentId == key.DocumentId); - if (documentId != null) - { - return GetSpecificDiagnostics(_documentToDiagnosticsMap, documentId); - } - - Contract.Requires(projectId == key.ProjectId); - if (projectId != null) - { - return GetSpecificDiagnostics(_projectToDiagnosticsMap, projectId); - } - - return Contract.FailWithReturn>("shouldn't reach here"); - } - - private ImmutableArray GetSpecificDiagnostics(Dictionary> map, T key) - { - lock (_gate) - { - HashSet data; - if (map.TryGetValue(key, out data)) - { - return data.ToImmutableArrayOrEmpty(); - } - - return ImmutableArray.Empty; - } - } - private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) { switch (e.Kind) @@ -171,7 +72,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) case WorkspaceChangeKind.SolutionReloaded: { var asyncToken = _listener.BeginAsyncOperation("OnSolutionChanged"); - _taskQueue.ScheduleTask(() => e.OldSolution.ProjectIds.Do(p => ClearProjectErrors(p))).CompletesAsyncOperation(asyncToken); + _taskQueue.ScheduleTask(() => e.OldSolution.ProjectIds.Do(p => ClearProjectErrors(p, e.OldSolution))).CompletesAsyncOperation(asyncToken); break; } @@ -179,7 +80,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) case WorkspaceChangeKind.ProjectReloaded: { var asyncToken = _listener.BeginAsyncOperation("OnProjectChanged"); - _taskQueue.ScheduleTask(() => ClearProjectErrors(e.ProjectId)).CompletesAsyncOperation(asyncToken); + _taskQueue.ScheduleTask(() => ClearProjectErrors(e.ProjectId, e.OldSolution)).CompletesAsyncOperation(asyncToken); break; } @@ -215,109 +116,151 @@ internal void OnSolutionBuild(object sender, UIContextChangedEventArgs e) } var asyncToken = _listener.BeginAsyncOperation("OnSolutionBuild"); - _taskQueue.ScheduleTask(() => + _taskQueue.ScheduleTask(async () => { - // build is done, remove live error and report errors - IDictionary> liveProjectErrors; - IDictionary> liveDocumentErrors; - GetLiveProjectAndDocumentErrors(out liveProjectErrors, out liveDocumentErrors); + // nothing to do + if (_state == null) + { + return; + } - using (var documentIds = SharedPools.Default>().GetPooledObject()) - using (var projectIds = SharedPools.Default>().GetPooledObject()) + // we are about to update live analyzer data using one from build. + // pause live analyzer + var service = _workspace.Services.GetService(); + using (var operation = service.Start("BuildDone")) { - lock (_gate) - { - documentIds.Object.AddRange(_documentToDiagnosticsMap.Keys); - projectIds.Object.AddRange(_projectToDiagnosticsMap.Keys); - } + // we will have a race here since we can't track version of solution the out of proc build actually used. + // result of the race will be us dropping some diagnostics from the build to the floor. + var solution = _workspace.CurrentSolution; - foreach (var documentId in documentIds.Object) - { - var errors = liveDocumentErrors.GetValueOrDefault(documentId); - RemoveBuildErrorsDuplicatedByLiveErrorsAndReport(documentId.ProjectId, documentId, errors, reportOnlyIfChanged: false); - } + await CleanupAllLiveErrorsIfNeededAsync(solution).ConfigureAwait(false); - foreach (var projectId in projectIds.Object) + var supportedIdMap = GetSupportedLiveDiagnosticId(solution, _state); + Func liveDiagnosticChecker = d => { - var errors = liveProjectErrors.GetValueOrDefault(projectId); - RemoveBuildErrorsDuplicatedByLiveErrorsAndReport(projectId, null, errors, reportOnlyIfChanged: false); - } + // REVIEW: we probably need a better design on de-duplicating live and build errors. or don't de-dup at all. + // for now, we are special casing compiler error case. + var project = solution.GetProject(d.ProjectId); + if (project == null) + { + // project doesn't exist + return false; + } + + // REVIEW: current design is that we special case compiler analyzer case and we accept only document level + // diagnostic as live. otherwise, we let them be build errors. we changed compiler analyzer accordingly as well + // so that it doesn't report project level diagnostic as live errors. + if (_diagnosticService.IsCompilerDiagnostic(project.Language, d) && d.DocumentId == null) + { + // compiler error but project level error + return false; + } + + HashSet set; + if (supportedIdMap.TryGetValue(d.ProjectId, out set) && set.Contains(d.Id)) + { + return true; + } + + return false; + }; + + await SyncBuildErrorsAndReportAsync(solution, liveDiagnosticChecker, _state.GetDocumentAndErrors(solution)).ConfigureAwait(false); + await SyncBuildErrorsAndReportAsync(solution, liveDiagnosticChecker, _state.GetProjectAndErrors(solution)).ConfigureAwait(false); + + // we are done. set inprogress state to null + _state = null; } }).CompletesAsyncOperation(asyncToken); } - private void OnDiagnosticUpdated(object sender, DiagnosticsUpdatedArgs args) + private async System.Threading.Tasks.Task CleanupAllLiveErrorsIfNeededAsync(Solution solution) { - if (args.Diagnostics.Length == 0 || _workspace != args.Workspace) + if (!_workspace.Options.GetOption(InternalDiagnosticsOptions.BuildErrorIsTheGod)) { return; } - // live errors win over build errors - var asyncToken = _listener.BeginAsyncOperation("OnDiagnosticUpdated"); - _taskQueue.ScheduleTask( - () => RemoveBuildErrorsDuplicatedByLiveErrorsAndReport(args.ProjectId, args.DocumentId, args.Diagnostics, reportOnlyIfChanged: true)).CompletesAsyncOperation(asyncToken); + // clear all existing live errors + foreach (var project in solution.Projects) + { + foreach (var document in project.Documents) + { + await SynchronizeWithBuildAsync(document, ImmutableArray.Empty).ConfigureAwait(false); + } + + await SynchronizeWithBuildAsync(project, ImmutableArray.Empty).ConfigureAwait(false); + } + } + + private async System.Threading.Tasks.Task SyncBuildErrorsAndReportAsync( + Solution solution, + Func liveDiagnosticChecker, IEnumerable>> items) + { + 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(kv.Key, liveErrors).ConfigureAwait(false); + + // raise events for ones left-out + if (liveErrors.Length != kv.Value.Count) + { + var buildErrors = kv.Value.Except(liveErrors).ToImmutableArray(); + ReportBuildErrors(kv.Key, buildErrors); + } + } } - private void RemoveBuildErrorsDuplicatedByLiveErrorsAndReport(ProjectId projectId, DocumentId documentId, IEnumerable liveErrors, bool reportOnlyIfChanged) + private async System.Threading.Tasks.Task SynchronizeWithBuildAsync(T item, ImmutableArray liveErrors) { - if (documentId != null) + var diagnosticService = _diagnosticService as DiagnosticAnalyzerService; + if (diagnosticService == null) { - RemoveBuildErrorsDuplicatedByLiveErrorsAndReport(_documentToDiagnosticsMap, documentId, projectId, documentId, liveErrors, reportOnlyIfChanged); + // we don't synchronize if implementation is not DiagnosticService return; } - if (projectId != null) + var project = item as Project; + if (project != null) { - RemoveBuildErrorsDuplicatedByLiveErrorsAndReport(_projectToDiagnosticsMap, projectId, projectId, null, liveErrors, reportOnlyIfChanged); + await diagnosticService.SynchronizeWithBuildAsync(project, liveErrors).ConfigureAwait(false); return; } - // diagnostic errors without any associated workspace project/document? - Contract.Requires(false, "how can this happen?"); + // must be not null + var document = item as Document; + await diagnosticService.SynchronizeWithBuildAsync(document, liveErrors).ConfigureAwait(false); } - private void RemoveBuildErrorsDuplicatedByLiveErrorsAndReport( - Dictionary> buildErrorMap, T key, - ProjectId projectId, DocumentId documentId, IEnumerable liveErrors, bool reportOnlyIfChanged) + private void ReportBuildErrors(T item, ImmutableArray buildErrors) { - ImmutableArray items; - - lock (_gate) + var project = item as Project; + if (project != null) { - HashSet buildErrors; - if (!buildErrorMap.TryGetValue(key, out buildErrors)) - { - return; - } - - var originalBuildErrorCount = buildErrors.Count; - if (liveErrors != null) - { - buildErrors.ExceptWith(liveErrors); - } - - if (buildErrors.Count == 0) - { - buildErrorMap.Remove(key); - } + RaiseDiagnosticsUpdated(project.Id, project.Id, null, buildErrors); + return; + } - // nothing to refresh. - if (originalBuildErrorCount == 0) - { - return; - } + // must be not null + var document = item as Document; + RaiseDiagnosticsUpdated(document.Id, document.Project.Id, document.Id, buildErrors); + } - // if nothing has changed and we are asked to report only when something has changed - if (reportOnlyIfChanged && originalBuildErrorCount == buildErrors.Count) - { - return; - } + private Dictionary> GetSupportedLiveDiagnosticId(Solution solution, InprogressState state) + { + var map = new Dictionary>(); - items = buildErrors.ToImmutableArrayOrEmpty(); + // here, we don't care about perf that much since build is already expensive work + foreach (var project in state.GetProjectsWithErrors(solution)) + { + var descriptorMap = _diagnosticService.GetDiagnosticDescriptors(project); + map.Add(project.Id, new HashSet(descriptorMap.Values.SelectMany(v => v.Select(d => d.Id)))); } - RaiseDiagnosticsUpdated(key, projectId, documentId, items); + return map; } public void ClearErrors(ProjectId projectId) @@ -326,38 +269,26 @@ public void ClearErrors(ProjectId projectId) _taskQueue.ScheduleTask(() => ClearProjectErrors(projectId)).CompletesAsyncOperation(asyncToken); } - private void ClearProjectErrors(ProjectId projectId) + private void ClearProjectErrors(ProjectId projectId, Solution solution = null) { - var clearProjectError = false; - using (var pool = SharedPools.Default>().GetPooledObject()) - { - lock (_gate) - { - clearProjectError = _projectToDiagnosticsMap.Remove(projectId); - pool.Object.AddRange(_documentToDiagnosticsMap.Keys.Where(k => k.ProjectId == projectId)); - } + // remove all project errors + RaiseDiagnosticsUpdated(projectId, projectId, null, ImmutableArray.Empty); - if (clearProjectError) - { - // remove all project errors - RaiseDiagnosticsUpdated(projectId, projectId, null, ImmutableArray.Empty); - } + var project = (solution ?? _workspace.CurrentSolution).GetProject(projectId); + if (project == null) + { + return; + } - // remove all document errors - foreach (var documentId in pool.Object) - { - ClearDocumentErrors(projectId, documentId); - } + // remove all document errors + foreach (var documentId in project.DocumentIds) + { + ClearDocumentErrors(projectId, documentId); } } private void ClearDocumentErrors(ProjectId projectId, DocumentId documentId) { - lock (_gate) - { - _documentToDiagnosticsMap.Remove(documentId); - } - RaiseDiagnosticsUpdated(documentId, projectId, documentId, ImmutableArray.Empty); } @@ -366,75 +297,133 @@ public void AddNewErrors(DocumentId documentId, DiagnosticData diagnostic) var asyncToken = _listener.BeginAsyncOperation("Document New Errors"); _taskQueue.ScheduleTask(() => { - lock (_gate) - { - var errors = _documentToDiagnosticsMap.GetOrAdd(documentId, _ => new HashSet(DiagnosticDataComparer.Instance)); - errors.Add(diagnostic); - } + GetOrCreateInprogressState().AddError(documentId, diagnostic); }).CompletesAsyncOperation(asyncToken); } public void AddNewErrors( - ProjectId projectId, HashSet projectErrorSet, Dictionary> documentErrorMap) + ProjectId projectId, HashSet projectErrors, Dictionary> documentErrorMap) { var asyncToken = _listener.BeginAsyncOperation("Project New Errors"); _taskQueue.ScheduleTask(() => { - lock (_gate) + var state = GetOrCreateInprogressState(); + foreach (var kv in documentErrorMap) { - foreach (var kv in documentErrorMap) - { - var documentErrors = _documentToDiagnosticsMap.GetOrAdd(kv.Key, _ => new HashSet(DiagnosticDataComparer.Instance)); - documentErrors.UnionWith(kv.Value); - } - - var projectErrors = _projectToDiagnosticsMap.GetOrAdd(projectId, _ => new HashSet(DiagnosticDataComparer.Instance)); - projectErrors.UnionWith(projectErrorSet); + state.AddErrors(kv.Key, kv.Value); } + + state.AddErrors(projectId, projectErrors); }).CompletesAsyncOperation(asyncToken); } - private void GetLiveProjectAndDocumentErrors( - out IDictionary> projectErrors, - out IDictionary> documentErrors) + private InprogressState GetOrCreateInprogressState() + { + if (_state == null) + { + _state = new InprogressState(); + } + + return _state; + } + + private void RaiseDiagnosticsUpdated(object id, ProjectId projectId, DocumentId documentId, ImmutableArray items) + { + var diagnosticsUpdated = DiagnosticsUpdated; + if (diagnosticsUpdated != null) + { + diagnosticsUpdated(this, new DiagnosticsUpdatedArgs( + new ArgumentKey(id), _workspace, _workspace.CurrentSolution, projectId, documentId, items)); + } + } + + #region not supported + public bool SupportGetDiagnostics { get { return false; } } + + public ImmutableArray GetDiagnostics( + Workspace workspace, ProjectId projectId, DocumentId documentId, object id, CancellationToken cancellationToken) { - projectErrors = null; - documentErrors = null; + return ImmutableArray.Empty; + } + #endregion + + private class InprogressState + { + private readonly Dictionary> _projectMap = new Dictionary>(); + private readonly Dictionary> _documentMap = new Dictionary>(); + + 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; + } - foreach (var diagnostic in _diagnosticService.GetDiagnostics(_workspace, id: null, projectId: null, documentId: null, cancellationToken: CancellationToken.None)) + yield return project; + } + } + + public IEnumerable>> GetDocumentAndErrors(Solution solution) { - if (diagnostic.DocumentId != null) + foreach (var kv in _documentMap) { - documentErrors = documentErrors ?? new Dictionary>(); + var document = solution.GetDocument(kv.Key); + if (document == null) + { + continue; + } - var errors = documentErrors.GetOrAdd(diagnostic.DocumentId, _ => new List()); - errors.Add(diagnostic); - continue; + yield return KeyValuePair.Create(document, kv.Value); } + } - if (diagnostic.ProjectId != null) + public IEnumerable>> GetProjectAndErrors(Solution solution) + { + foreach (var kv in _projectMap) { - projectErrors = projectErrors ?? new Dictionary>(); + var project = solution.GetProject(kv.Key); + if (project == null) + { + continue; + } - var errors = projectErrors.GetOrAdd(diagnostic.ProjectId, _ => new List()); - errors.Add(diagnostic); - continue; + yield return KeyValuePair.Create(project, kv.Value); } + } - Contract.Requires(false, "shouldn't happen"); + public void AddErrors(DocumentId key, HashSet diagnostics) + { + AddErrors(_documentMap, key, diagnostics); } - projectErrors = projectErrors ?? SpecializedCollections.EmptyDictionary>(); - documentErrors = documentErrors ?? SpecializedCollections.EmptyDictionary>(); - } + public void AddErrors(ProjectId key, HashSet diagnostics) + { + AddErrors(_projectMap, key, diagnostics); + } - private void RaiseDiagnosticsUpdated(object id, ProjectId projectId, DocumentId documentId, ImmutableArray items) - { - var diagnosticsUpdated = DiagnosticsUpdated; - if (diagnosticsUpdated != null) + public void AddError(DocumentId key, DiagnosticData diagnostic) { - diagnosticsUpdated(this, new DiagnosticsUpdatedArgs( - new ArgumentKey(id), _workspace, _workspace.CurrentSolution, projectId, documentId, items)); + AddError(_documentMap, key, diagnostic); + } + + private void AddErrors(Dictionary> map, T key, HashSet diagnostics) + { + var errors = GetErrors(map, key); + errors.UnionWith(diagnostics); + } + + private void AddError(Dictionary> map, T key, DiagnosticData diagnostic) + { + var errors = GetErrors(map, key); + errors.Add(diagnostic); + } + + private HashSet GetErrors(Dictionary> map, T key) + { + return map.GetOrAdd(key, _ => new HashSet(DiagnosticDataComparer.Instance)); } } diff --git a/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb b/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb index c1a25d6ef68c1..1e5e3ac6203f2 100644 --- a/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb +++ b/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb @@ -22,12 +22,12 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Dim service = New TestDiagnosticAnalyzerService() Dim source = New ExternalErrorDiagnosticUpdateSource(workspace, service, waiter) - Assert.True(source.SupportGetDiagnostics) + Assert.False(source.SupportGetDiagnostics) End Using End Sub - Public Sub TestExternalDiagnostics_GetDiagnostics() + Public Sub TestExternalDiagnostics_RaiseEvents() Using workspace = CSharpWorkspaceFactory.CreateWorkspaceFromLines(String.Empty) Dim waiter = New Waiter() Dim service = New TestDiagnosticAnalyzerService() @@ -36,46 +36,21 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Dim project = workspace.CurrentSolution.Projects.First() Dim diagnostic = GetDiagnosticData(workspace, project.Id) - source.AddNewErrors(project.DocumentIds.First(), diagnostic) - waiter.CreateWaitTask().PumpingWait() - - Dim data1 = source.GetDiagnostics(workspace, Nothing, Nothing, Nothing, CancellationToken.None) - Assert.Equal(1, data1.Length) - Assert.Equal(data1(0), diagnostic) - - source.ClearErrors(project.Id) - waiter.CreateWaitTask().PumpingWait() - - Dim data2 = source.GetDiagnostics(workspace, Nothing, Nothing, Nothing, CancellationToken.None) - Assert.Equal(0, data2.Length) - End Using - End Sub + Dim expected = 1 + AddHandler source.DiagnosticsUpdated, Sub(o, a) + Assert.Equal(expected, a.Diagnostics.Length) + If expected = 1 Then + Assert.Equal(a.Diagnostics(0), diagnostic) + End If + End Sub - - Public Sub TestExternalDiagnostics_GetDiagnostics2() - Using workspace = CSharpWorkspaceFactory.CreateWorkspaceFromLines(String.Empty) - Dim waiter = New Waiter() - Dim service = New TestDiagnosticAnalyzerService() - Dim source = New ExternalErrorDiagnosticUpdateSource(workspace, service, waiter) - - Dim project = workspace.CurrentSolution.Projects.First() - Dim diagnostic = GetDiagnosticData(workspace, project.Id) - - Dim map = New Dictionary(Of DocumentId, HashSet(Of DiagnosticData))() - map.Add(project.DocumentIds.First(), New HashSet(Of DiagnosticData)( - SpecializedCollections.SingletonEnumerable(GetDiagnosticData(workspace, project.Id)))) - - source.AddNewErrors(project.Id, New HashSet(Of DiagnosticData)(SpecializedCollections.SingletonEnumerable(diagnostic)), map) + source.AddNewErrors(project.DocumentIds.First(), diagnostic) + source.OnSolutionBuild(Me, Shell.UIContextChangedEventArgs.From(False)) waiter.CreateWaitTask().PumpingWait() - Dim data1 = source.GetDiagnostics(workspace, Nothing, Nothing, Nothing, CancellationToken.None) - Assert.Equal(2, data1.Length) - + expected = 0 source.ClearErrors(project.Id) waiter.CreateWaitTask().PumpingWait() - - Dim data2 = source.GetDiagnostics(workspace, Nothing, Nothing, Nothing, CancellationToken.None) - Assert.Equal(0, data2.Length) End Using End Sub @@ -96,16 +71,11 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics source.AddNewErrors(project.Id, New HashSet(Of DiagnosticData)(SpecializedCollections.SingletonEnumerable(diagnostic)), map) source.OnSolutionBuild(Me, Shell.UIContextChangedEventArgs.From(False)) - waiter.CreateWaitTask().PumpingWait() - - Dim data1 = source.GetDiagnostics(workspace, Nothing, Nothing, Nothing, CancellationToken.None) - Assert.Equal(1, data1.Length) - source.ClearErrors(project.Id) + AddHandler source.DiagnosticsUpdated, Sub(o, a) + Assert.Equal(1, a.Diagnostics.Length) + End Sub waiter.CreateWaitTask().PumpingWait() - - Dim data2 = source.GetDiagnostics(workspace, Nothing, Nothing, Nothing, CancellationToken.None) - Assert.Equal(0, data2.Length) End Using End Sub @@ -199,6 +169,10 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Public Function GetDiagnosticDescriptors(analyzer As DiagnosticAnalyzer) As ImmutableArray(Of DiagnosticDescriptor) Implements IDiagnosticAnalyzerService.GetDiagnosticDescriptors Return ImmutableArray(Of DiagnosticDescriptor).Empty End Function + + Public Function IsCompilerDiagnostic(language As String, diagnostic As DiagnosticData) As Boolean Implements IDiagnosticAnalyzerService.IsCompilerDiagnostic + Return False + End Function End Class End Class End Namespace diff --git a/src/VisualStudio/Core/Test/EditAndContinue/EditAndContinueTestHelper.vb b/src/VisualStudio/Core/Test/EditAndContinue/EditAndContinueTestHelper.vb index 13e469d8ac968..2ccd2ab283f90 100644 --- a/src/VisualStudio/Core/Test/EditAndContinue/EditAndContinueTestHelper.vb +++ b/src/VisualStudio/Core/Test/EditAndContinue/EditAndContinueTestHelper.vb @@ -90,6 +90,10 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.EditAndContinue Public Function GetDiagnosticDescriptors(analyzer As DiagnosticAnalyzer) As ImmutableArray(Of DiagnosticDescriptor) Implements IDiagnosticAnalyzerService.GetDiagnosticDescriptors Return ImmutableArray(Of DiagnosticDescriptor).Empty End Function + + Public Function IsCompilerDiagnostic(language As String, diagnostic As DiagnosticData) As Boolean Implements IDiagnosticAnalyzerService.IsCompilerDiagnostic + Return False + End Function End Class End Class