File: Operations\Conditional.cs
Web Access
Project: src\src\sdk\src\TemplateEngine\Microsoft.TemplateEngine.Core\Microsoft.TemplateEngine.Core.csproj (Microsoft.TemplateEngine.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.TemplateEngine.Core.Contracts;
using Microsoft.TemplateEngine.Core.Util;

namespace Microsoft.TemplateEngine.Core.Operations
{
    public class Conditional : IOperationProvider
    {
        public static readonly string OperationName = "conditional";

        // the unusual order of these is historical, no special meaning
        // if actual_token_index % 10 == baseTokenIndex
        // then actual_token_index is of the baseTokenIndex type
        // these are now "Base indexes"
        private const int IfTokenBaseIndex = 0;

        private const int EndTokenBaseIndex = 1;
        private const int ElseIfTokenBaseIndex = 2;
        private const int ElseTokenBaseIndex = 3;
        private const int IfTokenActionableBaseIndex = 4;
        private const int ElseIfTokenActionableBaseIndex = 5;
        private const int ElseTokenActionableBaseIndex = 6;

        // must be > the highest token type index
        private const int TokenTypeModulus = 10;

        private readonly bool _initialState;

        public Conditional(ConditionalTokens tokenVariants, bool wholeLine, bool trimWhitespace, ConditionEvaluator evaluator, string? id, bool initialState)
        {
            TrimWhitespace = trimWhitespace;
            WholeLine = wholeLine;
            Evaluator = evaluator;
            Tokens = tokenVariants;
            Id = id;
            _initialState = initialState;
        }

        public string? Id { get; }

        public bool WholeLine { get; }

        public bool TrimWhitespace { get; }

        public ConditionEvaluator Evaluator { get; }

        public ConditionalTokens Tokens { get; }

        /// <summary>
        /// Returns the number of elements in the longest of the token variant lists.
        /// </summary>
        private int LongestTokenVariantListSize
        {
            get
            {
                int maxListSize = Math.Max(Tokens.IfTokens.Count, Tokens.ElseTokens.Count);
                maxListSize = Math.Max(maxListSize, Tokens.ElseIfTokens.Count);
                maxListSize = Math.Max(maxListSize, Tokens.EndIfTokens.Count);
                maxListSize = Math.Max(maxListSize, Tokens.ActionableIfTokens.Count);
                maxListSize = Math.Max(maxListSize, Tokens.ActionableElseTokens.Count);
                maxListSize = Math.Max(maxListSize, Tokens.ActionableElseIfTokens.Count);

                return maxListSize;
            }
        }

        public IOperation GetOperation(Encoding encoding, IProcessorState processorState)
        {
            TokenTrie trie = new TokenTrie();

            List<IToken> tokens = new List<IToken>(TokenTypeModulus * LongestTokenVariantListSize);
            for (int i = 0; i < tokens.Capacity; i++)
            {
                tokens.Add(null!);
            }

            AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.IfTokens, IfTokenBaseIndex, encoding);
            AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.ElseTokens, ElseTokenBaseIndex, encoding);
            AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.ElseIfTokens, ElseIfTokenBaseIndex, encoding);
            AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.EndIfTokens, EndTokenBaseIndex, encoding);
            AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.ActionableIfTokens, IfTokenActionableBaseIndex, encoding);
            AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.ActionableElseTokens, ElseTokenActionableBaseIndex, encoding);
            AddTokensOfTypeToTokenListAndTrie(trie, tokens, Tokens.ActionableElseIfTokens, ElseIfTokenActionableBaseIndex, encoding);

            return new Implementation(this, tokens, trie, Id, _initialState);
        }

        /// <summary>
        /// Returns true if the tokenIndex indicates the token is a variant of its base type,
        /// false otherwise.
        /// </summary>
        /// <param name="tokenIndex"></param>
        /// <param name="baseTypeIndex"></param>
        /// <returns></returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static bool IsTokenIndexOfType(int tokenIndex, int baseTypeIndex)
        {
            return (tokenIndex % TokenTypeModulus) == baseTypeIndex;
        }

        /// <summary>
        /// Puts the tokensOfType into the tokenMasterList at indexes which are congruent to typeRemainder mod TokenTypeModulus.
        /// </summary>
        /// <param name="trie"></param>
        /// <param name="tokenMasterList"></param>
        /// <param name="tokensOfType"></param>
        /// <param name="typeRemainder"></param>
        /// <param name="encoding"></param>
        private void AddTokensOfTypeToTokenListAndTrie(ITokenTrie trie, List<IToken> tokenMasterList, IReadOnlyList<ITokenConfig> tokensOfType, int typeRemainder, Encoding encoding)
        {
            int tokenIndex = typeRemainder;

            for (int i = 0; i < tokensOfType.Count; i++)
            {
                tokenMasterList[tokenIndex] = tokensOfType[i].ToToken(encoding);
                trie.AddToken(tokenMasterList[tokenIndex], typeRemainder);
                tokenIndex += TokenTypeModulus;
            }
        }

        private class Implementation : IOperation
        {
            private readonly Conditional _definition;
            private readonly Stack<EvaluationState> _pendingCompletion = new Stack<EvaluationState>();
            private readonly ITokenTrie _trie;
            private EvaluationState? _current;

            public Implementation(Conditional definition, IReadOnlyList<IToken> tokens, ITokenTrie trie, string? id, bool initialState)
            {
                _trie = trie;
                _definition = definition;
                Tokens = tokens;
                Id = id;
                IsInitialStateOn = string.IsNullOrEmpty(id) || initialState;
            }

            public string? Id { get; }

            public IReadOnlyList<IToken> Tokens { get; }

            public bool IsInitialStateOn { get; }

            public int HandleMatch(IProcessorState processor, int bufferLength, ref int currentBufferPosition, int token)
            {
                if (processor.Config.Flags.TryGetValue(OperationName, out bool flag) && !flag)
                {
                    processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length);
                    return Tokens[token].Length;
                }

                // conditional has not started, or this is the "if"
                if (_current != null || IsTokenIndexOfType(token, IfTokenBaseIndex) || IsTokenIndexOfType(token, IfTokenActionableBaseIndex))
                {
                    if (_definition.WholeLine)
                    {
                        processor.SeekTargetBackUntil(processor.EncodingConfig.LineEndings);
                    }
                    else if (_definition.TrimWhitespace)
                    {
                        processor.TrimWhitespace(false, true, ref bufferLength, ref currentBufferPosition);
                    }
                }

            BEGIN:
                //Got the "if" token...
                if (IsTokenIndexOfType(token, IfTokenBaseIndex) || IsTokenIndexOfType(token, IfTokenActionableBaseIndex))
                {
                    if (_current == null)
                    {
                        _current = new EvaluationState(this);
                    }
                    else
                    {
                        _pendingCompletion.Push(_current);
                        _current = new EvaluationState(this);
                    }

                    //If the "if" branch is taken, all else and elseif blocks will be omitted, return
                    //  control to the processor so nested "if"s/mutations can be processed. Note that
                    //  this block will not be terminated until the corresponding endif is found
                    if (_current.Evaluate(processor, ref bufferLength, ref currentBufferPosition))
                    {
                        if (_definition.WholeLine)
                        {
                            processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: true);
                        }

                        if (IsTokenIndexOfType(token, IfTokenActionableBaseIndex))
                        {
                            // "Actionable" if token, so enable the flag operation(s)
                            _current.ToggleActionableOperations(true, processor);
                        }

                        // if (true_condition) was found.
                        return 0;
                    }
                    else
                    {
                        // if (false_condition) was found. Skip to the next token of the if-elseif-elseif-...elseif-else-endif
                        SeekToNextTokenAtSameLevel(processor, ref bufferLength, ref currentBufferPosition, out token);
                        goto BEGIN;
                    }
                }

                // If we've got an unbalanced statement, emit the token
                if (_current == null)
                {
                    processor.WriteToTarget(Tokens[token].Value, Tokens[token].Start, Tokens[token].Length);
                    return Tokens[token].Length;
                }

                //Got the endif token, exit to the parent "if" scope if it exists
                if (IsTokenIndexOfType(token, EndTokenBaseIndex))
                {
                    if (_pendingCompletion.Count > 0)
                    {
                        _current = _pendingCompletion.Pop();
                        _current.ToggleActionableOperations(_current.ActionableOperationsEnabled, processor);
                    }
                    else
                    {
                        // disable the special case operations (note: they may already be disabled, but cheaper to do than check)
                        _current.ToggleActionableOperations(false, processor);
                        _current = null;
                    }

                    if (_definition.WholeLine)
                    {
                        processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: true);
                    }
                    else if (_definition.TrimWhitespace)
                    {
                        processor.TrimWhitespace(true, false, ref bufferLength, ref currentBufferPosition);
                    }

                    return 0;
                }

                if (_current.BranchTaken)
                {
                    processor.SeekTargetBackUntil(processor.EncodingConfig.LineEndings, true);
                    //A previous branch was taken. Skip to the endif token.
                    // NOTE: this can probably use the new method SeekToNextTokenAtSameLevel() - they do almost the same thing.
                    SkipToMatchingEndif(processor, ref bufferLength, ref currentBufferPosition, ref token);

                    if (_pendingCompletion.Count > 0)
                    {
                        _current = _pendingCompletion.Pop();
                        _current.ToggleActionableOperations(_current.ActionableOperationsEnabled, processor);
                    }
                    else
                    {
                        // disable the special case operation (note: it may already be disabled, but cheaper to do than check)
                        _current.ToggleActionableOperations(false, processor);
                        _current = null;
                    }

                    if (_definition.WholeLine)
                    {
                        processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: false);
                    }
                    else if (_definition.TrimWhitespace)
                    {
                        processor.TrimWhitespace(true, false, ref bufferLength, ref currentBufferPosition);
                    }

                    return 0;
                }

                //We have an "elseif" and haven't taken a previous branch
                if (IsTokenIndexOfType(token, ElseIfTokenBaseIndex) || IsTokenIndexOfType(token, ElseIfTokenActionableBaseIndex))
                {
                    // 8-19 attempt to make the same as if() handling
                    //
                    if (_current.Evaluate(processor, ref bufferLength, ref currentBufferPosition))
                    {
                        if (_definition.WholeLine)
                        {
                            processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: true);
                        }

                        if (IsTokenIndexOfType(token, ElseIfTokenActionableBaseIndex))
                        {
                            // the elseif branch is taken.
                            _current.ToggleActionableOperations(true, processor);
                        }

                        return 0;
                    }
                    else
                    {
                        SeekToNextTokenAtSameLevel(processor, ref bufferLength, ref currentBufferPosition, out token);

                        // In the original version this was conditional on SeekToToken() succeeding.
                        // Not sure if it should be conditional. It should never fail, unless the template is malformed.
                        goto BEGIN;
                    }
                }

                //We have an "else" token and haven't taken any other branches, return control
                //  after setting that a branch has been taken
                if (IsTokenIndexOfType(token, ElseTokenBaseIndex) || IsTokenIndexOfType(token, ElseTokenActionableBaseIndex))
                {
                    if (IsTokenIndexOfType(token, ElseTokenActionableBaseIndex))
                    {
                        _current.ToggleActionableOperations(true, processor);
                    }

                    _current.BranchTaken = true;
                    processor.WhitespaceHandler(ref bufferLength, ref currentBufferPosition, wholeLine: _definition.WholeLine, trim: _definition.TrimWhitespace);
                    return 0;
                }
                else
                {
                    Debug.Assert(true, "Unknown token index: " + token);
                    return 0;   // TODO: revisit. Not sure what's best here.
                }
            }

            // moves the buffer to the next token at the same level.
            // Returns false if no end token can be found at the same level.
            //      this is probably indicative of a template authoring problem, or possibly a buffer problem.
            private bool SkipToMatchingEndif(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, ref int token)
            {
                while (!IsTokenIndexOfType(token, EndTokenBaseIndex))
                {
                    bool seekSucceeded = SeekToNextTokenAtSameLevel(processor, ref bufferLength, ref currentBufferPosition, out token);

                    if (!seekSucceeded)
                    {
                        return false;
                    }
                }

                return true;
            }

            // Moves the buffer to the next token at the same level of nesting as the current token.
            // Should never be called if we're on an end token!!!
            // Returns false if no next token can be found at the same level.
            //      this is probably indicative of a template authoring problem, or possibly a buffer problem.
            private bool SeekToNextTokenAtSameLevel(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, out int token)
            {
                if (_definition.WholeLine)
                {
                    processor.SeekSourceForwardUntil(processor.EncodingConfig.LineEndings, ref bufferLength, ref currentBufferPosition, consumeToken: true);
                }

                bool seekSucceeded = SeekToToken(processor, ref bufferLength, ref currentBufferPosition, out token);

                //Keep on scanning until we've hit a balancing token that belongs to us
                // each "if" found opens a new level of nesting
                while (IsTokenIndexOfType(token, IfTokenBaseIndex) || IsTokenIndexOfType(token, IfTokenActionableBaseIndex))
                {
                    int open = 1;
                    while (open != 0)
                    {
                        seekSucceeded &= SeekToToken(processor, ref bufferLength, ref currentBufferPosition, out token);

                        if (IsTokenIndexOfType(token, IfTokenBaseIndex) || IsTokenIndexOfType(token, IfTokenActionableBaseIndex))
                        {
                            ++open;
                        }
                        else if (IsTokenIndexOfType(token, EndTokenBaseIndex))
                        {
                            --open;
                        }
                    }

                    seekSucceeded &= SeekToToken(processor, ref bufferLength, ref currentBufferPosition, out token);
                }

                // this may be irrelevant. If it happens, the template is malformed (i think)
                return seekSucceeded;
            }

            // moves to the next token
            // returns false if the end of the buffer was reached without finding a token.
            private bool SeekToToken(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, out int token)
            {
                bool bufferAdvanceFailed = false;
                ITokenTrieEvaluator evaluator = _trie.CreateEvaluator();

                while (true)
                {
                    for (; currentBufferPosition < bufferLength; ++currentBufferPosition)
                    {
                        if (evaluator.Accept(processor.CurrentBuffer[currentBufferPosition], ref currentBufferPosition, out token))
                        {
                            if (bufferAdvanceFailed || (currentBufferPosition != bufferLength))
                            {
                                return true;
                            }
                        }
                    }

                    if (bufferAdvanceFailed)
                    {
                        if (evaluator.TryFinalizeMatchesInProgress(ref currentBufferPosition, out token))
                        {
                            return true;
                        }

                        break;
                    }

                    bufferAdvanceFailed = !processor.AdvanceBuffer(bufferLength - evaluator.BytesToKeepInBuffer);
                    currentBufferPosition = evaluator.BytesToKeepInBuffer;
                    bufferLength = processor.CurrentBufferLength;
                }

                //If we run out of places to look, assert that the end of the buffer is the end
                token = EndTokenBaseIndex;
                currentBufferPosition = bufferLength;
                return false;   // no terminator found
            }

            private class EvaluationState
            {
                private readonly Implementation _implementation;
                private bool _branchTaken;

                public EvaluationState(Implementation implementation)
                {
                    _implementation = implementation;
                    ActionableOperationsEnabled = false;
                }

                public bool BranchTaken
                {
                    get => _branchTaken;
                    set => _branchTaken |= value;
                }

                public bool ActionableOperationsEnabled { get; private set; }

                public void ToggleActionableOperations(bool enabled, IProcessorState processor)
                {
                    ActionableOperationsEnabled = enabled;

                    foreach (string otherOptionDisableFlag in _implementation._definition.Tokens.ActionableOperations)
                    {
                        processor.Config.Flags[otherOptionDisableFlag] = enabled;
                    }
                }

                internal bool Evaluate(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition)
                {
                    BranchTaken = _implementation._definition.Evaluator(processor, ref bufferLength, ref currentBufferPosition, out bool faulted);
                    return BranchTaken;
                }
            }
        }
    }
}