-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Support variables in #:project directives
#51108
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
Support variables in #:project directives
#51108
Conversation
9ad9ae2 to
bc17aea
Compare
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
Adds support for MSBuild variable expansion inside #:project directives and updates tests and documentation accordingly.
- Introduces directive evaluation pass (EvaluateDirectives) to expand MSBuild variables and resolve project paths.
- Extends CSharpDirective.Project to preserve original and unresolved names for later path adjustments during project conversion.
- Adds tests covering variable usage and updates documentation about variable handling limitations.
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| RunTelemetryTests.cs | Adapts tests to new CSharpDirective.Project constructor. |
| RunFileTests.cs | Adds tests for variable-based project references and malformed variable syntax. |
| DotnetProjectConvertTests.cs | Adds test cases for variable-containing paths; updates expectations for cross-platform separators. |
| VirtualProjectBuildingCommand.cs | Adds directive evaluation, caching of source file, and enhanced project directive handling. |
| ProjectConvertCommand.cs | Integrates directive evaluation and updates path rewrite logic to preserve variable intent. |
| dotnet-run-file.md | Documents variable support and caveats for #:project directives. |
Co-authored-by: Copilot <[email protected]>
|
The linked issue has milestone 10.0.2xx, was this PR meant to target release/10.0.2xx branch? |
|
Definitely, I was worried there might be changes missing in the 2xx branch, but looks like it's fairly up to date wrt file-based app PRs, so I can retarget now. |
This comment was marked as outdated.
This comment was marked as outdated.
|
@RikkiGibson @333fred @MiYanni for reviews, thanks |
src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
Outdated
Show resolved
Hide resolved
src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
Outdated
Show resolved
Hide resolved
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.
LGTM, but, would like to wait to merge until #51373 goes in, if possible, so that I can punt the conflicts over to you :P
I guess there will also be a need to decide whether the analyzer will do the evaluation step. I lean slightly toward yes it should, but, don't feel very strongly at the moment.
| /// the <see cref="UnresolvedName"/> will be expanded, but not resolved | ||
| /// (i.e., can be pointing to project directory or file). | ||
| /// </remarks> | ||
| public string UnresolvedName { get; init; } |
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 feels like it's not straightforward to determine whether certain steps have been performed or not on a given Project instance. I'm concerned that if the Project instance reached a layer of the system which expects expansion and/or resolution to have already been done, when those steps weren't actually performed, it may pass by unnoticed.
Is there anything we can do about that? For example, maybe there should be ExpandedName, ResolvedName etc properties which are just null when that step hasn't been performed on the instance. And the base Name property might just hold "whatever the 'best name' is" for the instance, which appears to be used for deduplication, etc.
Curious if you think this situation is worth doing anything about.
It also looks like properties which refer to the same project, using different original paths, are supposed to be treated as duplicates. I think I filed a bug on that for when path/to/project/ and path/to/project/Project.csproj are both used. But do we also have tests for cases where a variable expansion causes paths $(Prop1)/Project.csproj and $(Prop2)/Project.csproj to be the same, for example?
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 also looks like properties which refer to the same project, using different original paths, are supposed to be treated as duplicates. I think I filed a bug on that for when
path/to/project/andpath/to/project/Project.csprojare both used. But do we also have tests for cases where a variable expansion causes paths$(Prop1)/Project.csprojand$(Prop2)/Project.csprojto be the same, for example?
I will add a test referencing the issue, but I think solving that is part of that issue, so I'd rather do it in a follow up.
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 there anything we can do about that? For example, maybe there should be
ExpandedName,ResolvedNameetc properties which are just null when that step hasn't been performed on the instance. And the baseNameproperty might just hold "whatever the 'best name' is" for the instance, which appears to be used for deduplication, etc.
You're right, that sounds like it will clarify the code a lot; I will try that; thanks!
|
@RikkiGibson for another look, thanks |
| // https://github.com/dotnet/sdk/issues/51487: Behavior should not depend on process current directory | ||
| var sourceDirectory = Path.GetDirectoryName(context.SourceFile.Path) ?? "."; | ||
| // Also normalize backslashes to forward slashes to ensure the directive works on all platforms. | ||
| var sourceDirectory = Path.GetDirectoryName(sourceFile.Path) ?? "."; |
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 we should restore the tracking comment.
| var sourceDirectory = Path.GetDirectoryName(sourceFile.Path) ?? "."; | |
| // https://github.com/dotnet/sdk/issues/51487: Behavior should not depend on process current directory | |
| var sourceDirectory = Path.GetDirectoryName(sourceFile.Path) ?? "."; |
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 don't think this code path depends on the current directory. There are even tests for that - you can run dotnet project convert from any working directory and it should work the same way.
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 seems like if the value "." were used for sourceDirectory here, then the behavior of the following Directory.Exists call, would depend on the process current directory.
If "." is never used, then I think it should be deleted, and perhaps replaced with an assertion that GetDirectoryName return value is non-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.
Ah, you're right, I completely missed that ".", thanks!
I think that's unreachable if sourceFile.Path is a valid file path anyway (Path.GetDirectoryName would return null only for something like / or C:/ afaik), so I will change this to throw.
|
|
||
| // Keep a relative path only if the original directive was a relative path. | ||
| directiveText = ExternalHelpers.IsPathFullyQualified(directiveText) | ||
| directiveText = Path.IsPathFullyQualified(directiveText) |
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 we should revert to release/10.0.2xx version of this line
| directiveText = Path.IsPathFullyQualified(directiveText) | |
| directiveText = ExternalHelpers.IsPathFullyQualified(directiveText) |
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.
Thanks, looks like my mistake when resolving merge conflicts.
| directiveText = Path.IsPathFullyQualified(directiveText) | ||
| ? fullFilePath | ||
| : ExternalHelpers.GetRelativePath(relativeTo: sourceDirectory, fullFilePath); | ||
| : Path.GetRelativePath(relativeTo: sourceDirectory, fullFilePath); |
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 we should revert to release/10.0.2xx version of this line
| : Path.GetRelativePath(relativeTo: sourceDirectory, fullFilePath); | |
| : ExternalHelpers.GetRelativePath(relativeTo: sourceDirectory, fullFilePath); |
I wonder if the last CI build was made before the source package was added to the solution. I would expect the usages in this PR to fail the netstandard2.0 build of the source package.
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 wonder if the last CI build was made before the source package was added to the solution. I would expect the usages in this PR to fail the netstandard2.0 build of the source package.
Yes, looks like that's the case - the CI run is from Nov 5, and the sln was updated Nov 6.
| /// <summary> | ||
| /// If there are any <c>#:project</c> <paramref name="directives"/>, expand <c>$()</c> in them and then resolve the project paths. | ||
| /// </summary> | ||
| public static ImmutableArray<CSharpDirective> EvaluateDirectives( |
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.
When this change is consumed on the Roslyn side, I think the Roslyn analyzer should call this method also, to avoid a change in behavior. See also dotnet/roslyn#80575 (comment)
| /// </summary> | ||
| public Project ResolveProjectPath(SourceFile sourceFile, DiagnosticBag diagnostics) | ||
| { | ||
| var directiveText = Name; |
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: would be good to rename this to something like resolvedName
Closes #49286.