File: Expressions\Shared\SharedEvaluatorDefinition.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.Text;
using Microsoft.Extensions.Logging;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Core.Contracts;
using Microsoft.TemplateEngine.Core.Util;
using Microsoft.TemplateEngine.Utils;

namespace Microsoft.TemplateEngine.Core.Expressions.Shared
{
    public abstract class SharedEvaluatorDefinition<TSelf, TTokens>
        where TSelf : SharedEvaluatorDefinition<TSelf, TTokens>, new()
        where TTokens : struct
    {
        private static readonly TSelf Instance = new();
        private static readonly IOperatorMap<Operators, TTokens> Map = Instance.GenerateMap();
        private static readonly bool DereferenceInLiteralsSetting = Instance.DereferenceInLiterals;
        private static readonly string NullToken = Instance.NullTokenValue;
        private static readonly IOperationProvider[] NoOperationProviders = [];

        protected abstract string NullTokenValue { get; }

        protected abstract bool DereferenceInLiterals { get; }

        public static bool Evaluate(IProcessorState processor, ref int bufferLength, ref int currentBufferPosition, out bool faulted)
        {
            bool result = Evaluate(processor, ref bufferLength, ref currentBufferPosition, out string? faultedMessage, null, false);
            faulted = !string.IsNullOrEmpty(faultedMessage);
            return result;
        }

        /// <summary>
        /// Inspect the passed string, creates the expression, substitutes parameters within expression, evaluates substituted expression and returns result.
        /// If non-null bag for variable references is passed, it will be populated with references of variables used within the evaluable expression.
        /// </summary>
        /// <param name="logger">The logger to be used to log the messages during evaluation.</param>
        /// <param name="text">The string to be inspected and turned into expression.</param>
        /// <param name="variables">Variables to be substituted within the expression.</param>
        /// <returns>A boolean value indicating the result of the evaluation.</returns>
        public static bool EvaluateFromString(ILogger logger, string text, IVariableCollection variables)
        {
            return EvaluateFromString(logger, text, variables, out string? _, null);
        }

        /// <summary>
        /// Inspect the passed string, creates the expression, substitutes parameters within expression, evaluates substituted expression and returns result.
        /// If non-null bag for variable references is passed, it will be populated with references of variables used within the evaluable expression.
        /// </summary>
        /// <param name="logger">The logger to be used to log the messages during evaluation.</param>
        /// <param name="text">The string to be inspected and turned into expression.</param>
        /// <param name="variables">Variables to be substituted within the expression.</param>
        /// <param name="faultedMessage">Error message detailing failing evaluation, should it fail.</param>
        /// <param name="referencedVariablesKeys">If passed (if not null) it will be populated with references to variables used within the inspected expression.</param>
        /// <returns>A boolean value indicating the result of the evaluation.</returns>
        public static bool EvaluateFromString(ILogger logger, string text, IVariableCollection variables, out string? faultedMessage, HashSet<string>? referencedVariablesKeys = null)
        {
            using MemoryStream ms = new(Encoding.UTF8.GetBytes(text));
            using MemoryStream res = new();
            EngineConfig cfg = new(logger, variables);
            IProcessorState state = new ProcessorState(ms, res, (int)ms.Length, (int)ms.Length, cfg, NoOperationProviders);
            int len = (int)ms.Length;
            int pos = 0;
            return Evaluate(state, ref len, ref pos, out faultedMessage, referencedVariablesKeys, true);
        }

        /// <summary>
        /// Creates the evaluable expression based on passed string,
        /// collects used symbols in the expression and reports if any errors occurs on expression creation.
        /// </summary>
        /// <param name="logger">The logger to be used to log the messages during building the evaluable expression.</param>
        /// <param name="text">The string to be inspected and turned into expression.</param>
        /// <param name="variables">Variables to be substituted within the expression.</param>
        /// <param name="evaluableExpressionError">Error message detailing failing building evaluable expression.</param>
        /// <param name="referencedVariablesKeys">If passed (if not null) it will be populated with references to variables used within the inspected expression.</param>
        /// <returns>Evaluable expression that represents decomposed <paramref name="text"></paramref>.</returns>
        public static IEvaluable? GetEvaluableExpression(
            ILogger logger,
            string text,
            IVariableCollection variables,
            out string? evaluableExpressionError,
            HashSet<string> referencedVariablesKeys)
        {
            using MemoryStream ms = new(Encoding.UTF8.GetBytes(text));
            using MemoryStream res = new();
            EngineConfig cfg = new(logger, variables);
            IProcessorState state = new ProcessorState(ms, res, (int)ms.Length, (int)ms.Length, cfg, NoOperationProviders);
            int len = (int)ms.Length;
            int pos = 0;

            return GetEvaluableExpression(state, ref len, ref pos, out evaluableExpressionError, referencedVariablesKeys);
        }

        protected static int Compare(object? left, object? right)
        {
            if (Equals(right, NullToken))
            {
                right = null;
            }

            if (Equals(left, NullToken))
            {
                left = null;
            }

            return AttemptNumericComparison(left, right)
                   ?? AttemptBooleanComparison(left, right)
                   ?? AttemptVersionComparison(left, right)
                   ?? AttemptMultiValueComparison(left, right)
                   ?? AttemptLexicographicComparison(left, right)
                   ?? AttemptComparableComparison(left, right)
                   ?? 0;
        }

        protected abstract IOperatorMap<Operators, TTokens> GenerateMap();

        protected abstract ITokenTrie GetSymbols(IProcessorState processor);

        private static IEvaluable? GetEvaluableExpression(
            IProcessorState processor,
            ref int bufferLength,
            ref int currentBufferPosition,
            out string? faultedMessage,
            HashSet<string> referencedVariablesKeys)
        {
            faultedMessage = null;
            ITokenTrie tokens = Instance.GetSymbols(processor);
            ScopeBuilder<Operators, TTokens> builder = processor.ScopeBuilder(tokens, Map, DereferenceInLiteralsSetting);
            string? faultedSection = null;

            return builder.Build(
                ref bufferLength,
                ref currentBufferPosition,
                x => faultedSection = processor.Encoding.GetString(x.ToArray()),
                referencedVariablesKeys);
        }

        private static bool Evaluate(
            IProcessorState processor,
            ref int bufferLength,
            ref int currentBufferPosition,
            out string? faultedMessage,
            HashSet<string>? referencedVariablesKeys,
            // indicates whether passed buffer within processor contains only the analyzed expression,
            //  or it can possibly contain other content (e.g. the full template)
            bool shouldProcessWholeBuffer)
        {
            string? faultedSection = null;

            IEvaluable? expression = GetEvaluableExpression(
                processor,
                ref bufferLength,
                ref currentBufferPosition,
                out faultedMessage,
                referencedVariablesKeys ?? new HashSet<string>());

            bool result;
            if (faultedSection != null)
            {
                faultedMessage = faultedSection;
                result = false;
            }
            else
            {
                // Buffer continues after expression - let's populate error only if this is single expression evaluation
                //  as we want to avoid creation of message that would contain whole template content after some condition
                if (shouldProcessWholeBuffer && bufferLength != 0)
                {
                    faultedMessage = LocalizableStrings.Error_Evaluation_Expression_Substring +
                                     processor.Encoding.GetString(
                                         processor.CurrentBuffer,
                                         currentBufferPosition,
                                         bufferLength - currentBufferPosition);
                }

                try
                {
                    object? evalResult = expression?.Evaluate();
                    result = (bool)Convert.ChangeType(evalResult, typeof(bool));
                }
                catch (Exception e)
                {
                    faultedMessage = faultedMessage == null
                        ? e.Message
                        : (faultedMessage + Environment.NewLine + e.Message);
                    result = false;
                }
            }

            if (!string.IsNullOrEmpty(faultedMessage))
            {
                processor.Config.Logger.LogDebug(LocalizableStrings.Error_Evaluation_Expression + faultedMessage);
            }
            return result;
        }

        private static int? AttemptBooleanComparison(object? left, object? right)
        {
            bool leftIsBool = Map.TryConvert(left!, out bool lb);
            bool rightIsBool = Map.TryConvert(right!, out bool rb);

            if (!leftIsBool || !rightIsBool)
            {
                return null;
            }

            return lb.CompareTo(rb);
        }

        private static int? AttemptComparableComparison(object? left, object? right)
        {
            if (left is not IComparable ls || right is not IComparable rs)
            {
                return null;
            }

            return ls.CompareTo(rs);
        }

        private static int? AttemptMultiValueComparison(object? left, object? right)
        {
            if (MultiValueParameter.TryPerformMultiValueEqual(left!, right!, out bool result))
            {
                return result ? 0 : -1;
            }

            return null;
        }

        private static int? AttemptLexicographicComparison(object? left, object? right)
        {
            if (left is not string ls || right is not string rs)
            {
                return null;
            }

            return string.Compare(ls, rs, StringComparison.OrdinalIgnoreCase);
        }

        private static int? AttemptNumericComparison(object? left, object? right)
        {
            bool leftIsDouble = Map.TryConvert(left!, out double ld);
            bool rightIsDouble = Map.TryConvert(right!, out double rd);

            if (!leftIsDouble)
            {
                if (!Map.TryConvert(left!, out long ll))
                {
                    return null;
                }

                ld = ll;
            }

            if (!rightIsDouble)
            {
                if (!Map.TryConvert(right!, out long rl))
                {
                    return null;
                }

                rd = rl;
            }

            return ld.CompareTo(rd);
        }

        private static int? AttemptVersionComparison(object? left, object? right)
        {
            Version? lv = left as Version;

            if (lv == null)
            {
                if (left is not string ls || !Version.TryParse(ls, out lv))
                {
                    return null;
                }
            }

            Version? rv = right as Version;

            if (rv == null)
            {
                if (right is not string rs || !Version.TryParse(rs, out rv))
                {
                    return null;
                }
            }

            return lv.CompareTo(rv);
        }
    }
}