File: BackEnd\Components\RequestBuilder\IntrinsicTasks\ItemGroupLoggingHelper.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
#if FEATURE_APPDOMAIN
using System.Runtime.Remoting;
#endif
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Collections;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
 
#nullable disable
 
namespace Microsoft.Build.BackEnd
{
    /// <summary>
    /// Helper class to convert ItemGroup parameters to a string value for logging.
    /// </summary>
    internal static class ItemGroupLoggingHelper
    {
        /// <summary>
        /// The default character limit for logging parameters. 10k is somewhat arbitrary, see https://github.com/dotnet/msbuild/issues/4907.
        /// </summary>
        internal static int parameterCharacterLimit = 40_000;
 
        /// <summary>
        /// The default parameter limit for logging. 200 is somewhat arbitrary, see https://github.com/dotnet/msbuild/pull/5210.
        /// </summary>
        internal static int parameterLimit = 200;
 
        internal static string ItemGroupIncludeLogMessagePrefix = ResourceUtilities.GetResourceString("ItemGroupIncludeLogMessagePrefix");
        internal static string ItemGroupRemoveLogMessage = ResourceUtilities.GetResourceString("ItemGroupRemoveLogMessage");
        internal static string OutputItemParameterMessagePrefix = ResourceUtilities.GetResourceString("OutputItemParameterMessagePrefix");
        internal static string OutputPropertyLogMessagePrefix = ResourceUtilities.GetResourceString("OutputPropertyLogMessagePrefix");
        internal static string TaskParameterPrefix = ResourceUtilities.GetResourceString("TaskParameterPrefix");
        internal static string SkipTargetUpToDateInputs = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("SkipTargetUpToDateInputs", string.Empty);
        internal static string SkipTargetUpToDateOutputs = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("SkipTargetUpToDateOutputs", string.Empty);
 
        /// <summary>
        /// <see cref="TaskParameterEventArgs"/> by itself doesn't have the implementation
        /// to materialize the Message as that's a declaration assembly. We inject the logic
        /// here.
        /// </summary>
#pragma warning disable CA1810 // Initialize reference type static fields inline
        static ItemGroupLoggingHelper()
#pragma warning restore CA1810 // Initialize reference type static fields inline
        {
            BuildEventArgs.ResourceStringFormatter = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword;
            TaskParameterEventArgs.MessageGetter = GetTaskParameterText;
            TaskParameterEventArgs.DictionaryFactory = ArrayDictionary<string, string>.Create;
        }
 
        /// <summary>
        /// Gets a text serialized value of a parameter for logging.
        /// </summary>
        internal static string GetParameterText(string prefix, string parameterName, IList parameterValue, bool logItemMetadata = true)
        {
            if (parameterValue == null || parameterValue.Count == 0)
            {
                return parameterName;
            }
 
            using (var sb = new ReuseableStringBuilder())
            {
                sb.Append(prefix);
 
                bool firstEntryIsTaskItemWithSomeCustomMetadata = false;
                var firstItem = parameterValue[0] as ITaskItem;
                if (firstItem != null)
                {
                    if (firstItem.CloneCustomMetadata().Count > 0)
                    {
                        firstEntryIsTaskItemWithSomeCustomMetadata = true;
                    }
                }
 
                // If it's just one entry in the list, and it's not a task item with metadata, keep it on one line like a scalar
                bool specialTreatmentForSingle = (parameterValue.Count == 1 && !firstEntryIsTaskItemWithSomeCustomMetadata);
 
                // If the parameterName is not specified, no need to have an extra indent.
                // Without parameterName:
                //
                // Input files:
                //     a.txt
                //     b.txt
                //
                // With parameterName:
                //
                // Input files:
                //     ParamName=
                //         a.txt
                //         b.txt
                string indent = "        ";
                if (parameterName == null)
                {
                    indent = "    ";
                }
 
                if (!specialTreatmentForSingle)
                {
                    sb.Append("\n");
                    if (parameterName != null)
                    {
                        sb.Append("    ");
                    }
                }
 
                if (parameterName != null)
                {
                    sb.Append(parameterName);
                    sb.Append('=');
 
                    if (!specialTreatmentForSingle)
                    {
                        sb.Append("\n");
                    }
                }
 
                bool truncateTaskInputs = Traits.Instance.EscapeHatches.TruncateTaskInputs;
 
                for (int i = 0; i < parameterValue.Count; i++)
                {
                    if (parameterValue[i] == null)
                    {
                        continue;
                    }
 
                    if (!specialTreatmentForSingle)
                    {
                        sb.Append(indent);
                    }
 
                    AppendStringFromParameterValue(sb, parameterValue[i], logItemMetadata);
 
                    if (!specialTreatmentForSingle && i < parameterValue.Count - 1)
                    {
                        sb.Append("\n");
                    }
 
                    if (truncateTaskInputs && (sb.Length >= parameterCharacterLimit || i > parameterLimit))
                    {
                        sb.Append(ResourceUtilities.GetResourceString("LogTaskInputs.Truncated"));
                        break;
                    }
                }
 
                return sb.ToString();
            }
        }
 
