/// <summary> /// Returns true if this node evaluates to an empty string, /// otherwise false. /// It may be cheaper to determine whether an expression will evaluate /// to empty than to fully evaluate it. /// Implementations should cache the result so that calls after the first are free. /// </summary> internal override bool EvaluatesToEmpty(ConditionEvaluator.IConditionEvaluationState state) { if (_cachedExpandedValue == null) { if (_expandable) { string expandBreakEarly = state.ExpandIntoStringBreakEarly(_value); if (expandBreakEarly == null) { // It broke early: we can't store the value, we just // know it's non empty return(false); } // It didn't break early, the result is accurate, // so store it so the work isn't done again. _cachedExpandedValue = expandBreakEarly; } else { _cachedExpandedValue = _value; } } return(_cachedExpandedValue.Length == 0); }
/// <summary> /// Evaluate as boolean /// </summary> internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) { bool isLeftNum = LeftChild.TryNumericEvaluate(state, out double leftNum); bool isLeftVersion = LeftChild.TryVersionEvaluate(state, out Version leftVersion); bool isRightNum = RightChild.TryNumericEvaluate(state, out double rightNum); bool isRightVersion = RightChild.TryVersionEvaluate(state, out Version rightVersion); if ((!isLeftNum && !isLeftVersion) || (!isRightNum && !isRightVersion)) { ProjectErrorUtilities.ThrowInvalidProject( state.ElementLocation, "ComparisonOnNonNumericExpression", state.Condition, /* helpfully display unexpanded token and expanded result in error message */ isLeftNum ? RightChild.GetUnexpandedValue(state) : LeftChild.GetUnexpandedValue(state), isLeftNum ? RightChild.GetExpandedValue(state) : LeftChild.GetExpandedValue(state)); } return((isLeftNum, isLeftVersion, isRightNum, isRightVersion) switch { (true, _, true, _) => Compare(leftNum, rightNum), (_, true, _, true) => Compare(leftVersion, rightVersion), (true, _, _, true) => Compare(leftNum, rightVersion), (_, true, true, _) => Compare(leftVersion, rightNum), _ => false });
/// <summary> /// Evaluate as boolean /// </summary> internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) { ProjectErrorUtilities.VerifyThrowInvalidProject (LeftChild.CanBoolEvaluate(state), state.ElementLocation, "ExpressionDoesNotEvaluateToBoolean", LeftChild.GetUnexpandedValue(state), LeftChild.GetExpandedValue(state), state.Condition); if (LeftChild.BoolEvaluate(state)) { // Short circuit return(true); } else { ProjectErrorUtilities.VerifyThrowInvalidProject (RightChild.CanBoolEvaluate(state), state.ElementLocation, "ExpressionDoesNotEvaluateToBoolean", RightChild.GetUnexpandedValue(state), RightChild.GetExpandedValue(state), state.Condition); return(RightChild.BoolEvaluate(state)); } }
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; } }
/// <summary> /// Evaluate as boolean /// </summary> internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) { if (!LeftChild.TryBoolEvaluate(state, out bool leftBool)) { ProjectErrorUtilities.ThrowInvalidProject( state.ElementLocation, "ExpressionDoesNotEvaluateToBoolean", LeftChild.GetUnexpandedValue(state), LeftChild.GetExpandedValue(state), state.Condition); } if (!leftBool) { // Short circuit return(false); } else { if (!RightChild.TryBoolEvaluate(state, out bool rightBool)) { ProjectErrorUtilities.ThrowInvalidProject( state.ElementLocation, "ExpressionDoesNotEvaluateToBoolean", RightChild.GetUnexpandedValue(state), RightChild.GetExpandedValue(state), state.Condition); } return(rightBool); } }
/// <summary> /// Whether it can be evaluated as a Version /// </summary> internal override bool CanVersionEvaluate(ConditionEvaluator.IConditionEvaluationState state) { // Check if the value can be formatted as a Version number // This is needed for nodes that identify as Numeric but can't be parsed as numbers (e.g. 8.1.1.0 vs 8.1) Version unused; return(Version.TryParse(_value, out unused)); }
internal override bool CanVersionEvaluate(ConditionEvaluator.IConditionEvaluationState state) { if (ShouldBeTreatedAsVisualStudioVersion(state)) { return(true); } return(Version.TryParse(GetExpandedValue(state), out _)); }
internal override bool CanNumericEvaluate(ConditionEvaluator.IConditionEvaluationState state) { if (ShouldBeTreatedAsVisualStudioVersion(state)) { return(true); } return(ConversionUtilities.ValidDecimalOrHexNumber(GetExpandedValue(state))); }
internal override Version VersionEvaluate(ConditionEvaluator.IConditionEvaluationState state) { if (ShouldBeTreatedAsVisualStudioVersion(state)) { return(Version.Parse(MSBuildConstants.CurrentVisualStudioVersion)); } return(Version.Parse(GetExpandedValue(state))); }
/// <summary> /// Evaluate as numeric /// </summary> internal override double NumericEvaluate(ConditionEvaluator.IConditionEvaluationState state) { if (ShouldBeTreatedAsVisualStudioVersion(state)) { return(ConversionUtilities.ConvertDecimalOrHexToDouble(MSBuildConstants.CurrentVisualStudioVersion)); } return(ConversionUtilities.ConvertDecimalOrHexToDouble(GetExpandedValue(state))); }
/// <summary> /// Check that the number of function arguments is correct. /// </summary> /// <param name="expected"></param> private void VerifyArgumentCount(int expected, ConditionEvaluator.IConditionEvaluationState state) { ProjectErrorUtilities.VerifyThrowInvalidProject (_arguments.Count == expected, state.ElementLocation, "IncorrectNumberOfFunctionArguments", state.Condition, _arguments.Count, expected); }
/// <summary> /// The main evaluate entry point for expression trees /// </summary> /// <param name="state"></param> /// <returns></returns> internal bool Evaluate(ConditionEvaluator.IConditionEvaluationState state) { ProjectErrorUtilities.VerifyThrowInvalidProject( CanBoolEvaluate(state), state.ElementLocation, "ConditionNotBooleanDetail", state.Condition, GetExpandedValue(state)); return(BoolEvaluate(state)); }
/// <summary> /// Evaluate as boolean /// </summary> internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) { ProjectErrorUtilities.VerifyThrowInvalidProject (LeftChild.TryBoolEvaluate(state, out bool boolValue), state.ElementLocation, "ExpressionDoesNotEvaluateToBoolean", LeftChild.GetUnexpandedValue(state), LeftChild.GetExpandedValue(state), state.Condition); return !boolValue; }
internal override bool TryNumericEvaluate(ConditionEvaluator.IConditionEvaluationState state, out double result) { if (ShouldBeTreatedAsVisualStudioVersion(state)) { result = ConversionUtilities.ConvertDecimalOrHexToDouble(MSBuildConstants.CurrentVisualStudioVersion); return(true); } else { return(ConversionUtilities.TryConvertDecimalOrHexToDouble(GetExpandedValue(state), out result)); } }
internal override bool TryVersionEvaluate(ConditionEvaluator.IConditionEvaluationState state, out Version result) { if (ShouldBeTreatedAsVisualStudioVersion(state)) { result = Version.Parse(MSBuildConstants.CurrentVisualStudioVersion); return(true); } else { return(Version.TryParse(GetExpandedValue(state), out result)); } }
internal bool Evaluate(ConditionEvaluator.IConditionEvaluationState state) { if (!TryBoolEvaluate(state, out bool boolValue)) { ProjectErrorUtilities.ThrowInvalidProject( state.ElementLocation, "ConditionNotBooleanDetail", state.Condition, GetExpandedValue(state)); } return(boolValue); }
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. 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); return(Compare(leftExpandedValue, rightExpandedValue)); }
/// <summary> /// Returns true if this node evaluates to an empty string, /// otherwise false. /// It may be cheaper to determine whether an expression will evaluate /// to empty than to fully evaluate it. /// Implementations should cache the result so that calls after the first are free. /// </summary> internal override bool EvaluatesToEmpty(ConditionEvaluator.IConditionEvaluationState state) { if (_cachedExpandedValue == null) { if (_expandable) { switch (_value.Length) { case 0: _cachedExpandedValue = String.Empty; return(true); // If the length is 1 or 2, it can't possibly be a property, item, or metadata, and it isn't empty. case 1: case 2: _cachedExpandedValue = _value; return(false); default: if (_value[1] != '(' || (_value[0] != '$' && _value[0] != '%' && _value[0] != '@') || _value[_value.Length - 1] != ')') { // This isn't just a property, item, or metadata value, and it isn't empty. return(false); } break; } string expandBreakEarly = state.ExpandIntoStringBreakEarly(_value); if (expandBreakEarly == null) { // It broke early: we can't store the value, we just // know it's non empty return(false); } // It didn't break early, the result is accurate, // so store it so the work isn't done again. _cachedExpandedValue = expandBreakEarly; } else { _cachedExpandedValue = _value; } } return(_cachedExpandedValue.Length == 0); }
/// <summary> /// Value after any item and property expressions are expanded /// </summary> /// <returns></returns> internal override string GetExpandedValue(ConditionEvaluator.IConditionEvaluationState state) { if (_cachedExpandedValue == null) { if (_expandable) { _cachedExpandedValue = state.ExpandIntoString(_value); } else { _cachedExpandedValue = _value; } } return(_cachedExpandedValue); }
/// <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. if (LeftChild.EvaluatesToEmpty(state) || RightChild.EvaluatesToEmpty(state)) { UpdateConditionedProperties(state); return(Compare(LeftChild.EvaluatesToEmpty(state), RightChild.EvaluatesToEmpty(state))); } if (LeftChild.CanNumericEvaluate(state) && RightChild.CanNumericEvaluate(state)) { return(Compare(LeftChild.NumericEvaluate(state), RightChild.NumericEvaluate(state))); } else if (LeftChild.CanBoolEvaluate(state) && RightChild.CanBoolEvaluate(state)) { return(Compare(LeftChild.BoolEvaluate(state), RightChild.BoolEvaluate(state))); } else // string comparison { string leftExpandedValue = LeftChild.GetExpandedValue(state); string rightExpandedValue = RightChild.GetExpandedValue(state); ProjectErrorUtilities.VerifyThrowInvalidProject (leftExpandedValue != null && rightExpandedValue != null, state.ElementLocation, "IllFormedCondition", state.Condition); UpdateConditionedProperties(state); return(Compare(leftExpandedValue, rightExpandedValue)); } }
/// <summary> /// Evaluate as boolean /// </summary> internal override bool BoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) { bool isLeftNum = LeftChild.CanNumericEvaluate(state); bool isLeftVersion = LeftChild.CanVersionEvaluate(state); bool isRightNum = RightChild.CanNumericEvaluate(state); bool isRightVersion = RightChild.CanVersionEvaluate(state); bool isNumeric = isLeftNum && isRightNum; bool isVersion = isLeftVersion && isRightVersion; bool isValidComparison = isNumeric || isVersion || (isLeftNum && isRightVersion) || (isLeftVersion && isRightNum); ProjectErrorUtilities.VerifyThrowInvalidProject (isValidComparison, state.ElementLocation, "ComparisonOnNonNumericExpression", state.Condition, /* helpfully display unexpanded token and expanded result in error message */ LeftChild.CanNumericEvaluate(state) ? RightChild.GetUnexpandedValue(state) : LeftChild.GetUnexpandedValue(state), LeftChild.CanNumericEvaluate(state) ? RightChild.GetExpandedValue(state) : LeftChild.GetExpandedValue(state)); // If the values identify as numeric, make that comparison instead of the Version comparison since numeric has a stricter definition if (isNumeric) { return(Compare(LeftChild.NumericEvaluate(state), RightChild.NumericEvaluate(state))); } else if (isVersion) { return(Compare(LeftChild.VersionEvaluate(state), RightChild.VersionEvaluate(state))); } // If the numbers are of a mixed type, call that specific Compare method if (isLeftNum && isRightVersion) { return(Compare(LeftChild.NumericEvaluate(state), RightChild.VersionEvaluate(state))); } else if (isLeftVersion && isRightNum) { return(Compare(LeftChild.VersionEvaluate(state), RightChild.NumericEvaluate(state))); } // Throw error here as this code should be unreachable ErrorUtilities.ThrowInternalErrorUnreachable(); return(false); }
/// <summary> /// Should this node be treated as an expansion of VisualStudioVersion, rather than /// its literal meaning? /// </summary> /// <remarks> /// Needed to provide a compat shim for numeric/version comparisons /// on MSBuildToolsVersion, which were fine when it was a number /// but now cause the project to throw InvalidProjectException when /// ToolsVersion is "Current". https://github.com/Microsoft/msbuild/issues/4150 /// </remarks> private bool ShouldBeTreatedAsVisualStudioVersion(ConditionEvaluator.IConditionEvaluationState state) { if (!_shouldBeTreatedAsVisualStudioVersion.HasValue) { // Treat specially if the node would expand to "Current". // Do this check first, because if it's not (common) we can early-out and the next // expansion will be cheap because this will populate the cached expanded value. if (string.Equals(GetExpandedValue(state), MSBuildConstants.CurrentToolsVersion, StringComparison.Ordinal)) { // and it is just an expansion of MSBuildToolsVersion _shouldBeTreatedAsVisualStudioVersion = string.Equals(_value, "$(MSBuildToolsVersion)", StringComparison.OrdinalIgnoreCase); } else { _shouldBeTreatedAsVisualStudioVersion = false; } } return(_shouldBeTreatedAsVisualStudioVersion.Value); }
/// <summary> /// Whether it can be evaluated as a boolean: never allowed for numerics /// </summary> internal override bool CanBoolEvaluate(ConditionEvaluator.IConditionEvaluationState state) { // Numeric expressions are never allowed to be treated as booleans. return(false); }
/// <summary> /// Evaluate as numeric /// </summary> internal override double NumericEvaluate(ConditionEvaluator.IConditionEvaluationState state) { return(ConversionUtilities.ConvertDecimalOrHexToDouble(_value)); }
/// <summary> /// Evaluate as a Version /// </summary> internal override Version VersionEvaluate(ConditionEvaluator.IConditionEvaluationState state) { return(Version.Parse(_value)); }
/// <summary> /// Whether it can be evaluated as numeric /// </summary> internal override bool CanNumericEvaluate(ConditionEvaluator.IConditionEvaluationState state) { // It is not always possible to numerically evaluate even a numerical expression - // for example, it may overflow a double. So check here. return(ConversionUtilities.ValidDecimalOrHexNumber(_value)); }
internal override bool TryBoolEvaluate(ConditionEvaluator.IConditionEvaluationState state, out bool result) { return(ConversionUtilities.TryConvertStringToBool(GetExpandedValue(state), out result)); }
/// <summary> /// Value before any item and property expressions are expanded /// </summary> /// <returns></returns> internal override string GetUnexpandedValue(ConditionEvaluator.IConditionEvaluationState state) { return(_value); }
private void AssertParseEvaluate(Parser p, string expression, Expander <ProjectPropertyInstance, ProjectItemInstance> expander, bool expected, ConditionEvaluator.IConditionEvaluationState state) { if (expander.Metadata == null) { expander.Metadata = new StringMetadataTable(null); } GenericExpressionNode tree = p.Parse(expression, ParserOptions.AllowAll, MockElementLocation.Instance); if (state == null) { state = new ConditionEvaluator.ConditionEvaluationState <ProjectPropertyInstance, ProjectItemInstance> ( String.Empty, expander, ExpanderOptions.ExpandAll, null, Directory.GetCurrentDirectory(), ElementLocation.EmptyLocation, FileSystems.Default ); } bool result = tree.Evaluate(state); Assert.Equal(expected, result); }
private void AssertParseEvaluateThrow(Parser p, string expression, Expander <ProjectPropertyInstance, ProjectItemInstance> expander, ConditionEvaluator.IConditionEvaluationState state) { bool fExceptionCaught; if (expander.Metadata == null) { expander.Metadata = new StringMetadataTable(null); } try { fExceptionCaught = false; GenericExpressionNode tree = p.Parse(expression, ParserOptions.AllowAll, MockElementLocation.Instance); if (state == null) { state = new ConditionEvaluator.ConditionEvaluationState <ProjectPropertyInstance, ProjectItemInstance> ( String.Empty, expander, ExpanderOptions.ExpandAll, null, Directory.GetCurrentDirectory(), ElementLocation.EmptyLocation, FileSystems.Default ); } tree.Evaluate(state); } catch (InvalidProjectFileException e) { Console.WriteLine(e.BaseMessage); fExceptionCaught = true; } Assert.True(fExceptionCaught); }