in src/Build/Evaluation/Expander.cs [906:1099]
internal static object ExpandPropertiesLeaveTypedAndEscaped(string expression, IPropertyProvider<T> properties, ExpanderOptions options, IElementLocation elementLocation, UsedUninitializedProperties usedUninitializedProperties)
{
if (((options & ExpanderOptions.ExpandProperties) == 0) || String.IsNullOrEmpty(expression))
{
return expression;
}
ErrorUtilities.VerifyThrow(properties != null, "Cannot expand properties without providing properties");
// These are also zero-based indices into the expression, but
// these tell us where the current property tag begins and ends.
int propertyStartIndex, propertyEndIndex;
// If there are no substitutions, then just return the string.
propertyStartIndex = s_invariantCompareInfo.IndexOf(expression, "$(", CompareOptions.Ordinal);
if (propertyStartIndex == -1)
{
return expression;
}
// We will build our set of results as object components
// so that we can either maintain the object's type in the event
// that we have a single component, or convert to a string
// if concatenation is required.
List<object> results = null;
object lastResult = null;
// The sourceIndex is the zero-based index into the expression,
// where we've essentially read up to and copied into the target string.
int sourceIndex = 0;
int expressionLength = expression.Length;
// Search for "$(" in the expression. Loop until we don't find it
// any more.
while (propertyStartIndex != -1)
{
if (lastResult != null)
{
if (results == null)
{
results = new List<object>(4);
}
results.Add(lastResult);
}
bool tryExtractPropertyFunction = false;
bool tryExtractRegistryFunction = false;
// Append the result with the portion of the expression up to
// (but not including) the "$(", and advance the sourceIndex pointer.
if (propertyStartIndex - sourceIndex > 0)
{
if (results == null)
{
results = new List<object>(4);
}
results.Add(expression.Substring(sourceIndex, propertyStartIndex - sourceIndex));
}
sourceIndex = propertyStartIndex;
// Following the "$(" we need to locate the matching ')'
// Scan for the matching closing bracket, skipping any nested ones
// This is a very complete, fast validation of parenthesis matching including for nested
// function calls.
propertyEndIndex = ScanForClosingParenthesis(expression, propertyStartIndex + 2, out tryExtractPropertyFunction, out tryExtractRegistryFunction);
if (propertyEndIndex == -1)
{
// If we didn't find the closing parenthesis, that means this
// isn't really a well-formed property tag. Just literally
// copy the remainder of the expression (starting with the "$("
// that we found) into the result, and quit.
lastResult = expression.Substring(propertyStartIndex, expression.Length - propertyStartIndex);
sourceIndex = expression.Length;
}
else
{
// Aha, we found the closing parenthesis. All the stuff in
// between the "$(" and the ")" constitutes the property body.
// Note: Current propertyStartIndex points to the "$", and
// propertyEndIndex points to the ")". That's why we have to
// add 2 for the start of the substring, and subtract 2 for
// the length.
string propertyBody;
// A property value of null will indicate that we're calling a static function on a type
object propertyValue = null;
// Compat: $() should return String.Empty
if (propertyStartIndex + 2 == propertyEndIndex)
{
propertyValue = String.Empty;
}
#if FEATURE_WIN32_REGISTRY
else if ((expression.Length - (propertyStartIndex + 2)) > 9 && tryExtractRegistryFunction && s_invariantCompareInfo.IndexOf(expression, "Registry:", propertyStartIndex + 2, 9, CompareOptions.OrdinalIgnoreCase) == propertyStartIndex + 2)
{
propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2);
// If the property body starts with any of our special objects, then deal with them
// This is a registry reference, like $(Registry:HKEY_LOCAL_MACHINE\Software\Vendor\Tools@TaskLocation)
propertyValue = ExpandRegistryValue(propertyBody, elementLocation);
}
#endif
// Compat hack: as a special case, $(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory) should return String.Empty
// In this case, tryExtractRegistryFunction will be false. Note that very few properties are exactly 77 chars, so this check should be fast.
else if ((propertyEndIndex - (propertyStartIndex + 2)) == 77 && s_invariantCompareInfo.IndexOf(expression, @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\VSTSDB@VSTSDBDirectory", propertyStartIndex + 2, 77, CompareOptions.OrdinalIgnoreCase) == propertyStartIndex + 2)
{
propertyValue = String.Empty;
}
// Compat hack: WebProjects may have an import with a condition like:
// Condition=" '$(Solutions.VSVersion)' == '8.0'"
// These would have been '' in prior versions of msbuild but would be treated as a possible string function in current versions.
// Be compatible by returning an empty string here.
else if ((propertyEndIndex - (propertyStartIndex + 2)) == 19 && String.Equals(expression, "$(Solutions.VSVersion)", StringComparison.Ordinal))
{
propertyValue = String.Empty;
}
else if (tryExtractPropertyFunction)
{
propertyBody = expression.Substring(propertyStartIndex + 2, propertyEndIndex - propertyStartIndex - 2);
// This is likely to be a function expression
propertyValue = ExpandPropertyBody(propertyBody, null, properties, options, elementLocation, usedUninitializedProperties);
}
else // This is a regular property
{
propertyValue = LookupProperty(properties, expression, propertyStartIndex + 2, propertyEndIndex - 1, elementLocation, usedUninitializedProperties);
}
// Record our result, and advance
// our sourceIndex pointer to the character just after the closing
// parenthesis.
lastResult = propertyValue;
sourceIndex = propertyEndIndex + 1;
}
propertyStartIndex = s_invariantCompareInfo.IndexOf(expression, "$(", sourceIndex, CompareOptions.Ordinal);
}
// If we have only a single result, then just return it
if (results == null && expression.Length == sourceIndex)
{
var resultString = lastResult as string;
return resultString != null ? FileUtilities.MaybeAdjustFilePath(resultString) : lastResult;
}
else
{
// The expression is constant, return it as is
if (sourceIndex == 0)
{
return expression;
}
// We have more than one result collected, therefore we need to concatenate
// into the final result string. This does mean that we will lose type information.
// However since the user wanted contatenation, then they clearly wanted that to happen.
// Initialize our output string to empty string.
// This method is called very often - of the order of 3,000 times per project.
// With the reuseable string builder, there's no particular need to initialize the length as it will already have grown.
using (var result = new ReuseableStringBuilder())
{
// Append our collected results
if (results != null)
{
// Create a combined result string from the result components that we've gathered
foreach (object component in results)
{
result.Append(FileUtilities.MaybeAdjustFilePath(component.ToString()));
}
}
// Append the last result we collected (it wasn't added to the list)
if (lastResult != null)
{
result.Append(FileUtilities.MaybeAdjustFilePath(lastResult.ToString()));
}
// And if we couldn't find anymore property tags in the expression,
// so just literally copy the remainder into the result.
if (expression.Length - sourceIndex > 0)
{
result.Append(expression, sourceIndex, expression.Length - sourceIndex);
}
return OpportunisticIntern.InternableToString(result);
}
}
}