File: ResourceUtilities.cs
Web Access
Project: ..\..\..\src\Framework.UnitTests\Microsoft.Build.Framework.UnitTests.csproj (Microsoft.Build.Framework.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
#if !BUILDINGAPPXTASKS
using System.Resources;
using System.Diagnostics;
#endif
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.ComponentModel;
 
#nullable disable
 
#if BUILDINGAPPXTASKS
namespace Microsoft.Build.AppxPackage.Shared
#else
namespace Microsoft.Build.Shared
#endif
{
    /// <summary>
    /// This class contains utility methods for dealing with resources.
    /// </summary>
    internal static class ResourceUtilities
    {
        /// <summary>
        /// Extracts the message code (if any) prefixed to the given string.
        /// <![CDATA[
        /// MSBuild codes match "^\s*(?<CODE>MSB\d\d\d\d):\s*(?<MESSAGE>.*)$"
        /// Arbitrary codes match "^\s*(?<CODE>[A-Za-z]+\d+):\s*(?<MESSAGE>.*)$"
        /// ]]>
        /// Thread safe.
        /// </summary>
        /// <param name="msbuildCodeOnly">Whether to match only MSBuild error codes, or any error code.</param>
        /// <param name="message">The string to parse.</param>
        /// <param name="code">[out] The message code, or null if there was no code.</param>
        /// <returns>The string without its message code prefix, if any.</returns>
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Build.Shared.ResourceUtilities.#ExtractMessageCode(System.Boolean,System.String,System.String&)", Justification = "Unavoidable complexity")]
        internal static string ExtractMessageCode(bool msbuildCodeOnly, string message, out string code)
        {
#if !BUILDINGAPPXTASKS
            ErrorUtilities.VerifyThrowInternalNull(message);
#endif
 
            code = null;
            int i = 0;
 
            while (i < message.Length && Char.IsWhiteSpace(message[i]))
            {
                i++;
            }
 
#if !BUILDINGAPPXTASKS
            if (msbuildCodeOnly)
            {
                if (
                    message.Length < i + 8 ||
                    message[i] != 'M' ||
                    message[i + 1] != 'S' ||
                    message[i + 2] != 'B' ||
                    message[i + 3] < '0' || message[i + 3] > '9' ||
                    message[i + 4] < '0' || message[i + 4] > '9' ||
                    message[i + 5] < '0' || message[i + 5] > '9' ||
                    message[i + 6] < '0' || message[i + 6] > '9' ||
                    message[i + 7] != ':')
                {
                    return message;
                }
 
                code = message.Substring(i, 7);
 
                i += 8;
            }
            else
#endif
            {
                int j = i;
                for (; j < message.Length; j++)
                {
                    char c = message[j];
                    if (((c < 'a') || (c > 'z')) && ((c < 'A') || (c > 'Z')))
                    {
                        break;
                    }
                }
 
                if (j == i)
                {
                    return message; // Should have been at least one letter
                }
 
                int k = j;
 
                for (; k < message.Length; k++)
                {
                    char c = message[k];
                    if (c < '0' || c > '9')
                    {
                        break;
                    }
                }
 
                if (k == j)
                {
                    return message; // Should have been at least one digit
                }
 
                if (k == message.Length || message[k] != ':')
                {
                    return message;
                }
 
                code = message.Substring(i, k - i);
 
                i = k + 1;
            }
 
            while (i < message.Length && Char.IsWhiteSpace(message[i]))
            {
                i++;
            }
 
            if (i < message.Length)
            {
                message = message.Substring(i, message.Length - i);
            }
 
            return message;
        }
 
        /// <summary>
        /// Retrieves the MSBuild F1-help keyword for the given resource string. Help keywords are used to index help topics in
        /// host IDEs.
        /// </summary>
        /// <param name="resourceName">Resource string to get the MSBuild F1-keyword for.</param>
        /// <returns>The MSBuild F1-help keyword string.</returns>
        private static string GetHelpKeyword(string resourceName)
            => "MSBuild." + resourceName;
 
#if !BUILDINGAPPXTASKS
        /// <summary>
        /// Retrieves the contents of the named resource string.
        /// </summary>
        /// <param name="resourceName">Resource string name.</param>
        /// <returns>Resource string contents.</returns>
        internal static string GetResourceString(string resourceName)
            => AssemblyResources.GetString(resourceName);
 
        /// <summary>
        /// Loads the specified string resource and formats it with the arguments passed in. If the string resource has an MSBuild
        /// message code and help keyword associated with it, they too are returned.
        ///
        /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for
        /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios.
        /// </summary>
        /// <remarks>This method is thread-safe.</remarks>
        /// <param name="code">[out] The MSBuild message code, or null.</param>
        /// <param name="helpKeyword">[out] The MSBuild F1-help keyword for the host IDE, or null.</param>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="args">Optional arguments for formatting the resource string.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringStripCodeAndKeyword(out string code, out string helpKeyword, string resourceName, params object[] args)
        {
            helpKeyword = GetHelpKeyword(resourceName);
 
            // NOTE: the AssemblyResources.GetString() method is thread-safe
            return ExtractMessageCode(true /* msbuildCodeOnly */, FormatString(GetResourceString(resourceName), args), out code);
        }
 
        // Overloads with 0-3 arguments to avoid array allocations.
 
        /// <summary>
        /// Loads the specified string resource and formats it with the arguments passed in. If the string resource has an MSBuild
        /// message code and help keyword associated with it, they too are returned.
        /// </summary>
        /// <remarks>This method is thread-safe.</remarks>
        /// <param name="code">[out] The MSBuild message code, or null.</param>
        /// <param name="helpKeyword">[out] The MSBuild F1-help keyword for the host IDE, or null.</param>
        /// <param name="resourceName">Resource string to load.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringStripCodeAndKeyword(out string code, out string helpKeyword, string resourceName)
        {
            helpKeyword = GetHelpKeyword(resourceName);
            return ExtractMessageCode(true, GetResourceString(resourceName), out code);
        }
 
        /// <summary>
        /// Loads the specified string resource and formats it with the arguments passed in. If the string resource has an MSBuild
        /// message code and help keyword associated with it, they too are returned.
        /// </summary>
        /// <param name="code">[out] The MSBuild message code, or null.</param>
        /// <param name="helpKeyword">[out] The MSBuild F1-help keyword for the host IDE, or null.</param>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="arg1">Argument for formatting the resource string.</param>
        internal static string FormatResourceStringStripCodeAndKeyword(out string code, out string helpKeyword, string resourceName, object arg1)
        {
            helpKeyword = GetHelpKeyword(resourceName);
            return ExtractMessageCode(true, FormatString(GetResourceString(resourceName), arg1), out code);
        }
 
        /// <summary>
        /// Loads the specified string resource and formats it with the arguments passed in. If the string resource has an MSBuild
        /// message code and help keyword associated with it, they too are returned.
        /// </summary>
        /// <param name="code">[out] The MSBuild message code, or null.</param>
        /// <param name="helpKeyword">[out] The MSBuild F1-help keyword for the host IDE, or null.</param>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="arg1">First argument for formatting the resource string.</param>
        /// <param name="arg2">Second argument for formatting the resource string.</param>
        internal static string FormatResourceStringStripCodeAndKeyword(out string code, out string helpKeyword, string resourceName, object arg1, object arg2)
        {
            helpKeyword = GetHelpKeyword(resourceName);
            return ExtractMessageCode(true, FormatString(GetResourceString(resourceName), arg1, arg2), out code);
        }
 
        /// <summary>
        /// Loads the specified string resource and formats it with the arguments passed in. If the string resource has an MSBuild
        /// message code and help keyword associated with it, they too are returned.
        /// </summary>
        /// <param name="code">[out] The MSBuild message code, or null.</param>
        /// <param name="helpKeyword">[out] The MSBuild F1-help keyword for the host IDE, or null.</param>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="arg1">First argument for formatting the resource string.</param>
        /// <param name="arg2">Second argument for formatting the resource string.</param>
        /// <param name="arg3">Third argument for formatting the resource string.</param>
        internal static string FormatResourceStringStripCodeAndKeyword(out string code, out string helpKeyword, string resourceName, object arg1, object arg2, object arg3)
        {
            helpKeyword = GetHelpKeyword(resourceName);
            return ExtractMessageCode(true, FormatString(GetResourceString(resourceName), arg1, arg2, arg3), out code);
        }
 
        [Obsolete("Use GetResourceString instead.", true)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        internal static string FormatResourceString(string resourceName)
        {   // Avoids an accidental dependency on FormatResourceString(string, params object[])
            return null;
        }
 
        /// <summary>
        /// Looks up a string in the resources, and formats it with the arguments passed in. If the string resource has an MSBuild
        /// message code and help keyword associated with it, they are discarded.
        ///
        /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for
        /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios.
        /// </summary>
        /// <remarks>This method is thread-safe.</remarks>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="args">Optional arguments for formatting the resource string.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringStripCodeAndKeyword(string resourceName, params object[] args)
            => FormatResourceStringStripCodeAndKeyword(out _, out _, resourceName, args);
 
        // Overloads with 0-3 arguments to avoid array allocations.
 
        /// <summary>
        /// Looks up a string in the resources. If the string resource has an MSBuild
        /// message code and help keyword associated with it, they are discarded.
        /// </summary>
        /// <remarks>This method is thread-safe.</remarks>
        /// <param name="resourceName">Resource string to load.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringStripCodeAndKeyword(string resourceName)
           => FormatResourceStringStripCodeAndKeyword(out _, out _, resourceName);
 
        /// <summary>
        /// Looks up a string in the resources, and formats it with the argument passed in. If the string resource has an MSBuild
        /// message code and help keyword associated with it, they are discarded.
        /// </summary>
        /// <remarks>This method is thread-safe.</remarks>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="arg1">Argument for formatting the resource string.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringStripCodeAndKeyword(string resourceName, object arg1)
           => FormatResourceStringStripCodeAndKeyword(out _, out _, resourceName, arg1);
 
        /// <summary>
        /// Looks up a string in the resources, and formats it with the arguments passed in. If the string resource has an MSBuild
        /// message code and help keyword associated with it, they are discarded.
        /// </summary>
        /// <remarks>This method is thread-safe.</remarks>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="arg1">First argument for formatting the resource string.</param>
        /// <param name="arg2">Second argument for formatting the resource string.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringStripCodeAndKeyword(string resourceName, object arg1, object arg2)
            => FormatResourceStringStripCodeAndKeyword(out _, out _, resourceName, arg1, arg2);
 
        /// <summary>
        /// Looks up a string in the resources, and formats it with the arguments passed in. If the string resource has an MSBuild
        /// message code and help keyword associated with it, they are discarded.
        /// </summary>
        /// <remarks>This method is thread-safe.</remarks>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="arg1">First argument for formatting the resource string.</param>
        /// <param name="arg2">Second argument for formatting the resource string.</param>
        /// <param name="arg3">Third argument for formatting the resource string.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringStripCodeAndKeyword(string resourceName, object arg1, object arg2, object arg3)
            => FormatResourceStringStripCodeAndKeyword(out _, out _, resourceName, arg1, arg2, arg3);
 
        /// <summary>
        /// Formats the resource string with the given arguments.
        /// Ignores error codes and keywords.
        /// </summary>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="args">Optional arguments for formatting the resource string.</param>
        /// <returns>The formatted resource string.</returns>
        /// <remarks>the AssemblyResources.GetString() method is thread-safe.</remarks>
        internal static string FormatResourceStringIgnoreCodeAndKeyword(string resourceName, params object[] args)
            => FormatString(GetResourceString(resourceName), args);
 
        // Overloads with 0-3 arguments to avoid array allocations.
 
        /// <summary>
        /// Formats the resource string.
        /// Ignores error codes and keywords.
        /// </summary>
        /// <param name="resourceName">Resource string to load.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringIgnoreCodeAndKeyword(string resourceName)
            => GetResourceString(resourceName);
 
        /// <summary>
        /// Formats the resource string with the given argument.
        /// Ignores error codes and keywords.
        /// </summary>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="arg1">Argument for formatting the resource string.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringIgnoreCodeAndKeyword(string resourceName, object arg1)
            => FormatString(GetResourceString(resourceName), arg1);
 
        /// <summary>
        /// Formats the resource string with the given arguments.
        /// Ignores error codes and keywords.
        /// </summary>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="arg1">First argument for formatting the resource string.</param>
        /// <param name="arg2">Second argument for formatting the resource string.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringIgnoreCodeAndKeyword(string resourceName, object arg1, object arg2)
            => FormatString(GetResourceString(resourceName), arg1, arg2);
 
        /// <summary>
        /// Formats the resource string with the given arguments.
        /// Ignores error codes and keywords.
        /// </summary>
        /// <param name="resourceName">Resource string to load.</param>
        /// <param name="arg1">First argument for formatting the resource string.</param>
        /// <param name="arg2">Second argument for formatting the resource string.</param>
        /// <param name="arg3">Third argument for formatting the resource string.</param>
        /// <returns>The formatted resource string.</returns>
        internal static string FormatResourceStringIgnoreCodeAndKeyword(string resourceName, object arg1, object arg2, object arg3)
            => FormatString(GetResourceString(resourceName), arg1, arg2, arg3);
 
        /// <summary>
        /// Formats the given string using the variable arguments passed in.
        ///
        /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for
        /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios
        ///
        /// Thread safe.
        /// </summary>
        /// <param name="unformatted">The string to format.</param>
        /// <param name="args">Optional arguments for formatting the given string.</param>
        /// <returns>The formatted string.</returns>
        internal static string FormatString(string unformatted, params object[] args)
        {
            string formatted = unformatted;
 
            // NOTE: String.Format() does not allow a null arguments array
            if (args?.Length > 0)
            {
                ValidateArgsIfDebug(args);
 
                // Format the string, using the variable arguments passed in.
                // NOTE: all String methods are thread-safe
                formatted = string.Format(CultureInfo.CurrentCulture, unformatted, args);
            }
 
            return formatted;
        }
 
        // Overloads with 1-3 arguments to avoid array allocations.
 
        /// <summary>
        /// Formats the given string using the variable arguments passed in.
        /// </summary>
        /// <param name="unformatted">The string to format.</param>
        /// <param name="arg1">Argument for formatting the given string.</param>
        /// <returns>The formatted string.</returns>
        internal static string FormatString(string unformatted, object arg1)
        {
            ValidateArgsIfDebug([arg1]);
            return string.Format(CultureInfo.CurrentCulture, unformatted, arg1);
        }
 
        /// <summary>
        /// Formats the given string using the variable arguments passed in.
        /// </summary>
        /// <param name="unformatted">The string to format.</param>
        /// <param name="arg1">First argument for formatting the given string.</param>
        /// <param name="arg2">Second argument for formatting the given string.</param>
        /// <returns>The formatted string.</returns>
        internal static string FormatString(string unformatted, object arg1, object arg2)
        {
            ValidateArgsIfDebug([arg1, arg2]);
            return string.Format(CultureInfo.CurrentCulture, unformatted, arg1, arg2);
        }
 
        /// <summary>
        /// Formats the given string using the variable arguments passed in.
        /// </summary>
        /// <param name="unformatted">The string to format.</param>
        /// <param name="arg1">First argument for formatting the given string.</param>
        /// <param name="arg2">Second argument for formatting the given string.</param>
        /// <param name="arg3">Third argument for formatting the given string.</param>
        /// <returns>The formatted string.</returns>
        internal static string FormatString(string unformatted, object arg1, object arg2, object arg3)
        {
            ValidateArgsIfDebug([arg1, arg2, arg3]);
            return string.Format(CultureInfo.CurrentCulture, unformatted, arg1, arg2, arg3);
        }
 
        [Conditional("DEBUG")]
        private static void ValidateArgsIfDebug(object[] args)
        {
            // If you accidentally pass some random type in that can't be converted to a string,
            // FormatResourceString calls ToString() which returns the full name of the type!
            foreach (object param in args)
            {
                // Check it has a real implementation of ToString() and the type is not actually System.String
                if (param != null)
                {
                    if (string.Equals(param.GetType().ToString(), param.ToString(), StringComparison.Ordinal) &&
                        param.GetType() != typeof(string))
                    {
                        ErrorUtilities.ThrowInternalError(
                            "Invalid resource parameter type, was {0}",
                            param.GetType().FullName);
                    }
                }
            }
        }
 
        /// <summary>
        /// Verifies that a particular resource string actually exists in the string table. This will only be called in debug
        /// builds. It helps catch situations where a dev calls VerifyThrowXXX with a new resource string, but forgets to add the
        /// resource string to the string table, or misspells it!
        /// </summary>
        /// <remarks>This method is thread-safe.</remarks>
        /// <param name="resourceName">Resource string to check.</param>
        [Conditional("DEBUG")]
        internal static void VerifyResourceStringExists(string resourceName)
        {
            try
            {
                // Look up the resource string in the engine's string table.
                // NOTE: the AssemblyResources.GetString() method is thread-safe
                string unformattedMessage = AssemblyResources.GetString(resourceName);
 
                if (unformattedMessage == null)
                {
                    ErrorUtilities.ThrowInternalError("The resource string \"" + resourceName + "\" was not found.");
                }
            }
            catch (ArgumentException e)
            {
#if FEATURE_DEBUG_LAUNCH
                Debug.Fail("The resource string \"" + resourceName + "\" was not found.");
#endif
                ErrorUtilities.ThrowInternalError(e.Message);
            }
            catch (InvalidOperationException e)
            {
#if FEATURE_DEBUG_LAUNCH
                Debug.Fail("The resource string \"" + resourceName + "\" was not found.");
#endif
                ErrorUtilities.ThrowInternalError(e.Message);
            }
            catch (MissingManifestResourceException e)
            {
#if FEATURE_DEBUG_LAUNCH
                Debug.Fail("The resource string \"" + resourceName + "\" was not found.");
#endif
                ErrorUtilities.ThrowInternalError(e.Message);
            }
#endif
        }
    }
}