Skip to content

Commit

Permalink
Report source generator failures in Hot Reload diagnostics (#75029)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmat authored Sep 10, 2024
1 parent a69841b commit 85ec699
Show file tree
Hide file tree
Showing 15 changed files with 337 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -319,17 +319,16 @@ public async ValueTask<ManagedHotReloadUpdates> GetUpdatesAsync(CancellationToke
var designTimeSolution = GetCurrentDesignTimeSolution();
var solution = GetCurrentCompileTimeSolution(designTimeSolution);
var activeStatementSpanProvider = GetActiveStatementSpanProvider(solution);
var (moduleUpdates, diagnosticData, rudeEdits, syntaxError) = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false);
var result = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false);

// Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called.
if (moduleUpdates.Status == ModuleUpdateStatus.Ready)
if (result.ModuleUpdates.Status == ModuleUpdateStatus.Ready)
{
_pendingUpdatedDesignTimeSolution = designTimeSolution;
}

UpdateApplyChangesDiagnostics(diagnosticData);
UpdateApplyChangesDiagnostics(result.Diagnostics);

var diagnostics = EmitSolutionUpdateResults.GetAllDiagnostics(diagnosticData, rudeEdits, syntaxError, moduleUpdates.Status);
return new ManagedHotReloadUpdates(moduleUpdates.Updates.FromContract(), diagnostics.FromContract());
return new ManagedHotReloadUpdates(result.ModuleUpdates.Updates.FromContract(), result.GetAllDiagnostics().FromContract());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,8 @@ public void EndDebuggingSession()

