File: RestoreCommand\Logging\RestoreCollectorLogger.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Commands\NuGet.Commands.csproj (NuGet.Commands)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Packaging.Signing;
using NuGet.ProjectModel;
using NuGet.Shared;

namespace NuGet.Commands
{
    public class RestoreCollectorLogger : LoggerBase, ICollectorLogger
    {
        private readonly ILogger _innerLogger;
        private readonly ConcurrentQueue<IRestoreLogMessage> _errors;
        private readonly ConcurrentQueue<IRestoreLogMessage> _suppressedWarnings;

        private readonly bool _hideWarningsAndErrors;
        private IEnumerable<RestoreTargetGraph> _restoreTargetGraphs;
        private PackageSpec _projectSpec;
        private WarningPropertiesCollection _transitiveWarningPropertiesCollection;

        public string ProjectPath => _projectSpec?.RestoreMetadata?.ProjectPath;

        public IEnumerable<IRestoreLogMessage> Errors => _errors.ToArray();
        internal IEnumerable<IRestoreLogMessage> SuppressedWarnings => _suppressedWarnings.ToArray();


        internal WarningPropertiesCollection ProjectWarningPropertiesCollection { get; set; }

        internal WarningPropertiesCollection TransitiveWarningPropertiesCollection
        {
            get
            {
                if (_transitiveWarningPropertiesCollection == null)
                {
                    // Populate TransitiveWarningPropertiesCollection only if it is null and we have RestoreTargetGraphs.
                    // This will happen at most once and only if we have the project spec with restore metadata.
                    if (_restoreTargetGraphs != null &&
                        _restoreTargetGraphs.Any() &&
                        _projectSpec != null &&
                        _projectSpec.RestoreMetadata != null)
                    {
                        TransitiveWarningPropertiesCollection = TransitiveNoWarnUtils.CreateTransitiveWarningPropertiesCollection(
                            _restoreTargetGraphs,
                            _projectSpec);
                    }
                }

                return _transitiveWarningPropertiesCollection;
            }

            set => _transitiveWarningPropertiesCollection = value;
        }

        /// <summary>
        /// Stores a reference to PackageSpec for the project from the restore request.
        /// This are used to generate the warning properties for the project.
        /// </summary>
        /// <param name="projectSpec">PackageSpec to be stored for reference.</param>
        public void ApplyRestoreInputs(PackageSpec projectSpec)
        {
            _projectSpec = projectSpec;

            ProjectWarningPropertiesCollection = new WarningPropertiesCollection(
                projectSpec.RestoreMetadata?.ProjectWideWarningProperties,
                PackageSpecificWarningProperties.CreatePackageSpecificWarningProperties(projectSpec),
                projectSpec.TargetFrameworks.Select(f => f.TargetAlias).AsList().AsReadOnly()
                );
        }

        /// <summary>
        /// Stores a reference to RestoreTargetGraphs from the restore output.
        /// These graphs are used to generate the transitive warning properties.
        /// </summary>
        /// <param name="restoreTargetGraphs">RestoreTargetGraphs to be stored for reference.</param>
        public void ApplyRestoreOutput(IEnumerable<RestoreTargetGraph> restoreTargetGraphs)
        {
            _restoreTargetGraphs = restoreTargetGraphs;
        }

        /// <summary>
        /// Initializes an instance of the <see cref="RestoreCollectorLogger"/>, while still
        /// delegating all log messages to the inner logger.
        /// </summary>
        /// <param name="innerLogger">The inner logger used to delegate the logging.</param>
        /// <param name="verbosity">Minimum verbosity below which no logs will be passed to the inner logger.</param>
        /// <param name="hideWarningsAndErrors">If this is true, then errors and warnings will not be passed to inner logger.</param>
        public RestoreCollectorLogger(ILogger innerLogger, LogLevel verbosity, bool hideWarningsAndErrors)
            : base(verbosity)
        {
            _innerLogger = innerLogger;
            _errors = new ConcurrentQueue<IRestoreLogMessage>();
            _suppressedWarnings = new ConcurrentQueue<IRestoreLogMessage>();
            _hideWarningsAndErrors = hideWarningsAndErrors;
        }

        /// <summary>
        /// Initializes an instance of the <see cref="RestoreCollectorLogger"/>, while still
        /// delegating all log messages to the inner logger.
        /// </summary>
        /// <param name="innerLogger">The inner logger used to delegate the logging.</param>
        /// <param name="hideWarningsAndErrors">If this is false, then errors and warnings will not be passed to inner logger.</param>
        public RestoreCollectorLogger(ILogger innerLogger, bool hideWarningsAndErrors)
            : this(innerLogger, LogLevel.Debug, hideWarningsAndErrors)
        {
        }

        /// <summary>
        /// Initializes an instance of the <see cref="RestoreCollectorLogger"/>, while still
        /// delegating all log messages to the inner logger.
        /// </summary>
        /// <param name="innerLogger">The inner logger used to delegate the logging.</param>
        /// <param name="verbosity">Minimum verbosity below which no logs will be passed to the inner logger.</param>
        public RestoreCollectorLogger(ILogger innerLogger, LogLevel verbosity)
            : this(innerLogger, verbosity, hideWarningsAndErrors: false)
        {
        }

        /// <summary>
        /// Initializes an instance of the <see cref="RestoreCollectorLogger"/>, while still
        /// delegating all log messages to the inner logger.
        /// </summary>
        /// <param name="innerLogger">The inner logger used to delegate the logging.</param>
        public RestoreCollectorLogger(ILogger innerLogger)
            : this(innerLogger, LogLevel.Debug, hideWarningsAndErrors: false)
        {
        }

        public void Log(IRestoreLogMessage message)
        {
            // check if the message is a warning and it is suppressed 
            if (!IsWarningSuppressed(message))
            {
                // if the message is not suppressed then check if it needs to be upgraded to an error
                UpgradeWarningToErrorIfNeeded(message);

                if (string.IsNullOrEmpty(message.ProjectPath))
                {
                    message.ProjectPath = ProjectPath;
                }

                if (string.IsNullOrEmpty(message.FilePath))
                {
                    message.FilePath = message.ProjectPath;
                }

                if (CollectMessage(message.Level))
                {
                    _errors.Enqueue(message);
                }

                if (DisplayMessage(message))
                {
                    _innerLogger.Log(message);
                }
            }
        }

        public Task LogAsync(IRestoreLogMessage message)
        {
            // check if the message is a warning and it is suppressed 
            if (!IsWarningSuppressed(message))
            {
                // if the message is not suppressed then check if it needs to be upgraded to an error
                UpgradeWarningToErrorIfNeeded(message);

                if (string.IsNullOrEmpty(message.FilePath))
                {
                    message.FilePath = message.ProjectPath ?? ProjectPath;
                }

                if (string.IsNullOrEmpty(message.ProjectPath))
                {
                    message.ProjectPath = ProjectPath;
                }

                if (CollectMessage(message.Level))
                {
                    _errors.Enqueue(message);
                }

                if (DisplayMessage(message))
                {
                    return _innerLogger.LogAsync(message);
                }
            }

            return Task.CompletedTask;
        }

        public override void Log(ILogMessage message)
        {
            Log(ToRestoreLogMessage(message));
        }

        public override Task LogAsync(ILogMessage message)
        {
            return LogAsync(ToRestoreLogMessage(message));
        }

        /// <summary>
        /// Decides if the log should be passed to the inner logger.
        /// </summary>
        /// <param name="message">IRestoreLogMessage to be logged.</param>
        /// <returns>bool indicating if this message should be logged.</returns>
        protected bool DisplayMessage(IRestoreLogMessage message)
        {
            if (message.Level == LogLevel.Error || message.Level == LogLevel.Warning)
            {
                return ((!_hideWarningsAndErrors || message.ShouldDisplay) && message.Level >= VerbosityLevel);
            }
            else
            {
                return (message.Level >= VerbosityLevel);
            }
        }

        /// <summary>
        /// This method checks if at least one of the warning properties collections is not null and it suppresses the warning.
        /// </summary>
        /// <param name="message">IRestoreLogMessage to be logged.</param>
        /// <returns>bool indicating if the message should be suppressed.</returns>
        private bool IsWarningSuppressed(IRestoreLogMessage message)
        {
            var isWarningSuppressed = false;
            if (message.Level == LogLevel.Warning)
            {
                // If the ProjectWarningPropertiesCollection is present then test if the warning is suppressed in
                // project wide no warn or package specific no warn
                if (ProjectWarningPropertiesCollection?.ApplyNoWarnProperties(message) == true)
                {
                    isWarningSuppressed = true;
                }
                else
                {
                    // Use transitive warning properties only if the project does not suppress the warning
                    // In transitive warning properties look at only the package specific ones as all properties are per package reference.
                    isWarningSuppressed = TransitiveWarningPropertiesCollection?.ApplyNoWarnProperties(message) == true;
                }
            }

            if (isWarningSuppressed)
            {
                _suppressedWarnings.Enqueue(message);
            }

            return isWarningSuppressed;
        }

        /// <summary>
        /// This method upgrades the warning to an error if the project wide warning properties have set the code in WarningsAsErrors or
        /// set TreatWarningsAsErrors to true
        /// </summary>
        /// <param name="message">IRestoreLogMessage to be logged as an error or warning.</param>
        /// <returns>bool indicating if the message should be suppressed.</returns>
        private void UpgradeWarningToErrorIfNeeded(IRestoreLogMessage message)
        {
            ProjectWarningPropertiesCollection?.ApplyWarningAsErrorProperties(message);
        }

        private static IRestoreLogMessage ToRestoreLogMessage(ILogMessage message)
        {
            if (message is IRestoreLogMessage restoreLogMessage)
            {
                return restoreLogMessage;
            }

            if (message is SignatureLog signatureLog)
            {
                return signatureLog.AsRestoreLogMessage();
            }

            return new RestoreLogMessage(message.Level, message.Code, message.Message)
            {
                Time = message.Time,
                WarningLevel = message.WarningLevel,
                ProjectPath = message.ProjectPath
            };
        }
    }
}