private void ProcessMetadataElements(ProjectItemElement itemElement, OperationBuilderWithMetadata operationBuilder) { if (itemElement.HasMetadata) { operationBuilder.Metadata.AddRange(itemElement.Metadata); List <string> values = new List <string>(itemElement.Metadata.Count * 2); foreach (ProjectMetadataElement metadatumElement in itemElement.Metadata) { values.Add(metadatumElement.Value); values.Add(metadatumElement.Condition); } ItemsAndMetadataPair itemsAndMetadataFound = ExpressionShredder.GetReferencedItemNamesAndMetadata(values); if (itemsAndMetadataFound.Items != null) { foreach (var itemType in itemsAndMetadataFound.Items) { var itemList = GetItemList(itemType); if (itemList != null) { operationBuilder.ReferencedItemLists[itemType] = itemList; } } } } }
/// <summary> /// Returns true if there is a metadata expression (outside of a transform) in the expression. /// </summary> internal static bool ContainsMetadataExpressionOutsideTransform(string expression) { ItemsAndMetadataPair pair = new ItemsAndMetadataPair(null, null); GetReferencedItemNamesAndMetadata(expression, 0, expression.Length, ref pair, ShredderOptions.MetadataOutsideTransforms); bool result = (pair.Metadata?.Count > 0); return(result); }
/// <summary> /// Given a list of expressions that may contain item list expressions, /// returns a pair of tables of all item names found, as K=Name, V=String.Empty; /// and all metadata not in transforms, as K=Metadata key, V=MetadataReference, /// where metadata key is like "itemname.metadataname" or "metadataname". /// PERF: Tables are null if there are no entries, because this is quite a common case. /// </summary> internal static ItemsAndMetadataPair GetReferencedItemNamesAndMetadata(IEnumerable <string> expressions) { ItemsAndMetadataPair pair = new ItemsAndMetadataPair(null, null); foreach (string expression in expressions) { GetReferencedItemNamesAndMetadata(expression, 0, expression.Length, ref pair, ShredderOptions.All); } return(pair); }
/// <summary> /// Given a subexpression, finds referenced item names and inserts them into the table /// as K=Name, V=String.Empty. /// </summary> /// <remarks> /// We can ignore any semicolons in the expression, since we're not itemizing it. /// </remarks> private static void GetReferencedItemNamesAndMetadata(string expression, int start, int end, ref ItemsAndMetadataPair pair, ShredderOptions whatToShredFor) { for (int i = start; i < end; i++) { int restartPoint; if (Sink(expression, ref i, end, '@', '(')) { // Start of a possible item list expression // Store the index to backtrack to if this doesn't turn out to be a well // formed metadata expression. (Subtract one for the increment when we loop around.) restartPoint = i - 1; SinkWhitespace(expression, ref i); int startOfName = i; if (!SinkValidName(expression, ref i, end)) { i = restartPoint; continue; } // '-' is a legitimate char in an item name, but we should match '->' as an arrow // in '@(foo->'x')' rather than as the last char of the item name. // The old regex accomplished this by being "greedy" if (end > i && expression[i - 1] == '-' && expression[i] == '>') { i--; } // Grab the name, but continue to verify it's a well-formed expression // before we store it. string name = expression.Substring(startOfName, i - startOfName); SinkWhitespace(expression, ref i); bool transformOrFunctionFound = true; // If there's an '->' eat it and the subsequent quoted expression or transform function while (Sink(expression, ref i, end, '-', '>') && transformOrFunctionFound) { SinkWhitespace(expression, ref i); int startTransform = i; bool isQuotedTransform = SinkSingleQuotedExpression(expression, ref i, end); if (isQuotedTransform) { continue; } ItemExpressionCapture functionCapture = SinkItemFunctionExpression(expression, startTransform, ref i, end); if (functionCapture != null) { continue; } if (!isQuotedTransform && functionCapture == null) { i = restartPoint; transformOrFunctionFound = false; } } if (!transformOrFunctionFound) { continue; } SinkWhitespace(expression, ref i); // If there's a ',', eat it and the subsequent quoted expression if (Sink(expression, ref i, ',')) { SinkWhitespace(expression, ref i); if (!Sink(expression, ref i, '\'')) { i = restartPoint; continue; } int closingQuote = expression.IndexOf('\'', i); if (closingQuote == -1) { i = restartPoint; continue; } // Look for metadata in the separator expression // e.g., @(foo, '%(bar)') contains batchable metadata 'bar' GetReferencedItemNamesAndMetadata(expression, i, closingQuote, ref pair, ShredderOptions.MetadataOutsideTransforms); i = closingQuote + 1; } SinkWhitespace(expression, ref i); if (!Sink(expression, ref i, ')')) { i = restartPoint; continue; } // If we've got this far, we know the item expression was // well formed, so make sure the name's in the table if ((whatToShredFor & ShredderOptions.ItemTypes) != 0) { pair.Items ??= new HashSet <string>(MSBuildNameIgnoreCaseComparer.Default); pair.Items.Add(name); } i--; continue; } if (Sink(expression, ref i, end, '%', '(')) { // Start of a possible metadata expression // Store the index to backtrack to if this doesn't turn out to be a well // formed metadata expression. (Subtract one for the increment when we loop around.) restartPoint = i - 1; SinkWhitespace(expression, ref i); int startOfText = i; if (!SinkValidName(expression, ref i, end)) { i = restartPoint; continue; } // Grab this, but we don't know if it's an item or metadata name yet string firstPart = expression.Substring(startOfText, i - startOfText); string itemName = null; string metadataName; string qualifiedMetadataName; SinkWhitespace(expression, ref i); bool qualified = Sink(expression, ref i, '.'); if (qualified) { SinkWhitespace(expression, ref i); startOfText = i; if (!SinkValidName(expression, ref i, end)) { i = restartPoint; continue; } itemName = firstPart; metadataName = expression.Substring(startOfText, i - startOfText); qualifiedMetadataName = itemName + "." + metadataName; } else { metadataName = firstPart; qualifiedMetadataName = metadataName; } SinkWhitespace(expression, ref i); if (!Sink(expression, ref i, ')')) { i = restartPoint; continue; } if ((whatToShredFor & ShredderOptions.MetadataOutsideTransforms) != 0) { pair.Metadata ??= new Dictionary <string, MetadataReference>(MSBuildNameIgnoreCaseComparer.Default); pair.Metadata[qualifiedMetadataName] = new MetadataReference(itemName, metadataName); } i--; } } }
protected void DecorateItemsWithMetadata(ImmutableList <I> items, ImmutableList <ProjectMetadataElement> metadata) { if (metadata.Count > 0) { //////////////////////////////////////////////////// // UNDONE: Implement batching here. // // We want to allow built-in metadata in metadata values here. // For example, so that an Idl file can specify that its Tlb output should be named %(Filename).tlb. // // In other words, we want batching. However, we won't need to go to the trouble of using the regular batching code! // That's because that code is all about grouping into buckets of similar items. In this context, we're not // invoking a task, and it's fine to process each item individually, which will always give the correct results. // // For the CTP, to make the minimal change, we will not do this quite correctly. // // We will do this: // -- check whether any metadata values or their conditions contain any bare built-in metadata expressions, // or whether they contain any custom metadata && the Include involved an @(itemlist) expression. // -- if either case is found, we go ahead and evaluate all the metadata separately for each item. // -- otherwise we can do the old thing (evaluating all metadata once then applying to all items) // // This algorithm gives the correct results except when: // -- batchable expressions exist on the include, exclude, or condition on the item element itself // // It means that 99% of cases still go through the old code, which is best for the CTP. // When we ultimately implement this correctly, we should make sure we optimize for the case of very many items // and little metadata, none of which varies between items. // Do not expand properties as they have been already expanded by the lazy evaluator upon item operation construction. // Prior to lazy evaluation ExpanderOptions.ExpandAll was used. const ExpanderOptions metadataExpansionOptions = ExpanderOptions.ExpandAll; List <string> values = new List <string>(metadata.Count * 2); foreach (var metadataElement in metadata) { values.Add(metadataElement.Value); values.Add(metadataElement.Condition); } ItemsAndMetadataPair itemsAndMetadataFound = ExpressionShredder.GetReferencedItemNamesAndMetadata(values); bool needToProcessItemsIndividually = false; if (itemsAndMetadataFound.Metadata != null && itemsAndMetadataFound.Metadata.Values.Count > 0) { // If there is bare metadata of any kind, and the Include involved an item list, we should // run items individually, as even non-built-in metadata might differ between items if (_referencedItemLists.Count >= 0) { needToProcessItemsIndividually = true; } else { // If there is bare built-in metadata, we must always run items individually, as that almost // always differs between items. // UNDONE: When batching is implemented for real, we need to make sure that // item definition metadata is included in all metadata operations during evaluation if (itemsAndMetadataFound.Metadata.Values.Count > 0) { needToProcessItemsIndividually = true; } } } if (needToProcessItemsIndividually) { foreach (I item in items) { _expander.Metadata = item; foreach (var metadataElement in metadata) { #if FEATURE_MSBUILD_DEBUGGER //if (DebuggerManager.DebuggingEnabled) //{ // DebuggerManager.PulseState(metadataElementElement.Location, _itemPassLocals); //} #endif if (!EvaluateCondition(metadataElement.Condition, metadataElement, metadataExpansionOptions, ParserOptions.AllowAll, _expander, _lazyEvaluator)) { continue; } string evaluatedValue = _expander.ExpandIntoStringLeaveEscaped(metadataElement.Value, metadataExpansionOptions, metadataElement.Location); item.SetMetadata(metadataElement, FileUtilities.MaybeAdjustFilePath(evaluatedValue, metadataElement.ContainingProject.DirectoryPath)); } } // End of legal area for metadata expressions. _expander.Metadata = null; } // End of pseudo batching //////////////////////////////////////////////////// // Start of old code else { // Metadata expressions are allowed here. // Temporarily gather and expand these in a table so they can reference other metadata elements above. EvaluatorMetadataTable metadataTable = new EvaluatorMetadataTable(_itemType); _expander.Metadata = metadataTable; // Also keep a list of everything so we can get the predecessor objects correct. List <Pair <ProjectMetadataElement, string> > metadataList = new List <Pair <ProjectMetadataElement, string> >(metadata.Count); foreach (var metadataElement in metadata) { // Because of the checking above, it should be safe to expand metadata in conditions; the condition // will be true for either all the items or none if (!EvaluateCondition(metadataElement.Condition, metadataElement, metadataExpansionOptions, ParserOptions.AllowAll, _expander, _lazyEvaluator)) { continue; } #if FEATURE_MSBUILD_DEBUGGER //if (DebuggerManager.DebuggingEnabled) //{ // DebuggerManager.PulseState(metadataElementElement.Location, _itemPassLocals); //} #endif string evaluatedValue = _expander.ExpandIntoStringLeaveEscaped(metadataElement.Value, metadataExpansionOptions, metadataElement.Location); evaluatedValue = FileUtilities.MaybeAdjustFilePath(evaluatedValue, metadataElement.ContainingProject.DirectoryPath); metadataTable.SetValue(metadataElement, evaluatedValue); metadataList.Add(new Pair <ProjectMetadataElement, string>(metadataElement, evaluatedValue)); } // Apply those metadata to each item // Note that several items could share the same metadata objects // Set all the items at once to make a potential copy-on-write optimization possible. // This is valuable in the case where one item element evaluates to // many items (either by semicolon or wildcards) // and that item also has the same piece/s of metadata for each item. _itemFactory.SetMetadata(metadataList, items); // End of legal area for metadata expressions. _expander.Metadata = null; } } }
protected bool NeedToExpandMetadataForEachItem(ImmutableList <ProjectMetadataElement> metadata, out ItemsAndMetadataPair itemsAndMetadataFound) { itemsAndMetadataFound = ExpressionShredder.GetReferencedItemNamesAndMetadata(GetMetadataValuesAndConditions(metadata)); bool needToExpandMetadataForEachItem = false; if (itemsAndMetadataFound.Metadata?.Values.Count > 0) { // If there is bare metadata of any kind, and the Include involved an item list, we should // run items individually, as even non-built-in metadata might differ between items if (_referencedItemLists.Count >= 0) { needToExpandMetadataForEachItem = true; } else { // If there is bare built-in metadata, we must always run items individually, as that almost // always differs between items. // UNDONE: When batching is implemented for real, we need to make sure that // item definition metadata is included in all metadata operations during evaluation if (itemsAndMetadataFound.Metadata.Values.Count > 0) { needToExpandMetadataForEachItem = true; } } } return(needToExpandMetadataForEachItem); }
/// <summary> /// Given a subexpression, finds referenced item names and inserts them into the table /// as K=Name, V=String.Empty. /// </summary> /// <remarks> /// We can ignore any semicolons in the expression, since we're not itemizing it. /// </remarks> private static void GetReferencedItemNamesAndMetadata(string expression, int start, int end, ref ItemsAndMetadataPair pair, ShredderOptions whatToShredFor) { for (int i = start; i < end; i++) { int restartPoint; if (Sink(expression, ref i, end, '@', '(')) { // Start of a possible item list expression // Store the index to backtrack to if this doesn't turn out to be a well // formed metadata expression. (Subtract one for the increment when we loop around.) restartPoint = i - 1; SinkWhitespace(expression, ref i); int startOfName = i; if (!SinkValidName(expression, ref i, end)) { i = restartPoint; continue; } // '-' is a legitimate char in an item name, but we should match '->' as an arrow // in '@(foo->'x')' rather than as the last char of the item name. // The old regex accomplished this by being "greedy" if (end > i && expression[i - 1] == '-' && expression[i] == '>') { i--; } // Grab the name, but continue to verify it's a well-formed expression // before we store it. string name = expression.Substring(startOfName, i - startOfName); SinkWhitespace(expression, ref i); bool transformOrFunctionFound = true; // If there's an '->' eat it and the subsequent quoted expression or transform function while (Sink(expression, ref i, end, '-', '>') && transformOrFunctionFound) { SinkWhitespace(expression, ref i); int startTransform = i; bool isQuotedTransform = SinkSingleQuotedExpression(expression, ref i, end); if (isQuotedTransform) { continue; } ItemExpressionCapture functionCapture = SinkItemFunctionExpression(expression, startTransform, ref i, end); if (functionCapture != null) { continue; } if (!isQuotedTransform && functionCapture == null) { i = restartPoint; transformOrFunctionFound = false; } } if (!transformOrFunctionFound) { continue; } SinkWhitespace(expression, ref i); // If there's a ',', eat it and the subsequent quoted expression if (Sink(expression, ref i, ',')) { SinkWhitespace(expression, ref i); if (!Sink(expression, ref i, '\'')) { i = restartPoint; continue; } int closingQuote = expression.IndexOf('\'', i); if (closingQuote == -1) { i = restartPoint; continue; } // Look for metadata in the separator expression // e.g., @(foo, '%(bar)') contains batchable metadata 'bar' GetReferencedItemNamesAndMetadata(expression, i, closingQuote, ref pair, ShredderOptions.MetadataOutsideTransforms); i = closingQuote + 1; } SinkWhitespace(expression, ref i); if (!Sink(expression, ref i, ')')) { i = restartPoint; continue; } // If we've got this far, we know the item expression was // well formed, so make sure the name's in the table if ((whatToShredFor & ShredderOptions.ItemTypes) != 0) { pair.Items = pair.Items ?? new HashSet<string>(MSBuildNameIgnoreCaseComparer.Default); pair.Items.Add(name); } i--; continue; } if (Sink(expression, ref i, end, '%', '(')) { // Start of a possible metadata expression // Store the index to backtrack to if this doesn't turn out to be a well // formed metadata expression. (Subtract one for the increment when we loop around.) restartPoint = i - 1; SinkWhitespace(expression, ref i); int startOfText = i; if (!SinkValidName(expression, ref i, end)) { i = restartPoint; continue; } // Grab this, but we don't know if it's an item or metadata name yet string firstPart = expression.Substring(startOfText, i - startOfText); string itemName = null; string metadataName; string qualifiedMetadataName; SinkWhitespace(expression, ref i); bool qualified = Sink(expression, ref i, '.'); if (qualified) { SinkWhitespace(expression, ref i); startOfText = i; if (!SinkValidName(expression, ref i, end)) { i = restartPoint; continue; } itemName = firstPart; metadataName = expression.Substring(startOfText, i - startOfText); qualifiedMetadataName = itemName + "." + metadataName; } else { metadataName = firstPart; qualifiedMetadataName = metadataName; } SinkWhitespace(expression, ref i); if (!Sink(expression, ref i, ')')) { i = restartPoint; continue; } if ((whatToShredFor & ShredderOptions.MetadataOutsideTransforms) != 0) { pair.Metadata = pair.Metadata ?? new Dictionary<string, MetadataReference>(MSBuildNameIgnoreCaseComparer.Default); pair.Metadata[qualifiedMetadataName] = new MetadataReference(itemName, metadataName); } i--; } } }
/// <summary> /// Returns true if there is a metadata expression (outside of a transform) in the expression. /// </summary> internal static bool ContainsMetadataExpressionOutsideTransform(string expression) { ItemsAndMetadataPair pair = new ItemsAndMetadataPair(null, null); GetReferencedItemNamesAndMetadata(expression, 0, expression.Length, ref pair, ShredderOptions.MetadataOutsideTransforms); bool result = (pair.Metadata != null && pair.Metadata.Count > 0); return result; }
/// <summary> /// Given a list of expressions that may contain item list expressions, /// returns a pair of tables of all item names found, as K=Name, V=String.Empty; /// and all metadata not in transforms, as K=Metadata key, V=MetadataReference, /// where metadata key is like "itemname.metadataname" or "metadataname". /// PERF: Tables are null if there are no entries, because this is quite a common case. /// </summary> internal static ItemsAndMetadataPair GetReferencedItemNamesAndMetadata(List<string> expressions) { ItemsAndMetadataPair pair = new ItemsAndMetadataPair(null, null); foreach (string expression in expressions) { GetReferencedItemNamesAndMetadata(expression, 0, expression.Length, ref pair, ShredderOptions.All); } return pair; }