Skip to content

EnC: Simplify diagnostic reporting #78708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -398,26 +398,26 @@ public async ValueTask<ManagedHotReloadUpdates> GetUpdatesAsync(ImmutableArray<s
break;
}

ArrayBuilder<DiagnosticData>? deletedDocumentRudeEdits = null;
foreach (var rudeEdit in result.RudeEdits)
ArrayBuilder<DiagnosticData>? applyChangesDiagnostics = null;
foreach (var diagnostic in result.Diagnostics)
{
if (await solution.GetDocumentAsync(rudeEdit.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false) == null)
// Report warnings and errors that are not reported when analyzing documents or are reported for deleted documents.

if (diagnostic.Severity is not (DiagnosticSeverity.Error or DiagnosticSeverity.Warning))
{
deletedDocumentRudeEdits ??= ArrayBuilder<DiagnosticData>.GetInstance();
deletedDocumentRudeEdits.Add(rudeEdit);
continue;
}
}

if (deletedDocumentRudeEdits != null)
{
deletedDocumentRudeEdits.AddRange(result.Diagnostics);
UpdateApplyChangesDiagnostics(deletedDocumentRudeEdits.ToImmutableAndFree());
}
else
{
UpdateApplyChangesDiagnostics(result.Diagnostics);
if ((!EditAndContinueDiagnosticDescriptors.IsRudeEdit(diagnostic.Id)) ||
await solution.GetDocumentAsync(diagnostic.DocumentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false) == null)
{
applyChangesDiagnostics ??= ArrayBuilder<DiagnosticData>.GetInstance();
applyChangesDiagnostics.Add(diagnostic);
}
}

UpdateApplyChangesDiagnostics(applyChangesDiagnostics.ToImmutableOrEmptyAndFree());

return new ManagedHotReloadUpdates(
result.ModuleUpdates.Updates.FromContract(),
result.GetAllDiagnostics().FromContract(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public async Task Test(bool commitChanges)

await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution
.AddTestProject("proj", out var projectId)
.AddTestDocument("test.cs", "class C { }", out var documentId).Project.Solution);
.AddTestDocument("class C { }", "test.cs", out var documentId).Project.Solution);

var solution = localWorkspace.CurrentSolution;
var project = solution.GetRequiredProject(projectId);
Expand Down Expand Up @@ -156,24 +156,57 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution

// EmitSolutionUpdate

var diagnosticDescriptor1 = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile);
var errorReadingFileDescriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile);
var moduleErrorDescriptor = EditAndContinueDiagnosticDescriptors.GetModuleDiagnosticDescriptor(Contracts.EditAndContinue.ManagedHotReloadAvailabilityStatus.Optimized);
var syntaxErrorDescriptor = new DiagnosticDescriptor("CS0001", "Syntax error", "Syntax error", "Compiler", DiagnosticSeverity.Error, isEnabledByDefault: true);
var compilerHiddenDescriptor = new DiagnosticDescriptor("CS0002", "Hidden", "Emit Hidden", "Compiler", DiagnosticSeverity.Hidden, isEnabledByDefault: true);
var compilerInfoDescriptor = new DiagnosticDescriptor("CS0003", "Info", "Emit Info", "Compiler", DiagnosticSeverity.Info, isEnabledByDefault: true);
var compilerWarningDescriptor = new DiagnosticDescriptor("CS0004", "Emit Warning", "Emit Warning", "Compiler", DiagnosticSeverity.Warning, isEnabledByDefault: true);
var compilerErrorDescriptor = new DiagnosticDescriptor("CS0005", "Emit Error", "Emit Error", "Compiler", DiagnosticSeverity.Error, isEnabledByDefault: true);

