diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/CheckForDuplicateItems.cs b/src/Tasks/Microsoft.NET.Build.Tasks/CheckForDuplicateItems.cs new file mode 100644 index 000000000000..79cbf8c33d92 --- /dev/null +++ b/src/Tasks/Microsoft.NET.Build.Tasks/CheckForDuplicateItems.cs @@ -0,0 +1,50 @@ +using Microsoft.Build.Framework; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Microsoft.NET.Build.Tasks +{ + public class CheckForDuplicateItems : TaskBase + { + [Required] + public ITaskItem [] Items { get; set; } + + [Required] + public string ItemName { get; set; } + + public bool DefaultItemsEnabled { get; set; } + + public bool DefaultItemsOfThisTypeEnabled { get; set; } + + [Required] + public string PropertyNameToDisableDefaultItems { get; set; } + + public string PropertyValueToDisableDefaultItems { get; set; } = "false"; + + [Required] + public string MoreInformationLink { get; set; } + + protected override void ExecuteCore() + { + if (DefaultItemsEnabled && DefaultItemsOfThisTypeEnabled) + { + var duplicateItems = Items.GroupBy(i => i.ItemSpec).Where(g => g.Count() > 1).ToList(); + if (duplicateItems.Any()) + { + string duplicateItemsFormatted = string.Join("; ", duplicateItems.Select(d => $"'{d.Key}'")); + + string message = string.Format(CultureInfo.CurrentCulture, Strings.DuplicateItemsError, + ItemName, + PropertyNameToDisableDefaultItems, + PropertyValueToDisableDefaultItems, + duplicateItemsFormatted); + + Log.LogError(message); + } + } + } + } +} diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj b/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj index cf3869152262..3a3f411e6a7d 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj @@ -35,6 +35,7 @@ + diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.Designer.cs b/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.Designer.cs index b6f0882a00ad..61239917c95a 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.Designer.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.Designer.cs @@ -196,6 +196,15 @@ internal static string DOTNET1017 { } } + /// + /// Looks up a localized string similar to Duplicate {0} items were included. The .NET SDK includes {0} items from your project directory by default. You can either remove these items from your project file, or set the '{1}' property to '{2}' if you want to explicitly include them in your project file. The duplicate items were: {3}. + /// + internal static string DuplicateItemsError { + get { + return ResourceManager.GetString("DuplicateItemsError", resourceCulture); + } + } + /// /// Looks up a localized string similar to The preprocessor token '{0}' has been given more than one value. Choosing '{1}' as the value.. /// diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.resx b/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.resx index acc22ff8db16..aac2ad67d272 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.resx +++ b/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.resx @@ -225,4 +225,7 @@ Dependency conflict. '{0}' expected '{1}' but received '{2}' + + Duplicate '{0}' items were included. The .NET SDK includes '{0}' items from your project directory by default. You can either remove these items from your project file, or set the '{1}' property to '{2}' if you want to explicitly include them in your project file. The duplicate items were: {3} + \ No newline at end of file diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.DefaultItems.targets b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.DefaultItems.targets index 113e61cf442b..0cc5ead9e063 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.DefaultItems.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.DefaultItems.targets @@ -52,4 +52,42 @@ Copyright (c) .NET Foundation. All rights reserved. + + + + + + https://aka.ms/sdkimplicititems + + + + + + + + + + + diff --git a/test/Microsoft.NET.Build.Tests/GivenThereAreDefaultItems.cs b/test/Microsoft.NET.Build.Tests/GivenThereAreDefaultItems.cs index 3f0cb0941d1b..bf911974bf3e 100644 --- a/test/Microsoft.NET.Build.Tests/GivenThereAreDefaultItems.cs +++ b/test/Microsoft.NET.Build.Tests/GivenThereAreDefaultItems.cs @@ -14,6 +14,7 @@ using System.Xml.Linq; using Xunit; using static Microsoft.NET.TestFramework.Commands.MSBuildTest; +using Microsoft.NET.TestFramework.ProjectConstruction; namespace Microsoft.NET.Build.Tests { @@ -352,6 +353,82 @@ public void Compile_items_can_be_explicitly_specified_while_default_EmbeddedReso GivenThatWeWantAllResourcesInSatellite.TestSatelliteResources(_testAssetsManager, projectChanges, setup, "ExplicitCompileDefaultEmbeddedResource"); } + [Fact] + public void It_gives_an_error_message_if_duplicate_compile_items_are_included() + { + var testProject = new TestProject() + { + Name = "DuplicateCompileItems", + TargetFrameworks = "netstandard1.6", + IsSdkProject = true + }; + + var testAsset = _testAssetsManager.CreateTestProject(testProject) + .WithProjectChanges(project => + { + var ns = project.Root.Name.Namespace; + var itemGroup = new XElement(ns + "ItemGroup"); + project.Root.Add(itemGroup); + itemGroup.Add(new XElement(ns + "Compile", new XAttribute("Include", @"**\*.cs"))); + }) + .Restore(testProject.Name); + + var buildCommand = new BuildCommand(Stage0MSBuild, Path.Combine(testAsset.TestRoot, testProject.Name)); + + WriteFile(Path.Combine(buildCommand.ProjectRootPath, "Class1.cs"), "public class Class1 {}"); + + buildCommand + .CaptureStdOut() + .Execute() + .Should() + .Fail() + .And.HaveStdOutContaining("DuplicateCompileItems.cs") + .And.HaveStdOutContaining("Class1.cs") + .And.HaveStdOutContaining("EnableDefaultCompileItems"); + } + + [Fact] + public void It_gives_the_correct_error_if_duplicate_compile_items_are_included_and_default_items_are_disabled() + { + var testProject = new TestProject() + { + Name = "DuplicateCompileItems", + TargetFrameworks = "netstandard1.6", + IsSdkProject = true + }; + + var testAsset = _testAssetsManager.CreateTestProject(testProject, "DuplicateCompileItemsWithDefaultItemsDisabled") + .WithProjectChanges(project => + { + var ns = project.Root.Name.Namespace; + + project.Root.Element(ns + "PropertyGroup").Add( + new XElement(ns + "EnableDefaultCompileItems", "false")); + + var itemGroup = new XElement(ns + "ItemGroup"); + project.Root.Add(itemGroup); + itemGroup.Add(new XElement(ns + "Compile", new XAttribute("Include", @"**\*.cs"))); + itemGroup.Add(new XElement(ns + "Compile", new XAttribute("Include", @"DuplicateCompileItems.cs"))); + }) + .Restore(testProject.Name); + + var buildCommand = new BuildCommand(Stage0MSBuild, Path.Combine(testAsset.TestRoot, testProject.Name)); + + WriteFile(Path.Combine(buildCommand.ProjectRootPath, "Class1.cs"), "public class Class1 {}"); + + buildCommand + .CaptureStdOut() + .Execute() + .Should() + .Fail() + .And.HaveStdOutContaining("DuplicateCompileItems.cs") + // Class1.cs wasn't included multiple times, so it shouldn't be mentioned + .And.NotHaveStdOutMatching("Class1.cs") + // Default items weren't enabled, so the error message should come from the C# compiler and shouldn't include the information about default compile items + .And.HaveStdOutContaining("MSB3105") + .And.NotHaveStdOutMatching("EnableDefaultCompileItems"); + } + void RemoveGeneratedCompileItems(List compileItems) { // Remove auto-generated compile items. diff --git a/test/Microsoft.NET.TestFramework/TestAsset.cs b/test/Microsoft.NET.TestFramework/TestAsset.cs index 657b2e3d776b..a7d253194291 100644 --- a/test/Microsoft.NET.TestFramework/TestAsset.cs +++ b/test/Microsoft.NET.TestFramework/TestAsset.cs @@ -42,8 +42,7 @@ internal void FindProjectFiles() { _projectFiles = new List(); - var files = Directory.GetFiles(base.Path, "*.*", SearchOption.AllDirectories) - .Where(file => !IsInBinOrObjFolder(file)); + var files = Directory.GetFiles(base.Path, "*.*", SearchOption.AllDirectories); foreach (string file in files) {