using System; using System.Collections.Generic; using System.Diagnostics; using Uigenx.Protocol; using UnityEditor; using UnityEditor.Compilation; namespace Uigenx.Editor { /// /// Collects compiler diagnostics for the whole compilation and reports one `compile.result` /// when it finishes. Diagnostics arrive per assembly, so they are accumulated in between. /// [InitializeOnLoad] public static class UgxCompileReporter { /// Diagnostics kept per compilation. Beyond this only the count is reported. public const int MaxDiagnostics = 200; /// A compilation ends in a domain reload, so these two keys carry state across it. const string RequestKey = "Uigenx.Editor.Compile.RequestId"; const string ResultKey = "Uigenx.Editor.Compile.PendingResult"; static readonly List Errors = new List(16); static readonly List Warnings = new List(16); static Stopwatch _stopwatch; /// Raised on the main thread when a compilation finishes. public static event Action Reported; static UgxCompileReporter() { CompilationPipeline.compilationStarted += OnCompilationStarted; CompilationPipeline.assemblyCompilationFinished += OnAssemblyFinished; CompilationPipeline.compilationFinished += OnCompilationFinished; } /// Correlates the next `compile.result` with a `compile.request`. public static void ExpectRequest(string requestId) => SessionState.SetString(RequestKey, requestId ?? string.Empty); /// /// Takes the result of a compilation that finished while nothing was listening, which is /// what happens when the bridge disconnects for the domain reload the compilation triggers. /// public static bool TryTakePending(out UgxCompileResult result) { result = null; var json = SessionState.GetString(ResultKey, string.Empty); if (string.IsNullOrEmpty(json)) return false; SessionState.EraseString(ResultKey); return UgxJson.TryDeserialize(json, out result, out _) && result != null; } /// Reports the last known state without waiting for a compilation. public static UgxCompileResult Snapshot(string requestId = null) => new UgxCompileResult { RequestId = requestId, Ok = Errors.Count == 0 && !EditorUtility.scriptCompilationFailed, Errors = new List(Errors), Warnings = new List(Warnings), DurationMs = 0, }; static void OnCompilationStarted(object context) { Errors.Clear(); Warnings.Clear(); _stopwatch = Stopwatch.StartNew(); } static void OnAssemblyFinished(string assemblyPath, CompilerMessage[] messages) { if (messages == null) return; foreach (var message in messages) { var target = message.type == CompilerMessageType.Error ? Errors : Warnings; if (message.type == CompilerMessageType.Info) continue; if (target.Count >= MaxDiagnostics) continue; target.Add(new UgxCompileDiagnostic { File = message.file ?? string.Empty, Line = message.line, Column = message.column, Code = ExtractCode(message.message), Message = message.message ?? string.Empty, }); } } static void OnCompilationFinished(object context) { var requestId = SessionState.GetString(RequestKey, string.Empty); var result = new UgxCompileResult { RequestId = string.IsNullOrEmpty(requestId) ? null : requestId, Ok = Errors.Count == 0, Errors = new List(Errors), Warnings = new List(Warnings), DurationMs = _stopwatch?.Elapsed.TotalMilliseconds ?? 0d, }; SessionState.EraseString(RequestKey); _stopwatch = null; // The reload that follows this callback tears the bridge down, so the result is parked // for the reconnect rather than dropped with the socket. SessionState.SetString(ResultKey, UgxJson.Serialize(result)); Reported?.Invoke(result); } /// Pulls `CS0246` out of `error CS0246: The type or namespace ...`. public static string ExtractCode(string message) { if (string.IsNullOrEmpty(message)) return string.Empty; int cs = message.IndexOf("CS", StringComparison.Ordinal); if (cs < 0) return string.Empty; int end = cs + 2; while (end < message.Length && char.IsDigit(message[end])) end++; return end > cs + 2 ? message.Substring(cs, end - cs) : string.Empty; } } }