diff --git a/eng/Packages.props b/eng/Packages.props index 79e87dbd81d6f..358d86df78b58 100644 --- a/eng/Packages.props +++ b/eng/Packages.props @@ -131,7 +131,7 @@ - + diff --git a/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs b/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs index 42cc6155d1722..aaa78055241fb 100644 --- a/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs +++ b/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs @@ -33,21 +33,30 @@ public static InternalContracts.ManagedHotReloadAvailability ToContract(this Man => new((InternalContracts.ManagedHotReloadAvailabilityStatus)value.Status, value.LocalizedMessage); public static ManagedHotReloadUpdate FromContract(this InternalContracts.ManagedHotReloadUpdate update) - => new( - module: update.Module, - moduleName: update.ModuleName, - ilDelta: update.ILDelta, - metadataDelta: update.MetadataDelta, - pdbDelta: update.PdbDelta, - updatedTypes: update.UpdatedTypes, - requiredCapabilities: update.RequiredCapabilities, - updatedMethods: update.UpdatedMethods, - sequencePoints: update.SequencePoints.SelectAsArray(FromContract), - activeStatements: update.ActiveStatements.SelectAsArray(FromContract), - exceptionRegions: update.ExceptionRegions.SelectAsArray(FromContract)); + => new() + { + Module = update.Module, + ModuleName = update.ModuleName, + ILDelta = update.ILDelta, + MetadataDelta = update.MetadataDelta, + PdbDelta = update.PdbDelta, + UpdatedTypes = update.UpdatedTypes, + RequiredCapabilities = update.RequiredCapabilities, + UpdatedMethods = update.UpdatedMethods, + SequencePoints = update.SequencePoints.SelectAsArray(FromContract), + ActiveStatements = update.ActiveStatements.SelectAsArray(FromContract), + ExceptionRegions = update.ExceptionRegions.SelectAsArray(FromContract) + }; public static ManagedHotReloadUpdates FromContract(this InternalContracts.ManagedHotReloadUpdates updates) - => new(updates.Updates.FromContract(), updates.Diagnostics.FromContract(), updates.ProjectsToRebuild.SelectAsArray(FromContract), updates.ProjectsToRestart.SelectAsArray(FromContract)); + => new() + { + Updates = updates.Updates.FromContract(), + Diagnostics = updates.Diagnostics.FromContract(), + ProjectInstancesToRebuild = updates.ProjectsToRebuild.SelectAsArray(FromContract), + ProjectInstancesToRestart = updates.ProjectsToRestart.SelectAsArray(FromContract), + HasPendingUpdates = updates.HasPendingUpdates, + }; public static ImmutableArray FromContract(this ImmutableArray diagnostics) => diagnostics.SelectAsArray(FromContract); diff --git a/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs index 56714364e9a7f..40a2cd6b6ce39 100644 --- a/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs +++ b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.VisualStudio.Debugger.Contracts.HotReload; -using InternalContracts = Microsoft.CodeAnalysis.Contracts.EditAndContinue; namespace Microsoft.CodeAnalysis.EditAndContinue; @@ -61,4 +60,8 @@ public ValueTask DiscardUpdatesAsync(CancellationToken cancellationToken) public ValueTask HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken) => impl.HasChangesAsync(sourceFilePath, cancellationToken); + + // internal for testing: + internal ManagedHotReloadLanguageServiceImpl Impl + => impl; } diff --git a/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs index 91ee6d2baf950..5154c15303def 100644 --- a/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs +++ b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs @@ -45,7 +45,20 @@ public NoSessionException() private DebuggingSessionProxy? _debuggingSession; private Solution? _pendingUpdatedSolution; - private Solution? _committedSolution; + + private Solution? CommittedSolution + { + get; + set + { + field = value; + + if (value != null) + { + SolutionCommitted?.Invoke(value); + } + } + } public event Action? SolutionCommitted; @@ -84,7 +97,7 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken) sourceTextProvider.Activate(); var currentSolution = await solutionSnapshotProvider.GetCurrentSolutionAsync(cancellationToken).ConfigureAwait(false); - _committedSolution = currentSolution; + CommittedSolution = currentSolution; sourceTextProvider.SetBaseline(currentSolution); @@ -162,15 +175,7 @@ public async ValueTask CommitUpdatesAsync(CancellationToken cancellationToken) var committedSolution = Interlocked.Exchange(ref _pendingUpdatedSolution, null); Contract.ThrowIfNull(committedSolution); - try - { - SolutionCommitted?.Invoke(committedSolution); - } - catch (Exception e) when (FatalError.ReportAndCatch(e)) - { - } - - _committedSolution = committedSolution; + CommittedSolution = committedSolution; try { @@ -226,7 +231,7 @@ public async ValueTask EndSessionAsync(CancellationToken cancellationToken) sourceTextProvider.Deactivate(); _debuggingSession = null; - _committedSolution = null; + CommittedSolution = null; _pendingUpdatedSolution = null; } @@ -252,8 +257,8 @@ public async ValueTask HasChangesAsync(string? sourceFilePath, Cancellatio return false; } - Contract.ThrowIfNull(_committedSolution); - var oldSolution = _committedSolution; + Contract.ThrowIfNull(CommittedSolution); + var oldSolution = CommittedSolution; var newSolution = await solutionSnapshotProvider.GetCurrentSolutionAsync(cancellationToken).ConfigureAwait(false); return (sourceFilePath != null) @@ -285,7 +290,14 @@ public async ValueTask GetUpdatesAsync(ImmutableArray GetUpdatesAsync(ImmutableArray ToProjectIntanceIds(IEnumerable ids) => ids.SelectAsArray(id => @@ -342,4 +357,13 @@ ImmutableArray ToProjectIntanceIds(IEnumerable ids return new ProjectInstanceId(project.FilePath!, project.State.NameAndFlavor.flavor ?? ""); }); } + + internal TestAccessor GetTestAccessor() + => new(this); + + internal readonly struct TestAccessor(ManagedHotReloadLanguageServiceImpl instance) + { + public Solution? PendingUpdatedSolution => instance._pendingUpdatedSolution; + public Solution? CommittedSolution => instance.CommittedSolution; + } } diff --git a/src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs b/src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs index 9fd927bcdb044..57d647f0465e0 100644 --- a/src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs +++ b/src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs @@ -111,7 +111,7 @@ private static string Inspect(DiagnosticData d) (!string.IsNullOrWhiteSpace(d.DataLocation.UnmappedFileSpan.Path) ? $" {d.DataLocation.UnmappedFileSpan.Path}({d.DataLocation.UnmappedFileSpan.StartLinePosition.Line}, {d.DataLocation.UnmappedFileSpan.StartLinePosition.Character}, {d.DataLocation.UnmappedFileSpan.EndLinePosition.Line}, {d.DataLocation.UnmappedFileSpan.EndLinePosition.Character}):" : "") + $" {d.Message}"; - private static string Inspect(Microsoft.VisualStudio.Debugger.Contracts.HotReload.ManagedHotReloadDiagnostic d) + private static string Inspect(DebuggerContracts.ManagedHotReloadDiagnostic d) => $"{d.Severity} {d.Id}:" + (!string.IsNullOrWhiteSpace(d.FilePath) ? $" {d.FilePath}({d.Span.StartLine}, {d.Span.StartColumn}, {d.Span.EndLine}, {d.Span.EndColumn}):" : "") + $" {d.Message}"; @@ -129,7 +129,7 @@ private TestWorkspace CreateEditorWorkspace(out Solution solution, out EditAndCo ((MockServiceBroker)workspace.Services.GetRequiredService().ServiceBroker).CreateService = t => t switch { - _ when t == typeof(Microsoft.VisualStudio.Debugger.Contracts.HotReload.IHotReloadLogger) => new MockHotReloadLogger(), + _ when t == typeof(DebuggerContracts.IHotReloadLogger) => new MockHotReloadLogger(), _ => throw ExceptionUtilities.UnexpectedValue(t) }; @@ -147,39 +147,60 @@ private TestWorkspace CreateEditorWorkspace(out Solution solution, out EditAndCo return workspace; } - [Theory, CombinatorialData] - public async Task Test(bool commitChanges) + private class TestContext : IDisposable { - var localComposition = EditorTestCompositions.LanguageServerProtocolEditorFeatures - .AddExcludedPartTypes( - typeof(EditAndContinueService.WorkspaceServiceFactory)) - .AddParts( - typeof(NoCompilationLanguageService), - typeof(MockHostWorkspaceProvider), - typeof(MockServiceBrokerProvider), - typeof(MockEditAndContinueServiceFactory), - typeof(MockManagedHotReloadService)); + public readonly TestWorkspace LocalWorkspace; + public readonly PdbMatchingSourceTextProvider PdbMatchingSourceTextProvider; + public readonly MockEditAndContinueService MockEncService; + public readonly ManagedHotReloadLanguageService LocalService; - using var localWorkspace = new TestWorkspace(composition: localComposition); + public TestContext() + { + var localComposition = EditorTestCompositions.LanguageServerProtocolEditorFeatures + .AddExcludedPartTypes( + typeof(EditAndContinueService.WorkspaceServiceFactory)) + .AddParts( + typeof(NoCompilationLanguageService), + typeof(MockHostWorkspaceProvider), + typeof(MockServiceBrokerProvider), + typeof(MockEditAndContinueServiceFactory), + typeof(MockManagedHotReloadService)); + + LocalWorkspace = new TestWorkspace(composition: localComposition); + + var globalOptions = LocalWorkspace.GetService(); + ((MockHostWorkspaceProvider)LocalWorkspace.GetService()).Workspace = LocalWorkspace; + + ((MockServiceBroker)LocalWorkspace.Services.GetRequiredService().ServiceBroker).CreateService = t => t switch + { + _ when t == typeof(DebuggerContracts.IHotReloadLogger) => new MockHotReloadLogger(), + _ => throw ExceptionUtilities.UnexpectedValue(t) + }; - var globalOptions = localWorkspace.GetService(); - ((MockHostWorkspaceProvider)localWorkspace.GetService()).Workspace = localWorkspace; + MockEncService = (MockEditAndContinueService)LocalWorkspace.Services.GetRequiredService().Service; - ((MockServiceBroker)localWorkspace.Services.GetRequiredService().ServiceBroker).CreateService = t => t switch - { - _ when t == typeof(DebuggerContracts.IHotReloadLogger) => new MockHotReloadLogger(), - _ => throw ExceptionUtilities.UnexpectedValue(t) - }; + var localFactory = LocalWorkspace.GetService(); + var localBroker = LocalWorkspace.Services.GetRequiredService().ServiceBroker; + var localSnapshotProvider = LocalWorkspace.GetService(); + PdbMatchingSourceTextProvider = new PdbMatchingSourceTextProvider(LocalWorkspace); + LocalService = localFactory.Create(localBroker, localSnapshotProvider, LocalWorkspace.GetService(), PdbMatchingSourceTextProvider); + } - MockEditAndContinueService mockEncService; + public void Dispose() + { + LocalWorkspace.Dispose(); + PdbMatchingSourceTextProvider.Dispose(); + } + } - mockEncService = (MockEditAndContinueService)localWorkspace.Services.GetRequiredService().Service; + [Theory, CombinatorialData] + public async Task Test(bool commitChanges) + { + using var context = new TestContext(); - var localFactory = localWorkspace.GetService(); - var localBroker = localWorkspace.Services.GetRequiredService().ServiceBroker; - var localSnapshotProvider = localWorkspace.GetService(); - using var pdbMatchingSourceTextProvider = new PdbMatchingSourceTextProvider(localWorkspace); - var localService = localFactory.Create(localBroker, localSnapshotProvider, localWorkspace.GetService(), pdbMatchingSourceTextProvider); + var localWorkspace = context.LocalWorkspace; + var mockEncService = context.MockEncService; + var localService = context.LocalService; await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution .AddTestProject("proj", out var projectId) @@ -250,6 +271,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution return new() { + SolutionAction = SolutionAction.PendingUpdate, Solution = solution, ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Ready, []), Diagnostics = @@ -279,8 +301,8 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution }; }; - var runningProjectInfo = new Microsoft.VisualStudio.Debugger.Contracts.HotReload.RunningProjectInfo( - new Microsoft.VisualStudio.Debugger.Contracts.HotReload.ProjectInstanceId(project.FilePath, "net10.0"), + var runningProjectInfo = new DebuggerContracts.RunningProjectInfo( + new DebuggerContracts.ProjectInstanceId(project.FilePath, "net10.0"), restartAutomatically: false); var updates = await localService.GetUpdatesAsync([runningProjectInfo], CancellationToken.None); @@ -322,6 +344,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution return new() { + SolutionAction = SolutionAction.PendingUpdate, Solution = solution, ModuleUpdates = new ModuleUpdates( ModuleUpdateStatus.Ready, @@ -416,6 +439,84 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution Assert.False(sessionState.IsSessionActive); } + [Theory] + [InlineData(SolutionAction.None, null)] + [InlineData(SolutionAction.Committed, null)] + [InlineData(SolutionAction.PendingUpdate, true)] + [InlineData(SolutionAction.PendingUpdate, false)] + internal async Task SolutionActions(SolutionAction solutionAction, bool? commit) + { + using var context = new TestContext(); + + var localService = context.LocalService; + var localWorkspace = context.LocalWorkspace; + var serviceImpl = localService.Impl.GetTestAccessor(); + + context.MockEncService.StartDebuggingSessionImpl = (_, _, _, _) => new DebuggingSessionId(1); + + context.MockEncService.EmitSolutionUpdateImpl = (solution, _, _) => new() + { + SolutionAction = solutionAction, + Solution = solution, + ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Ready, []), + Diagnostics = [], + SyntaxError = null, + ProjectsToRebuild = [], + ProjectsToRestart = [], + ProjectsToRedeploy = [], + }; + + await localService.StartSessionAsync(CancellationToken.None); + + var initialSolution = localWorkspace.CurrentSolution; + Assert.Same(initialSolution, serviceImpl.CommittedSolution); + + await localWorkspace.ChangeSolutionAsync(initialSolution.AddTestProject("proj", out var projectId).Solution); + + var updatedSolution = localWorkspace.CurrentSolution; + + await localService.GetUpdatesAsync(runningProjects: ImmutableArray.Empty, CancellationToken.None); + + switch (solutionAction) + { + case SolutionAction.None: + Assert.Null(serviceImpl.PendingUpdatedSolution); + Assert.Same(initialSolution, serviceImpl.CommittedSolution); + break; + + case SolutionAction.Committed: + Assert.Null(serviceImpl.PendingUpdatedSolution); + Assert.Same(updatedSolution, serviceImpl.CommittedSolution); + break; + + case SolutionAction.PendingUpdate: + Assert.Same(updatedSolution, serviceImpl.PendingUpdatedSolution); + Assert.Same(initialSolution, serviceImpl.CommittedSolution); + + if (commit.Value) + { + await localService.CommitUpdatesAsync(CancellationToken.None); + + Assert.Null(serviceImpl.PendingUpdatedSolution); + Assert.Same(updatedSolution, serviceImpl.CommittedSolution); + } + else + { + await localService.DiscardUpdatesAsync(CancellationToken.None); + + Assert.Null(serviceImpl.PendingUpdatedSolution); + Assert.Same(initialSolution, serviceImpl.CommittedSolution); + } + + break; + + default: + throw ExceptionUtilities.UnexpectedValue(solutionAction); + } + + await localService.EndSessionAsync(CancellationToken.None); + } + [Fact] public async Task DefaultPdbMatchingSourceTextProvider() { diff --git a/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs b/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs index 04149dfbb14d1..10b82919e80d3 100644 --- a/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs +++ b/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs @@ -9361,6 +9361,60 @@ public void PartialMember_DeleteInsert_AddFieldInitializer() ]) ]); + [Fact] + public void PartialMember_DeleteInsert_AddFieldInitializer2() + => EditAndContinueValidation.VerifySemantics( + editScripts: + [ + GetTopEdits(""" + partial class C + { + public C() => M(); + + private partial void M(); + } + """, + """ + partial class C + { + public C() => M(); + + private partial void M(); + } + """), + GetTopEdits(""" + partial class C + { + private partial void M() + { + _ = 1; + } + } + """, + """ + partial class C + { + private int f = 0; + + private partial void M() + { + _ = 2; + } + } + """) + ], + results: + [ + DocumentResults(), + DocumentResults(semanticEdits: + [ + SemanticEdit(SemanticEditKind.Insert, c => c.GetMember("C.f")), + SemanticEdit(SemanticEditKind.Update, c => c.GetParameterlessConstructor("C"), partialType: "C", preserveLocalVariables: true), + SemanticEdit(SemanticEditKind.Update, c => c.GetMember("C.M"), partialType: "C"), + ]) + ], + capabilities: EditAndContinueCapabilities.AddInstanceFieldToExistingType); + [Fact] public void PartialMember_DeleteInsert_RemoveFieldInitializer() => EditAndContinueValidation.VerifySemantics( diff --git a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadUpdates.cs b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadUpdates.cs index e8f0b167f8815..dd70554517faf 100644 --- a/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadUpdates.cs +++ b/src/Features/Core/Portable/Contracts/EditAndContinue/ManagedHotReloadUpdates.cs @@ -8,17 +8,20 @@ namespace Microsoft.CodeAnalysis.Contracts.EditAndContinue; [DataContract] -internal readonly struct ManagedHotReloadUpdates(ImmutableArray updates, ImmutableArray diagnostics, ImmutableArray projectsToRebuild, ImmutableArray projectsToRestart) +internal readonly struct ManagedHotReloadUpdates { - [DataMember(Name = "updates")] - public ImmutableArray Updates { get; } = updates; + [DataMember] + public ImmutableArray Updates { get; init; } - [DataMember(Name = "diagnostics")] - public ImmutableArray Diagnostics { get; } = diagnostics; + [DataMember] + public ImmutableArray Diagnostics { get; init; } - [DataMember(Name = "projectsToRebuild")] - public ImmutableArray ProjectsToRebuild { get; } = projectsToRebuild; + [DataMember] + public ImmutableArray ProjectsToRebuild { get; init; } - [DataMember(Name = "projectsToRestart")] - public ImmutableArray ProjectsToRestart { get; } = projectsToRestart; + [DataMember] + public ImmutableArray ProjectsToRestart { get; init; } + + [DataMember] + public bool HasPendingUpdates { get; init; } } diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs index 811035619a1dc..7cedeb6f7a7fd 100644 --- a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs @@ -35,6 +35,7 @@ internal sealed class DebuggingSession : IDisposable private readonly CancellationTokenSource _cancellationSource = new(); internal readonly IPdbMatchingSourceTextProvider SourceTextProvider; + internal readonly EditAndContinueDiagnosticLevel DiagnosticLevel; /// /// Logs debugging session events. @@ -134,6 +135,7 @@ internal DebuggingSession( IPdbMatchingSourceTextProvider sourceTextProvider, TraceLog sessionLog, TraceLog analysisLog, + EditAndContinueDiagnosticLevel diagnosticLevel, bool reportDiagnostics) { sessionLog.Write($"Debugging session started: #{id}"); @@ -157,6 +159,7 @@ internal DebuggingSession( inBreakState: false); ReportDiagnostics = reportDiagnostics; + DiagnosticLevel = diagnosticLevel; } public void Dispose() @@ -540,6 +543,8 @@ public async ValueTask EmitSolutionUpdateAsync( solutionUpdate.Log(SessionLog, updateId); _lastModuleUpdatesLog = solutionUpdate.ModuleUpdates.Updates; + SolutionAction solutionAction; + switch (solutionUpdate.ModuleUpdates.Status) { case ModuleUpdateStatus.Ready: @@ -556,6 +561,7 @@ public async ValueTask EmitSolutionUpdateAsync( solutionUpdate.ModuleUpdates.Updates, solutionUpdate.NonRemappableRegions)); + solutionAction = SolutionAction.PendingUpdate; break; case ModuleUpdateStatus.None: @@ -569,6 +575,12 @@ public async ValueTask EmitSolutionUpdateAsync( // No significant changes have been made. // Commit the solution to apply any insignificant changes that do not generate updates. LastCommittedSolution.CommitChanges(solution, solutionUpdate.StaleProjects); + + solutionAction = SolutionAction.Committed; + break; + + default: + solutionAction = SolutionAction.None; break; } @@ -577,6 +589,7 @@ public async ValueTask EmitSolutionUpdateAsync( return new EmitSolutionUpdateResults() { Solution = solution, + SolutionAction = solutionAction, ModuleUpdates = solutionUpdate.ModuleUpdates, Diagnostics = solutionUpdate.Diagnostics, SyntaxError = solutionUpdate.SyntaxError, diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticLevel.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticLevel.cs new file mode 100644 index 0000000000000..6bc35e8236d99 --- /dev/null +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticLevel.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal enum EditAndContinueDiagnosticLevel +{ + /// + /// No extra validation. + /// + None = 0, + + /// + /// Adds extra validation that is normally not performed due to its impact on performance + /// and should only be used when diagnosing issues with EnC. + /// + Debug = 1, +} diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs index 6dacf0f6fe69e..868511abf786e 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs @@ -11,7 +11,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host; @@ -44,6 +43,11 @@ internal sealed class WorkspaceService(IEditAndContinueService service) : IEditA public IEditAndContinueService Service { get; } = service; } + private static EditAndContinueDiagnosticLevel DiagnosticLevel + => byte.TryParse(Environment.GetEnvironmentVariable("Microsoft_CodeAnalysis_EditAndContinue_DiagnosticLevel"), out var level) + ? (EditAndContinueDiagnosticLevel)level + : EditAndContinueDiagnosticLevel.None; + private static readonly string? s_logDir = GetLogDirectory(); internal readonly TraceLog Log; @@ -156,7 +160,7 @@ public DebuggingSessionId StartDebuggingSession( solution = solution.WithUpToDateSourceGeneratorDocuments(solution.ProjectIds); var sessionId = new DebuggingSessionId(Interlocked.Increment(ref s_debuggingSessionId)); - var session = new DebuggingSession(sessionId, solution, debuggerService, _compilationOutputsProvider, sourceTextProvider, Log, AnalysisLog, reportDiagnostics); + var session = new DebuggingSession(sessionId, solution, debuggerService, _compilationOutputsProvider, sourceTextProvider, Log, AnalysisLog, DiagnosticLevel, reportDiagnostics); lock (_debuggingSessions) { diff --git a/src/Features/Core/Portable/EditAndContinue/EditSession.cs b/src/Features/Core/Portable/EditAndContinue/EditSession.cs index 55d9d652091dc..33ac3276fbbbb 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditSession.cs @@ -340,7 +340,11 @@ internal static async ValueTask HasDifferencesAsync(Project oldProject, Pr return false; } - if (AbstractEditAndContinueAnalyzer.EnableProjectLevelAnalysis && HasProjectLevelDifferences(oldProject, newProject, differences) && differences == null) + // Check for project differences even when AbstractEditAndContinueAnalyzer.EnableProjectLevelAnalysis is false. + // A project-level change may be used by a source generator to produce different outputs. + // We assume that a source generator will not produce different outputs if the project state + // and content of all input documents are the same. + if (HasProjectLevelDifferences(oldProject, newProject, differences) && differences == null) { return true; } @@ -494,7 +498,14 @@ internal static bool HasDifferences(CompilationOptions oldOptions, CompilationOp .WithXmlReferenceResolver(newOptions.XmlReferenceResolver) .Equals(newOptions); - internal static async Task GetProjectDifferencesAsync(TraceLog log, Project? oldProject, Project newProject, ProjectDifferences documentDifferences, ArrayBuilder diagnostics, CancellationToken cancellationToken) + internal static async Task GetProjectDifferencesAsync( + TraceLog log, + Project? oldProject, + Project newProject, + ProjectDifferences documentDifferences, + ArrayBuilder diagnostics, + EditAndContinueDiagnosticLevel diagnosticLevel, + CancellationToken cancellationToken) { documentDifferences.Clear(); @@ -503,8 +514,12 @@ internal static async Task GetProjectDifferencesAsync(TraceLog log, Project? old return; } - if (!await HasDifferencesAsync(oldProject, newProject, documentDifferences, cancellationToken).ConfigureAwait(false)) + var hasNonGeneratedDifferences = await HasDifferencesAsync(oldProject, newProject, documentDifferences, cancellationToken).ConfigureAwait(false); + if (!hasNonGeneratedDifferences && diagnosticLevel == EditAndContinueDiagnosticLevel.None) { + // When not running in diagnostic mode we expect source generators to be deterministic + // and not produce any changes if the project state and content of all input documents are the same. + // Therefore, we can avoid computing the source generated document states. return; } @@ -546,6 +561,25 @@ internal static async Task GetProjectDifferencesAsync(TraceLog log, Project? old documentDifferences.DeletedDocuments.Add(oldProject.GetOrCreateSourceGeneratedDocument(oldState)); } + + if (!hasNonGeneratedDifferences) + { + Contract.ThrowIfTrue(diagnosticLevel == EditAndContinueDiagnosticLevel.None); + + foreach (var newDocument in documentDifferences.ChangedOrAddedDocuments) + { + log.Write($"Source-generated document '{newDocument.FilePath}' changed even though there are no differences in the project. The generator is faulty.", LogMessageSeverity.Warning); + } + + foreach (var document in documentDifferences.DeletedDocuments) + { + log.Write($"Source-generated document '{document.FilePath}' has been deleted even though there are no differences in the project. The generator is faulty.", LogMessageSeverity.Warning); + } + + // Keep the changed documents in the result. + // This modifies the behavior compared to non-diagnostic mode, + // but it allows to work around the issue with the source generator. + } } private static async ValueTask> GetSourceGeneratedDocumentStatesAsync(TraceLog log, Project project, ArrayBuilder? diagnostics, CancellationToken cancellationToken) @@ -1156,7 +1190,7 @@ void UpdateChangedDocumentsStaleness(DocumentStalenessReason? staleness) continue; } - await GetProjectDifferencesAsync(Log, oldProject, newProject, projectDifferences, projectDiagnostics, cancellationToken).ConfigureAwait(false); + await GetProjectDifferencesAsync(Log, oldProject, newProject, projectDifferences, projectDiagnostics, DebuggingSession.DiagnosticLevel, cancellationToken).ConfigureAwait(false); projectDifferences.Log(Log, newProject); if (projectDifferences.IsEmpty) @@ -1597,7 +1631,11 @@ async ValueTask LogDocumentChangesAsync(int? generation, CancellationToken cance bool LogException(Exception e) { - Log.Write($"Exception while emitting update: {e}", LogMessageSeverity.Error); + if (e is not OperationCanceledException) + { + Log.Write($"Exception while emitting update: {e}", LogMessageSeverity.Error); + } + return true; } } diff --git a/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs b/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs index 34ac6f9574295..c7a3c306930e5 100644 --- a/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs +++ b/src/Features/Core/Portable/EditAndContinue/EmitSolutionUpdateResults.cs @@ -12,7 +12,6 @@ using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; @@ -23,6 +22,9 @@ internal readonly struct EmitSolutionUpdateResults [DataContract] internal readonly struct Data { + [DataMember] + public required SolutionAction SolutionAction { get; init; } + [DataMember] public required ModuleUpdates ModuleUpdates { get; init; } @@ -89,8 +91,12 @@ public static Data CreateFromInternalError(Solution solution, string errorMessag Location.None, errorMessage); + // An internal error should be treated as a blocking rude edit in all running projects + // since restarting all projects will apply any changes that were made and allow the user to continue debugging. + return new() { + SolutionAction = SolutionAction.None, ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Ready, []), Diagnostics = [DiagnosticData.Create(diagnostic, firstProject)], SyntaxError = null, @@ -104,6 +110,7 @@ public static Data CreateFromInternalError(Solution solution, string errorMessag public static readonly EmitSolutionUpdateResults Empty = new() { Solution = null, + SolutionAction = SolutionAction.None, ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.None, []), Diagnostics = [], SyntaxError = null, @@ -121,6 +128,11 @@ public static Data CreateFromInternalError(Solution solution, string errorMessag /// public required Solution? Solution { get; init; } + /// + /// Action taken on the solution. + /// + public required SolutionAction SolutionAction { get; init; } + public required ModuleUpdates ModuleUpdates { get; init; } /// @@ -153,6 +165,7 @@ public Data Dehydrate() => Solution == null ? new() { + SolutionAction = SolutionAction, ModuleUpdates = ModuleUpdates, Diagnostics = [], SyntaxError = null, @@ -162,6 +175,7 @@ public Data Dehydrate() } : new() { + SolutionAction = SolutionAction, ModuleUpdates = ModuleUpdates, Diagnostics = Diagnostics.ToDiagnosticData(Solution), SyntaxError = GetSyntaxErrorData(), diff --git a/src/Features/Core/Portable/EditAndContinue/SolutionAction.cs b/src/Features/Core/Portable/EditAndContinue/SolutionAction.cs new file mode 100644 index 0000000000000..5dc18ce6d46a0 --- /dev/null +++ b/src/Features/Core/Portable/EditAndContinue/SolutionAction.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal enum SolutionAction +{ + /// + /// No action should be taken on the solution. + /// + None, + + /// + /// The solution has been committed. + /// + Committed, + + /// + /// Pending solution updates have been stored and will need to be committed or discarded. + /// + PendingUpdate, +} diff --git a/src/Features/ExternalAccess/HotReload/Api/HotReloadService.cs b/src/Features/ExternalAccess/HotReload/Api/HotReloadService.cs index ec917a27bfea9..2d02441ccc86a 100644 --- a/src/Features/ExternalAccess/HotReload/Api/HotReloadService.cs +++ b/src/Features/ExternalAccess/HotReload/Api/HotReloadService.cs @@ -87,7 +87,7 @@ public readonly struct Updates /// /// Status of the updates. /// - public readonly Status Status { get; init; } + public required Status Status { get; init; } /// /// Returns all diagnostics that can't be addressed by rebuilding/restarting the project. diff --git a/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs b/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs index 070117c2d7925..d5820825f3c2d 100644 --- a/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs +++ b/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs @@ -2435,7 +2435,7 @@ public async Task HasChanges_Documents(TextDocumentKind documentKind) await EditSession.GetChangedDocumentsAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None)); var diagnostics = new ArrayBuilder(); - await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, CancellationToken.None); + await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, EditAndContinueDiagnosticLevel.None, CancellationToken.None); Assert.Empty(diagnostics); Assert.Empty(projectDifferences.DeletedDocuments); AssertEx.Equal(documentKind == TextDocumentKind.Document ? [documentId, generatedDocumentId] : [generatedDocumentId], projectDifferences.ChangedOrAddedDocuments.Select(d => d.Id)); @@ -2465,7 +2465,7 @@ public async Task HasChanges_Documents(TextDocumentKind documentKind) AssertEx.Equal(documentKind == TextDocumentKind.Document ? new[] { documentId } : [], await EditSession.GetChangedDocumentsAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None)); - await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, CancellationToken.None); + await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, EditAndContinueDiagnosticLevel.None, CancellationToken.None); Assert.Empty(diagnostics); Assert.True(projectDifferences.IsEmpty); @@ -2490,7 +2490,7 @@ public async Task HasChanges_Documents(TextDocumentKind documentKind) AssertEx.Equal(documentKind == TextDocumentKind.Document ? [documentId, generatedDocumentId] : [generatedDocumentId], await EditSession.GetChangedDocumentsAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None)); - await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, CancellationToken.None); + await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, EditAndContinueDiagnosticLevel.None, CancellationToken.None); Assert.Empty(diagnostics); Assert.Empty(projectDifferences.DeletedDocuments); AssertEx.Equal(documentKind == TextDocumentKind.Document ? [documentId, generatedDocumentId] : [generatedDocumentId], projectDifferences.ChangedOrAddedDocuments.Select(d => d.Id)); @@ -2518,7 +2518,7 @@ public async Task HasChanges_Documents(TextDocumentKind documentKind) AssertEx.Equal([generatedDocumentId], await EditSession.GetChangedDocumentsAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None)); - await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, CancellationToken.None); + await EditSession.GetProjectDifferencesAsync(log, oldSolution.GetProject(projectId), solution.GetProject(projectId), projectDifferences, diagnostics, EditAndContinueDiagnosticLevel.None, CancellationToken.None); Assert.Empty(diagnostics); if (documentKind == TextDocumentKind.Document) @@ -2601,7 +2601,7 @@ public async Task HasChanges_SourceGeneratorFailure() AssertEx.Empty(await EditSession.GetChangedDocumentsAsync(log, oldProject, project, CancellationToken.None).ToImmutableArrayAsync(CancellationToken.None)); var diagnostics = new ArrayBuilder(); - await EditSession.GetProjectDifferencesAsync(log, oldProject, project, diffences, diagnostics, CancellationToken.None); + await EditSession.GetProjectDifferencesAsync(log, oldProject, project, diffences, diagnostics, EditAndContinueDiagnosticLevel.None, CancellationToken.None); Assert.Contains("System.InvalidOperationException: Source generator failed", diagnostics.Single().GetMessage()); AssertEx.Empty(diffences.ChangedOrAddedDocuments); AssertEx.Equal(["generated.cs"], diffences.DeletedDocuments.Select(d => d.Name)); diff --git a/src/Features/Test/EditAndContinue/EditSessionActiveStatementsTests.cs b/src/Features/Test/EditAndContinue/EditSessionActiveStatementsTests.cs index 38f306fce25e1..8fac9b0eed2c3 100644 --- a/src/Features/Test/EditAndContinue/EditSessionActiveStatementsTests.cs +++ b/src/Features/Test/EditAndContinue/EditSessionActiveStatementsTests.cs @@ -53,6 +53,7 @@ private static EditSession CreateEditSession( NullPdbMatchingSourceTextProvider.Instance, log, log, + EditAndContinueDiagnosticLevel.None, reportDiagnostics: true); if (initialState != CommittedSolution.DocumentState.None) diff --git a/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs b/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs index d488b6254c34c..bf4e52d32b4a1 100644 --- a/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs +++ b/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs @@ -137,6 +137,7 @@ public async Task GetHotReloadDiagnostics() var data = new EmitSolutionUpdateResults.Data() { + SolutionAction = SolutionAction.None, Diagnostics = [.. diagnostics, .. rudeEdits], SyntaxError = syntaxError, ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Blocked, Updates: []), diff --git a/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs b/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs index 574994e52993c..539cfac9ed3ff 100644 --- a/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs +++ b/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs @@ -209,6 +209,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution return new() { + SolutionAction = SolutionAction.None, Solution = solution, ModuleUpdates = updates, Diagnostics = diagnostics, diff --git a/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestVerifier.cs b/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestVerifier.cs index e47e89f1ed1ce..ff63c71f865d9 100644 --- a/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestVerifier.cs +++ b/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestVerifier.cs @@ -62,6 +62,33 @@ internal abstract class EditAndContinueTestVerifier public const EditAndContinueCapabilities AllRuntimeCapabilities = Net10RuntimeCapabilities; + internal static readonly SymbolDisplayFormat TestFormat = + new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + propertyStyle: SymbolDisplayPropertyStyle.ShowReadWriteDescriptor, + localOptions: SymbolDisplayLocalOptions.IncludeType, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeVariance, + memberOptions: + SymbolDisplayMemberOptions.IncludeParameters | + SymbolDisplayMemberOptions.IncludeContainingType | + SymbolDisplayMemberOptions.IncludeType | + SymbolDisplayMemberOptions.IncludeRef | + SymbolDisplayMemberOptions.IncludeExplicitInterface, + kindOptions: + SymbolDisplayKindOptions.IncludeMemberKeyword, + parameterOptions: + SymbolDisplayParameterOptions.IncludeOptionalBrackets | + SymbolDisplayParameterOptions.IncludeDefaultValue | + SymbolDisplayParameterOptions.IncludeParamsRefOut | + SymbolDisplayParameterOptions.IncludeExtensionThis | + SymbolDisplayParameterOptions.IncludeType | + SymbolDisplayParameterOptions.IncludeName, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers | + SymbolDisplayMiscellaneousOptions.UseErrorTypeSymbolName | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + public AbstractEditAndContinueAnalyzer Analyzer { get; } protected EditAndContinueTestVerifier(Action? faultInjector) @@ -352,12 +379,12 @@ static int CompareEdits(SymbolKey leftKey, SemanticEditKind leftKind, SymbolKey => leftKey.ToString().CompareTo(rightKey.ToString()) is not 0 and var result ? result : leftKind.CompareTo(rightKind); SymbolKey CreateSymbolKey(SemanticEditDescription edit) - => SymbolKey.Create(edit.SymbolProvider((edit.Kind == SemanticEditKind.Delete) ? oldCompilation : newCompilation)); + => SymbolKey.Create(edit.GetSymbol((edit.Kind == SemanticEditKind.Delete) ? oldCompilation : newCompilation)); // string comparison to simplify understanding why a test failed: AssertEx.Equal( - expectedSemanticEdits.Select(e => $"{e.Kind}: {e.SymbolProvider((e.Kind == SemanticEditKind.Delete ? oldCompilation : newCompilation))}"), - actualSemanticEdits.Select(e => $"{e.Kind}: {e.Symbol.Resolve(e.Kind == SemanticEditKind.Delete ? oldCompilation : newCompilation).Symbol}"), + expectedSemanticEdits.Select(e => $"{e.Kind}: {Inspect(e.GetSymbol((e.Kind == SemanticEditKind.Delete ? oldCompilation : newCompilation)))}"), + actualSemanticEdits.Select(e => $"{e.Kind}: {Inspect(e.Symbol.Resolve(e.Kind == SemanticEditKind.Delete ? oldCompilation : newCompilation).Symbol)}"), message: message); for (var i = 0; i < actualSemanticEdits.Length; i++) @@ -374,23 +401,23 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit) switch (editKind) { case SemanticEditKind.Update: - expectedOldSymbol = expectedSemanticEdit.SymbolProvider(oldCompilation); - expectedNewSymbol = expectedSemanticEdit.SymbolProvider(newCompilation); + expectedOldSymbol = expectedSemanticEdit.GetSymbol(oldCompilation); + expectedNewSymbol = expectedSemanticEdit.GetSymbol(newCompilation); - Assert.Equal(expectedOldSymbol, symbolKey.Resolve(oldCompilation).Symbol); - Assert.Equal(expectedNewSymbol, symbolKey.Resolve(newCompilation).Symbol); + VerifySymbolsEqual(expectedOldSymbol, symbolKey.Resolve(oldCompilation).Symbol); + VerifySymbolsEqual(expectedNewSymbol, symbolKey.Resolve(newCompilation).Symbol); break; case SemanticEditKind.Delete: - expectedOldSymbol = expectedSemanticEdit.SymbolProvider(oldCompilation); + expectedOldSymbol = expectedSemanticEdit.GetSymbol(oldCompilation); // Symbol key will happily resolve to a definition part that has no implementation, so we validate that // differently if (expectedOldSymbol.IsPartialDefinition() && symbolKey.Resolve(oldCompilation).Symbol is ISymbol resolvedSymbol) { - Assert.Equal(expectedOldSymbol, resolvedSymbol.PartialDefinitionPart()); - Assert.Equal(null, resolvedSymbol.PartialImplementationPart()); + VerifySymbolsEqual(expectedOldSymbol, resolvedSymbol.PartialDefinitionPart()); + Assert.Null(resolvedSymbol.PartialImplementationPart()); } else { @@ -401,7 +428,7 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit) // represented in the symbol key, so the check below would fail, so we skip it. if (expectedSemanticEdit.DeletedSymbolContainerProvider is null) { - Assert.Equal(null, symbolKey.Resolve(newCompilation).Symbol); + Assert.Null(symbolKey.Resolve(newCompilation).Symbol); } } @@ -409,13 +436,13 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit) AssertEx.AreEqual( deletedSymbolContainer, expectedSemanticEdit.DeletedSymbolContainerProvider?.Invoke(newCompilation), - message: $"{message}, {editKind}({expectedNewSymbol ?? expectedOldSymbol}): Incorrect deleted container"); + message: $"{message}, {editKind}({Inspect(expectedNewSymbol ?? expectedOldSymbol)}): Incorrect deleted container"); break; case SemanticEditKind.Insert or SemanticEditKind.Replace: - expectedNewSymbol = expectedSemanticEdit.SymbolProvider(newCompilation); - Assert.Equal(expectedNewSymbol, symbolKey.Resolve(newCompilation).Symbol); + expectedNewSymbol = expectedSemanticEdit.GetSymbol(newCompilation); + VerifySymbolsEqual(expectedNewSymbol, symbolKey.Resolve(newCompilation).Symbol); break; default: @@ -426,7 +453,7 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit) AssertEx.AreEqual( expectedSemanticEdit.PartialType?.Invoke(newCompilation), actualSemanticEdit.PartialType?.Resolve(newCompilation).Symbol, - message: $"{message}, {editKind}({expectedNewSymbol ?? expectedOldSymbol}): Partial types do not match"); + message: $"{message}, {editKind}({Inspect(expectedNewSymbol ?? expectedOldSymbol)}): Partial types do not match"); var expectedSyntaxMap = expectedSemanticEdit.GetSyntaxMap(); @@ -435,7 +462,7 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit) AssertEx.AreEqual( expectedSyntaxMap != null, actualSyntaxMaps.HasMap, - message: $"{message}, {editKind}({expectedNewSymbol ?? expectedOldSymbol}): Incorrect syntax map"); + message: $"{message}, {editKind}({Inspect(expectedNewSymbol ?? expectedOldSymbol)}): Incorrect syntax map"); // If expected map is specified validate its mappings with the actual one: if (expectedSyntaxMap != null) @@ -445,6 +472,35 @@ SymbolKey CreateSymbolKey(SemanticEditDescription edit) } } + public static void VerifySymbolsEqual(ISymbol expectedSymbol, ISymbol? actualSymbol) + { + if (expectedSymbol != actualSymbol) + { + Assert.Fail($"Expected: {Inspect(expectedSymbol)}; actual: {Inspect(actualSymbol)}"); + } + } + + private static string Inspect(ISymbol? symbol) + { + if (symbol is null) + { + return ""; + } + + var display = $"'{symbol.ToDisplayString(TestFormat)}'"; + + if (symbol.IsPartialDefinition()) + { + display += " (partial def)"; + } + else if (symbol.IsPartialImplementation()) + { + display += " (partial impl)"; + } + + return display; + } + public static SyntaxNode FindNode(SyntaxNode root, TextSpan span) { var result = root.FindToken(span.Start).Parent; diff --git a/src/Features/TestUtilities/EditAndContinue/SemanticEditDescription.cs b/src/Features/TestUtilities/EditAndContinue/SemanticEditDescription.cs index b8c9b4d57b7e2..6e5c2e36d2bec 100644 --- a/src/Features/TestUtilities/EditAndContinue/SemanticEditDescription.cs +++ b/src/Features/TestUtilities/EditAndContinue/SemanticEditDescription.cs @@ -20,7 +20,6 @@ internal sealed class SemanticEditDescription( Func? deletedSymbolContainerProvider) { public readonly SemanticEditKind Kind = kind; - public readonly Func SymbolProvider = symbolProvider; public readonly Func? PartialType = partialType; public readonly Func? DeletedSymbolContainerProvider = deletedSymbolContainerProvider; @@ -32,6 +31,9 @@ internal sealed class SemanticEditDescription( public readonly bool HasSyntaxMap = hasSyntaxMap; + public ISymbol GetSymbol(Compilation compilation) + => symbolProvider(compilation).PartialAsImplementation(); + private static IEnumerable<(TextSpan oldSpan, TextSpan newSpan, RuntimeRudeEditDescription? runtimeRudeEdit)> GetSyntaxMapWithRudeEdits(IEnumerable<(TextSpan, TextSpan)>? syntaxMap, IEnumerable? rudeEdits) { if (syntaxMap == null) diff --git a/src/VisualStudio/Xaml/Impl/Implementation/XamlEditAndContinueSolutionProvider.cs b/src/VisualStudio/Xaml/Impl/Implementation/XamlEditAndContinueSolutionProvider.cs index 8b1a66e8430bc..a57b5492e5581 100644 --- a/src/VisualStudio/Xaml/Impl/Implementation/XamlEditAndContinueSolutionProvider.cs +++ b/src/VisualStudio/Xaml/Impl/Implementation/XamlEditAndContinueSolutionProvider.cs @@ -7,6 +7,7 @@ using System.ComponentModel.Composition; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host.Mef; namespace Microsoft.VisualStudio.LanguageServices.Xaml; @@ -43,6 +44,12 @@ public void Dispose() private void OnEditAndContinueSolutionCommitted(Solution solution) { - SolutionCommitted?.Invoke(solution); + try + { + SolutionCommitted?.Invoke(solution); + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) + { + } } }