diff --git a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs index 096c90e5ff9..72d42315df5 100644 --- a/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs +++ b/src/Build/BackEnd/Components/RequestBuilder/IntrinsicTasks/ItemGroupIntrinsicTask.cs @@ -413,27 +413,28 @@ private List ExpandItemIntoItems( // Split Include on any semicolons, and take each split in turn var includeSplits = ExpressionShredder.SplitSemiColonSeparatedList(evaluatedInclude); - ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(this.Project, originalItem.ItemType); + ProjectItemInstanceFactory itemFactory = new ProjectItemInstanceFactory(Project, originalItem.ItemType); + + // EngineFileUtilities.GetFileListEscaped api invocation evaluates excludes by default. + // If the code process any expression like "@(x)", we need to handle excludes explicitly using EvaluateExcludePaths(). + bool anyTransformExprProceeded = false; foreach (string includeSplit in includeSplits) { // If expression is "@(x)" copy specified list with its metadata, otherwise just treat as string - bool throwaway; - - IList itemsFromSplit = expander.ExpandSingleItemVectorExpressionIntoItems(includeSplit, + IList itemsFromSplit = expander.ExpandSingleItemVectorExpressionIntoItems( + includeSplit, itemFactory, ExpanderOptions.ExpandItems, false /* do not include null expansion results */, - out throwaway, + out _, originalItem.IncludeLocation); if (itemsFromSplit != null) { // Expression is in form "@(X)", so add these items directly. - foreach (ProjectItemInstance item in itemsFromSplit) - { - items.Add(item); - } + items.AddRange(itemsFromSplit); + anyTransformExprProceeded = true; } else { @@ -463,35 +464,18 @@ private List ExpandItemIntoItems( } } - // Evaluate, split, expand and subtract any Exclude - HashSet excludesUnescapedForComparison = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (string excludeSplit in excludes) + // There is a need to Evaluate Exclude part explicitly because of of the expressions had the form "@(X)". + if (anyTransformExprProceeded) { - string[] excludeSplitFiles = EngineFileUtilities.GetFileListUnescaped( - Project.Directory, - excludeSplit, - loggingMechanism: LoggingContext, - excludeLocation: originalItem.ExcludeLocation); + // Calculate all Exclude + var excludesUnescapedForComparison = EvaluateExcludePaths(excludes, originalItem.ExcludeLocation); - foreach (string excludeSplitFile in excludeSplitFiles) - { - excludesUnescapedForComparison.Add(excludeSplitFile.NormalizeForPathComparison()); - } - } - - List remainingItems = new List(); - - for (int i = 0; i < items.Count; i++) - { - if (!excludesUnescapedForComparison.Contains(((IItem)items[i]).EvaluatedInclude.NormalizeForPathComparison())) - { - remainingItems.Add(items[i]); - } + // Subtract any Exclude + items = items + .Where(i => !excludesUnescapedForComparison.Contains(((IItem)i).EvaluatedInclude.NormalizeForPathComparison())) + .ToList(); } - items = remainingItems; - // Filter the metadata as appropriate if (keepMetadata != null) { @@ -519,6 +503,32 @@ private List ExpandItemIntoItems( return items; } + /// + /// Returns a list of all items specified in Exclude parameter. + /// If no items match, returns empty list. + /// + /// The items to match + /// The specification to match against the items. + /// A list of matching items + private HashSet EvaluateExcludePaths(IReadOnlyList excludes, ElementLocation excludeLocation) + { + HashSet excludesUnescapedForComparison = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string excludeSplit in excludes) + { + string[] excludeSplitFiles = EngineFileUtilities.GetFileListUnescaped( + Project.Directory, + excludeSplit, + loggingMechanism: LoggingContext, + excludeLocation: excludeLocation); + foreach (string excludeSplitFile in excludeSplitFiles) + { + excludesUnescapedForComparison.Add(excludeSplitFile.NormalizeForPathComparison()); + } + } + + return excludesUnescapedForComparison; + } + /// /// Returns a list of all items in the provided item group whose itemspecs match the specification, after it is split and any wildcards are expanded. /// If no items match, returns null. diff --git a/src/Build/Utilities/EngineFileUtilities.cs b/src/Build/Utilities/EngineFileUtilities.cs index a0dd2580e23..0c26f6e1a8b 100644 --- a/src/Build/Utilities/EngineFileUtilities.cs +++ b/src/Build/Utilities/EngineFileUtilities.cs @@ -191,8 +191,10 @@ private static string[] GetFileList( FileMatcher.SearchAction action = FileMatcher.SearchAction.None; string excludeFileSpec = string.Empty; - if (!FilespecHasWildcards(filespecEscaped) || - FilespecMatchesLazyWildcard(filespecEscaped, forceEvaluateWildCards)) + var noWildcards = !FilespecHasWildcards(filespecEscaped) || FilespecMatchesLazyWildcard(filespecEscaped, forceEvaluateWildCards); + + // It is possible to return original string if no wildcard matches and no entries in Exclude set. + if (noWildcards && excludeSpecsEscaped?.Any() != true) { // Just return the original string. fileList = new string[] { returnEscaped ? filespecEscaped : EscapingUtilities.UnescapeAll(filespecEscaped) };