File: Evaluation\Conditionals\MultipleComparisonExpressionNode.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 Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Shared;
 
#nullable disable
 
namespace Microsoft.Build.Evaluation
{
    /// <summary>
    /// Evaluates as boolean and evaluates children as boolean, numeric, or string.
    /// Order in which comparisons are attempted is numeric, boolean, then string.
    /// Updates conditioned properties table.
    /// </summary>
    internal abstract class MultipleComparisonNode : OperatorExpressionNode
    {
        private bool _conditionedPropertiesUpdated = false;
 
        /// <summary>
        /// Compare numbers
        /// </summary>
        protected abstract bool Compare(double left, double right);
 
        /// <summary>
        /// Compare booleans
        /// </summary>
        protected abstract bool Compare(bool left, bool right);
 
        /// <summary>
        /// Compare strings
        /// </summary>
        protected abstract bool Compare(string left, string right);
 
        /// <summary>
        /// Evaluates as boolean and evaluates children as boolean, numeric, or string.
        /// Order in which comparisons are attempted is numeric, boolean, then string.
        /// Updates conditioned properties table.
        /// </summary>
        internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state)
        {
            ProjectErrorUtilities.VerifyThrowInvalidProject(
                LeftChild != null && RightChild != null,
                 state.ElementLocation,
                 "IllFormedCondition",
                 state.Condition);
 
            // It's sometimes possible to bail out of expansion early if we just need to know whether
            // the result is empty string.
            // If at least one of the left or the right hand side will evaluate to empty,
            // and we know which do, then we already have enough information to evaluate this expression.
            // That means we don't have to fully expand a condition like " '@(X)' == '' "
            // which is a performance advantage if @(X) is a huge item list.
 
            // This is the possible case of an expression similar to '$(a)' == '', where a usage of uninitialized
            //  property is reasonable and should not be flagged by uninitialized reads detection.
            // So if at least one side is empty, we know to signal to PropertiesUseTracker to not flag in this scope.
            // The other side might not be property at all - that's fine, as then PropertiesUseTracker won't be even called.
            if (LeftChild.IsUnexpandedValueEmpty() || RightChild.IsUnexpandedValueEmpty())
            {
                state.PropertiesUseTracker.PropertyReadContext = PropertyReadContext.ConditionEvaluationWithOneSideEmpty;
            }
 
            bool leftEmpty = LeftChild.EvaluatesToEmpty(state);
            bool rightEmpty = RightChild.EvaluatesToEmpty(state);
            if (leftEmpty || rightEmpty)
            {
                UpdateConditionedProperties(state);
 
                return Compare(leftEmpty, rightEmpty);
            }
            else if (LeftChild.TryNumericEvaluate(state, out double leftNumericValue) && RightChild.TryNumericEvaluate(state, out double rightNumericValue))
            {
                // The left child evaluating to a number and the right child not evaluating to a number
                // is insufficient to say they are not equal because $(MSBuildToolsVersion) evaluates to
                // the string "Current" most of the time but when doing numeric comparisons, is treated
                // as a version and returns "17.0" (or whatever the current tools version is). This means
                // that if '$(MSBuildToolsVersion)' is "equal" to BOTH '17.0' and 'Current' (if 'Current'
                // is 17.0).
                return Compare(leftNumericValue, rightNumericValue);
            }
            else if (LeftChild.TryBoolEvaluate(state, out bool leftBoolValue) && RightChild.TryBoolEvaluate(state, out bool rightBoolValue))
            {
                return Compare(leftBoolValue, rightBoolValue);
            }
 
            string leftExpandedValue = LeftChild.GetExpandedValue(state);
            string rightExpandedValue = RightChild.GetExpandedValue(state);
 
            ProjectErrorUtilities.VerifyThrowInvalidProject(
                leftExpandedValue != null && rightExpandedValue != null,
                    state.ElementLocation,
                    "IllFormedCondition",
                    state.Condition);
 
            UpdateConditionedProperties(state);
 
            // reset back the property read context (it's no longer a condition with one side empty)
            state.PropertiesUseTracker.PropertyReadContext = PropertyReadContext.ConditionEvaluation;
 
            return Compare(leftExpandedValue, rightExpandedValue);
        }
 
        /// <summary>
        /// Reset temporary state
        /// </summary>
        internal override void ResetState()
        {
            base.ResetState();
            _conditionedPropertiesUpdated = false;
        }
 
        /// <summary>
        /// Updates the conditioned properties table if it hasn't already been done.
        /// </summary>
        private void UpdateConditionedProperties(ConditionEvaluator.IConditionEvaluationState state)
        {
            if (!_conditionedPropertiesUpdated && state.ConditionedPropertiesInProject != null)
            {
                string leftUnexpandedValue = LeftChild.GetUnexpandedValue(state);
                string rightUnexpandedValue = RightChild.GetUnexpandedValue(state);
 
                if (leftUnexpandedValue != null)
                {
                    ConditionEvaluator.UpdateConditionedPropertiesTable(
                        state.ConditionedPropertiesInProject,
                         leftUnexpandedValue,
                         RightChild.GetExpandedValue(state));
                }
 
                if (rightUnexpandedValue != null)
                {
                    ConditionEvaluator.UpdateConditionedPropertiesTable(
                        state.ConditionedPropertiesInProject,
                         rightUnexpandedValue,
                         LeftChild.GetExpandedValue(state));
                }
 
                _conditionedPropertiesUpdated = true;
            }
        }
    }
}