public async ValueTask<ManagedHotReloadUpdates> GetUpdatesAsync(Solution solution, CancellationToken cancellationToken)
{
var result = await _encService.EmitSolutionUpdateAsync(GetSessionId(), solution, s_noActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false);
var diagnostics = EmitSolutionUpdateResults.GetAllDiagnostics(result.Diagnostics.ToDiagnosticData(solution), result.RudeEdits.ToDiagnosticData(solution), result.GetSyntaxErrorData(solution), result.ModuleUpdates.Status);
return new ManagedHotReloadUpdates(result.ModuleUpdates.Updates.FromContract(), diagnostics.FromContract());
var results = (await _encService.EmitSolutionUpdateAsync(GetSessionId(), solution, s_noActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false)).Dehydrate();
return new ManagedHotReloadUpdates(results.ModuleUpdates.Updates.FromContract(), results.GetAllDiagnostics().FromContract());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
return new()
{
Solution = solution,
ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Ready, []),
Diagnostics = [new ProjectDiagnostics(project.Id, [documentDiagnostic, projectDiagnostic])],
RudeEdits = [new ProjectDiagnostics(project.Id, [rudeEditDiagnostic])],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ internal DebuggingSession(
IEnumerable<KeyValuePair<DocumentId, CommittedSolution.DocumentState>> initialDocumentStates,
bool reportDiagnostics)
{
EditAndContinueService.Log.Write($"Debugging session started: #{id}");

_compilationOutputsProvider = compilationOutputsProvider;
SourceTextProvider = sourceTextProvider;
_reportTelemetry = ReportTelemetry;
Expand Down Expand Up @@ -198,13 +200,17 @@ public void EndSession(out DebuggingSessionTelemetry.Data telemetryData)
_reportTelemetry(telemetryData);

Dispose();

EditAndContinueService.Log.Write($"Debugging session ended: #{Id}");
}

public void BreakStateOrCapabilitiesChanged(bool? inBreakState)
=> RestartEditSession(nonRemappableRegions: null, inBreakState);

internal void RestartEditSession(ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>? nonRemappableRegions, bool? inBreakState)
{
EditAndContinueService.Log.Write($"Edit session restarted (break state: {inBreakState?.ToString() ?? "null"})");

ThrowIfDisposed();

EndEditSession();
Expand Down Expand Up @@ -550,6 +556,7 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
// The debugger will still call commit or discard on the update batch.
return new EmitSolutionUpdateResults()
{
Solution = solution,
ModuleUpdates = solutionUpdate.ModuleUpdates,
Diagnostics = solutionUpdate.Diagnostics,
RudeEdits = rudeEditDiagnostics.ToImmutable(),
Expand Down
33 changes: 23 additions & 10 deletions src/Features/Core/Portable/EditAndContinue/EditSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ internal static async ValueTask<bool> HasChangedOrAddedDocumentsAsync(Project ol
return false;
}

internal static async Task PopulateChangedAndAddedDocumentsAsync(Project oldProject, Project newProject, ArrayBuilder<Document> changedOrAddedDocuments, CancellationToken cancellationToken)
internal static async Task PopulateChangedAndAddedDocumentsAsync(Project oldProject, Project newProject, ArrayBuilder<Document> changedOrAddedDocuments, ArrayBuilder<ProjectDiagnostics> diagnostics, CancellationToken cancellationToken)
{
changedOrAddedDocuments.Clear();

Expand All @@ -432,14 +432,12 @@ internal static async Task PopulateChangedAndAddedDocumentsAsync(Project oldProj
return;
}

var oldSourceGeneratedDocumentStates = await GetSourceGeneratedDocumentStatesAsync(oldProject, diagnostics, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();

var oldSourceGeneratedDocumentStates = await oldProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(oldProject.State, cancellationToken).ConfigureAwait(false);

var newSourceGeneratedDocumentStates = await GetSourceGeneratedDocumentStatesAsync(newProject, diagnostics, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();

var newSourceGeneratedDocumentStates = await newProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(newProject.State, cancellationToken).ConfigureAwait(false);

foreach (var documentId in newSourceGeneratedDocumentStates.GetChangedStateIds(oldSourceGeneratedDocumentStates, ignoreUnchangedContent: true))
{
var newState = newSourceGeneratedDocumentStates.GetRequiredState(documentId);
Expand All @@ -463,6 +461,23 @@ internal static async Task PopulateChangedAndAddedDocumentsAsync(Project oldProj
}
}

private static async ValueTask<TextDocumentStates<SourceGeneratedDocumentState>> GetSourceGeneratedDocumentStatesAsync(Project project, ArrayBuilder<ProjectDiagnostics>? diagnostics, CancellationToken cancellationToken)
{
var generatorDiagnostics = await project.Solution.CompilationState.GetSourceGeneratorDiagnosticsAsync(project.State, cancellationToken).ConfigureAwait(false);

if (generatorDiagnostics is not [])
{
diagnostics?.Add(new ProjectDiagnostics(project.Id, generatorDiagnostics));
}

foreach (var generatorDiagnostic in generatorDiagnostics)
{
EditAndContinueService.Log.Write("Source generator failed: {0}", generatorDiagnostic);
}

return await project.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(project.State, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Enumerates <see cref="DocumentId"/>s of changed (not added or removed) <see cref="Document"/>s (not additional nor analyzer config).
/// </summary>
Expand Down Expand Up @@ -496,14 +511,12 @@ internal static async IAsyncEnumerable<DocumentId> GetChangedDocumentsAsync(Proj
yield break;
}

var oldSourceGeneratedDocumentStates = await GetSourceGeneratedDocumentStatesAsync(oldProject, diagnostics: null, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();

var oldSourceGeneratedDocumentStates = await oldProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(oldProject.State, cancellationToken).ConfigureAwait(false);

var newSourceGeneratedDocumentStates = await GetSourceGeneratedDocumentStatesAsync(newProject, diagnostics: null, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();

var newSourceGeneratedDocumentStates = await newProject.Solution.CompilationState.GetSourceGeneratedDocumentStatesAsync(newProject.State, cancellationToken).ConfigureAwait(false);

foreach (var documentId in newSourceGeneratedDocumentStates.GetChangedStateIds(oldSourceGeneratedDocumentStates, ignoreUnchangedContent: true))
{
yield return documentId;
Expand Down Expand Up @@ -830,7 +843,7 @@ public async ValueTask<SolutionUpdate> EmitSolutionUpdateAsync(Solution solution
continue;
}

await PopulateChangedAndAddedDocumentsAsync(oldProject, newProject, changedOrAddedDocuments, cancellationToken).ConfigureAwait(false);
await PopulateChangedAndAddedDocumentsAsync(oldProject, newProject, changedOrAddedDocuments, diagnostics, cancellationToken).ConfigureAwait(false);
if (changedOrAddedDocuments.IsEmpty)
{
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,39 +36,96 @@ internal readonly struct Data

[DataMember]
public required DiagnosticData? SyntaxError { get; init; }

internal ImmutableArray<ManagedHotReloadDiagnostic> GetAllDiagnostics()
{
using var _ = ArrayBuilder<ManagedHotReloadDiagnostic>.GetInstance(out var builder);

// Add semantic and lowering diagnostics reported during delta emit:

foreach (var diagnostic in Diagnostics)
{
builder.Add(diagnostic.ToHotReloadDiagnostic(ModuleUpdates.Status, isRudeEdit: false));
}

// Add syntax error:

if (SyntaxError != null)
{
Debug.Assert(SyntaxError.DataLocation != null);
Debug.Assert(SyntaxError.Message != null);

var fileSpan = SyntaxError.DataLocation.MappedFileSpan;

builder.Add(new ManagedHotReloadDiagnostic(
SyntaxError.Id,
SyntaxError.Message,
ManagedHotReloadDiagnosticSeverity.Error,
fileSpan.Path,
fileSpan.Span.ToSourceSpan()));
}

// Report all rude edits.

foreach (var data in RudeEdits)
{
builder.Add(data.ToHotReloadDiagnostic(ModuleUpdates.Status, isRudeEdit: true));
}

return builder.ToImmutableAndClear();
}
}

public static readonly EmitSolutionUpdateResults Empty = new()
{
Solution = null,
ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.None, []),
Diagnostics = [],
RudeEdits = [],
SyntaxError = null
};

/// <summary>
/// Solution snapshot to resolve diagnostics in.
/// Note that this might be a different snapshot from the one passed to <see cref="IEditAndContinueService.EmitSolutionUpdateAsync(DebuggingSessionId, Solution, ActiveStatementSpanProvider, CancellationToken)"/>,
/// with source generator files refreshed.
///
/// Null only for empty results.
/// </summary>
public required Solution? Solution { get; init; }

public required ModuleUpdates ModuleUpdates { get; init; }
public required ImmutableArray<ProjectDiagnostics> Diagnostics { get; init; }
public required ImmutableArray<ProjectDiagnostics> RudeEdits { get; init; }
public required Diagnostic? SyntaxError { get; init; }

public Data Dehydrate(Solution solution)
=> new()
public Data Dehydrate()
=> Solution == null
? new()
{
ModuleUpdates = ModuleUpdates,
Diagnostics = Diagnostics.ToDiagnosticData(solution),
RudeEdits = RudeEdits.ToDiagnosticData(solution),
SyntaxError = GetSyntaxErrorData(solution)
Diagnostics = [],
RudeEdits = [],
SyntaxError = null
}
: new()
{
ModuleUpdates = ModuleUpdates,
Diagnostics = Diagnostics.ToDiagnosticData(Solution),
RudeEdits = RudeEdits.ToDiagnosticData(Solution),
SyntaxError = GetSyntaxErrorData()
};

public DiagnosticData? GetSyntaxErrorData(Solution solution)
private DiagnosticData? GetSyntaxErrorData()
{
if (SyntaxError == null)
{
return null;
}

Debug.Assert(Solution != null);
Debug.Assert(SyntaxError.Location.SourceTree != null);
return DiagnosticData.Create(SyntaxError, solution.GetRequiredDocument(SyntaxError.Location.SourceTree));
return DiagnosticData.Create(SyntaxError, Solution.GetRequiredDocument(SyntaxError.Location.SourceTree));
}

private IEnumerable<Project> GetProjectsContainingBlockingRudeEdits(Solution solution)
Expand Down Expand Up @@ -216,46 +273,4 @@ public ImmutableArray<Diagnostic> GetAllDiagnostics()

return diagnostics.ToImmutableAndClear();
}

internal static ImmutableArray<ManagedHotReloadDiagnostic> GetAllDiagnostics(
ImmutableArray<DiagnosticData> diagnosticData,
ImmutableArray<DiagnosticData> rudeEdits,
DiagnosticData? syntaxError,
ModuleUpdateStatus updateStatus)
{
using var _ = ArrayBuilder<ManagedHotReloadDiagnostic>.GetInstance(out var builder);

// Add semantic and lowering diagnostics reported during delta emit:

foreach (var data in diagnosticData)
{
builder.Add(data.ToHotReloadDiagnostic(updateStatus, isRudeEdit: false));
}

// Add syntax error:

if (syntaxError != null)
{
Debug.Assert(syntaxError.DataLocation != null);
Debug.Assert(syntaxError.Message != null);

var fileSpan = syntaxError.DataLocation.MappedFileSpan;

builder.Add(new ManagedHotReloadDiagnostic(
syntaxError.Id,
syntaxError.Message,
ManagedHotReloadDiagnosticSeverity.Error,
fileSpan.Path,
fileSpan.Span.ToSourceSpan()));
}

// Report all rude edits.

foreach (var data in rudeEdits)
{
builder.Add(data.ToHotReloadDiagnostic(updateStatus, isRudeEdit: true));
}

return builder.ToImmutableAndClear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,64 +53,43 @@ await client.TryInvokeAsync<IRemoteEditAndContinueService>(
Dispose();
}

public async ValueTask<(
ModuleUpdates updates,
ImmutableArray<DiagnosticData> diagnostics,
ImmutableArray<DiagnosticData> rudeEdits,
DiagnosticData? syntaxError)> EmitSolutionUpdateAsync(
public async ValueTask<EmitSolutionUpdateResults.Data> EmitSolutionUpdateAsync(
Solution solution,
ActiveStatementSpanProvider activeStatementSpanProvider,
CancellationToken cancellationToken)
{
ModuleUpdates moduleUpdates;
ImmutableArray<DiagnosticData> diagnosticData;
ImmutableArray<DiagnosticData> rudeEdits;
DiagnosticData? syntaxError;

try
{
var client = await RemoteHostClient.TryGetClientAsync(services, cancellationToken).ConfigureAwait(false);
if (client == null)
{
var results = await GetLocalService().EmitSolutionUpdateAsync(sessionId, solution, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false);
moduleUpdates = results.ModuleUpdates;
diagnosticData = results.Diagnostics.ToDiagnosticData(solution);
rudeEdits = results.RudeEdits.ToDiagnosticData(solution);
syntaxError = results.GetSyntaxErrorData(solution);
return (await GetLocalService().EmitSolutionUpdateAsync(sessionId, solution, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false)).Dehydrate();
}
else

var result = await client.TryInvokeAsync<IRemoteEditAndContinueService, EmitSolutionUpdateResults.Data>(
solution,
(service, solutionInfo, callbackId, cancellationToken) => service.EmitSolutionUpdateAsync(solutionInfo, callbackId, sessionId, cancellationToken),
callbackTarget: new ActiveStatementSpanProviderCallback(activeStatementSpanProvider),
cancellationToken).ConfigureAwait(false);

return result.HasValue ? result.Value : new EmitSolutionUpdateResults.Data()
{
var result = await client.TryInvokeAsync<IRemoteEditAndContinueService, EmitSolutionUpdateResults.Data>(
solution,
(service, solutionInfo, callbackId, cancellationToken) => service.EmitSolutionUpdateAsync(solutionInfo, callbackId, sessionId, cancellationToken),
callbackTarget: new ActiveStatementSpanProviderCallback(activeStatementSpanProvider),
cancellationToken).ConfigureAwait(false);

if (result.HasValue)
{
moduleUpdates = result.Value.ModuleUpdates;
diagnosticData = result.Value.Diagnostics;
rudeEdits = result.Value.RudeEdits;
syntaxError = result.Value.SyntaxError;
}
else
{
moduleUpdates = new ModuleUpdates(ModuleUpdateStatus.RestartRequired, []);
diagnosticData = [];
rudeEdits = [];
syntaxError = null;
}
}
ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.RestartRequired, []),
Diagnostics = [],
RudeEdits = [],
SyntaxError = null,
};
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
{
diagnosticData = GetInternalErrorDiagnosticData(solution, e);
rudeEdits = [];
moduleUpdates = new ModuleUpdates(ModuleUpdateStatus.RestartRequired, []);
syntaxError = null;
return new EmitSolutionUpdateResults.Data()
{
ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.RestartRequired, []),
Diagnostics = GetInternalErrorDiagnosticData(solution, e),
RudeEdits = [],
SyntaxError = null,
};
}

return (moduleUpdates, diagnosticData, rudeEdits, syntaxError);
}

private static ImmutableArray<DiagnosticData> GetInternalErrorDiagnosticData(Solution solution, Exception e)
Expand Down
Loading

0 comments on commit 85ec699

Please sign in to comment.