mockEncService.EmitSolutionUpdateImpl = (solution, _, _) =>
{
var syntaxTree = solution.GetRequiredDocument(documentId).GetSyntaxTreeSynchronously(CancellationToken.None)!;

var documentDiagnostic = CodeAnalysis.Diagnostic.Create(diagnosticDescriptor1, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)), ["doc", "error 1"]);
var projectDiagnostic = CodeAnalysis.Diagnostic.Create(diagnosticDescriptor1, Location.None, ["proj", "error 2"]);
var syntaxError = CodeAnalysis.Diagnostic.Create(diagnosticDescriptor1, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)), ["doc", "syntax error 3"]);
var documentDiagnostic = CodeAnalysis.Diagnostic.Create(errorReadingFileDescriptor, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)), ["doc", "error 1"]);
var projectDiagnostic = CodeAnalysis.Diagnostic.Create(errorReadingFileDescriptor, Location.None, ["proj", "error 2"]);
var moduleError = CodeAnalysis.Diagnostic.Create(moduleErrorDescriptor, Location.None, ["proj", "module error"]);
var syntaxError = CodeAnalysis.Diagnostic.Create(syntaxErrorDescriptor, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)));
var compilerDocHidden = CodeAnalysis.Diagnostic.Create(compilerHiddenDescriptor, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)));
var compilerDocInfo = CodeAnalysis.Diagnostic.Create(compilerInfoDescriptor, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)));
var compilerDocWarning = CodeAnalysis.Diagnostic.Create(compilerWarningDescriptor, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)));
var compilerDocError = CodeAnalysis.Diagnostic.Create(compilerErrorDescriptor, Location.Create(syntaxTree, TextSpan.FromBounds(1, 2)));
var compilerProjectHidden = CodeAnalysis.Diagnostic.Create(compilerHiddenDescriptor, Location.None);
var compilerProjectInfo = CodeAnalysis.Diagnostic.Create(compilerInfoDescriptor, Location.None);
var compilerProjectWarning = CodeAnalysis.Diagnostic.Create(compilerWarningDescriptor, Location.None);
var compilerProjectError = CodeAnalysis.Diagnostic.Create(compilerErrorDescriptor, Location.None);
var rudeEditDiagnostic = new RudeEditDiagnostic(RudeEditKind.Delete, TextSpan.FromBounds(2, 3), arguments: ["x"]).ToDiagnostic(syntaxTree);
var deletedDocumentRudeEdit = new RudeEditDiagnostic(RudeEditKind.Delete, TextSpan.FromBounds(2, 3), arguments: ["<deleted>"]).ToDiagnostic(tree: null);

