Editor diagnostics helper. Computes live compiler diagnostics for a code snapshot using Roslyn, filters warnings considered "noise" by comparing saved-file diagnostics to s&box compile results, and returns diagnostics to show while editing.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
/// <summary>
/// An error or warning found while typing. Lines and columns are zero-based.
/// </summary>
public record LiveDiagnostic( int Line, int Column, int EndLine, int EndColumn, bool IsError, string Id, string Message )
{
/// <summary>
/// What makes two diagnostics "the same problem" when line numbers shift.
/// </summary>
public string Key => $"{Id}|{Message}";
}
/// <summary>
/// The result of checking one file: what to underline, and what's known to be noise.
/// </summary>
public record LiveCheckResult( IReadOnlyList<LiveDiagnostic> Diagnostics, HashSet<string> Noise );
/// <summary>
/// Errors and warnings for the file being edited, straight from HotCode's own compilation, so they show up
/// while typing instead of after s&box's next compile.
///
/// HotCode's compilation copies s&box's but can't run everything s&box does (source generators, Razor), so it
/// can report problems that s&box wouldn't. To hide those, the saved file is checked too and compared with
/// s&box's real results for it: anything only HotCode reports there is "noise" and gets filtered out while typing.
/// </summary>
public static class DiagnosticsProvider
{
/// <summary>
/// Checks the snapshot's file. Safe to call off the main thread.
/// </summary>
/// <param name="snapshot">The code as it is now (with unsaved edits).</param>
/// <param name="savedSnapshot">The same, but with this file as saved on disk; null to skip working out noise.</param>
/// <param name="sboxProblems">s&box's problems for this file from its last compile (Id, Line), or null if s&box isn't compiling it.</param>
/// <param name="knownNoise">Noise worked out earlier; reused when there's no saved snapshot to compare.</param>
public static LiveCheckResult Check( RoslynProject.Snapshot snapshot, RoslynProject.Snapshot savedSnapshot,
IReadOnlyList<(string Id, int Line)> sboxProblems, HashSet<string> knownNoise )
{
try
{
var noise = knownNoise;
if ( noise is null && savedSnapshot is not null && sboxProblems is not null )
noise = FindNoise( savedSnapshot, sboxProblems );
var current = Get( snapshot ) ?? new List<LiveDiagnostic>();
IEnumerable<LiveDiagnostic> shown = current;
if ( noise is not null ) shown = shown.Where( d => !noise.Contains( d.Key ) );
// Without s&box's results to compare against, only trust errors; warnings are more likely to differ
if ( sboxProblems is null ) shown = shown.Where( d => d.IsError );
return new LiveCheckResult( shown.ToList(), noise );
}
catch ( Exception )
{
return new LiveCheckResult( Array.Empty<LiveDiagnostic>(), knownNoise );
}
}
/// <summary>
/// Problems HotCode finds in the saved file that s&box's compile didn't report.
/// </summary>
private static HashSet<string> FindNoise( RoslynProject.Snapshot saved, IReadOnlyList<(string Id, int Line)> sboxProblems )
{
var noise = new HashSet<string>();
var mine = Get( saved );
if ( mine is null ) return noise;
foreach ( var d in mine )
{
// Matched if s&box reported the same error id within a couple of lines
var reportedBySbox = sboxProblems.Any( p => p.Id == d.Id && Math.Abs( p.Line - d.Line ) <= 2 );
if ( !reportedBySbox ) noise.Add( d.Key );
}
return noise;
}
/// <summary>
/// All errors and warnings in the snapshot's file, or null if it isn't part of the compilation.
/// </summary>
public static List<LiveDiagnostic> Get( RoslynProject.Snapshot snapshot )
{
var (compilation, tree) = RoslynProject.GetCompilation( snapshot );
if ( tree is null ) return null;
var model = compilation.GetSemanticModel( tree );
return model.GetDiagnostics()
.Where( d => d.Severity >= DiagnosticSeverity.Warning && !d.IsSuppressed && d.Location.IsInSource && d.Location.SourceTree == tree )
.Select( d =>
{
var span = d.Location.GetLineSpan();
return new LiveDiagnostic( span.StartLinePosition.Line, span.StartLinePosition.Character,
span.EndLinePosition.Line, span.EndLinePosition.Character,
d.Severity == DiagnosticSeverity.Error, d.Id, d.GetMessage() );
} )
.ToList();
}
}