-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Implement #:package
manipulation for file-based apps
#49635
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR extends the dotnet package
commands to support file-based C# apps by introducing a --file
option, updating parsing/completion, and adding source‐editor logic to manipulate #:package
directives.
- Introduces
--file
alongside--project
in parsers and completion scripts - Implements add/remove of
#:package
directives inFileBasedAppSourceEditor
and wiring inPackageAddCommand
/PackageRemoveCommand
- Adds extensive unit tests and updates documentation for the new file-based scenarios
Reviewed Changes
Copilot reviewed 57 out of 57 changed files in this pull request and generated 2 comments.
Show a summary per file
File | Description |
---|---|
src/Cli/dotnet/Commands/Package/PackageCommandParser.cs | Adds FileOption and path‐processing logic for file-based apps |
src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs | Implements file‐based add logic, including CPM and restore support |
src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs | Implements file‐based remove logic and reports removed directives |
src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs | Refactors diagnostics handling and directive loading for files |
src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs | New editor for inserting/removing #:package directives |
src/Cli/dotnet/Commands/Run/RunCommand.cs | Updates error message constant for invalid option combinations |
Comments suppressed due to low confidence (1)
src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs:105
- Typo in variable name
addAfer
—it should be spelledaddAfter
to match the intended meaning.
CSharpDirective? addAfer = null;
@dotnet/run-file for reviews, thanks |
@RikkiGibson @chsienki for reviews, thanks |
test/dotnet.Tests/CommandTests/Package/Add/GivenDotnetPackageAdd.cs
Outdated
Show resolved
Hide resolved
@RikkiGibson @jaredpar for another look, thanks |
@RikkiGibson @333fred for reviews, thanks |
} | ||
if (_arguments.Count != 1) | ||
|
||
var packageToRemove = arguments.Single(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can you just index? Or call First
? No need for assertions around the length, we just checked that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can use pattern matching. Thanks.
trailingNewLines = 0; | ||
} | ||
|
||
bool isEndOfLine = trivia.IsKind(SyntaxKind.EndOfLineTrivia); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure you're testing with a documentation comment at the top of the file, as well as things that will generate SkippedTokensTrivia. See https://github.com/dotnet/razor/blob/main/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Legacy/RoslynCSharpTokenizer.cs#L503 and surrounding cases for some examples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there tests for files with comments at the top?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, see test Comments
.
""" | ||
/* test */ | ||
|
||
#:package MyPackage@1.0.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a bit subjective, but I'd rather see:
#:package MyPackage@1.0.0
/* test */ Console.WriteLine();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -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!); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the order of the declarations is the only reason the suppressions are needed. That seems unfortunate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I can reorder, thanks.
if (!hasVersion) | ||
{ | ||
skipUpdate = true; | ||
return (Revert: NoOp, Update: Unreachable, Save: Revert); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am finding the flow here pretty wonky. It feels like some of the cases update the file on disk before we know whether that's what we should really do, and other cases wait to confirm that's what we should do before doing the update. Does it have to be that way?
I was also kinda hoping that the case where we need to update a Directory.Packages.props file could share more with the project-based version of the command. I guess that is not practical to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that once we have NuGet/Home#14390, lots of this logic can go away. But I didn't want to block the basic experience on that.
? (projectOrFile, AppKinds.Any) | ||
: (Environment.CurrentDirectory, AppKinds.ProjectBased), | ||
(true, false) => (parseResult.GetValue(FileOption)!, AppKinds.FileBased), | ||
(false, true) => (parseResult.GetValue(ProjectOption)!, AppKinds.ProjectBased), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So is the "optionality" of an Option<T>
indicated by giving it a nullable type argument? I am just curious if something would change for the worse here if FileOption
and ProjectOption
were Option<string>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wouldn't make a difference, the GetValue
returns nullable T
no matter what the original T
is:
public T? GetValue<T>(Option<T> option)
return new FileBasedAppSourceEditor | ||
{ | ||
SourceFile = sourceFile, | ||
Directives = LoadDirectives(sourceFile), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if Directives is just initialized based on value of SourceFile
, would it be cleaner to delete the setter for it and just rely on it being initialized on access?
|
||
public void Add(CSharpDirective directive) | ||
{ | ||
TextSpan span = DetermineWhereToAdd(directive, out var append); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Roslyn has a type TextChange
, which consists of a TextSpan
and string NewText
, and SourceText.WithChanges()
can take them to created edited versions of source files. Maybe it would be a bit cleaner to use that here?
|
||
// 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addAfter
?
.Should().Pass(); | ||
|
||
File.ReadAllText(file).Should().Be(""" | ||
#:package Humanizer@* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this consistent with the behavior for ordinary projects?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, <PackageReference Include="Humanizer" Version="*" />
gets added if you don't specify a version and use the --no-restore
flag.
Part of #49200. This PR implements
dotnet package add
anddotnet package remove
.When more logic is shared with NuGet, more
package
-related commands can be implemented and some of the logic here simplified. See NuGet/Home#14390.I've modeled the commands around their project-based counterparts.
package remove
is relatively simple - corresponding#:package
directives are removed from the C# file,Directory.Packages.props
is not considered at all.package add
needs to updateDirectory.Packages.props
if Central Package Management is enabled. It also needs to perform restore. If no version is specified by the user on the command-line, it needs to determine the latest version and put that into the package reference directive.