        /// <summary>
        /// Given an object wrapping a scalar value that will be set on a task,
        /// returns a suitable string to log its value, with a trailing newline.
        /// First line is already indented.
        /// Indent of any subsequent line should be 12 spaces.
        /// </summary>
        internal static string GetStringFromParameterValue(object parameterValue, bool logItemMetadata = true)
        {
            // fast path for the common case
            if (parameterValue is string valueText)
            {
                return valueText;
            }
 
            using (var sb = new ReuseableStringBuilder())
            {
                AppendStringFromParameterValue(sb, parameterValue, logItemMetadata);
                return sb.ToString();
            }
        }
 
        // Avoid allocating a temporary list to hold metadata for sorting every time.
        // Each thread gets its own copy.
        [ThreadStatic]
        private static List<KeyValuePair<string, string>> keyValuePairList;
 
        private static void AppendStringFromParameterValue(ReuseableStringBuilder sb, object parameterValue, bool logItemMetadata = true)
        {
            if (parameterValue is string text)
            {
                sb.Append(text);
            }
            else if (parameterValue is ITaskItem item)
            {
                sb.Append(item.ItemSpec);
 
                if (!logItemMetadata)
                {
                    return;
                }
 
                var customMetadata = item.CloneCustomMetadata();
                int count = customMetadata.Count;
 
                if (count > 0)
                {
                    sb.Append('\n');
 
                    // need to initialize the thread static on each new thread
                    if (keyValuePairList == null)
                    {
                        keyValuePairList = new List<KeyValuePair<string, string>>(count);
                    }
 
                    if (customMetadata is IDictionary<string, string> customMetadataDictionary)
                    {
                        foreach (KeyValuePair<string, string> kvp in customMetadataDictionary)
                        {
                            keyValuePairList.Add(kvp);
                        }
                    }
                    else
                    {
                        foreach (DictionaryEntry kvp in customMetadata)
                        {
                            keyValuePairList.Add(new KeyValuePair<string, string>((string)kvp.Key, (string)kvp.Value));
                        }
                    }
 
                    if (count > 1)
                    {
                        keyValuePairList.Sort((l, r) => StringComparer.OrdinalIgnoreCase.Compare(l.Key, r.Key));
                    }
 
                    for (int i = 0; i < count; i++)
                    {
                        var kvp = keyValuePairList[i];
                        sb.Append("                ");
                        sb.Append(kvp.Key);
                        sb.Append('=');
                        sb.Append(kvp.Value);
 
                        if (i < count - 1)
                        {
                            sb.Append('\n');
                        }
                    }
 
                    keyValuePairList.Clear();
                }
            }
            else if (parameterValue.GetType().IsValueType)
            {
                sb.Append((string)Convert.ChangeType(parameterValue, typeof(string), CultureInfo.CurrentCulture));
            }
            else
            {
                ErrorUtilities.ThrowInternalErrorUnreachable();
            }
        }
 
