diff --git a/Directory.Packages.props b/Directory.Packages.props index c0f9bea..4f2ffaf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,7 +10,7 @@ - + diff --git a/MSBuildPrediction.sln b/MSBuildPrediction.sln index 147f7e0..3e1a1d5 100644 --- a/MSBuildPrediction.sln +++ b/MSBuildPrediction.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props Directory.build.rsp = Directory.build.rsp Directory.Build.targets = Directory.Build.targets + Directory.Packages.props = Directory.Packages.props LICENSE.txt = LICENSE.txt NuGet.config = NuGet.config README.md = README.md diff --git a/src/BuildPrediction/Predictors/CopyTask/CopyTaskPredictor.cs b/src/BuildPrediction/Predictors/CopyTask/CopyTaskPredictor.cs index 7ae0c35..3ca334c 100644 --- a/src/BuildPrediction/Predictors/CopyTask/CopyTaskPredictor.cs +++ b/src/BuildPrediction/Predictors/CopyTask/CopyTaskPredictor.cs @@ -22,6 +22,7 @@ public sealed class CopyTaskPredictor : IProjectPredictor { private const string CopyTaskName = "Copy"; private const string CopyTaskSourceFiles = "SourceFiles"; + private const string CopyTaskSourceFolders = "SourceFolders"; private const string CopyTaskDestinationFiles = "DestinationFiles"; private const string CopyTaskDestinationFolder = "DestinationFolder"; @@ -81,55 +82,70 @@ private static void ParseCopyTask( { if (projectInstance.EvaluateConditionCarefully(task.Condition)) { - var inputs = new FileExpressionList( - task.Parameters[CopyTaskSourceFiles], - projectInstance, - task); - if (inputs.NumExpressions == 0) + bool hasSourceFiles = task.Parameters.TryGetValue(CopyTaskSourceFiles, out string sourceFiles) && !string.IsNullOrEmpty(sourceFiles); + bool hasSourceFolders = task.Parameters.TryGetValue(CopyTaskSourceFolders, out string sourceFolders) && !string.IsNullOrEmpty(sourceFolders); + bool hasDestinationFiles = task.Parameters.TryGetValue(CopyTaskDestinationFiles, out string destinationFiles) && !string.IsNullOrEmpty(destinationFiles); + bool hasDestinationFolder = task.Parameters.TryGetValue(CopyTaskDestinationFolder, out string destinationFolder) && !string.IsNullOrEmpty(destinationFolder); + + // The task will nop if there are no sources. + if (!hasSourceFiles && !hasSourceFolders) { continue; } - foreach (var file in inputs.DedupedFiles) + // The task will error if there is no destination + if (!hasDestinationFiles && !hasDestinationFolder) { - predictionReporter.ReportInputFile(file); + continue; } - bool hasDestinationFolder = task.Parameters.TryGetValue( - CopyTaskDestinationFolder, - out string destinationFolder); - bool hasDestinationFiles = task.Parameters.TryGetValue( - CopyTaskDestinationFiles, - out string destinationFiles); - - if (hasDestinationFiles || hasDestinationFolder) + // The task will error if both destination types are used. + if (hasDestinationFolder && hasDestinationFiles) { - // Having both is an MSBuild violation, which it will complain about. - if (hasDestinationFolder && hasDestinationFiles) - { - continue; - } + continue; + } - string destination = destinationFolder ?? destinationFiles; + // SourceFolders and DestinationFiles can't be used together. + if (hasSourceFolders && hasDestinationFiles) + { + continue; + } - var outputs = new FileExpressionList(destination, projectInstance, task); + var inputs = EvaluateExpression(hasSourceFolders ? sourceFolders : sourceFiles, projectInstance, task); + if (inputs.NumExpressions == 0) + { + continue; + } - // When using batch tokens, the user should specify exactly one total token, and it must appear in both the input and output. - // Doing otherwise should be a BuildCop error. If not using batch tokens, then any number of other tokens is fine. - if ((outputs.NumBatchExpressions == 1 && outputs.NumExpressions == 1 && - inputs.NumBatchExpressions == 1 && inputs.NumExpressions == 1) || - (outputs.NumBatchExpressions == 0 && inputs.NumBatchExpressions == 0)) + foreach (string file in inputs.Paths) + { + if (hasSourceFolders) { - ProcessOutputs(inputs, outputs, hasDestinationFolder, predictionReporter); + predictionReporter.ReportInputDirectory(file); } else { - // Ignore case we cannot handle. + predictionReporter.ReportInputFile(file); } } + + var outputs = EvaluateExpression(hasDestinationFolder ? destinationFolder : destinationFiles, projectInstance, task); + if (outputs.NumExpressions == 0) + { + continue; + } + + // When using batch tokens, the user should specify exactly one total token, and it must appear in both the input and output. + // If not using batch tokens, then any number of other tokens is fine. + if ((outputs.NumBatchExpressions == 1 && outputs.NumExpressions == 1 && + inputs.NumBatchExpressions == 1 && inputs.NumExpressions == 1) || + (outputs.NumBatchExpressions == 0 && inputs.NumBatchExpressions == 0)) + { + ProcessOutputs(inputs.Paths, outputs.Paths, hasDestinationFolder, predictionReporter); + } else { - // Ignore malformed case. + // Ignore case we cannot handle. } } } @@ -146,12 +162,12 @@ private static void ParseCopyTask( /// True if the user has specified DestinationFolder. /// A reporter to report predictions to. private static void ProcessOutputs( - FileExpressionList inputs, - FileExpressionList outputs, + List inputs, + List outputs, bool copyTaskSpecifiesDestinationFolder, ProjectPredictionReporter predictionReporter) { - for (int i = 0; i < inputs.DedupedFiles.Count; i++) + for (int i = 0; i < inputs.Count; i++) { string predictedOutputDirectory; @@ -159,33 +175,65 @@ private static void ProcessOutputs( // either exactly one or N folders. We need to handle each case. if (copyTaskSpecifiesDestinationFolder) { - if (outputs.DedupedFiles.Count == 0) + if (outputs.Count == 0) { // Output files couldn't be parsed, bail out. break; } // If output directories isn't 1 or N, bail out. - if (inputs.DedupedFiles.Count != outputs.DedupedFiles.Count && outputs.DedupedFiles.Count > 1) + if (inputs.Count != outputs.Count && outputs.Count > 1) { break; } - predictedOutputDirectory = outputs.DedupedFiles.Count == 1 ? outputs.DedupedFiles[0] : outputs.DedupedFiles[i]; + predictedOutputDirectory = outputs.Count == 1 ? outputs[0] : outputs[i]; } else { - if (i >= outputs.DedupedFiles.Count) + if (i >= outputs.Count) { break; } // The output list is a set of files. Predict their directories. - predictedOutputDirectory = Path.GetDirectoryName(outputs.DedupedFiles[i]); + predictedOutputDirectory = Path.GetDirectoryName(outputs[i]); } predictionReporter.ReportOutputDirectory(predictedOutputDirectory); } } + + private static (List Paths, int NumExpressions, int NumBatchExpressions) EvaluateExpression(string rawFileListString, ProjectInstance project, ProjectTaskInstance task) + { + List expressions = rawFileListString.SplitStringList(); + int numBatchExpressions = 0; + + List paths = new(); + HashSet seenPaths = new(PathComparer.Instance); + foreach (string expression in expressions) + { + List evaluatedFiles = FileExpression.EvaluateExpression(expression, project, task, out bool isBatched); + if (isBatched) + { + numBatchExpressions++; + } + + foreach (string file in evaluatedFiles) + { + if (string.IsNullOrWhiteSpace(file)) + { + continue; + } + + if (seenPaths.Add(file)) + { + paths.Add(file); + } + } + } + + return (paths, expressions.Count, numBatchExpressions); + } } } \ No newline at end of file diff --git a/src/BuildPrediction/Predictors/CopyTask/FileExpressionList.cs b/src/BuildPrediction/Predictors/CopyTask/FileExpressionList.cs deleted file mode 100644 index 0c2699f..0000000 --- a/src/BuildPrediction/Predictors/CopyTask/FileExpressionList.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Collections.Generic; -using Microsoft.Build.Execution; - -namespace Microsoft.Build.Prediction.Predictors.CopyTask -{ - /// - /// Contains a parsed list of file expressions as well as the list of files derived from evaluating said - /// expressions. - /// - internal sealed class FileExpressionList - { - /// - /// Initializes a new instance of the class. - /// - /// The unprocessed list of file expressions. - /// The project where the expression list exists. - /// The task where the expression list exists. - public FileExpressionList(string rawFileListString, ProjectInstance project, ProjectTaskInstance task) - { - List expressions = rawFileListString.SplitStringList(); - NumExpressions = expressions.Count; - - var seenFiles = new HashSet(PathComparer.Instance); - foreach (string expression in expressions) - { - List evaluatedFiles = FileExpression.EvaluateExpression(expression, project, task, out bool isBatched); - if (isBatched) - { - NumBatchExpressions++; - } - - foreach (string file in evaluatedFiles) - { - if (string.IsNullOrWhiteSpace(file)) - { - continue; - } - - if (seenFiles.Add(file)) - { - DedupedFiles.Add(file); - } - - AllFiles.Add(file); - } - } - } - - /// - /// Gets the set of all files in all of the expanded expressions. May include duplicates. - /// - public List AllFiles { get; } = new List(); - - /// - /// Gets the set of all files in the expanded expressions. Duplicates are removed. - /// - public List DedupedFiles { get; } = new List(); - - /// - /// Gets the total number of expressions in the file list. - /// - public int NumExpressions { get; } - - /// - /// Gets the number of batch expressions in the file list. - /// - public int NumBatchExpressions { get; } - } -} \ No newline at end of file diff --git a/src/BuildPredictionTests/Predictors/CopyTaskPredictorTests.cs b/src/BuildPredictionTests/Predictors/CopyTaskPredictorTests.cs index 185bd97..ec08181 100644 --- a/src/BuildPredictionTests/Predictors/CopyTaskPredictorTests.cs +++ b/src/BuildPredictionTests/Predictors/CopyTaskPredictorTests.cs @@ -321,5 +321,22 @@ public void TargetUnevaluatableConditionInCopy() var predictor = new CopyTaskPredictor(); ParseAndVerifyProject("TargetUnevaluatableConditionInCopy.csproj", predictor, expectedInputFiles, null, null, expectedOutputDirectories); } + + [Fact] + public void SourceFolders() + { + PredictedItem[] expectedInputDirectories = + { + new PredictedItem(@"Copy", nameof(CopyTaskPredictor)), + }; + + PredictedItem[] expectedOutputDirectories = + { + new PredictedItem(@"target\Debug\x64\folder", nameof(CopyTaskPredictor)), + }; + + var predictor = new CopyTaskPredictor(); + ParseAndVerifyProject("SourceFolders.csproj", predictor, null, expectedInputDirectories, null, expectedOutputDirectories); + } } } \ No newline at end of file diff --git a/src/BuildPredictionTests/TestsData/Copy/SourceFolders.csproj b/src/BuildPredictionTests/TestsData/Copy/SourceFolders.csproj new file mode 100644 index 0000000..794041a --- /dev/null +++ b/src/BuildPredictionTests/TestsData/Copy/SourceFolders.csproj @@ -0,0 +1,25 @@ + + + + {0000000A-0000-00AA-AA00-0AA00A00A00A} + Exe + objd\amd64 + SomeNamespace + SomeName + + + + + + + + + + + + target\$(Configuration)\$(Platform)\folder + + + + + \ No newline at end of file