From a8c241a0532a3e4ca25377113387f759052ec57a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Matou=C5=A1ek?= Date: Mon, 25 Jul 2022 16:50:30 -0700 Subject: [PATCH 1/2] HasChangesAsync should return false if the document content did not changed (#62849) * HasChangesAsync should return false if the document content did not changed * Comment * Test source generated docs * Fix rebase * Include PopulateChangedAndAddedDocumentsAsync in test --- .../EditAndContinueLanguageService.cs | 37 +++- .../EditAndContinueWorkspaceServiceTests.cs | 186 +++++++++++++++--- .../Portable/EditAndContinue/EditSession.cs | 122 +++++++----- .../Shared/Extensions/ProjectExtensions.cs | 6 + 4 files changed, 267 insertions(+), 84 deletions(-) diff --git a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs index b3b56f1e800ce..ec49f53b391be 100644 --- a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs +++ b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs @@ -182,21 +182,26 @@ public async ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellation public async ValueTask CommitUpdatesAsync(CancellationToken cancellationToken) { + if (_disabled) + { + return; + } + + var committedDesignTimeSolution = Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null); + Contract.ThrowIfNull(committedDesignTimeSolution); + try { - var committedDesignTimeSolution = Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null); - Contract.ThrowIfNull(committedDesignTimeSolution); SolutionCommitted?.Invoke(committedDesignTimeSolution); - - _committedDesignTimeSolution = committedDesignTimeSolution; } catch (Exception e) when (FatalError.ReportAndCatch(e)) { } + _committedDesignTimeSolution = committedDesignTimeSolution; + try { - Contract.ThrowIfTrue(_disabled); await GetDebuggingSession().CommitSolutionUpdateAsync(_diagnosticService, cancellationToken).ConfigureAwait(false); } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) @@ -260,6 +265,9 @@ private ActiveStatementSpanProvider GetActiveStatementSpanProvider(Solution solu /// will disappear once the debuggee is resumed - if they are caused by presence of active statements around the change. /// If the result is a false positive the debugger attempts to apply the changes, which will result in a delay but will correctly end up /// with no actual deltas to be applied. + /// + /// If is specified checks for changes only in a document of the given path. + /// This is not supported (returns false) for source-generated documents. /// public async ValueTask HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken) { @@ -293,9 +301,17 @@ public async ValueTask GetEditAndContinueUpdatesAsync(Canc } var workspace = WorkspaceProvider.Value.Workspace; - var solution = GetCurrentCompileTimeSolution(_pendingUpdatedDesignTimeSolution = workspace.CurrentSolution); + var designTimeSolution = workspace.CurrentSolution; + var solution = GetCurrentCompileTimeSolution(designTimeSolution); var activeStatementSpanProvider = GetActiveStatementSpanProvider(solution); var (updates, _, _, _) = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, _diagnosticService, _diagnosticUpdateSource, cancellationToken).ConfigureAwait(false); + + // Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called. + if (updates.Status == Contracts.ManagedModuleUpdateStatus.Ready) + { + _pendingUpdatedDesignTimeSolution = designTimeSolution; + } + return updates.FromContract(); } @@ -307,9 +323,16 @@ public async ValueTask GetHotReloadUpdatesAsync(Cancell } var workspace = WorkspaceProvider.Value.Workspace; - var solution = GetCurrentCompileTimeSolution(_pendingUpdatedDesignTimeSolution = workspace.CurrentSolution); + var designTimeSolution = workspace.CurrentSolution; + var solution = GetCurrentCompileTimeSolution(designTimeSolution); var (moduleUpdates, diagnosticData, rudeEdits, syntaxError) = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, s_noActiveStatementSpanProvider, _diagnosticService, _diagnosticUpdateSource, cancellationToken).ConfigureAwait(false); + // Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called. + if (moduleUpdates.Status == Contracts.ManagedModuleUpdateStatus.Ready) + { + _pendingUpdatedDesignTimeSolution = designTimeSolution; + } + var updates = moduleUpdates.Updates.SelectAsArray( update => new ManagedHotReloadUpdate(update.Module, update.ILDelta, update.MetadataDelta, update.PdbDelta, update.UpdatedTypes)); diff --git a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs b/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs index b0495237767d4..c6c9c44711e08 100644 --- a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs +++ b/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs @@ -12,6 +12,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Runtime.Remoting.Contexts; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -24,6 +25,7 @@ using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.Api; using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.UnitTests; @@ -1766,60 +1768,184 @@ public async Task HasChanges() var (updates, _) = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ManagedModuleUpdateStatus.Blocked, updates.Status); - // add a document: + // add a project: oldSolution = solution; - projectC = solution.GetProjectsByName("C").Single(); - var documentDId = DocumentId.CreateNewId(projectC.Id); - solution = solution.AddDocument(documentDId, "D", "class D {}", filePath: "D.cs"); + var projectD = solution.AddProject("D", "D", "C#"); + solution = projectD.Solution; Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None)); - Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, sourceFilePath: "D.cs", CancellationToken.None)); - // remove a document: + // remove a project: + Assert.True(await EditSession.HasChangesAsync(solution, solution.RemoveProject(projectD.Id), CancellationToken.None)); - oldSolution = solution; - solution = solution.RemoveDocument(documentDId); + EndDebuggingSession(debuggingSession); + } - Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None)); - Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, sourceFilePath: "D.cs", CancellationToken.None)); + public enum DocumentKind + { + Source, + Additional, + AnalyzerConfig, + } - // add an additional document: + [Theory] + [CombinatorialData] + public async Task HasChanges_Documents(DocumentKind documentKind) + { + using var _ = CreateWorkspace(out var solution, out var service); - oldSolution = solution; - projectC = solution.GetProjectsByName("C").Single(); - var documentXId = DocumentId.CreateNewId(projectC.Id); - solution = solution.AddAdditionalDocument(documentXId, "X", "xxx", filePath: "X"); + var pathX = Path.Combine(TempRoot.Root, "X.cs"); + var pathA = Path.Combine(TempRoot.Root, "A.cs"); + + var generatorExecutionCount = 0; + var generator = new TestSourceGenerator() + { + ExecuteImpl = context => + { + switch (documentKind) + { + case DocumentKind.Source: + context.AddSource("Generated.cs", context.Compilation.SyntaxTrees.SingleOrDefault(t => t.FilePath.EndsWith("X.cs"))?.ToString() ?? "none"); + break; + + case DocumentKind.Additional: + context.AddSource("Generated.cs", context.AdditionalFiles.FirstOrDefault()?.GetText().ToString() ?? "none"); + break; + + case DocumentKind.AnalyzerConfig: + var syntaxTree = context.Compilation.SyntaxTrees.Single(t => t.FilePath.EndsWith("A.cs")); + var content = context.AnalyzerConfigOptions.GetOptions(syntaxTree).TryGetValue("x", out var optionValue) ? optionValue.ToString() : "none"; + + context.AddSource("Generated.cs", content); + break; + } + + generatorExecutionCount++; + } + }; + + var project = solution.AddProject("A", "A", "C#").AddDocument("A.cs", "", filePath: pathA).Project; + var projectId = project.Id; + solution = project.Solution.AddAnalyzerReference(projectId, new TestGeneratorReference(generator)); + project = solution.GetRequiredProject(projectId); + var generatedDocument = (await project.GetSourceGeneratedDocumentsAsync()).Single(); + var generatedDocumentId = generatedDocument.Id; + + var debuggingSession = await StartDebuggingSessionAsync(service, solution); + EnterBreakState(debuggingSession); + + Assert.Equal(1, generatorExecutionCount); + var changedOrAddedDocuments = new PooledObjects.ArrayBuilder(); + + // + // Add document + // + generatorExecutionCount = 0; + var oldSolution = solution; + var documentId = DocumentId.CreateNewId(projectId); + solution = documentKind switch + { + DocumentKind.Source => solution.AddDocument(documentId, "X", SourceText.From("xxx", Encoding.UTF8, SourceHashAlgorithm.Sha256), filePath: pathX), + DocumentKind.Additional => solution.AddAdditionalDocument(documentId, "X", SourceText.From("xxx", Encoding.UTF8, SourceHashAlgorithm.Sha256), filePath: pathX), + DocumentKind.AnalyzerConfig => solution.AddAnalyzerConfigDocument(documentId, "X", GetAnalyzerConfigText(new[] { ("x", "1") }), filePath: pathX), + _ => throw ExceptionUtilities.Unreachable, + }; Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None)); + Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, pathX, CancellationToken.None)); + + // always returns false for source generated files: + Assert.False(await EditSession.HasChangesAsync(oldSolution, solution, generatedDocument.FilePath, CancellationToken.None)); + + // generator is not executed since we already know the solution changed without inspecting generated files: + Assert.Equal(0, generatorExecutionCount); - // remove an additional document: - Assert.True(await EditSession.HasChangesAsync(solution, solution.RemoveAdditionalDocument(documentXId), CancellationToken.None)); + AssertEx.Equal(new[] { generatedDocumentId }, + await EditSession.GetChangedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None)); - // add a config document: + await EditSession.PopulateChangedAndAddedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), changedOrAddedDocuments, CancellationToken.None); + AssertEx.Equal(documentKind == DocumentKind.Source ? new[] { documentId, generatedDocumentId } : new[] { generatedDocumentId }, changedOrAddedDocuments.Select(d => d.Id)); + Assert.Equal(1, generatorExecutionCount); + + // + // Update document to a different document snapshot but the same content + // + + generatorExecutionCount = 0; oldSolution = solution; - projectC = solution.GetProjectsByName("C").Single(); - var documentYId = DocumentId.CreateNewId(projectC.Id); - solution = solution.AddAnalyzerConfigDocument(documentYId, "Y", SourceText.From("yyy"), filePath: "Y"); - Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None)); + solution = documentKind switch + { + DocumentKind.Source => solution.WithDocumentText(documentId, SourceText.From("xxx", Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256)), + DocumentKind.Additional => solution.WithAdditionalDocumentText(documentId, SourceText.From("xxx", Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256)), + DocumentKind.AnalyzerConfig => solution.WithAnalyzerConfigDocumentText(documentId, GetAnalyzerConfigText(new[] { ("x", "1") })), + _ => throw ExceptionUtilities.Unreachable, + }; + Assert.False(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None)); + Assert.False(await EditSession.HasChangesAsync(oldSolution, solution, pathX, CancellationToken.None)); - // remove a config document: - Assert.True(await EditSession.HasChangesAsync(solution, solution.RemoveAnalyzerConfigDocument(documentYId), CancellationToken.None)); + Assert.Equal(0, generatorExecutionCount); - // add a project: + // source generator infrastructure compares content and reuses state if it matches (SourceGeneratedDocumentState.WithUpdatedGeneratedContent): + AssertEx.Equal(documentKind == DocumentKind.Source ? new[] { documentId } : Array.Empty(), + await EditSession.GetChangedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None)); + + await EditSession.PopulateChangedAndAddedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), changedOrAddedDocuments, CancellationToken.None); + Assert.Empty(changedOrAddedDocuments); + + Assert.Equal(1, generatorExecutionCount); + // + // Update document content + // + + generatorExecutionCount = 0; oldSolution = solution; - var projectD = solution.AddProject("D", "D", "C#"); - solution = projectD.Solution; + solution = documentKind switch + { + DocumentKind.Source => solution.WithDocumentText(documentId, SourceText.From("xxx-changed", Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256)), + DocumentKind.Additional => solution.WithAdditionalDocumentText(documentId, SourceText.From("xxx-changed", Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256)), + DocumentKind.AnalyzerConfig => solution.WithAnalyzerConfigDocumentText(documentId, GetAnalyzerConfigText(new[] { ("x", "2") })), + _ => throw ExceptionUtilities.Unreachable, + }; + Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None)); + Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, pathX, CancellationToken.None)); + + AssertEx.Equal(documentKind == DocumentKind.Source ? new[] { documentId, generatedDocumentId } : new[] { generatedDocumentId }, + await EditSession.GetChangedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None)); + + await EditSession.PopulateChangedAndAddedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), changedOrAddedDocuments, CancellationToken.None); + AssertEx.Equal(documentKind == DocumentKind.Source ? new[] { documentId, generatedDocumentId } : new[] { generatedDocumentId }, changedOrAddedDocuments.Select(d => d.Id)); + + Assert.Equal(1, generatorExecutionCount); + + // + // Remove document + // + generatorExecutionCount = 0; + oldSolution = solution; + solution = documentKind switch + { + DocumentKind.Source => solution.RemoveDocument(documentId), + DocumentKind.Additional => solution.RemoveAdditionalDocument(documentId), + DocumentKind.AnalyzerConfig => solution.RemoveAnalyzerConfigDocument(documentId), + _ => throw ExceptionUtilities.Unreachable, + }; Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None)); + Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, pathX, CancellationToken.None)); - // remove a project: - Assert.True(await EditSession.HasChangesAsync(solution, solution.RemoveProject(projectD.Id), CancellationToken.None)); + Assert.Equal(0, generatorExecutionCount); - EndDebuggingSession(debuggingSession); + AssertEx.Equal(new[] { generatedDocumentId }, + await EditSession.GetChangedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None)); + + await EditSession.PopulateChangedAndAddedDocumentsAsync(oldSolution.GetProject(projectId), solution.GetProject(projectId), changedOrAddedDocuments, CancellationToken.None); + AssertEx.Equal(new[] { generatedDocumentId }, changedOrAddedDocuments.Select(d => d.Id)); + + Assert.Equal(1, generatorExecutionCount); } [Fact] diff --git a/src/Features/Core/Portable/EditAndContinue/EditSession.cs b/src/Features/Core/Portable/EditAndContinue/EditSession.cs index 6c77a7e1dda9d..8da1f41df8a94 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditSession.cs @@ -263,6 +263,7 @@ private async Task GetBaseActiveStatementsAsync(Cancellatio public static async ValueTask HasChangesAsync(Solution oldSolution, Solution newSolution, string sourceFilePath, CancellationToken cancellationToken) { + // Note that this path look up does not work for source-generated files: var newDocumentIds = newSolution.GetDocumentIdsWithFilePath(sourceFilePath); if (newDocumentIds.IsEmpty) { @@ -272,14 +273,14 @@ public static async ValueTask HasChangesAsync(Solution oldSolution, Soluti // it suffices to check the content of one of the document if there are multiple linked ones: var documentId = newDocumentIds.First(); - var oldDocument = oldSolution.GetDocument(documentId); + var oldDocument = oldSolution.GetTextDocument(documentId); if (oldDocument == null) { // file has been added return true; } - var newDocument = newSolution.GetRequiredDocument(documentId); + var newDocument = newSolution.GetRequiredTextDocument(documentId); return oldDocument != newDocument && !await ContentEqualsAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false); } @@ -309,7 +310,7 @@ public static async ValueTask HasChangesAsync(Solution oldSolution, Soluti return false; } - private static async ValueTask ContentEqualsAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken) + private static async ValueTask ContentEqualsAsync(TextDocument oldDocument, TextDocument newDocument, CancellationToken cancellationToken) { // Check if the currently observed document content has changed compared to the base document content. // This is an important optimization that aims to avoid IO while stepping in sources that have not changed. @@ -325,7 +326,7 @@ private static async ValueTask ContentEqualsAsync(Document oldDocument, Do return oldSource.ContentEquals(newSource); } - private static async ValueTask HasChangedOrAddedDocumentsAsync(Project oldProject, Project newProject, ArrayBuilder? changedOrAddedDocuments, CancellationToken cancellationToken) + internal static async ValueTask HasChangedOrAddedDocumentsAsync(Project oldProject, Project newProject, ArrayBuilder? changedOrAddedDocuments, CancellationToken cancellationToken) { if (!newProject.SupportsEditAndContinue()) { @@ -374,35 +375,49 @@ private static async ValueTask HasChangedOrAddedDocumentsAsync(Project old changedOrAddedDocuments.Add(document); } - // Any changes in non-generated documents might affect source generated documents as well, + // Any changes in non-generated document content might affect source generated documents as well, // no need to check further in that case. - return changedOrAddedDocuments is { Count: > 0 } || - HasChangesThatMayAffectSourceGenerators(oldProject.State, newProject.State); - } - private static async Task PopulateChangedAndAddedDocumentsAsync(CommittedSolution oldSolution, Project newProject, ArrayBuilder changedOrAddedDocuments, CancellationToken cancellationToken) - { - changedOrAddedDocuments.Clear(); + if (changedOrAddedDocuments is { Count: > 0 }) + { + return true; + } - var oldProject = oldSolution.GetProject(newProject.Id); - if (oldProject == null) + foreach (var documentId in newProject.State.AdditionalDocumentStates.GetChangedStateIds(oldProject.State.AdditionalDocumentStates, ignoreUnchangedContent: true)) { - EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not loaded", newProject.Id.DebugName, newProject.Id); - - // TODO (https://github.com/dotnet/roslyn/issues/1204): - // - // When debugging session is started some projects might not have been loaded to the workspace yet (may be explicitly unloaded by the user). - // We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied - // and will result in source mismatch when the user steps into them. - // - // We can allow project to be added by including all its documents here. - // When we analyze these documents later on we'll check if they match the PDB. - // If so we can add them to the committed solution and detect further changes. - // It might be more efficient though to track added projects separately. + var document = newProject.GetRequiredAdditionalDocument(documentId); + if (!await ContentEqualsAsync(oldProject.GetRequiredAdditionalDocument(documentId), document, cancellationToken).ConfigureAwait(false)) + { + return true; + } + } - return; + foreach (var documentId in newProject.State.AnalyzerConfigDocumentStates.GetChangedStateIds(oldProject.State.AnalyzerConfigDocumentStates, ignoreUnchangedContent: true)) + { + var document = newProject.GetRequiredAnalyzerConfigDocument(documentId); + if (!await ContentEqualsAsync(oldProject.GetRequiredAnalyzerConfigDocument(documentId), document, cancellationToken).ConfigureAwait(false)) + { + return true; + } + } + + // TODO: should handle removed documents above (detect them as edits) https://github.com/dotnet/roslyn/issues/62848 + if (newProject.State.DocumentStates.GetRemovedStateIds(oldProject.State.DocumentStates).Any() || + newProject.State.AdditionalDocumentStates.GetRemovedStateIds(oldProject.State.AdditionalDocumentStates).Any() || + newProject.State.AdditionalDocumentStates.GetAddedStateIds(oldProject.State.AdditionalDocumentStates).Any() || + newProject.State.AnalyzerConfigDocumentStates.GetRemovedStateIds(oldProject.State.AnalyzerConfigDocumentStates).Any() || + newProject.State.AnalyzerConfigDocumentStates.GetAddedStateIds(oldProject.State.AnalyzerConfigDocumentStates).Any()) + { + return true; } + return false; + } + + internal static async Task PopulateChangedAndAddedDocumentsAsync(Project oldProject, Project newProject, ArrayBuilder changedOrAddedDocuments, CancellationToken cancellationToken) + { + changedOrAddedDocuments.Clear(); + if (!await HasChangedOrAddedDocumentsAsync(oldProject, newProject, changedOrAddedDocuments, cancellationToken).ConfigureAwait(false)) { return; @@ -439,6 +454,9 @@ private static async Task PopulateChangedAndAddedDocumentsAsync(CommittedSolutio } } + /// + /// Enumerates s of changed (not added or removed) s (not additional nor analyzer config). + /// internal static async IAsyncEnumerable GetChangedDocumentsAsync(Project oldProject, Project newProject, [EnumeratorCancellation] CancellationToken cancellationToken) { Debug.Assert(oldProject.Id == newProject.Id); @@ -453,7 +471,17 @@ internal static async IAsyncEnumerable GetChangedDocumentsAsync(Proj yield return documentId; } - if (!HasChangesThatMayAffectSourceGenerators(oldProject.State, newProject.State)) + // Given the following assumptions: + // - source generators are deterministic, + // - metadata references and compilation options have not changed (TODO -- need to check), + // - source documents have not changed, + // - additional documents have not changed, + // - analyzer config documents have not changed, + // the outputs of source generators will not change. + + if (!newProject.State.DocumentStates.HasAnyStateChanges(oldProject.State.DocumentStates) && + !newProject.State.AdditionalDocumentStates.HasAnyStateChanges(oldProject.State.AdditionalDocumentStates) && + !newProject.State.AnalyzerConfigDocumentStates.HasAnyStateChanges(oldProject.State.AnalyzerConfigDocumentStates)) { // Based on the above assumption there are no changes in source generated files. yield break; @@ -473,21 +501,6 @@ internal static async IAsyncEnumerable GetChangedDocumentsAsync(Proj } } - /// - /// Given the following assumptions: - /// - source generators are deterministic, - /// - source documents, metadata references and compilation options have not changed, - /// - additional documents have not changed, - /// - analyzer config documents have not changed, - /// the outputs of source generators will not change. - /// - /// Currently it's not possible to change compilation options (Project System is readonly during debugging). - /// - private static bool HasChangesThatMayAffectSourceGenerators(ProjectState oldProject, ProjectState newProject) - => newProject.DocumentStates.HasAnyStateChanges(oldProject.DocumentStates) || - newProject.AdditionalDocumentStates.HasAnyStateChanges(oldProject.AdditionalDocumentStates) || - newProject.AnalyzerConfigDocumentStates.HasAnyStateChanges(oldProject.AnalyzerConfigDocumentStates); - private async Task<(ImmutableArray results, ImmutableArray diagnostics)> AnalyzeDocumentsAsync( ArrayBuilder changedOrAddedDocuments, ActiveStatementSpanProvider newDocumentActiveStatementSpanProvider, @@ -796,7 +809,26 @@ public async ValueTask EmitSolutionUpdateAsync(Solution solution var hasEmitErrors = false; foreach (var newProject in solution.Projects) { - await PopulateChangedAndAddedDocumentsAsync(oldSolution, newProject, changedOrAddedDocuments, cancellationToken).ConfigureAwait(false); + var oldProject = oldSolution.GetProject(newProject.Id); + if (oldProject == null) + { + EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not loaded", newProject.Id.DebugName, newProject.Id); + + // TODO (https://github.com/dotnet/roslyn/issues/1204): + // + // When debugging session is started some projects might not have been loaded to the workspace yet (may be explicitly unloaded by the user). + // We capture the base solution. Edits in files that are in projects that haven't been loaded won't be applied + // and will result in source mismatch when the user steps into them. + // + // We can allow project to be added by including all its documents here. + // When we analyze these documents later on we'll check if they match the PDB. + // If so we can add them to the committed solution and detect further changes. + // It might be more efficient though to track added projects separately. + + continue; + } + + await PopulateChangedAndAddedDocumentsAsync(oldProject, newProject, changedOrAddedDocuments, cancellationToken).ConfigureAwait(false); if (changedOrAddedDocuments.IsEmpty()) { continue; @@ -852,10 +884,6 @@ public async ValueTask EmitSolutionUpdateAsync(Solution solution continue; } - // PopulateChangedAndAddedDocumentsAsync returns no changes if base project does not exist - var oldProject = oldSolution.GetProject(newProject.Id); - Contract.ThrowIfNull(oldProject); - // The capability of a module to apply edits may change during edit session if the user attaches debugger to // an additional process that doesn't support EnC (or detaches from such process). Before we apply edits // we need to check with the debugger. diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs index a39298e965a5f..12d1b7b61b888 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs @@ -45,6 +45,12 @@ public static Document GetRequiredDocument(this Project project, DocumentId docu return document; } + public static TextDocument GetRequiredAdditionalDocument(this Project project, DocumentId documentId) + => project.GetAdditionalDocument(documentId) ?? throw new InvalidOperationException(WorkspaceExtensionsResources.The_solution_does_not_contain_the_specified_document); + + public static TextDocument GetRequiredAnalyzerConfigDocument(this Project project, DocumentId documentId) + => project.GetAnalyzerConfigDocument(documentId) ?? throw new InvalidOperationException(WorkspaceExtensionsResources.The_solution_does_not_contain_the_specified_document); + public static TextDocument GetRequiredTextDocument(this Project project, DocumentId documentId) { var document = project.GetTextDocument(documentId); From 95a985ab5bec6295dbc7213cb0610c547091d9cf Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Thu, 28 Jul 2022 13:51:09 -0700 Subject: [PATCH 2/2] Remove redundant extension methods --- .../Core/Portable/Shared/Extensions/ProjectExtensions.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs index 2985b96f5987d..b22b35955e68e 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ProjectExtensions.cs @@ -43,12 +43,6 @@ public static TextDocument GetRequiredAdditionalDocument(this Project project, D public static TextDocument GetRequiredAnalyzerConfigDocument(this Project project, DocumentId documentId) => project.GetAnalyzerConfigDocument(documentId) ?? throw new InvalidOperationException(WorkspaceExtensionsResources.The_solution_does_not_contain_the_specified_document); - public static TextDocument GetRequiredAdditionalDocument(this Project project, DocumentId documentId) - => project.GetAdditionalDocument(documentId) ?? throw new InvalidOperationException(WorkspaceExtensionsResources.The_solution_does_not_contain_the_specified_document); - - public static TextDocument GetRequiredAnalyzerConfigDocument(this Project project, DocumentId documentId) - => project.GetAnalyzerConfigDocument(documentId) ?? throw new InvalidOperationException(WorkspaceExtensionsResources.The_solution_does_not_contain_the_specified_document); - public static TextDocument GetRequiredTextDocument(this Project project, DocumentId documentId) { var document = project.GetTextDocument(documentId);