diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md
index 8e70cb3b11db..2458c9b5bb28 100644
--- a/documentation/general/dotnet-run-file.md
+++ b/documentation/general/dotnet-run-file.md
@@ -100,6 +100,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.
@@ -373,8 +376,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 75c76f009030..773e525cbdce 100644
--- a/src/Cli/dotnet/Commands/CliCommandStrings.resx
+++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx
@@ -1251,6 +1251,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}
@@ -1530,14 +1535,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..e97898a0a5ec 100644
--- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs
+++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs
@@ -1,27 +1,33 @@
// 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.Evaluation;
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 +120,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 +139,214 @@ 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 (!string.Equals(projectInstance.GetProperty("ManagePackageVersionsCentrally")?.EvaluatedValue, bool.TrueString, StringComparison.OrdinalIgnoreCase))
+ {
+ 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, Update, Save);
+
+ void Update(string value)
+ {
+ versionAttribute.Value = value;
+ directoryPackagesPropsProject.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, Update, Save);
+
+ void Update(string value)
+ {
+ metadata.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..f25150679a90 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;
@@ -18,7 +16,7 @@ public static class PackageAddCommandParser
.AddCompletions((context) =>
{
// we should take --prerelease flags into account for version completion
- var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption);
+ var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption!);
return QueryNuGet(context.WordToComplete, allowPrerelease, CancellationToken.None).Result.Select(packageId => new CompletionItem(packageId));
});
@@ -33,7 +31,7 @@ public static class PackageAddCommandParser
if (context.ParseResult.GetValue(CmdPackageArgument) is { HasVersion: false } packageId)
{
// we should take --prerelease flags into account for version completion
- var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption);
+ var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption!);
return QueryVersionsForPackage(packageId.Id, context.WordToComplete, allowPrerelease, CancellationToken.None)
.Result
.Select(version => new CompletionItem(version.ToNormalizedString()));
@@ -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..d21fc639735b 100644
--- a/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs
+++ b/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs
@@ -1,51 +1,47 @@
// 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.Length != 1)
{
- throw new ArgumentNullException(nameof(_fileOrDirectory));
+ throw new GracefulException(CliCommandStrings.PackageRemoveSpecifyExactlyOnePackageReference);
}
- if (_arguments.Count != 1)
+
+ var packageToRemove = arguments.Single();
+
+ 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 +65,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 d4ae5207be00..96fdccc110e9 100644
--- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
+++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
@@ -30,8 +30,8 @@ public override int Execute()
}
// 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 aa6c6db3c950..4a6ae511e885 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..096aaf9dd41c
--- /dev/null
+++ b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs
@@ -0,0 +1,223 @@
+// 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.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
+{
+ private bool _modified;
+
+ public SourceFile SourceFile
+ {
+ get;
+ private set
+ {
+ field = value;
+ _modified = true;
+ }
+ }
+
+ public ImmutableArray Directives
+ {
+ get
+ {
+ ReloadIfNecessary();
+ return field;
+ }
+ private set
+ {
+ field = value;
+ _modified = false;
+ }
+ }
+
+ public required string NewLine { get; init; }
+
+ private FileBasedAppSourceEditor() { }
+
+ public static FileBasedAppSourceEditor Load(SourceFile sourceFile)
+ {
+ return new FileBasedAppSourceEditor
+ {
+ SourceFile = sourceFile,
+ Directives = LoadDirectives(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",
+ _ => Environment.NewLine,
+ };
+ }
+ }
+
+ private static ImmutableArray LoadDirectives(SourceFile sourceFile)
+ {
+ return VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: false, DiagnosticBag.Ignore());
+ }
+
+ private void ReloadIfNecessary()
+ {
+ if (_modified)
+ {
+ Directives = LoadDirectives(SourceFile);
+ }
+ }
+
+ public void Add(CSharpDirective directive)
+ {
+ TextSpan span = DetermineWhereToAdd(directive, out var append);
+ string newText = append.Prefix + directive.ToString() + NewLine + append.Suffix;
+ SourceFile = SourceFile.WithText(SourceFile.Text.Replace(span, newText: newText));
+ }
+
+ private TextSpan DetermineWhereToAdd(CSharpDirective directive, out (string? Prefix, string? Suffix) append)
+ {
+ append = default;
+
+ // 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 toReplace.Info.Span;
+ }
+
+ // 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? addAfer = null;
+ foreach (var existingDirective in Directives)
+ {
+ if (existingDirective.GetType() == directive.GetType())
+ {
+ addAfer = existingDirective;
+ }
+ else if (addAfer != null)
+ {
+ break;
+ }
+ }
+
+ if (addAfer != null)
+ {
+ return new TextSpan(start: addAfer.Info.Span.End, length: 0);
+ }
+
+ // 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 i = 0;
+ bool anyComments = false;
+ int trailingNewLines = 0;
+ for (; i < leadingTrivia.Count; i++)
+ {
+ var trivia = leadingTrivia[i];
+ bool isComment = IsComment(trivia);
+ if (isComment)
+ {
+ anyComments = true;
+ trailingNewLines = 0;
+ }
+
+ bool isEndOfLine = trivia.IsKind(SyntaxKind.EndOfLineTrivia);
+ if (isEndOfLine)
+ {
+ trailingNewLines++;
+ }
+
+ if (!isComment && !isEndOfLine && !trivia.IsKind(SyntaxKind.WhitespaceTrivia))
+ {
+ break;
+ }
+ }
+
+ if (!anyComments)
+ {
+ i = -1;
+ }
+
+ if (i > 0)
+ {
+ var lastCommentOrWhiteSpace = leadingTrivia[i - 1];
+
+ // Add newline after the comment if there is not one already (can happen with block comments).
+ if (!lastCommentOrWhiteSpace.IsKind(SyntaxKind.EndOfLineTrivia))
+ {
+ append.Prefix += NewLine;
+ }
+
+ // Add a blank separating line between the comment and the directive (unless there is already one).
+ if (trailingNewLines < 2)
+ {
+ append.Prefix += NewLine;
+ }
+
+ start = lastCommentOrWhiteSpace.FullSpan.End;
+ }
+
+ // Add a blank line after the directive unless there is already a blank line or another directive before the first C# token.
+ if (!leadingTrivia.Skip(i).Any(static t => t.IsKind(SyntaxKind.EndOfLineTrivia) || t.IsDirective))
+ {
+ append.Suffix += NewLine;
+ }
+
+ return new TextSpan(start: start, length: 0);
+
+ static bool IsComment(SyntaxTrivia trivia)
+ {
+ return trivia.Kind() is SyntaxKind.SingleLineCommentTrivia or SyntaxKind.MultiLineCommentTrivia
+ or SyntaxKind.SingleLineDocumentationCommentTrivia or SyntaxKind.MultiLineDocumentationCommentTrivia;
+ }
+ }
+
+ 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 efdf946c313e..536da09e3972 100644
--- a/src/Cli/dotnet/Commands/Run/RunCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/RunCommand.cs
@@ -134,7 +134,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 0e497bc11d8b..cd757bde7a64 100644
--- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
+++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
@@ -140,15 +140,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()
@@ -809,6 +809,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.
@@ -816,23 +824,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)
@@ -849,13 +850,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()
@@ -865,24 +873,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
{
@@ -908,12 +919,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));
@@ -922,38 +933,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)
@@ -963,12 +991,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;
@@ -978,9 +1006,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();
}
}
@@ -1013,6 +1039,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));
@@ -1034,66 +1078,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);
@@ -1102,12 +1149,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; }
}
@@ -1115,22 +1167,19 @@ 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,
};
@@ -1140,27 +1189,27 @@ public string ToSlashDelimitedString()
{
return Version is null ? Name : $"{Name}/{Version}";
}
+
+ 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
@@ -1169,62 +1218,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))
{
@@ -1238,15 +1287,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}";
}
}
@@ -1298,6 +1348,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;
@@ -1329,3 +1412,12 @@ public RunFileBuildCacheEntry(Dictionary globalProperties)
[JsonSerializable(typeof(RunFileBuildCacheEntry))]
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 fcab6e61ce88..560c4f21714b 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf
@@ -1052,6 +1052,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}.
@@ -1451,10 +1457,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 f70cb9600a52..2f63481ad1d0 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf
@@ -1052,6 +1052,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}"
@@ -1451,10 +1457,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 e349ccd9aae5..dd705fac2fb7 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf
@@ -1052,6 +1052,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}"
@@ -1451,10 +1457,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 9243b52795f5..e30aeb94a9a9 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf
@@ -1052,6 +1052,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} »
@@ -1451,10 +1457,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 a383e84c2284..43ba2d54abfb 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf
@@ -1052,6 +1052,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}'
@@ -1451,10 +1457,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 12b1a2d0d6e0..487a350a0185 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf
@@ -1052,6 +1052,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}'
@@ -1451,10 +1457,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 6f1e0be285b6..19c0626e8a8a 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf
@@ -1052,6 +1052,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}'
@@ -1451,10 +1457,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 950656ce22f2..50a86401dd75 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf
@@ -1052,6 +1052,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}”
@@ -1451,10 +1457,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 d8bb4763fa06..539776c09a24 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf
@@ -1052,6 +1052,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}"
@@ -1451,10 +1457,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 b605c45c2d9e..a1ed5ec96f67 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf
@@ -1052,6 +1052,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}"
@@ -1451,10 +1457,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 7eb51cb17ed3..19277defe445 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf
@@ -1052,6 +1052,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}'
@@ -1451,10 +1457,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 3d1c446d846d..a31e975224d2 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf
@@ -1052,6 +1052,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}'
@@ -1451,10 +1457,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 af3ea934d94f..f458e74cfb0b 100644
--- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf
+++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf
@@ -1052,6 +1052,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}'
@@ -1451,10 +1457,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/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/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs b/test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs
index 4d21d53fad6c..9821706ecf78 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,370 @@ 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();
+ """);
+ }
+
+ [Fact]
+ public void FileBasedApp_ReplaceExisting()
+ {
+ var testInstance = _testAssetsManager.CreateTestDirectory();
+ var file = Path.Join(testInstance.Path, "Program.cs");
+ File.WriteAllText(file, """
+ #:package Humanizer@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/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 8c955857a1f2..ecdbbba69358 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;
@@ -931,6 +930,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.
///
@@ -1152,7 +1198,7 @@ public void Directives_Duplicate()
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();
@@ -1178,8 +1224,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..4ba49d77ed67
--- /dev/null
+++ b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs
@@ -0,0 +1,344 @@
+// 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();
+ """));
+ }
+
+ [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_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 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 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:");
+ Log.WriteLine(expectedOutput);
+ Log.WriteLine("\nActual output:");
+ Log.WriteLine(actualOutput);
+ 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 98b62c75dd06..1d82b203a7e1 100644
--- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
+++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
@@ -1806,7 +1806,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 1e61c0df86d4..14da88f8363e 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 60fbc9bdda7f..721ec47e13c1 100644
--- a/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1
+++ b/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1
@@ -609,6 +609,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.")
)
@@ -651,6 +652,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 fa12d3005129..924599fe36d9 100644
--- a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh
+++ b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh
@@ -596,6 +596,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' \
@@ -641,6 +642,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.: ' \