Skip to content

Conversation

@jjonescz
Copy link
Member

@jjonescz jjonescz commented Oct 2, 2025

Closes #49286.

@jjonescz jjonescz added the Area-run-file Items related to the "dotnet run <file>" effort label Oct 2, 2025
@jjonescz jjonescz force-pushed the sprint-project-vars branch from 9ad9ae2 to bc17aea Compare October 2, 2025 08:52
Copy link
Contributor

Copilot AI left a 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]>
@jjonescz jjonescz marked this pull request as ready for review October 2, 2025 14:49
@jjonescz jjonescz requested a review from a team October 2, 2025 14:49
@RikkiGibson
Copy link
Member

The linked issue has milestone 10.0.2xx, was this PR meant to target release/10.0.2xx branch?

@jjonescz jjonescz changed the base branch from release/10.0.1xx to release/10.0.2xx October 2, 2025 18:36
@jjonescz
Copy link
Member Author

jjonescz commented Oct 2, 2025

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.

@jjonescz

This comment was marked as outdated.

@jjonescz jjonescz mentioned this pull request Oct 3, 2025
@jjonescz jjonescz added this to the 10.0.2xx milestone Oct 3, 2025
@dotnet dotnet deleted a comment from azure-pipelines bot Oct 3, 2025
@jjonescz
Copy link
Member Author

jjonescz commented Oct 8, 2025

@RikkiGibson @333fred @MiYanni for reviews, thanks

Copy link
Member

@RikkiGibson RikkiGibson left a 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; }
Copy link
Member

@RikkiGibson RikkiGibson Oct 31, 2025

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?

Copy link
Member Author

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/ 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?

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.

Copy link
Member Author

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, 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.

You're right, that sounds like it will clarify the code a lot; I will try that; thanks!

@jjonescz
Copy link
Member Author

@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) ?? ".";
Copy link
Member

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.

Suggested change
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) ?? ".";

Copy link
Member Author

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.

Copy link
Member

@RikkiGibson RikkiGibson Nov 13, 2025

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.

Copy link
Member Author

@jjonescz jjonescz Nov 14, 2025

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)
Copy link
Member

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

Suggested change
directiveText = Path.IsPathFullyQualified(directiveText)
directiveText = ExternalHelpers.IsPathFullyQualified(directiveText)

Copy link
Member Author

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);
Copy link
Member

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

Suggested change
: 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.

Copy link
Member Author

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(
Copy link
Member

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;
Copy link
Member

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

@jjonescz jjonescz merged commit d04687c into dotnet:release/10.0.2xx Nov 14, 2025
22 of 25 checks passed
@jjonescz jjonescz deleted the sprint-project-vars branch November 14, 2025 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-run-file Items related to the "dotnet run <file>" effort

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants