File: RestoreCommand\Logging\WarningPropertiesCollection.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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using NuGet.Common;
using NuGet.ProjectModel;
using NuGet.Shared;

namespace NuGet.Commands
{
    /// <summary>
    /// Class to hold ProjectWide and PackageSpecific WarningProperties.
    /// </summary>
    internal class WarningPropertiesCollection : IEquatable<WarningPropertiesCollection>
    {

        private readonly ConcurrentDictionary<string, string> _getTargetGraphAliasCache = new ConcurrentDictionary<string, string>();

        /// <summary>
        /// Contains the target frameworks for the project.
        /// These are used for no warn filtering in case of a log message without a target graph.
        /// </summary>
        public IReadOnlyList<string> ProjectFrameworks { get; }

        /// <summary>
        /// Contains Project wide properties for Warnings.
        /// </summary>
        public WarningProperties ProjectWideWarningProperties { get; }

        /// <summary>
        /// Contains Package specific properties for Warnings.
        /// NuGetLogCode -> LibraryId -> Set of Frameworks.
        /// </summary>
        public PackageSpecificWarningProperties PackageSpecificWarningProperties { get; }

        public WarningPropertiesCollection(WarningProperties projectWideWarningProperties,
            PackageSpecificWarningProperties packageSpecificWarningProperties,
            IReadOnlyList<string> projectFrameworks)
        {
            ProjectWideWarningProperties = projectWideWarningProperties;
            PackageSpecificWarningProperties = packageSpecificWarningProperties;
            ProjectFrameworks = projectFrameworks ?? new ReadOnlyCollection<string>(new List<string>());
        }

        /// <summary>
        /// Attempts to suppress a warning log message or upgrade it to error log message.
        /// The decision is made based on the Package Specific or Project wide warning properties.
        /// </summary>
        /// <param name="message">Message that should be suppressed or upgraded to an error.</param>
        /// <returns>Bool indicating is the warning should be suppressed or not. 
        /// If not then the param message should have been mutated to an error</returns>
        public bool ApplyWarningProperties(IRestoreLogMessage message)
        {
            if (ApplyProjectWideNoWarnProperties(message, ProjectWideWarningProperties) || ApplyPackageSpecificNoWarnProperties(message))
            {
                return true;
            }
            else
            {
                ApplyWarningAsErrorProperties(message);
                return false;
            }
        }

        /// <summary>
        /// Attempts to suppress a warning log message.
        /// The decision is made based on the Package Specific or Project wide no warn properties.
        /// </summary>
        /// <param name="message">Message that should be suppressed.</param>
        /// <returns>Bool indicating is the warning should be suppressed or not.</returns>
        public bool ApplyNoWarnProperties(IRestoreLogMessage message)
        {
            return ApplyProjectWideNoWarnProperties(message, ProjectWideWarningProperties) || ApplyPackageSpecificNoWarnProperties(message);
        }

        /// <summary>
        /// Method is used to upgrade a warning to an error if needed.
        /// </summary>
        /// <param name="message">Message which should be upgraded to error if needed.</param>
        public void ApplyWarningAsErrorProperties(IRestoreLogMessage message)
        {
            ApplyProjectWideWarningsAsErrorProperties(message, ProjectWideWarningProperties);
        }

