Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions documentation/general/dotnet-run-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,16 @@ The directives are processed as follows:
(because `ProjectReference` items don't support directory paths).
An error is reported if zero or more than one projects are found in the directory, just like `dotnet reference add` would do.

Directive values support MSBuild variables (like `$(..)`) normally as they are translated literally and left to MSBuild engine to process.
However, in `#:project` directives, variables might not be preserved during [grow up](#grow-up),
because there is additional processing of those directives that makes it technically challenging to preserve variables in all cases
(project directive values need to be resolved to be relative to the target directory
and also to point to a project file rather than a directory).
Note that it is not expected that variables inside the path change their meaning during the conversion,
so for example `#:project ../$(LibName)` is translated to `<ProjectReference Include="../../$(LibName)/Lib.csproj" />` (i.e., the variable is preserved).
However, variables at the start can change, so for example `#:project $(ProjectDir)../Lib` is translated to `<ProjectReference Include="../../Lib/Lib.csproj" />` (i.e., the variable is expanded).
In other directives, all variables are preserved during conversion.

Because these directives are limited by the C# language to only appear before the first "C# token" and any `#if`,
dotnet CLI can look for them via a regex or Roslyn lexer without any knowledge of defined conditional symbols
and can do that efficiently by stopping the search when it sees the first "C# token".
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.Build.Execution;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand Down Expand Up @@ -225,6 +227,30 @@ static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int in
}
}
}

/// <summary>
/// If there are any <c>#:project</c> <paramref name="directives"/>, expands <c>$()</c> in them and ensures they point to project files (not directories).
/// </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)

ProjectInstance? project,
ImmutableArray<CSharpDirective> directives,
SourceFile sourceFile,
DiagnosticBag diagnostics)
{
if (directives.OfType<CSharpDirective.Project>().Any())
{
return directives
.Select(d => d is CSharpDirective.Project p
? (project is null
? p
: p.WithName(project.ExpandString(p.Name), CSharpDirective.Project.NameKind.Expanded))
.EnsureProjectFilePath(sourceFile, diagnostics)
: d)
.ToImmutableArray();
}

return directives;
}
}

internal readonly record struct SourceFile(string Path, SourceText Text)
Expand Down Expand Up @@ -457,8 +483,32 @@ public sealed class Package(in ParseInfo info) : Named(info)
/// <summary>
/// <c>#:project</c> directive.
/// </summary>
public sealed class Project(in ParseInfo info) : Named(info)
public sealed class Project : Named
{
[SetsRequiredMembers]
public Project(in ParseInfo info, string name) : base(info)
{
Name = name;
OriginalName = name;
}

/// <summary>
/// Preserved across <see cref="WithName"/> calls, i.e.,
/// this is the original directive text as entered by the user.
/// </summary>
public string OriginalName { get; init; }

/// <summary>
/// This is the <see cref="OriginalName"/> with MSBuild <c>$(..)</c> vars expanded (via <see cref="ProjectInstance.ExpandString"/>).
/// </summary>
public string? ExpandedName { get; init; }

/// <summary>
/// This is the <see cref="ExpandedName"/> resolved via <see cref="EnsureProjectFilePath"/>
/// (i.e., this is a file path if the original text pointed to a directory).
/// </summary>
public string? ProjectFilePath { get; init; }

public static new Project? Parse(in ParseContext context)
{
var directiveText = context.DirectiveText;
Expand All @@ -468,19 +518,57 @@ public sealed class Project(in ParseInfo info) : Named(info)
return context.Diagnostics.AddError<Project?>(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind));
}

return new Project(context.Info, directiveText);
}

public enum NameKind
{
/// <summary>
/// Change <see cref="Named.Name"/> and <see cref="ExpandedName"/>.
/// </summary>
Expanded = 1,

/// <summary>
/// Change <see cref="Named.Name"/> and <see cref="Project.ProjectFilePath"/>.
/// </summary>
ProjectFilePath = 2,

/// <summary>
/// Change only <see cref="Named.Name"/>.
/// </summary>
Final = 3,
}

public Project WithName(string name, NameKind kind)
{
return new Project(Info, name)
{
OriginalName = OriginalName,
ExpandedName = kind == NameKind.Expanded ? name : ExpandedName,
ProjectFilePath = kind == NameKind.ProjectFilePath ? name : ProjectFilePath,
};
}

