diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs index 2b2db793324ef..d42191d2ac5d5 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs @@ -1557,6 +1557,11 @@ private void ExecuteDeclaringReferenceActions( ImmutableDictionary>> nodeActionsByKind; if (this.NodeActionsByAnalyzerAndKind.TryGetValue(analyzer, out nodeActionsByKind)) { + if (nodeActionsByKind.IsEmpty) + { + continue; + } + analyzerExecutor.ExecuteSyntaxNodeActions(nodesToAnalyze, nodeActionsByKind, analyzer, semanticModel, _getKind, declarationAnalysisData.TopmostNodeForAnalysis.FullSpan, decl, declarationIndex, symbol, analysisScope, analysisStateOpt, isInGeneratedCode); @@ -1610,6 +1615,11 @@ private void ExecuteDeclaringReferenceActions( ImmutableDictionary> operationActionsByKind; if (this.OperationActionsByAnalyzerAndKind.TryGetValue(analyzer, out operationActionsByKind)) { + if (operationActionsByKind.IsEmpty) + { + continue; + } + analyzerExecutor.ExecuteOperationActions(operationsToAnalyze, operationActionsByKind, analyzer, semanticModel, declarationAnalysisData.TopmostNodeForAnalysis.FullSpan, decl, declarationIndex, symbol, analysisScope, analysisStateOpt, isInGeneratedCode); @@ -1621,6 +1631,13 @@ private void ExecuteDeclaringReferenceActions( { foreach (var analyzerActions in codeBlockActions) { + if (analyzerActions.OperationBlockStartActions.IsEmpty && + analyzerActions.OperationBlockActions.IsEmpty && + analyzerActions.OpererationBlockEndActions.IsEmpty) + { + continue; + } + analyzerExecutor.ExecuteOperationBlockActions( analyzerActions.OperationBlockStartActions, analyzerActions.OperationBlockActions, analyzerActions.OpererationBlockEndActions, analyzerActions.Analyzer, declarationAnalysisData.TopmostNodeForAnalysis, symbol, @@ -1639,6 +1656,13 @@ private void ExecuteDeclaringReferenceActions( { foreach (var analyzerActions in codeBlockActions) { + if (analyzerActions.CodeBlockStartActions.IsEmpty && + analyzerActions.CodeBlockActions.IsEmpty && + analyzerActions.CodeBlockEndActions.IsEmpty) + { + continue; + } + analyzerExecutor.ExecuteCodeBlockActions( analyzerActions.CodeBlockStartActions, analyzerActions.CodeBlockActions, analyzerActions.CodeBlockEndActions, analyzerActions.Analyzer, declarationAnalysisData.TopmostNodeForAnalysis, symbol, diff --git a/src/EditorFeatures/Test/Diagnostics/DiagnosticDataSerializerTests.cs b/src/EditorFeatures/Test/Diagnostics/DiagnosticDataSerializerTests.cs new file mode 100644 index 0000000000000..0f968fb01aecc --- /dev/null +++ b/src/EditorFeatures/Test/Diagnostics/DiagnosticDataSerializerTests.cs @@ -0,0 +1,291 @@ +// 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.Composition; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Diagnostics.EngineV2; +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Test.Utilities; +using Roslyn.Utilities; +using Xunit; +using Traits = Microsoft.CodeAnalysis.Test.Utilities.Traits; + +namespace Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics +{ + public class DiagnosticDataSerializerTests : TestBase + { + [Fact, Trait(Traits.Feature, Traits.Features.Diagnostics)] + public async Task SerializationTest_Document() + { + using (var workspace = new TestWorkspace(TestExportProvider.ExportProviderWithCSharpAndVisualBasic, workspaceKind: "DiagnosticDataSerializerTest")) + { + var document = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp).AddDocument("TestDocument", ""); + + var diagnostics = new[] + { + new DiagnosticData( + "test1", "Test", "test1 message", "test1 message format", + DiagnosticSeverity.Info, DiagnosticSeverity.Info, false, 1, + ImmutableArray.Empty, ImmutableDictionary.Empty, + workspace, document.Project.Id, new DiagnosticDataLocation(document.Id, + new TextSpan(10, 20), "originalFile1", 30, 30, 40, 40, "mappedFile1", 10, 10, 20, 20)), + new DiagnosticData( + "test2", "Test", "test2 message", "test2 message format", + DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, true, 0, + ImmutableArray.Create("Test2"), ImmutableDictionary.Empty.Add("propertyKey", "propertyValue"), + workspace, document.Project.Id, new DiagnosticDataLocation(document.Id, + new TextSpan(30, 40), "originalFile2", 70, 70, 80, 80, "mappedFile2", 50, 50, 60, 60), title: "test2 title", description: "test2 description", helpLink: "http://test2link"), + new DiagnosticData( + "test3", "Test", "test3 message", "test3 message format", + DiagnosticSeverity.Error, DiagnosticSeverity.Warning, true, 2, + ImmutableArray.Create("Test3", "Test3_2"), ImmutableDictionary.Empty.Add("p1Key", "p1Value").Add("p2Key", "p2Value"), + workspace, document.Project.Id, new DiagnosticDataLocation(document.Id, + new TextSpan(50, 60), "originalFile3", 110, 110, 120, 120, "mappedFile3", 90, 90, 100, 100), title: "test3 title", description: "test3 description", helpLink: "http://test3link"), + }.ToImmutableArray(); + + var utcTime = DateTime.UtcNow; + var analyzerVersion = VersionStamp.Create(utcTime); + var version = VersionStamp.Create(utcTime.AddDays(1)); + + var key = "document"; + var serializer = new DiagnosticDataSerializer(analyzerVersion, version); + + Assert.True(await serializer.SerializeAsync(document, key, diagnostics, CancellationToken.None).ConfigureAwait(false)); + var recovered = await serializer.DeserializeAsync(document, key, CancellationToken.None); + + AssertDiagnostics(diagnostics, recovered); + } + } + + [Fact, Trait(Traits.Feature, Traits.Features.Diagnostics)] + public async Task SerializationTest_Project() + { + using (var workspace = new TestWorkspace(TestExportProvider.ExportProviderWithCSharpAndVisualBasic, workspaceKind: "DiagnosticDataSerializerTest")) + { + var document = workspace.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp).AddDocument("TestDocument", ""); + + var diagnostics = new[] + { + new DiagnosticData( + "test1", "Test", "test1 message", "test1 message format", + DiagnosticSeverity.Info, DiagnosticSeverity.Info, false, 1, + ImmutableArray.Empty, ImmutableDictionary.Empty, + workspace, document.Project.Id, description: "test1 description", helpLink: "http://test1link"), + new DiagnosticData( + "test2", "Test", "test2 message", "test2 message format", + DiagnosticSeverity.Warning, DiagnosticSeverity.Warning, true, 0, + ImmutableArray.Create("Test2"), ImmutableDictionary.Empty.Add("p1Key", "p2Value"), + workspace, document.Project.Id), + new DiagnosticData( + "test3", "Test", "test3 message", "test3 message format", + DiagnosticSeverity.Error, DiagnosticSeverity.Warning, true, 2, + ImmutableArray.Create("Test3", "Test3_2"), ImmutableDictionary.Empty.Add("p2Key", "p2Value").Add("p1Key", "p1Value"), + workspace, document.Project.Id, description: "test3 description", helpLink: "http://test3link"), + }.ToImmutableArray(); + + var utcTime = DateTime.UtcNow; + var analyzerVersion = VersionStamp.Create(utcTime); + var version = VersionStamp.Create(utcTime.AddDays(1)); + + var key = "project"; + var serializer = new DiagnosticDataSerializer(analyzerVersion, version); + + Assert.True(await serializer.SerializeAsync(document, key, diagnostics, CancellationToken.None).ConfigureAwait(false)); + var recovered = await serializer.DeserializeAsync(document, key, CancellationToken.None); + + AssertDiagnostics(diagnostics, recovered); + } + } + + [WorkItem(6104, "https://github.com/dotnet/roslyn/issues/6104")] + [Fact] + public void DiagnosticEquivalence() + { +#if DEBUG + var source = +@"class C +{ + static int F(string s) { return 1; } + static int x = F(new { }); + static int y = F(new { A = 1 }); +}"; + var tree = SyntaxFactory.ParseSyntaxTree(source); + var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, concurrentBuild: false); + var compilation = CSharpCompilation.Create(GetUniqueName(), new[] { tree }, new[] { MscorlibRef }, options); + var model = compilation.GetSemanticModel(tree); + + // Each call to GetDiagnostics will bind field initializers + // (see https://github.com/dotnet/roslyn/issues/6264). + var diagnostics1 = model.GetDiagnostics().ToArray(); + var diagnostics2 = model.GetDiagnostics().ToArray(); + + diagnostics1.Verify( + // (4,22): error CS1503: Argument 1: cannot convert from '' to 'string' + // static int x = F(new { }); + Diagnostic(1503, "new { }").WithArguments("1", "", "string").WithLocation(4, 22), + // (5,22): error CS1503: Argument 1: cannot convert from '' to 'string' + // static int y = F(new { A = 1 }); + Diagnostic(1503, "new { A = 1 }").WithArguments("1", "", "string").WithLocation(5, 22)); + + Assert.NotSame(diagnostics1[0], diagnostics2[0]); + Assert.NotSame(diagnostics1[1], diagnostics2[1]); + Assert.Equal(diagnostics1, diagnostics2); + Assert.True(DiagnosticIncrementalAnalyzer.AreEquivalent(diagnostics1, diagnostics2)); + + // Verify that not all collections are treated as equivalent. + diagnostics1 = new[] { diagnostics1[0] }; + diagnostics2 = new[] { diagnostics2[1] }; + + Assert.NotEqual(diagnostics1, diagnostics2); + Assert.False(DiagnosticIncrementalAnalyzer.AreEquivalent(diagnostics1, diagnostics2)); +#endif + } + + private static void AssertDiagnostics(ImmutableArray items1, ImmutableArray items2) + { + Assert.Equal(items1.Length, items2.Length); + + for (var i = 0; i < items1.Length; i++) + { + AssertDiagnostics(items1[i], items2[i]); + } + } + + private static void AssertDiagnostics(DiagnosticData item1, DiagnosticData item2) + { + Assert.Equal(item1.Id, item2.Id); + Assert.Equal(item1.Category, item2.Category); + Assert.Equal(item1.Message, item2.Message); + Assert.Equal(item1.ENUMessageForBingSearch, item2.ENUMessageForBingSearch); + Assert.Equal(item1.Severity, item2.Severity); + Assert.Equal(item1.IsEnabledByDefault, item2.IsEnabledByDefault); + Assert.Equal(item1.WarningLevel, item2.WarningLevel); + Assert.Equal(item1.DefaultSeverity, item2.DefaultSeverity); + + Assert.Equal(item1.CustomTags.Count, item2.CustomTags.Count); + for (var j = 0; j < item1.CustomTags.Count; j++) + { + Assert.Equal(item1.CustomTags[j], item2.CustomTags[j]); + } + + Assert.Equal(item1.Properties.Count, item2.Properties.Count); + Assert.True(item1.Properties.SetEquals(item2.Properties)); + + Assert.Equal(item1.Workspace, item2.Workspace); + Assert.Equal(item1.ProjectId, item2.ProjectId); + Assert.Equal(item1.DocumentId, item2.DocumentId); + + Assert.Equal(item1.HasTextSpan, item2.HasTextSpan); + if (item1.HasTextSpan) + { + Assert.Equal(item1.TextSpan, item2.TextSpan); + } + + Assert.Equal(item1.DataLocation?.MappedFilePath, item2.DataLocation?.MappedFilePath); + Assert.Equal(item1.DataLocation?.MappedStartLine, item2.DataLocation?.MappedStartLine); + Assert.Equal(item1.DataLocation?.MappedStartColumn, item2.DataLocation?.MappedStartColumn); + Assert.Equal(item1.DataLocation?.MappedEndLine, item2.DataLocation?.MappedEndLine); + Assert.Equal(item1.DataLocation?.MappedEndColumn, item2.DataLocation?.MappedEndColumn); + + Assert.Equal(item1.DataLocation?.OriginalFilePath, item2.DataLocation?.OriginalFilePath); + Assert.Equal(item1.DataLocation?.OriginalStartLine, item2.DataLocation?.OriginalStartLine); + Assert.Equal(item1.DataLocation?.OriginalStartColumn, item2.DataLocation?.OriginalStartColumn); + Assert.Equal(item1.DataLocation?.OriginalEndLine, item2.DataLocation?.OriginalEndLine); + Assert.Equal(item1.DataLocation?.OriginalEndColumn, item2.DataLocation?.OriginalEndColumn); + + Assert.Equal(item1.Description, item2.Description); + Assert.Equal(item1.HelpLink, item2.HelpLink); + } + + [ExportWorkspaceServiceFactory(typeof(IPersistentStorageService), "DiagnosticDataSerializerTest"), Shared] + public class PersistentStorageServiceFactory : IWorkspaceServiceFactory + { + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + return new Service(); + } + + public class Service : IPersistentStorageService + { + private readonly Storage _instance = new Storage(); + + IPersistentStorage IPersistentStorageService.GetStorage(Solution solution) + { + return _instance; + } + + internal class Storage : IPersistentStorage + { + private readonly Dictionary _map = new Dictionary(); + + public Task ReadStreamAsync(string name, CancellationToken cancellationToken = default(CancellationToken)) + { + var stream = _map[name]; + stream.Position = 0; + + return Task.FromResult(stream); + } + + public Task ReadStreamAsync(Project project, string name, CancellationToken cancellationToken = default(CancellationToken)) + { + var stream = _map[Tuple.Create(project, name)]; + stream.Position = 0; + + return Task.FromResult(stream); + } + + public Task ReadStreamAsync(Document document, string name, CancellationToken cancellationToken = default(CancellationToken)) + { + var stream = _map[Tuple.Create(document, name)]; + stream.Position = 0; + + return Task.FromResult(stream); + } + + public Task WriteStreamAsync(string name, Stream stream, CancellationToken cancellationToken = default(CancellationToken)) + { + _map[name] = new MemoryStream(); + stream.CopyTo(_map[name]); + + return SpecializedTasks.True; + } + + public Task WriteStreamAsync(Project project, string name, Stream stream, CancellationToken cancellationToken = default(CancellationToken)) + { + _map[Tuple.Create(project, name)] = new MemoryStream(); + stream.CopyTo(_map[Tuple.Create(project, name)]); + + return SpecializedTasks.True; + } + + public Task WriteStreamAsync(Document document, string name, Stream stream, CancellationToken cancellationToken = default(CancellationToken)) + { + _map[Tuple.Create(document, name)] = new MemoryStream(); + stream.CopyTo(_map[Tuple.Create(document, name)]); + + return SpecializedTasks.True; + } + + protected virtual void Dispose(bool disposing) + { + } + + public void Dispose() + { + Dispose(true); + } + } + } + } + } +} diff --git a/src/EditorFeatures/Test/Diagnostics/TestDiagnosticAnalyzerService.cs b/src/EditorFeatures/Test/Diagnostics/TestDiagnosticAnalyzerService.cs index 4a598c4cbaebe..6cf13eb6c5115 100644 --- a/src/EditorFeatures/Test/Diagnostics/TestDiagnosticAnalyzerService.cs +++ b/src/EditorFeatures/Test/Diagnostics/TestDiagnosticAnalyzerService.cs @@ -1,6 +1,7 @@ // 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 Microsoft.CodeAnalysis.Diagnostics.Log; using Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics; @@ -11,6 +12,7 @@ namespace Microsoft.CodeAnalysis.Diagnostics internal sealed class TestDiagnosticAnalyzerService : DiagnosticAnalyzerService { private readonly Action _onAnalyzerException; + private readonly ImmutableDictionary _hostAnalyzerReferenceMap; internal TestDiagnosticAnalyzerService( string language, @@ -54,6 +56,7 @@ private TestDiagnosticAnalyzerService( IDiagnosticUpdateSourceRegistrationService registrationService = null) : base(hostAnalyzerManager, hostDiagnosticUpdateSource, registrationService ?? new MockDiagnosticUpdateSourceRegistrationService()) { + _hostAnalyzerReferenceMap = hostAnalyzerManager.CreateAnalyzerReferencesMap(projectOpt: null); _onAnalyzerException = onAnalyzerException; } @@ -92,5 +95,7 @@ internal override Action GetOnAnalyze { return _onAnalyzerException ?? base.GetOnAnalyzerException(projectId, diagnosticLogAggregator); } + + internal IEnumerable HostAnalyzerReferences => _hostAnalyzerReferenceMap.Values; } } diff --git a/src/EditorFeatures/Test/EditorServicesTest.csproj b/src/EditorFeatures/Test/EditorServicesTest.csproj index 23c80b95d3679..204932a3565d2 100644 --- a/src/EditorFeatures/Test/EditorServicesTest.csproj +++ b/src/EditorFeatures/Test/EditorServicesTest.csproj @@ -230,6 +230,7 @@ + @@ -369,4 +370,4 @@ - + \ No newline at end of file diff --git a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb index 1d7e6b84de81c..4430d7c6a12bc 100644 --- a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb +++ b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb @@ -117,14 +117,12 @@ Namespace Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics.UnitTests Assert.Equal(workspaceDiagnosticAnalyzer.DiagDescriptor.Id, descriptors(0).Id) ' Add an existing workspace analyzer to the project, ensure no duplicate diagnostics. - Dim duplicateProjectAnalyzers = ImmutableArray.Create(Of DiagnosticAnalyzer)(workspaceDiagnosticAnalyzer) - Dim duplicateProjectAnalyzersReference = New AnalyzerImageReference(duplicateProjectAnalyzers) + Dim duplicateProjectAnalyzersReference = diagnosticService.HostAnalyzerReferences.FirstOrDefault() project = project.WithAnalyzerReferences({duplicateProjectAnalyzersReference}) ' Verify duplicate descriptors or diagnostics. - ' We don't do de-duplication of analyzer that belong to different layer (host and project) descriptorsMap = diagnosticService.GetDiagnosticDescriptors(project) - Assert.Equal(2, descriptorsMap.Count) + Assert.Equal(1, descriptorsMap.Count) descriptors = descriptorsMap.Values.SelectMany(Function(d) d).OrderBy(Function(d) d.Id).ToImmutableArray() Assert.Equal(workspaceDiagnosticAnalyzer.DiagDescriptor.Id, descriptors(0).Id) @@ -132,7 +130,7 @@ Namespace Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics.UnitTests diagnostics = diagnosticService.GetDiagnosticsForSpanAsync(document, document.GetSyntaxRootAsync().WaitAndGetResult(CancellationToken.None).FullSpan ).WaitAndGetResult(CancellationToken.None) - Assert.Equal(2, diagnostics.Count()) + Assert.Equal(1, diagnostics.Count()) End Using End Sub @@ -732,18 +730,22 @@ class AnonymousFunctions Assert.Equal(1, diagnostics.Count()) Assert.Equal(document.Id, diagnostics.First().DocumentId) + ' REVIEW: GetProjectDiagnosticsForIdsAsync is for project diagnostics with no location. not sure why in v1, this API returns + ' diagnostic with location? + ' Verify compilation diagnostics are reported with correct location info when asked for project diagnostics. - Dim projectDiagnostics = diagnosticService.GetProjectDiagnosticsForIdsAsync(project.Solution, project.Id).WaitAndGetResult(CancellationToken.None) + Dim projectDiagnostics = diagnosticService.GetDiagnosticsForIdsAsync(project.Solution, project.Id).WaitAndGetResult(CancellationToken.None) Assert.Equal(2, projectDiagnostics.Count()) - Dim noLocationDiagnostic = projectDiagnostics.First() + Dim noLocationDiagnostic = projectDiagnostics.First(Function(d) Not d.HasTextSpan) Assert.Equal(CompilationEndedAnalyzer.Descriptor.Id, noLocationDiagnostic.Id) Assert.Equal(False, noLocationDiagnostic.HasTextSpan) - Dim withDocumentLocationDiagnostic = projectDiagnostics.Last() + Dim withDocumentLocationDiagnostic = projectDiagnostics.First(Function(d) d.HasTextSpan) Assert.Equal(CompilationEndedAnalyzer.Descriptor.Id, withDocumentLocationDiagnostic.Id) Assert.Equal(True, withDocumentLocationDiagnostic.HasTextSpan) Assert.NotNull(withDocumentLocationDiagnostic.DocumentId) + Dim diagnosticDocument = project.GetDocument(withDocumentLocationDiagnostic.DocumentId) Dim tree = diagnosticDocument.GetSyntaxTreeAsync().Result Dim actualLocation = withDocumentLocationDiagnostic.ToDiagnosticAsync(project, CancellationToken.None).Result.Location 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/EngineV1/DiagnosticAnalyzerCategory.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerCategory.cs similarity index 100% rename from src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticAnalyzerCategory.cs rename to src/Features/Core/Portable/Diagnostics/DiagnosticAnalyzerCategory.cs 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 fd96677ce8dcf..03f4692a885bf 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticAnalyzerDriver.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticAnalyzerDriver.cs @@ -295,12 +295,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(); } @@ -311,29 +314,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.NestedTypes.cs b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.NestedTypes.cs index dbec975cdf9ee..45fc27292bc78 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.NestedTypes.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.NestedTypes.cs @@ -1,9 +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; using System.Collections.Immutable; -using System.Linq; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics.EngineV1 { @@ -82,52 +79,5 @@ public VersionArgument(VersionStamp textVersion, VersionStamp dataVersion, Versi this.ProjectVersion = projectVersion; } } - - public class HostAnalyzerKey : ArgumentKey - { - private readonly string _analyzerPackageName; - - public HostAnalyzerKey(DiagnosticAnalyzer analyzer, StateType stateType, object key, string analyzerPackageName) : - base(analyzer, stateType, key) - { - _analyzerPackageName = analyzerPackageName; - } - - public override string BuildTool - { - get - { - return _analyzerPackageName; - } - } - } - - public class ArgumentKey : AnalyzerUpdateArgsId - { - public readonly StateType StateType; - public readonly object Key; - - public ArgumentKey(DiagnosticAnalyzer analyzer, StateType stateType, object key) : base(analyzer) - { - StateType = stateType; - Key = key; - } - - public override bool Equals(object obj) - { - var other = obj as ArgumentKey; - if (other == null) - { - return false; - } - - return StateType == other.StateType && Equals(Key, other.Key) && base.Equals(obj); - } - - public override int GetHashCode() - { - return Hash.Combine(Key, Hash.Combine((int)StateType, base.GetHashCode())); - } - } } } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateManager.cs b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateManager.cs index 0bbe0a1e3d3b9..96ac1e9f691d3 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateManager.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateManager.cs @@ -1,7 +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; -using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; @@ -69,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 @@ -127,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.cs b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.cs index ad69a6a3c4510..e0f5c198985b5 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.cs @@ -824,7 +824,7 @@ private void RaiseDiagnosticsCreated( StateType type, object key, StateSet stateSet, SolutionArgument solution, ImmutableArray diagnostics, Action raiseEvents) { // get right arg id for the given analyzer - var id = CreateArgumentKey(type, key, stateSet); + var id = new LiveDiagnosticUpdateArgsId(stateSet.Analyzer, key, (int)type, stateSet.ErrorSourceName); raiseEvents(DiagnosticsUpdatedArgs.DiagnosticsCreated(id, Workspace, solution.Solution, solution.ProjectId, solution.DocumentId, diagnostics)); } @@ -838,7 +838,7 @@ private void RaiseDiagnosticsRemoved( StateType type, object key, StateSet stateSet, SolutionArgument solution, Action raiseEvents) { // get right arg id for the given analyzer - var id = CreateArgumentKey(type, key, stateSet); + var id = new LiveDiagnosticUpdateArgsId(stateSet.Analyzer, key, (int)type, stateSet.ErrorSourceName); raiseEvents(DiagnosticsUpdatedArgs.DiagnosticsRemoved(id, Workspace, solution.Solution, solution.ProjectId, solution.DocumentId)); } @@ -868,13 +868,6 @@ private void RaiseProjectDiagnosticsRemoved(Project project, IEnumerable UpdateDocumentDiagnostics( AnalysisData existingData, ImmutableArray range, ImmutableArray memberDiagnostics, SyntaxTree tree, SyntaxNode member, int memberId) 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 dd1054d5a5819..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; } @@ -295,7 +295,7 @@ public async Task> GetSpecificDiagnosticsAsync(Ca return ImmutableArray.Empty; } - var key = Id as ArgumentKey; + var key = Id as LiveDiagnosticUpdateArgsId; if (key == null) { return ImmutableArray.Empty; @@ -316,7 +316,7 @@ public async Task> GetSpecificDiagnosticsAsync(Ca return ImmutableArray.Empty; } - var state = stateSet.GetState(key.StateType); + var state = stateSet.GetState((StateType)key.Kind); if (state == null) { return ImmutableArray.Empty; @@ -584,7 +584,7 @@ public async Task> GetSpecificDiagnosticsAsync(Ca return ImmutableArray.Empty; } - var key = Id as ArgumentKey; + var key = Id as LiveDiagnosticUpdateArgsId; if (key == null) { return ImmutableArray.Empty; @@ -597,7 +597,7 @@ public async Task> GetSpecificDiagnosticsAsync(Ca return ImmutableArray.Empty; } - if (key.StateType != StateType.Project) + if (key.Kind != (int)StateType.Project) { return await GetSpecificDiagnosticsAsync(documentOrProject, key, cancellationToken).ConfigureAwait(false); } @@ -605,9 +605,9 @@ public async Task> GetSpecificDiagnosticsAsync(Ca return await GetSpecificDiagnosticsAsync(GetProject(documentOrProject), key, cancellationToken).ConfigureAwait(false); } - private async Task> GetSpecificDiagnosticsAsync(object documentOrProject, ArgumentKey key, CancellationToken cancellationToken) + private async Task> GetSpecificDiagnosticsAsync(object documentOrProject, LiveDiagnosticUpdateArgsId key, CancellationToken cancellationToken) { - var versions = await GetVersionsAsync(documentOrProject, key.StateType, cancellationToken).ConfigureAwait(false); + var versions = await GetVersionsAsync(documentOrProject, (StateType)key.Kind, cancellationToken).ConfigureAwait(false); var project = GetProject(documentOrProject); var stateSet = this.StateManager.GetOrCreateStateSet(project, key.Analyzer); @@ -617,10 +617,10 @@ private async Task> GetSpecificDiagnosticsAsync(o } var analyzers = Owner._stateManager.GetOrCreateAnalyzers(project); - var driver = await GetDiagnosticAnalyzerDriverAsync(documentOrProject, analyzers, key.StateType, cancellationToken).ConfigureAwait(false); + var driver = await GetDiagnosticAnalyzerDriverAsync(documentOrProject, analyzers, (StateType)key.Kind, cancellationToken).ConfigureAwait(false); - var analysisData = await GetDiagnosticAnalysisDataAsync(driver, stateSet, key.StateType, versions).ConfigureAwait(false); - if (key.StateType != StateType.Project) + var analysisData = await GetDiagnosticAnalysisDataAsync(driver, stateSet, (StateType)key.Kind, versions).ConfigureAwait(false); + if (key.Kind != (int)StateType.Project) { return analysisData.Items; } 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..f666a0c0af1d5 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/AnalysisResult.cs @@ -0,0 +1,143 @@ +// 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.Linq; +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) : this( + projectId, version, + documentIds: ImmutableHashSet.Empty, + syntaxLocals: ImmutableDictionary>.Empty, + semanticLocals: ImmutableDictionary>.Empty, + nonLocals: ImmutableDictionary>.Empty, + others: ImmutableArray.Empty) + { + } + + 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, + ImmutableDictionary> syntaxLocals, + ImmutableDictionary> semanticLocals, + ImmutableDictionary> nonLocals, + ImmutableArray others, + ImmutableHashSet documentIds) + { + ProjectId = projectId; + Version = version; + + _syntaxLocals = syntaxLocals; + _semanticLocals = semanticLocals; + _nonLocals = nonLocals; + _others = others; + + DocumentIds = documentIds; + IsEmpty = false; + + // do after all fields are assigned. + DocumentIds = DocumentIds ?? CreateDocumentIds(); + IsEmpty = DocumentIds.IsEmpty && _others.IsEmpty; + } + + // aggregated form means it has aggregated information but no actual data. + public bool IsAggregatedForm => _syntaxLocals == null; + + // default analysis result + public bool IsDefault => DocumentIds == null; + + // make sure we don't return null + public ImmutableHashSet DocumentIdsOrEmpty => DocumentIds ?? ImmutableHashSet.Empty; + + // this shouldn't be called for aggregated form. + public ImmutableDictionary> SyntaxLocals => ReturnIfNotDefault(_syntaxLocals); + public ImmutableDictionary> SemanticLocals => ReturnIfNotDefault(_semanticLocals); + public ImmutableDictionary> NonLocals => ReturnIfNotDefault(_nonLocals); + public ImmutableArray Others => ReturnIfNotDefault(_others); + + public ImmutableArray GetResultOrEmpty(ImmutableDictionary> map, DocumentId key) + { + // this is just a helper method. + ImmutableArray diagnostics; + if (map.TryGetValue(key, out diagnostics)) + { + Contract.ThrowIfFalse(DocumentIds.Contains(key)); + return diagnostics; + } + + return ImmutableArray.Empty; + } + + public AnalysisResult ToAggregatedForm() + { + return new AnalysisResult(ProjectId, Version, DocumentIds, IsEmpty); + } + + private T ReturnIfNotDefault(T value) + { + if (object.Equals(value, default(T))) + { + Contract.Fail("shouldn't be called"); + } + + return value; + } + + private ImmutableHashSet CreateDocumentIds() + { + var documents = SpecializedCollections.EmptyEnumerable(); + if (_syntaxLocals != null) + { + documents = documents.Concat(_syntaxLocals.Keys); + } + + if (_semanticLocals != null) + { + documents = documents.Concat(_semanticLocals.Keys); + } + + if (_nonLocals != null) + { + documents = documents.Concat(_nonLocals.Keys); + } + + return ImmutableHashSet.CreateRange(documents); + } + } +} \ 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 new file mode 100644 index 0000000000000..a6889e9df2d64 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/CompilerDiagnosticExecutor.cs @@ -0,0 +1,297 @@ +// 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 Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + /// + /// Diagnostic Executor that only relies on compiler layer. this might be replaced by new CompilationWithAnalyzer API. + /// + internal static class CompilerDiagnosticExecutor + { + public static async Task> AnalyzeAsync(this CompilationWithAnalyzers analyzerDriver, Project project, CancellationToken cancellationToken) + { + 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? + // REVIEW: we drop all those allocations for the diagnostics returned. can we avoid this? + await analyzerDriver.GetAnalyzerDiagnosticsAsync(cancellationToken).ConfigureAwait(false); + + // 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 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); + + foreach (var tree in compilation.SyntaxTrees) + { + var model = compilation.GetSemanticModel(tree); + + var syntax = await analyzerDriver.GetAnalyzerSyntaxDiagnosticsAsync(tree, oneAnalyzers, cancellationToken).ConfigureAwait(false); + Contract.Requires(syntax.Count() == CompilationWithAnalyzers.GetEffectiveDiagnostics(syntax, analyzerDriver.Compilation).Count()); + result.AddSyntaxDiagnostics(tree, syntax); + + var semantic = await analyzerDriver.GetAnalyzerSemanticDiagnosticsAsync(model, noSpanFilter, oneAnalyzers, cancellationToken).ConfigureAwait(false); + Contract.Requires(semantic.Count() == CompilationWithAnalyzers.GetEffectiveDiagnostics(semantic, analyzerDriver.Compilation).Count()); + result.AddSemanticDiagnostics(tree, semantic); + } + + var rest = await analyzerDriver.GetAnalyzerCompilationDiagnosticsAsync(oneAnalyzers, cancellationToken).ConfigureAwait(false); + Contract.Requires(rest.Count() == CompilationWithAnalyzers.GetEffectiveDiagnostics(rest, analyzerDriver.Compilation).Count()); + result.AddCompilationDiagnostics(rest); + + builder.Add(analyzer, result.ToResult()); + } + + return builder.ToImmutable(); + } + + /// + /// 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 + { + private readonly Project _project; + private readonly VersionStamp _version; + + 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, syntaxLocals, semanticLocals, nonLocals, others, documentIds); + } + + private ImmutableDictionary> Convert(Dictionary> map) + { + return map == null ? ImmutableDictionary>.Empty : map.ToImmutableDictionary(kv => kv.Key, kv => kv.Value.ToImmutableArray()); + } + + public void AddExternalSyntaxDiagnostics(DocumentId documentId, IEnumerable diagnostics) + { + // this is for diagnostic producer that doesnt use compiler based DiagnosticAnalyzer such as TypeScript. + AddExternalDiagnostics(ref _lazySyntaxLocals, documentId, diagnostics); + } + + public void AddExternalSemanticDiagnostics(DocumentId documentId, IEnumerable diagnostics) + { + // this is for diagnostic producer that doesnt use compiler based DiagnosticAnalyzer such as TypeScript. + AddExternalDiagnostics(ref _lazySemanticLocals, documentId, diagnostics); + } + + private void AddExternalDiagnostics( + ref Dictionary> lazyLocals, DocumentId documentId, IEnumerable diagnostics) + { + Contract.ThrowIfTrue(_project.SupportsCompilation); + + foreach (var diagnostic in diagnostics) + { + // REVIEW: what is our plan for additional locations? + switch (diagnostic.Location.Kind) + { + case LocationKind.ExternalFile: + { + var diagnosticDocumentId = GetExternalDocumentId(diagnostic); + if (documentId == diagnosticDocumentId) + { + var document = _project.GetDocument(diagnosticDocumentId); + if (document != null) + { + // local diagnostics to a file + lazyLocals = lazyLocals ?? new Dictionary>(); + lazyLocals.GetOrAdd(document.Id, _ => new List()).Add(DiagnosticData.Create(document, diagnostic)); + + AddDocumentToSet(document); + } + } + else if (diagnosticDocumentId != null) + { + var document = _project.GetDocument(diagnosticDocumentId); + if (document != null) + { + // non local diagnostics to a file + _lazyNonLocals = _lazyNonLocals ?? new Dictionary>(); + _lazyNonLocals.GetOrAdd(document.Id, _ => new List()).Add(DiagnosticData.Create(document, diagnostic)); + + AddDocumentToSet(document); + } + } + else + { + // non local diagnostics without location + _lazyOthers = _lazyOthers ?? new List(); + _lazyOthers.Add(DiagnosticData.Create(_project, diagnostic)); + } + + break; + } + case LocationKind.None: + { + _lazyOthers = _lazyOthers ?? new List(); + _lazyOthers.Add(DiagnosticData.Create(_project, diagnostic)); + break; + } + case LocationKind.SourceFile: + case LocationKind.MetadataFile: + case LocationKind.XmlFile: + { + // something we don't care + continue; + } + default: + { + Contract.Fail("should not reach"); + break; + } + } + } + } + + public void AddSyntaxDiagnostics(SyntaxTree tree, IEnumerable diagnostics) + { + AddDiagnostics(ref _lazySyntaxLocals, tree, diagnostics); + } + + public void AddSemanticDiagnostics(SyntaxTree tree, IEnumerable diagnostics) + { + AddDiagnostics(ref _lazySemanticLocals, tree, diagnostics); + } + + public void AddCompilationDiagnostics(IEnumerable diagnostics) + { + Dictionary> dummy = null; + AddDiagnostics(ref dummy, tree: null, diagnostics: diagnostics); + + // dummy should be always null + Contract.Requires(dummy == null); + } + + private void AddDiagnostics( + ref Dictionary> lazyLocals, SyntaxTree tree, IEnumerable diagnostics) + { + foreach (var diagnostic in diagnostics) + { + // REVIEW: what is our plan for additional locations? + switch (diagnostic.Location.Kind) + { + case LocationKind.ExternalFile: + { + // 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) + { + 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)); + + AddDocumentToSet(document); + } + } + else if (diagnostic.Location.SourceTree != null) + { + 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)); + + AddDocumentToSet(document); + } + } + else + { + // non local diagnostics without location + _lazyOthers = _lazyOthers ?? new List(); + _lazyOthers.Add(DiagnosticData.Create(_project, diagnostic)); + } + + break; + } + case LocationKind.MetadataFile: + case LocationKind.XmlFile: + { + // something we don't care + continue; + } + default: + { + Contract.Fail("should not reach"); + break; + } + } + } + } + + private void AddDocumentToSet(Document document) + { + _lazySet = _lazySet ?? new HashSet(); + _lazySet.Add(document.Id); + } + + private Document GetDocument(Diagnostic diagnostic) + { + return _project.GetDocument(diagnostic.Location.SourceTree); + } + + private DocumentId GetExternalDocumentId(Diagnostic diagnostic) + { + var projectId = _project.Id; + var lineSpan = diagnostic.Location.GetLineSpan(); + + return _project.Solution.GetDocumentIdsWithFilePath(lineSpan.Path).FirstOrDefault(id => id.ProjectId == projectId); + } + } + } +} 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 new file mode 100644 index 0000000000000..d20fdceba5324 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ActiveFileState.cs @@ -0,0 +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 Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + /// + /// 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..39fc6dad93646 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.AnalysisData.cs @@ -0,0 +1,149 @@ +// 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 + { + /// + /// Simple data holder for local diagnostics for an analyzer + /// + private struct DocumentAnalysisData + { + public static readonly DocumentAnalysisData Empty = new DocumentAnalysisData(VersionStamp.Default, ImmutableArray.Empty); + + /// + /// Version of the Items + /// + public readonly VersionStamp Version; + + /// + /// Current data that matches the version + /// + public readonly ImmutableArray Items; + + /// + /// When present, This hold onto last data we broadcast to outer world + /// + public readonly ImmutableArray OldItems; + + 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; } + } + } + + /// + /// Data holder for all diagnostics for a project for an analyzer + /// + private struct ProjectAnalysisData + { + /// + /// ProjectId of this data + /// + public readonly ProjectId ProjectId; + + /// + /// Version of the Items + /// + public readonly VersionStamp Version; + + /// + /// Current data that matches the version + /// + public readonly ImmutableDictionary Result; + + /// + /// When present, This hold onto last data we broadcast to outer world + /// + public readonly ImmutableDictionary OldResult; + + public ProjectAnalysisData(ProjectId projectId, VersionStamp version, ImmutableDictionary result) + { + ProjectId = projectId; + Version = version; + Result = result; + + OldResult = null; + } + + public ProjectAnalysisData( + ProjectId projectId, + VersionStamp version, + ImmutableDictionary oldResult, + ImmutableDictionary newResult) : + this(projectId, version, newResult) + { + this.OldResult = oldResult; + } + + public AnalysisResult GetResult(DiagnosticAnalyzer analyzer) + { + return GetResultOrEmpty(Result, analyzer, ProjectId, Version); + } + + 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); + Contract.ThrowIfFalse(project.Id == result.ProjectId); + + if (!version.HasValue) + { + if (result.Version != VersionStamp.Default) + { + version = result.Version; + } + } + else + { + // all version must be same or default (means not there yet) + Contract.Requires(version == result.Version || result.Version == VersionStamp.Default); + } + + builder.Add(stateSet.Analyzer, result); + } + + if (!version.HasValue) + { + // there is no saved data to return. + return new ProjectAnalysisData(project.Id, VersionStamp.Default, ImmutableDictionary.Empty); + } + + return new ProjectAnalysisData(project.Id, 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..125f42f18bb6d --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.CompilationManager.cs @@ -0,0 +1,218 @@ +// 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) + { + if (!project.SupportsCompilation) + { + return null; + } + + CompilationWithAnalyzers analyzerDriverOpt; + if (_map.TryGetValue(project, out analyzerDriverOpt)) + { + // we have cached one, return that. + AssertAnalyzers(analyzerDriverOpt, stateSets); + return analyzerDriverOpt; + } + + // Create driver that holds onto compilation and associated analyzers + var includeSuppressedDiagnostics = true; + var newAnalyzerDriverOpt = await CreateAnalyzerDriverAsync(project, stateSets, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + + // Add new analyzer driver to the map + analyzerDriverOpt = _map.GetValue(project, _ => newAnalyzerDriverOpt); + + // if somebody has beat us, make sure analyzers are good. + if (analyzerDriverOpt != newAnalyzerDriverOpt) + { + AssertAnalyzers(analyzerDriverOpt, stateSets); + } + + // return driver + return analyzerDriverOpt; + } + + public Task CreateAnalyzerDriverAsync(Project project, IEnumerable stateSets, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) + { + var analyzers = stateSets.Select(s => s.Analyzer).ToImmutableArrayOrEmpty(); + return CreateAnalyzerDriverAsync(project, analyzers, includeSuppressedDiagnostics, cancellationToken); + } + + public async Task CreateAnalyzerDriverAsync( + Project project, ImmutableArray analyzers, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) + { + if (!project.SupportsCompilation) + { + return null; + } + + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + + // Create driver that holds onto compilation and associated analyzers + return CreateAnalyzerDriver( + project, compilation, analyzers, logAnalyzerExecutionTime: false, reportSuppressedDiagnostics: includeSuppressedDiagnostics); + } + + private CompilationWithAnalyzers CreateAnalyzerDriver( + Project project, + Compilation compilation, + ImmutableArray analyzers, + bool logAnalyzerExecutionTime, + bool reportSuppressedDiagnostics) + { + // PERF: there is no analyzers for this compilation. + // compilationWithAnalyzer will throw if it is created with no analyzers which is perf optimization. + if (analyzers.IsEmpty) + { + return null; + } + + Contract.ThrowIfFalse(project.SupportsCompilation); + AssertCompilation(project, compilation); + + var analysisOptions = GetAnalyzerOptions(project, logAnalyzerExecutionTime, reportSuppressedDiagnostics); + + // Create driver that holds onto compilation and associated analyzers + return compilation.WithAnalyzers(analyzers, analysisOptions); + } + + private CompilationWithAnalyzersOptions GetAnalyzerOptions( + Project project, + bool logAnalyzerExecutionTime, + bool reportSuppressedDiagnostics) + { + // in IDE, we always set concurrentAnalysis == false otherwise, we can get into thread starvation due to + // async being used with syncronous blocking concurrency. + return new CompilationWithAnalyzersOptions( + options: new WorkspaceAnalyzerOptions(project.AnalyzerOptions, project.Solution.Workspace), + onAnalyzerException: GetOnAnalyzerException(project.Id), + analyzerExceptionFilter: GetAnalyzerExceptionFilter(project), + concurrentAnalysis: false, + 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) + { + if (analyzerDriver == null) + { + // this can happen if project doesn't support compilation or no stateSets are given. + return; + } + + // 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..f7fdf0cb80cd5 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs @@ -0,0 +1,386 @@ +// 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; + + if (project.SupportsCompilation) + { + return ConvertToLocalDiagnosticsWithCompilation(targetDocument, diagnostics, span); + } + + return ConvertToLocalDiagnosticsWithoutCompilation(targetDocument, diagnostics, span); + } + + /// + /// Return all local diagnostics (syntax, semantic) that belong to given document for the given StateSet (analyzer) either from cache or by calculating them + /// + public async Task GetDocumentAnalysisDataAsync( + CompilationWithAnalyzers analyzerDriverOpt, 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(analyzerDriverOpt, 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; + } + } + + /// + /// Return all diagnostics that belong to given project for the given StateSets (analyzers) either from cache or by calculating them + /// + public async Task GetProjectAnalysisDataAsync( + CompilationWithAnalyzers analyzerDriverOpt, 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(project.Id, version, existingData.Result, ImmutableDictionary.Empty); + } + + var result = await ComputeDiagnosticsAsync(analyzerDriverOpt, project, stateSets, cancellationToken).ConfigureAwait(false); + + return new ProjectAnalysisData(project.Id, version, existingData.Result, result); + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + + /// + /// Return all local diagnostics (syntax, semantic) that belong to given document for the given StateSet (analyzer) by calculating them + /// + public async Task> ComputeDiagnosticsAsync( + CompilationWithAnalyzers analyzerDriverOpt, 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, analyzerDriverOpt?.Compilation, cancellationToken).ConfigureAwait(false); + return ConvertToLocalDiagnostics(document, diagnostics); + } + + var documentDiagnostics = await ComputeDiagnosticAnalyzerDiagnosticsAsync(analyzerDriverOpt, document, analyzer, kind, spanOpt, cancellationToken).ConfigureAwait(false); + return ConvertToLocalDiagnostics(document, documentDiagnostics); + } + + /// + /// Return all diagnostics that belong to given project for the given StateSets (analyzers) by calculating them + /// + public async Task> ComputeDiagnosticsAsync( + CompilationWithAnalyzers analyzerDriverOpt, Project project, IEnumerable stateSets, CancellationToken cancellationToken) + { + var result = ImmutableDictionary.Empty; + + // analyzerDriver can be null if given project doesn't support compilation. + if (analyzerDriverOpt != null) + { + // calculate regular diagnostic analyzers diagnostics + result = await analyzerDriverOpt.AnalyzeAsync(project, cancellationToken).ConfigureAwait(false); + + // record telemetry data + await UpdateAnalyzerTelemetryDataAsync(analyzerDriverOpt, project, cancellationToken).ConfigureAwait(false); + } + + // check whether there is IDE specific project diagnostic analyzer + return await MergeProjectDiagnosticAnalyzerDiagnosticsAsync(project, stateSets, analyzerDriverOpt?.Compilation, result, cancellationToken).ConfigureAwait(false); + } + + private async Task> MergeProjectDiagnosticAnalyzerDiagnosticsAsync( + Project project, IEnumerable stateSets, Compilation compilationOpt, ImmutableDictionary result, CancellationToken cancellationToken) + { + // check whether there is IDE specific project diagnostic analyzer + var ideAnalyzers = stateSets.Select(s => s.Analyzer).Where(a => a is ProjectDiagnosticAnalyzer || a is DocumentDiagnosticAnalyzer).ToImmutableArrayOrEmpty(); + if (ideAnalyzers.Length <= 0) + { + return result; + } + + // create result map + var version = await GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); + var builder = new CompilerDiagnosticExecutor.Builder(project, version); + + foreach (var analyzer in ideAnalyzers) + { + var documentAnalyzer = analyzer as DocumentDiagnosticAnalyzer; + if (documentAnalyzer != null) + { + foreach (var document in project.Documents) + { + if (document.SupportsSyntaxTree) + { + var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + builder.AddSyntaxDiagnostics(tree, await ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync(document, documentAnalyzer, AnalysisKind.Syntax, compilationOpt, cancellationToken).ConfigureAwait(false)); + builder.AddSemanticDiagnostics(tree, await ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync(document, documentAnalyzer, AnalysisKind.Semantic, compilationOpt, cancellationToken).ConfigureAwait(false)); + } + else + { + builder.AddExternalSyntaxDiagnostics(document.Id, await ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync(document, documentAnalyzer, AnalysisKind.Syntax, compilationOpt, cancellationToken).ConfigureAwait(false)); + builder.AddExternalSemanticDiagnostics(document.Id, await ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync(document, documentAnalyzer, AnalysisKind.Semantic, compilationOpt, cancellationToken).ConfigureAwait(false)); + } + } + } + + var projectAnalyzer = analyzer as ProjectDiagnosticAnalyzer; + if (projectAnalyzer != null) + { + builder.AddCompilationDiagnostics(await ComputeProjectDiagnosticAnalyzerDiagnosticsAsync(project, projectAnalyzer, compilationOpt, cancellationToken).ConfigureAwait(false)); + } + + // merge the result to existing one. + result = result.Add(analyzer, builder.ToResult()); + } + + return result; + } + + private async Task> ComputeProjectDiagnosticAnalyzerDiagnosticsAsync( + Project project, ProjectDiagnosticAnalyzer analyzer, Compilation compilationOpt, CancellationToken cancellationToken) + { + using (var pooledObject = SharedPools.Default>().GetPooledObject()) + { + var diagnostics = pooledObject.Object; + cancellationToken.ThrowIfCancellationRequested(); + + try + { + await analyzer.AnalyzeProjectAsync(project, diagnostics.Add, cancellationToken).ConfigureAwait(false); + + // REVIEW: V1 doesn't convert diagnostics to effective diagnostics. not sure why. + return compilationOpt == null ? diagnostics.ToImmutableArrayOrEmpty() : CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, compilationOpt); + } + catch (Exception e) when (!IsCanceled(e, cancellationToken)) + { + OnAnalyzerException(analyzer, project.Id, compilationOpt, e); + return ImmutableArray.Empty; + } + } + } + + private async Task> ComputeDocumentDiagnosticAnalyzerDiagnosticsAsync( + Document document, DocumentDiagnosticAnalyzer analyzer, AnalysisKind kind, Compilation compilationOpt, 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 compilationOpt == null ? diagnostics.ToImmutableArrayOrEmpty() : CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, compilationOpt); + case AnalysisKind.Semantic: + await analyzer.AnalyzeSemanticsAsync(document, diagnostics.Add, cancellationToken).ConfigureAwait(false); + return compilationOpt == null ? diagnostics.ToImmutableArrayOrEmpty() : CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, compilationOpt); + default: + return Contract.FailWithReturn>("shouldn't reach here"); + } + } + catch (Exception e) when (!IsCanceled(e, cancellationToken)) + { + OnAnalyzerException(analyzer, document.Project.Id, compilationOpt, e); + return ImmutableArray.Empty; + } + } + } + + private async Task> ComputeDiagnosticAnalyzerDiagnosticsAsync( + CompilationWithAnalyzers analyzerDriverOpt, Document document, DiagnosticAnalyzer analyzer, AnalysisKind kind, TextSpan? spanOpt, CancellationToken cancellationToken) + { + // quick optimization to reduce allocations. + if (analyzerDriverOpt == null || !_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 analyzerDriverOpt.GetAnalyzerSyntaxDiagnosticsAsync(tree, oneAnalyzers, cancellationToken).ConfigureAwait(false); + + Contract.Requires(diagnostics.Count() == CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, analyzerDriverOpt.Compilation).Count()); + return diagnostics.ToImmutableArrayOrEmpty(); + case AnalysisKind.Semantic: + var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + diagnostics = await analyzerDriverOpt.GetAnalyzerSemanticDiagnosticsAsync(model, spanOpt, oneAnalyzers, cancellationToken).ConfigureAwait(false); + + Contract.Requires(diagnostics.Count() == CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, analyzerDriverOpt.Compilation).Count()); + return diagnostics.ToImmutableArrayOrEmpty(); + 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 compilationOpt, Exception ex) + { + var exceptionDiagnostic = AnalyzerHelper.CreateAnalyzerExceptionDiagnostic(analyzer, ex); + + if (compilationOpt != null) + { + exceptionDiagnostic = CompilationWithAnalyzers.GetEffectiveDiagnostics(ImmutableArray.Create(exceptionDiagnostic), compilationOpt).SingleOrDefault(); + } + + var onAnalyzerException = _owner.GetOnAnalyzerException(projectId); + onAnalyzerException(ex, analyzer, exceptionDiagnostic); + } + + private IEnumerable ConvertToLocalDiagnosticsWithoutCompilation(Document targetDocument, IEnumerable diagnostics, TextSpan? span = null) + { + var project = targetDocument.Project; + Contract.ThrowIfTrue(project.SupportsCompilation); + + foreach (var diagnostic in diagnostics) + { + var location = diagnostic.Location; + if (location.Kind != LocationKind.ExternalFile) + { + continue; + } + + var lineSpan = location.GetLineSpan(); + + var documentIds = project.Solution.GetDocumentIdsWithFilePath(lineSpan.Path); + if (documentIds.IsEmpty || documentIds.All(id => id != targetDocument.Id)) + { + continue; + } + + yield return DiagnosticData.Create(targetDocument, diagnostic); + } + } + + private IEnumerable ConvertToLocalDiagnosticsWithCompilation(Document targetDocument, IEnumerable diagnostics, TextSpan? span = null) + { + var project = targetDocument.Project; + Contract.ThrowIfFalse(project.SupportsCompilation); + + 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); + } + } + } + } +} 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..de82d860e8bba --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InMemoryStorage.cs @@ -0,0 +1,90 @@ +// 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) + { + var tuple = (ValueTuple)key; + Contract.ThrowIfFalse(tuple.Item1 is DocumentId || tuple.Item1 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 new file mode 100644 index 0000000000000..aadb80393b965 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectState.cs @@ -0,0 +1,409 @@ +// 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; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer + { + /// + /// 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, documentIds: null, isEmpty: true); + } + + public ImmutableHashSet GetDocumentsWithDiagnostics() + { + return _lastResult.DocumentIdsOrEmpty; + } + + public bool IsEmpty() + { + return _lastResult.IsEmpty; + } + + public bool IsEmpty(DocumentId documentId) + { + return IsEmpty(_lastResult, documentId); + } + + /// + /// Return all diagnostics for the given project stored in this state + /// + public async Task GetAnalysisDataAsync(Project project, bool avoidLoadingData, CancellationToken cancellationToken) + { + // make a copy of last result. + var lastResult = _lastResult; + Contract.ThrowIfFalse(lastResult.ProjectId == project.Id); + + if (lastResult.IsDefault) + { + return await LoadInitialAnalysisDataAsync(project, cancellationToken).ConfigureAwait(false); + } + + // PERF: avoid loading data if version is not right one. + // avoid loading data flag is there as a strictly perf optimization. + var version = await GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); + if (avoidLoadingData && lastResult.Version != version) + { + return lastResult; + } + + // if given project doesnt have any diagnostics, return empty. + if (lastResult.IsEmpty) + { + return new AnalysisResult(lastResult.ProjectId, lastResult.Version); + } + + // loading data can be cancelled any time. + var serializer = new DiagnosticDataSerializer(_owner.AnalyzerVersion, lastResult.Version); + var builder = new Builder(project.Id, lastResult.Version, lastResult.DocumentIds); + + foreach (var documentId in lastResult.DocumentIds) + { + cancellationToken.ThrowIfCancellationRequested(); + + var document = project.GetDocument(documentId); + if (document == null) + { + continue; + } + + if (!await TryDeserializeDocumentAsync(serializer, document, builder, cancellationToken).ConfigureAwait(false)) + { + Contract.Requires(false, "How this can happen?"); + continue; + } + } + + if (!await TryDeserializeAsync(serializer, project, project.Id, _owner.NonLocalStateName, builder.AddOthers, cancellationToken).ConfigureAwait(false)) + { + Contract.Requires(false, "How this can happen?"); + } + + return builder.ToResult(); + } + + /// + /// Return all diagnostics for the given document stored in this state including non local diagnostics for this document + /// + public async Task GetAnalysisDataAsync(Document document, bool avoidLoadingData, CancellationToken cancellationToken) + { + // make a copy of last result. + var lastResult = _lastResult; + Contract.ThrowIfFalse(lastResult.ProjectId == document.Project.Id); + + if (lastResult.IsDefault) + { + return await LoadInitialAnalysisDataAsync(document, cancellationToken).ConfigureAwait(false); + } + + var version = await GetDiagnosticVersionAsync(document.Project, cancellationToken).ConfigureAwait(false); + if (avoidLoadingData && lastResult.Version != version) + { + return lastResult; + } + + // if given document doesnt have any diagnostics, return empty. + if (IsEmpty(lastResult, document.Id)) + { + return new AnalysisResult(lastResult.ProjectId, lastResult.Version); + } + + // loading data can be cancelled any time. + var serializer = new DiagnosticDataSerializer(_owner.AnalyzerVersion, lastResult.Version); + var builder = new Builder(document.Project.Id, lastResult.Version); + + if (!await TryDeserializeDocumentAsync(serializer, document, builder, cancellationToken).ConfigureAwait(false)) + { + Contract.Requires(false, "How this can happen?"); + } + + return builder.ToResult(); + } + + /// + /// Return all no location diagnostics for the given project stored in this state + /// + public async Task GetProjectAnalysisDataAsync(Project project, bool avoidLoadingData, CancellationToken cancellationToken) + { + // make a copy of last result. + var lastResult = _lastResult; + Contract.ThrowIfFalse(lastResult.ProjectId == project.Id); + + if (lastResult.IsDefault) + { + return await LoadInitialProjectAnalysisDataAsync(project, cancellationToken).ConfigureAwait(false); + } + + var version = await GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); + if (avoidLoadingData && lastResult.Version != version) + { + return lastResult; + } + + // if given document doesnt have any diagnostics, return empty. + if (lastResult.IsEmpty) + { + return new AnalysisResult(lastResult.ProjectId, lastResult.Version); + } + + // loading data can be cancelled any time. + var serializer = new DiagnosticDataSerializer(_owner.AnalyzerVersion, lastResult.Version); + var builder = new Builder(project.Id, lastResult.Version); + + if (!await TryDeserializeAsync(serializer, project, project.Id, _owner.NonLocalStateName, builder.AddOthers, cancellationToken).ConfigureAwait(false)) + { + Contract.Requires(false, "How this can happen?"); + } + + return builder.ToResult(); + } + + public async Task SaveAsync(Project project, AnalysisResult result) + { + Contract.ThrowIfTrue(result.IsAggregatedForm); + + RemoveInMemoryCache(_lastResult); + + // 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) + { + RemoveInMemoryCacheEntries(id); + return !IsEmpty(id); + } + + public bool OnProjectRemoved(ProjectId id) + { + RemoveInMemoryCacheEntry(id, _owner.NonLocalStateName); + return !IsEmpty(); + } + + private async Task LoadInitialAnalysisDataAsync(Project project, CancellationToken cancellationToken) + { + // loading data can be cancelled any time. + var version = await GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); + var serializer = new DiagnosticDataSerializer(_owner.AnalyzerVersion, version); + var builder = new Builder(project.Id, version); + + foreach (var document in project.Documents) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!await TryDeserializeDocumentAsync(serializer, document, builder, cancellationToken).ConfigureAwait(false)) + { + continue; + } + } + + if (!await TryDeserializeAsync(serializer, project, project.Id, _owner.NonLocalStateName, builder.AddOthers, cancellationToken).ConfigureAwait(false)) + { + return new AnalysisResult(project.Id, VersionStamp.Default, ImmutableHashSet.Empty, isEmpty: true); + } + + return builder.ToResult(); + } + + private async Task LoadInitialAnalysisDataAsync(Document document, CancellationToken cancellationToken) + { + // loading data can be cancelled any time. + var project = document.Project; + + var version = await GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); + var serializer = new DiagnosticDataSerializer(_owner.AnalyzerVersion, version); + var builder = new Builder(project.Id, version); + + if (!await TryDeserializeDocumentAsync(serializer, document, builder, cancellationToken).ConfigureAwait(false)) + { + return new AnalysisResult(project.Id, VersionStamp.Default, ImmutableHashSet.Empty, isEmpty: true); + } + + return builder.ToResult(); + } + + private async Task LoadInitialProjectAnalysisDataAsync(Project project, CancellationToken cancellationToken) + { + // loading data can be cancelled any time. + var version = await GetDiagnosticVersionAsync(project, cancellationToken).ConfigureAwait(false); + var serializer = new DiagnosticDataSerializer(_owner.AnalyzerVersion, version); + var builder = new Builder(project.Id, version); + + if (!await TryDeserializeAsync(serializer, project, project.Id, _owner.NonLocalStateName, builder.AddOthers, cancellationToken).ConfigureAwait(false)) + { + return new AnalysisResult(project.Id, VersionStamp.Default, ImmutableHashSet.Empty, isEmpty: true); + } + + return builder.ToResult(); + } + + 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, stateKey); + return; + } + + // if serialization fail, hold it in the memory + InMemoryStorage.Cache(_owner.Analyzer, ValueTuple.Create(key, stateKey), new CacheEntry(serializer.Version, diagnostics)); + } + + private async Task TryDeserializeDocumentAsync(DiagnosticDataSerializer serializer, Document document, Builder builder, CancellationToken cancellationToken) + { + var result = true; + + result &= await TryDeserializeAsync(serializer, document, document.Id, _owner.SyntaxStateName, builder.AddSyntaxLocals, cancellationToken).ConfigureAwait(false); + result &= await TryDeserializeAsync(serializer, document, document.Id, _owner.SemanticStateName, builder.AddSemanticLocals, cancellationToken).ConfigureAwait(false); + result &= await TryDeserializeAsync(serializer, document, document.Id, _owner.NonLocalStateName, builder.AddNonLocals, cancellationToken).ConfigureAwait(false); + + return result; + } + + 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, ValueTuple.Create(key, stateKey), out entry) && serializer.Version == entry.Version) + { + return entry.Diagnostics; + } + + // try to deserialize it + return await serializer.DeserializeAsync(documentOrProject, stateKey, cancellationToken).ConfigureAwait(false); + } + + private void RemoveInMemoryCache(AnalysisResult lastResult) + { + // remove old cache + foreach (var documentId in lastResult.DocumentIdsOrEmpty) + { + RemoveInMemoryCacheEntries(documentId); + } + } + + private void RemoveInMemoryCacheEntries(DocumentId id) + { + RemoveInMemoryCacheEntry(id, _owner.SyntaxStateName); + RemoveInMemoryCacheEntry(id, _owner.SemanticStateName); + RemoveInMemoryCacheEntry(id, _owner.NonLocalStateName); + } + + private void RemoveInMemoryCacheEntry(object key, string stateKey) + { + // remove in memory cache if entry exist + InMemoryStorage.Remove(_owner.Analyzer, ValueTuple.Create(key, stateKey)); + } + + private bool IsEmpty(AnalysisResult result, DocumentId documentId) + { + return !result.DocumentIdsOrEmpty.Contains(documentId); + } + + // 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 = null) + { + _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, + _syntaxLocals?.ToImmutable() ?? ImmutableDictionary>.Empty, + _semanticLocals?.ToImmutable() ?? ImmutableDictionary>.Empty, + _nonLocals?.ToImmutable() ?? ImmutableDictionary>.Empty, + _others.IsDefault ? ImmutableArray.Empty : _others, + _documentIds); + } + } + } + } +} 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 e2433e5c724ec..b884b8ebf0f7d 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs @@ -1,232 +1,218 @@ // 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.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 { - internal class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer + /// + /// Diagnostic Analyzer Engine V2 + /// + /// This one follows pattern compiler has set for diagnostic analyzer. + /// + internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer { private readonly int _correlationId; - public DiagnosticIncrementalAnalyzer(DiagnosticAnalyzerService owner, int correlationId, Workspace workspace, HostAnalyzerManager hostAnalyzerManager, AbstractHostDiagnosticUpdateSource hostDiagnosticUpdateSource) + private readonly StateManager _stateManager; + private readonly Executor _executor; + private readonly CompilationManager _compilationManager; + + public DiagnosticIncrementalAnalyzer( + DiagnosticAnalyzerService owner, + int correlationId, + Workspace workspace, + HostAnalyzerManager hostAnalyzerManager, + AbstractHostDiagnosticUpdateSource hostDiagnosticUpdateSource) : base(owner, workspace, hostAnalyzerManager, hostDiagnosticUpdateSource) { _correlationId = correlationId; - } - #region IIncrementalAnalyzer - public override Task AnalyzeDocumentAsync(Document document, SyntaxNode bodyOpt, CancellationToken cancellationToken) - { - return SpecializedTasks.EmptyTask; + _stateManager = new StateManager(hostAnalyzerManager); + _stateManager.ProjectAnalyzerReferenceChanged += OnProjectAnalyzerReferenceChanged; + + _executor = new Executor(this); + _compilationManager = new CompilationManager(this); } - public override async Task AnalyzeProjectAsync(Project project, bool semanticsChanged, CancellationToken cancellationToken) + public override bool ContainsDiagnostics(Workspace workspace, ProjectId projectId) { - var diagnostics = await GetDiagnosticsAsync(project.Solution, project.Id, null, includeSuppressedDiagnostics: true, cancellationToken: cancellationToken).ConfigureAwait(false); + foreach (var stateSet in _stateManager.GetStateSets(projectId)) + { + if (stateSet.ContainsAnyDocumentOrProjectDiagnostics(projectId)) + { + return true; + } + } - RaiseEvents(project, diagnostics); + return false; } - public override Task AnalyzeSyntaxAsync(Document document, CancellationToken cancellationToken) + private bool SupportAnalysisKind(DiagnosticAnalyzer analyzer, string language, AnalysisKind kind) { - return SpecializedTasks.EmptyTask; - } + // compiler diagnostic analyzer always support all kinds + if (HostAnalyzerManager.IsCompilerDiagnosticAnalyzer(language, analyzer)) + { + return true; + } - public override Task DocumentOpenAsync(Document document, CancellationToken cancellationToken) - { - return SpecializedTasks.EmptyTask; + switch (kind) + { + case AnalysisKind.Syntax: + return analyzer.SupportsSyntaxDiagnosticAnalysis(); + case AnalysisKind.Semantic: + return analyzer.SupportsSemanticDiagnosticAnalysis(); + default: + return Contract.FailWithReturn("shouldn't reach here"); + } } - public override Task DocumentCloseAsync(Document document, CancellationToken cancellationToken) + private void OnProjectAnalyzerReferenceChanged(object sender, ProjectAnalyzerReferenceChangedEventArgs e) { - return SpecializedTasks.EmptyTask; - } + if (e.Removed.Length == 0) + { + // nothing to refresh + return; + } - public override Task DocumentResetAsync(Document document, CancellationToken cancellationToken) - { - return SpecializedTasks.EmptyTask; - } + // events will be automatically serialized. + var project = e.Project; + var stateSets = e.Removed; - public override Task NewSolutionSnapshotAsync(Solution solution, CancellationToken cancellationToken) - { - return SpecializedTasks.EmptyTask; - } + // make sure we drop cache related to the analyzers + foreach (var stateSet in stateSets) + { + stateSet.OnRemoved(); + } - public override void RemoveDocument(DocumentId documentId) - { - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsRemoved( - ValueTuple.Create(this, documentId), Workspace, null, null, null)); + ClearAllDiagnostics(stateSets, project.Id); } - public override void RemoveProject(ProjectId projectId) + private void ClearAllDiagnostics(ImmutableArray stateSets, ProjectId projectId) { - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsRemoved( - ValueTuple.Create(this, projectId), Workspace, null, null, null)); - } - #endregion + Owner.RaiseBulkDiagnosticsUpdated(raiseEvents => + { + var handleActiveFile = true; + foreach (var stateSet in stateSets) + { + // PERF: don't fire events for ones that we dont have any diagnostics on + if (!stateSet.ContainsAnyDocumentOrProjectDiagnostics(projectId)) + { + continue; + } - public override Task> GetCachedDiagnosticsAsync(Solution solution, ProjectId projectId = null, DocumentId documentId = null, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) - { - return GetDiagnosticsAsync(solution, projectId, documentId, includeSuppressedDiagnostics, cancellationToken); + RaiseProjectDiagnosticsRemoved(stateSet, projectId, stateSet.GetDocumentsWithDiagnostics(projectId), handleActiveFile, raiseEvents); + } + }); } - public override Task> GetSpecificCachedDiagnosticsAsync(Solution solution, object id, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + private void RaiseDiagnosticsCreated( + Project project, StateSet stateSet, ImmutableArray items, Action raiseEvents) { - return GetSpecificDiagnosticsAsync(solution, id, includeSuppressedDiagnostics, cancellationToken); + 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)); } - public override async Task> GetDiagnosticsAsync(Solution solution, ProjectId projectId = null, DocumentId documentId = null, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + private void RaiseDiagnosticsRemoved( + ProjectId projectId, Solution solution, StateSet stateSet, Action raiseEvents) { - if (documentId != null) - { - var diagnostics = await GetProjectDiagnosticsAsync(solution.GetProject(projectId), includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return diagnostics.Where(d => d.DocumentId == documentId).ToImmutableArrayOrEmpty(); - } + Contract.ThrowIfFalse(solution == null || solution.Workspace == Workspace); - if (projectId != null) - { - return await GetProjectDiagnosticsAsync(solution.GetProject(projectId), includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - } + raiseEvents(DiagnosticsUpdatedArgs.DiagnosticsRemoved( + CreateId(stateSet.Analyzer, projectId, AnalysisKind.NonLocal, stateSet.ErrorSourceName), + Workspace, + solution, + projectId, + documentId: null)); + } - var builder = ImmutableArray.CreateBuilder(); - foreach (var project in solution.Projects) - { - builder.AddRange(await GetProjectDiagnosticsAsync(project, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false)); - } + private void RaiseDiagnosticsCreated( + Document document, StateSet stateSet, AnalysisKind kind, ImmutableArray items, Action raiseEvents) + { + Contract.ThrowIfFalse(document.Project.Solution.Workspace == Workspace); - return builder.ToImmutable(); + raiseEvents(DiagnosticsUpdatedArgs.DiagnosticsCreated( + CreateId(stateSet.Analyzer, document.Id, kind, stateSet.ErrorSourceName), + document.Project.Solution.Workspace, + document.Project.Solution, + document.Project.Id, + document.Id, + items)); } - public override async Task> GetSpecificDiagnosticsAsync(Solution solution, object id, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + private void RaiseDiagnosticsRemoved( + DocumentId documentId, Solution solution, StateSet stateSet, AnalysisKind kind, Action raiseEvents) { - if (id is ValueTuple) - { - var key = (ValueTuple)id; - return await GetDiagnosticsAsync(solution, key.Item2.ProjectId, key.Item2, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - } - - if (id is ValueTuple) - { - var key = (ValueTuple)id; - var diagnostics = await GetDiagnosticsAsync(solution, key.Item2, null, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return diagnostics.Where(d => d.DocumentId == null).ToImmutableArray(); - } + Contract.ThrowIfFalse(solution == null || solution.Workspace == Workspace); - return ImmutableArray.Empty; + raiseEvents(DiagnosticsUpdatedArgs.DiagnosticsRemoved( + CreateId(stateSet.Analyzer, documentId, kind, stateSet.ErrorSourceName), + Workspace, + solution, + documentId.ProjectId, + documentId)); } - public override async Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId projectId = null, DocumentId documentId = null, ImmutableHashSet diagnosticIds = null, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + private object CreateId(DiagnosticAnalyzer analyzer, DocumentId key, AnalysisKind kind, string errorSourceName) { - var diagnostics = await GetDiagnosticsAsync(solution, projectId, documentId, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return diagnostics.Where(d => diagnosticIds.Contains(d.Id)).ToImmutableArrayOrEmpty(); + return CreateIdInternal(analyzer, key, kind, errorSourceName); } - public override async Task> GetProjectDiagnosticsForIdsAsync(Solution solution, ProjectId projectId = null, ImmutableHashSet diagnosticIds = null, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + private object CreateId(DiagnosticAnalyzer analyzer, ProjectId key, AnalysisKind kind, string errorSourceName) { - var diagnostics = await GetDiagnosticsForIdsAsync(solution, projectId, null, diagnosticIds, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - return diagnostics.Where(d => d.DocumentId == null).ToImmutableArray(); + return CreateIdInternal(analyzer, key, kind, errorSourceName); } - public override async Task TryAppendDiagnosticsForSpanAsync(Document document, TextSpan range, List result, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + private static object CreateIdInternal(DiagnosticAnalyzer analyzer, object key, AnalysisKind kind, string errorSourceName) { - result.AddRange(await GetDiagnosticsForSpanAsync(document, range, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false)); - return true; + return new LiveDiagnosticUpdateArgsId(analyzer, key, (int)kind, errorSourceName); } - public override async Task> GetDiagnosticsForSpanAsync(Document document, TextSpan range, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + public static Task GetDiagnosticVersionAsync(Project project, CancellationToken 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)); + return project.GetDependentVersionAsync(cancellationToken); } - private async Task> GetProjectDiagnosticsAsync(Project project, bool includeSuppressedDiagnostics, CancellationToken cancellationToken) + private static AnalysisResult GetResultOrEmpty(ImmutableDictionary map, DiagnosticAnalyzer analyzer, ProjectId projectId, VersionStamp version) { - if (project == null) + AnalysisResult result; + if (map.TryGetValue(analyzer, out result)) { - return ImmutableArray.Empty; + return result; } - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - - var analyzers = HostAnalyzerManager.CreateDiagnosticAnalyzers(project); - - var compilationWithAnalyzer = compilation.WithAnalyzers(analyzers, project.AnalyzerOptions, cancellationToken); - - // 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(); - - return dxs; + return new AnalysisResult(projectId, version); } - private IEnumerable GetDiagnosticData(Project project, ImmutableArray diagnostics) + private static ImmutableArray GetResult(AnalysisResult result, AnalysisKind kind, DocumentId id) { - foreach (var diagnostic in diagnostics) + if (result.IsEmpty || !result.DocumentIds.Contains(id) || result.IsAggregatedForm) { - if (diagnostic.Location == Location.None) - { - yield return DiagnosticData.Create(project, diagnostic); - continue; - } - - var document = project.GetDocument(diagnostic.Location.SourceTree); - if (document == null) - { - continue; - } - - yield return DiagnosticData.Create(document, diagnostic); + return ImmutableArray.Empty; } - } - - public override Task SynchronizeWithBuildAsync(DiagnosticAnalyzerService.BatchUpdateToken token, 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(DiagnosticAnalyzerService.BatchUpdateToken token, 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); - var solution = project.Solution; - var workspace = solution.Workspace; - - foreach (var kv in groups) + switch (kind) { - if (kv.Key == null) - { - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsCreated( - ValueTuple.Create(this, project.Id), workspace, solution, project.Id, null, kv.ToImmutableArrayOrEmpty())); - continue; - } - - Owner.RaiseDiagnosticsUpdated(DiagnosticsUpdatedArgs.DiagnosticsCreated( - ValueTuple.Create(this, kv.Key), workspace, solution, project.Id, kv.Key, kv.ToImmutableArrayOrEmpty())); + 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 bool ContainsDiagnostics(Workspace workspace, ProjectId projectId) - { - // for now, it always return false; - return false; - } } } diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs new file mode 100644 index 0000000000000..d8556a9113332 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs @@ -0,0 +1,194 @@ +// 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; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer + { + public override async Task SynchronizeWithBuildAsync(Workspace workspace, ImmutableDictionary> map) + { + 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(project.Id, 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: group.Where(g => g.Key == null).SelectMany(g => g).ToImmutableArrayOrEmpty()); + + 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(); + } + + private static DiagnosticData CreateLiveDiagnostic(DiagnosticDescriptor descriptor, DiagnosticData diagnostic) + { + 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 new file mode 100644 index 0000000000000..00d4634a1d1a9 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs @@ -0,0 +1,476 @@ +// 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 + { + public override Task> GetSpecificCachedDiagnosticsAsync(Solution solution, object id, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + { + return new IDECachedDiagnosticGetter(this, solution, id, includeSuppressedDiagnostics).GetSpecificDiagnosticsAsync(cancellationToken); + } + + public override Task> GetCachedDiagnosticsAsync(Solution solution, ProjectId projectId, DocumentId documentId, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + { + return new IDECachedDiagnosticGetter(this, solution, projectId, documentId, includeSuppressedDiagnostics).GetDiagnosticsAsync(cancellationToken); + } + + public override Task> GetSpecificDiagnosticsAsync(Solution solution, object id, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + { + 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 CurrentSolution; + protected readonly ProjectId CurrentProjectId; + protected readonly DocumentId CurrentDocumentId; + 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) + { + Owner = owner; + CurrentSolution = solution; + + CurrentDocumentId = documentId; + CurrentProjectId = projectId; + + Id = id; + IncludeSuppressedDiagnostics = includeSuppressedDiagnostics; + + // try to retrieve projectId/documentId from id if possible. + var argsId = id as LiveDiagnosticUpdateArgsId; + if (argsId != null) + { + CurrentDocumentId = CurrentDocumentId ?? argsId.Key as DocumentId; + CurrentProjectId = CurrentProjectId ?? (argsId.Key as ProjectId) ?? CurrentDocumentId.ProjectId; + } + + _builder = null; + } + + protected StateManager StateManager => this.Owner._stateManager; + + protected Project CurrentProject => CurrentSolution.GetProject(CurrentProjectId); + protected Document CurrentDocument => CurrentSolution.GetDocument(CurrentDocumentId); + + protected virtual bool ShouldIncludeDiagnostic(DiagnosticData diagnostic) => true; + + protected ImmutableArray GetDiagnosticData() + { + return _builder != null ? _builder.ToImmutableArray() : ImmutableArray.Empty; + } + + protected abstract Task?> GetDiagnosticsAsync(StateSet stateSet, Project project, DocumentId documentId, AnalysisKind kind, CancellationToken cancellationToken); + protected abstract Task AppendDiagnosticsAsync(Project project, IEnumerable documentIds, bool includeProjectNonLocalResult, CancellationToken cancellationToken); + + public async Task> GetSpecificDiagnosticsAsync(CancellationToken cancellationToken) + { + if (CurrentSolution == null) + { + return ImmutableArray.Empty; + } + + var argsId = Id as LiveDiagnosticUpdateArgsId; + if (argsId == null) + { + return ImmutableArray.Empty; + } + + if (CurrentProject == 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(CurrentProject, argsId.Analyzer); + if (stateSet == null) + { + return ImmutableArray.Empty; + } + + var diagnostics = await GetDiagnosticsAsync(stateSet, CurrentProject, CurrentDocumentId, (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) + { + if (CurrentSolution == null) + { + return ImmutableArray.Empty; + } + + if (CurrentProjectId != null) + { + if (CurrentProject == null) + { + return GetDiagnosticData(); + } + + var documentIds = CurrentDocumentId != null ? SpecializedCollections.SingletonEnumerable(CurrentDocumentId) : CurrentProject.DocumentIds; + + // return diagnostics specific to one project or document + var includeProjectNonLocalResult = CurrentDocumentId == null; + await AppendDiagnosticsAsync(CurrentProject, documentIds, includeProjectNonLocalResult, cancellationToken).ConfigureAwait(false); + return GetDiagnosticData(); + } + + await AppendDiagnosticsAsync(CurrentSolution, cancellationToken).ConfigureAwait(false); + return GetDiagnosticData(); + } + + protected async Task AppendDiagnosticsAsync(Solution solution, CancellationToken cancellationToken) + { + // PERF; run projects parallely rather than running CompilationWithAnalyzer with concurrency == true. + // we doing this to be safe (not get into thread starvation causing hundreds of threads to be spawn up). + var includeProjectNonLocalResult = true; + + var tasks = new Task[solution.ProjectIds.Count]; + var index = 0; + foreach (var project in solution.Projects) + { + var localProject = project; + tasks[index++] = Task.Run( + () => AppendDiagnosticsAsync( + localProject, localProject.DocumentIds, includeProjectNonLocalResult, cancellationToken), cancellationToken); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + } + + protected void AppendDiagnostics(IEnumerable items) + { + if (items == null) + { + return; + } + + if (_builder == null) + { + Interlocked.CompareExchange(ref _builder, ImmutableArray.CreateBuilder(), null); + } + + lock (_builder) + { + _builder.AddRange(items.Where(ShouldIncludeSuppressedDiagnostic).Where(ShouldIncludeDiagnostic)); + } + } + + 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; + } + } + + private class IDECachedDiagnosticGetter : DiagnosticGetter + { + 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, IEnumerable documentIds, bool includeProjectNonLocalResult, 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.GetStateSets(project.Id)) + { + 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 (includeProjectNonLocalResult) + { + // include project diagnostics if there is no target document + DocumentId targetDocumentId = null; + AppendDiagnostics(await GetProjectStateDiagnosticsAsync(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 GetProjectStateDiagnosticsAsync(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 || kind == AnalysisKind.NonLocal) + { + return null; + } + + ActiveFileState state; + if (!stateSet.TryGetActiveFileState(documentId, out state)) + { + return null; + } + + return state.GetAnalysisData(kind).Items; + } + + private async Task?> GetProjectStateDiagnosticsAsync( + 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 = project.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; + } + } + + private class IDELatestDiagnosticGetter : DiagnosticGetter + { + 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 (CurrentSolution == null) + { + return GetDiagnosticData(); + } + + if (CurrentProjectId != null) + { + var includeProjectNonLocalResult = true; + await AppendDiagnosticsAsync(CurrentProject, SpecializedCollections.EmptyEnumerable(), includeProjectNonLocalResult, cancellationToken).ConfigureAwait(false); + return GetDiagnosticData(); + } + + await AppendDiagnosticsAsync(CurrentSolution, 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, IEnumerable documentIds, bool includeProjectNonLocalResult, 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 analyzerDriverOpt = await Owner._compilationManager.CreateAnalyzerDriverAsync(project, stateSets, IncludeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + + var result = await Owner._executor.GetProjectAnalysisDataAsync(analyzerDriverOpt, 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 (includeProjectNonLocalResult) + { + // 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 stateSets = SpecializedCollections.SingletonCollection(stateSet); + var analyzerDriverOpt = await Owner._compilationManager.CreateAnalyzerDriverAsync(project, stateSets, IncludeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + + if (documentId != null) + { + var document = project.Solution.GetDocument(documentId); + Contract.ThrowIfNull(document); + + switch (kind) + { + case AnalysisKind.Syntax: + case AnalysisKind.Semantic: + { + var result = await Owner._executor.GetDocumentAnalysisDataAsync(analyzerDriverOpt, document, stateSet, kind, cancellationToken).ConfigureAwait(false); + return result.Items; + } + case AnalysisKind.NonLocal: + { + var nonLocalDocumentResult = await Owner._executor.GetProjectAnalysisDataAsync(analyzerDriverOpt, 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(analyzerDriverOpt, 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 new file mode 100644 index 0000000000000..d331f2057acc8 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnosticsForSpan.cs @@ -0,0 +1,422 @@ +// 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 +{ + internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer + { + public override async Task TryAppendDiagnosticsForSpanAsync(Document document, TextSpan range, List result, bool includeSuppressedDiagnostics = false, CancellationToken cancellationToken = default(CancellationToken)) + { + 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 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 _analyzerDriverOpt; + 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) + { + // REVIEW: IsAnalyzerSuppressed can be quite expensive in some cases. try to find a way to make it cheaper + var stateSets = owner._stateManager.GetOrCreateStateSets(document.Project).Where(s => !owner.Owner.IsAnalyzerSuppressed(s.Analyzer, document.Project)); + var analyzerDriverOpt = await owner._compilationManager.CreateAnalyzerDriverAsync(document.Project, stateSets, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + + return new LatestDiagnosticsForSpanGetter(owner, analyzerDriverOpt, document, stateSets, range, blockForData, includeSuppressedDiagnostics); + } + + private LatestDiagnosticsForSpanGetter( + DiagnosticIncrementalAnalyzer owner, + CompilationWithAnalyzers analyzerDriverOpt, + Document document, + IEnumerable stateSets, + TextSpan range, bool blockForData, bool includeSuppressedDiagnostics) + { + _owner = owner; + + _project = document.Project; + _document = document; + + _stateSets = stateSets; + _analyzerDriverOpt = analyzerDriverOpt; + _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(_analyzerDriverOpt, _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(_analyzerDriverOpt, _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(_analyzerDriverOpt, _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) + { + if (!_owner.SupportAnalysisKind(stateSet.Analyzer, stateSet.Language, kind)) + { + 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) + { + if (!stateSet.Analyzer.SupportsProjectDiagnosticAnalysis()) + { + 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 new file mode 100644 index 0000000000000..f0fe49d324bb6 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs @@ -0,0 +1,402 @@ +// 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.ErrorReporting; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV2 +{ + // TODO: make it to use cache + internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer + { + public override Task AnalyzeSyntaxAsync(Document document, CancellationToken cancellationToken) + { + return AnalyzeDocumentForKindAsync(document, AnalysisKind.Syntax, cancellationToken); + } + + public override Task AnalyzeDocumentAsync(Document document, SyntaxNode bodyOpt, CancellationToken cancellationToken) + { + return AnalyzeDocumentForKindAsync(document, AnalysisKind.Semantic, cancellationToken); + } + + 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; + } + + var stateSets = _stateManager.GetOrUpdateStateSets(document.Project); + var analyzerDriverOpt = await _compilationManager.GetAnalyzerDriverAsync(document.Project, stateSets, cancellationToken).ConfigureAwait(false); + + foreach (var stateSet in stateSets) + { + var analyzer = stateSet.Analyzer; + + var result = await _executor.GetDocumentAnalysisDataAsync(analyzerDriverOpt, document, stateSet, kind, cancellationToken).ConfigureAwait(false); + if (result.FromCache) + { + RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, result.Items); + continue; + } + + // no cancellation after this point. + var state = stateSet.GetActiveFileState(document.Id); + state.Save(kind, result.ToPersistData()); + + RaiseDocumentDiagnosticsIfNeeded(document, stateSet, kind, result.OldItems, result.Items); + } + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + + public override async Task AnalyzeProjectAsync(Project project, bool semanticsChanged, CancellationToken cancellationToken) + { + try + { + var stateSets = _stateManager.GetOrUpdateStateSets(project); + + // 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(); + + // get driver only with active analyzers. + var includeSuppressedDiagnostics = true; + var analyzerDriverOpt = await _compilationManager.CreateAnalyzerDriverAsync(project, activeAnalyzers, includeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); + + var result = await _executor.GetProjectAnalysisDataAsync(analyzerDriverOpt, project, stateSets, cancellationToken).ConfigureAwait(false); + if (result.FromCache) + { + RaiseProjectDiagnosticsIfNeeded(project, stateSets, result.Result); + return; + } + + // 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); + } + + RaiseProjectDiagnosticsIfNeeded(project, stateSets, result.OldResult, result.Result); + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + + public override Task DocumentOpenAsync(Document document, CancellationToken cancellationToken) + { + // let other component knows about this event + _compilationManager.OnDocumentOpened(); + + // 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) + { + 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) + { + 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) + { + var stateSets = _stateManager.GetStateSets(documentId.ProjectId); + + // let other components knows about this event + _compilationManager.OnDocumentRemoved(); + var changed = _stateManager.OnDocumentRemoved(stateSets, documentId); + + // 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 void RemoveProject(ProjectId projectId) + { + 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) + { + // this is Perf to reduce raising events unnecessarily. + return; + } + + // 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(); + + return SpecializedTasks.EmptyTask; + } + + 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(); + } + + private void RaiseLocalDocumentEventsFromProjectOverActiveFile(IEnumerable stateSets, Document document, bool activeFileDiagnosticExist) + { + // 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 = GetResultOrEmpty(oldResult, analyzer, project.Id, VersionStamp.Default); + var newAnalysisResult = GetResultOrEmpty(newResult, analyzer, project.Id, VersionStamp.Default); + + // 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/HostAnalyzerManager.cs b/src/Features/Core/Portable/Diagnostics/HostAnalyzerManager.cs index ab38c6646ec6f..96f3e24d33cd0 100644 --- a/src/Features/Core/Portable/Diagnostics/HostAnalyzerManager.cs +++ b/src/Features/Core/Portable/Diagnostics/HostAnalyzerManager.cs @@ -161,7 +161,7 @@ private ImmutableArray GetDiagnosticDescriptorsCore(Diagno public bool IsAnalyzerSuppressed(DiagnosticAnalyzer analyzer, Project project) { var options = project.CompilationOptions; - if (options == null || analyzer.IsCompilerAnalyzer()) + if (options == null || IsCompilerDiagnosticAnalyzer(project.Language, analyzer)) { return false; } diff --git a/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.cs b/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.cs new file mode 100644 index 0000000000000..354163b2ef1c0 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/LiveDiagnosticUpdateArgsId.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 Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics +{ + internal class LiveDiagnosticUpdateArgsId : AnalyzerUpdateArgsId + { + private readonly string _analyzerPackageName; + + public readonly object Key; + public readonly int Kind; + + public LiveDiagnosticUpdateArgsId(DiagnosticAnalyzer analyzer, object key, int kind, string analyzerPackageName) : + base(analyzer) + { + Contract.ThrowIfNull(key); + + Key = key; + Kind = kind; + + _analyzerPackageName = analyzerPackageName; + } + + public override string BuildTool => _analyzerPackageName ?? base.BuildTool; + + public override bool Equals(object obj) + { + var other = obj as LiveDiagnosticUpdateArgsId; + if (other == null) + { + return false; + } + + return Kind == other.Kind && Equals(Key, other.Key) && base.Equals(obj); + } + + public override int GetHashCode() + { + return Hash.Combine(Key, Hash.Combine(Kind, base.GetHashCode())); + } + } +} diff --git a/src/Features/Core/Portable/Features.csproj b/src/Features/Core/Portable/Features.csproj index 57f29a18f1c8d..4d5a6cabd2bd2 100644 --- a/src/Features/Core/Portable/Features.csproj +++ b/src/Features/Core/Portable/Features.csproj @@ -183,21 +183,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - @@ -212,10 +241,6 @@ - - - - @@ -279,11 +304,6 @@ - - - - - @@ -573,6 +593,7 @@ + diff --git a/src/VisualStudio/Core/Def/Implementation/TableDataSource/VisualStudioBaseDiagnosticListTable.LiveTableDataSource.cs b/src/VisualStudio/Core/Def/Implementation/TableDataSource/VisualStudioBaseDiagnosticListTable.LiveTableDataSource.cs index e6d4be18053f4..3f2310c091a50 100644 --- a/src/VisualStudio/Core/Def/Implementation/TableDataSource/VisualStudioBaseDiagnosticListTable.LiveTableDataSource.cs +++ b/src/VisualStudio/Core/Def/Implementation/TableDataSource/VisualStudioBaseDiagnosticListTable.LiveTableDataSource.cs @@ -123,14 +123,14 @@ private object CreateAggregationKey(object data) return GetItemKey(data); } - var argumentKey = args.Id as DiagnosticIncrementalAnalyzer.ArgumentKey; - if (argumentKey == null) + var liveArgsId = args.Id as LiveDiagnosticUpdateArgsId; + if (liveArgsId == null) { return GetItemKey(data); } var documents = args.Solution.GetRelatedDocumentIds(args.DocumentId); - return new AggregatedKey(documents, argumentKey.Analyzer, argumentKey.StateType); + return new AggregatedKey(documents, liveArgsId.Analyzer, liveArgsId.Kind); } private void PopulateInitialData(Workspace workspace, IDiagnosticService diagnosticService) diff --git a/src/VisualStudio/Core/Def/Implementation/TableDataSource/VisualStudioBaseDiagnosticListTable.cs b/src/VisualStudio/Core/Def/Implementation/TableDataSource/VisualStudioBaseDiagnosticListTable.cs index 13f534a75e5c5..6fbe30e97d62f 100644 --- a/src/VisualStudio/Core/Def/Implementation/TableDataSource/VisualStudioBaseDiagnosticListTable.cs +++ b/src/VisualStudio/Core/Def/Implementation/TableDataSource/VisualStudioBaseDiagnosticListTable.cs @@ -104,13 +104,13 @@ protected class AggregatedKey { public readonly ImmutableArray DocumentIds; public readonly DiagnosticAnalyzer Analyzer; - public readonly DiagnosticIncrementalAnalyzer.StateType StateType; + public readonly int Kind; - public AggregatedKey(ImmutableArray documentIds, DiagnosticAnalyzer analyzer, DiagnosticIncrementalAnalyzer.StateType stateType) + public AggregatedKey(ImmutableArray documentIds, DiagnosticAnalyzer analyzer, int kind) { DocumentIds = documentIds; Analyzer = analyzer; - StateType = stateType; + Kind = kind; } public override bool Equals(object obj) @@ -121,12 +121,12 @@ public override bool Equals(object obj) return false; } - return this.DocumentIds == other.DocumentIds && this.Analyzer == other.Analyzer && this.StateType == other.StateType; + return this.DocumentIds == other.DocumentIds && this.Analyzer == other.Analyzer && this.Kind == other.Kind; } public override int GetHashCode() { - return Hash.Combine(Analyzer.GetHashCode(), Hash.Combine(DocumentIds.GetHashCode(), (int)StateType)); + return Hash.Combine(Analyzer.GetHashCode(), Hash.Combine(DocumentIds.GetHashCode(), Kind)); } } } 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)); } diff --git a/src/VisualStudio/VisualStudioDiagnosticsToolWindow/project.lock.json b/src/VisualStudio/VisualStudioDiagnosticsToolWindow/project.lock.json new file mode 100644 index 0000000000000..9188a838a83f7 --- /dev/null +++ b/src/VisualStudio/VisualStudioDiagnosticsToolWindow/project.lock.json @@ -0,0 +1,986 @@ +{ + "locked": false, + "version": 1, + "targets": { + ".NETFramework,Version=v4.6": { + "ManagedEsent/1.9.2": { + "compile": { + "lib/net40/Esent.Interop.dll": {} + }, + "runtime": { + "lib/net40/Esent.Interop.dll": {} + } + }, + "Microsoft.CodeAnalysis.Elfie/0.10.6-rc2": { + "compile": { + "lib/net46/Microsoft.CodeAnalysis.Elfie.dll": {} + }, + "runtime": { + "lib/net46/Microsoft.CodeAnalysis.Elfie.dll": {} + } + }, + "Microsoft.CodeAnalysis.Test.Resources.Proprietary/1.2.0-beta1-20160105-04": { + "compile": { + "lib/net45/Microsoft.CodeAnalysis.Test.Resources.Proprietary.dll": {} + }, + "runtime": { + "lib/net45/Microsoft.CodeAnalysis.Test.Resources.Proprietary.dll": {} + } + }, + "Microsoft.Composition/1.0.27": { + "compile": { + "lib/portable-net45+win8+wp8+wpa81/System.Composition.AttributedModel.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Convention.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Hosting.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Runtime.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.TypedParts.dll": {} + }, + "runtime": { + "lib/portable-net45+win8+wp8+wpa81/System.Composition.AttributedModel.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Convention.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Hosting.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Runtime.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.TypedParts.dll": {} + } + }, + "Microsoft.DiaSymReader/1.0.7": { + "compile": { + "lib/net20/Microsoft.DiaSymReader.dll": {} + }, + "runtime": { + "lib/net20/Microsoft.DiaSymReader.dll": {} + } + }, + "Microsoft.DiaSymReader.Native/1.3.3": { + "runtimeTargets": { + "runtimes/win-x64/native/Microsoft.DiaSymReader.Native.amd64.dll": { + "assetType": "native", + "rid": "win-x64" + }, + "runtimes/win-x86/native/Microsoft.DiaSymReader.Native.x86.dll": { + "assetType": "native", + "rid": "win-x86" + }, + "runtimes/win/native/Microsoft.DiaSymReader.Native.amd64.dll": { + "assetType": "native", + "rid": "win" + }, + "runtimes/win/native/Microsoft.DiaSymReader.Native.arm.dll": { + "assetType": "native", + "rid": "win" + }, + "runtimes/win/native/Microsoft.DiaSymReader.Native.x86.dll": { + "assetType": "native", + "rid": "win" + }, + "runtimes/win8-arm/native/Microsoft.DiaSymReader.Native.arm.dll": { + "assetType": "native", + "rid": "win8-arm" + } + } + }, + "System.Collections/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Collections.Immutable/1.1.37": { + "dependencies": { + "System.Collections": "4.0.0", + "System.Diagnostics.Debug": "4.0.0", + "System.Globalization": "4.0.0", + "System.Linq": "4.0.0", + "System.Resources.ResourceManager": "4.0.0", + "System.Runtime": "4.0.0", + "System.Runtime.Extensions": "4.0.0", + "System.Threading": "4.0.0" + }, + "compile": { + "lib/dotnet/System.Collections.Immutable.dll": {} + }, + "runtime": { + "lib/dotnet/System.Collections.Immutable.dll": {} + } + }, + "System.Diagnostics.Debug/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Globalization/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Linq/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Reflection.Metadata/1.2.0": { + "dependencies": { + "System.Collections.Immutable": "1.1.37" + }, + "compile": { + "lib/portable-net45+win8/System.Reflection.Metadata.dll": {} + }, + "runtime": { + "lib/portable-net45+win8/System.Reflection.Metadata.dll": {} + } + }, + "System.Resources.ResourceManager/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Runtime/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Runtime.Extensions/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Threading/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "xunit/2.1.0": { + "dependencies": { + "xunit.assert": "[2.1.0]", + "xunit.core": "[2.1.0]" + } + }, + "xunit.abstractions/2.0.0": { + "compile": { + "lib/net35/xunit.abstractions.dll": {} + }, + "runtime": { + "lib/net35/xunit.abstractions.dll": {} + } + }, + "xunit.assert/2.1.0": { + "compile": { + "lib/dotnet/xunit.assert.dll": {} + }, + "runtime": { + "lib/dotnet/xunit.assert.dll": {} + } + }, + "xunit.core/2.1.0": { + "dependencies": { + "xunit.extensibility.core": "[2.1.0]", + "xunit.extensibility.execution": "[2.1.0]" + } + }, + "xunit.extensibility.core/2.1.0": { + "dependencies": { + "xunit.abstractions": "[2.0.0]" + }, + "compile": { + "lib/dotnet/xunit.core.dll": {} + }, + "runtime": { + "lib/dotnet/xunit.core.dll": {} + } + }, + "xunit.extensibility.execution/2.1.0": { + "dependencies": { + "xunit.extensibility.core": "[2.1.0]" + }, + "compile": { + "lib/net45/xunit.execution.desktop.dll": {} + }, + "runtime": { + "lib/net45/xunit.execution.desktop.dll": {} + } + } + }, + ".NETFramework,Version=v4.6/win7": { + "ManagedEsent/1.9.2": { + "compile": { + "lib/net40/Esent.Interop.dll": {} + }, + "runtime": { + "lib/net40/Esent.Interop.dll": {} + } + }, + "Microsoft.CodeAnalysis.Elfie/0.10.6-rc2": { + "compile": { + "lib/net46/Microsoft.CodeAnalysis.Elfie.dll": {} + }, + "runtime": { + "lib/net46/Microsoft.CodeAnalysis.Elfie.dll": {} + } + }, + "Microsoft.CodeAnalysis.Test.Resources.Proprietary/1.2.0-beta1-20160105-04": { + "compile": { + "lib/net45/Microsoft.CodeAnalysis.Test.Resources.Proprietary.dll": {} + }, + "runtime": { + "lib/net45/Microsoft.CodeAnalysis.Test.Resources.Proprietary.dll": {} + } + }, + "Microsoft.Composition/1.0.27": { + "compile": { + "lib/portable-net45+win8+wp8+wpa81/System.Composition.AttributedModel.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Convention.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Hosting.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Runtime.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.TypedParts.dll": {} + }, + "runtime": { + "lib/portable-net45+win8+wp8+wpa81/System.Composition.AttributedModel.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Convention.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Hosting.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Runtime.dll": {}, + "lib/portable-net45+win8+wp8+wpa81/System.Composition.TypedParts.dll": {} + } + }, + "Microsoft.DiaSymReader/1.0.7": { + "compile": { + "lib/net20/Microsoft.DiaSymReader.dll": {} + }, + "runtime": { + "lib/net20/Microsoft.DiaSymReader.dll": {} + } + }, + "Microsoft.DiaSymReader.Native/1.3.3": {}, + "System.Collections/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Collections.Immutable/1.1.37": { + "dependencies": { + "System.Collections": "4.0.0", + "System.Diagnostics.Debug": "4.0.0", + "System.Globalization": "4.0.0", + "System.Linq": "4.0.0", + "System.Resources.ResourceManager": "4.0.0", + "System.Runtime": "4.0.0", + "System.Runtime.Extensions": "4.0.0", + "System.Threading": "4.0.0" + }, + "compile": { + "lib/dotnet/System.Collections.Immutable.dll": {} + }, + "runtime": { + "lib/dotnet/System.Collections.Immutable.dll": {} + } + }, + "System.Diagnostics.Debug/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Globalization/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Linq/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Reflection.Metadata/1.2.0": { + "dependencies": { + "System.Collections.Immutable": "1.1.37" + }, + "compile": { + "lib/portable-net45+win8/System.Reflection.Metadata.dll": {} + }, + "runtime": { + "lib/portable-net45+win8/System.Reflection.Metadata.dll": {} + } + }, + "System.Resources.ResourceManager/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Runtime/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Runtime.Extensions/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "System.Threading/4.0.0": { + "compile": { + "ref/net45/_._": {} + }, + "runtime": { + "lib/net45/_._": {} + } + }, + "xunit/2.1.0": { + "dependencies": { + "xunit.assert": "[2.1.0]", + "xunit.core": "[2.1.0]" + } + }, + "xunit.abstractions/2.0.0": { + "compile": { + "lib/net35/xunit.abstractions.dll": {} + }, + "runtime": { + "lib/net35/xunit.abstractions.dll": {} + } + }, + "xunit.assert/2.1.0": { + "compile": { + "lib/dotnet/xunit.assert.dll": {} + }, + "runtime": { + "lib/dotnet/xunit.assert.dll": {} + } + }, + "xunit.core/2.1.0": { + "dependencies": { + "xunit.extensibility.core": "[2.1.0]", + "xunit.extensibility.execution": "[2.1.0]" + } + }, + "xunit.extensibility.core/2.1.0": { + "dependencies": { + "xunit.abstractions": "[2.0.0]" + }, + "compile": { + "lib/dotnet/xunit.core.dll": {} + }, + "runtime": { + "lib/dotnet/xunit.core.dll": {} + } + }, + "xunit.extensibility.execution/2.1.0": { + "dependencies": { + "xunit.extensibility.core": "[2.1.0]" + }, + "compile": { + "lib/net45/xunit.execution.desktop.dll": {} + }, + "runtime": { + "lib/net45/xunit.execution.desktop.dll": {} + } + } + } + }, + "libraries": { + "ManagedEsent/1.9.2": { + "sha512": "SgF21lefi66R2nVFtWw8OSRCrMakuwLXQohI1lTRhNSiZ0VJBqv0zLdgi9ShNhrz9pGxKijWJvUI21djYg2Tqw==", + "type": "package", + "files": [ + "ManagedEsent.1.9.2.nupkg.sha512", + "ManagedEsent.nuspec", + "lib/net40/Esent.Interop.dll", + "lib/net40/Esent.Interop.pdb", + "lib/net40/Esent.Interop.xml", + "lib/netcore45/Esent.Interop.Wsa.dll", + "lib/netcore45/Esent.Interop.Wsa.pdb" + ] + }, + "Microsoft.CodeAnalysis.Elfie/0.10.6-rc2": { + "sha512": "nw8Gelt5AYgv9PLxK3MIB5hPnMoMoC2s73YXwcM3dxxim5yxRdPbzmBy0ddEeUa9zb57qoaDLYpCF8Zajg54Dg==", + "type": "package", + "files": [ + "Microsoft.CodeAnalysis.Elfie.0.10.6-rc2.nupkg.sha512", + "Microsoft.CodeAnalysis.Elfie.nuspec", + "lib/net46/Microsoft.CodeAnalysis.Elfie.dll" + ] + }, + "Microsoft.CodeAnalysis.Test.Resources.Proprietary/1.2.0-beta1-20160105-04": { + "sha512": "QIkINpihvcij/pZhZdLmSTt4wMgjSwvRjXavvcR86lIRXi7pk4+mNgjFF5FXiR8B4WEmttdb5diyVZq7ZP4unw==", + "type": "package", + "files": [ + "Microsoft.CodeAnalysis.Test.Resources.Proprietary.1.2.0-beta1-20160105-04.nupkg.sha512", + "Microsoft.CodeAnalysis.Test.Resources.Proprietary.nuspec", + "lib/net45/Microsoft.CodeAnalysis.Test.Resources.Proprietary.dll" + ] + }, + "Microsoft.Composition/1.0.27": { + "sha512": "pwu80Ohe7SBzZ6i69LVdzowp6V+LaVRzd5F7A6QlD42vQkX0oT7KXKWWPlM/S00w1gnMQMRnEdbtOV12z6rXdQ==", + "type": "package", + "files": [ + "License-Stable.rtf", + "Microsoft.Composition.1.0.27.nupkg.sha512", + "Microsoft.Composition.nuspec", + "lib/portable-net45+win8+wp8+wpa81/System.Composition.AttributedModel.XML", + "lib/portable-net45+win8+wp8+wpa81/System.Composition.AttributedModel.dll", + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Convention.dll", + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Convention.xml", + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Hosting.XML", + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Hosting.dll", + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Runtime.XML", + "lib/portable-net45+win8+wp8+wpa81/System.Composition.Runtime.dll", + "lib/portable-net45+win8+wp8+wpa81/System.Composition.TypedParts.XML", + "lib/portable-net45+win8+wp8+wpa81/System.Composition.TypedParts.dll" + ] + }, + "Microsoft.DiaSymReader/1.0.7": { + "sha512": "4tPrkKu02w87HEvoubBGm7Hqjps69DucsBWQvGezwvDV5RJt+eZqdmdC/jNH1qn6hIem9JpJnLBK0abBzhErOg==", + "type": "package", + "files": [ + "Microsoft.DiaSymReader.1.0.7.nupkg.sha512", + "Microsoft.DiaSymReader.nuspec", + "lib/net20/Microsoft.DiaSymReader.dll", + "lib/net20/Microsoft.DiaSymReader.xml", + "lib/portable-net45+win8/Microsoft.DiaSymReader.dll", + "lib/portable-net45+win8/Microsoft.DiaSymReader.xml" + ] + }, + "Microsoft.DiaSymReader.Native/1.3.3": { + "sha512": "mjATkm+L2UlP35gO/ExNutLDfgX4iiwz1l/8sYVoeGHp5WnkEDu0NfIEsC4Oy/pCYeRw0/6SGB+kArJVNNvENQ==", + "type": "package", + "files": [ + "Microsoft.DiaSymReader.Native.1.3.3.nupkg.sha512", + "Microsoft.DiaSymReader.Native.nuspec", + "build/Microsoft.DiaSymReader.Native.props", + "runtimes/win-x64/native/Microsoft.DiaSymReader.Native.amd64.dll", + "runtimes/win-x86/native/Microsoft.DiaSymReader.Native.x86.dll", + "runtimes/win/native/Microsoft.DiaSymReader.Native.amd64.dll", + "runtimes/win/native/Microsoft.DiaSymReader.Native.arm.dll", + "runtimes/win/native/Microsoft.DiaSymReader.Native.x86.dll", + "runtimes/win8-arm/native/Microsoft.DiaSymReader.Native.arm.dll" + ] + }, + "System.Collections/4.0.0": { + "sha512": "i2vsGDIEbWdHcUSNDPKZP/ZWod6o740el7mGTCy0dqbCxQh74W4QoC+klUwPEtGEFuvzJ7bJgvwJqscosVNyZQ==", + "type": "package", + "files": [ + "License.rtf", + "System.Collections.4.0.0.nupkg.sha512", + "System.Collections.nuspec", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net45/_._", + "lib/win8/_._", + "lib/wp80/_._", + "lib/wpa81/_._", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/dotnet/System.Collections.dll", + "ref/dotnet/System.Collections.xml", + "ref/dotnet/de/System.Collections.xml", + "ref/dotnet/es/System.Collections.xml", + "ref/dotnet/fr/System.Collections.xml", + "ref/dotnet/it/System.Collections.xml", + "ref/dotnet/ja/System.Collections.xml", + "ref/dotnet/ko/System.Collections.xml", + "ref/dotnet/ru/System.Collections.xml", + "ref/dotnet/zh-hans/System.Collections.xml", + "ref/dotnet/zh-hant/System.Collections.xml", + "ref/net45/_._", + "ref/netcore50/System.Collections.dll", + "ref/netcore50/System.Collections.xml", + "ref/netcore50/de/System.Collections.xml", + "ref/netcore50/es/System.Collections.xml", + "ref/netcore50/fr/System.Collections.xml", + "ref/netcore50/it/System.Collections.xml", + "ref/netcore50/ja/System.Collections.xml", + "ref/netcore50/ko/System.Collections.xml", + "ref/netcore50/ru/System.Collections.xml", + "ref/netcore50/zh-hans/System.Collections.xml", + "ref/netcore50/zh-hant/System.Collections.xml", + "ref/win8/_._", + "ref/wp80/_._", + "ref/wpa81/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._" + ] + }, + "System.Collections.Immutable/1.1.37": { + "sha512": "fTpqwZYBzoklTT+XjTRK8KxvmrGkYHzBiylCcKyQcxiOM8k+QvhNBxRvFHDWzy4OEP5f8/9n+xQ9mEgEXY+muA==", + "type": "package", + "files": [ + "System.Collections.Immutable.1.1.37.nupkg.sha512", + "System.Collections.Immutable.nuspec", + "lib/dotnet/System.Collections.Immutable.dll", + "lib/dotnet/System.Collections.Immutable.xml", + "lib/portable-net45+win8+wp8+wpa81/System.Collections.Immutable.dll", + "lib/portable-net45+win8+wp8+wpa81/System.Collections.Immutable.xml" + ] + }, + "System.Diagnostics.Debug/4.0.0": { + "sha512": "AYJsLLGDVTC/nyURjgAo7Lpye0+HuSkcQujUf+NgQVdC/C/ky5NyamQHCforHJzgqspitMMtBe8B4UBdGXy1zQ==", + "type": "package", + "files": [ + "License.rtf", + "System.Diagnostics.Debug.4.0.0.nupkg.sha512", + "System.Diagnostics.Debug.nuspec", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net45/_._", + "lib/win8/_._", + "lib/wp80/_._", + "lib/wpa81/_._", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/dotnet/System.Diagnostics.Debug.dll", + "ref/dotnet/System.Diagnostics.Debug.xml", + "ref/dotnet/de/System.Diagnostics.Debug.xml", + "ref/dotnet/es/System.Diagnostics.Debug.xml", + "ref/dotnet/fr/System.Diagnostics.Debug.xml", + "ref/dotnet/it/System.Diagnostics.Debug.xml", + "ref/dotnet/ja/System.Diagnostics.Debug.xml", + "ref/dotnet/ko/System.Diagnostics.Debug.xml", + "ref/dotnet/ru/System.Diagnostics.Debug.xml", + "ref/dotnet/zh-hans/System.Diagnostics.Debug.xml", + "ref/dotnet/zh-hant/System.Diagnostics.Debug.xml", + "ref/net45/_._", + "ref/netcore50/System.Diagnostics.Debug.dll", + "ref/netcore50/System.Diagnostics.Debug.xml", + "ref/netcore50/de/System.Diagnostics.Debug.xml", + "ref/netcore50/es/System.Diagnostics.Debug.xml", + "ref/netcore50/fr/System.Diagnostics.Debug.xml", + "ref/netcore50/it/System.Diagnostics.Debug.xml", + "ref/netcore50/ja/System.Diagnostics.Debug.xml", + "ref/netcore50/ko/System.Diagnostics.Debug.xml", + "ref/netcore50/ru/System.Diagnostics.Debug.xml", + "ref/netcore50/zh-hans/System.Diagnostics.Debug.xml", + "ref/netcore50/zh-hant/System.Diagnostics.Debug.xml", + "ref/win8/_._", + "ref/wp80/_._", + "ref/wpa81/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._" + ] + }, + "System.Globalization/4.0.0": { + "sha512": "IBJyTo1y7ZtzzoJUA60T1XPvNTyw/wfFmjFoBFtlYfkekIOtD/AzDDIg0YdUa7eNtFEfliED2R7HdppTdU4t5A==", + "type": "package", + "files": [ + "License.rtf", + "System.Globalization.4.0.0.nupkg.sha512", + "System.Globalization.nuspec", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net45/_._", + "lib/win8/_._", + "lib/wp80/_._", + "lib/wpa81/_._", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/dotnet/System.Globalization.dll", + "ref/dotnet/System.Globalization.xml", + "ref/dotnet/de/System.Globalization.xml", + "ref/dotnet/es/System.Globalization.xml", + "ref/dotnet/fr/System.Globalization.xml", + "ref/dotnet/it/System.Globalization.xml", + "ref/dotnet/ja/System.Globalization.xml", + "ref/dotnet/ko/System.Globalization.xml", + "ref/dotnet/ru/System.Globalization.xml", + "ref/dotnet/zh-hans/System.Globalization.xml", + "ref/dotnet/zh-hant/System.Globalization.xml", + "ref/net45/_._", + "ref/netcore50/System.Globalization.dll", + "ref/netcore50/System.Globalization.xml", + "ref/netcore50/de/System.Globalization.xml", + "ref/netcore50/es/System.Globalization.xml", + "ref/netcore50/fr/System.Globalization.xml", + "ref/netcore50/it/System.Globalization.xml", + "ref/netcore50/ja/System.Globalization.xml", + "ref/netcore50/ko/System.Globalization.xml", + "ref/netcore50/ru/System.Globalization.xml", + "ref/netcore50/zh-hans/System.Globalization.xml", + "ref/netcore50/zh-hant/System.Globalization.xml", + "ref/win8/_._", + "ref/wp80/_._", + "ref/wpa81/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._" + ] + }, + "System.Linq/4.0.0": { + "sha512": "r6Hlc+ytE6m/9UBr+nNRRdoJEWjoeQiT3L3lXYFDHoXk3VYsRBCDNXrawcexw7KPLaH0zamQLiAb6avhZ50cGg==", + "type": "package", + "files": [ + "System.Linq.4.0.0.nupkg.sha512", + "System.Linq.nuspec", + "lib/dotnet/System.Linq.dll", + "lib/net45/_._", + "lib/netcore50/System.Linq.dll", + "lib/win8/_._", + "lib/wp80/_._", + "lib/wpa81/_._", + "ref/dotnet/System.Linq.dll", + "ref/dotnet/System.Linq.xml", + "ref/dotnet/de/System.Linq.xml", + "ref/dotnet/es/System.Linq.xml", + "ref/dotnet/fr/System.Linq.xml", + "ref/dotnet/it/System.Linq.xml", + "ref/dotnet/ja/System.Linq.xml", + "ref/dotnet/ko/System.Linq.xml", + "ref/dotnet/ru/System.Linq.xml", + "ref/dotnet/zh-hans/System.Linq.xml", + "ref/dotnet/zh-hant/System.Linq.xml", + "ref/net45/_._", + "ref/netcore50/System.Linq.dll", + "ref/netcore50/System.Linq.xml", + "ref/win8/_._", + "ref/wp80/_._", + "ref/wpa81/_._" + ] + }, + "System.Reflection.Metadata/1.2.0": { + "sha512": "ubQKFCNYPwhqPXPLjRKCvTDR2UvL5L5+Tm181D/5kl/df7264AuXDi2j2Bf5DxplBxevq8eUH9LRomcFCXTQKw==", + "type": "package", + "files": [ + "System.Reflection.Metadata.1.2.0.nupkg.sha512", + "System.Reflection.Metadata.nuspec", + "ThirdPartyNotices.txt", + "dotnet_library_license.txt", + "lib/netstandard1.1/System.Reflection.Metadata.dll", + "lib/netstandard1.1/System.Reflection.Metadata.xml", + "lib/portable-net45+win8/System.Reflection.Metadata.dll", + "lib/portable-net45+win8/System.Reflection.Metadata.xml" + ] + }, + "System.Resources.ResourceManager/4.0.0": { + "sha512": "qmqeZ4BJgjfU+G2JbrZt4Dk1LsMxO4t+f/9HarNY6w8pBgweO6jT+cknUH7c3qIrGvyUqraBhU45Eo6UtA0fAw==", + "type": "package", + "files": [ + "System.Resources.ResourceManager.4.0.0.nupkg.sha512", + "System.Resources.ResourceManager.nuspec", + "lib/DNXCore50/System.Resources.ResourceManager.dll", + "lib/net45/_._", + "lib/netcore50/System.Resources.ResourceManager.dll", + "lib/win8/_._", + "lib/wp80/_._", + "lib/wpa81/_._", + "ref/dotnet/System.Resources.ResourceManager.dll", + "ref/dotnet/System.Resources.ResourceManager.xml", + "ref/dotnet/de/System.Resources.ResourceManager.xml", + "ref/dotnet/es/System.Resources.ResourceManager.xml", + "ref/dotnet/fr/System.Resources.ResourceManager.xml", + "ref/dotnet/it/System.Resources.ResourceManager.xml", + "ref/dotnet/ja/System.Resources.ResourceManager.xml", + "ref/dotnet/ko/System.Resources.ResourceManager.xml", + "ref/dotnet/ru/System.Resources.ResourceManager.xml", + "ref/dotnet/zh-hans/System.Resources.ResourceManager.xml", + "ref/dotnet/zh-hant/System.Resources.ResourceManager.xml", + "ref/net45/_._", + "ref/netcore50/System.Resources.ResourceManager.dll", + "ref/netcore50/System.Resources.ResourceManager.xml", + "ref/win8/_._", + "ref/wp80/_._", + "ref/wpa81/_._", + "runtimes/win8-aot/lib/netcore50/System.Resources.ResourceManager.dll" + ] + }, + "System.Runtime/4.0.0": { + "sha512": "Uq9epame8hEqJlj4KaWb67dDJvj4IM37jRFGVeFbugRdPz48bR0voyBhrbf3iSa2tAmlkg4lsa6BUOL9iwlMew==", + "type": "package", + "files": [ + "License.rtf", + "System.Runtime.4.0.0.nupkg.sha512", + "System.Runtime.nuspec", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net45/_._", + "lib/win8/_._", + "lib/wp80/_._", + "lib/wpa81/_._", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/dotnet/System.Runtime.dll", + "ref/dotnet/System.Runtime.xml", + "ref/dotnet/de/System.Runtime.xml", + "ref/dotnet/es/System.Runtime.xml", + "ref/dotnet/fr/System.Runtime.xml", + "ref/dotnet/it/System.Runtime.xml", + "ref/dotnet/ja/System.Runtime.xml", + "ref/dotnet/ko/System.Runtime.xml", + "ref/dotnet/ru/System.Runtime.xml", + "ref/dotnet/zh-hans/System.Runtime.xml", + "ref/dotnet/zh-hant/System.Runtime.xml", + "ref/net45/_._", + "ref/netcore50/System.Runtime.dll", + "ref/netcore50/System.Runtime.xml", + "ref/netcore50/de/System.Runtime.xml", + "ref/netcore50/es/System.Runtime.xml", + "ref/netcore50/fr/System.Runtime.xml", + "ref/netcore50/it/System.Runtime.xml", + "ref/netcore50/ja/System.Runtime.xml", + "ref/netcore50/ko/System.Runtime.xml", + "ref/netcore50/ru/System.Runtime.xml", + "ref/netcore50/zh-hans/System.Runtime.xml", + "ref/netcore50/zh-hant/System.Runtime.xml", + "ref/win8/_._", + "ref/wp80/_._", + "ref/wpa81/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._" + ] + }, + "System.Runtime.Extensions/4.0.0": { + "sha512": "zPzwoJcA7qar/b5Ihhzfcdr3vBOR8FIg7u//Qc5mqyAriasXuMFVraBZ5vOQq5asfun9ryNEL8Z2BOlUK5QRqA==", + "type": "package", + "files": [ + "License.rtf", + "System.Runtime.Extensions.4.0.0.nupkg.sha512", + "System.Runtime.Extensions.nuspec", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net45/_._", + "lib/win8/_._", + "lib/wp80/_._", + "lib/wpa81/_._", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/dotnet/System.Runtime.Extensions.dll", + "ref/dotnet/System.Runtime.Extensions.xml", + "ref/dotnet/de/System.Runtime.Extensions.xml", + "ref/dotnet/es/System.Runtime.Extensions.xml", + "ref/dotnet/fr/System.Runtime.Extensions.xml", + "ref/dotnet/it/System.Runtime.Extensions.xml", + "ref/dotnet/ja/System.Runtime.Extensions.xml", + "ref/dotnet/ko/System.Runtime.Extensions.xml", + "ref/dotnet/ru/System.Runtime.Extensions.xml", + "ref/dotnet/zh-hans/System.Runtime.Extensions.xml", + "ref/dotnet/zh-hant/System.Runtime.Extensions.xml", + "ref/net45/_._", + "ref/netcore50/System.Runtime.Extensions.dll", + "ref/netcore50/System.Runtime.Extensions.xml", + "ref/netcore50/de/System.Runtime.Extensions.xml", + "ref/netcore50/es/System.Runtime.Extensions.xml", + "ref/netcore50/fr/System.Runtime.Extensions.xml", + "ref/netcore50/it/System.Runtime.Extensions.xml", + "ref/netcore50/ja/System.Runtime.Extensions.xml", + "ref/netcore50/ko/System.Runtime.Extensions.xml", + "ref/netcore50/ru/System.Runtime.Extensions.xml", + "ref/netcore50/zh-hans/System.Runtime.Extensions.xml", + "ref/netcore50/zh-hant/System.Runtime.Extensions.xml", + "ref/win8/_._", + "ref/wp80/_._", + "ref/wpa81/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._" + ] + }, + "System.Threading/4.0.0": { + "sha512": "H6O/9gUrjPDNYanh/7OFGAZHjVXvEuITD0RcnjfvIV04HOGrOPqUBU0kmz9RIX/7YGgCQn1o1S2DX6Cuv8kVGQ==", + "type": "package", + "files": [ + "License.rtf", + "System.Threading.4.0.0.nupkg.sha512", + "System.Threading.nuspec", + "lib/MonoAndroid10/_._", + "lib/MonoTouch10/_._", + "lib/net45/_._", + "lib/win8/_._", + "lib/wp80/_._", + "lib/wpa81/_._", + "lib/xamarinios10/_._", + "lib/xamarinmac20/_._", + "ref/MonoAndroid10/_._", + "ref/MonoTouch10/_._", + "ref/dotnet/System.Threading.dll", + "ref/dotnet/System.Threading.xml", + "ref/dotnet/de/System.Threading.xml", + "ref/dotnet/es/System.Threading.xml", + "ref/dotnet/fr/System.Threading.xml", + "ref/dotnet/it/System.Threading.xml", + "ref/dotnet/ja/System.Threading.xml", + "ref/dotnet/ko/System.Threading.xml", + "ref/dotnet/ru/System.Threading.xml", + "ref/dotnet/zh-hans/System.Threading.xml", + "ref/dotnet/zh-hant/System.Threading.xml", + "ref/net45/_._", + "ref/netcore50/System.Threading.dll", + "ref/netcore50/System.Threading.xml", + "ref/netcore50/de/System.Threading.xml", + "ref/netcore50/es/System.Threading.xml", + "ref/netcore50/fr/System.Threading.xml", + "ref/netcore50/it/System.Threading.xml", + "ref/netcore50/ja/System.Threading.xml", + "ref/netcore50/ko/System.Threading.xml", + "ref/netcore50/ru/System.Threading.xml", + "ref/netcore50/zh-hans/System.Threading.xml", + "ref/netcore50/zh-hant/System.Threading.xml", + "ref/win8/_._", + "ref/wp80/_._", + "ref/wpa81/_._", + "ref/xamarinios10/_._", + "ref/xamarinmac20/_._" + ] + }, + "xunit/2.1.0": { + "sha512": "u/7VQSOSXa7kSG4iK6Lcn7RqKZQ3hk7cnyMNVMpXHSP0RI5VQEtc44hvkG3LyWOVsx1dhUDD3rPAHAxyOUDQJw==", + "type": "package", + "files": [ + "xunit.2.1.0.nupkg.sha512", + "xunit.nuspec" + ] + }, + "xunit.abstractions/2.0.0": { + "sha512": "NAdxKQRzuLnCZ0g++x6i87/8rMBpQoRiRlRNLAqfODm2zJPbteHRoSER3DXfxnqrHXyBJT8rFaZ8uveBeQyaMA==", + "type": "package", + "files": [ + "lib/net35/xunit.abstractions.dll", + "lib/net35/xunit.abstractions.xml", + "lib/portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS/xunit.abstractions.dll", + "lib/portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS/xunit.abstractions.xml", + "xunit.abstractions.2.0.0.nupkg.sha512", + "xunit.abstractions.nuspec" + ] + }, + "xunit.assert/2.1.0": { + "sha512": "Hhhw+YaTe+BGhbr57dxVE+6VJk8BfThqFFii1XIsSZ4qx+SSCixprJC10JkiLRVSTfWyT8W/4nAf6NQgIrmBxA==", + "type": "package", + "files": [ + "lib/dotnet/xunit.assert.dll", + "lib/dotnet/xunit.assert.pdb", + "lib/dotnet/xunit.assert.xml", + "lib/portable-net45+win8+wp8+wpa81/xunit.assert.dll", + "lib/portable-net45+win8+wp8+wpa81/xunit.assert.pdb", + "lib/portable-net45+win8+wp8+wpa81/xunit.assert.xml", + "xunit.assert.2.1.0.nupkg.sha512", + "xunit.assert.nuspec" + ] + }, + "xunit.core/2.1.0": { + "sha512": "jlbYdPbnkPIRwJllcT/tQZCNsSElVDEymdpJfH79uTUrPARkELVYw9o/zhAjKZXmeikGqGK5C2Yny4gTNoEu0Q==", + "type": "package", + "files": [ + "build/_desktop/xunit.execution.desktop.dll", + "build/dnx451/_._", + "build/monoandroid/_._", + "build/monotouch/_._", + "build/net45/_._", + "build/portable-net45+win8+wp8+wpa81/xunit.core.props", + "build/win8/_._", + "build/win81/xunit.core.props", + "build/wp8/_._", + "build/wpa81/xunit.core.props", + "build/xamarinios/_._", + "xunit.core.2.1.0.nupkg.sha512", + "xunit.core.nuspec" + ] + }, + "xunit.extensibility.core/2.1.0": { + "sha512": "ANWM3WxeaeHjACLRlmrv+xOc0WAcr3cvIiJE+gqbdzTv1NCH4p1VDyT+8WmmdCc9db0WFiJLaDy4YTYsL1wWXw==", + "type": "package", + "files": [ + "lib/dotnet/xunit.core.dll", + "lib/dotnet/xunit.core.dll.tdnet", + "lib/dotnet/xunit.core.pdb", + "lib/dotnet/xunit.core.xml", + "lib/dotnet/xunit.runner.tdnet.dll", + "lib/dotnet/xunit.runner.utility.desktop.dll", + "lib/portable-net45+win8+wp8+wpa81/xunit.core.dll", + "lib/portable-net45+win8+wp8+wpa81/xunit.core.dll.tdnet", + "lib/portable-net45+win8+wp8+wpa81/xunit.core.pdb", + "lib/portable-net45+win8+wp8+wpa81/xunit.core.xml", + "lib/portable-net45+win8+wp8+wpa81/xunit.runner.tdnet.dll", + "lib/portable-net45+win8+wp8+wpa81/xunit.runner.utility.desktop.dll", + "xunit.extensibility.core.2.1.0.nupkg.sha512", + "xunit.extensibility.core.nuspec" + ] + }, + "xunit.extensibility.execution/2.1.0": { + "sha512": "tAoNafoVknKa3sZJPMvtZRnhOSk3gasEGeceSm7w/gyGwsR/OXFxndWJB1xSHeoy33d3Z6jFqn4A3j+pWCF0Ew==", + "type": "package", + "files": [ + "lib/dnx451/xunit.execution.dotnet.dll", + "lib/dnx451/xunit.execution.dotnet.pdb", + "lib/dnx451/xunit.execution.dotnet.xml", + "lib/dotnet/xunit.execution.dotnet.dll", + "lib/dotnet/xunit.execution.dotnet.pdb", + "lib/dotnet/xunit.execution.dotnet.xml", + "lib/monoandroid/xunit.execution.dotnet.dll", + "lib/monoandroid/xunit.execution.dotnet.pdb", + "lib/monoandroid/xunit.execution.dotnet.xml", + "lib/monotouch/xunit.execution.dotnet.dll", + "lib/monotouch/xunit.execution.dotnet.pdb", + "lib/monotouch/xunit.execution.dotnet.xml", + "lib/net45/xunit.execution.desktop.dll", + "lib/net45/xunit.execution.desktop.pdb", + "lib/net45/xunit.execution.desktop.xml", + "lib/portable-net45+win8+wp8+wpa81/xunit.execution.dotnet.dll", + "lib/portable-net45+win8+wp8+wpa81/xunit.execution.dotnet.pdb", + "lib/portable-net45+win8+wp8+wpa81/xunit.execution.dotnet.xml", + "lib/win8/xunit.execution.dotnet.dll", + "lib/win8/xunit.execution.dotnet.pdb", + "lib/win8/xunit.execution.dotnet.xml", + "lib/wp8/xunit.execution.dotnet.dll", + "lib/wp8/xunit.execution.dotnet.pdb", + "lib/wp8/xunit.execution.dotnet.xml", + "lib/wpa81/xunit.execution.dotnet.dll", + "lib/wpa81/xunit.execution.dotnet.pdb", + "lib/wpa81/xunit.execution.dotnet.xml", + "lib/xamarinios/xunit.execution.dotnet.dll", + "lib/xamarinios/xunit.execution.dotnet.pdb", + "lib/xamarinios/xunit.execution.dotnet.xml", + "xunit.extensibility.execution.2.1.0.nupkg.sha512", + "xunit.extensibility.execution.nuspec" + ] + } + }, + "projectFileDependencyGroups": { + "": [], + ".NETFramework,Version=v4.6": [] + } +} \ No newline at end of file