diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md
index 2a06c445a566..e0747e8f1d90 100644
--- a/documentation/general/dotnet-run-file.md
+++ b/documentation/general/dotnet-run-file.md
@@ -102,6 +102,9 @@ To opt out, use `#:property PublishAot=false` directive in your `.cs` file.
Command `dotnet clean file.cs` can be used to clean build artifacts of the file-based program.
+Commands `dotnet package add PackageName --file app.cs` and `dotnet package remove PackageName --file app.cs`
+can be used to manipulate `#:package` directives in the C# files, similarly to what the commands do for project-based apps.
+
## Entry points
If a file is given to `dotnet run`, it has to be an *entry-point file*, otherwise an error is reported.
@@ -378,8 +381,7 @@ We could also add `dotnet compile` command that would be the equivalent of `dotn
`dotnet clean` could be extended to support cleaning all file-based app outputs,
e.g., `dotnet clean --all-file-based-apps`.
-Adding references via `dotnet package add`/`dotnet reference add` could be supported for file-based programs as well,
-i.e., the command would add a `#:package`/`#:project` directive to the top of a `.cs` file.
+More NuGet commands (like `dotnet nuget why` or `dotnet package list`) could be supported for file-based programs as well.
### Explicit importing
diff --git a/src/Cli/dotnet/CliStrings.resx b/src/Cli/dotnet/CliStrings.resx
index 39e1a3e261df..a41cfa9c3ea1 100644
--- a/src/Cli/dotnet/CliStrings.resx
+++ b/src/Cli/dotnet/CliStrings.resx
@@ -273,9 +273,18 @@
PROJECT
+
+ PROJECT | FILE
+
The project file to operate on. If a file is not specified, the command will search the current directory for one.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+ The file-based app to operate on.
+
FRAMEWORK
@@ -814,4 +823,4 @@ The default is 'false.' However, when targeting .NET 7 or lower, the default is
Display the command schema as JSON.
-
\ No newline at end of file
+
diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx
index f45b7837ae10..ab136a927e42 100644
--- a/src/Cli/dotnet/Commands/CliCommandStrings.resx
+++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx
@@ -1280,6 +1280,11 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man
Specify only one package reference to remove.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
Command names conflict. Command names are case insensitive.
{0}
@@ -1590,14 +1595,14 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man
Duplicate directives are not supported: {0} at {1}
{0} is the directive type and name. {1} is the file path and line number.
-
- Cannot combine option '{0}' and '{1}'.
- {0} and {1} are option names like '--no-build'.
-
Cannot specify option '{0}' when also using '-' to read the file from standard input.
{0} is an option name like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
+
Warning: Binary log option was specified but build will be skipped because output is up to date, specify '--no-cache' to force build.
{Locked="--no-cache"}
diff --git a/src/Cli/dotnet/Commands/Hidden/Add/AddCommandParser.cs b/src/Cli/dotnet/Commands/Hidden/Add/AddCommandParser.cs
index 89c12c174c08..43cb90737cd6 100644
--- a/src/Cli/dotnet/Commands/Hidden/Add/AddCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Hidden/Add/AddCommandParser.cs
@@ -1,11 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
using System.CommandLine;
using Microsoft.DotNet.Cli.Commands.Hidden.Add.Package;
using Microsoft.DotNet.Cli.Commands.Hidden.Add.Reference;
+using Microsoft.DotNet.Cli.Commands.Package;
using Microsoft.DotNet.Cli.Extensions;
namespace Microsoft.DotNet.Cli.Commands.Hidden.Add;
@@ -14,11 +13,6 @@ internal static class AddCommandParser
{
public static readonly string DocsLink = "https://aka.ms/dotnet-add";
- public static readonly Argument ProjectArgument = new Argument(CliStrings.ProjectArgumentName)
- {
- Description = CliStrings.ProjectArgumentDescription
- }.DefaultToCurrentDirectory();
-
private static readonly Command Command = ConstructCommand();
public static Command GetCommand()
@@ -33,7 +27,7 @@ private static Command ConstructCommand()
Hidden = true
};
- command.Arguments.Add(ProjectArgument);
+ command.Arguments.Add(PackageCommandParser.ProjectOrFileArgument);
command.Subcommands.Add(AddPackageCommandParser.GetCommand());
command.Subcommands.Add(AddReferenceCommandParser.GetCommand());
diff --git a/src/Cli/dotnet/Commands/Hidden/Add/Package/AddPackageCommandParser.cs b/src/Cli/dotnet/Commands/Hidden/Add/Package/AddPackageCommandParser.cs
index ca1e014bf4a6..f6cb1909b23d 100644
--- a/src/Cli/dotnet/Commands/Hidden/Add/Package/AddPackageCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Hidden/Add/Package/AddPackageCommandParser.cs
@@ -1,12 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
using System.CommandLine;
using Microsoft.DotNet.Cli.Commands.Package;
using Microsoft.DotNet.Cli.Commands.Package.Add;
-using Microsoft.DotNet.Cli.Extensions;
namespace Microsoft.DotNet.Cli.Commands.Hidden.Add.Package;
@@ -32,20 +29,9 @@ private static Command ConstructCommand()
command.Options.Add(PackageAddCommandParser.InteractiveOption);
command.Options.Add(PackageAddCommandParser.PrereleaseOption);
command.Options.Add(PackageCommandParser.ProjectOption);
+ command.Options.Add(PackageCommandParser.FileOption);
- command.SetAction((parseResult) =>
- {
- // this command can be called with an argument or an option for the project path - we prefer the option.
- // if the option is not present, we use the argument value instead.
- if (parseResult.HasOption(PackageCommandParser.ProjectOption))
- {
- return new PackageAddCommand(parseResult, parseResult.GetValue(PackageCommandParser.ProjectOption)).Execute();
- }
- else
- {
- return new PackageAddCommand(parseResult, parseResult.GetValue(AddCommandParser.ProjectArgument) ?? Directory.GetCurrentDirectory()).Execute();
- }
- });
+ command.SetAction((parseResult) => new PackageAddCommand(parseResult).Execute());
return command;
}
diff --git a/src/Cli/dotnet/Commands/Hidden/Remove/Package/RemovePackageCommandParser.cs b/src/Cli/dotnet/Commands/Hidden/Remove/Package/RemovePackageCommandParser.cs
index bc661b2a71da..51eb32535cc2 100644
--- a/src/Cli/dotnet/Commands/Hidden/Remove/Package/RemovePackageCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Hidden/Remove/Package/RemovePackageCommandParser.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
using System.CommandLine;
using Microsoft.DotNet.Cli.Commands.Package.Remove;
diff --git a/src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandParser.cs b/src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandParser.cs
index 02ba2402c32f..0ac1f1834dbb 100644
--- a/src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Hidden/Remove/RemoveCommandParser.cs
@@ -1,11 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
using System.CommandLine;
using Microsoft.DotNet.Cli.Commands.Hidden.Remove.Package;
using Microsoft.DotNet.Cli.Commands.Hidden.Remove.Reference;
+using Microsoft.DotNet.Cli.Commands.Package;
using Microsoft.DotNet.Cli.Extensions;
namespace Microsoft.DotNet.Cli.Commands.Hidden.Remove;
@@ -14,11 +13,6 @@ internal static class RemoveCommandParser
{
public static readonly string DocsLink = "https://aka.ms/dotnet-remove";
- public static readonly Argument ProjectArgument = new Argument(CliStrings.ProjectArgumentName)
- {
- Description = CliStrings.ProjectArgumentDescription
- }.DefaultToCurrentDirectory();
-
private static readonly Command Command = ConstructCommand();
public static Command GetCommand()
@@ -33,7 +27,7 @@ private static Command ConstructCommand()
Hidden = true
};
- command.Arguments.Add(ProjectArgument);
+ command.Arguments.Add(PackageCommandParser.ProjectOrFileArgument);
command.Subcommands.Add(RemovePackageCommandParser.GetCommand());
command.Subcommands.Add(RemoveReferenceCommandParser.GetCommand());
diff --git a/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs b/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs
index fabaa4ba0787..46ddfe28c4b0 100644
--- a/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs
+++ b/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs
@@ -21,7 +21,7 @@ internal static bool AddPackageReference(string projectPath, string packageName,
{
commandArgs = commandArgs.Append(PackageAddCommandParser.VersionOption.Name).Append(version);
}
- var addPackageReferenceCommand = new PackageAddCommand(AddCommandParser.GetCommand().Parse([.. commandArgs]), projectPath);
+ var addPackageReferenceCommand = new PackageAddCommand(AddCommandParser.GetCommand().Parse([.. commandArgs]));
return addPackageReferenceCommand.Execute() == 0;
}
diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs
index 080fa97a52e6..2c686a89308f 100644
--- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs
+++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs
@@ -1,27 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
using System.CommandLine;
+using System.Diagnostics;
+using Microsoft.Build.Construction;
+using Microsoft.Build.Evaluation;
+using Microsoft.CodeAnalysis;
using Microsoft.DotNet.Cli.Commands.MSBuild;
using Microsoft.DotNet.Cli.Commands.NuGet;
+using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;
+using NuGet.ProjectModel;
namespace Microsoft.DotNet.Cli.Commands.Package.Add;
-///
-///
-/// Since this command is invoked via both 'package add' and 'add package', different symbols will control what the project path to search is.
-/// It's cleaner for the separate callsites to know this instead of pushing that logic here.
-///
-internal class PackageAddCommand(ParseResult parseResult, string fileOrDirectory) : CommandBase(parseResult)
+internal class PackageAddCommand(ParseResult parseResult) : CommandBase(parseResult)
{
- private readonly PackageIdentityWithRange _packageId = parseResult.GetValue(PackageAddCommandParser.CmdPackageArgument);
+ private readonly PackageIdentityWithRange _packageId = parseResult.GetValue(PackageAddCommandParser.CmdPackageArgument)!;
public override int Execute()
{
+ var (fileOrDirectory, allowedAppKinds) = PackageCommandParser.ProcessPathOptions(_parseResult);
+
+ if (allowedAppKinds.HasFlag(AppKinds.FileBased) && VirtualProjectBuildingCommand.IsValidEntryPointPath(fileOrDirectory))
+ {
+ return ExecuteForFileBasedApp(fileOrDirectory);
+ }
+
+ Debug.Assert(allowedAppKinds.HasFlag(AppKinds.ProjectBased));
+
string projectFilePath;
if (!File.Exists(fileOrDirectory))
{
@@ -114,7 +122,7 @@ private string[] TransformArgs(PackageIdentityWithRange packageId, string tempDg
if (packageId.HasVersion)
{
args.Add("--version");
- args.Add(packageId.VersionRange.OriginalString);
+ args.Add(packageId.VersionRange.OriginalString ?? string.Empty);
}
args.AddRange(_parseResult
@@ -133,4 +141,208 @@ private string[] TransformArgs(PackageIdentityWithRange packageId, string tempDg
return [.. args];
}
+
+ // More logic should live in NuGet: https://github.com/NuGet/Home/issues/14390
+ private int ExecuteForFileBasedApp(string path)
+ {
+ // Check disallowed options.
+ ReadOnlySpan disallowedOptions =
+ [
+ PackageAddCommandParser.FrameworkOption,
+ PackageAddCommandParser.SourceOption,
+ PackageAddCommandParser.PackageDirOption,
+ ];
+ foreach (var option in disallowedOptions)
+ {
+ if (_parseResult.HasOption(option))
+ {
+ throw new GracefulException(CliCommandStrings.InvalidOptionForFileBasedApp, option.Name);
+ }
+ }
+
+ bool hasVersion = _packageId.HasVersion;
+ bool prerelease = _parseResult.GetValue(PackageAddCommandParser.PrereleaseOption);
+
+ if (hasVersion && prerelease)
+ {
+ throw new GracefulException(CliCommandStrings.PrereleaseAndVersionAreNotSupportedAtTheSameTime);
+ }
+
+ var fullPath = Path.GetFullPath(path);
+
+ // Create restore command, used also for obtaining MSBuild properties.
+ bool interactive = _parseResult.GetValue(PackageAddCommandParser.InteractiveOption);
+ var command = new VirtualProjectBuildingCommand(
+ entryPointFileFullPath: fullPath,
+ msbuildArgs: MSBuildArgs.FromProperties(new Dictionary(2)
+ {
+ ["NuGetInteractive"] = interactive.ToString(),
+ // Floating versions are needed if user did not specify a version
+ // - then we restore with version '*' to determine the latest version.
+ ["CentralPackageFloatingVersionsEnabled"] = bool.TrueString,
+ }.AsReadOnly()))
+ {
+ NoCache = true,
+ NoBuild = true,
+ };
+ var projectCollection = new ProjectCollection();
+ var projectInstance = command.CreateProjectInstance(projectCollection);
+
+ // Set initial version to Directory.Packages.props and/or C# file
+ // (we always need to add the package reference to the C# file but when CPM is enabled, it's added without a version).
+ string version = hasVersion
+ ? _packageId.VersionRange?.OriginalString ?? string.Empty
+ : (prerelease
+ ? "*-*"
+ : "*");
+ bool skipUpdate = false;
+ var central = SetCentralVersion(version);
+ var local = SetLocalVersion(central != null ? null : version);
+
+ if (!_parseResult.GetValue(PackageAddCommandParser.NoRestoreOption))
+ {
+ // Restore.
+ int exitCode = command.Execute();
+ if (exitCode != 0)
+ {
+ // If restore fails, revert any changes made.
+ central?.Revert();
+ return exitCode;
+ }
+
+ // If no version was specified by the user, save the actually restored version.
+ if (!hasVersion && !skipUpdate)
+ {
+ var projectAssetsFile = projectInstance.GetProperty("ProjectAssetsFile")?.EvaluatedValue;
+ if (!File.Exists(projectAssetsFile))
+ {
+ Reporter.Verbose.WriteLine($"Assets file does not exist: {projectAssetsFile}");
+ }
+ else
+ {
+ var lockFile = new LockFileFormat().Read(projectAssetsFile);
+ var library = lockFile.Libraries.FirstOrDefault(l => string.Equals(l.Name, _packageId.Id, StringComparison.OrdinalIgnoreCase));
+ if (library != null)
+ {
+ var restoredVersion = library.Version.ToString();
+ if (central is { } centralValue)
+ {
+ centralValue.Update(restoredVersion);
+ local.Save();
+ }
+ else
+ {
+ local.Update(restoredVersion);
+ }
+
+ return 0;
+ }
+ }
+ }
+ }
+
+ central?.Save();
+ local.Save();
+ return 0;
+
+ (Action Save, Action Update) SetLocalVersion(string? version)
+ {
+ // Add #:package directive to the C# file.
+ var file = SourceFile.Load(fullPath);
+ var editor = FileBasedAppSourceEditor.Load(file);
+ editor.Add(new CSharpDirective.Package(default) { Name = _packageId.Id, Version = version });
+ command.Directives = editor.Directives;
+ return (Save, Update);
+
+ void Save()
+ {
+ editor.SourceFile.Save();
+ }
+
+ void Update(string value)
+ {
+ // Update the C# file with the given version.
+ editor.Add(new CSharpDirective.Package(default) { Name = _packageId.Id, Version = value });
+ editor.SourceFile.Save();
+ }
+ }
+
+ (Action Revert, Action Update, Action Save)? SetCentralVersion(string version)
+ {
+ // Find out whether CPM is enabled.
+ if (!MSBuildUtilities.ConvertStringToBool(projectInstance.GetProperty("ManagePackageVersionsCentrally")?.EvaluatedValue))
+ {
+ return null;
+ }
+
+ // Load the Directory.Packages.props project.
+ var directoryPackagesPropsPath = projectInstance.GetProperty("DirectoryPackagesPropsPath")?.EvaluatedValue;
+ if (!File.Exists(directoryPackagesPropsPath))
+ {
+ Reporter.Verbose.WriteLine($"Directory.Packages.props file does not exist: {directoryPackagesPropsPath}");
+ return null;
+ }
+
+ var snapshot = File.ReadAllText(directoryPackagesPropsPath);
+ var directoryPackagesPropsProject = projectCollection.LoadProject(directoryPackagesPropsPath);
+
+ const string packageVersionItemType = "PackageVersion";
+ const string versionAttributeName = "Version";
+
+ // Update existing PackageVersion if it exists.
+ var packageVersion = directoryPackagesPropsProject.GetItems(packageVersionItemType)
+ .LastOrDefault(i => string.Equals(i.EvaluatedInclude, _packageId.Id, StringComparison.OrdinalIgnoreCase));
+ if (packageVersion != null)
+ {
+ var packageVersionItemElement = packageVersion.Project.GetItemProvenance(packageVersion).LastOrDefault()?.ItemElement;
+ var versionAttribute = packageVersionItemElement?.Metadata.FirstOrDefault(i => i.Name.Equals(versionAttributeName, StringComparison.OrdinalIgnoreCase));
+ if (versionAttribute != null)
+ {
+ versionAttribute.Value = version;
+ directoryPackagesPropsProject.Save();
+
+ // If user didn't specify a version and a version is already specified in Directory.Packages.props,
+ // don't update the Directory.Packages.props (that's how the project-based equivalent behaves as well).
+ if (!hasVersion)
+ {
+ skipUpdate = true;
+ return (Revert: NoOp, Update: Unreachable, Save: Revert);
+
+ static void NoOp() { }
+ static void Unreachable(string value) => Debug.Fail("Unreachable.");
+ }
+
+ return (Revert, v => Update(versionAttribute, v), Save);
+ }
+ }
+
+ {
+ // Get the ItemGroup to add a PackageVersion to or create a new one.
+ var itemGroup = directoryPackagesPropsProject.Xml.ItemGroups
+ .Where(e => e.Items.Any(i => string.Equals(i.ItemType, packageVersionItemType, StringComparison.OrdinalIgnoreCase)))
+ .FirstOrDefault()
+ ?? directoryPackagesPropsProject.Xml.AddItemGroup();
+
+ // Add a PackageVersion item.
+ var item = itemGroup.AddItem(packageVersionItemType, _packageId.Id);
+ var metadata = item.AddMetadata(versionAttributeName, version, expressAsAttribute: true);
+ directoryPackagesPropsProject.Save();
+
+ return (Revert, v => Update(metadata, v), Save);
+ }
+
+ void Update(ProjectMetadataElement element, string value)
+ {
+ element.Value = value;
+ directoryPackagesPropsProject.Save();
+ }
+
+ void Revert()
+ {
+ File.WriteAllText(path: directoryPackagesPropsPath, contents: snapshot);
+ }
+
+ static void Save() { /* No-op by default. */ }
+ }
+ }
}
diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs
index 8a504a9e01aa..1d24fd9c10de 100644
--- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
using System.CommandLine;
using System.CommandLine.Completions;
using System.CommandLine.Parsing;
@@ -14,6 +12,12 @@ namespace Microsoft.DotNet.Cli.Commands.Package.Add;
public static class PackageAddCommandParser
{
+ public static readonly Option PrereleaseOption = new ForwardedOption("--prerelease")
+ {
+ Description = CliStrings.CommandPrereleaseOptionDescription,
+ Arity = ArgumentArity.Zero
+ }.ForwardAs("--prerelease");
+
public static readonly Argument CmdPackageArgument = CommonArguments.RequiredPackageIdentityArgument()
.AddCompletions((context) =>
{
@@ -70,12 +74,6 @@ public static class PackageAddCommandParser
public static readonly Option InteractiveOption = CommonOptions.InteractiveOption().ForwardIfEnabled("--interactive");
- public static readonly Option PrereleaseOption = new ForwardedOption("--prerelease")
- {
- Description = CliStrings.CommandPrereleaseOptionDescription,
- Arity = ArgumentArity.Zero
- }.ForwardAs("--prerelease");
-
private static readonly Command Command = ConstructCommand();
public static Command GetCommand()
@@ -97,15 +95,16 @@ private static Command ConstructCommand()
command.Options.Add(InteractiveOption);
command.Options.Add(PrereleaseOption);
command.Options.Add(PackageCommandParser.ProjectOption);
+ command.Options.Add(PackageCommandParser.FileOption);
- command.SetAction((parseResult) => new PackageAddCommand(parseResult, parseResult.GetValue(PackageCommandParser.ProjectOption)).Execute());
+ command.SetAction((parseResult) => new PackageAddCommand(parseResult).Execute());
return command;
}
private static void DisallowVersionIfPackageIdentityHasVersionValidator(OptionResult result)
{
- if (result.Parent.GetValue(CmdPackageArgument).HasVersion)
+ if (result.Parent?.GetValue(CmdPackageArgument).HasVersion == true)
{
result.AddError(CliCommandStrings.ValidationFailedDuplicateVersion);
}
diff --git a/src/Cli/dotnet/Commands/Package/PackageCommandParser.cs b/src/Cli/dotnet/Commands/Package/PackageCommandParser.cs
index 436751c13337..a6e44f5bccbe 100644
--- a/src/Cli/dotnet/Commands/Package/PackageCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Package/PackageCommandParser.cs
@@ -1,14 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
using System.CommandLine;
using Microsoft.DotNet.Cli.Commands.Package.Add;
using Microsoft.DotNet.Cli.Commands.Package.List;
using Microsoft.DotNet.Cli.Commands.Package.Remove;
using Microsoft.DotNet.Cli.Commands.Package.Search;
+using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Extensions;
+using Microsoft.DotNet.Cli.Utils;
+using Command = System.CommandLine.Command;
namespace Microsoft.DotNet.Cli.Commands.Package;
@@ -16,13 +17,24 @@ internal class PackageCommandParser
{
private const string DocsLink = "https://aka.ms/dotnet-package";
- public static readonly Option ProjectOption = new Option("--project")
+ public static readonly Option ProjectOption = new("--project")
{
Recursive = true,
- DefaultValueFactory = _ => Environment.CurrentDirectory,
Description = CliStrings.ProjectArgumentDescription
};
+ public static readonly Option FileOption = new("--file")
+ {
+ Recursive = true,
+ Description = CliStrings.FileArgumentDescription
+ };
+
+ // Used by the legacy 'add/remove package' commands.
+ public static readonly Argument ProjectOrFileArgument = new Argument(CliStrings.ProjectOrFileArgumentName)
+ {
+ Description = CliStrings.ProjectOrFileArgumentDescription
+ }.DefaultToCurrentDirectory();
+
public static Command GetCommand()
{
Command command = new DocumentedCommand("package", DocsLink);
@@ -34,4 +46,20 @@ public static Command GetCommand()
return command;
}
-}
+
+ public static (string Path, AppKinds AllowedAppKinds) ProcessPathOptions(ParseResult parseResult)
+ {
+ bool hasFileOption = parseResult.HasOption(FileOption);
+ bool hasProjectOption = parseResult.HasOption(ProjectOption);
+
+ return (hasFileOption, hasProjectOption) switch
+ {
+ (false, false) => parseResult.GetValue(ProjectOrFileArgument) is { } projectOrFile
+ ? (projectOrFile, AppKinds.Any)
+ : (Environment.CurrentDirectory, AppKinds.ProjectBased),
+ (true, false) => (parseResult.GetValue(FileOption)!, AppKinds.FileBased),
+ (false, true) => (parseResult.GetValue(ProjectOption)!, AppKinds.ProjectBased),
+ (true, true) => throw new GracefulException(CliCommandStrings.CannotCombineOptions, FileOption.Name, ProjectOption.Name),
+ };
+ }
+}
diff --git a/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs b/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs
index 61ddc6c522a0..2706bed56b21 100644
--- a/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs
+++ b/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs
@@ -1,51 +1,45 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
using System.CommandLine;
-using Microsoft.DotNet.Cli.Commands.Hidden.Remove;
+using System.Diagnostics;
using Microsoft.DotNet.Cli.Commands.NuGet;
+using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;
namespace Microsoft.DotNet.Cli.Commands.Package.Remove;
-internal class PackageRemoveCommand : CommandBase
+internal class PackageRemoveCommand(ParseResult parseResult) : CommandBase(parseResult)
{
- private readonly string _fileOrDirectory;
- private readonly IReadOnlyCollection _arguments;
-
- public PackageRemoveCommand(
- ParseResult parseResult) : base(parseResult)
+ public override int Execute()
{
- _fileOrDirectory = parseResult.HasOption(PackageCommandParser.ProjectOption) ?
- parseResult.GetValue(PackageCommandParser.ProjectOption) :
- parseResult.GetValue(RemoveCommandParser.ProjectArgument);
- _arguments = parseResult.GetValue(PackageRemoveCommandParser.CmdPackageArgument).ToList().AsReadOnly();
- if (_fileOrDirectory == null)
+ var arguments = _parseResult.GetValue(PackageRemoveCommandParser.CmdPackageArgument) ?? [];
+
+ if (arguments is not [{ } packageToRemove])
{
- throw new ArgumentNullException(nameof(_fileOrDirectory));
+ throw new GracefulException(CliCommandStrings.PackageRemoveSpecifyExactlyOnePackageReference);
}
- if (_arguments.Count != 1)
+
+ var (fileOrDirectory, allowedAppKinds) = PackageCommandParser.ProcessPathOptions(_parseResult);
+
+ if (allowedAppKinds.HasFlag(AppKinds.FileBased) && VirtualProjectBuildingCommand.IsValidEntryPointPath(fileOrDirectory))
{
- throw new GracefulException(CliCommandStrings.PackageRemoveSpecifyExactlyOnePackageReference);
+ return ExecuteForFileBasedApp(path: fileOrDirectory, packageId: packageToRemove);
}
- }
- public override int Execute()
- {
+ Debug.Assert(allowedAppKinds.HasFlag(AppKinds.ProjectBased));
+
string projectFilePath;
- if (!File.Exists(_fileOrDirectory))
+ if (!File.Exists(fileOrDirectory))
{
- projectFilePath = MsbuildProject.GetProjectFileFromDirectory(_fileOrDirectory).FullName;
+ projectFilePath = MsbuildProject.GetProjectFileFromDirectory(fileOrDirectory).FullName;
}
else
{
- projectFilePath = _fileOrDirectory;
+ projectFilePath = fileOrDirectory;
}
- var packageToRemove = _arguments.Single();
var result = NuGetCommand.Run(TransformArgs(packageToRemove, projectFilePath));
return result;
@@ -69,4 +63,29 @@ private string[] TransformArgs(string packageId, string projectFilePath)
return [.. args];
}
+
+ private static int ExecuteForFileBasedApp(string path, string packageId)
+ {
+ var fullPath = Path.GetFullPath(path);
+
+ // Remove #:package directive from the C# file.
+ // We go through the directives in reverse order so removing one doesn't affect spans of the remaining ones.
+ var editor = FileBasedAppSourceEditor.Load(SourceFile.Load(fullPath));
+ var count = 0;
+ var directives = editor.Directives;
+ for (int i = directives.Length - 1; i >= 0; i--)
+ {
+ var directive = directives[i];
+ if (directive is CSharpDirective.Package p &&
+ string.Equals(p.Name, packageId, StringComparison.OrdinalIgnoreCase))
+ {
+ editor.Remove(directive);
+ count++;
+ }
+ }
+ editor.SourceFile.Save();
+
+ Reporter.Output.WriteLine(CliCommandStrings.DirectivesRemoved, "#:package", count, packageId, fullPath);
+ return count > 0 ? 0 : 1; // success if any directives were found and removed
+ }
}
diff --git a/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommandParser.cs b/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommandParser.cs
index ad7ce92da24e..e1b020f7695f 100644
--- a/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommandParser.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#nullable disable
-
using System.CommandLine;
using Microsoft.DotNet.Cli.Extensions;
@@ -10,7 +8,7 @@ namespace Microsoft.DotNet.Cli.Commands.Package.Remove;
internal static class PackageRemoveCommandParser
{
- public static readonly Argument> CmdPackageArgument = new(CliCommandStrings.CmdPackage)
+ public static readonly Argument CmdPackageArgument = new(CliCommandStrings.CmdPackage)
{
Description = CliCommandStrings.PackageRemoveAppHelpText,
Arity = ArgumentArity.OneOrMore,
@@ -32,6 +30,7 @@ private static Command ConstructCommand()
command.Arguments.Add(CmdPackageArgument);
command.Options.Add(InteractiveOption);
command.Options.Add(PackageCommandParser.ProjectOption);
+ command.Options.Add(PackageCommandParser.FileOption);
command.SetAction((parseResult) => new PackageRemoveCommand(parseResult).Execute());
diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
index 7f8f6a0ee747..c166f61769fe 100644
--- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
+++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
@@ -28,8 +28,8 @@ public override int Execute()
string targetDirectory = DetermineOutputDirectory(file);
// Find directives (this can fail, so do this before creating the target directory).
- var sourceFile = VirtualProjectBuildingCommand.LoadSourceFile(file);
- var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !_force, errors: null);
+ var sourceFile = SourceFile.Load(file);
+ var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !_force, DiagnosticBag.ThrowOnFirst());
// Find other items to copy over, e.g., default Content items like JSON files in Web apps.
var includeItems = FindIncludedItems().ToList();
diff --git a/src/Cli/dotnet/Commands/Reference/Add/ReferenceAddCommand.cs b/src/Cli/dotnet/Commands/Reference/Add/ReferenceAddCommand.cs
index 38edd8cc9bb6..b60050fb12cc 100644
--- a/src/Cli/dotnet/Commands/Reference/Add/ReferenceAddCommand.cs
+++ b/src/Cli/dotnet/Commands/Reference/Add/ReferenceAddCommand.cs
@@ -5,7 +5,7 @@
using System.CommandLine;
using Microsoft.Build.Evaluation;
-using Microsoft.DotNet.Cli.Commands.Hidden.Add;
+using Microsoft.DotNet.Cli.Commands.Package;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;
using NuGet.Frameworks;
@@ -16,7 +16,7 @@ internal class ReferenceAddCommand(ParseResult parseResult) : CommandBase(parseR
{
private readonly string _fileOrDirectory = parseResult.HasOption(ReferenceCommandParser.ProjectOption) ?
parseResult.GetValue(ReferenceCommandParser.ProjectOption) :
- parseResult.GetValue(AddCommandParser.ProjectArgument);
+ parseResult.GetValue(PackageCommandParser.ProjectOrFileArgument);
public override int Execute()
{
diff --git a/src/Cli/dotnet/Commands/Reference/Remove/ReferenceRemoveCommand.cs b/src/Cli/dotnet/Commands/Reference/Remove/ReferenceRemoveCommand.cs
index 875313bea788..64db0bec340d 100644
--- a/src/Cli/dotnet/Commands/Reference/Remove/ReferenceRemoveCommand.cs
+++ b/src/Cli/dotnet/Commands/Reference/Remove/ReferenceRemoveCommand.cs
@@ -5,7 +5,7 @@
using System.CommandLine;
using Microsoft.Build.Evaluation;
-using Microsoft.DotNet.Cli.Commands.Hidden.Remove;
+using Microsoft.DotNet.Cli.Commands.Package;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;
@@ -21,7 +21,7 @@ public ReferenceRemoveCommand(
{
_fileOrDirectory = parseResult.HasOption(ReferenceCommandParser.ProjectOption) ?
parseResult.GetValue(ReferenceCommandParser.ProjectOption) :
- parseResult.GetValue(RemoveCommandParser.ProjectArgument);
+ parseResult.GetValue(PackageCommandParser.ProjectOrFileArgument);
_arguments = parseResult.GetValue(ReferenceRemoveCommandParser.ProjectPathArgument).ToList().AsReadOnly();
if (_arguments.Count == 0)
diff --git a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs
index 01ac32cb7ad5..067ff538a740 100644
--- a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs
@@ -63,9 +63,8 @@ public sealed class GetProject : RunApiInput
public override RunApiOutput Execute()
{
- var sourceFile = VirtualProjectBuildingCommand.LoadSourceFile(EntryPointFileFullPath);
- var errors = ImmutableArray.CreateBuilder();
- var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: true, errors);
+ var sourceFile = SourceFile.Load(EntryPointFileFullPath);
+ var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: true, DiagnosticBag.Collect(out var diagnostics));
string artifactsPath = ArtifactsPath ?? VirtualProjectBuildingCommand.GetArtifactsPath(EntryPointFileFullPath);
var csprojWriter = new StringWriter();
@@ -74,7 +73,7 @@ public override RunApiOutput Execute()
return new RunApiOutput.Project
{
Content = csprojWriter.ToString(),
- Diagnostics = errors.ToImmutableArray(),
+ Diagnostics = diagnostics.ToImmutableArray(),
};
}
}
diff --git a/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs
new file mode 100644
index 000000000000..402da626784e
--- /dev/null
+++ b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs
@@ -0,0 +1,241 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Immutable;
+using System.Diagnostics;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.DotNet.Cli.Commands.Run;
+
+///
+/// A helper to perform edits of file-based app C# source files (e.g., updating the directives).
+///
+internal sealed class FileBasedAppSourceEditor
+{
+ public SourceFile SourceFile
+ {
+ get;
+ private set
+ {
+ field = value;
+
+ // Make sure directives are reloaded next time they are accessed.
+ Directives = default;
+ }
+ }
+
+ public ImmutableArray Directives
+ {
+ get
+ {
+ if (field.IsDefault)
+ {
+ field = VirtualProjectBuildingCommand.FindDirectives(SourceFile, reportAllErrors: false, DiagnosticBag.Ignore());
+ Debug.Assert(!field.IsDefault);
+ }
+
+ return field;
+ }
+ private set
+ {
+ field = value;
+ }
+ }
+
+ public required string NewLine { get; init; }
+
+ private FileBasedAppSourceEditor() { }
+
+ public static FileBasedAppSourceEditor Load(SourceFile sourceFile)
+ {
+ return new FileBasedAppSourceEditor
+ {
+ SourceFile = sourceFile,
+ NewLine = GetNewLine(sourceFile.Text),
+ };
+
+ static string GetNewLine(SourceText text)
+ {
+ // Try to detect existing line endings.
+ string firstLine = text.Lines is [{ } line, ..]
+ ? text.ToString(line.SpanIncludingLineBreak)
+ : string.Empty;
+ return firstLine switch
+ {
+ [.., '\r', '\n'] => "\r\n",
+ [.., '\n'] => "\n",
+ [.., '\r'] => "\r",
+ [.., '\u0085'] => "\u0085",
+ [.., '\u2028'] => "\u2028",
+ [.., '\u2029'] => "\u2029",
+ _ => Environment.NewLine,
+ };
+ }
+ }
+
+ public void Add(CSharpDirective directive)
+ {
+ var change = DetermineAddChange(directive);
+ SourceFile = SourceFile.WithText(SourceFile.Text.WithChanges([change]));
+ }
+
+ private TextChange DetermineAddChange(CSharpDirective directive)
+ {
+ // Find one that has the same kind and name.
+ // If found, we will replace it with the new directive.
+ if (directive is CSharpDirective.Named named &&
+ Directives.OfType().FirstOrDefault(d => NamedDirectiveComparer.Instance.Equals(d, named)) is { } toReplace)
+ {
+ return new TextChange(toReplace.Info.Span, newText: directive.ToString() + NewLine);
+ }
+
+ // Find the last directive of the first group of directives of the same kind.
+ // If found, we will insert the new directive after it.
+ CSharpDirective? addAfter = null;
+ foreach (var existingDirective in Directives)
+ {
+ if (existingDirective.GetType() == directive.GetType())
+ {
+ addAfter = existingDirective;
+ }
+ else if (addAfter != null)
+ {
+ break;
+ }
+ }
+
+ if (addAfter != null)
+ {
+ var span = new TextSpan(start: addAfter.Info.Span.End, length: 0);
+ return new TextChange(span, newText: directive.ToString() + NewLine);
+ }
+
+ // Otherwise, we will add the directive to the top of the file.
+ int start = 0;
+
+ var tokenizer = VirtualProjectBuildingCommand.CreateTokenizer(SourceFile.Text);
+ var result = tokenizer.ParseNextToken();
+ var leadingTrivia = result.Token.LeadingTrivia;
+
+ // If there is a comment at the top of the file, we add the directive after it
+ // (the comment might be a license which should always stay at the top).
+ int insertAfterIndex = -1;
+ int trailingNewLines = 0;
+ for (int i = 0; i < leadingTrivia.Count; i++)
+ {
+ var trivia = leadingTrivia[i];
+
+ switch (trivia.Kind())
+ {
+ case SyntaxKind.SingleLineCommentTrivia:
+ case SyntaxKind.MultiLineCommentTrivia:
+ case SyntaxKind.MultiLineDocumentationCommentTrivia:
+ // Do not consider block comments that do not end with a line break (unless at the end of the file).
+ if (result.Token.IsKind(SyntaxKind.EndOfFileToken))
+ {
+ insertAfterIndex = i;
+ }
+ else if (i < leadingTrivia.Count - 1 &&
+ leadingTrivia[i + 1].IsKind(SyntaxKind.EndOfLineTrivia))
+ {
+ i++;
+ trailingNewLines = 1;
+ insertAfterIndex = i;
+ }
+ else
+ {
+ Debug.Assert(!trivia.IsKind(SyntaxKind.SingleLineCommentTrivia),
+ "Only block comments might not end with a line break.");
+ }
+ break;
+
+ case SyntaxKind.SingleLineDocumentationCommentTrivia:
+ if (trivia.GetStructure() is DocumentationCommentTriviaSyntax s &&
+ s.ChildNodes().LastOrDefault() is XmlTextSyntax { TextTokens: [.., { RawKind: (int)SyntaxKind.XmlTextLiteralNewLineToken }] })
+ {
+ trailingNewLines = 1;
+ insertAfterIndex = i;
+ }
+ break;
+
+ case SyntaxKind.EndOfLineTrivia:
+ if (insertAfterIndex >= 0)
+ {
+ trailingNewLines++;
+ insertAfterIndex = i;
+ }
+ break;
+
+ case SyntaxKind.WhitespaceTrivia:
+ break;
+
+ default:
+ i = leadingTrivia.Count; // Break the loop.
+ break;
+ }
+ }
+
+ string prefix = string.Empty;
+ string suffix = NewLine;
+
+ if (insertAfterIndex >= 0)
+ {
+ var insertAfter = leadingTrivia[insertAfterIndex];
+ start = insertAfter.FullSpan.End;
+
+ // Add newline after the comment if there is not one already (can happen at the end of file).
+ if (trailingNewLines < 1)
+ {
+ prefix += NewLine;
+ }
+
+ // Add a blank separating line between the comment and the directive (unless there is already one).
+ if (trailingNewLines < 2)
+ {
+ prefix += NewLine;
+ }
+ }
+
+ // Add a blank line after the directive unless there are no other tokens (i.e., the first token is EOF),
+ // or there is already a blank line or another directive before the first C# token.
+ var remainingLeadingTrivia = leadingTrivia.Skip(insertAfterIndex + 1);
+ if (!(result.Token.IsKind(SyntaxKind.EndOfFileToken) && !remainingLeadingTrivia.Any() && !result.Token.HasTrailingTrivia) &&
+ !remainingLeadingTrivia.Any(static t => t.Kind() is SyntaxKind.EndOfLineTrivia or SyntaxKind.IgnoredDirectiveTrivia))
+ {
+ suffix += NewLine;
+ }
+
+ return new TextChange(new TextSpan(start: start, length: 0), newText: prefix + directive.ToString() + suffix);
+ }
+
+ public void Remove(CSharpDirective directive)
+ {
+ var span = directive.Info.Span;
+ var start = span.Start;
+ var length = span.Length + DetermineTrailingLengthToRemove(directive);
+ SourceFile = SourceFile.WithText(SourceFile.Text.Replace(start: start, length: length, newText: string.Empty));
+ }
+
+ private static int DetermineTrailingLengthToRemove(CSharpDirective directive)
+ {
+ // If there are blank lines both before and after the directive, remove the trailing white space.
+ if (directive.Info.LeadingWhiteSpace.LineBreaks > 0 && directive.Info.TrailingWhiteSpace.LineBreaks > 0)
+ {
+ return directive.Info.TrailingWhiteSpace.TotalLength;
+ }
+
+ // If the directive (including leading white space) starts at the beginning of the file,
+ // remove both the leading and trailing white space.
+ var startBeforeWhiteSpace = directive.Info.Span.Start - directive.Info.LeadingWhiteSpace.TotalLength;
+ if (startBeforeWhiteSpace == 0)
+ {
+ return directive.Info.LeadingWhiteSpace.TotalLength + directive.Info.TrailingWhiteSpace.TotalLength;
+ }
+
+ Debug.Assert(startBeforeWhiteSpace > 0);
+ return 0;
+ }
+}
diff --git a/src/Cli/dotnet/Commands/Run/RunCommand.cs b/src/Cli/dotnet/Commands/Run/RunCommand.cs
index 68e4c399774b..9009efe7dfd7 100644
--- a/src/Cli/dotnet/Commands/Run/RunCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs
@@ -131,7 +131,7 @@ public int Execute()
{
if (NoCache)
{
- throw new GracefulException(CliCommandStrings.InvalidOptionCombination, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoBuildOption.Name);
+ throw new GracefulException(CliCommandStrings.CannotCombineOptions, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoBuildOption.Name);
}
if (EntryPointFileFullPath is not null)
diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
index 5386123d7cbb..113337995562 100644
--- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
@@ -162,15 +162,15 @@ public ImmutableArray Directives
{
if (field.IsDefault)
{
- var sourceFile = LoadSourceFile(EntryPointFileFullPath);
- field = FindDirectives(sourceFile, reportAllErrors: false, errors: null);
+ var sourceFile = SourceFile.Load(EntryPointFileFullPath);
+ field = FindDirectives(sourceFile, reportAllErrors: false, DiagnosticBag.ThrowOnFirst());
Debug.Assert(!field.IsDefault);
}
return field;
}
- init;
+ set;
}
public override int Execute()
@@ -901,6 +901,14 @@ static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk s
}
}
+#pragma warning disable RSEXPERIMENTAL003 // 'SyntaxTokenParser' is experimental
+ public static SyntaxTokenParser CreateTokenizer(SourceText text)
+ {
+ return SyntaxFactory.CreateTokenParser(text,
+ CSharpParseOptions.Default.WithFeatures([new("FileBasedProgram", "true")]));
+ }
+#pragma warning restore RSEXPERIMENTAL003 // 'SyntaxTokenParser' is experimental
+
///
/// If , the whole is parsed to find diagnostics about every app directive.
/// Otherwise, only directives up to the first C# token is checked.
@@ -908,23 +916,16 @@ static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk s
/// The latter is useful for dotnet run file.cs where if there are app directives after the first token,
/// compiler reports anyway, so we speed up success scenarios by not parsing the whole file up front in the SDK CLI.
///
- ///
- /// If , the first error is thrown as .
- /// Otherwise, all errors are put into the list.
- /// Does not have any effect when is .
- ///
- public static ImmutableArray FindDirectives(SourceFile sourceFile, bool reportAllErrors, ImmutableArray.Builder? errors)
+ public static ImmutableArray FindDirectives(SourceFile sourceFile, bool reportAllErrors, DiagnosticBag diagnostics)
{
-#pragma warning disable RSEXPERIMENTAL003 // 'SyntaxTokenParser' is experimental
-
var deduplicated = new HashSet(NamedDirectiveComparer.Instance);
var builder = ImmutableArray.CreateBuilder();
- SyntaxTokenParser tokenizer = SyntaxFactory.CreateTokenParser(sourceFile.Text,
- CSharpParseOptions.Default.WithFeatures([new("FileBasedProgram", "true")]));
+ var tokenizer = CreateTokenizer(sourceFile.Text);
var result = tokenizer.ParseLeadingTrivia();
TextSpan previousWhiteSpaceSpan = default;
- foreach (var trivia in result.Token.LeadingTrivia)
+ var triviaList = result.Token.LeadingTrivia;
+ foreach (var (index, trivia) in triviaList.Index())
{
// Stop when the trivia contains an error (e.g., because it's after #if).
if (trivia.ContainsDiagnostics)
@@ -941,13 +942,20 @@ public static ImmutableArray FindDirectives(SourceFile sourceFi
if (trivia.IsKind(SyntaxKind.ShebangDirectiveTrivia))
{
- TextSpan span = getFullSpan(previousWhiteSpaceSpan, trivia);
+ TextSpan span = GetFullSpan(previousWhiteSpaceSpan, trivia);
- builder.Add(new CSharpDirective.Shebang { Span = span });
+ var whiteSpace = GetWhiteSpaceInfo(triviaList, index);
+ var info = new CSharpDirective.ParseInfo
+ {
+ Span = span,
+ LeadingWhiteSpace = whiteSpace.Leading,
+ TrailingWhiteSpace = whiteSpace.Trailing,
+ };
+ builder.Add(new CSharpDirective.Shebang(info));
}
else if (trivia.IsKind(SyntaxKind.IgnoredDirectiveTrivia))
{
- TextSpan span = getFullSpan(previousWhiteSpaceSpan, trivia);
+ TextSpan span = GetFullSpan(previousWhiteSpaceSpan, trivia);
var message = trivia.GetStructure() is IgnoredDirectiveTriviaSyntax { Content: { RawKind: (int)SyntaxKind.StringLiteralToken } content }
? content.Text.AsSpan().Trim()
@@ -957,24 +965,27 @@ public static ImmutableArray FindDirectives(SourceFile sourceFi
var value = parts.MoveNext() ? message[parts.Current] : default;
Debug.Assert(!parts.MoveNext());
- if (CSharpDirective.Parse(errors, sourceFile, span, name.ToString(), value.ToString()) is { } directive)
+ var whiteSpace = GetWhiteSpaceInfo(triviaList, index);
+ var context = new CSharpDirective.ParseContext
+ {
+ Info = new()
+ {
+ Span = span,
+ LeadingWhiteSpace = whiteSpace.Leading,
+ TrailingWhiteSpace = whiteSpace.Trailing,
+ },
+ Diagnostics = diagnostics,
+ SourceFile = sourceFile,
+ DirectiveKind = name.ToString(),
+ DirectiveText = value.ToString()
+ };
+ if (CSharpDirective.Parse(context) is { } directive)
{
// If the directive is already present, report an error.
if (deduplicated.TryGetValue(directive, out var existingDirective))
{
var typeAndName = $"#:{existingDirective.GetType().Name.ToLowerInvariant()} {existingDirective.Name}";
- if (errors != null)
- {
- errors.Add(new SimpleDiagnostic
- {
- Location = sourceFile.GetFileLinePositionSpan(directive.Span),
- Message = string.Format(CliCommandStrings.DuplicateDirective, typeAndName, sourceFile.GetLocationString(directive.Span)),
- });
- }
- else
- {
- throw new GracefulException(CliCommandStrings.DuplicateDirective, typeAndName, sourceFile.GetLocationString(directive.Span));
- }
+ diagnostics.AddError(sourceFile, directive.Info.Span, location => string.Format(CliCommandStrings.DuplicateDirective, typeAndName, location));
}
else
{
@@ -1000,12 +1011,12 @@ public static ImmutableArray FindDirectives(SourceFile sourceFi
foreach (var trivia in result.Token.LeadingTrivia)
{
- reportErrorFor(trivia);
+ ReportErrorFor(trivia);
}
foreach (var trivia in result.Token.TrailingTrivia)
{
- reportErrorFor(trivia);
+ ReportErrorFor(trivia);
}
}
while (!result.Token.IsKind(SyntaxKind.EndOfFileToken));
@@ -1014,38 +1025,55 @@ public static ImmutableArray FindDirectives(SourceFile sourceFi
// The result should be ordered by source location, RemoveDirectivesFromFile depends on that.
return builder.ToImmutable();
- static TextSpan getFullSpan(TextSpan previousWhiteSpaceSpan, SyntaxTrivia trivia)
+ static TextSpan GetFullSpan(TextSpan previousWhiteSpaceSpan, SyntaxTrivia trivia)
{
// Include the preceding whitespace in the span, i.e., span will be the whole line.
return previousWhiteSpaceSpan.IsEmpty ? trivia.FullSpan : TextSpan.FromBounds(previousWhiteSpaceSpan.Start, trivia.FullSpan.End);
}
- void reportErrorFor(SyntaxTrivia trivia)
+ void ReportErrorFor(SyntaxTrivia trivia)
{
if (trivia.ContainsDiagnostics && trivia.IsKind(SyntaxKind.IgnoredDirectiveTrivia))
{
- string location = sourceFile.GetLocationString(trivia.Span);
- if (errors != null)
+ diagnostics.AddError(sourceFile, trivia.Span, location => string.Format(CliCommandStrings.CannotConvertDirective, location));
+ }
+ }
+
+ static (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) GetWhiteSpaceInfo(in SyntaxTriviaList triviaList, int index)
+ {
+ (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) result = default;
+
+ for (int i = index - 1; i >= 0; i--)
+ {
+ if (!Fill(ref result.Leading, triviaList, i)) break;
+ }
+
+ for (int i = index + 1; i < triviaList.Count; i++)
+ {
+ if (!Fill(ref result.Trailing, triviaList, i)) break;
+ }
+
+ return result;
+
+ static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int index)
+ {
+ var trivia = triviaList[index];
+ if (trivia.IsKind(SyntaxKind.EndOfLineTrivia))
{
- errors.Add(new SimpleDiagnostic
- {
- Location = sourceFile.GetFileLinePositionSpan(trivia.Span),
- Message = string.Format(CliCommandStrings.CannotConvertDirective, location),
- });
+ info.LineBreaks += 1;
+ info.TotalLength += trivia.FullSpan.Length;
+ return true;
}
- else
+
+ if (trivia.IsKind(SyntaxKind.WhitespaceTrivia))
{
- throw new GracefulException(CliCommandStrings.CannotConvertDirective, location);
+ info.TotalLength += trivia.FullSpan.Length;
+ return true;
}
+
+ return false;
}
}
-#pragma warning restore RSEXPERIMENTAL003 // 'SyntaxTokenParser' is experimental
- }
-
- public static SourceFile LoadSourceFile(string filePath)
- {
- using var stream = File.OpenRead(filePath);
- return new SourceFile(filePath, SourceText.From(stream, Encoding.UTF8));
}
public static SourceText? RemoveDirectivesFromFile(ImmutableArray directives, SourceText text)
@@ -1055,12 +1083,12 @@ public static SourceFile LoadSourceFile(string filePath)
return null;
}
- Debug.Assert(directives.OrderBy(d => d.Span.Start).SequenceEqual(directives), "Directives should be ordered by source location.");
+ Debug.Assert(directives.OrderBy(d => d.Info.Span.Start).SequenceEqual(directives), "Directives should be ordered by source location.");
for (int i = directives.Length - 1; i >= 0; i--)
{
var directive = directives[i];
- text = text.Replace(directive.Span, string.Empty);
+ text = text.Replace(directive.Info.Span, string.Empty);
}
return text;
@@ -1070,9 +1098,7 @@ public static void RemoveDirectivesFromFile(ImmutableArray dire
{
if (RemoveDirectivesFromFile(directives, text) is { } modifiedText)
{
- using var stream = File.Open(filePath, FileMode.Create, FileAccess.Write);
- using var writer = new StreamWriter(stream, Encoding.UTF8);
- modifiedText.Write(writer);
+ new SourceFile(filePath, modifiedText).Save();
}
}
@@ -1105,6 +1131,24 @@ public static bool IsValidEntryPointPath(string entryPointFilePath)
internal readonly record struct SourceFile(string Path, SourceText Text)
{
+ public static SourceFile Load(string filePath)
+ {
+ using var stream = File.OpenRead(filePath);
+ return new SourceFile(filePath, SourceText.From(stream, Encoding.UTF8));
+ }
+
+ public SourceFile WithText(SourceText newText)
+ {
+ return new SourceFile(Path, newText);
+ }
+
+ public void Save()
+ {
+ using var stream = File.Open(Path, FileMode.Create, FileAccess.Write);
+ using var writer = new StreamWriter(stream, Encoding.UTF8);
+ Text.Write(writer);
+ }
+
public FileLinePositionSpan GetFileLinePositionSpan(TextSpan span)
{
return new FileLinePositionSpan(Path, Text.Lines.GetLinePositionSpan(span));
@@ -1126,66 +1170,69 @@ internal static partial class Patterns
public static partial Regex DisallowedNameCharacters { get; }
}
+internal struct WhiteSpaceInfo
+{
+ public int LineBreaks;
+ public int TotalLength;
+}
+
///
/// Represents a C# directive starting with #: (a.k.a., "file-level directive").
/// Those are ignored by the language but recognized by us.
///
-internal abstract class CSharpDirective
+internal abstract class CSharpDirective(in CSharpDirective.ParseInfo info)
{
- private CSharpDirective() { }
-
- ///
- /// Span of the full line including the trailing line break.
- ///
- public required TextSpan Span { get; init; }
+ public ParseInfo Info { get; } = info;
- public static Named? Parse(ImmutableArray.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText)
+ public readonly struct ParseInfo
{
- return directiveKind switch
- {
- "sdk" => Sdk.Parse(errors, sourceFile, span, directiveKind, directiveText),
- "property" => Property.Parse(errors, sourceFile, span, directiveKind, directiveText),
- "package" => Package.Parse(errors, sourceFile, span, directiveKind, directiveText),
- "project" => Project.Parse(errors, sourceFile, span, directiveKind, directiveText),
- _ => ReportError(errors, sourceFile, span, string.Format(CliCommandStrings.UnrecognizedDirective, directiveKind, sourceFile.GetLocationString(span))),
- };
+ ///
+ /// Span of the full line including the trailing line break.
+ ///
+ public required TextSpan Span { get; init; }
+ public required WhiteSpaceInfo LeadingWhiteSpace { get; init; }
+ public required WhiteSpaceInfo TrailingWhiteSpace { get; init; }
}
- private static T? ReportError(ImmutableArray.Builder? errors, SourceFile sourceFile, TextSpan span, string message, Exception? inner = null)
+ public readonly struct ParseContext
{
- ReportError(errors, sourceFile, span, message, inner);
- return default;
+ public required ParseInfo Info { get; init; }
+ public required DiagnosticBag Diagnostics { get; init; }
+ public required SourceFile SourceFile { get; init; }
+ public required string DirectiveKind { get; init; }
+ public required string DirectiveText { get; init; }
}
- private static void ReportError(ImmutableArray.Builder? errors, SourceFile sourceFile, TextSpan span, string message, Exception? inner = null)
+ public static Named? Parse(in ParseContext context)
{
- if (errors != null)
- {
- errors.Add(new SimpleDiagnostic { Location = sourceFile.GetFileLinePositionSpan(span), Message = message });
- }
- else
+ return context.DirectiveKind switch
{
- throw new GracefulException(message, inner);
- }
+ "sdk" => Sdk.Parse(context),
+ "property" => Property.Parse(context),
+ "package" => Package.Parse(context),
+ "project" => Project.Parse(context),
+ var other => context.Diagnostics.AddError(context.SourceFile, context.Info.Span, location => string.Format(CliCommandStrings.UnrecognizedDirective, other, location)),
+ };
}
- private static (string, string?)? ParseOptionalTwoParts(ImmutableArray.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText, char separator)
+ private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, char separator)
{
- var i = directiveText.IndexOf(separator, StringComparison.Ordinal);
- var firstPart = (i < 0 ? directiveText : directiveText.AsSpan(..i)).TrimEnd();
+ var i = context.DirectiveText.IndexOf(separator, StringComparison.Ordinal);
+ var firstPart = (i < 0 ? context.DirectiveText : context.DirectiveText.AsSpan(..i)).TrimEnd();
+ string directiveKind = context.DirectiveKind;
if (firstPart.IsWhiteSpace())
{
- return ReportError<(string, string?)?>(errors, sourceFile, span, string.Format(CliCommandStrings.MissingDirectiveName, directiveKind, sourceFile.GetLocationString(span)));
+ return context.Diagnostics.AddError<(string, string?)?>(context.SourceFile, context.Info.Span, location => string.Format(CliCommandStrings.MissingDirectiveName, directiveKind, location));
}
// If the name contains characters that resemble separators, report an error to avoid any confusion.
if (Patterns.DisallowedNameCharacters.IsMatch(firstPart))
{
- return ReportError<(string, string?)?>(errors, sourceFile, span, string.Format(CliCommandStrings.InvalidDirectiveName, directiveKind, separator, sourceFile.GetLocationString(span)));
+ return context.Diagnostics.AddError<(string, string?)?>(context.SourceFile, context.Info.Span, location => string.Format(CliCommandStrings.InvalidDirectiveName, directiveKind, separator, location));
}
- var secondPart = i < 0 ? [] : directiveText.AsSpan((i + 1)..).TrimStart();
+ var secondPart = i < 0 ? [] : context.DirectiveText.AsSpan((i + 1)..).TrimStart();
if (i < 0 || secondPart.IsWhiteSpace())
{
return (firstPart.ToString(), null);
@@ -1194,12 +1241,17 @@ private static (string, string?)? ParseOptionalTwoParts(ImmutableArray
/// #! directive.
///
- public sealed class Shebang : CSharpDirective;
+ public sealed class Shebang(in ParseInfo info) : CSharpDirective(info)
+ {
+ public override string ToString() => "#!";
+ }
- public abstract class Named : CSharpDirective
+ public abstract class Named(in ParseInfo info) : CSharpDirective(info)
{
public required string Name { get; init; }
}
@@ -1207,47 +1259,44 @@ public abstract class Named : CSharpDirective
///
/// #:sdk directive.
///
- public sealed class Sdk : Named
+ public sealed class Sdk(in ParseInfo info) : Named(info)
{
- private Sdk() { }
-
public string? Version { get; init; }
- public static new Sdk? Parse(ImmutableArray.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText)
+ public static new Sdk? Parse(in ParseContext context)
{
- if (ParseOptionalTwoParts(errors, sourceFile, span, directiveKind, directiveText, separator: '@') is not var (sdkName, sdkVersion))
+ if (ParseOptionalTwoParts(context, separator: '@') is not var (sdkName, sdkVersion))
{
return null;
}
- return new Sdk
+ return new Sdk(context.Info)
{
- Span = span,
Name = sdkName,
Version = sdkVersion,
};
}
+
+ public override string ToString() => Version is null ? $"#:sdk {Name}" : $"#:sdk {Name}@{Version}";
}
///
/// #:property directive.
///
- public sealed class Property : Named
+ public sealed class Property(in ParseInfo info) : Named(info)
{
- private Property() { }
-
public required string Value { get; init; }
- public static new Property? Parse(ImmutableArray.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText)
+ public static new Property? Parse(in ParseContext context)
{
- if (ParseOptionalTwoParts(errors, sourceFile, span, directiveKind, directiveText, separator: '=') is not var (propertyName, propertyValue))
+ if (ParseOptionalTwoParts(context, separator: '=') is not var (propertyName, propertyValue))
{
return null;
}
if (propertyValue is null)
{
- return ReportError(errors, sourceFile, span, string.Format(CliCommandStrings.PropertyDirectiveMissingParts, sourceFile.GetLocationString(span)));
+ return context.Diagnostics.AddError(context.SourceFile, context.Info.Span, location => string.Format(CliCommandStrings.PropertyDirectiveMissingParts, location));
}
try
@@ -1256,62 +1305,62 @@ private Property() { }
}
catch (XmlException ex)
{
- return ReportError(errors, sourceFile, span, string.Format(CliCommandStrings.PropertyDirectiveInvalidName, sourceFile.GetLocationString(span), ex.Message), ex);
+ return context.Diagnostics.AddError(context.SourceFile, context.Info.Span, location => string.Format(CliCommandStrings.PropertyDirectiveInvalidName, location, ex.Message), ex);
}
- return new Property
+ return new Property(context.Info)
{
- Span = span,
Name = propertyName,
Value = propertyValue,
};
}
+
+ public override string ToString() => $"#:property {Name}={Value}";
}
///
/// #:package directive.
///
- public sealed class Package : Named
+ public sealed class Package(in ParseInfo info) : Named(info)
{
- private Package() { }
-
public string? Version { get; init; }
- public static new Package? Parse(ImmutableArray.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText)
+ public static new Package? Parse(in ParseContext context)
{
- if (ParseOptionalTwoParts(errors, sourceFile, span, directiveKind, directiveText, separator: '@') is not var (packageName, packageVersion))
+ if (ParseOptionalTwoParts(context, separator: '@') is not var (packageName, packageVersion))
{
return null;
}
- return new Package
+ return new Package(context.Info)
{
- Span = span,
Name = packageName,
Version = packageVersion,
};
}
+
+ public override string ToString() => Version is null ? $"#:package {Name}" : $"#:package {Name}@{Version}";
}
///
/// #:project directive.
///
- public sealed class Project : Named
+ public sealed class Project(in ParseInfo info) : Named(info)
{
- private Project() { }
-
- public static new Project? Parse(ImmutableArray.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText)
+ public static new Project? Parse(in ParseContext context)
{
+ var directiveText = context.DirectiveText;
if (directiveText.IsWhiteSpace())
{
- return ReportError(errors, sourceFile, span, string.Format(CliCommandStrings.MissingDirectiveName, directiveKind, sourceFile.GetLocationString(span)));
+ string directiveKind = context.DirectiveKind;
+ return context.Diagnostics.AddError(context.SourceFile, context.Info.Span, location => string.Format(CliCommandStrings.MissingDirectiveName, directiveKind, location));
}
try
{
// If the path is a directory like '../lib', transform it to a project file path like '../lib/lib.csproj'.
// Also normalize blackslashes to forward slashes to ensure the directive works on all platforms.
- var sourceDirectory = Path.GetDirectoryName(sourceFile.Path) ?? ".";
+ var sourceDirectory = Path.GetDirectoryName(context.SourceFile.Path) ?? ".";
var resolvedProjectPath = Path.Combine(sourceDirectory, directiveText.Replace('\\', '/'));
if (Directory.Exists(resolvedProjectPath))
{
@@ -1325,15 +1374,16 @@ private Project() { }
}
catch (GracefulException e)
{
- ReportError(errors, sourceFile, span, string.Format(CliCommandStrings.InvalidProjectDirective, sourceFile.GetLocationString(span), e.Message), e);
+ context.Diagnostics.AddError(context.SourceFile, context.Info.Span, location => string.Format(CliCommandStrings.InvalidProjectDirective, location, e.Message), e);
}
- return new Project
+ return new Project(context.Info)
{
- Span = span,
Name = directiveText,
};
}
+
+ public override string ToString() => $"#:project {Name}";
}
}
@@ -1385,6 +1435,39 @@ public readonly struct Position
}
}
+internal readonly struct DiagnosticBag
+{
+ public bool IgnoreDiagnostics { get; private init; }
+
+ ///
+ /// If and is , the first diagnostic is thrown as .
+ ///
+ public ImmutableArray.Builder? Builder { get; private init; }
+
+ public static DiagnosticBag ThrowOnFirst() => default;
+ public static DiagnosticBag Collect(out ImmutableArray.Builder builder) => new() { Builder = builder = ImmutableArray.CreateBuilder() };
+ public static DiagnosticBag Ignore() => new() { IgnoreDiagnostics = true, Builder = null };
+
+ public void AddError(SourceFile sourceFile, TextSpan span, Func messageFactory, Exception? inner = null)
+ {
+ if (Builder != null)
+ {
+ Debug.Assert(!IgnoreDiagnostics);
+ Builder.Add(new SimpleDiagnostic { Location = sourceFile.GetFileLinePositionSpan(span), Message = messageFactory(sourceFile.GetLocationString(span)) });
+ }
+ else if (!IgnoreDiagnostics)
+ {
+ throw new GracefulException(messageFactory(sourceFile.GetLocationString(span)), inner);
+ }
+ }
+
+ public T? AddError(SourceFile sourceFile, TextSpan span, Func messageFactory, Exception? inner = null)
+ {
+ AddError(sourceFile, span, messageFactory, inner);
+ return default;
+ }
+}
+
internal sealed class RunFileBuildCacheEntry
{
private static StringComparer GlobalPropertiesComparer => StringComparer.OrdinalIgnoreCase;
@@ -1417,3 +1500,12 @@ public RunFileBuildCacheEntry(Dictionary globalProperties)
[JsonSerializable(typeof(RunFileBuildCacheEntry))]
[JsonSerializable(typeof(RunFileArtifactsMetadata))]
internal partial class RunFileJsonSerializerContext : JsonSerializerContext;
+
+[Flags]
+internal enum AppKinds
+{
+ None = 0,
+ ProjectBased = 1 << 0,
+ FileBased = 1 << 1,
+ Any = ProjectBased | FileBased,
+}
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
index c8c8fe03a182..9a61dc48f955 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
Povolí diagnostický výstup.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
Cílový adresář již existuje: {0}.
@@ -1491,10 +1497,10 @@ Nastavte odlišné názvy profilů.
Zadaný soubor musí existovat a musí mít příponu souboru .cs:{0}
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- Parametr {0} nelze kombinovat s parametrem {1}.
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf
index e3e142e6847b..46b66f9e8365 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
Aktiviert die Diagnoseausgabe.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
Das Zielverzeichnis ist bereits vorhanden: "{0}"
@@ -1491,10 +1497,10 @@ Erstellen Sie eindeutige Profilnamen.
Die angegebene Datei muss vorhanden sein und die Dateierweiterung ".cs" aufweisen: "{0}".
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- Die Option "{0}" und "{1}" kann nicht kombiniert werden.
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf
index d11950159bca..9c9d99cd0702 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
Permite habilitar la salida de diagnóstico.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
El directorio de destino ya existe: "{0}"
@@ -1491,10 +1497,10 @@ Defina nombres de perfiles distintos.
El archivo especificado debe existir y tener la extensión de archivo ".cs": "{0}"
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- No se puede combinar la opción "{0}" y "{1}".
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf
index 9b27c9af419e..5817ed3dff1a 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
Active la sortie des diagnostics.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
Le répertoire cible existe déjà : « {0} »
@@ -1491,10 +1497,10 @@ Faites en sorte que les noms de profil soient distincts.
Le fichier spécifié doit exister et avoir l'extension « .cs » : « {0} »
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- Impossible de combiner l’option « {0} » et « {1} ».
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf
index 757299b0224a..2336f00ae759 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
Abilita l'output di diagnostica.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
La directory di destinazione esiste già: '{0}'
@@ -1491,10 +1497,10 @@ Rendi distinti i nomi profilo.
Il file specificato deve esistere e avere l'estensione '.cs': '{0}'
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- Non è possibile combinare l'opzione '{0}' e '{1}'.
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf
index f76428fb868e..e1091d7eca62 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
診断出力を有効にします。
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
ターゲット ディレクトリは既に存在します: '{0}'
@@ -1491,10 +1497,10 @@ Make the profile names distinct.
指定されたファイルが存在し、ファイル拡張子が '.cs' である必要があります: '{0}'
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- オプション '{0}' と '{1}' を組み合わせることはできません。
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf
index c5111741b144..4b76e902b830 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
진단 출력을 사용하도록 설정합니다.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
대상 디렉터리가 이미 있습니다. '{0}'
@@ -1491,10 +1497,10 @@ Make the profile names distinct.
지정한 파일이 존재해야 하며 '.cs' 파일 확장명이 있어야 합니다. '{0}'.
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- 옵션 '{0}'와(과) '{1}'을(를) 결합할 수 없습니다.
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf
index 917639a04db3..fa8019db3fe2 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
Włącza dane wyjściowe diagnostyki.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
Katalog docelowy już istnieje: „{0}”
@@ -1491,10 +1497,10 @@ Rozróżnij nazwy profilów.
Określony plik musi istnieć i mieć rozszerzenie pliku „.cs”: „{0}”
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- Nie można połączyć opcji „{0}” i „{1}”.
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf
index 8c6d16abe5dc..bd36ffbd84e3 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
Habilita a saída de diagnóstico.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
O diretório de destino já existe: "{0}"
@@ -1491,10 +1497,10 @@ Diferencie os nomes dos perfis.
O arquivo especificado deve existir e ter a extensão de arquivo ".cs": "{0}"
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- Não é possível combinar as opções "{0}" e "{1}".
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf
index 49e1035f0d9c..f2ab4687963f 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
Включает диагностические выходные данные.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
Целевой каталог уже существует: "{0}"
@@ -1491,10 +1497,10 @@ Make the profile names distinct.
Указанный файл должен существовать и иметь расширение ".cs": "{0}"
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- Единовременно можно использовать только один из параметров: "{0}" или "{1}".
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf
index 693a649ce731..cbba29a337a1 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
Tanılama çıkışı sağlar.
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
Hedef dizin zaten mevcut: '{0}'
@@ -1491,10 +1497,10 @@ Lütfen profil adlarını değiştirin.
Belirtilen dosya mevcut olmalıdır ve uzantısı '.cs' olmalıdır: '{0}'
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- Seçenek '{0}' ve '{1}' birleştirilemez.
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf
index 318f8be12f7d..7f63bdb3dc57 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
启用诊断输出。
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
目标目录已存在: '{0}'
@@ -1491,10 +1497,10 @@ Make the profile names distinct.
指定的文件必须存在且具有 ‘.cs’ 文件扩展名: ‘{0}’
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- 无法组合选项 ‘{0}’ 和 ‘{1}’。
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf
index 94330effe376..83d7f6519363 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf
@@ -1092,6 +1092,12 @@ dotnet.config is a name don't translate.
啟用診斷輸出。
+
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ Removed '{0}' directives ({1}) for '{2}' from: {3}
+ {0} is a directive kind (like '#:package'). {1} is number of removed directives.
+ {2} is directive key (e.g., package name). {3} is file path from which directives were removed.
+
The target directory already exists: '{0}'
目標目錄已存在: '{0}'
@@ -1491,10 +1497,10 @@ Make the profile names distinct.
指定的檔案必須存在,並具有 '.cs' 副檔名:'{0}'
{Locked=".cs"}
-
- Cannot combine option '{0}' and '{1}'.
- 無法合併選項 '{0}' 與 '{1}'。
- {0} and {1} are option names like '--no-build'.
+
+ Cannot specify option '{0}' when operating on a file-based app.
+ Cannot specify option '{0}' when operating on a file-based app.
+ {0} is an option name like '--source'.
Cannot specify option '{0}' when also using '-' to read the file from standard input.
diff --git a/src/Cli/dotnet/CommonArguments.cs b/src/Cli/dotnet/CommonArguments.cs
index 8102e620d5c0..8adb11aa1400 100644
--- a/src/Cli/dotnet/CommonArguments.cs
+++ b/src/Cli/dotnet/CommonArguments.cs
@@ -3,6 +3,7 @@
using System.CommandLine;
using System.CommandLine.Parsing;
+using System.Diagnostics.CodeAnalysis;
using Microsoft.DotNet.Cli.Utils;
using NuGet.Versioning;
@@ -59,6 +60,7 @@ public static DynamicArgument RequiredPackageIdentityA
public readonly record struct PackageIdentityWithRange(string Id, VersionRange? VersionRange)
{
+ [MemberNotNullWhen(returnValue: true, nameof(VersionRange))]
public bool HasVersion => VersionRange != null;
}
}
diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj
index 7b6ef5b54f18..d06486ccaef9 100644
--- a/src/Cli/dotnet/dotnet.csproj
+++ b/src/Cli/dotnet/dotnet.csproj
@@ -24,6 +24,7 @@
+
diff --git a/src/Cli/dotnet/xlf/CliStrings.cs.xlf b/src/Cli/dotnet/xlf/CliStrings.cs.xlf
index 556eb133da50..60f543fb27df 100644
--- a/src/Cli/dotnet/xlf/CliStrings.cs.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.cs.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
Soubor
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
Verze formátu je vyšší, než se podporuje. Tento nástroj se možná v této verzi SDK nepodporuje. Aktualizujte sadu SDK.
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
Projekt {0} se v řešení nenašel.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
Odkaz na projekt
diff --git a/src/Cli/dotnet/xlf/CliStrings.de.xlf b/src/Cli/dotnet/xlf/CliStrings.de.xlf
index 6238432278c6..bdd135c9322a 100644
--- a/src/Cli/dotnet/xlf/CliStrings.de.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.de.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
Datei
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
Die Formatversion ist höher als unterstützt. Dieses Tool wird in dieser SDK-Version möglicherweise nicht unterstützt. Aktualisieren Sie Ihr SDK.
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
Das Projekt "{0}" wurde in der Projektmappe nicht gefunden.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
Projektverweis
diff --git a/src/Cli/dotnet/xlf/CliStrings.es.xlf b/src/Cli/dotnet/xlf/CliStrings.es.xlf
index 8362051505df..8b64efcec0a4 100644
--- a/src/Cli/dotnet/xlf/CliStrings.es.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.es.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
archivo
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
La versión de formato es superior a la admitida. Puede que la herramienta no sea compatible con esta versión del SDK. Actualice el SDK.
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
No se encuentra el proyecto "{0}" en la solución.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
Referencia de proyecto
diff --git a/src/Cli/dotnet/xlf/CliStrings.fr.xlf b/src/Cli/dotnet/xlf/CliStrings.fr.xlf
index 626e1ac16396..b2ceced2b465 100644
--- a/src/Cli/dotnet/xlf/CliStrings.fr.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.fr.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
fichier
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
Version de format supérieure à la version prise en charge. Cet outil risque de ne pas être pris en charge dans cette version de SDK. Mettez à jour votre SDK.
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
Projet '{0}' introuvable dans la solution.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
Référence de projet
diff --git a/src/Cli/dotnet/xlf/CliStrings.it.xlf b/src/Cli/dotnet/xlf/CliStrings.it.xlf
index 13421dee5305..8113f271be8c 100644
--- a/src/Cli/dotnet/xlf/CliStrings.it.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.it.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
File
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
La versione di Format è successiva a quella supportata. È possibile che questo strumento non sia supportato in questa versione dell'SDK. Aggiornare l'SDK.
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
Il progetto `{0}` non è stato trovato nella soluzione.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
Riferimento al progetto
diff --git a/src/Cli/dotnet/xlf/CliStrings.ja.xlf b/src/Cli/dotnet/xlf/CliStrings.ja.xlf
index aea1fa2ec15b..d3e8084b397c 100644
--- a/src/Cli/dotnet/xlf/CliStrings.ja.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.ja.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
ファイル
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
形式のバージョンがサポートされているものを超えています。このツールはこのバージョンの SDK ではサポートされていない可能性があります。SDK を更新します。
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
プロジェクト `{0}` がソリューション内に見つかりませんでした。
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
プロジェクト参照
diff --git a/src/Cli/dotnet/xlf/CliStrings.ko.xlf b/src/Cli/dotnet/xlf/CliStrings.ko.xlf
index 971c2c751a1e..e79215ae1ad0 100644
--- a/src/Cli/dotnet/xlf/CliStrings.ko.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.ko.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
파일
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
형식 버전이 지원되는 버전보다 높습니다. 이 SDK 버전에서 이 도구를 지원하지 않을 수 있습니다. SDK를 업데이트하세요.
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
솔루션에서 '{0}' 프로젝트를 찾을 수 없습니다.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
프로젝트 참조
diff --git a/src/Cli/dotnet/xlf/CliStrings.pl.xlf b/src/Cli/dotnet/xlf/CliStrings.pl.xlf
index a235caa8ae20..1fe5fb0e964b 100644
--- a/src/Cli/dotnet/xlf/CliStrings.pl.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.pl.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
Plik
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
Wersja formatu jest nowsza niż obsługiwana. To narzędzie może nie być obsługiwane w tej wersji zestawu SDK. Zaktualizuj zestaw SDK.
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
Nie można odnaleźć projektu „{0}” w rozwiązaniu.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
Odwołanie do projektu
diff --git a/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf b/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf
index e1eeef54320a..bb761f80f014 100644
--- a/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
Arquivo
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
A versão do formato é superior à versão compatível. Pode não haver suporte para esta ferramenta nesta versão do SDK. Atualize o SDK.
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
Não foi possível encontrar o projeto ‘{0}’ na solução.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
Referência do projeto
diff --git a/src/Cli/dotnet/xlf/CliStrings.ru.xlf b/src/Cli/dotnet/xlf/CliStrings.ru.xlf
index 59ec6c3db27a..ce0bcc153825 100644
--- a/src/Cli/dotnet/xlf/CliStrings.ru.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.ru.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
Файл
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
Версия формата выше поддерживаемой. Возможно, средство не поддерживается в этой версии пакета SDK. Обновите пакет SDK.
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
Проект "{0}" не удалось найти в решении.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
Ссылка на проект
diff --git a/src/Cli/dotnet/xlf/CliStrings.tr.xlf b/src/Cli/dotnet/xlf/CliStrings.tr.xlf
index 11679a3e19eb..5aa3771d7ff7 100644
--- a/src/Cli/dotnet/xlf/CliStrings.tr.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.tr.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
Dosya
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
Biçim sürümü desteklenenden daha yüksek. Bu araç bu SDK sürümünde desteklenmeyebilir. SDK’nızı güncelleştirin.
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
`{0}` projesi çözümde bulunamadı.
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
Proje başvurusu
diff --git a/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf b/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf
index 542e8d2e637b..8dd7dfe752b7 100644
--- a/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
文件
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
格式版本高于受支持的版本。该 SDK 版本可能不支持此工具。请更新 SDK。
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
未能在解决方案中找到项目“{0}”。
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
项目引用
diff --git a/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf b/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf
index 4df207fdec67..40cf73e5eee5 100644
--- a/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf
@@ -451,6 +451,11 @@ setx PATH "%PATH%;{0}"
檔案
+
+ The file-based app to operate on.
+ The file-based app to operate on.
+
+
Format version is higher than supported. This tool may not be supported in this SDK version. Update your SDK.
格式版本高於支援的版本。此 SDK 版本可能不支援這項工具。請更新您的 SDK。
@@ -846,6 +851,16 @@ setx PATH "%PATH%;{0}"
在解決方案中找不到專案 `{0}`。
+
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file.
+
+
+
+ PROJECT | FILE
+ PROJECT | FILE
+
+
Project reference
專案參考
diff --git a/src/Tasks/Common/MSBuildUtilities.cs b/src/Common/MSBuildUtilities.cs
similarity index 98%
rename from src/Tasks/Common/MSBuildUtilities.cs
rename to src/Common/MSBuildUtilities.cs
index b540c06b0374..d1ad0ea46959 100644
--- a/src/Tasks/Common/MSBuildUtilities.cs
+++ b/src/Common/MSBuildUtilities.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.NET.Build.Tasks
+namespace Microsoft.DotNet.Cli
{
///
/// Internal utilities copied from microsoft/MSBuild repo.
diff --git a/src/Tasks/Common/ItemUtilities.cs b/src/Tasks/Common/ItemUtilities.cs
index c05fe48c9085..42bc735ab857 100644
--- a/src/Tasks/Common/ItemUtilities.cs
+++ b/src/Tasks/Common/ItemUtilities.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Framework;
+using Microsoft.DotNet.Cli;
using Microsoft.NET.Build.Tasks.ConflictResolution;
namespace Microsoft.NET.Build.Tasks
diff --git a/src/Tasks/Microsoft.NET.Build.Extensions.Tasks/Microsoft.NET.Build.Extensions.Tasks.csproj b/src/Tasks/Microsoft.NET.Build.Extensions.Tasks/Microsoft.NET.Build.Extensions.Tasks.csproj
index d98dec41824d..969088345f1a 100644
--- a/src/Tasks/Microsoft.NET.Build.Extensions.Tasks/Microsoft.NET.Build.Extensions.Tasks.csproj
+++ b/src/Tasks/Microsoft.NET.Build.Extensions.Tasks/Microsoft.NET.Build.Extensions.Tasks.csproj
@@ -54,6 +54,7 @@
+
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 9dd44663b715..b0356f4a0c04 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
@@ -101,6 +101,7 @@
+
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs
index 463b83aa6d48..09fa432633fa 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ValidateExecutableReferences.cs
@@ -5,6 +5,7 @@
using System.Runtime.Versioning;
using Microsoft.Build.Framework;
+using Microsoft.DotNet.Cli;
namespace Microsoft.NET.Build.Tasks
{
diff --git a/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs b/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs
index 4d21d53fad6c..c924c1ac65bd 100644
--- a/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs
+++ b/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.CompilerServices;
+using Microsoft.DotNet.Cli.Commands;
namespace Microsoft.DotNet.Cli.Package.Add.Tests
{
@@ -31,19 +32,18 @@ public void WhenValidPackageIsPassedBeforeVersionItGetsAdded()
cmd.StdErr.Should().BeEmpty();
}
- public static readonly List AddPkg_PackageVersionsLatestPrereleaseSucessData
- = new()
- {
- new object[] { new string[] { "0.0.5", "0.9.0", "1.0.0-preview.3" }, "1.0.0-preview.3" },
- new object[] { new string[] { "0.0.5", "0.9.0", "1.0.0-preview.3", "1.1.1-preview.7" }, "1.1.1-preview.7" },
- new object[] { new string[] { "0.0.5", "0.9.0", "1.0.0" }, "1.0.0" },
- new object[] { new string[] { "0.0.5", "0.9.0", "1.0.0-preview.3", "2.0.0" }, "2.0.0" },
- new object[] { new string[] { "1.0.0-preview.1", "1.0.0-preview.2", "1.0.0-preview.3" }, "1.0.0-preview.3" },
- };
+ public static readonly TheoryData PackageVersionsTheoryData = new()
+ {
+ { ["0.0.5", "0.9.0", "1.0.0-preview.3"], "0.9.0", "1.0.0-preview.3" },
+ { ["0.0.5", "0.9.0", "1.0.0-preview.3", "1.1.1-preview.7"], "0.9.0", "1.1.1-preview.7" },
+ { ["0.0.5", "0.9.0", "1.0.0"], "1.0.0", "1.0.0" },
+ { ["0.0.5", "0.9.0", "1.0.0-preview.3", "2.0.0"], "2.0.0", "2.0.0" },
+ { ["1.0.0-preview.1", "1.0.0-preview.2", "1.0.0-preview.3"], null, "1.0.0-preview.3" },
+ };
[Theory]
- [MemberData(nameof(AddPkg_PackageVersionsLatestPrereleaseSucessData))]
- public void WhenPrereleaseOptionIsPassed(string[] inputVersions, string expectedVersion)
+ [MemberData(nameof(PackageVersionsTheoryData))]
+ public void WhenPrereleaseOptionIsPassed(string[] inputVersions, string? _, string expectedVersion)
{
var targetFramework = ToolsetInfo.CurrentTargetFramework;
TestProject testProject = new()
@@ -71,6 +71,44 @@ public void WhenPrereleaseOptionIsPassed(string[] inputVersions, string expected
.And.NotHaveStdErr();
}
+ [Theory]
+ [MemberData(nameof(PackageVersionsTheoryData))]
+ public void WhenNoVersionIsPassed(string[] inputVersions, string? expectedVersion, string prereleaseVersion)
+ {
+ var targetFramework = ToolsetInfo.CurrentTargetFramework;
+ TestProject testProject = new()
+ {
+ Name = "Project",
+ IsExe = false,
+ TargetFrameworks = targetFramework,
+ };
+
+ var packages = inputVersions.Select(e => GetPackagePath(targetFramework, "A", e, identifier: expectedVersion + e + inputVersions.GetHashCode().ToString())).ToArray();
+
+ // disable implicit use of the Roslyn Toolset compiler package
+ testProject.AdditionalProperties["BuildWithNetFrameworkHostedCompiler"] = false.ToString();
+ testProject.AdditionalProperties.Add("RestoreSources",
+ "$(RestoreSources);" + string.Join(";", packages.Select(package => Path.GetDirectoryName(package))));
+
+ var testAsset = _testAssetsManager.CreateTestProject(testProject, identifier: inputVersions.GetHashCode().ToString());
+
+ var cmd = new DotnetCommand(Log)
+ .WithWorkingDirectory(Path.Combine(testAsset.TestRoot, testProject.Name))
+ .Execute("add", "package", "A");
+
+ if (expectedVersion is null)
+ {
+ cmd.Should().Fail()
+ .And.HaveStdOutContaining($"There are no stable versions available, {prereleaseVersion} is the best available. Consider adding the --prerelease option");
+ }
+ else
+ {
+ cmd.Should().Pass()
+ .And.HaveStdOutContaining($"PackageReference for package 'A' version '{expectedVersion}' ")
+ .And.NotHaveStdErr();
+ }
+ }
+
[Fact]
public void WhenPrereleaseAndVersionOptionIsPassedFails()
{
@@ -252,6 +290,373 @@ public void VersionRange(bool asArgument)
cmd.StdErr.Should().BeEmpty();
}
+ [Fact]
+ public void FileBasedApp()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ File.WriteAllText(file, """
+ Console.WriteLine();
+ """);
+
+ new DotnetCommand(Log, "package", "add", "Humanizer@2.14.1", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ File.ReadAllText(file).Should().Be("""
+ #:package Humanizer@2.14.1
+
+ Console.WriteLine();
+ """);
+ }
+
+ [Theory]
+ [InlineData("Humanizer")]
+ [InlineData("humanizer")]
+ public void FileBasedApp_ReplaceExisting(
+ string sourceFilePackageId)
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ File.WriteAllText(file, $"""
+ #:package {sourceFilePackageId}@2.9.9
+ Console.WriteLine();
+ """);
+
+ new DotnetCommand(Log, "package", "add", "Humanizer@2.14.1", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ File.ReadAllText(file).Should().Be($"""
+ #:package Humanizer@2.14.1
+ Console.WriteLine();
+ """);
+ }
+
+ [Theory, MemberData(nameof(PackageVersionsTheoryData))]
+ public void FileBasedApp_NoVersion(string[] inputVersions, string? expectedVersion, string _)
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+
+ var packages = inputVersions.Select(e => GetPackagePath(ToolsetInfo.CurrentTargetFramework, "A", e, identifier: expectedVersion + e + inputVersions.GetHashCode().ToString())).ToArray();
+
+ var restoreSources = string.Join(";", packages.Select(package => Path.GetDirectoryName(package)));
+
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ var source = $"""
+ #:property RestoreSources=$(RestoreSources);{restoreSources}
+ Console.WriteLine();
+ """;
+ File.WriteAllText(file, source);
+
+ var cmd = new DotnetCommand(Log, "package", "add", "A", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute();
+
+ if (expectedVersion is null)
+ {
+ cmd.Should().Fail();
+
+ File.ReadAllText(file).Should().Be(source);
+ }
+ else
+ {
+ cmd.Should().Pass();
+
+ File.ReadAllText(file).Should().Be($"""
+ #:package A@{expectedVersion}
+ {source}
+ """);
+ }
+ }
+
+ [Theory, MemberData(nameof(PackageVersionsTheoryData))]
+ public void FileBasedApp_NoVersion_Prerelease(string[] inputVersions, string? _, string expectedVersion)
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+
+ var packages = inputVersions.Select(e => GetPackagePath(ToolsetInfo.CurrentTargetFramework, "A", e, identifier: expectedVersion + e + inputVersions.GetHashCode().ToString())).ToArray();
+
+ var restoreSources = string.Join(";", packages.Select(package => Path.GetDirectoryName(package)));
+
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ var source = $"""
+ #:property RestoreSources=$(RestoreSources);{restoreSources}
+ Console.WriteLine();
+ """;
+ File.WriteAllText(file, source);
+
+ var cmd = new DotnetCommand(Log, "package", "add", "A", "--prerelease", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute();
+
+ cmd.Should().Pass();
+
+ File.ReadAllText(file).Should().Be($"""
+ #:package A@{expectedVersion}
+ {source}
+ """);
+ }
+
+ [Fact]
+ public void FileBasedApp_NoVersionAndNoRestore()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ File.WriteAllText(file, """
+ Console.WriteLine();
+ """);
+
+ new DotnetCommand(Log, "package", "add", "Humanizer", "--file", "Program.cs", "--no-restore")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ File.ReadAllText(file).Should().Be("""
+ #:package Humanizer@*
+
+ Console.WriteLine();
+ """);
+ }
+
+ [Fact]
+ public void FileBasedApp_VersionAndPrerelease()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ var source = """
+ Console.WriteLine();
+ """;
+ File.WriteAllText(file, source);
+
+ new DotnetCommand(Log, "package", "add", "Humanizer@2.14.1", "--file", "Program.cs", "--prerelease")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Fail()
+ .And.HaveStdErrContaining(CliCommandStrings.PrereleaseAndVersionAreNotSupportedAtTheSameTime);
+
+ File.ReadAllText(file).Should().Be(source);
+ }
+
+ [Fact]
+ public void FileBasedApp_InvalidPackage()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ var source = """
+ Console.WriteLine();
+ """;
+ File.WriteAllText(file, source);
+
+ new DotnetCommand(Log, "package", "add", "Microsoft.ThisPackageDoesNotExist", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Fail();
+
+ File.ReadAllText(file).Should().Be(source);
+ }
+
+ [Fact]
+ public void FileBasedApp_InvalidPackage_NoRestore()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ File.WriteAllText(file, """
+ Console.WriteLine();
+ """);
+
+ new DotnetCommand(Log, "package", "add", "Microsoft.ThisPackageDoesNotExist", "--file", "Program.cs", "--no-restore")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ File.ReadAllText(file).Should().Be("""
+ #:package Microsoft.ThisPackageDoesNotExist@*
+
+ Console.WriteLine();
+ """);
+ }
+
+ [Fact]
+ public void FileBasedApp_CentralPackageManagement()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ var source = """
+ Console.WriteLine();
+ """;
+ File.WriteAllText(file, source);
+
+ var directoryPackagesProps = Path.Join(testInstance.Path, "Directory.Packages.props");
+ File.WriteAllText(directoryPackagesProps, """
+
+
+ true
+
+
+ """);
+
+ new DotnetCommand(Log, "package", "add", "Humanizer@2.14.1", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ File.ReadAllText(file).Should().Be($"""
+ #:package Humanizer
+
+ {source}
+ """);
+
+ File.ReadAllText(directoryPackagesProps).Should().Be("""
+
+
+ true
+
+
+
+
+
+ """);
+ }
+
+ [Theory, CombinatorialData]
+ public void FileBasedApp_CentralPackageManagement_ReplaceExisting(bool wasInFile)
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ var source = """
+ Console.WriteLine();
+ """;
+
+ if (wasInFile)
+ {
+ source = $"""
+ #:package Humanizer@2.9.9
+
+ {source}
+ """;
+ }
+
+ File.WriteAllText(file, source);
+
+ var directoryPackagesProps = Path.Join(testInstance.Path, "Directory.Packages.props");
+ File.WriteAllText(directoryPackagesProps, """
+
+
+ true
+
+
+
+
+
+ """);
+
+ new DotnetCommand(Log, "package", "add", "Humanizer@2.14.1", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ File.ReadAllText(file).Should().Be("""
+ #:package Humanizer
+
+ Console.WriteLine();
+ """);
+
+ File.ReadAllText(directoryPackagesProps).Should().Be("""
+
+
+ true
+
+
+
+
+
+ """);
+ }
+
+ [Fact]
+ public void FileBasedApp_CentralPackageManagement_NoVersionSpecified()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+
+ string[] versions = ["0.0.5", "0.9.0", "1.0.0-preview.3"];
+ var packages = versions.Select(e => GetPackagePath(ToolsetInfo.CurrentTargetFramework, "A", e, identifier: e + versions.GetHashCode().ToString())).ToArray();
+
+ var restoreSources = string.Join(";", packages.Select(package => Path.GetDirectoryName(package)));
+
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ var source = $"""
+ #:property RestoreSources=$(RestoreSources);{restoreSources}
+ Console.WriteLine();
+ """;
+ File.WriteAllText(file, source);
+
+ var directoryPackagesProps = Path.Join(testInstance.Path, "Directory.Packages.props");
+ File.WriteAllText(directoryPackagesProps, """
+
+
+ true
+
+
+ """);
+
+ new DotnetCommand(Log, "package", "add", "A", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ File.ReadAllText(file).Should().Be($"""
+ #:package A
+ {source}
+ """);
+
+ File.ReadAllText(directoryPackagesProps).Should().Be("""
+
+
+ true
+
+
+
+
+
+ """);
+ }
+
+ [Fact]
+ public void FileBasedApp_CentralPackageManagement_NoVersionSpecified_KeepExisting()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ var source = """
+ #:package Humanizer
+ Console.WriteLine();
+ """;
+ File.WriteAllText(file, source);
+
+ var directoryPackagesProps = Path.Join(testInstance.Path, "Directory.Packages.props");
+ var directoryPackagesPropsSource = """
+
+
+ true
+
+
+
+
+
+ """;
+ File.WriteAllText(directoryPackagesProps, directoryPackagesPropsSource);
+
+ new DotnetCommand(Log, "package", "add", "Humanizer", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass();
+
+ File.ReadAllText(file).Should().Be(source);
+
+ File.ReadAllText(directoryPackagesProps).Should().Be(directoryPackagesPropsSource);
+ }
private static TestProject GetProject(string targetFramework, string referenceProjectName, string version)
{
diff --git a/test/dotnet.Tests/CommandTests/Package/Remove/GivenDotnetPackageRemove.cs b/test/dotnet.Tests/CommandTests/Package/Remove/GivenDotnetPackageRemove.cs
new file mode 100644
index 000000000000..21bbdd008ca9
--- /dev/null
+++ b/test/dotnet.Tests/CommandTests/Package/Remove/GivenDotnetPackageRemove.cs
@@ -0,0 +1,82 @@
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.DotNet.Cli.Package.Remove.Tests;
+
+public sealed class GivenDotnetPackageRemove(ITestOutputHelper log) : SdkTest(log)
+{
+ [Fact]
+ public void WhenPackageIsRemovedWithoutProjectArgument()
+ {
+ var projectDirectory = _testAssetsManager
+ .CopyTestAsset("TestAppSimple")
+ .WithSource().Path;
+
+ const string packageName = "Newtonsoft.Json";
+ new DotnetCommand(Log)
+ .WithWorkingDirectory(projectDirectory)
+ .Execute("add", "package", packageName)
+ .Should().Pass();
+
+ // Test the new 'dotnet package remove' command without specifying project
+ new DotnetCommand(Log)
+ .WithWorkingDirectory(projectDirectory)
+ .Execute("package", "remove", packageName)
+ .Should().Pass()
+ .And.HaveStdOutContaining($"Removing PackageReference for package '{packageName}' from project '{projectDirectory + Path.DirectorySeparatorChar}TestAppSimple.csproj'.")
+ .And.NotHaveStdErr();
+ }
+
+ [Fact]
+ public void WhenPackageIsRemovedWithProjectOption()
+ {
+ var projectDirectory = _testAssetsManager
+ .CopyTestAsset("TestAppSimple")
+ .WithSource().Path;
+
+ const string packageName = "Newtonsoft.Json";
+ new DotnetCommand(Log)
+ .WithWorkingDirectory(projectDirectory)
+ .Execute("add", "package", packageName)
+ .Should().Pass();
+
+ // Test the new 'dotnet package remove' command with --project option
+ new DotnetCommand(Log)
+ .WithWorkingDirectory(projectDirectory)
+ .Execute("package", "remove", packageName, "--project", "TestAppSimple.csproj")
+ .Should().Pass()
+ .And.HaveStdOutContaining($"Removing PackageReference for package '{packageName}' from project 'TestAppSimple.csproj'.")
+ .And.NotHaveStdErr();
+ }
+
+ [Fact]
+ public void WhenNoPackageIsPassedCommandFails()
+ {
+ var projectDirectory = _testAssetsManager
+ .CopyTestAsset("TestAppSimple")
+ .WithSource()
+ .Path;
+
+ new DotnetCommand(Log)
+ .WithWorkingDirectory(projectDirectory)
+ .Execute("package", "remove")
+ .Should()
+ .Fail();
+ }
+
+ [Fact]
+ public void WhenMultiplePackagesArePassedCommandFails()
+ {
+ var projectDirectory = _testAssetsManager
+ .CopyTestAsset("TestAppSimple")
+ .WithSource()
+ .Path;
+
+ new DotnetCommand(Log)
+ .WithWorkingDirectory(projectDirectory)
+ .Execute("package", "remove", "package1", "package2")
+ .Should()
+ .Fail();
+ }
+}
diff --git a/test/dotnet.Tests/CommandTests/Package/Remove/GivenDotnetRemovePackage.cs b/test/dotnet.Tests/CommandTests/Package/Remove/GivenDotnetRemovePackage.cs
index 059877551298..3988a7e15b6d 100644
--- a/test/dotnet.Tests/CommandTests/Package/Remove/GivenDotnetRemovePackage.cs
+++ b/test/dotnet.Tests/CommandTests/Package/Remove/GivenDotnetRemovePackage.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.DotNet.Cli.Commands;
using Microsoft.DotNet.Cli.Utils;
namespace Microsoft.DotNet.Cli.Remove.Package.Tests
@@ -11,10 +12,10 @@ public class GivenDotnetRemovePackage : SdkTest
Remove a NuGet package reference from the project.
Usage:
- dotnet remove [] package ... [options]
+ dotnet remove [] package ... [options]
Arguments:
- The project file to operate on. If a file is not specified, the command will search the current directory for one. [default: {PathUtility.EnsureTrailingSlash(defaultVal)}]
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file. [default: {PathUtility.EnsureTrailingSlash(defaultVal)}]
The package reference to remove.
Options:
@@ -25,10 +26,10 @@ dotnet remove [] package ... [options]
.NET Remove Command
Usage:
- dotnet remove [command] [options]
+ dotnet remove [command] [options]
Arguments:
- The project file to operate on. If a file is not specified, the command will search the current directory for one. [default: {PathUtility.EnsureTrailingSlash(defaultVal)}]
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file. [default: {PathUtility.EnsureTrailingSlash(defaultVal)}]
Options:
-?, -h, --help Show command line help.
@@ -84,5 +85,75 @@ public void WhenReferencedPackageIsPassedItGetsRemoved()
remove.StdOut.Should().Contain($"Removing PackageReference for package '{packageName}' from project '{projectDirectory + Path.DirectorySeparatorChar}TestAppSimple.csproj'.");
remove.StdErr.Should().BeEmpty();
}
+
+ [Fact]
+ public void FileBasedApp()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ File.WriteAllText(file, """
+ #:package Humanizer@2.14.1
+
+ Console.WriteLine();
+ """);
+
+ new DotnetCommand(Log, "package", "remove", "Humanizer", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut(string.Format(CliCommandStrings.DirectivesRemoved, "#:package", 1, "Humanizer", file));
+
+ File.ReadAllText(file).Should().Be("""
+ Console.WriteLine();
+ """);
+ }
+
+ [Fact]
+ public void FileBasedApp_Multiple()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ File.WriteAllText(file, """
+ #:package Humanizer@2.14.1
+ #:package Another@1.0.0
+ #:property X=Y
+ #:package Humanizer@2.9.9
+
+ Console.WriteLine();
+ """);
+
+ new DotnetCommand(Log, "package", "remove", "Humanizer", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Pass()
+ .And.HaveStdOut(string.Format(CliCommandStrings.DirectivesRemoved, "#:package", 2, "Humanizer", file));
+
+ File.ReadAllText(file).Should().Be("""
+ #:package Another@1.0.0
+ #:property X=Y
+
+ Console.WriteLine();
+ """);
+ }
+
+ [Fact]
+ public void FileBasedApp_None()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ File.WriteAllText(file, """
+ Console.WriteLine();
+ """);
+
+ new DotnetCommand(Log, "package", "remove", "Humanizer", "--file", "Program.cs")
+ .WithWorkingDirectory(testInstance.Path)
+ .Execute()
+ .Should().Fail()
+ .And.HaveStdOut(string.Format(CliCommandStrings.DirectivesRemoved, "#:package", 0, "Humanizer", file));
+
+ File.ReadAllText(file).Should().Be("""
+ Console.WriteLine();
+ """);
+ }
}
}
diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
index 082f5c5ecd22..ad3600e59f00 100644
--- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
+++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Text;
using Microsoft.DotNet.Cli.Commands;
using Microsoft.DotNet.Cli.Commands.Run;
@@ -960,6 +959,53 @@ public void Directives_Whitespace()
""");
}
+ [Fact]
+ public void Directives_BlankLines()
+ {
+ var expectedProject = $"""
+
+
+
+ Exe
+ {ToolsetInfo.CurrentTargetFramework}
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+ """;
+
+ VerifyConversion(
+ inputCSharp: """
+ #:package A@B
+
+ Console.WriteLine();
+ """,
+ expectedProject: expectedProject,
+ expectedCSharp: """
+
+ Console.WriteLine();
+ """);
+
+ VerifyConversion(
+ inputCSharp: """
+
+ #:package A@B
+ Console.WriteLine();
+ """,
+ expectedProject: expectedProject,
+ expectedCSharp: """
+
+ Console.WriteLine();
+ """);
+ }
+
///
/// #: directives after C# code are ignored.
///
@@ -1199,7 +1245,7 @@ public void Directives_VersionedSdkFirst()
private static void Convert(string inputCSharp, out string actualProject, out string? actualCSharp, bool force, string? filePath)
{
var sourceFile = new SourceFile(filePath ?? "/app/Program.cs", SourceText.From(inputCSharp, Encoding.UTF8));
- var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !force, errors: null);
+ var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !force, DiagnosticBag.ThrowOnFirst());
var projectWriter = new StringWriter();
VirtualProjectBuildingCommand.WriteProjectFile(projectWriter, directives, isVirtualProject: false);
actualProject = projectWriter.ToString();
@@ -1225,8 +1271,7 @@ private static void VerifyConversionThrows(string inputCSharp, string expectedWi
private static void VerifyDirectiveConversionErrors(string inputCSharp, IEnumerable expectedErrors)
{
var sourceFile = new SourceFile("/app/Program.cs", SourceText.From(inputCSharp, Encoding.UTF8));
- var errors = ImmutableArray.CreateBuilder();
- VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: true, errors: errors);
- errors.Select(e => e.Message).Should().BeEquivalentTo(expectedErrors);
+ VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: true, DiagnosticBag.Collect(out var diagnostics));
+ diagnostics.Select(e => e.Message).Should().BeEquivalentTo(expectedErrors);
}
}
diff --git a/test/dotnet.Tests/CommandTests/Reference/Add/AddReferenceParserTests.cs b/test/dotnet.Tests/CommandTests/Reference/Add/AddReferenceParserTests.cs
index 1efd8b75d6df..f65d0720069c 100644
--- a/test/dotnet.Tests/CommandTests/Reference/Add/AddReferenceParserTests.cs
+++ b/test/dotnet.Tests/CommandTests/Reference/Add/AddReferenceParserTests.cs
@@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.CommandLine.Parsing;
-using Microsoft.DotNet.Cli.Commands.Hidden.Add;
+using Microsoft.DotNet.Cli.Commands.Package;
using Microsoft.DotNet.Cli.Commands.Reference.Add;
using Microsoft.DotNet.Cli.Utils;
using Parser = Microsoft.DotNet.Cli.Parser;
@@ -23,7 +23,7 @@ public void AddReferenceHasDefaultArgumentSetToCurrentDirectory()
{
var result = Parser.Instance.Parse("dotnet add reference my.csproj");
- result.GetValue(AddCommandParser.ProjectArgument)
+ result.GetValue(PackageCommandParser.ProjectOrFileArgument)
.Should()
.BeEquivalentTo(
PathUtility.EnsureTrailingSlash(Directory.GetCurrentDirectory()));
diff --git a/test/dotnet.Tests/CommandTests/Reference/Remove/GivenDotnetRemoveP2P.cs b/test/dotnet.Tests/CommandTests/Reference/Remove/GivenDotnetRemoveP2P.cs
index 7e9a35ea5c90..83de7f5eb3d9 100644
--- a/test/dotnet.Tests/CommandTests/Reference/Remove/GivenDotnetRemoveP2P.cs
+++ b/test/dotnet.Tests/CommandTests/Reference/Remove/GivenDotnetRemoveP2P.cs
@@ -14,10 +14,10 @@ public class GivenDotnetRemoveReference : SdkTest
Remove a project-to-project reference from the project.
Usage:
- dotnet remove reference ... [options]
+ dotnet remove reference ... [options]
Arguments:
- The project file to operate on. If a file is not specified, the command will search the current directory for one. [default: {PathUtility.EnsureTrailingSlash(defaultVal)}]
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file. [default: {PathUtility.EnsureTrailingSlash(defaultVal)}]
The paths to the referenced projects to remove.
Options:
@@ -28,10 +28,10 @@ dotnet remove reference ... [options]
.NET Remove Command
Usage:
- dotnet remove [command] [options]
+ dotnet remove [command] [options]
Arguments:
- The project file to operate on. If a file is not specified, the command will search the current directory for one. [default: {PathUtility.EnsureTrailingSlash(defaultVal)}]
+ The project file or C# file-based app to operate on. If a file is not specified, the command will search the current directory for a project file. [default: {PathUtility.EnsureTrailingSlash(defaultVal)}]
Options:
-?, -h, --help Show command line help.
diff --git a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs
new file mode 100644
index 000000000000..ea2480b032f4
--- /dev/null
+++ b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs
@@ -0,0 +1,475 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.CodeAnalysis.Text;
+using Microsoft.DotNet.Cli.Commands.Run;
+
+namespace Microsoft.DotNet.Cli.Run.Tests;
+
+public sealed class FileBasedAppSourceEditorTests(ITestOutputHelper log) : SdkTest(log)
+{
+ private static FileBasedAppSourceEditor CreateEditor(string source)
+ {
+ return FileBasedAppSourceEditor.Load(new SourceFile("/app/Program.cs", SourceText.From(source, Encoding.UTF8)));
+ }
+
+ [Theory]
+ [InlineData("#:package MyPackage@1.0.1")]
+ [InlineData("#:package MyPackage @ abc")]
+ [InlineData("#:package MYPACKAGE")]
+ public void ReplaceExisting(string inputLine)
+ {
+ Verify(
+ $"""
+ {inputLine}
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:package MyPackage@1.0.0
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void OnlyStatement()
+ {
+ Verify(
+ """
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:package MyPackage@1.0.0
+
+ Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives.Single()),
+ """
+ Console.WriteLine();
+ """));
+ }
+
+ [Theory]
+ [InlineData("// only comment")]
+ [InlineData("/* only comment */")]
+ public void OnlyComment(string comment)
+ {
+ Verify(
+ comment,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ $"""
+ {comment}
+
+ #:package MyPackage@1.0.0
+
+ """),
+ (static editor => editor.Remove(editor.Directives.Single()),
+ $"""
+ {comment}
+
+
+ """));
+ }
+
+ [Fact]
+ public void Empty()
+ {
+ Verify(
+ "",
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:package MyPackage@1.0.0
+
+ """),
+ (static editor => editor.Remove(editor.Directives.Single()),
+ ""));
+ }
+
+ [Fact]
+ public void PreExistingWhiteSpace()
+ {
+ Verify(
+ """
+
+
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:package MyPackage@1.0.0
+
+
+ Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives.Single()),
+ """
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void Comments()
+ {
+ Verify(
+ """
+ // Comment1a
+ // Comment1b
+
+ // Comment2a
+ // Comment2b
+ Console.WriteLine();
+ // Comment3
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ // Comment1a
+ // Comment1b
+
+ // Comment2a
+ // Comment2b
+
+ #:package MyPackage@1.0.0
+
+ Console.WriteLine();
+ // Comment3
+ """),
+ (static editor => editor.Remove(editor.Directives.Single()),
+ """
+ // Comment1a
+ // Comment1b
+
+ // Comment2a
+ // Comment2b
+
+ Console.WriteLine();
+ // Comment3
+ """));
+ }
+
+ [Fact]
+ public void CommentsWithWhiteSpaceAfter()
+ {
+ Verify(
+ """
+ // Comment
+
+
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ // Comment
+
+
+ #:package MyPackage@1.0.0
+
+ Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives.Single()),
+ """
+ // Comment
+
+
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void Comment_Documentation()
+ {
+ Verify(
+ """
+ /// doc comment
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ /// doc comment
+
+ #:package MyPackage@1.0.0
+
+ Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives.Single()),
+ """
+ /// doc comment
+
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void Comment_MultiLine()
+ {
+ Verify(
+ """
+ /* test */
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ /* test */
+
+ #:package MyPackage@1.0.0
+
+ Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives.Single()),
+ """
+ /* test */
+
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void Comment_MultiLine_NoNewLine()
+ {
+ Verify(
+ """
+ /* test */Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:package MyPackage@1.0.0
+
+ /* test */Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives.Single()),
+ """
+ /* test */Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void Comment_MultiLine_NoNewLine_Multiple()
+ {
+ Verify(
+ """
+ // test
+ /* test */Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ // test
+
+ #:package MyPackage@1.0.0
+
+ /* test */Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives.Single()),
+ """
+ // test
+
+ /* test */Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void Group()
+ {
+ Verify(
+ """
+ #:property X=Y
+ #:package B@C
+ #:project D
+ #:package E
+
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:property X=Y
+ #:package B@C
+ #:package MyPackage@1.0.0
+ #:project D
+ #:package E
+
+ Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives[2]),
+ """
+ #:property X=Y
+ #:package B@C
+ #:project D
+ #:package E
+
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void GroupEnd()
+ {
+ Verify(
+ """
+ #:property X=Y
+ #:package B@C
+
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:property X=Y
+ #:package B@C
+ #:package MyPackage@1.0.0
+
+ Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives[2]),
+ """
+ #:property X=Y
+ #:package B@C
+
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void GroupWithoutSpace()
+ {
+ Verify(
+ """
+ #:package B@C
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:package B@C
+ #:package MyPackage@1.0.0
+ Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives[1]),
+ """
+ #:package B@C
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void OtherDirectives()
+ {
+ Verify(
+ """
+ #:property A
+ #:project D
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:package MyPackage@1.0.0
+ #:property A
+ #:project D
+ Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives[0]),
+ """
+ #:property A
+ #:project D
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void AfterTokens()
+ {
+ Verify(
+ """
+ using System;
+
+ #:package A
+
+ Console.WriteLine();
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:package MyPackage@1.0.0
+
+ using System;
+
+ #:package A
+
+ Console.WriteLine();
+ """),
+ (static editor => editor.Remove(editor.Directives[0]),
+ """
+ using System;
+
+ #:package A
+
+ Console.WriteLine();
+ """));
+ }
+
+ [Fact]
+ public void SkippedTokensTrivia()
+ {
+ Verify(
+ """
+ #if false
+ Test
+ #endif
+ """,
+ (static editor => editor.Add(new CSharpDirective.Package(default) { Name = "MyPackage", Version = "1.0.0" }),
+ """
+ #:package MyPackage@1.0.0
+
+ #if false
+ Test
+ #endif
+ """),
+ (static editor => editor.Remove(editor.Directives[0]),
+ """
+ #if false
+ Test
+ #endif
+ """));
+ }
+
+ [Fact]
+ public void RemoveMultiple()
+ {
+ Verify(
+ """
+ #:package Humanizer@2.14.1
+ #:property X=Y
+ #:package Humanizer@2.9.9
+
+ Console.WriteLine();
+ """,
+ (static editor =>
+ {
+ editor.Remove(editor.Directives.OfType().First());
+ editor.Remove(editor.Directives.OfType().First());
+ },
+ """
+ #:property X=Y
+
+ Console.WriteLine();
+ """));
+ }
+
+ private void Verify(
+ string input,
+ params ReadOnlySpan<(Action action, string expectedOutput)> verify)
+ {
+ var editor = CreateEditor(input);
+ int index = 0;
+ foreach (var (action, expectedOutput) in verify)
+ {
+ action(editor);
+ var actualOutput = editor.SourceFile.Text.ToString();
+ if (actualOutput != expectedOutput)
+ {
+ Log.WriteLine("Expected output:\n---");
+ Log.WriteLine(expectedOutput);
+ Log.WriteLine("---\nActual output:\n---");
+ Log.WriteLine(actualOutput);
+ Log.WriteLine("---");
+ Assert.Fail($"Output mismatch at index {index}.");
+ }
+ index++;
+ }
+ }
+}
diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
index 012a920081e5..9077a244c86b 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
@@ -1831,7 +1831,7 @@ public void UpToDate_InvalidOptions()
.WithWorkingDirectory(testInstance.Path)
.Execute()
.Should().Fail()
- .And.HaveStdErrContaining(string.Format(CliCommandStrings.InvalidOptionCombination, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoBuildOption.Name));
+ .And.HaveStdErrContaining(string.Format(CliCommandStrings.CannotCombineOptions, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoBuildOption.Name));
}
private static string ToJson(string s) => JsonSerializer.Serialize(s);
diff --git a/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh b/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh
index 9d210eafca5c..b5faea11f6a6 100644
--- a/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh
+++ b/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh
@@ -1067,7 +1067,7 @@ _testhost_package_add() {
prev="${COMP_WORDS[COMP_CWORD-1]}"
COMPREPLY=()
- opts="--version --framework --no-restore --source --package-directory --interactive --prerelease --project --help"
+ opts="--version --framework --no-restore --source --package-directory --interactive --prerelease --project --file --help"
opts="$opts $(${COMP_WORDS[0]} complete --position ${COMP_POINT} ${COMP_LINE} 2>/dev/null | tr '\n' ' ')"
if [[ $COMP_CWORD == "$1" ]]; then
@@ -1120,7 +1120,7 @@ _testhost_package_remove() {
prev="${COMP_WORDS[COMP_CWORD-1]}"
COMPREPLY=()
- opts="--interactive --project --help"
+ opts="--interactive --project --file --help"
if [[ $COMP_CWORD == "$1" ]]; then
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
diff --git a/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1 b/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1
index c7c6d89b9f37..6477aa8f0ed6 100644
--- a/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1
+++ b/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1
@@ -617,6 +617,7 @@ Register-ArgumentCompleter -Native -CommandName 'testhost' -ScriptBlock {
[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, "Allows the command to stop and wait for user input or action (for example to complete authentication).")
[CompletionResult]::new('--prerelease', '--prerelease', [CompletionResultType]::ParameterName, "Allows prerelease packages to be installed.")
[CompletionResult]::new('--project', '--project', [CompletionResultType]::ParameterName, "The project file to operate on. If a file is not specified, the command will search the current directory for one.")
+ [CompletionResult]::new('--file', '--file', [CompletionResultType]::ParameterName, "The file-based app to operate on.")
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, "Show command line help.")
[CompletionResult]::new('--help', '-h', [CompletionResultType]::ParameterName, "Show command line help.")
)
@@ -659,6 +660,7 @@ Register-ArgumentCompleter -Native -CommandName 'testhost' -ScriptBlock {
$staticCompletions = @(
[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, "Allows the command to stop and wait for user input or action (for example to complete authentication).")
[CompletionResult]::new('--project', '--project', [CompletionResultType]::ParameterName, "The project file to operate on. If a file is not specified, the command will search the current directory for one.")
+ [CompletionResult]::new('--file', '--file', [CompletionResultType]::ParameterName, "The file-based app to operate on.")
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, "Show command line help.")
[CompletionResult]::new('--help', '-h', [CompletionResultType]::ParameterName, "Show command line help.")
)
diff --git a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh
index 98b08081a697..f62b5eefb072 100644
--- a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh
+++ b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh
@@ -613,6 +613,7 @@ _testhost() {
'--interactive[Allows the command to stop and wait for user input or action (for example to complete authentication).]' \
'--prerelease[Allows prerelease packages to be installed.]' \
'--project=[The project file to operate on. If a file is not specified, the command will search the current directory for one.]: : ' \
+ '--file=[The file-based app to operate on.]: : ' \
'--help[Show command line help.]' \
'-h[Show command line help.]' \
':packageId -- Package reference in the form of a package identifier like '\''Newtonsoft.Json'\'' or package identifier and version separated by '\''@'\'' like '\''Newtonsoft.Json@13.0.3'\''.:->dotnet_dynamic_complete' \
@@ -658,6 +659,7 @@ _testhost() {
_arguments "${_arguments_options[@]}" : \
'--interactive[Allows the command to stop and wait for user input or action (for example to complete authentication).]' \
'--project=[The project file to operate on. If a file is not specified, the command will search the current directory for one.]: : ' \
+ '--file=[The file-based app to operate on.]: : ' \
'--help[Show command line help.]' \
'-h[Show command line help.]' \
'*::PACKAGE_NAME -- The package reference to remove.: ' \