Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for dotnet sln add file and dotnet sln add folder #45072

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
870 changes: 485 additions & 385 deletions src/Cli/dotnet/SlnFileExtensions.cs

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions src/Cli/dotnet/SlnProjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,18 @@ public static string GetFullSolutionFolderPath(this SlnProject slnProject)

return path;
}

public static SlnSection GetSolutionItemsSectionOrDefault(this SlnProject project) =>
project.Sections.GetSection("SolutionItems", SlnSectionType.PreProcess);

public static bool ContainsSolutionItem(this SlnProject project, string solutionItemName)
{
var section = project.GetSolutionItemsSectionOrDefault();
if (section == null) { return false; }

// solution item names are case-insensitive
return new Dictionary<string, string>(section.GetContent(), StringComparer.OrdinalIgnoreCase)
.ContainsKey(solutionItemName);
}
}
}
79 changes: 78 additions & 1 deletion src/Cli/dotnet/commands/dotnet-sln/LocalizableStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,42 @@
<data name="AppHelpText" xml:space="preserve">
<value>Projects to add or to remove from the solution.</value>
</data>
<data name="CouldNotFindFile" xml:space="preserve">
<value>Could not find file `{0}`.</value>
</data>
<data name="AddAppFullName" xml:space="preserve">
<value>Add one or more projects to a solution file.</value>
</data>
<data name="AddFileFullName" xml:space="preserve">
<value>Add one or more solution items to a solution file.</value>
</data>
<data name="AddFolderFullName" xml:space="preserve">
<value>Add one or more solution folders to a solution file.</value>
</data>
<data name="AddProjectPathArgumentName" xml:space="preserve">
<value>PROJECT_PATH</value>
</data>
<data name="ProjectPathArgumentShouldNotBeProvidedForDotnetSlnAddFile" xml:space="preserve">
<value>PROJECT_PATH should not be provided for `dotnet sln add file`</value>
</data>
<data name="ProjectPathArgumentShouldNotBeProvidedForDotnetSlnAddFolder" xml:space="preserve">
<value>PROJECT_PATH should not be provided for `dotnet sln add folder`</value>
</data>
<data name="AddProjectPathArgumentDescription" xml:space="preserve">
<value>The paths to the projects to add to the solution.</value>
</data>
<data name="AddFilePathArgumentName" xml:space="preserve">
<value>FILE_PATH</value>
</data>
<data name="AddFilePathArgumentDescription" xml:space="preserve">
<value>The paths to the solution items to add to the solution.</value>
</data>
<data name="AddFolderPathArgumentName" xml:space="preserve">
<value>FOLDER_PATH</value>
</data>
<data name="AddFolderPathArgumentDescription" xml:space="preserve">
<value>The paths to the solution folders to add to the solution.</value>
</data>
<data name="RemoveProjectPathArgumentName" xml:space="preserve">
<value>PROJECT_PATH</value>
</data>
Expand All @@ -150,6 +177,32 @@
<data name="RemoveAppFullName" xml:space="preserve">
<value>Remove one or more projects from a solution file.</value>
</data>
<data name="SpecifyAtLeastOneFileToAdd" xml:space="preserve">
<value>You must specify at least one file to add.</value>
</data>
<data name="SpecifyAtLeastOneFolderToAdd" xml:space="preserve">
<value>You must specify at least one folder to add.</value>
</data>
<data name="SolutionAlreadyContainsFile" xml:space="preserve">
<value>The solution {0} already contains the solution item `{1}`</value>
</data>
<data name="SolutionAlreadyContainsFolder" xml:space="preserve">
<value>The solution {0} already contains the solution folder `{1}`</value>
</data>
<data name="SolutionItemAddedToTheSolution" xml:space="preserve">
<value>The solution item `{0}` was added to the solution folder `{1}`</value>
</data>
<data name="SolutionFolderAddedToTheSolution" xml:space="preserve">
<value>The solution folder `{0}` was added to the solution</value>
</data>
<data name="SolutionFolderNameCannot" xml:space="preserve">
<value>Solution Folder names cannot:
- contain any of the following characters: / : ? \ * &quot; &lt; &gt; |
- contain Unicode control characters
- contain surrogate characters
- be system reserved names, including CON, AUX, PRN, COM1 or LPT2
- be . or ..</value>
</data>
<data name="RemoveSubcommandHelpText" xml:space="preserve">
<value>Remove the specified project(s) from the solution. The project is not impacted.</value>
</data>
Expand All @@ -162,15 +215,39 @@
<data name="ProjectsHeader" xml:space="preserve">
<value>Project(s)</value>
</data>
<data name="InRoot" xml:space="preserve">
<data name="SolutionElementType" xml:space="preserve">
<value>The type of the solution element to add or remove. Allowed values are project, item, and folder.</value>
</data>
<data name="AddProjectInRootArgumentDescription" xml:space="preserve">
<value>Place project in root of the solution, rather than creating a solution folder.</value>
</data>
<data name="AddFileInRootArgumentDescription" xml:space="preserve">
<value>Place file in root of the solution, rather than creating a solution folder.</value>
</data>
<data name="AddFolderInRootArgumentDescription" xml:space="preserve">
<value>Place folder in root of the solution, rather than creating a solution folder.</value>
</data>
<data name="AddProjectSolutionFolderArgumentDescription" xml:space="preserve">
<value>The destination solution folder path to add the projects to.</value>
</data>
<data name="AddFileSolutionFolderArgumentDescription" xml:space="preserve">
<value>The destination solution folder path to add the files to.</value>
</data>
<data name="AddFolderSolutionFolderArgumentDescription" xml:space="preserve">
<value>The destination solution folder path to add the folders to.</value>
</data>
<data name="SolutionItemWithTheSameNameExists" xml:space="preserve">
<value>There is already an existing solution item '{0}' with the same name in the solution folder '{1}'.</value>
</data>
<data name="SolutionFolderAndInRootMutuallyExclusive" xml:space="preserve">
<value>The --solution-folder and --in-root options cannot be used together; use only one of the options.</value>
</data>
<data name="CannotAddTheSameSolutionToItself" xml:space="preserve">
<value>Cannot add the same solution to itself.</value>
</data>
<data name="CannotAddExistingProjectAsSolutionItem" xml:space="preserve">
<value>An existing project cannot be added as a solution item.</value>
</data>
<data name="ListSolutionFoldersArgumentDescription" xml:space="preserve">
<value>Display solution folder paths.</value>
</data>
Expand Down
131 changes: 85 additions & 46 deletions src/Cli/dotnet/commands/dotnet-sln/SlnArgumentValidator.cs
Original file line number Diff line number Diff line change
@@ -1,60 +1,99 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.Utils;

