diff --git a/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/App.sln b/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/App.sln new file mode 100644 index 000000000..6b63b4437 --- /dev/null +++ b/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/App.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26006.2 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "src\App\App.csproj", "{DDF3765C-59FB-4AA6-BE83-779ED13AA64A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{72BFCA87-B033-4721-8712-4D12166B4A39}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Debug|x64.ActiveCfg = Debug|x64 + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Debug|x64.Build.0 = Debug|x64 + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Debug|x86.ActiveCfg = Debug|x86 + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Debug|x86.Build.0 = Debug|x86 + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Release|Any CPU.Build.0 = Release|Any CPU + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Release|x64.ActiveCfg = Release|x64 + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Release|x64.Build.0 = Release|x64 + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Release|x86.ActiveCfg = Release|x86 + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DDF3765C-59FB-4AA6-BE83-779ED13AA64A} = {72BFCA87-B033-4721-8712-4D12166B4A39} + EndGlobalSection +EndGlobal diff --git a/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/App/App.csproj b/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/App/App.csproj new file mode 100644 index 000000000..b38669c62 --- /dev/null +++ b/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/App/App.csproj @@ -0,0 +1,15 @@ + + + Exe + netcoreapp1.0 + + + + + + + + + + + diff --git a/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/App/Program.cs b/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/App/Program.cs new file mode 100644 index 000000000..abb853a4a --- /dev/null +++ b/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/App/Program.cs @@ -0,0 +1,10 @@ +using System; + +class Program +{ + static void Main(string[] args) + { + Console.WriteLine("Hello from the main app"); + Console.WriteLine(Lib.Library.GetMessage()); + } +} diff --git a/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/src/Lib/Lib.csproj b/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/src/Lib/Lib.csproj new file mode 100644 index 000000000..9f8bcbb51 --- /dev/null +++ b/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/src/Lib/Lib.csproj @@ -0,0 +1,11 @@ + + + + netstandard1.4 + + + + + + + diff --git a/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/src/Lib/Library.cs b/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/src/Lib/Library.cs new file mode 100644 index 000000000..205c42a01 --- /dev/null +++ b/TestAssets/TestProjects/TestAppWithSlnAndSolutionFolders/src/src/Lib/Library.cs @@ -0,0 +1,12 @@ +using System; + +namespace Lib +{ + public class Library + { + public static string GetMessage() + { + return "Message from Lib"; + } + } +} diff --git a/src/dotnet/SlnProjectCollectionExtensions.cs b/src/dotnet/SlnProjectCollectionExtensions.cs index 1e964f775..d99b34eee 100644 --- a/src/dotnet/SlnProjectCollectionExtensions.cs +++ b/src/dotnet/SlnProjectCollectionExtensions.cs @@ -10,34 +10,18 @@ namespace Microsoft.DotNet.Tools.Common { public static class SlnProjectCollectionExtensions { - public static HashSet GetReferencedSolutionFolders(this SlnProjectCollection projects) + public static IEnumerable GetProjectsByType( + this SlnProjectCollection projects, + string typeGuid) { - var referencedSolutionFolders = new HashSet(); + return projects.Where(p => p.TypeGuid == typeGuid); + } - var solutionFolderProjects = projects - .Where(p => p.TypeGuid == ProjectTypeGuids.SolutionFolderGuid) - .ToList(); - - if (solutionFolderProjects.Any()) - { - var nonSolutionFolderProjects = projects - .Where(p => p.TypeGuid != ProjectTypeGuids.SolutionFolderGuid) - .ToList(); - - foreach (var project in nonSolutionFolderProjects) - { - var solutionFolders = project.GetSolutionFoldersFromProject(); - foreach (var solutionFolder in solutionFolders) - { - if (!referencedSolutionFolders.Contains(solutionFolder)) - { - referencedSolutionFolders.Add(solutionFolder); - } - } - } - } - - return referencedSolutionFolders; + public static IEnumerable GetProjectsNotOfType( + this SlnProjectCollection projects, + string typeGuid) + { + return projects.Where(p => p.TypeGuid != typeGuid); } } } diff --git a/src/dotnet/SlnProjectExtensions.cs b/src/dotnet/SlnProjectExtensions.cs index 14f730329..925089beb 100644 --- a/src/dotnet/SlnProjectExtensions.cs +++ b/src/dotnet/SlnProjectExtensions.cs @@ -12,17 +12,44 @@ namespace Microsoft.DotNet.Tools.Common { public static IList GetSolutionFoldersFromProject(this SlnProject project) { - var currentDirString = $".{Path.DirectorySeparatorChar}"; + var solutionFolders = new List(); - var directoryPath = Path.GetDirectoryName(project.FilePath); - if (directoryPath.StartsWith(currentDirString)) + var projectFilePath = project.FilePath; + if (IsPathInTreeRootedAtSolutionDirectory(projectFilePath)) { - directoryPath = directoryPath.Substring(currentDirString.Length); + var currentDirString = $".{Path.DirectorySeparatorChar}"; + if (projectFilePath.StartsWith(currentDirString)) + { + projectFilePath = projectFilePath.Substring(currentDirString.Length); + } + + var projectDirectoryPath = TrimProject(projectFilePath); + if (!string.IsNullOrEmpty(projectDirectoryPath)) + { + var solutionFoldersPath = TrimProjectDirectory(projectDirectoryPath); + if (!string.IsNullOrEmpty(solutionFoldersPath)) + { + solutionFolders.AddRange(solutionFoldersPath.Split(Path.DirectorySeparatorChar)); + } + } } - return directoryPath.StartsWith("..") - ? new List() - : new List(directoryPath.Split(Path.DirectorySeparatorChar)); + return solutionFolders; + } + + private static bool IsPathInTreeRootedAtSolutionDirectory(string path) + { + return !path.StartsWith(".."); + } + + private static string TrimProject(string path) + { + return Path.GetDirectoryName(path); + } + + private static string TrimProjectDirectory(string path) + { + return Path.GetDirectoryName(path); } } } diff --git a/src/dotnet/commands/dotnet-sln/add/Program.cs b/src/dotnet/commands/dotnet-sln/add/Program.cs index 5cb8869ce..889506662 100644 --- a/src/dotnet/commands/dotnet-sln/add/Program.cs +++ b/src/dotnet/commands/dotnet-sln/add/Program.cs @@ -163,28 +163,61 @@ namespace Microsoft.DotNet.Tools.Sln.Add "NestedProjects", SlnSectionType.PreProcess); + var pathToGuidMap = GetSolutionFolderPaths(slnFile, nestedProjectsSection.Properties); + string parentDirGuid = null; + var solutionFolderHierarchy = string.Empty; foreach (var dir in solutionFolders) { - var solutionFolder = new SlnProject + solutionFolderHierarchy = Path.Combine(solutionFolderHierarchy, dir); + if (pathToGuidMap.ContainsKey(solutionFolderHierarchy)) { - Id = Guid.NewGuid().ToString("B").ToUpper(), - TypeGuid = ProjectTypeGuids.SolutionFolderGuid, - Name = dir, - FilePath = dir - }; - - slnFile.Projects.Add(solutionFolder); - - if (parentDirGuid != null) - { - nestedProjectsSection.Properties[solutionFolder.Id] = parentDirGuid; + parentDirGuid = pathToGuidMap[solutionFolderHierarchy]; + } + else + { + var solutionFolder = new SlnProject + { + Id = Guid.NewGuid().ToString("B").ToUpper(), + TypeGuid = ProjectTypeGuids.SolutionFolderGuid, + Name = dir, + FilePath = dir + }; + + slnFile.Projects.Add(solutionFolder); + + if (parentDirGuid != null) + { + nestedProjectsSection.Properties[solutionFolder.Id] = parentDirGuid; + } + parentDirGuid = solutionFolder.Id; } - parentDirGuid = solutionFolder.Id; } nestedProjectsSection.Properties[slnProject.Id] = parentDirGuid; } } + + private IDictionary GetSolutionFolderPaths(SlnFile slnFile, SlnPropertySet nestedProjects) + { + var solutionFolderPaths = new Dictionary(); + + var solutionFolderProjects = slnFile.Projects.GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid); + foreach (var slnProject in solutionFolderProjects) + { + var path = slnProject.FilePath; + var id = slnProject.Id; + while (nestedProjects.ContainsKey(id)) + { + id = nestedProjects[id]; + var parentSlnProject = solutionFolderProjects.Where(p => p.Id == id).Single(); + path = Path.Combine(parentSlnProject.FilePath, path); + } + + solutionFolderPaths[path] = slnProject.Id; + } + + return solutionFolderPaths; + } } } diff --git a/src/dotnet/commands/dotnet-sln/remove/Program.cs b/src/dotnet/commands/dotnet-sln/remove/Program.cs index deb9ea72a..18f3866e5 100644 --- a/src/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/dotnet/commands/dotnet-sln/remove/Program.cs @@ -126,10 +126,8 @@ namespace Microsoft.DotNet.Tools.Sln.Remove private void RemoveEmptySolutionFolders(SlnFile slnFile) { - var referencedSolutionFolders = slnFile.Projects.GetReferencedSolutionFolders(); - var solutionFolderProjects = slnFile.Projects - .Where(p => p.TypeGuid == ProjectTypeGuids.SolutionFolderGuid) + .GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid) .ToList(); if (solutionFolderProjects.Any()) @@ -138,9 +136,13 @@ namespace Microsoft.DotNet.Tools.Sln.Remove "NestedProjects", SlnSectionType.PreProcess); + var solutionFoldersInUse = GetSolutionFoldersThatContainProjectsInItsHierarchy( + slnFile, + nestedProjectsSection.Properties); + foreach (var solutionFolderProject in solutionFolderProjects) { - if (!referencedSolutionFolders.Contains(solutionFolderProject.Name)) + if (!solutionFoldersInUse.Contains(solutionFolderProject.Id)) { slnFile.Projects.Remove(solutionFolderProject); nestedProjectsSection.Properties.Remove(solutionFolderProject.Id); @@ -153,5 +155,27 @@ namespace Microsoft.DotNet.Tools.Sln.Remove } } } + + private HashSet GetSolutionFoldersThatContainProjectsInItsHierarchy( + SlnFile slnFile, + SlnPropertySet nestedProjects) + { + var solutionFoldersInUse = new HashSet(); + + var nonSolutionFolderProjects = slnFile.Projects.GetProjectsNotOfType( + ProjectTypeGuids.SolutionFolderGuid); + + foreach (var nonSolutionFolderProject in nonSolutionFolderProjects) + { + var id = nonSolutionFolderProject.Id; + while (nestedProjects.ContainsKey(id)) + { + id = nestedProjects[id]; + solutionFoldersInUse.Add(id); + } + } + + return solutionFoldersInUse; + } } } diff --git a/test/dotnet-sln-add.Tests/GivenDotnetSlnAdd.cs b/test/dotnet-sln-add.Tests/GivenDotnetSlnAdd.cs index 4a0d87381..c1ca138d5 100644 --- a/test/dotnet-sln-add.Tests/GivenDotnetSlnAdd.cs +++ b/test/dotnet-sln-add.Tests/GivenDotnetSlnAdd.cs @@ -35,8 +35,6 @@ VisualStudioVersion = 15.0.26006.2 MinimumVisualStudioVersion = 10.0.40219.1 Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""App"", ""App\App.csproj"", ""{7072A694-548F-4CAE-A58F-12D257D5F486}"" EndProject -Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""Lib"", ""Lib"", ""__LIB_FOLDER_GUID__"" -EndProject Project(""{13B669BE-BB05-4DDF-9536-439F39A36129}"") = ""Lib"", ""Lib\Lib.csproj"", ""__LIB_PROJECT_GUID__"" EndProject Global @@ -77,9 +75,6 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(NestedProjects) = preSolution - __LIB_PROJECT_GUID__ = __LIB_FOLDER_GUID__ - EndGlobalSection EndGlobal "; @@ -88,8 +83,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26006.2 MinimumVisualStudioVersion = 10.0.40219.1 -Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""Lib"", ""Lib"", ""__LIB_FOLDER_GUID__"" -EndProject Project(""{13B669BE-BB05-4DDF-9536-439F39A36129}"") = ""Lib"", ""Lib\Lib.csproj"", ""__LIB_PROJECT_GUID__"" EndProject Global @@ -115,9 +108,6 @@ Global __LIB_PROJECT_GUID__.Release|x86.ActiveCfg = Release|x86 __LIB_PROJECT_GUID__.Release|x86.Build.0 = Release|x86 EndGlobalSection - GlobalSection(NestedProjects) = preSolution - __LIB_PROJECT_GUID__ = __LIB_FOLDER_GUID__ - EndGlobalSection EndGlobal "; @@ -130,8 +120,6 @@ Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""App"", ""App.csproj"", " EndProject Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""src"", ""src"", ""__SRC_FOLDER_GUID__"" EndProject -Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""Lib"", ""Lib"", ""__LIB_FOLDER_GUID__"" -EndProject Project(""{13B669BE-BB05-4DDF-9536-439F39A36129}"") = ""Lib"", ""src\Lib\Lib.csproj"", ""__LIB_PROJECT_GUID__"" EndProject Global @@ -173,8 +161,7 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - __LIB_FOLDER_GUID__ = __SRC_FOLDER_GUID__ - __LIB_PROJECT_GUID__ = __LIB_FOLDER_GUID__ + __LIB_PROJECT_GUID__ = __SRC_FOLDER_GUID__ EndGlobalSection EndGlobal "; @@ -345,6 +332,70 @@ EndGlobal .Should().BeVisuallyEquivalentTo(expectedSlnContents); } + [Fact] + public void WhenProjectDirectoryIsAddedSolutionFoldersAreNotCreated() + { + var projectDirectory = TestAssets + .Get("TestAppWithSlnAndCsprojFiles") + .CreateInstance() + .WithSourceFiles() + .Root + .FullName; + + var projectToAdd = Path.Combine("Lib", "Lib.csproj"); + var cmd = new DotnetCommand() + .WithWorkingDirectory(projectDirectory) + .ExecuteWithCapturedOutput($"sln App.sln add {projectToAdd}"); + cmd.Should().Pass(); + + var slnFile = SlnFile.Read(Path.Combine(projectDirectory, "App.sln")); + var solutionFolderProjects = slnFile.Projects.Where( + p => p.TypeGuid == ProjectTypeGuids.SolutionFolderGuid); + solutionFolderProjects.Count().Should().Be(0); + slnFile.Sections.GetSection("NestedProjects").Should().BeNull(); + } + + [Theory] + [InlineData(".")] + [InlineData("")] + public void WhenSolutionFolderExistsItDoesNotGetAdded(string firstComponent) + { + var projectDirectory = TestAssets + .Get("TestAppWithSlnAndSolutionFolders") + .CreateInstance() + .WithSourceFiles() + .Root + .FullName; + + var projectToAdd = Path.Combine($"{firstComponent}", "src", "src", "Lib", "Lib.csproj"); + var cmd = new DotnetCommand() + .WithWorkingDirectory(projectDirectory) + .ExecuteWithCapturedOutput($"sln App.sln add {projectToAdd}"); + cmd.Should().Pass(); + + var slnFile = SlnFile.Read(Path.Combine(projectDirectory, "App.sln")); + slnFile.Projects.Count().Should().Be(4); + + var solutionFolderProjects = slnFile.Projects.Where( + p => p.TypeGuid == ProjectTypeGuids.SolutionFolderGuid); + solutionFolderProjects.Count().Should().Be(2); + + var solutionFolders = slnFile.Sections.GetSection("NestedProjects").Properties; + solutionFolders.Count.Should().Be(3); + + solutionFolders["{DDF3765C-59FB-4AA6-BE83-779ED13AA64A}"] + .Should().Be("{72BFCA87-B033-4721-8712-4D12166B4A39}"); + + var newlyAddedSrcFolder = solutionFolderProjects.Where( + p => p.Id != "{72BFCA87-B033-4721-8712-4D12166B4A39}").Single(); + solutionFolders[newlyAddedSrcFolder.Id] + .Should().Be("{72BFCA87-B033-4721-8712-4D12166B4A39}"); + + var libProject = slnFile.Projects.Where(p => p.Name == "Lib").Single(); + solutionFolders[libProject.Id] + .Should().Be(newlyAddedSrcFolder.Id); + } + [Theory] [InlineData("TestAppWithSlnAndCsprojFiles", ExpectedSlnFileAfterAddingLibProj, "")] [InlineData("TestAppWithSlnAndCsprojProjectGuidFiles", ExpectedSlnFileAfterAddingLibProj, "{84A45D44-B677-492D-A6DA-B3A71135AB8E}")] @@ -574,12 +625,6 @@ EndGlobal } var slnContents = slnTemplate.Replace("__LIB_PROJECT_GUID__", expectedLibProjectGuid); - var matchingLibFolder = slnFile.Projects - .Where((p) => p.FilePath == "Lib") - .ToList(); - matchingLibFolder.Count.Should().Be(1); - slnContents = slnContents.Replace("__LIB_FOLDER_GUID__", matchingLibFolder[0].Id); - var matchingSrcFolder = slnFile.Projects .Where((p) => p.FilePath == "src") .ToList();