File: ErrorUtilities.cs
Web Access
Project: src\msbuild\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework)
// 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.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
using Microsoft.Build.Framework.Utilities;

namespace Microsoft.Build.Framework;

// TODO: this should be unified with Shared\ErrorUtilities.cs, but it is hard to untangle everything
//       because some of the errors there will use localized resources from different assemblies,
//       which won't be referenceable in Framework.

internal static class FrameworkErrorUtilities
{
    private static readonly bool s_enableMSBuildDebugTracing = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDENABLEDEBUGTRACING"));

    public static void DebugTraceMessage(string category, string message)
    {
        if (s_enableMSBuildDebugTracing)
        {
            Trace.WriteLine(message, category);
        }
    }

    public static void DebugTraceMessage(string category, ref DebugTraceInterpolatedStringHandler handler)
    {
        if (s_enableMSBuildDebugTracing)
        {
            Trace.WriteLine(handler.GetFormattedText(), category);
        }
    }

    /// <summary>
    ///  Interpolated string handler used by <see cref="DebugTraceMessage(string, ref DebugTraceInterpolatedStringHandler)"/>
    ///  to defer string formatting unless tracing is enabled.
    /// </summary>
    [InterpolatedStringHandler]
    public ref struct DebugTraceInterpolatedStringHandler
    {
        private StringBuilderHelper _builder;

        public DebugTraceInterpolatedStringHandler(int literalLength, int formattedCount, out bool isEnabled)
        {
            isEnabled = s_enableMSBuildDebugTracing;
            _builder = isEnabled ? new(literalLength) : default;
        }

        public readonly void AppendLiteral(string value)
            => _builder.AppendLiteral(value);

        public readonly void AppendFormatted<TValue>(TValue value)
            => _builder.AppendFormatted(value);

        public readonly void AppendFormatted<TValue>(TValue value, string format)
            where TValue : IFormattable
            => _builder.AppendFormatted(value, format);

        public string GetFormattedText()
            => _builder.GetFormattedText();
    }

    /// <summary>
    /// Helper to throw an InternalErrorException when the specified parameter is not a rooted path.
    /// This should be used ONLY if this would indicate a bug in MSBuild rather than
    /// anything caused by user action.
    /// </summary>
    /// <param name="value">Parameter that should be a rooted path.</param>
    public static void VerifyThrowInternalRooted(string value)
    {
        if (!Path.IsPathRooted(value))
        {
            InternalError.Throw($"{value} unexpectedly not a rooted path");
        }
    }

    /// <summary>
    ///  A utility that verifies the parameters provided to a standard <see cref="ICollection{T}.CopyTo"/> call.
    /// </summary>
    /// <typeparam name="T">The element type of the collection.</typeparam>
    /// <param name="collection">The destination collection to copy into.</param>
    /// <param name="index">The zero-based index in <paramref name="collection"/> at which copying begins.</param>
    /// <param name="requiredCapacity">The number of elements that need to be copied.</param>
    /// <param name="collectionParamName">The name of the <paramref name="collection"/> parameter.</param>
    /// <param name="indexParamName">The name of the <paramref name="index"/> parameter.</param>
    /// <exception cref="ArgumentNullException">If <paramref name="collection"/> is null.</exception>
    /// <exception cref="ArgumentOutOfRangeException">If <paramref name="index"/> falls outside of the bounds of <paramref name="collection"/>.</exception>
    /// <exception cref="ArgumentException">
    ///  If there is insufficient capacity to copy the collection contents into <paramref name="collection"/>
    ///  when starting at <paramref name="index"/>.
    /// </exception>
    public static void VerifyCollectionCopyToArguments<T>(
        [NotNull] ICollection<T>? collection,
        int index,
        int requiredCapacity,
        [CallerArgumentExpression(nameof(collection))] string? collectionParamName = null,
        [CallerArgumentExpression(nameof(index))] string? indexParamName = null)
    {
        ArgumentNullException.ThrowIfNull(collection, collectionParamName);
        ArgumentOutOfRangeException.ThrowIfNegative(index, indexParamName);
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, collection.Count, indexParamName);

        int capacity = collection.Count - index;
        if (requiredCapacity > capacity)
        {
            throw new ArgumentException(SR.CollectionCopyToFailureProvidedArrayIsTooSmall, collectionParamName);
        }
    }

    /// <summary>
    ///  Throws an <see cref="ArgumentException"/> if the given collection is not null but of zero length.
    /// </summary>
    /// <typeparam name="T">The element type of the collection.</typeparam>
    /// <param name="parameter">The collection to check.</param>
    /// <param name="parameterName">The name of the <paramref name="parameter"/>.</param>
    public static void VerifyThrowArgumentLengthIfNotNull<T>(IReadOnlyCollection<T>? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null)
    {
        if (parameter?.Count == 0)
        {
            ThrowArgumentLength(parameterName);
        }
    }

    /// <summary>
    ///  Throws an <see cref="ArgumentException"/> if the string has zero length, unless it is
    ///  null, in which case no exception is thrown.
    /// </summary>
    /// <param name="parameter">The string to check.</param>
    /// <param name="parameterName">The name of the <paramref name="parameter"/>.</param>
    public static void VerifyThrowArgumentLengthIfNotNull(string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null)
    {
        if (parameter?.Length == 0)
        {
            ThrowArgumentLength(parameterName);
        }
    }

    [DoesNotReturn]
    private static void ThrowArgumentLength(string? parameterName)
        => throw new ArgumentException(SR.Argument_EmptyString, parameterName);

    /// <summary>
    ///  Throws an <see cref="ArgumentNullException"/> if the given string parameter is null,
    ///  and an <see cref="ArgumentException"/> if it contains invalid path or file characters.
    /// </summary>
    /// <param name="parameter">The path to check.</param>
    /// <param name="parameterName">The name of the <paramref name="parameter"/>.</param>
    public static void VerifyThrowArgumentInvalidPath([NotNull] string? parameter, [CallerArgumentExpression(nameof(parameter))] string? parameterName = null)
    {
        ArgumentNullException.ThrowIfNull(parameter, parameterName);

        if (FileUtilities.PathIsInvalid(parameter))
        {
            throw new ArgumentException(SR.FormatParameterCannotHaveInvalidPathChars(parameterName, parameter));
        }
    }
}