        internal static void LogTaskParameter(
            LoggingContext loggingContext,
            TaskParameterMessageKind messageKind,
            string parameterName,
            string propertyName,
            string itemType,
            IList items,
            bool logItemMetadata,
            IElementLocation location = null)
        {
            var args = CreateTaskParameterEventArgs(
                loggingContext.BuildEventContext,
                messageKind,
                parameterName,
                propertyName,
                itemType,
                items,
                logItemMetadata,
                DateTime.UtcNow,
                location?.Line ?? 0,
                location?.Column ?? 0);
 
            loggingContext.LogBuildEvent(args);
        }
 
        internal static TaskParameterEventArgs CreateTaskParameterEventArgs(
            BuildEventContext buildEventContext,
            TaskParameterMessageKind messageKind,
            string parameterName,
            string propertyName,
            string itemType,
            IList items,
            bool logItemMetadata,
            DateTime timestamp,
            int line = 0,
            int column = 0)
        {
            // Only create a snapshot of items if we use AppDomains
#if FEATURE_APPDOMAIN
            CreateItemsSnapshot(ref items);
#endif
 
            var args = new TaskParameterEventArgs(
                messageKind,
                parameterName,
                propertyName,
                itemType,
                items,
                logItemMetadata,
                timestamp);
            args.BuildEventContext = buildEventContext;
            args.LineNumber = line;
            args.ColumnNumber = column;
            return args;
        }
 
#if FEATURE_APPDOMAIN
        private static void CreateItemsSnapshot(ref IList items)
        {
            if (items == null)
            {
                return;
            }
 
            // If we're in the default AppDomain, but any of the items come from a different AppDomain
            // we need to take a snapshot of the items right now otherwise that AppDomain might get
            // unloaded by the time we want to consume the items.
            // If we're not in the default AppDomain, always take the items snapshot.
            //
            // It is unfortunate to need to be doing this check, but ResolveComReference and other tasks
            // still use AppDomains and create a TaskParameterEventArgs in the default AppDomain, but
            // pass it Items from another AppDomain.
            if (AppDomain.CurrentDomain.IsDefaultAppDomain())
            {
                bool needsSnapshot = false;
                foreach (var item in items)
                {
                    if (RemotingServices.IsTransparentProxy(item))
                    {
                        needsSnapshot = true;
                        break;
                    }
                }
 
                if (!needsSnapshot)
                {
                    return;
                }
            }
 
            int count = items.Count;
            var cloned = new object[count];
 
            for (int i = 0; i < count; i++)
            {
                var item = items[i];
                if (item is ITaskItem taskItem)
                {
                    cloned[i] = new TaskItemData(taskItem);
                }
                else
                {
                    cloned[i] = item;
                }
            }
 
            items = cloned;
        }
#endif
 
        internal static string GetTaskParameterText(TaskParameterEventArgs args)
        {
            var resourceText = args.Kind switch
            {
                TaskParameterMessageKind.AddItem => ItemGroupIncludeLogMessagePrefix,
                TaskParameterMessageKind.RemoveItem => ItemGroupRemoveLogMessage,
                TaskParameterMessageKind.TaskInput => TaskParameterPrefix,
                TaskParameterMessageKind.TaskOutput => args.PropertyName is null ? OutputItemParameterMessagePrefix : OutputPropertyLogMessagePrefix,
                TaskParameterMessageKind.SkippedTargetInputs => SkipTargetUpToDateInputs,
                TaskParameterMessageKind.SkippedTargetOutputs => SkipTargetUpToDateOutputs,
                _ => throw new NotImplementedException($"Unsupported {nameof(TaskParameterMessageKind)} value: {args.Kind}")
            };
 
            var itemGroupText = GetParameterText(
                resourceText,
                args.PropertyName ?? args.ItemType,
                args.Items,
                args.LogItemMetadata);
            return itemGroupText;
        }
    }
}