File: Expansion\PropertyValueConverter.cs
Web Access
Project: 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.Globalization;
using System.Runtime.CompilerServices;
using Microsoft.Build.Shared;
using Microsoft.Build.Utilities;
using Microsoft.NET.StringTools;

namespace Microsoft.Build.Expansion;

internal static class PropertyValueConverter
{
    private const int InitialCapacity = 256;

    public static string ToString(object? value)
        => value switch
        {
            null => string.Empty,
            string s => s,
            _ => ConvertNonString(value),
        };

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static string ConvertNonString(object value) => value switch
    {
        IDictionary d => d.Count > 0
            ? ConvertDictionary(d)
            : string.Empty,

        Array a => a.Length > 0
            ? ConvertArray(a)
            : string.Empty,

        IEnumerable e => ConvertEnumerable(e),

        // The fall back is always to just convert to a string directly.
        // Issue: https://github.com/dotnet/msbuild/issues/9757
        _ => Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty,
    };

    private static string ConvertDictionary(IDictionary dictionary)
    {
        using Writer writer = new(InitialCapacity);
        writer.AppendDictionary(dictionary);

        return writer.ToString();
    }

    private static string ConvertArray(Array array)
    {
        using Writer writer = new(InitialCapacity);
        writer.AppendArray(array);

        return writer.ToString();
    }

    private static string ConvertEnumerable(IEnumerable enumerable)
    {
        using Writer writer = new(InitialCapacity);
        writer.AppendEnumerable(enumerable);

        return writer.ToString();
    }

    private ref struct Writer(int initialCapacity)
    {
        private ValueStringBuilder _builder = new(initialCapacity);
        private int _escapeDepth;

        public void Dispose()
            => _builder.Dispose();

        public override readonly string ToString()
            => Strings.WeakIntern(_builder.AsSpan());

        public void AppendDictionary(IDictionary dictionary)
        {
            int startLength = _builder.Length;

            // If the return type is an IDictionary, then we convert this to
            // a semi-colon delimited set of A=B pairs.
            // Key and Value are converted to string and escaped
            foreach (DictionaryEntry entry in dictionary)
            {
                if (_builder.Length > startLength)
                {
                    AppendSeparator();
                }

                // convert and escape each key and value in the dictionary entry
                AppendElement(entry.Key);
                _builder.Append("=");
                AppendElement(entry.Value);
            }
        }

        public void AppendArray(Array array)
        {
            int startLength = _builder.Length;

            if (array.Rank == 1)
            {
                int lowerBound = array.GetLowerBound(0);
                for (int i = 0; i < array.Length; i++)
                {
                    if (_builder.Length > startLength)
                    {
                        AppendSeparator();
                    }

                    AppendElement(array.GetValue(lowerBound + i));
                }

                return;
            }

            foreach (object element in array)
            {
                if (_builder.Length > startLength)
                {
                    AppendSeparator();
                }

                AppendElement(element);
            }
        }

        public void AppendEnumerable(IEnumerable enumerable)
        {
            int startLength = _builder.Length;

            // If the return is enumerable, then we'll convert to semi-colon delimited elements
            // each of which must be converted, so we'll recurse for each element
            foreach (object element in enumerable)
            {
                if (_builder.Length > startLength)
                {
                    AppendSeparator();
                }

                // we need to convert and escape each element of the array
                AppendElement(element);
            }
        }

        private void AppendElement(object? value)
        {
            _escapeDepth++;
            Append(value);
            _escapeDepth--;
        }

        private void Append(object? value)
        {
            switch (value)
            {
                case null:
                    break;

                case string s:
                    AppendEscaped(s);
                    break;

                case IDictionary d:
                    AppendDictionary(d);
                    break;

                case Array a:
                    AppendArray(a);
                    break;

                case IEnumerable e:
                    AppendEnumerable(e);
                    break;

                default:
                    AppendEscaped(Convert.ToString(value, CultureInfo.InvariantCulture));
                    break;
            }
        }

        private void AppendEscaped(string? value)
            => _builder.AppendEscaped(value, _escapeDepth);

        private void AppendSeparator()
            => _builder.AppendEscaped(';', _escapeDepth);
    }
}