        /// <summary>
        /// Method is used to check is a warning should be suppressed due to package specific no warn properties.
        /// </summary>
        /// <param name="message">Message to be checked for no warn.</param>
        /// <returns>bool indicating if the IRestoreLogMessage should be suppressed or not.</returns>
        private bool ApplyPackageSpecificNoWarnProperties(IRestoreLogMessage message)
        {
            if (message.Level == LogLevel.Warning &&
                PackageSpecificWarningProperties != null &&
                !string.IsNullOrEmpty(message.LibraryId))
            {
                // If the message does not contain a target graph, assume that it is applicable for all project frameworks.
                if (!message.TargetGraphs.Any())
                {
                    // Suppress the warning if the code + libraryId combination is suppressed for all project frameworks.
                    if (ProjectFrameworks.Count > 0 &&
                        ProjectFrameworks.All(e => PackageSpecificWarningProperties.Contains(message.Code, message.LibraryId, e)))
                    {
                        return true;
                    }
                }
                else
                {
                    bool legacy = ProjectFrameworks.Count == 1 && string.IsNullOrEmpty(ProjectFrameworks[0]);

                    // Get all the target graphs for which code + libraryId combination is not suppressed.
                    message.TargetGraphs = message
                        .TargetGraphs
                        .Where(e => !PackageSpecificWarningProperties.Contains(message.Code, message.LibraryId, legacy ? string.Empty : GetTargetGraphAlias(e)))
                        .ToList();

                    // If the message is left with no target graphs then suppress it.
                    if (message.TargetGraphs.Count == 0)
                    {
                        return true;
                    }
                }
            }

            // The message is not a warning or it does not contain a LibraryId or it is not suppressed in package specific settings.
            return false;
        }

        /// <summary>
        /// Method is used to check is a warning should be suppressed due to project wide no warn properties.
        /// </summary>
        /// <param name="message">Message to be checked for no warn.</param>
        /// <returns>bool indicating if the ILogMessage should be suppressed or not.</returns>
        public static bool ApplyProjectWideNoWarnProperties(ILogMessage message, WarningProperties warningProperties)
        {
            if (message.Level == LogLevel.Warning && warningProperties != null)
            {
                if (warningProperties.NoWarn.Contains(message.Code))
                {
                    // If the project wide NoWarn contains the message code then suppress it.
                    return true;
                }
            }

            // the project wide NoWarn does contain the message code. do not suppress the warning.
            return false;
        }

        /// <summary>
        /// Method is used to check is a warning should be treated as an error.
        /// </summary>
        /// <param name="message">Message which should be upgraded to error if needed.</param>
        public static void ApplyProjectWideWarningsAsErrorProperties(ILogMessage message, WarningProperties warningProperties)
        {
            if (message is null)
            {
                throw new ArgumentNullException(nameof(message));
            }

            if (message.Level == LogLevel.Warning && warningProperties != null)
            {
                if ((warningProperties.AllWarningsAsErrors && message.Code > NuGetLogCode.Undefined && !warningProperties.WarningsNotAsErrors.Contains(message.Code)) ||
                    warningProperties.WarningsAsErrors.Contains(message.Code))
                {
                    // If the project wide AllWarningsAsErrors is true, the message has a valid code and warning not as error is not enabled or
                    // Project wide WarningsAsErrors contains the message code then upgrade to error.
                    message.Message = string.Format(CultureInfo.CurrentCulture, Strings.WarningAsError, message.Message);
                    message.Level = LogLevel.Error;
                }
            }
        }

        private string GetTargetGraphAlias(string targetGraph)
        {
            return _getTargetGraphAliasCache.GetOrAdd(targetGraph, (s) => s.Split('/')[0]);
        }

        public override int GetHashCode()
        {
            var hashCode = new HashCodeCombiner();

            hashCode.AddObject(ProjectWideWarningProperties);
            hashCode.AddObject(PackageSpecificWarningProperties);
            hashCode.AddSequence(ProjectFrameworks);

            return hashCode.CombinedHash;
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as WarningPropertiesCollection);
        }

        public bool Equals(WarningPropertiesCollection other)
        {
            if (other == null)
            {
                return false;
            }

            if (ReferenceEquals(this, other))
            {
                return true;
            }

            return EqualityUtility.EqualsWithNullCheck(ProjectWideWarningProperties, other.ProjectWideWarningProperties) &&
                EqualityUtility.EqualsWithNullCheck(PackageSpecificWarningProperties, other.PackageSpecificWarningProperties) &&
                EqualityUtility.OrderedEquals(ProjectFrameworks, other.ProjectFrameworks, (fx) => fx, orderComparer: StringComparer.OrdinalIgnoreCase, StringComparer.OrdinalIgnoreCase);
        }
    }
}