namespace Microsoft.DotNet.Tools.Sln
namespace Microsoft.DotNet.Tools.Sln;

internal static class SlnArgumentValidator
{
internal static class SlnArgumentValidator
private static readonly SearchValues<char> s_invalidCharactersInSolutionFolderName = SearchValues.Create("/:?\\*\"<>|");
private static readonly string[] s_invalidSolutionFolderNames =
[
// system reserved names per https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
"CON", "PRN", "AUX", "NUL",
"COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
// relative path components
".", "..",
];

public enum CommandType
{
Add,
Remove
}
public static void ParseAndValidateArguments(IReadOnlyList<string> _arguments, CommandType commandType, bool _inRoot = false, string relativeRoot = null, string subcommand = null)
{
public enum CommandType
if (_arguments.Count == 0)
{
Add,
Remove
string message = commandType == CommandType.Add
? CommonLocalizableStrings.SpecifyAtLeastOneProjectToAdd
: CommonLocalizableStrings.SpecifyAtLeastOneProjectToRemove;
throw new GracefulException(message);
}
public static void ParseAndValidateArguments(string _fileOrDirectory, IReadOnlyCollection<string> _arguments, CommandType commandType, bool _inRoot = false, string relativeRoot = null)

bool hasRelativeRoot = !string.IsNullOrEmpty(relativeRoot);

if (_inRoot && hasRelativeRoot)
{
if (_arguments.Count == 0)
{
string message = commandType == CommandType.Add ? CommonLocalizableStrings.SpecifyAtLeastOneProjectToAdd : CommonLocalizableStrings.SpecifyAtLeastOneProjectToRemove;
throw new GracefulException(message);
}

bool hasRelativeRoot = !string.IsNullOrEmpty(relativeRoot);

if (_inRoot && hasRelativeRoot)
{
// These two options are mutually exclusive
throw new GracefulException(LocalizableStrings.SolutionFolderAndInRootMutuallyExclusive);
}

var slnFile = _arguments.FirstOrDefault(path => path.EndsWith(".sln"));
if (slnFile != null)
{
string args;
if (_inRoot)
{
args = $"--{SlnAddParser.InRootOption.Name} ";
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a bug where there were 4 dashes.
image

}
else if (hasRelativeRoot)
{
args = $"--{SlnAddParser.SolutionFolderOption.Name} {string.Join(" ", relativeRoot)} ";
}
else
{
args = "";
}

var projectArgs = string.Join(" ", _arguments.Where(path => !path.EndsWith(".sln")));
string command = commandType == CommandType.Add ? "add" : "remove";
throw new GracefulException(new string[]
{
string.Format(CommonLocalizableStrings.SolutionArgumentMisplaced, slnFile),
CommonLocalizableStrings.DidYouMean,
$" dotnet solution {slnFile} {command} {args}{projectArgs}"
});
}
// These two options are mutually exclusive
throw new GracefulException(LocalizableStrings.SolutionFolderAndInRootMutuallyExclusive);
}

// Something is wrong if there is a .sln file as an argument, so suggest that the arguments may have been misplaced.
// However, it is possible to add .sln file as a solution item, so don't suggest in the case of dotnet sln add file.
var slnFile = _arguments.FirstOrDefault(path => path.EndsWith(".sln", StringComparison.OrdinalIgnoreCase));
if (slnFile == null || subcommand == "file")
{
return;
}

string options = _inRoot
? $"{SlnAddParser.InRootOption.Name} "
: hasRelativeRoot
? $"{SlnAddParser.SolutionFolderOption.Name} {string.Join(" ", relativeRoot)} "
: "";

var nonSolutionArguments = string.Join(
" ",
_arguments.Where(a => !a.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)));

string command = commandType switch
{
CommandType.Add => "add",
CommandType.Remove => "remove",
_ => throw new InvalidOperationException($"Unable to handle command type {commandType}"),
};
throw new GracefulException(
[
string.Format(CommonLocalizableStrings.SolutionArgumentMisplaced, slnFile),
CommonLocalizableStrings.DidYouMean,
subcommand == null
? $" dotnet solution {slnFile} {command} {options}{nonSolutionArguments}"
: $" dotnet solution {slnFile} {command} {subcommand} {options}{nonSolutionArguments}"
]);
}

public static bool IsValidSolutionFolderName(string folderName)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

{
if (string.IsNullOrWhiteSpace(folderName))
return false;

if (folderName.AsSpan().IndexOfAny(s_invalidCharactersInSolutionFolderName) >= 0)
return false;

if (folderName.Any(char.IsControl))
return false;

if (folderName.Any(char.IsSurrogate))
return false;

if (s_invalidSolutionFolderNames.Contains(folderName, StringComparer.OrdinalIgnoreCase))
return false;

return true;
}
}
Loading