/// <summary>
/// If the directive points to a directory, returns a new directive pointing to the corresponding project file.
/// </summary>
public Project EnsureProjectFilePath(SourceFile sourceFile, DiagnosticBag diagnostics)
{
var resolvedName = Name;

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.
// https://github.com/dotnet/sdk/issues/51487: Behavior should not depend on process current directory
var sourceDirectory = Path.GetDirectoryName(context.SourceFile.Path) ?? ".";
var resolvedProjectPath = Path.Combine(sourceDirectory, directiveText.Replace('\\', '/'));
// Also normalize backslashes to forward slashes to ensure the directive works on all platforms.
var sourceDirectory = Path.GetDirectoryName(sourceFile.Path)
?? throw new InvalidOperationException($"Source file path '{sourceFile.Path}' does not have a containing directory.");
var resolvedProjectPath = Path.Combine(sourceDirectory, resolvedName.Replace('\\', '/'));
if (Directory.Exists(resolvedProjectPath))
{
var fullFilePath = GetProjectFileFromDirectory(resolvedProjectPath).FullName;

// Keep a relative path only if the original directive was a relative path.
directiveText = ExternalHelpers.IsPathFullyQualified(directiveText)
resolvedName = ExternalHelpers.IsPathFullyQualified(resolvedName)
? fullFilePath
: ExternalHelpers.GetRelativePath(relativeTo: sourceDirectory, fullFilePath);
}
Expand All @@ -491,22 +579,14 @@ public sealed class Project(in ParseInfo info) : Named(info)
}
catch (GracefulException e)
{
context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.InvalidProjectDirective, e.Message), e);
diagnostics.AddError(sourceFile, Info.Span, string.Format(FileBasedProgramsResources.InvalidProjectDirective, e.Message), e);
}

return new Project(context.Info)
{
Name = directiveText,
};
}

public Project WithName(string name)
{
return new Project(Info) { Name = name };
return WithName(resolvedName, NameKind.ProjectFilePath);
}

// https://github.com/dotnet/sdk/issues/51487: Delete copies of methods from MsbuildProject and MSBuildUtilities from the source package, sharing the original method(s) under src/Cli instead.
public static FileInfo GetProjectFileFromDirectory(string projectDirectory)
private static FileInfo GetProjectFileFromDirectory(string projectDirectory)
{
DirectoryInfo dir;
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,19 @@ Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.Span.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.TrailingWhiteSpace.get -> Microsoft.DotNet.FileBasedPrograms.WhiteSpaceInfo
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo.TrailingWhiteSpace.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Project(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.WithName(string! name) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project!
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.EnsureProjectFilePath(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, Microsoft.DotNet.FileBasedPrograms.DiagnosticBag diagnostics) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project!
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ExpandedName.get -> string?
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ExpandedName.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind.Expanded = 1 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind.Final = 3 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind.ProjectFilePath = 2 -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.OriginalName.get -> string!
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.OriginalName.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Project(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info, string! name) -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ProjectFilePath.get -> string?
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.ProjectFilePath.init -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.WithName(string! name, Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.NameKind kind) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project!
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Property(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseInfo info) -> void
Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Value.get -> string!
Expand Down Expand Up @@ -102,7 +113,6 @@ override Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Shebang.ToString() -
override Microsoft.DotNet.FileBasedPrograms.SourceFile.GetHashCode() -> int
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Package?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Named?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.GetProjectFileFromDirectory(string! projectDirectory) -> System.IO.FileInfo!
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Project?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Property?
static Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk.Parse(in Microsoft.DotNet.FileBasedPrograms.CSharpDirective.ParseContext context) -> Microsoft.DotNet.FileBasedPrograms.CSharpDirective.Sdk?
Expand All @@ -113,6 +123,7 @@ static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.CombineHashCodes(int v
static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.GetRelativePath(string! relativeTo, string! path) -> string!
static Microsoft.DotNet.FileBasedPrograms.ExternalHelpers.IsPathFullyQualified(string! path) -> bool
static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.CreateTokenizer(Microsoft.CodeAnalysis.Text.SourceText! text) -> Microsoft.CodeAnalysis.CSharp.SyntaxTokenParser!
static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.EvaluateDirectives(Microsoft.Build.Execution.ProjectInstance? project, System.Collections.Immutable.ImmutableArray<Microsoft.DotNet.FileBasedPrograms.CSharpDirective!> directives, Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, Microsoft.DotNet.FileBasedPrograms.DiagnosticBag diagnostics) -> System.Collections.Immutable.ImmutableArray<Microsoft.DotNet.FileBasedPrograms.CSharpDirective!>
static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.FindDirectives(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, bool reportAllErrors, Microsoft.DotNet.FileBasedPrograms.DiagnosticBag diagnostics) -> System.Collections.Immutable.ImmutableArray<Microsoft.DotNet.FileBasedPrograms.CSharpDirective!>
static Microsoft.DotNet.FileBasedPrograms.FileLevelDirectiveHelpers.FindLeadingDirectives(Microsoft.DotNet.FileBasedPrograms.SourceFile sourceFile, Microsoft.CodeAnalysis.SyntaxTriviaList triviaList, Microsoft.DotNet.FileBasedPrograms.DiagnosticBag diagnostics, System.Collections.Immutable.ImmutableArray<Microsoft.DotNet.FileBasedPrograms.CSharpDirective!>.Builder? builder) -> void
static Microsoft.DotNet.FileBasedPrograms.MSBuildUtilities.ConvertStringToBool(string? parameterValue, bool defaultValue = false) -> bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Contracts" />
<PackageReference Include="Microsoft.Build" />
<PackageReference Include="Microsoft.CodeAnalysis.Contracts" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="System.Text.Json" VersionOverride="$(SystemTextJsonToolsetPackageVersion)" />
<PackageReference Include="System.Text.Encoding.CodePages" VersionOverride="$(SystemTextEncodingCodePagesToolsetPackageVersion)" />
Expand Down
Loading
Loading