File: Evaluation\SemiColonTokenizer.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 Microsoft.NET.StringTools;
 
#nullable disable
 
namespace Microsoft.Build.Evaluation
{
    /// <summary>
    /// Splits an expression into fragments at semicolons, except where the
    /// semicolons are in a macro or separator expression.
    /// Fragments are trimmed and empty fragments discarded.
    /// </summary>
    /// <remarks>
    /// These complex cases prevent us from doing a simple split on ';':
    ///  (1) Macro expression: @(foo->'xxx;xxx')
    ///  (2) Separator expression: @(foo, 'xxx;xxx')
    ///  (3) Combination: @(foo->'xxx;xxx', 'xxx;xxx')
    ///  We must not split on semicolons in macro or separator expressions like these.
    /// </remarks>
    internal struct SemiColonTokenizer : IEnumerable<string>
    {
        private readonly string _expression;
 
        public SemiColonTokenizer(string expression)
        {
            _expression = expression;
        }
 
        public Enumerator GetEnumerator() => new Enumerator(_expression);
 
        IEnumerator<string> IEnumerable<string>.GetEnumerator() => GetEnumerator();
 
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 
        internal struct Enumerator : IEnumerator<string>
        {
            private readonly string _expression;
            private string _current;
            private int _index;
 
            public Enumerator(string expression)
            {
                _expression = expression;
                _index = 0;
                _current = default(string);
            }
 
            public string Current
            {
                get { return _current; }
            }
 
            object IEnumerator.Current => Current;
 
            public void Dispose()
            {
            }
 
            public bool MoveNext()
            {
                int segmentStart = _index;
                bool insideItemList = false;
                bool insideQuotedPart = false;
                string segment;
 
                // Walk along the string, keeping track of whether we are in an item list expression.
                // If we hit a semi-colon or the end of the string and we aren't in an item list,
                // add the segment to the list.
                for (; _index < _expression.Length; _index++)
                {
                    switch (_expression[_index])
                    {
                        case ';':
                            if (!insideItemList)
                            {
                                // End of segment, so add it to the list
                                segment = GetExpressionSubstring(segmentStart, _index - segmentStart);
                                if (segment != null)
                                {
                                    _current = segment;
                                    return true;
                                }
 
                                // Move past this semicolon
                                segmentStart = _index + 1;
                            }
 
                            break;
                        case '@':
                            // An '@' immediately followed by a '(' is the start of an item list
                            if (_expression.Length > _index + 1 && _expression[_index + 1] == '(')
                            {
                                // Start of item expression
                                insideItemList = true;
                            }
 
                            break;
                        case ')':
                            if (insideItemList && !insideQuotedPart)
                            {
                                // End of item expression
                                insideItemList = false;
                            }
 
                            break;
                        case '\'':
                            if (insideItemList)
                            {
                                // Start or end of quoted expression in item expression
                                insideQuotedPart = !insideQuotedPart;
                            }
 
                            break;
                    }
                }
 
                // Reached the end of the string: what's left is another segment
                _current = GetExpressionSubstring(segmentStart, _expression.Length - segmentStart);
                return _current != null;
            }
 
            public void Reset()
            {
                _current = default(string);
                _index = 0;
            }
 
            /// <summary>
            /// Returns a whitespace-trimmed and possibly interned substring of the expression.
            /// </summary>
            /// <param name="startIndex">Start index of the substring.</param>
            /// <param name="length">Length of the substring.</param>
            /// <returns>Equivalent to _expression.Substring(startIndex, length).Trim() or null if the trimmed substring is empty.</returns>
            private string GetExpressionSubstring(int startIndex, int length)
            {
                int endIndex = startIndex + length;
                while (startIndex < endIndex && char.IsWhiteSpace(_expression[startIndex]))
                {
                    startIndex++;
                }
                while (startIndex < endIndex && char.IsWhiteSpace(_expression[endIndex - 1]))
                {
                    endIndex--;
                }
                if (startIndex < endIndex)
                {
                    return Strings.WeakIntern(_expression.AsSpan(startIndex, endIndex - startIndex));
                }
                return null;
            }
        }
    }
}