From a2088e26413160325dd8532b2b8497354e09ee8f Mon Sep 17 00:00:00 2001 From: Justin Goshi Date: Mon, 23 Jan 2017 13:01:58 -0800 Subject: [PATCH] Refactor and finish the feature --- src/dotnet/SlnFileExtensions.cs | 310 ++++++++++++++++++ .../commands/dotnet-migrate/MigrateCommand.cs | 10 +- src/dotnet/commands/dotnet-sln/add/Program.cs | 165 +--------- .../commands/dotnet-sln/remove/Program.cs | 121 +------ .../GivenThatIWantToMigrateSolutions.cs | 2 +- 5 files changed, 322 insertions(+), 286 deletions(-) create mode 100644 src/dotnet/SlnFileExtensions.cs diff --git a/src/dotnet/SlnFileExtensions.cs b/src/dotnet/SlnFileExtensions.cs new file mode 100644 index 000000000..fc8054ef8 --- /dev/null +++ b/src/dotnet/SlnFileExtensions.cs @@ -0,0 +1,310 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.DotNet.Cli.Sln.Internal; +using Microsoft.DotNet.Cli.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.DotNet.Tools.Common +{ + public static class SlnFileExtensions + { + public static void AddProject(this SlnFile slnFile, string fullProjectPath) + { + var relativeProjectPath = PathUtility.GetRelativePath( + PathUtility.EnsureTrailingSlash(slnFile.BaseDirectory), + fullProjectPath); + + if (slnFile.Projects.Any((p) => + string.Equals(p.FilePath, relativeProjectPath, StringComparison.OrdinalIgnoreCase))) + { + Reporter.Output.WriteLine(string.Format( + CommonLocalizableStrings.SolutionAlreadyContainsProject, + slnFile.FullPath, + relativeProjectPath)); + } + else + { + var projectInstance = new ProjectInstance(fullProjectPath); + + var slnProject = new SlnProject + { + Id = projectInstance.GetProjectId(), + TypeGuid = projectInstance.GetProjectTypeGuid(), + Name = Path.GetFileNameWithoutExtension(relativeProjectPath), + FilePath = relativeProjectPath + }; + + slnFile.AddDefaultBuildConfigurations(slnProject); + + slnFile.AddSolutionFolders(slnProject); + + slnFile.Projects.Add(slnProject); + + Reporter.Output.WriteLine( + string.Format(CommonLocalizableStrings.ProjectAddedToTheSolution, relativeProjectPath)); + } + } + + public static void AddDefaultBuildConfigurations(this SlnFile slnFile, SlnProject slnProject) + { + var defaultConfigurations = new List() + { + "Debug|Any CPU", + "Debug|x64", + "Debug|x86", + "Release|Any CPU", + "Release|x64", + "Release|x86", + }; + + // NOTE: The order you create the sections determines the order they are written to the sln + // file. In the case of an empty sln file, in order to make sure the solution configurations + // section comes first we need to add it first. This doesn't affect correctness but does + // stop VS from re-ordering things later on. Since we are keeping the SlnFile class low-level + // it shouldn't care about the VS implementation details. That's why we handle this here. + AddDefaultSolutionConfigurations(defaultConfigurations, slnFile.SolutionConfigurationsSection); + AddDefaultProjectConfigurations( + defaultConfigurations, + slnFile.ProjectConfigurationsSection.GetOrCreatePropertySet(slnProject.Id)); + } + + private static void AddDefaultSolutionConfigurations( + List defaultConfigurations, + SlnPropertySet solutionConfigs) + { + foreach (var config in defaultConfigurations) + { + if (!solutionConfigs.ContainsKey(config)) + { + solutionConfigs[config] = config; + } + } + } + + private static void AddDefaultProjectConfigurations( + List defaultConfigurations, + SlnPropertySet projectConfigs) + { + foreach (var config in defaultConfigurations) + { + var activeCfgKey = $"{config}.ActiveCfg"; + if (!projectConfigs.ContainsKey(activeCfgKey)) + { + projectConfigs[activeCfgKey] = config; + } + + var build0Key = $"{config}.Build.0"; + if (!projectConfigs.ContainsKey(build0Key)) + { + projectConfigs[build0Key] = config; + } + } + } + + public static void AddSolutionFolders(this SlnFile slnFile, SlnProject slnProject) + { + var solutionFolders = slnProject.GetSolutionFoldersFromProject(); + + if (solutionFolders.Any()) + { + var nestedProjectsSection = slnFile.Sections.GetOrCreateSection( + "NestedProjects", + SlnSectionType.PreProcess); + + var pathToGuidMap = slnFile.GetSolutionFolderPaths(nestedProjectsSection.Properties); + + string parentDirGuid = null; + var solutionFolderHierarchy = string.Empty; + foreach (var dir in solutionFolders) + { + solutionFolderHierarchy = Path.Combine(solutionFolderHierarchy, dir); + if (pathToGuidMap.ContainsKey(solutionFolderHierarchy)) + { + 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; + } + } + + nestedProjectsSection.Properties[slnProject.Id] = parentDirGuid; + } + } + + private static IDictionary GetSolutionFolderPaths( + this 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; + } + + public static bool RemoveProject(this SlnFile slnFile, string projectPath) + { + var projectPathNormalized = PathUtility.GetPathWithDirectorySeparator(projectPath); + + var projectsToRemove = slnFile.Projects.Where((p) => + string.Equals(p.FilePath, projectPathNormalized, StringComparison.OrdinalIgnoreCase)).ToList(); + + bool projectRemoved = false; + if (projectsToRemove.Count == 0) + { + Reporter.Output.WriteLine(string.Format( + CommonLocalizableStrings.ProjectReferenceCouldNotBeFound, + projectPath)); + } + else + { + foreach (var slnProject in projectsToRemove) + { + var buildConfigsToRemove = slnFile.ProjectConfigurationsSection.GetPropertySet(slnProject.Id); + if (buildConfigsToRemove != null) + { + slnFile.ProjectConfigurationsSection.Remove(buildConfigsToRemove); + } + + var nestedProjectsSection = slnFile.Sections.GetSection( + "NestedProjects", + SlnSectionType.PreProcess); + if (nestedProjectsSection != null && nestedProjectsSection.Properties.ContainsKey(slnProject.Id)) + { + nestedProjectsSection.Properties.Remove(slnProject.Id); + } + + slnFile.Projects.Remove(slnProject); + Reporter.Output.WriteLine( + string.Format(CommonLocalizableStrings.ProjectReferenceRemoved, slnProject.FilePath)); + } + + projectRemoved = true; + } + + return projectRemoved; + } + + public static void RemoveEmptyConfigurationSections(this SlnFile slnFile) + { + if (slnFile.Projects.Count == 0) + { + var solutionConfigs = slnFile.Sections.GetSection("SolutionConfigurationPlatforms"); + if (solutionConfigs != null) + { + slnFile.Sections.Remove(solutionConfigs); + } + + var projectConfigs = slnFile.Sections.GetSection("ProjectConfigurationPlatforms"); + if (projectConfigs != null) + { + slnFile.Sections.Remove(projectConfigs); + } + } + } + + public static void RemoveEmptySolutionFolders(this SlnFile slnFile) + { + var solutionFolderProjects = slnFile.Projects + .GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid) + .ToList(); + + if (solutionFolderProjects.Any()) + { + var nestedProjectsSection = slnFile.Sections.GetSection( + "NestedProjects", + SlnSectionType.PreProcess); + + if (nestedProjectsSection == null) + { + foreach (var solutionFolderProject in solutionFolderProjects) + { + if (solutionFolderProject.Sections.Count() == 0) + { + slnFile.Projects.Remove(solutionFolderProject); + } + } + } + else + { + var solutionFoldersInUse = slnFile.GetSolutionFoldersThatContainProjectsInItsHierarchy( + nestedProjectsSection.Properties); + + foreach (var solutionFolderProject in solutionFolderProjects) + { + if (!solutionFoldersInUse.Contains(solutionFolderProject.Id)) + { + nestedProjectsSection.Properties.Remove(solutionFolderProject.Id); + if (solutionFolderProject.Sections.Count() == 0) + { + slnFile.Projects.Remove(solutionFolderProject); + } + } + } + + if (nestedProjectsSection.IsEmpty) + { + slnFile.Sections.Remove(nestedProjectsSection); + } + } + } + } + + private static HashSet GetSolutionFoldersThatContainProjectsInItsHierarchy( + this 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/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs b/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs index 70c9cd68d..c76293d71 100644 --- a/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs +++ b/src/dotnet/commands/dotnet-migrate/MigrateCommand.cs @@ -174,9 +174,7 @@ namespace Microsoft.DotNet.Tools.Migrate private void RemoveReferencesToMigratedFiles(SlnFile slnFile) { - // TODO: Need to merge my PRs then I can call the ProjectCollection extension methods. - // And create new extension methods. - var solutionFolders = slnFile.Projects.Where(p => p.TypeGuid == ProjectTypeGuids.SolutionFolderGuid); + var solutionFolders = slnFile.Projects.GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid); foreach (var solutionFolder in solutionFolders) { @@ -184,8 +182,14 @@ namespace Microsoft.DotNet.Tools.Migrate if (solutionItems != null && solutionItems.Properties.ContainsKey("global.json")) { solutionItems.Properties.Remove("global.json"); + if (solutionItems.IsEmpty) + { + solutionFolder.Sections.Remove(solutionItems); + } } } + + slnFile.RemoveEmptySolutionFolders(); } private void AddProject(string slnPath, string csprojPath) diff --git a/src/dotnet/commands/dotnet-sln/add/Program.cs b/src/dotnet/commands/dotnet-sln/add/Program.cs index 889506662..6b6124a29 100644 --- a/src/dotnet/commands/dotnet-sln/add/Program.cs +++ b/src/dotnet/commands/dotnet-sln/add/Program.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.Build.Construction; -using Microsoft.Build.Evaluation; -using Microsoft.Build.Execution; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.Sln.Internal; using Microsoft.DotNet.Cli.Utils; @@ -49,7 +46,7 @@ namespace Microsoft.DotNet.Tools.Sln.Add int preAddProjectCount = slnFile.Projects.Count; foreach (var fullProjectPath in fullProjectPaths) { - AddProject(slnFile, fullProjectPath); + slnFile.AddProject(fullProjectPath); } if (slnFile.Projects.Count > preAddProjectCount) @@ -59,165 +56,5 @@ namespace Microsoft.DotNet.Tools.Sln.Add return 0; } - - private void AddProject(SlnFile slnFile, string fullProjectPath) - { - var relativeProjectPath = PathUtility.GetRelativePath( - PathUtility.EnsureTrailingSlash(slnFile.BaseDirectory), - fullProjectPath); - - if (slnFile.Projects.Any((p) => - string.Equals(p.FilePath, relativeProjectPath, StringComparison.OrdinalIgnoreCase))) - { - Reporter.Output.WriteLine(string.Format( - CommonLocalizableStrings.SolutionAlreadyContainsProject, - slnFile.FullPath, - relativeProjectPath)); - } - else - { - var projectInstance = new ProjectInstance(fullProjectPath); - - var slnProject = new SlnProject - { - Id = projectInstance.GetProjectId(), - TypeGuid = projectInstance.GetProjectTypeGuid(), - Name = Path.GetFileNameWithoutExtension(relativeProjectPath), - FilePath = relativeProjectPath - }; - - AddDefaultBuildConfigurations(slnFile, slnProject); - - AddSolutionFolders(slnFile, slnProject); - - slnFile.Projects.Add(slnProject); - - Reporter.Output.WriteLine( - string.Format(CommonLocalizableStrings.ProjectAddedToTheSolution, relativeProjectPath)); - } - } - - private void AddDefaultBuildConfigurations(SlnFile slnFile, SlnProject slnProject) - { - var defaultConfigurations = new List() - { - "Debug|Any CPU", - "Debug|x64", - "Debug|x86", - "Release|Any CPU", - "Release|x64", - "Release|x86", - }; - - // NOTE: The order you create the sections determines the order they are written to the sln - // file. In the case of an empty sln file, in order to make sure the solution configurations - // section comes first we need to add it first. This doesn't affect correctness but does - // stop VS from re-ordering things later on. Since we are keeping the SlnFile class low-level - // it shouldn't care about the VS implementation details. That's why we handle this here. - AddDefaultSolutionConfigurations(defaultConfigurations, slnFile.SolutionConfigurationsSection); - AddDefaultProjectConfigurations( - defaultConfigurations, - slnFile.ProjectConfigurationsSection.GetOrCreatePropertySet(slnProject.Id)); - } - - private void AddDefaultSolutionConfigurations( - List defaultConfigurations, - SlnPropertySet solutionConfigs) - { - foreach (var config in defaultConfigurations) - { - if (!solutionConfigs.ContainsKey(config)) - { - solutionConfigs[config] = config; - } - } - } - - private void AddDefaultProjectConfigurations( - List defaultConfigurations, - SlnPropertySet projectConfigs) - { - foreach (var config in defaultConfigurations) - { - var activeCfgKey = $"{config}.ActiveCfg"; - if (!projectConfigs.ContainsKey(activeCfgKey)) - { - projectConfigs[activeCfgKey] = config; - } - - var build0Key = $"{config}.Build.0"; - if (!projectConfigs.ContainsKey(build0Key)) - { - projectConfigs[build0Key] = config; - } - } - } - - private void AddSolutionFolders(SlnFile slnFile, SlnProject slnProject) - { - var solutionFolders = slnProject.GetSolutionFoldersFromProject(); - - if (solutionFolders.Any()) - { - var nestedProjectsSection = slnFile.Sections.GetOrCreateSection( - "NestedProjects", - SlnSectionType.PreProcess); - - var pathToGuidMap = GetSolutionFolderPaths(slnFile, nestedProjectsSection.Properties); - - string parentDirGuid = null; - var solutionFolderHierarchy = string.Empty; - foreach (var dir in solutionFolders) - { - solutionFolderHierarchy = Path.Combine(solutionFolderHierarchy, dir); - if (pathToGuidMap.ContainsKey(solutionFolderHierarchy)) - { - 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; - } - } - - 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 18f3866e5..c147052cd 100644 --- a/src/dotnet/commands/dotnet-sln/remove/Program.cs +++ b/src/dotnet/commands/dotnet-sln/remove/Program.cs @@ -48,12 +48,12 @@ namespace Microsoft.DotNet.Tools.Sln.Remove bool slnChanged = false; foreach (var path in relativeProjectPaths) { - slnChanged |= RemoveProject(slnFile, path); + slnChanged |= slnFile.RemoveProject(path); } - RemoveEmptyConfigurationSections(slnFile); + slnFile.RemoveEmptyConfigurationSections(); - RemoveEmptySolutionFolders(slnFile); + slnFile.RemoveEmptySolutionFolders(); if (slnChanged) { @@ -62,120 +62,5 @@ namespace Microsoft.DotNet.Tools.Sln.Remove return 0; } - - private bool RemoveProject(SlnFile slnFile, string projectPath) - { - var projectPathNormalized = PathUtility.GetPathWithDirectorySeparator(projectPath); - - var projectsToRemove = slnFile.Projects.Where((p) => - string.Equals(p.FilePath, projectPathNormalized, StringComparison.OrdinalIgnoreCase)).ToList(); - - bool projectRemoved = false; - if (projectsToRemove.Count == 0) - { - Reporter.Output.WriteLine(string.Format( - CommonLocalizableStrings.ProjectReferenceCouldNotBeFound, - projectPath)); - } - else - { - foreach (var slnProject in projectsToRemove) - { - var buildConfigsToRemove = slnFile.ProjectConfigurationsSection.GetPropertySet(slnProject.Id); - if (buildConfigsToRemove != null) - { - slnFile.ProjectConfigurationsSection.Remove(buildConfigsToRemove); - } - - var nestedProjectsSection = slnFile.Sections.GetSection( - "NestedProjects", - SlnSectionType.PreProcess); - if (nestedProjectsSection != null && nestedProjectsSection.Properties.ContainsKey(slnProject.Id)) - { - nestedProjectsSection.Properties.Remove(slnProject.Id); - } - - slnFile.Projects.Remove(slnProject); - Reporter.Output.WriteLine( - string.Format(CommonLocalizableStrings.ProjectReferenceRemoved, slnProject.FilePath)); - } - - projectRemoved = true; - } - - return projectRemoved; - } - - private void RemoveEmptyConfigurationSections(SlnFile slnFile) - { - if (slnFile.Projects.Count == 0) - { - var solutionConfigs = slnFile.Sections.GetSection("SolutionConfigurationPlatforms"); - if (solutionConfigs != null) - { - slnFile.Sections.Remove(solutionConfigs); - } - - var projectConfigs = slnFile.Sections.GetSection("ProjectConfigurationPlatforms"); - if (projectConfigs != null) - { - slnFile.Sections.Remove(projectConfigs); - } - } - } - - private void RemoveEmptySolutionFolders(SlnFile slnFile) - { - var solutionFolderProjects = slnFile.Projects - .GetProjectsByType(ProjectTypeGuids.SolutionFolderGuid) - .ToList(); - - if (solutionFolderProjects.Any()) - { - var nestedProjectsSection = slnFile.Sections.GetSection( - "NestedProjects", - SlnSectionType.PreProcess); - - var solutionFoldersInUse = GetSolutionFoldersThatContainProjectsInItsHierarchy( - slnFile, - nestedProjectsSection.Properties); - - foreach (var solutionFolderProject in solutionFolderProjects) - { - if (!solutionFoldersInUse.Contains(solutionFolderProject.Id)) - { - slnFile.Projects.Remove(solutionFolderProject); - nestedProjectsSection.Properties.Remove(solutionFolderProject.Id); - } - } - - if (nestedProjectsSection.IsEmpty) - { - slnFile.Sections.Remove(nestedProjectsSection); - } - } - } - - 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-migrate.Tests/GivenThatIWantToMigrateSolutions.cs b/test/dotnet-migrate.Tests/GivenThatIWantToMigrateSolutions.cs index f70ce7c08..0a670cb7f 100644 --- a/test/dotnet-migrate.Tests/GivenThatIWantToMigrateSolutions.cs +++ b/test/dotnet-migrate.Tests/GivenThatIWantToMigrateSolutions.cs @@ -234,7 +234,7 @@ EndGlobal string expectedSlnContents) { var projectDirectory = TestAssets - .Get("NonRestoredTestProjects", "PJAppWithSlnAndSolutionItemsToMoveToBackup") + .GetProjectJson(TestAssetKinds.NonRestoredTestProjects, "PJAppWithSlnAndSolutionItemsToMoveToBackup") .CreateInstance(Path.GetFileNameWithoutExtension(slnFileName)) .WithSourceFiles() .Root