From 5f86d25c484ee6538c5e5921f97a2c5b644fa39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Matou=C5=A1ek?= Date: Tue, 14 Jan 2020 10:42:49 -0800 Subject: [PATCH] EnC: Tweak handling of out-of-sync documents to work around source file content inconsistencies (#40947) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Do not block in presence of out-of-sync documents. Instead, ignore any changes made to these documents while debugging until their content matches the source used to build the baseline DLL. * Only check output PDB, not debugger SymReader, for document checksums. Turns out SymReader does not support reading document checksums once EnC changes have been applied. Better handle errors that might occur when validating checksums. Previously some of the errors were not reported as diagnostics. We previously blocked EnC when we observed a source file that is out-of-sync (i.e. its current content does not match the checksum in the originally built PDB). We can however just ignore these files and report a warning that changes made to this file won’t be applied until the file content is reverted back to the state it was when the PDB was built (the file transitions to “matching” state). Once a file is in matching state it can’t change back to another state. We know that we have not applied any change to the code that was compiled from the file because we ignored the file while it was in out-of-sync state. Therefore we know that any changes made from now on can be safely applied to the debuggee. If we can’t determine whether a file matches or not due to error reading the PDB or the source file content we can treat it similarly to out-of-sync file. That is, ignore any changes until we are able to confirm the file matches. That can happen if, e.g. the PDB is temporarily locked by another process and unlocked later. Simplify implementation of GetStatusAsync. Fixes VSO [1051496](https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1051496) ([VS feedback](https://developercommunity.visualstudio.com/content/problem/880533/edits-were-made-which-cannot-be-compiled-stop-debu.html)) --- .../Core/Portable/FileSystem/PathUtilities.cs | 9 +- .../EditAndContinueWorkspaceServiceTests.cs | 367 ++++++++++++------ .../EditSessionActiveStatementsTests.cs | 4 +- .../EditAndContinue/CommittedSolution.cs | 270 +++++-------- .../EditAndContinue/DebuggingSession.cs | 2 +- .../EditAndContinueDiagnosticDescriptors.cs | 1 + .../EditAndContinueErrorCode.cs | 1 + .../EditAndContinueWorkspaceService.cs | 10 +- .../Portable/EditAndContinue/EditSession.cs | 273 +++++++------ .../IEditAndContinueWorkspaceService.cs | 2 +- .../EditAndContinue/PendingSolutionUpdate.cs | 5 +- .../EditAndContinue/SolutionUpdate.cs | 4 - .../Portable/FeaturesResources.Designer.cs | 11 +- .../Core/Portable/FeaturesResources.resx | 5 +- .../Portable/xlf/FeaturesResources.cs.xlf | 9 +- .../Portable/xlf/FeaturesResources.de.xlf | 9 +- .../Portable/xlf/FeaturesResources.es.xlf | 9 +- .../Portable/xlf/FeaturesResources.fr.xlf | 9 +- .../Portable/xlf/FeaturesResources.it.xlf | 9 +- .../Portable/xlf/FeaturesResources.ja.xlf | 9 +- .../Portable/xlf/FeaturesResources.ko.xlf | 9 +- .../Portable/xlf/FeaturesResources.pl.xlf | 9 +- .../Portable/xlf/FeaturesResources.pt-BR.xlf | 9 +- .../Portable/xlf/FeaturesResources.ru.xlf | 9 +- .../Portable/xlf/FeaturesResources.tr.xlf | 9 +- .../xlf/FeaturesResources.zh-Hans.xlf | 9 +- .../xlf/FeaturesResources.zh-Hant.xlf | 9 +- ...rosoft.CodeAnalysis.InteractiveHost.csproj | 2 + ...VisualStudioManagedModuleUpdateProvider.cs | 10 +- 29 files changed, 608 insertions(+), 485 deletions(-) diff --git a/src/Compilers/Core/Portable/FileSystem/PathUtilities.cs b/src/Compilers/Core/Portable/FileSystem/PathUtilities.cs index 24cd06ac821..7b23aeb7e1c 100644 --- a/src/Compilers/Core/Portable/FileSystem/PathUtilities.cs +++ b/src/Compilers/Core/Portable/FileSystem/PathUtilities.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -332,12 +333,14 @@ public static PathKind GetPathKind(string path) return PathKind.Relative; } +#nullable enable + /// /// True if the path is an absolute path (rooted to drive or network share) /// - public static bool IsAbsolute(string path) + public static bool IsAbsolute([NotNullWhen(true)]string? path) { - if (string.IsNullOrEmpty(path)) + if (RoslynString.IsNullOrEmpty(path)) { return false; } @@ -361,6 +364,8 @@ public static bool IsAbsolute(string path) IsDirectorySeparator(path[1]); } +#nullable restore + /// /// Returns true if given path is absolute and starts with a drive specification ("C:\"). /// diff --git a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs b/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs index 569e6e6f7c0..4c93a33946f 100644 --- a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs +++ b/src/EditorFeatures/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs @@ -88,7 +88,7 @@ private EditAndContinueWorkspaceService CreateEditAndContinueService(Workspace w _mockDebugeeModuleMetadataProvider, reportTelemetry: data => EditAndContinueWorkspaceService.LogDebuggingSessionTelemetry(data, (id, message) => _telemetryLog.Add($"{id}: {message.GetMessage()}"), () => ++_telemetryId)); - private DebuggingSession StartDebuggingSession(EditAndContinueWorkspaceService service, CommittedSolution.DocumentState initialState = CommittedSolution.DocumentState.MatchesDebuggee) + private DebuggingSession StartDebuggingSession(EditAndContinueWorkspaceService service, CommittedSolution.DocumentState initialState = CommittedSolution.DocumentState.MatchesBuildOutput) { service.StartDebuggingSession(); var session = service.Test_GetDebuggingSession(); @@ -280,8 +280,7 @@ public async Task RunMode_DesignTimeOnlyDocument() Assert.Empty(diagnostics); // validate solution update status and emit - changes made in design-time-only documents are ignored: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); service.EndDebuggingSession(); VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false); @@ -319,8 +318,9 @@ public async Task RunMode_ProjectNotBuilt() } [Fact] - public async Task RunMode_ErrorReadingFile() + public async Task RunMode_ErrorReadingModuleFile() { + // empty module file will cause read error: var moduleFile = Temp.CreateFile(); using (var workspace = TestWorkspace.CreateCSharp("class C1 { void M() { System.Console.WriteLine(1); } }")) @@ -347,8 +347,7 @@ public async Task RunMode_ErrorReadingFile() Assert.Empty(diagnostics); // validate solution update status and emit - changes made during run mode are ignored: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); @@ -386,7 +385,7 @@ public async Task RunMode_DocumentOutOfSync() Assert.Empty(diagnostics); // the document is now in-sync (a file watcher observed a change and updated the status): - debuggingSession.LastCommittedSolution.Test_SetDocumentState(document2.Id, CommittedSolution.DocumentState.MatchesDebuggee); + debuggingSession.LastCommittedSolution.Test_SetDocumentState(document2.Id, CommittedSolution.DocumentState.MatchesBuildOutput); diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false); AssertEx.Equal(new[] { "ENC1003" }, diagnostics.Select(d => d.Id)); @@ -428,9 +427,7 @@ public async Task RunMode_FileAdded() var diagnostics2 = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false); AssertEx.Equal(new[] { "ENC1003" }, diagnostics2.Select(d => d.Id)); - // validate solution update status and emit - changes made during run mode are ignored: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); service.EndDebuggingSession(); VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Create(document2.Id), false); @@ -455,8 +452,7 @@ public async Task RunMode_Diagnostics() var service = CreateEditAndContinueService(workspace); - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); StartDebuggingSession(service); @@ -465,16 +461,14 @@ public async Task RunMode_Diagnostics() var diagnostics = await service.GetDocumentDiagnosticsAsync(document1, CancellationToken.None).ConfigureAwait(false); Assert.Empty(diagnostics); - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); // change the source: workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M() { System.Console.WriteLine(2); } }")); var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single(); // validate solution update status and emit - changes made during run mode are ignored: - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); @@ -521,8 +515,7 @@ public async Task RunMode_DifferentDocumentWithSameContent() Assert.Empty(diagnostics2); // validate solution update status and emit - changes made during run mode are ignored: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); service.EndDebuggingSession(); @@ -556,8 +549,7 @@ public async Task BreakMode_ProjectThatDoesNotSupportEnC() var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single(); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); @@ -597,8 +589,7 @@ public async Task BreakMode_DesignTimeOnlyDocument_Dynamic() workspace.ChangeDocument(document1.Id, SourceText.From("class E {}")); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); @@ -658,8 +649,7 @@ public async Task BreakMode_DesignTimeOnlyDocument_Wpf(bool delayLoad) Assert.Empty(await service.GetDocumentDiagnosticsAsync(documentC2, CancellationToken.None).ConfigureAwait(false)); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); @@ -670,8 +660,7 @@ public async Task BreakMode_DesignTimeOnlyDocument_Wpf(bool delayLoad) LoadLibraryToDebuggee(moduleInfo); // validate solution update status and emit: - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); @@ -683,8 +672,9 @@ public async Task BreakMode_DesignTimeOnlyDocument_Wpf(bool delayLoad) } [Fact] - public async Task BreakMode_ErrorReadingFile() + public async Task BreakMode_ErrorReadingModuleFile() { + // module file is empty, which will cause a read error: var moduleFile = Temp.CreateFile(); using (var workspace = TestWorkspace.CreateCSharp("class C1 { void M() { System.Console.WriteLine(1); } }")) @@ -706,9 +696,7 @@ public async Task BreakMode_ErrorReadingFile() var diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false); Assert.Empty(diagnostics); - // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); Assert.Empty(_emitDiagnosticsUpdated); Assert.Equal(0, _emitDiagnosticsClearedCount); @@ -742,6 +730,161 @@ public async Task BreakMode_ErrorReadingFile() } } + [Fact] + public async Task BreakMode_ErrorReadingPdbFile() + { + var source1 = "class C1 { void M() { System.Console.WriteLine(1); } }"; + + var dir = Temp.CreateDirectory(); + var sourceFile = dir.CreateFile("a.cs").WriteAllText(source1); + + using var workspace = new TestWorkspace(); + + var document1 = workspace.CurrentSolution. + AddProject("test", "test", LanguageNames.CSharp). + AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)). + AddDocument("a.cs", SourceText.From(source1, Encoding.UTF8), filePath: sourceFile.Path); + + var project = document1.Project; + workspace.ChangeSolution(project.Solution); + + var (_, moduleId) = EmitAndLoadLibraryToDebuggee(source1, project.Id, sourceFilePath: sourceFile.Path); + + _mockCompilationOutputsService.Outputs[project.Id] = new MockCompilationOutputs(moduleId) + { + OpenPdbStreamImpl = () => + { + throw new IOException("Error"); + } + }; + + var service = CreateEditAndContinueService(workspace); + StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None); + service.StartEditSession(); + + // change the source: + workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M() { System.Console.WriteLine(2); } }", Encoding.UTF8)); + var document2 = workspace.CurrentSolution.GetDocument(document1.Id); + + // error not reported here since it might be intermittent and will be reported if the issue persist when applying the update: + var diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false); + Assert.Empty(diagnostics); + + // an error occured so we need to call update to determine whether we have changes to apply or not: + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); + + Assert.Empty(_emitDiagnosticsUpdated); + Assert.Equal(0, _emitDiagnosticsClearedCount); + + var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); + Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); + Assert.Empty(deltas); + + Assert.Equal(1, _emitDiagnosticsClearedCount); + var eventArgs = _emitDiagnosticsUpdated.Single(); + Assert.Null(eventArgs.DocumentId); + Assert.Equal(project.Id, eventArgs.ProjectId); + AssertEx.Equal(new[] { "Warning ENC1006: " + string.Format(FeaturesResources.UnableToReadSourceFileOrPdb, sourceFile.Path) }, + eventArgs.Diagnostics.Select(d => $"{d.Severity} {d.Id}: {d.Message}")); + + _emitDiagnosticsUpdated.Clear(); + _emitDiagnosticsClearedCount = 0; + + service.EndEditSession(); + Assert.Empty(_emitDiagnosticsUpdated); + Assert.Equal(0, _emitDiagnosticsClearedCount); + + service.EndDebuggingSession(); + Assert.Empty(_emitDiagnosticsUpdated); + Assert.Equal(1, _emitDiagnosticsClearedCount); + + AssertEx.Equal(new[] + { + "Debugging_EncSession: SessionId=1|SessionCount=0|EmptySessionCount=1" + }, _telemetryLog); + } + + [Fact] + public async Task BreakMode_ErrorReadingSourceFile() + { + var source1 = "class C1 { void M() { System.Console.WriteLine(1); } }"; + + var dir = Temp.CreateDirectory(); + var sourceFile = dir.CreateFile("a.cs").WriteAllText(source1); + + using var workspace = new TestWorkspace(); + + var document1 = workspace.CurrentSolution. + AddProject("test", "test", LanguageNames.CSharp). + AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)). + AddDocument("a.cs", SourceText.From(source1, Encoding.UTF8), filePath: sourceFile.Path); + + var project = document1.Project; + workspace.ChangeSolution(project.Solution); + + var (_, moduleId) = EmitAndLoadLibraryToDebuggee(source1, project.Id, sourceFilePath: sourceFile.Path); + + var service = CreateEditAndContinueService(workspace); + StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None); + service.StartEditSession(); + + // change the source: + workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void M() { System.Console.WriteLine(2); } }", Encoding.UTF8)); + var document2 = workspace.CurrentSolution.GetDocument(document1.Id); + + using var fileLock = File.Open(sourceFile.Path, FileMode.Open, FileAccess.Read, FileShare.None); + + // error not reported here since it might be intermittent and will be reported if the issue persist when applying the update: + var diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false); + Assert.Empty(diagnostics); + + // an error occured so we need to call update to determine whether we have changes to apply or not: + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); + + Assert.Empty(_emitDiagnosticsUpdated); + Assert.Equal(0, _emitDiagnosticsClearedCount); + + // try apply changes: + var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); + Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); + Assert.Empty(deltas); + + Assert.Equal(1, _emitDiagnosticsClearedCount); + var eventArgs = _emitDiagnosticsUpdated.Single(); + Assert.Null(eventArgs.DocumentId); + Assert.Equal(project.Id, eventArgs.ProjectId); + AssertEx.Equal(new[] { "Warning ENC1006: " + string.Format(FeaturesResources.UnableToReadSourceFileOrPdb, sourceFile.Path) }, + eventArgs.Diagnostics.Select(d => $"{d.Severity} {d.Id}: {d.Message}")); + + _emitDiagnosticsUpdated.Clear(); + _emitDiagnosticsClearedCount = 0; + + fileLock.Dispose(); + + // try apply changes again: + (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); + Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit); + Assert.NotEmpty(deltas); + + Assert.Equal(1, _emitDiagnosticsClearedCount); + Assert.Empty(_emitDiagnosticsUpdated); + _emitDiagnosticsClearedCount = 0; + + service.EndEditSession(); + Assert.Empty(_emitDiagnosticsUpdated); + Assert.Equal(0, _emitDiagnosticsClearedCount); + + service.EndDebuggingSession(); + Assert.Empty(_emitDiagnosticsUpdated); + Assert.Equal(1, _emitDiagnosticsClearedCount); + + AssertEx.Equal(new[] + { + "Debugging_EncSession: SessionId=1|SessionCount=1|EmptySessionCount=0", + "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=False|HadValidChanges=True|HadValidInsignificantChanges=False|RudeEditsCount=0|EmitDeltaErrorIdCount=0" + }, _telemetryLog); + } + [Fact] public async Task BreakMode_FileAdded() { @@ -775,9 +918,7 @@ public async Task BreakMode_FileAdded() var diagnostics2 = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false); AssertEx.Equal(new[] { "ENC2123" }, diagnostics2.Select(d => d.Id)); - // validate solution update status and emit - changes made during run mode are ignored: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); service.EndEditSession(); service.EndDebuggingSession(); @@ -824,7 +965,7 @@ void M() "[136..137): " + expectedMessage, }; - string inspectDiagnostic(Diagnostic d) + static string inspectDiagnostic(Diagnostic d) => $"{d.Location.SourceSpan}: {d.Id}: {d.GetMessage()}"; using (var workspace = TestWorkspace.CreateCSharp(source1)) @@ -871,8 +1012,7 @@ string inspectDiagnostic(Diagnostic d) AssertEx.Equal(expectedDiagnostics, diagnostics3.Select(inspectDiagnostic)); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit); @@ -930,8 +1070,7 @@ public async Task BreakMode_Encodings() await debuggingSession.LastCommittedSolution.OnSourceFileUpdatedAsync(documentId, debuggingSession.CancellationToken).ConfigureAwait(false); // EnC service queries for a document, which triggers read of the source file from disk. - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); service.EndEditSession(); service.EndDebuggingSession(); @@ -964,8 +1103,7 @@ public async Task BreakMode_RudeEdits() diagnostics1.Select(d => $"{d.Id}: {d.GetMessage()}")); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit); @@ -991,60 +1129,76 @@ public async Task BreakMode_RudeEdits() [Fact] public async Task BreakMode_RudeEdits_DocumentOutOfSync() { + var source0 = "class C1 { void M() { System.Console.WriteLine(0); } }"; var source1 = "class C1 { void M() { System.Console.WriteLine(1); } }"; - using var workspace = TestWorkspace.CreateCSharp(source1); + var dir = Temp.CreateDirectory(); + var sourceFile = dir.CreateFile("a.cs"); - var project = workspace.CurrentSolution.Projects.Single(); - var (_, moduleId) = EmitAndLoadLibraryToDebuggee(source1, project.Id); - var document1 = workspace.CurrentSolution.Projects.Single().Documents.Single(); + using var workspace = new TestWorkspace(); - var service = CreateEditAndContinueService(workspace); + var project = workspace.CurrentSolution. + AddProject("test", "test", LanguageNames.CSharp). + AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)); - var debuggingSession = StartDebuggingSession(service); - debuggingSession.LastCommittedSolution.Test_SetDocumentState(document1.Id, CommittedSolution.DocumentState.OutOfSync); + workspace.ChangeSolution(project.Solution); + + // compile with source0: + var (_, moduleId) = EmitAndLoadLibraryToDebuggee(source0, project.Id, sourceFilePath: sourceFile.Path); + + // update the file with source1 before session starts: + sourceFile.WriteAllText(source1); + + // source1 is reflected in workspace before session starts: + var document1 = project.AddDocument("a.cs", SourceText.From(source1, Encoding.UTF8), filePath: sourceFile.Path); + workspace.ChangeSolution(document1.Project.Solution); + + var service = CreateEditAndContinueService(workspace); + var debuggingSession = StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None); service.StartEditSession(); VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false); // change the source (rude edit): workspace.ChangeDocument(document1.Id, SourceText.From("class C1 { void RenamedMethod() { System.Console.WriteLine(1); } }")); - var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single(); + var document2 = workspace.CurrentSolution.GetDocument(document1.Id); // no Rude Edits, since the document is out-of-sync var diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false); Assert.Empty(diagnostics); - // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + // since the document is out-of-sync we need to call update to determine whether we have changes to apply or not: + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit); + Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); Assert.Empty(deltas); AssertEx.Equal( - new[] { "ENC1005: " + string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, "test1.cs") }, + new[] { "ENC1005: " + string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, sourceFile.Path) }, _emitDiagnosticsUpdated.Single().Diagnostics.Select(d => $"{d.Id}: {d.Message}")); _emitDiagnosticsUpdated.Clear(); _emitDiagnosticsClearedCount = 0; - // the document is now in-sync (a file watcher observed a change and updated the status): - debuggingSession.LastCommittedSolution.Test_SetDocumentState(document1.Id, CommittedSolution.DocumentState.MatchesDebuggee); + // update the file to match the build: + sourceFile.WriteAllText(source0); + // we do not reload the content of out-of-sync file for analyzer query: diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false); - AssertEx.Equal( - new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_will_prevent_the_debug_session_from_continuing, FeaturesResources.method) }, - diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); + Assert.Empty(diagnostics); - // validate solution update status and emit: - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + // debugger query will trigger reload of out-of-sync file content: + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); + + // now we see the rude edit: + diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false); + AssertEx.Equal(new[] { "ENC0020" }, diagnostics.Select(d => d.Id)); (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit); Assert.Empty(deltas); + Assert.Empty(_emitDiagnosticsUpdated); service.EndEditSession(); VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Create(document2.Id), false); @@ -1100,8 +1254,7 @@ public async Task BreakMode_RudeEdits_DocumentWithoutSequencePoints() diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit); @@ -1148,8 +1301,7 @@ public async Task BreakMode_RudeEdits_DelayLoadedModule() new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_will_prevent_the_debug_session_from_continuing, FeaturesResources.method) }, diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit); @@ -1164,8 +1316,7 @@ public async Task BreakMode_RudeEdits_DelayLoadedModule() new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_will_prevent_the_debug_session_from_continuing, FeaturesResources.method) }, diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}")); - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit); @@ -1202,8 +1353,7 @@ public async Task BreakMode_SyntaxError() AssertEx.Empty(diagnostics1); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit); @@ -1253,8 +1403,7 @@ public async Task BreakMode_SemanticError() // The EnC analyzer does not check for and block on all semantic errors as they are already reported by diagnostic analyzer. // Blocking update on semantic errors would be possible, but the status check is only an optimization to avoid emitting. - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit); @@ -1310,16 +1459,13 @@ public async Task BreakMode_FileStatus_CompilationError() workspace.ChangeDocument(documentC.Id, SourceText.From("class C { void M() { ")); // Common.cs is included in projects B and C. Both of these projects must have no errors, otherwise update is blocked. - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: "Common.cs", CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: "Common.cs", CancellationToken.None).ConfigureAwait(false)); // No changes in project containing file B.cs. - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: "B.cs", CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: "B.cs", CancellationToken.None).ConfigureAwait(false)); // All projects must have no errors. - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); service.EndEditSession(); service.EndDebuggingSession(); @@ -1353,8 +1499,7 @@ public async Task BreakMode_ValidSignificantChange_EmitError() AssertEx.Empty(diagnostics1); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); AssertEx.Equal(new[] { "CS8055" }, _emitDiagnosticsUpdated.Single().Diagnostics.Select(d => d.Id)); @@ -1378,8 +1523,7 @@ public async Task BreakMode_ValidSignificantChange_EmitError() Assert.Empty(editSession.DebuggingSession.GetBaselineModuleReaders()); // solution update status after discarding an update (still has update ready): - var commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Ready, commitedUpdateSolutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); service.EndEditSession(); Assert.Empty(_emitDiagnosticsUpdated); @@ -1455,10 +1599,9 @@ public async Task BreakMode_ValidSignificantChange_ApplyBeforeFileWatcherEvent(b } // EnC service queries for a document, which triggers read of the source file from disk. - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus); Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit); service.CommitSolutionUpdate(); @@ -1470,17 +1613,17 @@ public async Task BreakMode_ValidSignificantChange_ApplyBeforeFileWatcherEvent(b workspace.ChangeDocument(documentId, CreateSourceTextFromFile(sourceFile.Path)); var document3 = workspace.CurrentSolution.Projects.Single().Documents.Single(); - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); + var hasChanges = await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); if (saveDocument) { - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(hasChanges); Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); } else { - Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus); + Assert.True(hasChanges); Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit); } @@ -1491,9 +1634,9 @@ public async Task BreakMode_ValidSignificantChange_ApplyBeforeFileWatcherEvent(b [Fact] public async Task BreakMode_ValidSignificantChange_FileUpdateBeforeDebuggingSessionStarts() { - // workspace: --V0--------------V2-------|--------V3---------------V1--------------| - // file system: --V0---------V1-----V2-----|---------------------------V1------------| - // \--build--/ ^save F5 ^ ^F10 (blocked) ^save F10 (ok) + // workspace: --V0--------------V2-------|--------V3------------------V1--------------| + // file system: --V0---------V1-----V2-----|------------------------------V1------------| + // \--build--/ ^save F5 ^ ^F10 (no change) ^save F10 (ok) // file watcher: no-op var source1 = "class C1 { void M() { System.Console.WriteLine(1); } }"; @@ -1533,11 +1676,11 @@ public async Task BreakMode_ValidSignificantChange_FileUpdateBeforeDebuggingSess var diagnostics = await service.GetDocumentDiagnosticsAsync(document3, CancellationToken.None).ConfigureAwait(false); AssertEx.Empty(diagnostics); - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); + // since the document is out-of-sync we need to call update to determine whether we have changes to apply or not: + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus); - Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit); + var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); + Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); AssertEx.Equal( new[] { "ENC1005: " + string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, sourceFile.Path) }, @@ -1556,11 +1699,10 @@ public async Task BreakMode_ValidSignificantChange_FileUpdateBeforeDebuggingSess Assert.Equal(CommittedSolution.DocumentState.OutOfSync, state); sourceFile.WriteAllText(source1); - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); // the content actually hasn't changed: - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); service.EndEditSession(); @@ -1603,8 +1745,7 @@ public async Task BreakMode_ValidSignificantChange_DocumentOutOfSync(bool delayL VerifyReanalyzeInvocation(workspace, null, ImmutableArray.Empty, false); // no changes have been made to the project - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); @@ -1623,8 +1764,7 @@ public async Task BreakMode_ValidSignificantChange_DocumentOutOfSync(bool delayL Assert.Empty(diagnostics); // the content of the file is now exactly the same as the compiled document, so there is no change to be applied: - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, solutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); (solutionStatusEmit, _) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit); @@ -1675,8 +1815,7 @@ public async Task BreakMode_ValidSignificantChange_EmitSuccessful(bool commitUpd AssertEx.Empty(diagnostics1); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); AssertEx.Empty(emitDiagnosticsUpdated); @@ -1718,8 +1857,8 @@ public async Task BreakMode_ValidSignificantChange_EmitSuccessful(bool commitUpd Assert.Same(newBaseline, editSession.DebuggingSession.Test_GetProjectEmitBaseline(project.Id)); // solution update status after committing an update: - var commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, commitedUpdateSolutionStatus); + var commitedUpdateSolutionStatus = await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); + Assert.False(commitedUpdateSolutionStatus); } else { @@ -1729,8 +1868,8 @@ public async Task BreakMode_ValidSignificantChange_EmitSuccessful(bool commitUpd Assert.Null(service.Test_GetPendingSolutionUpdate()); // solution update status after committing an update: - var discardedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Ready, discardedUpdateSolutionStatus); + var discardedUpdateSolutionStatus = await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); + Assert.True(discardedUpdateSolutionStatus); } service.EndEditSession(); @@ -1804,8 +1943,7 @@ public async Task BreakMode_ValidSignificantChange_EmitSuccessful_UpdateDeferred var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single(); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit); @@ -1851,8 +1989,7 @@ public async Task BreakMode_ValidSignificantChange_EmitSuccessful_UpdateDeferred Assert.Same(newBaseline, editSession.DebuggingSession.Test_GetProjectEmitBaseline(project.Id)); // solution update status after committing an update: - var commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, commitedUpdateSolutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); service.EndEditSession(); @@ -1952,8 +2089,7 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule() workspace.ChangeDocument(projectB.Documents.Single().Id, SourceText.From(source2, Encoding.UTF8)); // validate solution update status and emit: - var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit); @@ -1995,8 +2131,7 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule() Assert.Same(newBaselineB1, editSession.DebuggingSession.Test_GetProjectEmitBaseline(projectB.Id)); // solution update status after committing an update: - var commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, commitedUpdateSolutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); service.EndEditSession(); service.StartEditSession(); @@ -2010,8 +2145,7 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule() workspace.ChangeDocument(projectB.Documents.Single().Id, SourceText.From(source3, Encoding.UTF8)); // validate solution update status and emit: - solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.Ready, solutionStatus); + Assert.True(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false); Assert.Equal(SolutionUpdateStatus.Ready, solutionStatusEmit); @@ -2052,8 +2186,7 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule() Assert.Same(newBaselineB2, editSession.DebuggingSession.Test_GetProjectEmitBaseline(projectB.Id)); // solution update status after committing an update: - commitedUpdateSolutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(SolutionUpdateStatus.None, commitedUpdateSolutionStatus); + Assert.False(await service.HasChangesAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false)); service.EndEditSession(); diff --git a/src/EditorFeatures/Test/EditAndContinue/EditSessionActiveStatementsTests.cs b/src/EditorFeatures/Test/EditAndContinue/EditSessionActiveStatementsTests.cs index a06a32d1b5c..98b9e83359b 100644 --- a/src/EditorFeatures/Test/EditAndContinue/EditSessionActiveStatementsTests.cs +++ b/src/EditorFeatures/Test/EditAndContinue/EditSessionActiveStatementsTests.cs @@ -97,7 +97,7 @@ private sealed class Validator ImmutableArray activeStatements, ImmutableDictionary> nonRemappableRegions = null, Func adjustSolution = null, - CommittedSolution.DocumentState initialState = CommittedSolution.DocumentState.MatchesDebuggee) + CommittedSolution.DocumentState initialState = CommittedSolution.DocumentState.MatchesBuildOutput) { var exportProviderFactory = ExportProviderCache.GetOrCreateExportProviderFactory( TestExportProvider.MinimumCatalogWithCSharpAndVisualBasic.WithPart(typeof(CSharpEditAndContinueAnalyzer)).WithPart(typeof(DummyLanguageService))); @@ -544,7 +544,7 @@ static void M() }, baseExceptionRegions.Select(r => r.Spans.IsDefault ? "out-of-sync" : "[" + string.Join(",", r.Spans) + "]")); // document got synchronized: - validator.EditSession.DebuggingSession.LastCommittedSolution.Test_SetDocumentState(docs[0], CommittedSolution.DocumentState.MatchesDebuggee); + validator.EditSession.DebuggingSession.LastCommittedSolution.Test_SetDocumentState(docs[0], CommittedSolution.DocumentState.MatchesBuildOutput); baseExceptionRegions = await validator.EditSession.GetBaseActiveExceptionRegionsAsync(CancellationToken.None).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs b/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs index 3350d70ac3f..aaea0a9a0ac 100644 --- a/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs +++ b/src/Features/Core/Portable/EditAndContinue/CommittedSolution.cs @@ -34,41 +34,31 @@ internal enum DocumentState { None = 0, - /// - /// The current document content matches the content the built module was compiled with. - /// The document content is matched with the build output instead of the loaded module - /// since the module hasn't been loaded yet. - /// - /// This document state may change to , , - /// or or once the module has been loaded. - /// - MatchesBuildOutput = 1, - /// /// The current document content does not match the content the module was compiled with. - /// This document state may change to or . + /// This document state may change to or . /// - OutOfSync = 2, + OutOfSync = 1, /// - /// The current document content matches the content the loaded module was compiled with. - /// This is a final state. Once a document is in this state it won't switch to a different one. + /// It hasn't been possible to determine whether the current document content does matches the content + /// the module was compiled with due to error while reading the PDB or the source file. + /// This document state may change to or . /// - MatchesDebuggee = 3, + Indeterminate = 2, /// /// The document is not compiled into the module. It's only included in the project /// to support design-time features such as completion, etc. /// This is a final state. Once a document is in this state it won't switch to a different one. /// - DesignTimeOnly = 4, - } + DesignTimeOnly = 3, - private enum SourceHashOrigin - { - None = 0, - LoadedPdb = 1, - BuiltPdb = 2 + /// + /// The current document content matches the content the built module was compiled with. + /// This is a final state. Once a document is in this state it won't switch to a different one. + /// + MatchesBuildOutput = 4 } /// @@ -93,8 +83,8 @@ private enum SourceHashOrigin /// from which the assembly is built. These documents won't have a record in the PDB and will be tracked as /// . /// - /// A document state can only change from to . - /// Once a document state is or + /// A document state can only change from to . + /// Once a document state is or /// it will never change. /// private readonly Dictionary _documentState; @@ -148,7 +138,6 @@ public Task OnSourceFileUpdatedAsync(DocumentId documentId, CancellationToken ca public async Task<(Document? Document, DocumentState State)> GetDocumentAndStateAsync(DocumentId documentId, CancellationToken cancellationToken, bool reloadOutOfSyncDocument = false) { Document? document; - var matchLoadedModulesOnly = false; lock (_guard) { @@ -158,29 +147,16 @@ public async Task<(Document? Document, DocumentState State)> GetDocumentAndState return (null, DocumentState.None); } - if (document.FilePath == null) - { - return (null, DocumentState.DesignTimeOnly); - } - if (_documentState.TryGetValue(documentId, out var documentState)) { switch (documentState) { - case DocumentState.MatchesDebuggee: + case DocumentState.MatchesBuildOutput: return (document, documentState); case DocumentState.DesignTimeOnly: return (null, documentState); - case DocumentState.MatchesBuildOutput: - // Module might have been loaded since the last time we checked, - // let's check whether that is so and the document now matches the debuggee. - // Do not try to read the information from on-disk module again. - // CONSIDER: Reusing the state until we receive module load event. - matchLoadedModulesOnly = true; - break; - case DocumentState.OutOfSync: if (reloadOutOfSyncDocument) { @@ -189,23 +165,33 @@ public async Task<(Document? Document, DocumentState State)> GetDocumentAndState return (null, documentState); + case DocumentState.Indeterminate: + // Previous attempt resulted in a read error. Try again. + break; + case DocumentState.None: throw ExceptionUtilities.Unreachable; } } + + if (!PathUtilities.IsAbsolute(document.FilePath)) + { + return (null, DocumentState.DesignTimeOnly); + } } var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - var (matchingSourceText, checksumOrigin, isDocumentMissing) = await TryGetPdbMatchingSourceTextAsync( - document.FilePath, sourceText.Encoding, document.Project.Id, matchLoadedModulesOnly, cancellationToken).ConfigureAwait(false); + var (matchingSourceText, pdbHasDocument) = await Task.Run( + () => TryGetPdbMatchingSourceText(document.FilePath, sourceText.Encoding, document.Project.Id), + cancellationToken).ConfigureAwait(false); lock (_guard) { // only listed document states can be changed: if (_documentState.TryGetValue(documentId, out var documentState) && documentState != DocumentState.OutOfSync && - documentState != DocumentState.MatchesBuildOutput) + documentState != DocumentState.Indeterminate) { return (document, documentState); } @@ -213,24 +199,15 @@ public async Task<(Document? Document, DocumentState State)> GetDocumentAndState DocumentState newState; Document? matchingDocument; - if (checksumOrigin == SourceHashOrigin.None) + if (pdbHasDocument == null) { - // We know the document matches the build output and the module is still not loaded. - if (matchLoadedModulesOnly) - { - return (document, DocumentState.MatchesBuildOutput); - } - - // PDB for the module not found (neither loaded nor in built outputs): - Debug.Assert(isDocumentMissing); - return (null, DocumentState.DesignTimeOnly); + // Unable to determine due to error reading the PDB or the source file. + return (document, DocumentState.Indeterminate); } - if (isDocumentMissing) + if (pdbHasDocument == false) { - // Source file is not listed in the PDB. This may happen for a couple of reasons: - // The library wasn't built with that source file - the file has been added before debugging session started but after build captured it. - // This is the case for WPF .g.i.cs files. + // Source file is not listed in the PDB (e.g. WPF .g.i.cs files). matchingDocument = null; newState = DocumentState.DesignTimeOnly; } @@ -246,7 +223,7 @@ public async Task<(Document? Document, DocumentState State)> GetDocumentAndState matchingDocument = _solution.GetDocument(documentId); } - newState = (checksumOrigin == SourceHashOrigin.LoadedPdb) ? DocumentState.MatchesDebuggee : DocumentState.MatchesBuildOutput; + newState = DocumentState.MatchesBuildOutput; } else { @@ -259,42 +236,20 @@ public async Task<(Document? Document, DocumentState State)> GetDocumentAndState } } - public void CommitSolution(Solution solution, ImmutableArray updatedDocuments) + public void CommitSolution(Solution solution) { lock (_guard) { - // Changes in the updated documents has just been applied to the debuggee process. - // Therefore, these documents now match exactly the state of the debuggee. - foreach (var document in updatedDocuments) - { - // Changes in design-time-only documents should have been ignored. - Debug.Assert(_documentState[document.Id] != DocumentState.DesignTimeOnly); - - _documentState[document.Id] = DocumentState.MatchesDebuggee; - Debug.Assert(document.Project.Solution == solution); - } - _solution = solution; } } - private async Task<(SourceText? Source, SourceHashOrigin ChecksumOrigin, bool IsDocumentMissing)> TryGetPdbMatchingSourceTextAsync( - string sourceFilePath, - Encoding? encoding, - ProjectId projectId, - bool matchLoadedModulesOnly, - CancellationToken cancellationToken) + private (SourceText? Source, bool? HasDocument) TryGetPdbMatchingSourceText(string sourceFilePath, Encoding? encoding, ProjectId projectId) { - var (symChecksum, algorithm, origin) = await TryReadSourceFileChecksumFromPdb(sourceFilePath, projectId, matchLoadedModulesOnly, cancellationToken).ConfigureAwait(false); - if (symChecksum.IsDefault) - { - return (Source: null, origin, IsDocumentMissing: true); - } - - if (!PathUtilities.IsAbsolute(sourceFilePath)) + bool? hasDocument = TryReadSourceFileChecksumFromPdb(sourceFilePath, projectId, out var symChecksum, out var algorithm); + if (hasDocument != true) { - EditAndContinueWorkspaceService.Log.Write("Error calculating checksum for source file '{0}': path not absolute", sourceFilePath); - return (Source: null, origin, IsDocumentMissing: false); + return (Source: null, hasDocument); } try @@ -306,131 +261,88 @@ public void CommitSolution(Solution solution, ImmutableArray updatedDo // might end up updating the committed solution with a document that has a different encoding than // the one that's in the workspace, resulting in false document changes when we compare the two. var sourceText = SourceText.From(fileStream, encoding, checksumAlgorithm: algorithm); + var fileChecksum = sourceText.GetChecksum(); - return (sourceText.GetChecksum().SequenceEqual(symChecksum) ? sourceText : null, origin, IsDocumentMissing: false); + if (fileChecksum.SequenceEqual(symChecksum)) + { + return (sourceText, hasDocument); + } + + EditAndContinueWorkspaceService.Log.Write("Checksum differs for source file '{0}'", sourceFilePath); + return (Source: null, hasDocument); } catch (Exception e) { EditAndContinueWorkspaceService.Log.Write("Error calculating checksum for source file '{0}': '{1}'", sourceFilePath, e.Message); - return (Source: null, origin, IsDocumentMissing: false); + return (Source: null, HasDocument: null); } } - private async Task<(ImmutableArray Checksum, SourceHashAlgorithm Algorithm, SourceHashOrigin Origin)> TryReadSourceFileChecksumFromPdb(string sourceFilePath, ProjectId projectId, bool matchLoadedModulesOnly, CancellationToken cancellationToken) + /// + /// Returns true if the PDB contains a document record for given , + /// in which case and contain its checksum. + /// False if the document is not found in the PDB. + /// Null if it can't be determined because the PDB is not available or an error occured while reading the PDB. + /// + private bool? TryReadSourceFileChecksumFromPdb(string sourceFilePath, ProjectId projectId, out ImmutableArray checksum, out SourceHashAlgorithm algorithm) { + checksum = default; + algorithm = default; + try { - var (mvid, mvidError) = await _debuggingSession.GetProjectModuleIdAsync(projectId, cancellationToken).ConfigureAwait(false); - if (mvid == Guid.Empty) + var compilationOutputs = _debuggingSession.CompilationOutputsProvider.GetCompilationOutputs(projectId); + + DebugInformationReaderProvider? debugInfoReaderProvider; + try { - EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: can't read MVID ('{1}')", sourceFilePath, mvidError); - return default; + debugInfoReaderProvider = compilationOutputs.OpenPdb(); } - - // Dispatch to a background thread - reading symbols from debuggee requires MTA thread. - var (checksum, algorithmId, origin) = (Thread.CurrentThread.GetApartmentState() != ApartmentState.MTA) ? - await Task.Factory.StartNew(ReadChecksum, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default).ConfigureAwait(false) : - ReadChecksum(); - - if (checksum.IsDefault) + catch (Exception e) { - return (default, default, origin); + EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: error opening PDB '{1}': {2}", sourceFilePath, compilationOutputs.PdbDisplayPath, e.Message); + debugInfoReaderProvider = null; } - var algorithm = SourceHashAlgorithms.GetSourceHashAlgorithm(algorithmId); - if (algorithm == SourceHashAlgorithm.None) + if (debugInfoReaderProvider == null) { - // unknown algorithm: - EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unknown checksum alg", sourceFilePath); - return (default, default, origin); + EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: PDB '{1}' not found", sourceFilePath, compilationOutputs.PdbDisplayPath); + return null; } - return (checksum, algorithm, origin); - - (ImmutableArray Checksum, Guid AlgorithmId, SourceHashOrigin Origin) ReadChecksum() + try { - try + var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader(); + if (!debugInfoReader.TryGetDocumentChecksum(sourceFilePath, out checksum, out var algorithmId)) { - // first try to check against loaded module - cancellationToken.ThrowIfCancellationRequested(); - - var moduleInfo = _debuggingSession.DebugeeModuleMetadataProvider.TryGetBaselineModuleInfo(mvid); - if (moduleInfo != null) - { - try - { - if (EditAndContinueMethodDebugInfoReader.TryGetDocumentChecksum(moduleInfo.SymReader, sourceFilePath, out var checksum, out var algorithmId)) - { - return (checksum, algorithmId, SourceHashOrigin.LoadedPdb); - } - - EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match loaded PDB: no SymDocument", sourceFilePath); - } - catch (Exception e) - { - EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match loaded PDB: error reading symbols: {1}", sourceFilePath, e.Message); - } - - return (default, default, SourceHashOrigin.LoadedPdb); - } - - if (matchLoadedModulesOnly) - { - return (default, default, SourceHashOrigin.None); - } - - // if the module is not loaded check against build output: - cancellationToken.ThrowIfCancellationRequested(); - - var compilationOutputs = _debuggingSession.CompilationOutputsProvider.GetCompilationOutputs(projectId); - - DebugInformationReaderProvider? debugInfoReaderProvider; - try - { - debugInfoReaderProvider = compilationOutputs.OpenPdb(); - } - catch (Exception e) - { - EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: error opening PDB: {1}", sourceFilePath, e.Message); - debugInfoReaderProvider = null; - } - - if (debugInfoReaderProvider == null) - { - EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: PDB not found", sourceFilePath); - return (default, default, SourceHashOrigin.None); - } - - try - { - var debugInfoReader = debugInfoReaderProvider.CreateEditAndContinueMethodDebugInfoReader(); - if (debugInfoReader.TryGetDocumentChecksum(sourceFilePath, out var checksum, out var algorithmId)) - { - return (checksum, algorithmId, SourceHashOrigin.BuiltPdb); - } - - EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: no SymDocument", sourceFilePath); - return (default, default, SourceHashOrigin.BuiltPdb); - } - catch (Exception e) - { - EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: error reading symbols: {1}", sourceFilePath, e.Message); - } - - return (default, default, SourceHashOrigin.BuiltPdb); + EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: no document", sourceFilePath); + return false; } - catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e)) + + algorithm = SourceHashAlgorithms.GetSourceHashAlgorithm(algorithmId); + if (algorithm == SourceHashAlgorithm.None) { - EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unexpected exception: {1}", sourceFilePath, e.Message); - return default; + // This can only happen if the PDB was post-processed by a misbehaving tool. + EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unknown checksum alg", sourceFilePath); } + + return true; + } + catch (Exception e) + { + EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match output PDB: error reading symbols: {1}", sourceFilePath, e.Message); + } + finally + { + debugInfoReaderProvider.Dispose(); } } catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e)) { EditAndContinueWorkspaceService.Log.Write("Source '{0}' doesn't match PDB: unexpected exception: {1}", sourceFilePath, e.Message); - return default; } + + return null; } } } diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs index 82ffbd50fbe..5655009f111 100644 --- a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs @@ -186,7 +186,7 @@ public void CommitSolutionUpdate(PendingSolutionUpdate update) } } - LastCommittedSolution.CommitSolution(update.Solution, update.ChangedDocuments); + LastCommittedSolution.CommitSolution(update.Solution); } /// diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs index b666725936d..6cabcc85988 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs @@ -154,6 +154,7 @@ void AddGeneralDiagnostic(EditAndContinueErrorCode code, string resourceName, Di AddGeneralDiagnostic(EditAndContinueErrorCode.ChangesDisallowedWhileStoppedAtException, nameof(FeaturesResources.ChangesDisallowedWhileStoppedAtException)); AddGeneralDiagnostic(EditAndContinueErrorCode.ChangesNotAppliedWhileRunning, nameof(FeaturesResources.ChangesNotAppliedWhileRunning), DiagnosticSeverity.Warning); AddGeneralDiagnostic(EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee, nameof(FeaturesResources.DocumentIsOutOfSyncWithDebuggee), DiagnosticSeverity.Warning); + AddGeneralDiagnostic(EditAndContinueErrorCode.UnableToReadSourceFileOrPdb, nameof(FeaturesResources.UnableToReadSourceFileOrPdb), DiagnosticSeverity.Warning); s_descriptors = builder.ToImmutable(); } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs index 0ffd4b8d79f..6343b9f520b 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs @@ -9,5 +9,6 @@ internal enum EditAndContinueErrorCode ChangesNotAppliedWhileRunning = 3, ChangesDisallowedWhileStoppedAtException = 4, DocumentIsOutOfSyncWithDebuggee = 5, + UnableToReadSourceFileOrPdb = 6, } } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueWorkspaceService.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueWorkspaceService.cs index b271ea4e103..49eac706824 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueWorkspaceService.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueWorkspaceService.cs @@ -205,6 +205,7 @@ public async Task> GetDocumentDiagnosticsAsync(Docume var (oldDocument, oldDocumentState) = await debuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(document.Id, cancellationToken).ConfigureAwait(false); if (oldDocumentState == CommittedSolution.DocumentState.OutOfSync || + oldDocumentState == CommittedSolution.DocumentState.Indeterminate || oldDocumentState == CommittedSolution.DocumentState.DesignTimeOnly) { // Do not report diagnostics for existing out-of-sync documents or design-time-only documents. @@ -396,7 +397,7 @@ private void ClearReportedRunModeDiagnostics() /// but does not provide a definitive answer. Only can definitively determine whether /// the update is valid or not. /// - public Task GetSolutionUpdateStatusAsync(string sourceFilePath, CancellationToken cancellationToken) + public Task HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken) { // GetStatusAsync is called outside of edit session when the debugger is determining // whether a source file checksum matches the one in PDB. @@ -404,10 +405,10 @@ public Task GetSolutionUpdateStatusAsync(string sourceFile var editSession = _editSession; if (editSession == null) { - return Task.FromResult(SolutionUpdateStatus.None); + return Task.FromResult(false); } - return editSession.GetSolutionUpdateStatusAsync(_workspace.CurrentSolution, sourceFilePath, cancellationToken); + return editSession.HasChangesAsync(_workspace.CurrentSolution, sourceFilePath, cancellationToken); } public async Task<(SolutionUpdateStatus Summary, ImmutableArray Deltas)> EmitSolutionUpdateAsync(CancellationToken cancellationToken) @@ -428,8 +429,7 @@ public async Task<(SolutionUpdateStatus Summary, ImmutableArray Deltas)> solution, solutionUpdate.EmitBaselines, solutionUpdate.Deltas, - solutionUpdate.ModuleReaders, - solutionUpdate.ChangedDocuments)); + solutionUpdate.ModuleReaders)); // commit/discard was not called: Contract.ThrowIfFalse(previousPendingUpdate == null); diff --git a/src/Features/Core/Portable/EditAndContinue/EditSession.cs b/src/Features/Core/Portable/EditAndContinue/EditSession.cs index 0bc4fb39977..e04e1f3c9e6 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditSession.cs @@ -307,11 +307,34 @@ internal async Task> GetBaseActi } } - private async Task<(ImmutableArray<(Document Document, AsyncLazy Results)>, ImmutableArray Diagnostics)> GetChangedDocumentsAnalysesAsync( - Project baseProject, Project project, CancellationToken cancellationToken) + private static async Task PopulateChangedAndAddedDocumentsAsync(CommittedSolution baseSolution, Project project, ArrayBuilder changedDocuments, ArrayBuilder addedDocuments, CancellationToken cancellationToken) { - var changedDocuments = ArrayBuilder<(Document? Old, Document New)>.GetInstance(); - var outOfSyncDiagnostics = ArrayBuilder.GetInstance(); + changedDocuments.Clear(); + addedDocuments.Clear(); + + if (!EditAndContinueWorkspaceService.SupportsEditAndContinue(project)) + { + return; + } + + var baseProject = baseSolution.GetProject(project.Id); + if (baseProject == project) + { + return; + } + + // When debugging session is started some projects might not have been loaded to the workspace yet. + // 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. + // + // TODO (https://github.com/dotnet/roslyn/issues/1204): + // hook up the debugger reported error, check that the project has not been loaded and report a better error. + // Here, we assume these projects are not modified. + if (baseProject == null) + { + EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not loaded", project.Id.DebugName, project.Id); + return; + } var changes = project.GetChanges(baseProject); foreach (var documentId in changes.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true)) @@ -338,48 +361,70 @@ internal async Task> GetBaseActi continue; } - var (oldDocument, oldDocumentState) = await DebuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(documentId, cancellationToken, reloadOutOfSyncDocument: true).ConfigureAwait(false); + changedDocuments.Add(document); + } + + foreach (var documentId in changes.GetAddedDocuments()) + { + var document = project.GetDocument(documentId)!; + if (EditAndContinueWorkspaceService.IsDesignTimeOnlyDocument(document)) + { + continue; + } + + addedDocuments.Add(document); + } + } + + private async Task<(ImmutableArray<(Document Document, AsyncLazy Results)>, ImmutableArray DocumentDiagnostics)> AnalyzeDocumentsAsync( + ArrayBuilder changedDocuments, ArrayBuilder addedDocuments, CancellationToken cancellationToken) + { + var documentDiagnostics = ArrayBuilder.GetInstance(); + var builder = ArrayBuilder<(Document? Old, Document New)>.GetInstance(); + + foreach (var document in changedDocuments) + { + var (oldDocument, oldDocumentState) = await DebuggingSession.LastCommittedSolution.GetDocumentAndStateAsync(document.Id, cancellationToken, reloadOutOfSyncDocument: true).ConfigureAwait(false); switch (oldDocumentState) { case CommittedSolution.DocumentState.DesignTimeOnly: continue; + case CommittedSolution.DocumentState.Indeterminate: case CommittedSolution.DocumentState.OutOfSync: - var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee); - outOfSyncDiagnostics.Add(Diagnostic.Create(descriptor, Location.Create(document.FilePath!, textSpan: default, lineSpan: default), new[] { document.FilePath })); + var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor((oldDocumentState == CommittedSolution.DocumentState.Indeterminate) ? + EditAndContinueErrorCode.UnableToReadSourceFileOrPdb : EditAndContinueErrorCode.DocumentIsOutOfSyncWithDebuggee); + documentDiagnostics.Add(Diagnostic.Create(descriptor, Location.Create(document.FilePath!, textSpan: default, lineSpan: default), new[] { document.FilePath })); continue; - default: + case CommittedSolution.DocumentState.MatchesBuildOutput: // Include the document regardless of whether the module it was built into has been loaded or not. // If the module has been built it might get loaded later during the debugging session, // at which point we apply all changes that have been made to the project so far. - changedDocuments.Add((oldDocument, document)); + builder.Add((oldDocument, document)); break; + + default: + throw ExceptionUtilities.UnexpectedValue(oldDocumentState); } } - foreach (var documentId in changes.GetAddedDocuments()) + foreach (var document in addedDocuments) { - var document = project.GetDocument(documentId)!; - if (EditAndContinueWorkspaceService.IsDesignTimeOnlyDocument(document)) - { - continue; - } - - changedDocuments.Add((null, document)); + builder.Add((null, document)); } var result = ImmutableArray<(Document, AsyncLazy)>.Empty; - if (changedDocuments.Count != 0) + if (builder.Count != 0) { lock (_analysesGuard) { - result = changedDocuments.SelectAsArray(change => (change.New, GetDocumentAnalysisNoLock(change.Old, change.New))); + result = builder.SelectAsArray(change => (change.New, GetDocumentAnalysisNoLock(change.Old, change.New))); } } - changedDocuments.Free(); - return (result, outOfSyncDiagnostics.ToImmutableAndFree()); + builder.Free(); + return (result, documentDiagnostics.ToImmutableAndFree()); } public AsyncLazy GetDocumentAnalysis(Document? baseDocument, Document document) @@ -449,108 +494,78 @@ internal void TrackDocumentWithReportedDiagnostics(DocumentId documentId) } /// - /// Determines the status of projects containing given or the entire solution if is null. + /// Determines whether projects contain any changes that might need to be applied. + /// Checks only projects containing a given or all projects of the solution if is null. /// Invoked by the debugger on every step. It is critical for stepping performance that this method returns as fast as possible in absence of changes. /// - public async Task GetSolutionUpdateStatusAsync(Solution solution, string sourceFilePath, CancellationToken cancellationToken) + public async Task HasChangesAsync(Solution solution, string? sourceFilePath, CancellationToken cancellationToken) { try { if (_changesApplied) { - return SolutionUpdateStatus.None; + return false; } - if (DebuggingSession.LastCommittedSolution.HasNoChanges(solution)) + var baseSolution = DebuggingSession.LastCommittedSolution; + if (baseSolution.HasNoChanges(solution)) { - return SolutionUpdateStatus.None; + return false; } var projects = (sourceFilePath == null) ? solution.Projects : from documentId in solution.GetDocumentIdsWithFilePath(sourceFilePath) select solution.GetDocument(documentId)!.Project; - bool anyChanges = false; + using var changedDocumentsDisposer = ArrayBuilder.GetInstance(out var changedDocuments); + using var addedDocumentsDisposer = ArrayBuilder.GetInstance(out var addedDocuments); + foreach (var project in projects) { - if (!EditAndContinueWorkspaceService.SupportsEditAndContinue(project)) + await PopulateChangedAndAddedDocumentsAsync(baseSolution, project, changedDocuments, addedDocuments, cancellationToken).ConfigureAwait(false); + if (changedDocuments.IsEmpty() && addedDocuments.IsEmpty()) { continue; } - var baseProject = DebuggingSession.LastCommittedSolution.GetProject(project.Id); - if (baseProject == project) + // Check MVID before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID. + var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(project.Id, cancellationToken).ConfigureAwait(false); + if (mvidReadError != null) { - continue; + // Can't read MVID. This might be an intermittent failure, so don't report it here. + // Report the project as containing changes, so that we proceed to EmitSolutionUpdateAsync where we report the error if it still persists. + EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not built", project.Id.DebugName, project.Id); + return true; } - // When debugging session is started some projects might not have been loaded to the workspace yet. - // 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. - // - // TODO (https://github.com/dotnet/roslyn/issues/1204): - // hook up the debugger reported error, check that the project has not been loaded and report a better error. - // Here, we assume these projects are not modified. - if (baseProject == null) + if (mvid == Guid.Empty) { - EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not loaded", project.Id.DebugName, project.Id); + // Project not built. We ignore any changes made in its sources. + EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not built", project.Id.DebugName, project.Id); continue; } - var (changedDocumentAnalyses, diagnostics) = await GetChangedDocumentsAnalysesAsync(baseProject, project, cancellationToken).ConfigureAwait(false); - if (diagnostics.Any()) + var (changedDocumentAnalyses, documentDiagnostics) = await AnalyzeDocumentsAsync(changedDocuments, addedDocuments, cancellationToken).ConfigureAwait(false); + if (documentDiagnostics.Any()) { EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: out-of-sync documents present (diagnostic: '{2}')", - project.Id.DebugName, project.Id, diagnostics[0]); - - return SolutionUpdateStatus.Blocked; - } + project.Id.DebugName, project.Id, documentDiagnostics[0]); - if (changedDocumentAnalyses.Length == 0) - { - continue; + // Although we do not apply changes in out-of-sync/indeterminate documents we report that changes are present, + // so that the debugger triggers emit of updates. There we check if these documents are still in a bad state and report warnings + // that any changes in such documents are not applied. + return true; } var projectSummary = await GetProjectAnalysisSymmaryAsync(changedDocumentAnalyses, cancellationToken).ConfigureAwait(false); - if (projectSummary == ProjectAnalysisSummary.ValidChanges) + if (projectSummary != ProjectAnalysisSummary.NoChanges) { - var (mvid, _) = await DebuggingSession.GetProjectModuleIdAsync(baseProject.Id, cancellationToken).ConfigureAwait(false); - if (mvid == Guid.Empty) - { - // project not built - EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: project not built", project.Id.DebugName, project.Id); - continue; - } - - if (!GetModuleDiagnostics(mvid, project.Name).IsEmpty) - { - EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: module blocking EnC", project.Id.DebugName, project.Id); - return SolutionUpdateStatus.Blocked; - } - } - - EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: {2}", project.Id.DebugName, project.Id, projectSummary); - - switch (projectSummary) - { - case ProjectAnalysisSummary.NoChanges: - continue; - - case ProjectAnalysisSummary.CompilationErrors: - case ProjectAnalysisSummary.RudeEdits: - return SolutionUpdateStatus.Blocked; - - case ProjectAnalysisSummary.ValidChanges: - case ProjectAnalysisSummary.ValidInsignificantChanges: - anyChanges = true; - continue; - - default: - throw ExceptionUtilities.UnexpectedValue(projectSummary); + EditAndContinueWorkspaceService.Log.Write("EnC state of '{0}' [0x{1:X8}] queried: {2}", project.Id.DebugName, project.Id, projectSummary); + return true; } } - return anyChanges ? SolutionUpdateStatus.Ready : SolutionUpdateStatus.None; + return false; } catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceledAndPropagate(e)) { @@ -672,39 +687,40 @@ internal ImmutableArray GetDebugeeStateDiagnostics() public async Task EmitSolutionUpdateAsync(Solution solution, CancellationToken cancellationToken) { - var deltas = ArrayBuilder.GetInstance(); - var emitBaselines = ArrayBuilder<(ProjectId, EmitBaseline)>.GetInstance(); - var readers = ArrayBuilder.GetInstance(); - var diagnostics = ArrayBuilder<(ProjectId, ImmutableArray)>.GetInstance(); - var changedDocuments = ArrayBuilder.GetInstance(); - try { - bool isBlocked = false; + using var deltasDisposer = ArrayBuilder.GetInstance(out var deltas); + using var emitBaselinesDisposer = ArrayBuilder<(ProjectId, EmitBaseline)>.GetInstance(out var emitBaselines); + using var readersDisposer = ArrayBuilder.GetInstance(out var readers); + using var diagnosticsDisposer = ArrayBuilder<(ProjectId, ImmutableArray)>.GetInstance(out var diagnostics); + using var changedDocumentsDisposer = ArrayBuilder.GetInstance(out var changedDocuments); + using var addedDocumentsDisposer = ArrayBuilder.GetInstance(out var addedDocuments); + var baseSolution = DebuggingSession.LastCommittedSolution; + + bool isBlocked = false; foreach (var project in solution.Projects) { - if (!EditAndContinueWorkspaceService.SupportsEditAndContinue(project)) + await PopulateChangedAndAddedDocumentsAsync(baseSolution, project, changedDocuments, addedDocuments, cancellationToken).ConfigureAwait(false); + if (changedDocuments.IsEmpty() && addedDocuments.IsEmpty()) { continue; } - var baseProject = DebuggingSession.LastCommittedSolution.GetProject(project.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. - // 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. - // TODO: hook up the debugger reported error, check that the project has not been loaded and report a better error. - // Here, we assume these projects are not modified. - if (baseProject == null) + var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(project.Id, cancellationToken).ConfigureAwait(false); + if (mvidReadError != null) { - EditAndContinueWorkspaceService.Log.Write("Emitting update of '{0}' [0x{1:X8}]: project not loaded", project.Id.DebugName, project.Id); + // The error hasn't been reported by GetDocumentDiagnosticsAsync since it might have been intermittent. + // The MVID is required for emit so we consider the error permanent and report it here. + // Bail before analyzing documents as the analysis needs to read the PDB which will likely fail if we can't even read the MVID. + diagnostics.Add((project.Id, ImmutableArray.Create(mvidReadError))); + + Telemetry.LogProjectAnalysisSummary(ProjectAnalysisSummary.ValidChanges, ImmutableArray.Create(mvidReadError.Descriptor.Id)); + isBlocked = true; continue; } - var (mvid, mvidReadError) = await DebuggingSession.GetProjectModuleIdAsync(project.Id, cancellationToken).ConfigureAwait(false); - if (mvid == Guid.Empty && mvidReadError == null) + if (mvid == Guid.Empty) { EditAndContinueWorkspaceService.Log.Write("Emitting update of '{0}' [0x{1:X8}]: project not built", project.Id.DebugName, project.Id); continue; @@ -724,16 +740,14 @@ public async Task EmitSolutionUpdateAsync(Solution solution, Can // e.g. the binary was built with an overload C.M(object), but a generator updated class C to also contain C.M(string), // which change we have not observed yet. Then call-sites of C.M in a changed document observed by the analysis will be seen as C.M(object) // instead of the true C.M(string). - var (changedDocumentAnalyses, outOfSyncDiagnostics) = await GetChangedDocumentsAnalysesAsync(baseProject, project, cancellationToken).ConfigureAwait(false); - if (outOfSyncDiagnostics.Any()) + var (changedDocumentAnalyses, documentDiagnostics) = await AnalyzeDocumentsAsync(changedDocuments, addedDocuments, cancellationToken).ConfigureAwait(false); + if (documentDiagnostics.Any()) { - // The error hasn't been reported by GetDocumentDiagnosticsAsync since out-of-sync documents are likely to be synchronized - // before the changes are attempted to be applied. If they are not the project changes can't be applied. - diagnostics.Add((project.Id, outOfSyncDiagnostics)); - - Telemetry.LogProjectAnalysisSummary(ProjectAnalysisSummary.RudeEdits, outOfSyncDiagnostics); - isBlocked = true; - continue; + // The diagnostic hasn't been reported by GetDocumentDiagnosticsAsync since out-of-sync documents are likely to be synchronized + // before the changes are attempted to be applied. If we still have any out-of-sync documents we report warnings and ignore changes in them. + // If in future the file is updated so that its content matches the PDB checksum, the document transitions to a matching state, + // and we consider any further changes to it for application. + diagnostics.Add((project.Id, documentDiagnostics)); } var projectSummary = await GetProjectAnalysisSymmaryAsync(changedDocumentAnalyses, cancellationToken).ConfigureAwait(false); @@ -750,17 +764,6 @@ public async Task EmitSolutionUpdateAsync(Solution solution, Can continue; } - if (mvidReadError != null) - { - // The error hasn't been reported by GetDocumentDiagnosticsAsync since it might have been intermittent. - // The MVID is required for emit so we consider the error permanent and report it here. - diagnostics.Add((project.Id, ImmutableArray.Create(mvidReadError))); - - Telemetry.LogProjectAnalysisSummary(projectSummary, ImmutableArray.Create(mvidReadError.Descriptor.Id)); - isBlocked = true; - continue; - } - var moduleDiagnostics = GetModuleDiagnostics(mvid, project.Name); if (!moduleDiagnostics.IsEmpty) { @@ -803,11 +806,6 @@ public async Task EmitSolutionUpdateAsync(Solution solution, Can Emit(); } - if (!isBlocked) - { - changedDocuments.AddRange(changedDocumentAnalyses.Select(a => a.Document)); - } - void Emit() { Debug.Assert(Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA, "SymReader requires MTA"); @@ -903,27 +901,20 @@ void Emit() if (isBlocked) { - deltas.Free(); - emitBaselines.Free(); - foreach (var reader in readers) { reader.Dispose(); } - readers.Free(); - changedDocuments.Free(); - - return SolutionUpdate.Blocked(diagnostics.ToImmutableAndFree()); + return SolutionUpdate.Blocked(diagnostics.ToImmutable()); } return new SolutionUpdate( (deltas.Count > 0) ? SolutionUpdateStatus.Ready : SolutionUpdateStatus.None, - deltas.ToImmutableAndFree(), - readers.ToImmutableAndFree(), - emitBaselines.ToImmutableAndFree(), - changedDocuments.ToImmutableAndFree(), - diagnostics.ToImmutableAndFree()); + deltas.ToImmutable(), + readers.ToImmutable(), + emitBaselines.ToImmutable(), + diagnostics.ToImmutable()); } catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceledAndPropagate(e)) { diff --git a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueWorkspaceService.cs b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueWorkspaceService.cs index 223135931c3..ed80ef288b2 100644 --- a/src/Features/Core/Portable/EditAndContinue/IEditAndContinueWorkspaceService.cs +++ b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueWorkspaceService.cs @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.EditAndContinue internal interface IEditAndContinueWorkspaceService : IWorkspaceService { Task> GetDocumentDiagnosticsAsync(Document document, CancellationToken cancellationToken); - Task GetSolutionUpdateStatusAsync(string sourceFilePath, CancellationToken cancellationToken); + Task HasChangesAsync(string sourceFilePath, CancellationToken cancellationToken); Task<(SolutionUpdateStatus Summary, ImmutableArray Deltas)> EmitSolutionUpdateAsync(CancellationToken cancellationToken); void CommitSolutionUpdate(); diff --git a/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs b/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs index c891b8409a3..153d2ba4c39 100644 --- a/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs +++ b/src/Features/Core/Portable/EditAndContinue/PendingSolutionUpdate.cs @@ -11,20 +11,17 @@ internal sealed class PendingSolutionUpdate public readonly ImmutableArray<(ProjectId ProjectId, EmitBaseline Baseline)> EmitBaselines; public readonly ImmutableArray Deltas; public readonly ImmutableArray ModuleReaders; - public readonly ImmutableArray ChangedDocuments; public PendingSolutionUpdate( Solution solution, ImmutableArray<(ProjectId ProjectId, EmitBaseline Baseline)> emitBaselines, ImmutableArray deltas, - ImmutableArray moduleReaders, - ImmutableArray changedDocuments) + ImmutableArray moduleReaders) { Solution = solution; EmitBaselines = emitBaselines; Deltas = deltas; ModuleReaders = moduleReaders; - ChangedDocuments = changedDocuments; } } } diff --git a/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs b/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs index 9f040b051ff..033ced9df8b 100644 --- a/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs +++ b/src/Features/Core/Portable/EditAndContinue/SolutionUpdate.cs @@ -12,21 +12,18 @@ namespace Microsoft.CodeAnalysis.EditAndContinue public readonly ImmutableArray ModuleReaders; public readonly ImmutableArray<(ProjectId ProjectId, EmitBaseline Baseline)> EmitBaselines; public readonly ImmutableArray<(ProjectId ProjectId, ImmutableArray Diagnostic)> Diagnostics; - public readonly ImmutableArray ChangedDocuments; public SolutionUpdate( SolutionUpdateStatus summary, ImmutableArray deltas, ImmutableArray moduleReaders, ImmutableArray<(ProjectId, EmitBaseline)> emitBaselines, - ImmutableArray changedDocuments, ImmutableArray<(ProjectId ProjectId, ImmutableArray Diagnostics)> diagnostics) { Summary = summary; Deltas = deltas; EmitBaselines = emitBaselines; ModuleReaders = moduleReaders; - ChangedDocuments = changedDocuments; Diagnostics = diagnostics; } @@ -38,7 +35,6 @@ public static SolutionUpdate Blocked() ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray<(ProjectId, EmitBaseline)>.Empty, - ImmutableArray.Empty, diagnostics); } } diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index 3bd058446dc..70f04e8177a 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -1347,7 +1347,7 @@ internal class FeaturesResources { } /// - /// Looks up a localized string similar to The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored.. + /// Looks up a localized string similar to The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source.. /// internal static string DocumentIsOutOfSyncWithDebuggee { get { @@ -4056,6 +4056,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source.. + /// + internal static string UnableToReadSourceFileOrPdb { + get { + return ResourceManager.GetString("UnableToReadSourceFileOrPdb", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unexpected interface member kind: {0}. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index c50a53796cd..31e0e6e77b2 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1681,7 +1681,10 @@ This version used in: {2} Changes made in project '{0}' will not be applied while the application is running - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. Changes are not allowed while stopped at exception diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf index d5470a0e769..b7f77bd6aa8 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - Aktuální obsah zdrojového souboru {0} se neshoduje se sestaveným zdrojem. Relace ladění nemůže pokračovat, dokud se obsah zdrojového souboru neobnoví. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + Aktuální obsah zdrojového souboru {0} se neshoduje se sestaveným zdrojem. Relace ladění nemůže pokračovat, dokud se obsah zdrojového souboru neobnoví. @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value Nepotřebné přiřazení hodnoty diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf index 71e521b88d0..eba8575b294 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - Der aktuelle Inhalt der Quelldatei "{0}" stimmt nicht mit der erstellten Quelle überein. Die Debugsitzung kann erst fortgesetzt werden, wenn der Inhalt der Quelldatei wiederhergestellt wurde. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + Der aktuelle Inhalt der Quelldatei "{0}" stimmt nicht mit der erstellten Quelle überein. Die Debugsitzung kann erst fortgesetzt werden, wenn der Inhalt der Quelldatei wiederhergestellt wurde. @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value Unnötige Zuweisung eines Werts. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf index 6320eb83df0..1ba4e130a34 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - El contenido actual del archivo de código fuente "{0}" no coincide con el del origen compilado. La sesión de depuración no puede continuar hasta que se restaure el contenido del archivo de código fuente. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + El contenido actual del archivo de código fuente "{0}" no coincide con el del origen compilado. La sesión de depuración no puede continuar hasta que se restaure el contenido del archivo de código fuente. @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value Asignación innecesaria de un valor diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf index aaa8cf92d0f..df3ff8d61ed 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - Le contenu actuel du fichier source « {0} » ne correspond pas à la source générée. La session de débogage ne peut pas continuer tant que le contenu du fichier source n'est pas restauré. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + Le contenu actuel du fichier source « {0} » ne correspond pas à la source générée. La session de débogage ne peut pas continuer tant que le contenu du fichier source n'est pas restauré. @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value Assignation inutile d'une valeur diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf index f7674a94246..a47c5dc7902 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - Il contenuto corrente del file di origine '{0}' non corrisponde all'origine compilata. La sessione di debug non può continuare finché non viene ripristinato il contenuto del file di origine. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + Il contenuto corrente del file di origine '{0}' non corrisponde all'origine compilata. La sessione di debug non può continuare finché non viene ripristinato il contenuto del file di origine. @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value Assegnazione non necessaria di un valore diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf index 4da17896cb6..581fb3c3681 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - ソース ファイル '{0}' の現在の内容はビルドされたソースと一致しません。ソース ファイルの内容が復元されるまで、デバッグ セッションを続行できません。 + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + ソース ファイル '{0}' の現在の内容はビルドされたソースと一致しません。ソース ファイルの内容が復元されるまで、デバッグ セッションを続行できません。 @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value 値の不必要な代入 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf index b1260dee49e..9345a4593ce 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - 소스 파일 '{0}'의 현재 콘텐츠가 빌드된 소스와 일치하지 않습니다. 소스 파일의 콘텐츠가 복원될 때까지 디버그 세션을 계속할 수 없습니다. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + 소스 파일 '{0}'의 현재 콘텐츠가 빌드된 소스와 일치하지 않습니다. 소스 파일의 콘텐츠가 복원될 때까지 디버그 세션을 계속할 수 없습니다. @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value 불필요한 값 할당 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf index 6dd49d22341..a873d008219 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - Bieżąca zawartość pliku źródłowego „{0}” nie pasuje do skompilowanego źródła. Nie można kontynuować sesji debugowania do czasu przywrócenia zawartości pliku źródłowego. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + Bieżąca zawartość pliku źródłowego „{0}” nie pasuje do skompilowanego źródła. Nie można kontynuować sesji debugowania do czasu przywrócenia zawartości pliku źródłowego. @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value Niepotrzebne przypisanie wartości diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf index 932d9b8df4a..93ed0afd533 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - O conteúdo atual do arquivo de origem '{0}' não corresponde à fonte compilada. A sessão de depuração não pode continuar até que o conteúdo do arquivo de origem seja restaurado. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + O conteúdo atual do arquivo de origem '{0}' não corresponde à fonte compilada. A sessão de depuração não pode continuar até que o conteúdo do arquivo de origem seja restaurado. @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value Atribuição desnecessária de um valor diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf index c7b6e20b84c..fa392e8b463 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - Текущее содержимое исходного файла "{0}" не соответствует созданному источнику. Сеанс отладки не может быть продолжен, пока не будет восстановлено содержимое исходного файла. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + Текущее содержимое исходного файла "{0}" не соответствует созданному источнику. Сеанс отладки не может быть продолжен, пока не будет восстановлено содержимое исходного файла. @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value Ненужное присваивание значения diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf index 2abfbf47854..512e5a2fd9f 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - '{0}' kaynak dosyasının geçerli içeriği, oluşturulan kaynakla eşleşmiyor. Hata ayıklama oturumu, kaynak dosyanın içeriği geri yüklenene kadar devam edemez. + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + '{0}' kaynak dosyasının geçerli içeriği, oluşturulan kaynakla eşleşmiyor. Hata ayıklama oturumu, kaynak dosyanın içeriği geri yüklenene kadar devam edemez. @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value Bir değerin gereksiz ataması diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf index 6c267f67180..eedcedc023d 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - 源文件 "{0}" 的当前内容与生成的源不匹配。除非还原源文件的内容,否则调试会话将无法继续。 + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + 源文件 "{0}" 的当前内容与生成的源不匹配。除非还原源文件的内容,否则调试会话将无法继续。 @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value 不需要赋值 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf index 64f93b6eb47..11b430b53c3 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf @@ -198,8 +198,8 @@ - The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored. - 來源檔案 '{0}' 的目前內容與建立的來源不符。在還原來源檔案的內容之前,無法繼續進行偵錯工作階段。 + The current content of source file '{0}' does not match the built source. Any changes made to this file while debugging won't be applied until its content matches the built source. + 來源檔案 '{0}' 的目前內容與建立的來源不符。在還原來源檔案的內容之前,無法繼續進行偵錯工作階段。 @@ -572,6 +572,11 @@ The selection contains a local function call without its declaration. + + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + Unable to read source file '{0}' or the PDB built for the containing project. Any changes made to this file while debugging won't be applied until its content matches the built source. + + Unnecessary assignment of a value 指派了不必要的值 diff --git a/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj b/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj index 834fac6415f..6fd55c8dfce 100644 --- a/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj +++ b/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj @@ -50,6 +50,8 @@ + + diff --git a/src/VisualStudio/Core/Def/Implementation/EditAndContinue/VisualStudioManagedModuleUpdateProvider.cs b/src/VisualStudio/Core/Def/Implementation/EditAndContinue/VisualStudioManagedModuleUpdateProvider.cs index 16c243e1709..e76c1d8aa3a 100644 --- a/src/VisualStudio/Core/Def/Implementation/EditAndContinue/VisualStudioManagedModuleUpdateProvider.cs +++ b/src/VisualStudio/Core/Def/Implementation/EditAndContinue/VisualStudioManagedModuleUpdateProvider.cs @@ -33,12 +33,20 @@ public Task GetStatusAsync(CancellationToken cancella /// Returns the state of the changes made to the source. /// The EnC manager calls this to determine whether there are any changes to the source /// and if so whether there are any rude edits. + /// + /// TODO: Future work in the debugger https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1051385 will replace this with bool HasChangesAsync. + /// The debugger currently uses as a signal to trigger emit of updates + /// (i.e. to call ). + /// When is returned updates are not emitted. + /// Since already handles all validation and error reporting + /// we either return if there are no changes or if there are any changes. /// public async Task GetStatusAsync(string sourceFilePath, CancellationToken cancellationToken) { try { - return (await _encService.GetSolutionUpdateStatusAsync(sourceFilePath, cancellationToken).ConfigureAwait(false)).ToModuleUpdateStatus(); + return (await _encService.HasChangesAsync(sourceFilePath, cancellationToken).ConfigureAwait(false)) ? + ManagedModuleUpdateStatus.Ready : ManagedModuleUpdateStatus.None; } catch (Exception e) when (FatalError.ReportWithoutCrashUnlessCanceled(e)) { -- GitLab