return new()
{
Solution = solution,
ModuleUpdates = new ModuleUpdates(ModuleUpdateStatus.Ready, []),
Diagnostics = [new ProjectDiagnostics(project.Id, [documentDiagnostic, projectDiagnostic])],
RudeEdits = [new ProjectDiagnostics(project.Id, [rudeEditDiagnostic, deletedDocumentRudeEdit])],
Diagnostics =
[
new ProjectDiagnostics(
project.Id,
[
documentDiagnostic,
projectDiagnostic,
moduleError,
rudeEditDiagnostic,
deletedDocumentRudeEdit,
compilerDocError,
compilerDocWarning,
compilerDocHidden,
compilerDocInfo,
compilerProjectError,
compilerProjectWarning,
compilerProjectHidden,
compilerProjectInfo,
])
],
SyntaxError = syntaxError,
ProjectsToRebuild = [project.Id],
ProjectsToRestart = ImmutableDictionary<ProjectId, ImmutableArray<ProjectId>>.Empty.Add(project.Id, [])
Expand All @@ -186,18 +219,28 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution

AssertEx.Equal(
[
$"Error ENC0033: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.Deleting_0_requires_restarting_the_application, "<deleted>")}",
$"Error ENC1001: {document.FilePath}(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "error 1")}",
$"Error ENC1001: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}"
$"Error ENC1001: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}",
$"Error ENC2012: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.EditAndContinueDisallowedByProject, "proj", "module error")}",
$"Error ENC0033: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.Deleting_0_requires_restarting_the_application, "<deleted>")}",
$"Error CS0005: {document.FilePath}(0, 1, 0, 2): Emit Error",
$"Warning CS0004: {document.FilePath}(0, 1, 0, 2): Emit Warning",
$"Error CS0005: {project.FilePath}(0, 0, 0, 0): Emit Error",
$"Warning CS0004: {project.FilePath}(0, 0, 0, 0): Emit Warning",
], sessionState.ApplyChangesDiagnostics.Select(Inspect));

AssertEx.Equal(
[
$"Error ENC1001: {document.FilePath}(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "error 1")}",
$"Error ENC1001: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}",
$"Error ENC1001: {document.FilePath}(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "syntax error 3")}",
$"RestartRequired ENC1001: {document.FilePath}(0, 1, 0, 2): {string.Format(FeaturesResources.ErrorReadingFile, "doc", "error 1")}",
$"RestartRequired ENC1001: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.ErrorReadingFile, "proj", "error 2")}",
$"RestartRequired ENC2012: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.EditAndContinueDisallowedByProject, "proj", "module error")}",
$"RestartRequired ENC0033: {document.FilePath}(0, 2, 0, 3): {string.Format(FeaturesResources.Deleting_0_requires_restarting_the_application, "x")}",
$"RestartRequired ENC0033: {project.FilePath}(0, 0, 0, 0): {string.Format(FeaturesResources.Deleting_0_requires_restarting_the_application, "<deleted>")}",
$"Error CS0005: {document.FilePath}(0, 1, 0, 2): Emit Error",
$"Warning CS0004: {document.FilePath}(0, 1, 0, 2): Emit Warning",
$"Error CS0005: {project.FilePath}(0, 0, 0, 0): Emit Error",
$"Warning CS0004: {project.FilePath}(0, 0, 0, 0): Emit Warning",
$"Error CS0001: {document.FilePath}(0, 1, 0, 2): Syntax error",
], updates.Diagnostics.Select(Inspect));

Assert.True(sessionState.IsSessionActive);
Expand Down
26 changes: 6 additions & 20 deletions src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ internal ImmutableList<ProjectBaseline> GetOrCreateEmitBaselines(
Guid moduleId,
Project baselineProject,
Compilation baselineCompilation,
out ImmutableArray<Diagnostic> errors,
ArrayBuilder<Diagnostic> diagnostics,
out ReaderWriterLockSlim baselineAccessLock)
{
baselineAccessLock = _baselineAccessLock;
Expand All @@ -319,17 +319,16 @@ internal ImmutableList<ProjectBaseline> GetOrCreateEmitBaselines(
{
if (TryGetBaselinesContainingModuleVersion(moduleId, out existingBaselines))
{
errors = [];
return existingBaselines;
}
}

var outputs = GetCompilationOutputs(baselineProject);
if (!TryCreateInitialBaseline(baselineCompilation, outputs, baselineProject.Id, out errors, out var initialBaseline, out var debugInfoReaderProvider, out var metadataReaderProvider))
if (!TryCreateInitialBaseline(baselineCompilation, outputs, baselineProject.Id, diagnostics, out var initialBaseline, out var debugInfoReaderProvider, out var metadataReaderProvider))
{
// Unable to read the DLL/PDB at this point (it might be open by another process).
// Don't cache the failure so that the user can attempt to apply changes again.
return existingBaselines ?? [];
return [];
}

lock (_projectEmitBaselinesGuard)
Expand Down Expand Up @@ -359,7 +358,7 @@ private unsafe bool TryCreateInitialBaseline(
Compilation compilation,
CompilationOutputs compilationOutputs,
ProjectId projectId,
out ImmutableArray<Diagnostic> errors,
ArrayBuilder<Diagnostic> diagnostics,
[NotNullWhen(true)] out EmitBaseline? baseline,
[NotNullWhen(true)] out DebugInformationReaderProvider? debugInfoReaderProvider,
[NotNullWhen(true)] out MetadataReaderProvider? metadataReaderProvider)
Expand All @@ -370,7 +369,6 @@ private unsafe bool TryCreateInitialBaseline(
// Alternatively, we could drop the data once we are done with emitting the delta and re-emit the baseline again
// when we need it next time and the module is loaded.

errors = [];
baseline = null;
debugInfoReaderProvider = null;
metadataReaderProvider = null;
Expand Down Expand Up @@ -413,7 +411,7 @@ private unsafe bool TryCreateInitialBaseline(
SessionLog.Write($"Failed to create baseline for '{projectId.DebugName}': {e.Message}", LogMessageSeverity.Error);

var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile);
errors = [Diagnostic.Create(descriptor, Location.None, [fileBeingRead, e.Message])];
diagnostics.Add(Diagnostic.Create(descriptor, Location.None, [fileBeingRead, e.Message]));
}
finally
{
Expand Down Expand Up @@ -552,21 +550,10 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
break;
}

using var _ = ArrayBuilder<ProjectDiagnostics>.GetInstance(out var rudeEditDiagnostics);
foreach (var (projectId, documentsWithRudeEdits) in solutionUpdate.DocumentsWithRudeEdits.GroupBy(static e => e.Id.ProjectId).OrderBy(static id => id))
{
foreach (var documentWithRudeEdits in documentsWithRudeEdits)
{
var document = await solution.GetDocumentAsync(documentWithRudeEdits.Id, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false);
var tree = (document != null) ? await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false) : null;
rudeEditDiagnostics.Add(new(projectId, documentWithRudeEdits.RudeEdits.SelectAsArray(static (rudeEdit, tree) => rudeEdit.ToDiagnostic(tree), tree)));
}
}

EmitSolutionUpdateResults.GetProjectsToRebuildAndRestart(
solution,
solutionUpdate.ModuleUpdates,
rudeEditDiagnostics,
solutionUpdate.Diagnostics,
runningProjects,
out var projectsToRestart,
out var projectsToRebuild);
Expand All @@ -578,7 +565,6 @@ public async ValueTask<EmitSolutionUpdateResults> EmitSolutionUpdateAsync(
Solution = solution,
ModuleUpdates = solutionUpdate.ModuleUpdates,
Diagnostics = solutionUpdate.Diagnostics,
RudeEdits = rudeEditDiagnostics.ToImmutable(),
SyntaxError = solutionUpdate.SyntaxError,
ProjectsToRestart = projectsToRestart,
ProjectsToRebuild = projectsToRebuild
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public static void Log(Data data, Action<FunctionId, LogMessage> log, Func<int>

map["RudeEditsCount"] = editSessionData.RudeEdits.Length;

// Number of emit errors.
// Number of emit errors. These are any errors only produced during emitting deltas and do not include document analysis errors.
map["EmitDeltaErrorIdCount"] = editSessionData.EmitErrorIds.Length;

// False for Hot Reload session, true or missing for EnC session (missing in older data that did not have this property).
Expand Down

This file was deleted.

Loading
Loading