File: Evaluation\Expander.SpanBasedConcatenator.cs
Web Access
Project: src\msbuild\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 Microsoft.Build.Framework;
using Microsoft.NET.StringTools;

#nullable disable

namespace Microsoft.Build.Evaluation;

internal partial class Expander<P, I>
    where P : class, IProperty
    where I : class, IItem
{
    /// <summary>
    /// A helper struct wrapping a <see cref="SpanBasedStringBuilder"/> and providing file path conversion
    /// as used in e.g. property expansion.
    /// </summary>
    /// <remarks>
    /// If exactly one value is added and no concatenation takes places, this value is returned without
    /// conversion. In other cases values are stringified and attempted to be interpreted as file paths
    /// before concatenation.
    /// </remarks>
    private struct SpanBasedConcatenator : IDisposable
    {
        /// <summary>
        /// The backing <see cref="SpanBasedStringBuilder"/>, null until the second value is added.
        /// </summary>
        private SpanBasedStringBuilder _builder;

        /// <summary>
        /// The first value added to the concatenator. Tracked in its own field so it can be returned
        /// without conversion if no concatenation takes place.
        /// </summary>
        private object _firstObject;

        /// <summary>
        /// The first value added to the concatenator if it is a span. Tracked in its own field so the
        /// <see cref="SpanBasedStringBuilder"/> functionality doesn't have to be invoked if no concatenation
        /// takes place.
        /// </summary>
        private ReadOnlyMemory<char> _firstSpan;

        /// <summary>
        /// True if this instance is already disposed.
        /// </summary>
        private bool _disposed;

        /// <summary>
        /// Adds an object to be concatenated.
        /// </summary>
        public void Add(object obj)
        {
            CheckDisposed();
            FlushFirstValueIfNeeded();
            if (_builder != null)
            {
                _builder.Append(FileUtilities.MaybeAdjustFilePath(obj.ToString()));
            }
            else
            {
                _firstObject = obj;
            }
        }

        /// <summary>
        /// Adds a span to be concatenated.
        /// </summary>
        public void Add(ReadOnlyMemory<char> span)
        {
            CheckDisposed();
            FlushFirstValueIfNeeded();
            if (_builder != null)
            {
                _builder.Append(FileUtilities.MaybeAdjustFilePath(span));
            }
            else
            {
                _firstSpan = span;
            }
        }

        /// <summary>
        /// Returns the result of the concatenation.
        /// </summary>
        /// <returns>
        /// If only one value has been added and it is not a string, it is returned unchanged.
        /// In all other cases (no value, one string value, multiple values) the result is a
        /// concatenation of the string representation of the values, each additionally subjected
        /// to file path adjustment.
        /// </returns>
        public readonly object GetResult()
        {
            CheckDisposed();
            if (_firstObject != null)
            {
                return (_firstObject is string stringValue) ? FileUtilities.MaybeAdjustFilePath(stringValue) : _firstObject;
            }
            return _firstSpan.IsEmpty
                ? _builder?.ToString() ?? string.Empty
                : FileUtilities.MaybeAdjustFilePath(_firstSpan).ToString();
        }

        /// <summary>
        /// Disposes of the struct by delegating the call to the underlying <see cref="SpanBasedStringBuilder"/>.
        /// </summary>
        public void Dispose()
        {
            CheckDisposed();
            _builder?.Dispose();
            _disposed = true;
        }

        /// <summary>
        /// Throws <see cref="ObjectDisposedException"/> if this concatenator is already disposed.
        /// </summary>
        private readonly void CheckDisposed() =>
            ObjectDisposedException.ThrowIf(_disposed, typeof(SpanBasedConcatenator));

        /// <summary>
        /// Lazily initializes <see cref="_builder"/> and populates it with the first value
        /// when the second value is being added.
        /// </summary>
        private void FlushFirstValueIfNeeded()
        {
            if (_firstObject != null)
            {
                _builder = Strings.GetSpanBasedStringBuilder();
                _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstObject.ToString()));
                _firstObject = null;
            }
            else if (!_firstSpan.IsEmpty)
            {
                _builder = Strings.GetSpanBasedStringBuilder();
#if FEATURE_SPAN
                _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstSpan));
#else
                _builder.Append(FileUtilities.MaybeAdjustFilePath(_firstSpan.ToString()));
#endif
                _firstSpan = new ReadOnlyMemory<char>();
            }
        }
    }
}