From dc8cbaccc01d4f709f17a827f2b8a6045d458676 Mon Sep 17 00:00:00 2001 From: Heejae Chang Date: Fri, 6 Mar 2015 03:53:37 -0800 Subject: [PATCH] Add support for compilation end code fix. now code fix should support diagnostics from compilation end action. a few options are added to test experience which can be changed from Tools/Options/Roslyn/Diagnostic pane. --- .../TodoComment/TodoCommentState.cs | 2 +- .../Diagnostics/DiagnosticServiceTests.vb | 15 +- .../EngineV1/DiagnosticAnalyzerDriver.cs | 5 - ...sticIncrementalAnalyzer.DiagnosticState.cs | 5 +- ...er.IncrementalAnalyzer.AnalyzerExecutor.cs | 27 +- .../DiagnosticIncrementalAnalyzer.StateSet.cs | 4 +- .../EngineV1/DiagnosticIncrementalAnalyzer.cs | 279 +++++------------- ...osticIncrementalAnalyzer_GetDiagnostics.cs | 3 +- ...talAnalyzer_GetLatestDiagnosticsForSpan.cs | 196 ++++++++++++ .../Diagnostics/InternalDiagnosticsOptions.cs | 6 + src/Features/Core/Features.csproj | 1 + .../State/AbstractAnalyzerState.cs | 23 +- 12 files changed, 316 insertions(+), 250 deletions(-) create mode 100644 src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetLatestDiagnosticsForSpan.cs diff --git a/src/EditorFeatures/Core/Implementation/TodoComment/TodoCommentState.cs b/src/EditorFeatures/Core/Implementation/TodoComment/TodoCommentState.cs index 9afa2b712f7..7851a453825 100644 --- a/src/EditorFeatures/Core/Implementation/TodoComment/TodoCommentState.cs +++ b/src/EditorFeatures/Core/Implementation/TodoComment/TodoCommentState.cs @@ -87,7 +87,7 @@ protected override void WriteTo(Stream stream, Data data, CancellationToken canc public ImmutableArray GetItems_TestingOnly(DocumentId documentId) { Data data; - if (this.DataCache.TryGetValue(documentId, out data)) + if (this.DataCache.TryGetValue(documentId, out data) && data != null) { return data.Items; } diff --git a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb index 2f621f58b93..e2f8b99ed0d 100644 --- a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb +++ b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb @@ -8,6 +8,7 @@ Imports Microsoft.CodeAnalysis.Diagnostics Imports Microsoft.CodeAnalysis.Diagnostics.EngineV1 Imports Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces +Imports Microsoft.CodeAnalysis.Options Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.CodeAnalysis.Text Imports Roslyn.Utilities @@ -594,6 +595,11 @@ class AnonymousFunctions Using workspace = TestWorkspaceFactory.CreateWorkspace(test) Dim project = workspace.CurrentSolution.Projects.Single() + + ' turn off heuristic + Dim options = workspace.Services.GetService(Of IOptionService)() + options.SetOptions(options.GetOptions.WithChangedOption(InternalDiagnosticsOptions.UseCompilationEndCodeFixHueristic, False)) + Dim analyzer = New CompilationEndedAnalyzer Dim analyzerReference = New AnalyzerImageReference(ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer)) project = project.AddAnalyzerReference(analyzerReference) @@ -604,17 +610,18 @@ class AnonymousFunctions Dim descriptorsMap = diagnosticService.GetDiagnosticDescriptors(project) Assert.Equal(1, descriptorsMap.Count) - ' Ask for document diagnostics multiple times, and verify compilation diagnostics are not reported. + ' Ask for document diagnostics multiple times, and verify compilation diagnostics are reported. Dim document = project.Documents.Single() + Dim fullSpan = document.GetSyntaxRootAsync().WaitAndGetResult(CancellationToken.None).FullSpan Dim diagnostics = diagnosticService.GetDiagnosticsForSpanAsync(document, fullSpan, CancellationToken.None).WaitAndGetResult(CancellationToken.None) - Assert.Equal(0, diagnostics.Count()) + Assert.Equal(1, diagnostics.Count()) diagnostics = diagnosticService.GetDiagnosticsForSpanAsync(document, fullSpan, CancellationToken.None).WaitAndGetResult(CancellationToken.None) - Assert.Equal(0, diagnostics.Count()) + Assert.Equal(1, diagnostics.Count()) diagnostics = diagnosticService.GetDiagnosticsForSpanAsync(document, fullSpan, CancellationToken.None).WaitAndGetResult(CancellationToken.None) - Assert.Equal(0, diagnostics.Count()) + Assert.Equal(1, diagnostics.Count()) ' 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) diff --git a/src/Features/Core/Diagnostics/EngineV1/DiagnosticAnalyzerDriver.cs b/src/Features/Core/Diagnostics/EngineV1/DiagnosticAnalyzerDriver.cs index c67428f5490..c4d2afef497 100644 --- a/src/Features/Core/Diagnostics/EngineV1/DiagnosticAnalyzerDriver.cs +++ b/src/Features/Core/Diagnostics/EngineV1/DiagnosticAnalyzerDriver.cs @@ -29,8 +29,6 @@ internal class DiagnosticAnalyzerDriver private readonly AbstractHostDiagnosticUpdateSource _hostDiagnosticUpdateSource; private readonly CancellationToken _cancellationToken; private readonly ISyntaxNodeAnalyzerService _syntaxNodeAnalyzerService; - private readonly Dictionary> _descendantExecutableNodesMap; - private readonly ISyntaxFactsService _syntaxFacts; private readonly IGeneratedCodeRecognitionService _generatedCodeService; private readonly IAnalyzerDriverService _analyzerDriverService; @@ -74,8 +72,6 @@ public DiagnosticAnalyzerDriver(Project project, LogAggregator logAggregator, Ab _syntaxNodeAnalyzerService = syntaxNodeAnalyzerService; _hostDiagnosticUpdateSource = hostDiagnosticUpdateSource; _cancellationToken = cancellationToken; - _descendantExecutableNodesMap = new Dictionary>(); - _syntaxFacts = document.Project.LanguageServices.GetService(); _generatedCodeService = document.Project.Solution.Workspace.Services.GetService(); _analyzerDriverService = document.Project.LanguageServices.GetService(); _analyzerOptions = new WorkspaceAnalyzerOptions(_project.AnalyzerOptions, _project.Solution.Workspace); @@ -97,7 +93,6 @@ public DiagnosticAnalyzerDriver(Project project, LogAggregator logAggregator, Ab _generatedCodeService = project.Solution.Workspace.Services.GetService(); _analyzerDriverService = project.LanguageServices.GetService(); _hostDiagnosticUpdateSource = hostDiagnosticUpdateSource; - _descendantExecutableNodesMap = null; _analyzerOptions = new WorkspaceAnalyzerOptions(_project.AnalyzerOptions, _project.Solution.Workspace); _onAnalyzerException = overriddenOnAnalyzerException ?? Default_OnAnalyzerException; _onAnalyzerException_NoTelemetryLogging = overriddenOnAnalyzerException ?? Default_OnAnalyzerException_NoTelemetryLogging; diff --git a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.DiagnosticState.cs b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.DiagnosticState.cs index 9f0022e82a9..bae050fad4c 100644 --- a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.DiagnosticState.cs +++ b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.DiagnosticState.cs @@ -128,15 +128,14 @@ private AnalysisData TryGetExistingData(Stream stream, Project project, Document var textVersion = VersionStamp.ReadFrom(reader); var dataVersion = VersionStamp.ReadFrom(reader); - // textversion can be default for document from project analysis. - if (dataVersion == VersionStamp.Default) + if (textVersion == VersionStamp.Default || dataVersion == VersionStamp.Default) { return null; } AppendItems(reader, project, document, list, cancellationToken); - return new AnalysisData(textVersion, dataVersion, list.ToImmutableArray()); + return new AnalysisData(textVersion, dataVersion, list.ToImmutableArray()); } } catch (Exception) diff --git a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.IncrementalAnalyzer.AnalyzerExecutor.cs b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.IncrementalAnalyzer.AnalyzerExecutor.cs index 9f8f79316fc..45baa54b375 100644 --- a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.IncrementalAnalyzer.AnalyzerExecutor.cs +++ b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.IncrementalAnalyzer.AnalyzerExecutor.cs @@ -121,16 +121,13 @@ public async Task GetProjectAnalysisDataAsync(DiagnosticAnalyzerDr var state = stateSet.GetState(StateType.Project); var existingData = await GetExistingProjectAnalysisDataAsync(project, state, cancellationToken).ConfigureAwait(false); - // TODO: - // if there is any document level result, we can't ever use cache since we can't track those changes in current design - // hopely v2 engine, either don't care this at all, or can deal with this better - if (CheckSemanticVersions(project, existingData, versions) && !existingData.Items.Any(d => d.DocumentId != null)) + if (CheckSemanticVersions(project, existingData, versions)) { return existingData; } var diagnosticData = await GetProjectDiagnosticsAsync(analyzerDriver, stateSet.Analyzer, _owner.ForceAnalyzeAllDocuments).ConfigureAwait(false); - return new AnalysisData(VersionStamp.Default, versions.DataVersion, GetExistingItems(existingData), diagnosticData.AsImmutableOrEmpty()); + return new AnalysisData(versions.TextVersion, versions.DataVersion, GetExistingItems(existingData), diagnosticData.AsImmutableOrEmpty()); } catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) { @@ -140,19 +137,22 @@ public async Task GetProjectAnalysisDataAsync(DiagnosticAnalyzerDr private async Task GetExistingProjectAnalysisDataAsync(Project project, DiagnosticState state, CancellationToken cancellationToken) { - var dataVersion = VersionStamp.Default; - var existingData = await state.TryGetExistingDataAsync(project, cancellationToken).ConfigureAwait(false); - - // quick path. - if (existingData == null || existingData.Items.Length == 0) + // quick bail out + if (state.Count == 0) { - return existingData; + return null; } + var textVersion = VersionStamp.Default; + var dataVersion = VersionStamp.Default; + var existingData = await state.TryGetExistingDataAsync(project, cancellationToken).ConfigureAwait(false); + var builder = ImmutableArray.CreateBuilder(); if (existingData != null) { + textVersion = existingData.TextVersion; dataVersion = existingData.DataVersion; + builder.AddRange(existingData.Items); } @@ -169,16 +169,19 @@ private async Task GetExistingProjectAnalysisDataAsync(Project pro continue; } + textVersion = existingData.TextVersion; dataVersion = existingData.DataVersion; + builder.AddRange(existingData.Items); } if (dataVersion == VersionStamp.Default) { + Contract.Requires(textVersion == VersionStamp.Default); return null; } - return new AnalysisData(VersionStamp.Default, dataVersion, builder.ToImmutable()); + return new AnalysisData(textVersion, dataVersion, builder.ToImmutable()); } private bool CanUseDocumentState(AnalysisData existingData, VersionStamp textVersion, VersionStamp dataVersion) diff --git a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateSet.cs b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateSet.cs index a395d13855c..df0fa2ee884 100644 --- a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateSet.cs +++ b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.StateSet.cs @@ -36,11 +36,11 @@ public DiagnosticState GetState(StateType stateType) return _state[(int)stateType]; } - public void Remove(object key) + public void Remove(object documentOrProjectId) { for (var stateType = 0; stateType < s_stateTypeCount; stateType++) { - _state[stateType].Remove(key); + _state[stateType].Remove(documentOrProjectId); } } diff --git a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.cs b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.cs index 7e705963aad..dbadbffe573 100644 --- a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.cs +++ b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer.cs @@ -72,7 +72,8 @@ public override Task DocumentOpenAsync(Document document, CancellationToken canc // we remove whatever information we used to have on document open/close and re-calcuate diagnostics // we had to do this since some diagnostic analyzer change its behavior based on whether the document is opend or not. // so we can't use cached information. - return ClearDocumentStatesAsync(document, _stateManger.GetStateSets(document.Project), cancellationToken); + ClearDocumentStates(document, _stateManger.GetStateSets(document.Project), cancellationToken); + return SpecializedTasks.EmptyTask; } } @@ -86,7 +87,8 @@ public override Task DocumentResetAsync(Document document, CancellationToken can // we remove whatever information we used to have on document open/close and re-calcuate diagnostics // we had to do this since some diagnostic analyzer change its behavior based on whether the document is opend or not. // so we can't use cached information. - return ClearDocumentStatesAsync(document, _stateManger.GetStateSets(document.Project), cancellationToken); + ClearDocumentStates(document, _stateManger.GetStateSets(document.Project), cancellationToken); + return SpecializedTasks.EmptyTask; } } @@ -300,21 +302,22 @@ private async Task AnalyzeProjectAsync(Project project, ImmutableHashSet return; } - var projectVersion = await project.GetDependentVersionAsync(cancellationToken).ConfigureAwait(false); + var projectTextVersion = await project.GetLatestDocumentVersionAsync(cancellationToken).ConfigureAwait(false); var semanticVersion = await project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); - var userDiagnosticDriver = new DiagnosticAnalyzerDriver(project, _diagnosticLogAggregator, HostDiagnosticUpdateSource, cancellationToken); + var projectVersion = await project.GetDependentVersionAsync(cancellationToken).ConfigureAwait(false); + var analyzerDriver = new DiagnosticAnalyzerDriver(project, _diagnosticLogAggregator, HostDiagnosticUpdateSource, cancellationToken); - var versions = new VersionArgument(VersionStamp.Default, semanticVersion, projectVersion); + var versions = new VersionArgument(projectTextVersion, semanticVersion, projectVersion); foreach (var stateSet in _stateManger.GetOrUpdateStateSets(project)) { - if (userDiagnosticDriver.IsAnalyzerSuppressed(stateSet.Analyzer)) + if (analyzerDriver.IsAnalyzerSuppressed(stateSet.Analyzer)) { await HandleSuppressedAnalyzerAsync(project, stateSet, cancellationToken).ConfigureAwait(false); } - else if (ShouldRunAnalyzerForStateType(userDiagnosticDriver, stateSet.Analyzer, StateType.Project, diagnosticIds) && + else if (ShouldRunAnalyzerForStateType(analyzerDriver, stateSet.Analyzer, StateType.Project, diagnosticIds) && (skipClosedFileChecks || ShouldRunAnalyzerForClosedFile(openedDocument: false, analyzer: stateSet.Analyzer))) { - var data = await _executor.GetProjectAnalysisDataAsync(userDiagnosticDriver, stateSet, versions).ConfigureAwait(false); + var data = await _executor.GetProjectAnalysisDataAsync(analyzerDriver, stateSet, versions).ConfigureAwait(false); if (data.FromCache) { RaiseProjectDiagnosticsUpdated(project, stateSet.Analyzer, data.Items); @@ -336,17 +339,25 @@ private async Task AnalyzeProjectAsync(Project project, ImmutableHashSet private static async Task PersistProjectData(Project project, DiagnosticState state, AnalysisData data) { + // TODO: cancellation is not allowed here to prevent data inconsistency. but there is still a possibility of data inconsistency due to + // things like exception. for now, I am letting it go and let v2 engine take care of it properly. if v2 doesnt come online soon enough + // more refactoring is required on project state. + + // clear all existing data + state.Remove(project.Id); + foreach (var document in project.Documents) + { + state.Remove(document.Id); + } + + // quick bail out if (data.Items.Length == 0) { - await state.PersistAsync(project, new AnalysisData(data.TextVersion, data.DataVersion, ImmutableArray.Empty), CancellationToken.None).ConfigureAwait(false); return; } - // TODO: cancellation is not allowed here to prevent data inconsistency. but there is still a possibility of data inconsistency due to - // things like exception. for now, I am letting it go and let v2 engine take care of it properly. if v2 doesnt come online soon enough - // more refactoring is required on project state. + // save new data var group = data.Items.GroupBy(d => d.DocumentId); - foreach (var kv in group) { if (kv.Key == null) @@ -355,7 +366,14 @@ private static async Task PersistProjectData(Project project, DiagnosticState st continue; } - await state.PersistAsync(project.GetDocument(kv.Key), new AnalysisData(data.TextVersion, data.DataVersion, kv.ToImmutableArrayOrEmpty()), CancellationToken.None).ConfigureAwait(false); + // save text version for the document + var document = project.GetDocument(kv.Key); + if (document == null) + { + continue; + } + + await state.PersistAsync(document, new AnalysisData(data.TextVersion, data.DataVersion, kv.ToImmutableArrayOrEmpty()), CancellationToken.None).ConfigureAwait(false); } } @@ -396,159 +414,20 @@ public override void RemoveProject(ProjectId projectId) public override async Task TryAppendDiagnosticsForSpanAsync(Document document, TextSpan range, List diagnostics, CancellationToken cancellationToken) { - try - { - var textVersion = await document.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); - var syntaxVersion = await document.GetSyntaxVersionAsync(cancellationToken).ConfigureAwait(false); - var semanticVersion = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); - - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - var result = true; - result &= await TryGetLatestDiagnosticsAsync( - StateType.Syntax, document, range, root, diagnostics, false, - (t, d) => t.Equals(textVersion) && d.Equals(syntaxVersion), - GetSyntaxDiagnosticsAsync, cancellationToken).ConfigureAwait(false); - - result &= await TryGetLatestDiagnosticsAsync( - StateType.Document, document, range, root, diagnostics, false, - (t, d) => t.Equals(textVersion) && d.Equals(semanticVersion), - GetSemanticDiagnosticsAsync, cancellationToken).ConfigureAwait(false); - - return result; - } - catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) - { - throw ExceptionUtilities.Unreachable; - } + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var getter = new LatestDiagnosticsForSpanGetter(this, document, root, range, blockForData: false, diagnostics: diagnostics, cancellationToken: cancellationToken); + return await getter.TryGetAsync().ConfigureAwait(false); } public override async Task> GetDiagnosticsForSpanAsync(Document document, TextSpan range, CancellationToken cancellationToken) { - try - { - var textVersion = await document.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); - var syntaxVersion = await document.GetSyntaxVersionAsync(cancellationToken).ConfigureAwait(false); - var semanticVersion = await document.Project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); - - var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - - var result = true; - using (var diagnostics = SharedPools.Default>().GetPooledObject()) - { - result &= await TryGetLatestDiagnosticsAsync( - StateType.Syntax, document, range, root, diagnostics.Object, true, - (t, d) => t.Equals(textVersion) && d.Equals(syntaxVersion), - GetSyntaxDiagnosticsAsync, cancellationToken).ConfigureAwait(false); - - result &= await TryGetLatestDiagnosticsAsync( - StateType.Document, document, range, root, diagnostics.Object, true, - (t, d) => t.Equals(textVersion) && d.Equals(semanticVersion), - GetSemanticDiagnosticsAsync, cancellationToken).ConfigureAwait(false); - - // must be always up-to-date - Debug.Assert(result); - if (diagnostics.Object.Count > 0) - { - return diagnostics.Object.ToImmutableArray(); - } - - return SpecializedCollections.EmptyEnumerable(); - } - } - catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) - { - throw ExceptionUtilities.Unreachable; - } - } - - private async Task TryGetLatestDiagnosticsAsync( - StateType stateType, Document document, TextSpan range, SyntaxNode root, - List diagnostics, bool requireUpToDateDocumentDiagnostic, - Func versionCheck, - Func>> getDiagnostics, - CancellationToken cancellationToken) - { - try - { - bool result = true; - var fullSpan = root == null ? null : (TextSpan?)root.FullSpan; - - // Share the diagnostic analyzer driver across all analyzers. - var spanBasedDriver = new DiagnosticAnalyzerDriver(document, range, root, _diagnosticLogAggregator, HostDiagnosticUpdateSource, cancellationToken); - var documentBasedDriver = new DiagnosticAnalyzerDriver(document, fullSpan, root, _diagnosticLogAggregator, HostDiagnosticUpdateSource, cancellationToken); - - foreach (var stateSet in _stateManger.GetOrCreateStateSets(document.Project)) - { - bool supportsSemanticInSpan; - if (!spanBasedDriver.IsAnalyzerSuppressed(stateSet.Analyzer) && - ShouldRunAnalyzerForStateType(spanBasedDriver, stateSet.Analyzer, stateType, out supportsSemanticInSpan)) - { - var userDiagnosticDriver = supportsSemanticInSpan ? spanBasedDriver : documentBasedDriver; - - result &= await TryGetLatestDiagnosticsAsync( - stateSet, stateType, document, range, root, diagnostics, requireUpToDateDocumentDiagnostic, - versionCheck, getDiagnostics, supportsSemanticInSpan, userDiagnosticDriver, cancellationToken).ConfigureAwait(false); - } - } - - return result; - } - catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) - { - throw ExceptionUtilities.Unreachable; - } - } - - private async Task TryGetLatestDiagnosticsAsync( - StateSet stateSet, StateType stateType, Document document, TextSpan range, SyntaxNode root, - List diagnostics, bool requireUpToDateDocumentDiagnostic, - Func versionCheck, - Func>> getDiagnostics, - bool supportsSemanticInSpan, - DiagnosticAnalyzerDriver userDiagnosticDriver, - CancellationToken cancellationToken) - { - try - { - var shouldInclude = (Func)(d => range.IntersectsWith(d.TextSpan)); - - // make sure we get state even when none of our analyzer has ran yet. - // but this shouldn't create analyzer that doesnt belong to this project (language) - var state = stateSet.GetState(stateType); - - // see whether we can use existing info - var existingData = await state.TryGetExistingDataAsync(document, cancellationToken).ConfigureAwait(false); - if (existingData != null && versionCheck(existingData.TextVersion, existingData.DataVersion)) - { - if (existingData.Items == null) - { - return true; - } + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var getter = new LatestDiagnosticsForSpanGetter(this, document, root, range, blockForData: true, cancellationToken: cancellationToken); - diagnostics.AddRange(existingData.Items.Where(shouldInclude)); - return true; - } + var result = await getter.TryGetAsync().ConfigureAwait(false); + Contract.Requires(result); - // check whether we want up-to-date document wide diagnostics - if (stateType == StateType.Document && !supportsSemanticInSpan && !requireUpToDateDocumentDiagnostic) - { - return false; - } - - var dx = await getDiagnostics(userDiagnosticDriver, stateSet.Analyzer).ConfigureAwait(false); - if (dx != null) - { - // no state yet - diagnostics.AddRange(dx.Where(shouldInclude)); - } - - return true; - } - catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) - { - throw ExceptionUtilities.Unreachable; - } + return getter.Diagnostics; } private bool ShouldRunAnalyzerForClosedFile(bool openedDocument, DiagnosticAnalyzer analyzer) @@ -641,7 +520,8 @@ private static bool CheckSemanticVersions(Project project, AnalysisData existing return false; } - return project.CanReusePersistedDependentSemanticVersion(versions.ProjectVersion, versions.DataVersion, existingData.DataVersion); + return VersionStamp.CanReusePersistedVersion(versions.TextVersion, existingData.TextVersion) && + project.CanReusePersistedDependentSemanticVersion(versions.ProjectVersion, versions.DataVersion, existingData.DataVersion); } private void RaiseDocumentDiagnosticsUpdatedIfNeeded( @@ -935,78 +815,53 @@ private static async Task> GetProjectDiagnosticsAsyn } } - private async Task ClearDocumentStatesAsync(Document document, IEnumerable states, CancellationToken cancellationToken) + private void ClearDocumentStates(Document document, IEnumerable states, CancellationToken cancellationToken) { - try + // Compiler + User diagnostics + foreach (var state in states) { - // Compiler + User diagnostics - foreach (var state in states) + for (var stateType = 0; stateType < s_stateTypeCount; stateType++) { - for (var stateType = 0; stateType < s_stateTypeCount; stateType++) - { - await ClearDocumentStateAsync(document, state.Analyzer, (StateType)stateType, state.GetState((StateType)stateType), cancellationToken).ConfigureAwait(false); - } + cancellationToken.ThrowIfCancellationRequested(); + ClearDocumentState(document, state.Analyzer, (StateType)stateType, state.GetState((StateType)stateType)); } } - catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) - { - throw ExceptionUtilities.Unreachable; - } } - private async Task ClearDocumentStateAsync(Document document, DiagnosticAnalyzer analyzer, StateType type, DiagnosticState state, CancellationToken cancellationToken) + private void ClearDocumentState(Document document, DiagnosticAnalyzer analyzer, StateType type, DiagnosticState state) { - try - { - // remove memory cache - state.Remove(document.Id); - - // remove persistent cache - await state.PersistAsync(document, AnalysisData.Empty, cancellationToken).ConfigureAwait(false); + // remove saved info + state.Remove(document.Id); - // raise diagnostic updated event - var documentId = document.Id; - var solutionArgs = new SolutionArgument(document); + // raise diagnostic updated event + var documentId = document.Id; + var solutionArgs = new SolutionArgument(document); - RaiseDiagnosticsUpdated(type, document.Id, analyzer, solutionArgs, ImmutableArray.Empty); - } - catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) - { - throw ExceptionUtilities.Unreachable; - } + RaiseDiagnosticsUpdated(type, document.Id, analyzer, solutionArgs, ImmutableArray.Empty); } - private async Task ClearProjectStatesAsync(Project project, IEnumerable states, CancellationToken cancellationToken) + private void ClearProjectStatesAsync(Project project, IEnumerable states, CancellationToken cancellationToken) { foreach (var document in project.Documents) { - await ClearDocumentStatesAsync(document, states, cancellationToken).ConfigureAwait(false); + ClearDocumentStates(document, states, cancellationToken); } foreach (var state in states) { - await ClearProjectStateAsync(project, state.Analyzer, state.GetState(StateType.Project), cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + ClearProjectState(project, state.Analyzer, state.GetState(StateType.Project)); } } - private async Task ClearProjectStateAsync(Project project, DiagnosticAnalyzer analyzer, DiagnosticState state, CancellationToken cancellationToken) + private void ClearProjectState(Project project, DiagnosticAnalyzer analyzer, DiagnosticState state) { - try - { - // remove memory cache - state.Remove(project.Id); - - // remove persistent cache - await state.PersistAsync(project, AnalysisData.Empty, cancellationToken).ConfigureAwait(false); + // remove saved cache + state.Remove(project.Id); - // raise diagnostic updated event - var solutionArgs = new SolutionArgument(project); - RaiseDiagnosticsUpdated(StateType.Project, project.Id, analyzer, solutionArgs, ImmutableArray.Empty); - } - catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) - { - throw ExceptionUtilities.Unreachable; - } + // raise diagnostic updated event + var solutionArgs = new SolutionArgument(project); + RaiseDiagnosticsUpdated(StateType.Project, project.Id, analyzer, solutionArgs, ImmutableArray.Empty); } private async Task HandleSuppressedAnalyzerAsync(Document document, StateSet stateSet, StateType type, CancellationToken cancellationToken) @@ -1015,7 +870,7 @@ private async Task HandleSuppressedAnalyzerAsync(Document document, StateSet sta var existingData = await state.TryGetExistingDataAsync(document, cancellationToken).ConfigureAwait(false); if (existingData != null && existingData.Items.Length > 0) { - await ClearDocumentStateAsync(document, stateSet.Analyzer, type, state, cancellationToken).ConfigureAwait(false); + ClearDocumentState(document, stateSet.Analyzer, type, state); } } @@ -1025,7 +880,7 @@ private async Task HandleSuppressedAnalyzerAsync(Project project, StateSet state var existingData = await state.TryGetExistingDataAsync(project, cancellationToken).ConfigureAwait(false); if (existingData != null && existingData.Items.Length > 0) { - await ClearProjectStateAsync(project, stateSet.Analyzer, state, cancellationToken).ConfigureAwait(false); + ClearProjectState(project, stateSet.Analyzer, state); } } diff --git a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs index 11b7b8bb7f0..83ff5ba468c 100644 --- a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs +++ b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs @@ -396,9 +396,10 @@ protected async Task GetVersionsAsync(object documentOrProject, case StateType.Project: { var project = (Project)documentOrProject; + var projectTextVersion = await project.GetLatestDocumentVersionAsync(cancellationToken).ConfigureAwait(false); var semanticVersion = await project.GetDependentSemanticVersionAsync(cancellationToken).ConfigureAwait(false); var projectVersion = await project.GetDependentVersionAsync(cancellationToken).ConfigureAwait(false); - return new VersionArgument(VersionStamp.Default, semanticVersion, projectVersion); + return new VersionArgument(projectTextVersion, semanticVersion, projectVersion); } default: diff --git a/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetLatestDiagnosticsForSpan.cs b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetLatestDiagnosticsForSpan.cs new file mode 100644 index 00000000000..f35a56e9ea8 --- /dev/null +++ b/src/Features/Core/Diagnostics/EngineV1/DiagnosticIncrementalAnalyzer_GetLatestDiagnosticsForSpan.cs @@ -0,0 +1,196 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Diagnostics.EngineV1 +{ + internal partial class DiagnosticIncrementalAnalyzer : BaseDiagnosticIncrementalAnalyzer + { + private class LatestDiagnosticsForSpanGetter + { + private readonly DiagnosticIncrementalAnalyzer _owner; + + private readonly Document _document; + private readonly TextSpan _range; + private readonly bool _blockForData; + private readonly CancellationToken _cancellationToken; + + private readonly DiagnosticAnalyzerDriver _spanBasedDriver; + private readonly DiagnosticAnalyzerDriver _documentBasedDriver; + private readonly DiagnosticAnalyzerDriver _projectDriver; + + private readonly List _diagnostics; + + public LatestDiagnosticsForSpanGetter( + DiagnosticIncrementalAnalyzer owner, Document document, SyntaxNode root, TextSpan range, bool blockForData, CancellationToken cancellationToken) : + this(owner, document, root, range, blockForData, new List(), cancellationToken) + { + } + + public LatestDiagnosticsForSpanGetter( + DiagnosticIncrementalAnalyzer owner, Document document, SyntaxNode root, TextSpan range, bool blockForData, List diagnostics, CancellationToken cancellationToken) + { + _owner = owner; + + _document = document; + _range = range; + _blockForData = blockForData; + _cancellationToken = cancellationToken; + + _diagnostics = diagnostics; + + // Share the diagnostic analyzer driver across all analyzers. + var fullSpan = root == null ? null : (TextSpan?)root.FullSpan; + + _spanBasedDriver = new DiagnosticAnalyzerDriver(_document, _range, root, _owner._diagnosticLogAggregator, _owner.HostDiagnosticUpdateSource, _cancellationToken); + _documentBasedDriver = new DiagnosticAnalyzerDriver(_document, fullSpan, root, _owner._diagnosticLogAggregator, _owner.HostDiagnosticUpdateSource, _cancellationToken); + _projectDriver = new DiagnosticAnalyzerDriver(_document.Project, _owner._diagnosticLogAggregator, _owner.HostDiagnosticUpdateSource, _cancellationToken); + } + + public List Diagnostics => _diagnostics; + + public async Task TryGetAsync() + { + try + { + var textVersion = await _document.GetTextVersionAsync(_cancellationToken).ConfigureAwait(false); + var syntaxVersion = await _document.GetSyntaxVersionAsync(_cancellationToken).ConfigureAwait(false); + var projectTextVersion = await _document.Project.GetLatestDocumentVersionAsync(_cancellationToken).ConfigureAwait(false); + var semanticVersion = await _document.Project.GetDependentSemanticVersionAsync(_cancellationToken).ConfigureAwait(false); + + var result = true; + foreach (var stateSet in _owner._stateManger.GetOrCreateStateSets(_document.Project)) + { + result &= await TryGetDocumentDiagnosticsAsync( + stateSet, StateType.Syntax, (t, d) => t.Equals(textVersion) && d.Equals(syntaxVersion), GetSyntaxDiagnosticsAsync).ConfigureAwait(false); + + result &= await TryGetDocumentDiagnosticsAsync( + stateSet, StateType.Document, (t, d) => t.Equals(textVersion) && d.Equals(semanticVersion), GetSemanticDiagnosticsAsync).ConfigureAwait(false); + + // check whether compilation end code fix is enabled + if (!_document.Project.Solution.Workspace.Options.GetOption(InternalDiagnosticsOptions.CompilationEndCodeFix)) + { + continue; + } + + // check whether hueristic is enabled + if (_blockForData && _document.Project.Solution.Workspace.Options.GetOption(InternalDiagnosticsOptions.UseCompilationEndCodeFixHueristic)) + { + var analysisData = await stateSet.GetState(StateType.Project).TryGetExistingDataAsync(_document, _cancellationToken).ConfigureAwait(false); + + // no previous compilation end diagnostics in this file. + if (analysisData == null || analysisData.Items.Length == 0 || + !analysisData.TextVersion.Equals(projectTextVersion) || + !analysisData.DataVersion.Equals(semanticVersion)) + { + continue; + } + } + + result &= await TryGetDocumentDiagnosticsAsync( + stateSet, StateType.Project, (t, d) => t.Equals(projectTextVersion) && d.Equals(semanticVersion), GetProjectDiagnosticsWorkerAsync).ConfigureAwait(false); + } + + return result; + } + catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) + { + throw ExceptionUtilities.Unreachable; + } + } + + private async Task TryGetDocumentDiagnosticsAsync( + StateSet stateSet, StateType stateType, Func versionCheck, + Func>> getDiagnostics) + { + bool supportsSemanticInSpan; + if (_spanBasedDriver.IsAnalyzerSuppressed(stateSet.Analyzer) || + !ShouldRunAnalyzerForStateType(stateSet, stateType, out supportsSemanticInSpan)) + { + return true; + } + + var analyzerDriver = GetAnalyzerDriverBasedOnStateType(stateType, supportsSemanticInSpan); + + var shouldInclude = (Func)(d => d.DocumentId == _document.Id && _range.IntersectsWith(d.TextSpan)); + + // make sure we get state even when none of our analyzer has ran yet. + // but this shouldn't create analyzer that doesnt belong to this project (language) + var state = stateSet.GetState(stateType); + + // see whether we can use existing info + var existingData = await state.TryGetExistingDataAsync(_document, _cancellationToken).ConfigureAwait(false); + if (existingData != null && versionCheck(existingData.TextVersion, existingData.DataVersion)) + { + if (existingData.Items == null || existingData.Items.Length == 0) + { + return true; + } + + _diagnostics.AddRange(existingData.Items.Where(shouldInclude)); + return true; + } + + // check whether we want up-to-date document wide diagnostics + if (!BlockForData(stateType, supportsSemanticInSpan)) + { + return false; + } + + var dx = await getDiagnostics(analyzerDriver, stateSet.Analyzer).ConfigureAwait(false); + if (dx != null) + { + // no state yet + _diagnostics.AddRange(dx.Where(shouldInclude)); + } + + return true; + } + + private bool ShouldRunAnalyzerForStateType(StateSet stateSet, StateType stateType, out bool supportsSemanticInSpan) + { + if (stateType == StateType.Project) + { + return DiagnosticIncrementalAnalyzer.ShouldRunAnalyzerForStateType(_projectDriver, stateSet.Analyzer, stateType, out supportsSemanticInSpan); + } + + return DiagnosticIncrementalAnalyzer.ShouldRunAnalyzerForStateType(_spanBasedDriver, stateSet.Analyzer, stateType, out supportsSemanticInSpan); + } + + private bool BlockForData(StateType stateType, bool supportsSemanticInSpan) + { + if (stateType == StateType.Document && !supportsSemanticInSpan && !_blockForData) + { + return false; + } + + if (stateType == StateType.Project && !_blockForData) + { + return false; + } + + // TODO: + // this probably need to change in v2 engine. but in v1 engine, we have assumption that all syntax related action + // will return diagnostics that only belong to given span + return true; + } + + private DiagnosticAnalyzerDriver GetAnalyzerDriverBasedOnStateType(StateType stateType, bool supportsSemanticInSpan) + { + return stateType == StateType.Project ? _projectDriver : supportsSemanticInSpan ? _spanBasedDriver : _documentBasedDriver; + } + + private Task> GetProjectDiagnosticsWorkerAsync(DiagnosticAnalyzerDriver driver, DiagnosticAnalyzer analyzer) + { + return GetProjectDiagnosticsAsync(driver, analyzer, _owner.ForceAnalyzeAllDocuments); + } + } + } +} diff --git a/src/Features/Core/Diagnostics/InternalDiagnosticsOptions.cs b/src/Features/Core/Diagnostics/InternalDiagnosticsOptions.cs index a79bbdc6542..648ea08ec3f 100644 --- a/src/Features/Core/Diagnostics/InternalDiagnosticsOptions.cs +++ b/src/Features/Core/Diagnostics/InternalDiagnosticsOptions.cs @@ -13,5 +13,11 @@ internal static class InternalDiagnosticsOptions [ExportOption] public static readonly Option UseDiagnosticEngineV2 = new Option(OptionName, "Use Diagnostic Engine V2", defaultValue: false); + + [ExportOption] + public static readonly Option CompilationEndCodeFix = new Option(OptionName, "Enable Compilation End Code Fix", defaultValue: true); + + [ExportOption] + public static readonly Option UseCompilationEndCodeFixHueristic = new Option(OptionName, "Enable Compilation End Code Fix Only If There is existing one", defaultValue: true); } } diff --git a/src/Features/Core/Features.csproj b/src/Features/Core/Features.csproj index d5cca6970f0..98a8d7fcbd1 100644 --- a/src/Features/Core/Features.csproj +++ b/src/Features/Core/Features.csproj @@ -184,6 +184,7 @@ + diff --git a/src/Features/Core/SolutionCrawler/State/AbstractAnalyzerState.cs b/src/Features/Core/SolutionCrawler/State/AbstractAnalyzerState.cs index 1f3d6a20cf6..ef43d6c7af6 100644 --- a/src/Features/Core/SolutionCrawler/State/AbstractAnalyzerState.cs +++ b/src/Features/Core/SolutionCrawler/State/AbstractAnalyzerState.cs @@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.SolutionCrawler.State { internal abstract class AbstractAnalyzerState { - protected readonly ConcurrentDictionary DataCache = new ConcurrentDictionary(); + protected readonly ConcurrentDictionary DataCache = new ConcurrentDictionary(concurrencyLevel: 2, capacity: 10); protected abstract TKey GetCacheKey(TValue value); protected abstract Solution GetSolution(TValue value); @@ -23,15 +23,24 @@ internal abstract class AbstractAnalyzerState protected abstract void WriteTo(Stream stream, TData data, CancellationToken cancellationToken); protected abstract Task WriteStreamAsync(IPersistentStorage storage, TValue value, Stream stream, CancellationToken cancellationToken); + public int Count { get { return this.DataCache.Count; } } + public async Task TryGetExistingDataAsync(TValue value, CancellationToken cancellationToken) { - // we have data for the document TData data; - if (this.DataCache.TryGetValue(GetCacheKey(value), out data)) + if (!this.DataCache.TryGetValue(GetCacheKey(value), out data)) + { + // we don't have data + return default(TData); + } + + // we have in memory cache for the document + if (!object.Equals(data, default(TData))) { return data; } + // we have persisted data var solution = GetSolution(value); var persistService = solution.Workspace.Services.GetService(); @@ -55,13 +64,7 @@ public async Task PersistAsync(TValue value, TData data, CancellationToken cance // if data is for opened document or if persistence failed, // we keep small cache so that we don't pay cost of deserialize/serializing data that keep changing - if (!succeeded || ShouldCache(value)) - { - this.DataCache[id] = data; - return; - } - - Remove(id); + this.DataCache[id] = (!succeeded || ShouldCache(value)) ? data : default(TData); } public bool Remove(TKey id) -- GitLab