|
// 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.Concurrent;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using BuildEventContext = Microsoft.Build.Framework.BuildEventContext;
using ElementLocation = Microsoft.Build.Construction.ElementLocation;
using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService;
using TaskItem = Microsoft.Build.Execution.ProjectItemInstance.TaskItem;
namespace Microsoft.Build.Evaluation
{
internal static class ConditionEvaluator
{
/// <summary>
/// Update our table which keeps track of all the properties that are referenced
/// inside of a condition and the string values that they are being tested against.
/// So, for example, if the condition was " '$(Configuration)' == 'Debug' ", we
/// would get passed in leftValue="$(Configuration)" and rightValueExpanded="Debug".
/// This call would add the string "Debug" to the list of possible values for the
/// "Configuration" property.
///
/// This method also handles the case when two or more properties are being
/// concatenated together with a vertical bar, as in '
/// $(Configuration)|$(Platform)' == 'Debug|x86'
/// </summary>
/// <param name="conditionedPropertiesTable">List of possible values, keyed by property name.</param>
/// <param name="leftValue">The raw value on the left side of the operator.</param>
/// <param name="rightValueExpanded">The fully expanded value on the right side of the operator.</param>
internal static void UpdateConditionedPropertiesTable(
Dictionary<string, List<string>> conditionedPropertiesTable,
string leftValue,
string rightValueExpanded)
{
if ((conditionedPropertiesTable != null) && (rightValueExpanded.Length > 0))
{
// The left side should be exactly "$(propertyname)" or "$(propertyname1)|$(propertyname2)"
// or "$(propertyname1)|$(propertyname2)|$(propertyname3)", etc. Anything else,
// and we don't touch the table.
// Split up the leftValue into pieces based on the vertical bar character.
// PERF: Avoid allocations from string.Split by forming spans between 'pieceStart' and 'pieceEnd'
var pieceStart = 0;
// Loop through each of the pieces.
while (true)
{
var pieceSeparator = leftValue.IndexOf('|', pieceStart);
var lastPiece = pieceSeparator < 0;
var pieceEnd = lastPiece ? leftValue.Length : pieceSeparator;
if (TryGetSingleProperty(leftValue.AsSpan(), pieceStart, pieceEnd - pieceStart, out ReadOnlySpan<char> propertyName))
{
// Find the first vertical bar on the right-hand-side expression.
var indexOfVerticalBar = rightValueExpanded.IndexOf('|');
string rightValueExpandedPiece;
// If there was no vertical bar, then just use the remainder of the right-hand-side
// expression as the value of the property, and terminate the loop after this iteration.
// Also, if we're on the last segment of the left-hand-side, then use the remainder
// of the right-hand-side expression as the value of the property.
if ((indexOfVerticalBar == -1) || lastPiece)
{
rightValueExpandedPiece = rightValueExpanded;
lastPiece = true;
}
else
{
// If we found a vertical bar, then the portion before the vertical bar is the
// property value which we will store in our table. Then remove that portion
// from the original string so that the next iteration of the loop can easily search
// for the first vertical bar again.
rightValueExpandedPiece = rightValueExpanded.Substring(0, indexOfVerticalBar);
rightValueExpanded = rightValueExpanded.Substring(indexOfVerticalBar + 1);
}
// Get the string collection for this property name, if one already exists.
// If not already in the table, add a new entry for it.
string propertyNameString = propertyName.ToString();
if (!conditionedPropertiesTable.TryGetValue(propertyNameString, out List<string>? conditionedPropertyValues))
{
conditionedPropertyValues = new List<string>();
conditionedPropertiesTable[propertyNameString] = conditionedPropertyValues;
}
// If the "rightValueExpanded" is not already in the string collection
// for this property name, add it now.
if (!conditionedPropertyValues.Contains(rightValueExpandedPiece))
{
conditionedPropertyValues.Add(rightValueExpandedPiece);
}
}
if (lastPiece)
{
break;
}
pieceStart = pieceSeparator + 1;
}
}
}
// Internal for testing purposes
internal static bool TryGetSingleProperty(ReadOnlySpan<char> input, int beginning, int length, out ReadOnlySpan<char> propertyName)
{
// This code is simulating the regex pattern: ^\$\(([^\$\(\)]*)\)$
if (input.Length < beginning + 3 ||
input[beginning] != '$' ||
input[beginning + 1] != '(' ||
input[beginning + length - 1] != ')' ||
ContainsInvalidCharacter(input.Slice(beginning + 2, length - 3)))
{
propertyName = null;
return false;
}
propertyName = input.Slice(beginning + 2, length - 3);
return true;
static bool ContainsInvalidCharacter(ReadOnlySpan<char> span)
{
return
span.IndexOf('$') != -1 ||
span.IndexOf('(') != -1 ||
span.IndexOf(')') != -1;
}
}
// Implements a pool of expression trees for each condition.
// This is because an expression tree is a mutually exclusive resource (has non thread safe state while it evaluates).
// During high demand when all expression trees are busy evaluating, a new expression tree is created and added to the pool.
// The pool is represented by the ConcurrentStack.
private struct ExpressionTreeForCurrentOptionsWithSize
{
// condition string -> pool of expression trees
private readonly ConcurrentDictionary<string, ConcurrentStack<GenericExpressionNode>> _conditionPools;
private int _mOptimisticSize;
public readonly int OptimisticSize => _mOptimisticSize;
public ExpressionTreeForCurrentOptionsWithSize(ConcurrentDictionary<string, ConcurrentStack<GenericExpressionNode>> conditionPools)
{
_conditionPools = conditionPools;
_mOptimisticSize = conditionPools.Count;
}
public ConcurrentStack<GenericExpressionNode> GetOrAdd(string condition, Func<string, ConcurrentStack<GenericExpressionNode>> addFunc)
{
if (!_conditionPools.TryGetValue(condition, out var stack))
{
// Count how many conditions there are in the cache.
// The condition evaluator will flush the cache when some threshold is exceeded.
Interlocked.Increment(ref _mOptimisticSize);
stack = _conditionPools.GetOrAdd(condition, addFunc);
}
return stack;
}
}
// Cached expression trees for all the combinations of condition strings and parser options
private static volatile ConcurrentDictionary<int, ExpressionTreeForCurrentOptionsWithSize> s_cachedExpressionTrees = new ConcurrentDictionary<int, ExpressionTreeForCurrentOptionsWithSize>();
/// <summary>
/// For debugging leaks, a way to disable caching expression trees, to reduce noise
/// </summary>
private static readonly bool s_disableExpressionCaching = (Environment.GetEnvironmentVariable("MSBUILDDONOTCACHEEXPRESSIONS") == "1");
/// <summary>
/// Evaluates a string representing a condition from a "condition" attribute.
/// If the condition is a malformed string, it throws an InvalidProjectFileException.
/// This method uses cached expression trees to avoid generating them from scratch every time it's called.
/// This method is thread safe and is called from engine and task execution module threads
/// </summary>
internal static bool EvaluateCondition<P, I>(
string condition,
ParserOptions options,
Expander<P, I> expander,
ExpanderOptions expanderOptions,
string evaluationDirectory,
ElementLocation elementLocation,
IFileSystem fileSystem,
LoggingContext? loggingContext,
ProjectRootElementCacheBase? projectRootElementCache = null)
where P : class, IProperty
where I : class, IItem
{
return EvaluateConditionCollectingConditionedProperties(
condition,
options,
expander,
expanderOptions,
conditionedPropertiesTable: null /* do not collect conditioned properties */,
evaluationDirectory,
elementLocation,
fileSystem,
loggingContext,
projectRootElementCache);
}
/// <summary>
/// Evaluates a string representing a condition from a "condition" attribute.
/// If the condition is a malformed string, it throws an InvalidProjectFileException.
/// This method uses cached expression trees to avoid generating them from scratch every time it's called.
/// This method is thread safe and is called from engine and task execution module threads
/// Logging service may be null.
/// </summary>
internal static bool EvaluateConditionCollectingConditionedProperties<P, I>(
string condition,
ParserOptions options,
Expander<P, I> expander,
ExpanderOptions expanderOptions,
Dictionary<string, List<string>>? conditionedPropertiesTable,
string evaluationDirectory,
ElementLocation elementLocation,
IFileSystem fileSystem,
LoggingContext? loggingContext,
ProjectRootElementCacheBase? projectRootElementCache = null)
where P : class, IProperty
where I : class, IItem
{
ErrorUtilities.VerifyThrowArgumentNull(condition, nameof(condition));
ErrorUtilities.VerifyThrowArgumentNull(expander, nameof(expander));
ErrorUtilities.VerifyThrowArgumentLength(evaluationDirectory, nameof(evaluationDirectory));
// An empty condition is equivalent to a "true" condition.
if (condition.Length == 0)
{
return true;
}
// If the condition wasn't empty, there must be a location for it
ErrorUtilities.VerifyThrowArgumentNull(elementLocation, nameof(elementLocation));
// Get the expression tree cache for the current parsing options.
var cachedExpressionTreesForCurrentOptions = s_cachedExpressionTrees.GetOrAdd(
(int)options,
_ => new ExpressionTreeForCurrentOptionsWithSize(new ConcurrentDictionary<string, ConcurrentStack<GenericExpressionNode>>(StringComparer.Ordinal)));
cachedExpressionTreesForCurrentOptions = FlushCacheIfLargerThanThreshold(options, cachedExpressionTreesForCurrentOptions);
// Get the pool of expressions for this condition.
var expressionPool = cachedExpressionTreesForCurrentOptions.GetOrAdd(condition, _ => new ConcurrentStack<GenericExpressionNode>());
// Try and see if there's an available expression tree in the pool.
// If not, parse a new expression tree and add it back to the pool.
if (!expressionPool.TryPop(out var parsedExpression))
{
var conditionParser = new Parser();
#region REMOVE_COMPAT_WARNING
conditionParser.LoggingServices = loggingContext?.LoggingService;
conditionParser.LogBuildEventContext = loggingContext?.BuildEventContext ?? BuildEventContext.Invalid;
#endregion
parsedExpression = conditionParser.Parse(condition, options, elementLocation);
}
bool result;
var state = new ConditionEvaluationState<P, I>(
condition,
expander,
expanderOptions,
conditionedPropertiesTable,
evaluationDirectory,
elementLocation,
fileSystem,
projectRootElementCache);
expander.PropertiesUseTracker.PropertyReadContext = PropertyReadContext.ConditionEvaluation;
// We are evaluating this expression now and it can cache some state for the duration,
// so we don't want multiple threads working on the same expression
lock (parsedExpression)
{
try
{
result = parsedExpression.Evaluate(state);
}
finally
{
parsedExpression.ResetState();
if (!s_disableExpressionCaching)
{
// Finished using the expression tree. Add it back to the pool so other threads can use it.
expressionPool.Push(parsedExpression);
}
expander.PropertiesUseTracker.ResetPropertyReadContext();
}
}
return result;
}
private static ExpressionTreeForCurrentOptionsWithSize FlushCacheIfLargerThanThreshold(
ParserOptions options,
ExpressionTreeForCurrentOptionsWithSize cachedExpressionTreesForCurrentOptions)
{
if (cachedExpressionTreesForCurrentOptions.OptimisticSize > 3000)
{
// VS stress tests could fill up this cache without end, for example if they use
// random configuration names - those appear in conditional expressions.
// So if we hit a limit that we should never hit in normal circumstances in VS,
// and rarely, periodically hit in normal circumstances in large tree builds,
// just clear out the cache. It can start repopulating again. Some kind of prioritized
// aging isn't worth it: although the hit rate of these caches is excellent (nearly 100%)
// the cost of reparsing expressions should the cache be cleared is not particularly large.
// Loading Australian Government in VS, there are 3 of these tables, two with about 50
// entries and one with about 650 entries. So 3000 seems large enough.
cachedExpressionTreesForCurrentOptions = s_cachedExpressionTrees.AddOrUpdate(
(int)options,
_ =>
new ExpressionTreeForCurrentOptionsWithSize(
new ConcurrentDictionary<string, ConcurrentStack<GenericExpressionNode>>(StringComparer.Ordinal)),
(key, existing) =>
{
if (existing.OptimisticSize > 3000)
{
return
new ExpressionTreeForCurrentOptionsWithSize(
new ConcurrentDictionary<string, ConcurrentStack<GenericExpressionNode>>(StringComparer.Ordinal));
}
else
{
return existing;
}
});
}
return cachedExpressionTreesForCurrentOptions;
}
internal interface IConditionEvaluationState
{
string Condition { get; }
string EvaluationDirectory { get; }
ElementLocation ElementLocation { get; }
PropertiesUseTracker PropertiesUseTracker { get; }
/// <summary>
/// Table of conditioned properties and their values.
/// Used to populate configuration lists in some project systems.
/// If this is null, as it is for command line builds, conditioned properties
/// are not recorded.
/// </summary>
Dictionary<string, List<string>>? ConditionedPropertiesInProject { get; }
/// <summary>
/// May return null if the expression would expand to non-empty and it broke out early.
/// Otherwise, returns the correctly expanded expression.
/// </summary>
string ExpandIntoStringBreakEarly(string expression);
/// <summary>
/// Expands the specified expression into a list of TaskItem's.
/// </summary>
IList<TaskItem> ExpandIntoTaskItems(string expression);
/// <summary>
/// Expands the specified expression into a string.
/// </summary>
string ExpandIntoString(string expression);
/// <summary>
/// PRE cache
/// </summary>
ProjectRootElementCacheBase? LoadedProjectsCache { get; }
IFileSystem FileSystem { get; }
}
/// <summary>
/// All the state necessary for the evaluation of conditionals so that the expression tree
/// is stateless and reusable
/// </summary>
internal class ConditionEvaluationState<P, I> : IConditionEvaluationState
where P : class, IProperty
where I : class, IItem
{
private readonly Expander<P, I> _expander;
private readonly ExpanderOptions _expanderOptions;
/// <summary>
/// Condition that was parsed. This does not belong here,
/// it belongs to the expression tree, not the condition evaluation state.
/// </summary>
public string Condition { get; }
public string EvaluationDirectory { get; }
public ElementLocation ElementLocation { get; }
public PropertiesUseTracker PropertiesUseTracker => _expander.PropertiesUseTracker;
public IFileSystem FileSystem { get; }
/// <summary>
/// Table of conditioned properties and their values.
/// Used to populate configuration lists in some project systems.
/// If this is null, as it is for command line builds, conditioned properties
/// are not recorded.
/// </summary>
public Dictionary<string, List<string>>? ConditionedPropertiesInProject { get; }
/// <summary>
/// PRE collection.
/// </summary>
public ProjectRootElementCacheBase? LoadedProjectsCache { get; }
internal ConditionEvaluationState(
string condition,
Expander<P, I> expander,
ExpanderOptions expanderOptions,
Dictionary<string, List<string>>? conditionedPropertiesInProject,
string evaluationDirectory,
ElementLocation elementLocation,
IFileSystem fileSystem,
ProjectRootElementCacheBase? projectRootElementCache = null)
{
ErrorUtilities.VerifyThrowArgumentNull(condition, nameof(condition));
ErrorUtilities.VerifyThrowArgumentNull(expander, nameof(expander));
ErrorUtilities.VerifyThrowArgumentNull(evaluationDirectory, nameof(evaluationDirectory));
ErrorUtilities.VerifyThrowArgumentNull(elementLocation, nameof(elementLocation));
Condition = condition;
_expander = expander;
_expanderOptions = expanderOptions;
ConditionedPropertiesInProject = conditionedPropertiesInProject; // May be null
EvaluationDirectory = evaluationDirectory;
ElementLocation = elementLocation;
LoadedProjectsCache = projectRootElementCache;
FileSystem = fileSystem;
}
/// <summary>
/// May return null if the expression would expand to non-empty and it broke out early.
/// Otherwise, returns the correctly expanded expression.
/// </summary>
public string ExpandIntoStringBreakEarly(string expression)
{
expression = _expander.ExpandIntoStringAndUnescape(expression, _expanderOptions | ExpanderOptions.BreakOnNotEmpty, ElementLocation);
return expression;
}
/// <summary>
/// Expands the properties and items in the specified expression into a list of taskitems.
/// </summary>
/// <param name="expression">The expression to expand.</param>
/// <returns>A list of items.</returns>
public IList<TaskItem> ExpandIntoTaskItems(string expression)
{
var items = _expander.ExpandIntoTaskItemsLeaveEscaped(expression, _expanderOptions, ElementLocation);
return items;
}
/// <summary>
/// Expands the specified expression into a string.
/// </summary>
/// <param name="expression">The expression to expand.</param>
/// <returns>The expanded string.</returns>
public string ExpandIntoString(string expression)
{
expression = _expander.ExpandIntoStringAndUnescape(expression, _expanderOptions, ElementLocation);
return expression;
}
}
}
}
|