File: MSBuild\Logging\MSBuildDiagnosticLogger.cs
Web Access
Project: src\roslyn\src\Workspaces\MSBuild\BuildHost\Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.csproj (Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost)
// 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.

using System.Collections.Concurrent;
using System.Diagnostics;
using Roslyn.Utilities;
using MSB = Microsoft.Build;

namespace Microsoft.CodeAnalysis.MSBuild;

internal sealed class MSBuildDiagnosticLogger : MSB.Framework.ILogger
{
    /// <summary>
    /// Maps a build's <see cref="MSB.Framework.BuildEventContext.SubmissionId"/> to the
    /// <see cref="DiagnosticLog"/> that should receive the errors and warnings raised while that
    /// submission is building. This lets a single logger instance route events to the correct project
    /// even when multiple builds are in flight. The id matches
    /// <see cref="MSB.Execution.BuildSubmission.SubmissionId"/>.
    /// </summary>
    private readonly ConcurrentDictionary<int, DiagnosticLog> _logsBySubmissionId = new();

    private MSB.Framework.IEventSource? _eventSource;

    public string? Parameters { get; set; }
    public MSB.Framework.LoggerVerbosity Verbosity { get; set; }

    /// <summary>
    /// Registers the <paramref name="log"/> that should receive diagnostics for the build submission with
    /// the given <paramref name="submissionId"/>. Must be called after the submission's id has been
    /// assigned (i.e. after <see cref="MSB.Execution.BuildManager.PendBuildRequest(MSB.Execution.BuildRequestData)"/>)
    /// but before the submission starts executing, so no event can arrive before the log is registered.
    /// </summary>
    public void RegisterLog(int submissionId, DiagnosticLog log)
        => Contract.ThrowIfFalse(_logsBySubmissionId.TryAdd(submissionId, log), $"A log is already registered for submission {submissionId}.");

    public bool TryUnregisterLog(int submissionId)
        => _logsBySubmissionId.TryRemove(submissionId, out _);

    private void OnErrorRaised(object sender, MSB.Framework.BuildErrorEventArgs e)
        => AddLogItem(DiagnosticLogItemKind.Error, e.BuildEventContext, e.ProjectFile, e.Message, e.File, e.LineNumber, e.ColumnNumber);

    private void OnWarningRaised(object sender, MSB.Framework.BuildWarningEventArgs e)
        => AddLogItem(DiagnosticLogItemKind.Warning, e.BuildEventContext, e.ProjectFile, e.Message, e.File, e.LineNumber, e.ColumnNumber);

    private void AddLogItem(DiagnosticLogItemKind kind, MSB.Framework.BuildEventContext? buildEventContext, string? projectFile, string? message, string? file, int lineNumber, int columnNumber)
    {
        // A build error or warning is always raised in the context of a building submission whose log we
        // registered before executing it, so we expect a context and a log registered for its submission.
        Debug.Assert(buildEventContext != null);
        if (buildEventContext is null)
            return;

        if (!_logsBySubmissionId.TryGetValue(buildEventContext.SubmissionId, out var log))
        {
            // We don't expect this, but if it happens there's no log to attribute the event to, so ignore it.
            Debug.Fail($"No log is registered for submission {buildEventContext.SubmissionId}.");
            return;
        }

        log.Add(new MSBuildDiagnosticLogItem(kind, projectFile ?? "", message ?? "", file ?? "", lineNumber, columnNumber));
    }

    public void Initialize(MSB.Framework.IEventSource eventSource)
    {
        Debug.Assert(_eventSource == null);

        _eventSource = eventSource;
        _eventSource.ErrorRaised += OnErrorRaised;
        _eventSource.WarningRaised += OnWarningRaised;
    }

    public void Shutdown()
    {
        if (_eventSource != null)
        {
            _eventSource.ErrorRaised -= OnErrorRaised;
            _eventSource.WarningRaised -= OnWarningRaised;

            _eventSource = null;

            _logsBySubmissionId.Clear();
        }
    }
}