Skip to content

Commit 6e11b41

Browse files
author
Jason Zhai
committed
Merge branch 'main' of https://github.com/dotnet/sdk into darc-main-d77c9975-6517-44d2-bafd-8a16b64c8d6e
2 parents 1b10aaf + 34ae4f2 commit 6e11b41

File tree

58 files changed

+686
-385
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+686
-385
lines changed

documentation/general/dotnet-run-file.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ We want to report an error for non-entry-point files to avoid the confusion of b
7171

7272
Internally, the SDK CLI detects entry points by parsing all `.cs` files in the directory tree of the entry point file with default parsing options (in particular, no `<DefineConstants>`)
7373
and checking which ones contain top-level statements (`Main` methods are not supported for now as that would require full semantic analysis, not just parsing).
74-
Results of this detection are used to exclude other entry points from [builds](#multiple-entry-points) and [app directive collection](#directives-for-project-metadata).
74+
Results of this detection are used to exclude other entry points from [builds](#multiple-entry-points) and [file-level directive collection](#directives-for-project-metadata).
7575
This means the CLI might consider a file to be an entry point which later the compiler doesn't
7676
(for example because its top-level statements are under `#if !SYMBOL` and the build has `DefineConstants=SYMBOL`).
7777
However such inconsistencies should be rare and hence that is a better trade off than letting the compiler decide which files are entry points
78-
because that could require multiple builds (first determine entry points and then re-build with app directives except those from other entry points).
78+
because that could require multiple builds (first determine entry points and then re-build with file-level directives except those from other entry points).
7979
To avoid parsing all C# files twice (in CLI and in the compiler), the CLI could use the compiler server for parsing so the trees are reused
8080
(unless the parse options change via the directives), and also [cache](#optimizations) the results to avoid parsing on subsequent runs.
8181

@@ -146,7 +146,7 @@ They are not cleaned immediately because they can be re-used on subsequent runs
146146

147147
## Directives for project metadata
148148

149-
It is possible to specify some project metadata via *app directives*
149+
It is possible to specify some project metadata via *file-level directives*
150150
which are [ignored][ignored-directives] by the C# language but recognized by the SDK CLI.
151151
Directives `sdk`, `package`, and `property` are translated into `<Project Sdk="...">`, `<PackageReference>`, and `<Property>` project elements, respectively.
152152
Other directives result in an error, reserving them for future use.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Guide to snapshot-based testing in the .NET SDK
2+
3+
Snapshot-based testing is a technique used in the .NET SDK to ensure that the command-line interface (CLI) behaves as expected. This document provides an overview of how snapshot-based testing works, particularly for CLI completions, and how to manage and update snapshots.
4+
5+
## CLI Completions Snapshot Testing
6+
7+
The point of the tests is to keep an eye on the CLI, since it is our user interface. When the CLI changes in a way that impacts completions (new commands, options, defaults) we need to inspect and reconcile the baselines. Most of this happens in the [dotnet.Tests][dotnet.Tests] project, in the [Microsoft.DotNet.Cli.Completions.Tests.DotnetCliSnapshotTests][snapshot-tests] tests.
8+
9+
These tests use [Verify][Verify] to perform snapshot testing - storing the results of a known good 'baseline' as a snapshot and comparing the output of the same action performed with changes in your PR. Verify calls these baselines 'verified' files. When the test computes a new snapshot for comparison, this is called a 'received' file. Verify compares the 'verified' file to the 'received' file for each test, and if they are not the same provides a git-diff in the console output for the test.
10+
11+
To fix these tests, you need to diff the two files and visually inspect the changes. If the changes to the 'received' file are what you want to see (new commands, new options, renames, etc) then you rename the 'received' file to 'verified' and commit that change to the 'verified' file. There are two MSBuild Targets on the dotnet.Tests project that you can use to help you do this, both of which are intended to be run after you run the snapshot tests locally:
12+
13+
* [CompareCliSnapshots][compare] - this Target copies the .received. files from the artifacts directory, where they are created due to the way we run tests, to the [snapshots][snapshots] directory in the dotnet.Tests project. This makes it much easier to diff the two.
14+
* [UpdateCliSnapshots][update] - this Target renames the .received. files to .verified. in the local [snapshots][snapshots] directory, and so acts as a giant 'I accept these changes' button. Only use this if you've diffed the snapshots and are sure they match your expectations.
15+
16+
[dotnet.Tests]: ../../test/dotnet.Tests/
17+
[snapshot-tests]: ../../test/dotnet.Tests/CompletionTests/DotnetCliSnapshotTests.cs
18+
[snapshots]: ../../test/dotnet.Tests/CompletionTests/snapshots/
19+
[Verify]: https://github.com/VerifyTests/Verify
20+
[compare]: ../../test/dotnet.Tests/dotnet.Tests.csproj#L100
21+
[update]: ../../test/dotnet.Tests/dotnet.Tests.csproj#L107

src/Cli/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,9 @@ public ICommand SetCommandArgs(string commandArgs)
181181
{
182182
throw new NotImplementedException();
183183
}
184+
185+
public ICommand StandardOutputEncoding(Encoding encoding)
186+
{
187+
throw new NotImplementedException();
188+
}
184189
}

src/Cli/Microsoft.DotNet.Cli.Utils/Command.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ public ICommand EnvironmentVariable(string name, string? value)
101101
return this;
102102
}
103103

104+
public ICommand StandardOutputEncoding(Encoding encoding)
105+
{
106+
_process.StartInfo.StandardOutputEncoding = encoding;
107+
return this;
108+
}
109+
104110
public ICommand CaptureStdOut()
105111
{
106112
ThrowIfRunning();

src/Cli/Microsoft.DotNet.Cli.Utils/ICommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public interface ICommand
2525

2626
ICommand SetCommandArgs(string commandArgs);
2727

28+
ICommand StandardOutputEncoding(Encoding encoding);
29+
2830
string CommandName { get; }
2931

3032
string CommandArgs { get; }

src/Cli/dotnet/CliStrings.resx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -577,8 +577,8 @@ The default is 'false.' However, when targeting .NET 7 or lower, the default is
577577
<value>outputpathresolver: {0} does not exist</value>
578578
</data>
579579
<data name="CannotFindAManifestFile" xml:space="preserve">
580-
<value>Cannot find a manifest file.
581-
For a list of locations searched, specify the "-d" option before the tool name.</value>
580+
<value>Cannot find a manifest file. The list of searched paths:
581+
{0}</value>
582582
</data>
583583
<data name="CannotFindPackageIdInManifest" xml:space="preserve">
584584
<value>Cannot find a package with the package id {0} in the manifest file.</value>
@@ -670,10 +670,6 @@ For a list of locations searched, specify the "-d" option before the tool name.<
670670
<data name="LibraryNotFoundInLockFile" xml:space="preserve">
671671
<value>{0}: library not found in lock file.</value>
672672
</data>
673-
<data name="ListOfSearched" xml:space="preserve">
674-
<value>The list of searched paths:
675-
{0}</value>
676-
</data>
677673
<data name="LookingForPreferCliRuntimeFile" xml:space="preserve">
678674
<value>{0}: Looking for prefercliruntime file at `{1}`</value>
679675
</data>

src/Cli/dotnet/Commands/CliCommandStrings.resx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,6 +1513,10 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man
15131513
<value>Some directives cannot be converted: the first error is at {0}. Run the file to see all compilation errors. Specify '--force' to convert anyway.</value>
15141514
<comment>{Locked="--force"}. {0} is the file path and line number.</comment>
15151515
</data>
1516+
<data name="DuplicateDirective" xml:space="preserve">
1517+
<value>Duplicate directives are not supported: {0} at {1}</value>
1518+
<comment>{0} is the directive type and name. {1} is the file path and line number.</comment>
1519+
</data>
15161520
<data name="InvalidOptionCombination" xml:space="preserve">
15171521
<value>Cannot combine option '{0}' and '{1}'.</value>
15181522
<comment>{0} and {1} are option names like '--no-build'.</comment>
@@ -1975,10 +1979,6 @@ Tool '{1}' (version '{2}') was successfully installed.</value>
19751979
<data name="ToolInstallManifestPathOptionName" xml:space="preserve">
19761980
<value>PATH</value>
19771981
</data>
1978-
<data name="ToolInstallNoManifestGuide" xml:space="preserve">
1979-
<value>If you intended to install a global tool, add `--global` to the command.
1980-
If you would like to create a manifest, use the `--create-manifest-if-needed` flag with the `dotnet tool install` command, or use `dotnet new tool-manifest`, usually in the repo root directory.</value>
1981-
</data>
19821982
<data name="ToolInstallNuGetConfigurationFileDoesNotExist" xml:space="preserve">
19831983
<value>NuGet configuration file '{0}' does not exist.</value>
19841984
</data>

src/Cli/dotnet/Commands/Run/RunCommand.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,14 @@ public int Execute()
110110
}
111111
else
112112
{
113-
if (EntryPointFileFullPath is not null)
113+
if (NoCache)
114114
{
115-
projectFactory = CreateVirtualCommand().PrepareProjectInstance().CreateProjectInstance;
115+
throw new GracefulException(CliCommandStrings.InvalidOptionCombination, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoBuildOption.Name);
116116
}
117117

118-
if (NoCache)
118+
if (EntryPointFileFullPath is not null)
119119
{
120-
throw new GracefulException(CliCommandStrings.InvalidOptionCombination, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoBuildOption.Name);
120+
projectFactory = CreateVirtualCommand().PrepareProjectInstance().CreateProjectInstance;
121121
}
122122
}
123123

src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ public VirtualProjectBuildingCommand(
8787
public override int Execute()
8888
{
8989
Debug.Assert(!(NoRestore && NoBuild));
90-
9190
var consoleLogger = RunCommand.MakeTerminalLogger(Verbosity);
9291
var binaryLogger = GetBinaryLogger(BinaryLoggerArgs);
9392

@@ -97,11 +96,6 @@ public override int Execute()
9796
{
9897
if (NoCache)
9998
{
100-
if (NoRestore)
101-
{
102-
throw new GracefulException(CliCommandStrings.InvalidOptionCombination, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoRestoreOption.Name);
103-
}
104-
10599
cacheEntry = ComputeCacheEntry(out _);
106100
}
107101
else if (!NeedsToBuild(out cacheEntry))
@@ -625,6 +619,15 @@ public static void WriteProjectFile(
625619
626620
""");
627621

622+
var targetDirectory = Path.GetDirectoryName(targetFilePath) ?? "";
623+
writer.WriteLine($"""
624+
<ItemGroup>
625+
<RuntimeHostConfigurationOption Include="EntryPointFilePath" Value="{EscapeValue(targetFilePath)}" />
626+
<RuntimeHostConfigurationOption Include="EntryPointFileDirectoryPath" Value="{EscapeValue(targetDirectory)}" />
627+
</ItemGroup>
628+
629+
""");
630+
628631
foreach (var sdk in sdkDirectives)
629632
{
630633
WriteImport(writer, "Sdk.targets", sdk);
@@ -709,6 +712,7 @@ public static ImmutableArray<CSharpDirective> FindDirectives(SourceFile sourceFi
709712
{
710713
#pragma warning disable RSEXPERIMENTAL003 // 'SyntaxTokenParser' is experimental
711714

715+
var deduplicated = new HashSet<CSharpDirective.Named>(NamedDirectiveComparer.Instance);
712716
var builder = ImmutableArray.CreateBuilder<CSharpDirective>();
713717
SyntaxTokenParser tokenizer = SyntaxFactory.CreateTokenParser(sourceFile.Text,
714718
CSharpParseOptions.Default.WithFeatures([new("FileBasedProgram", "true")]));
@@ -750,6 +754,28 @@ public static ImmutableArray<CSharpDirective> FindDirectives(SourceFile sourceFi
750754

751755
if (CSharpDirective.Parse(errors, sourceFile, span, name.ToString(), value.ToString()) is { } directive)
752756
{
757+
// If the directive is already present, report an error.
758+
if (deduplicated.TryGetValue(directive, out var existingDirective))
759+
{
760+
var typeAndName = $"#:{existingDirective.GetType().Name.ToLowerInvariant()} {existingDirective.Name}";
761+
if (errors != null)
762+
{
763+
errors.Add(new SimpleDiagnostic
764+
{
765+
Location = sourceFile.GetFileLinePositionSpan(directive.Span),
766+
Message = string.Format(CliCommandStrings.DuplicateDirective, typeAndName, sourceFile.GetLocationString(directive.Span)),
767+
});
768+
}
769+
else
770+
{
771+
throw new GracefulException(CliCommandStrings.DuplicateDirective, typeAndName, sourceFile.GetLocationString(directive.Span));
772+
}
773+
}
774+
else
775+
{
776+
deduplicated.Add(directive);
777+
}
778+
753779
builder.Add(directive);
754780
}
755781
}
@@ -872,7 +898,8 @@ internal static partial class Patterns
872898
}
873899

874900
/// <summary>
875-
/// Represents a C# directive starting with <c>#:</c>. Those are ignored by the language but recognized by us.
901+
/// Represents a C# directive starting with <c>#:</c> (a.k.a., "file-level directive").
902+
/// Those are ignored by the language but recognized by us.
876903
/// </summary>
877904
internal abstract class CSharpDirective
878905
{
@@ -883,14 +910,14 @@ private CSharpDirective() { }
883910
/// </summary>
884911
public required TextSpan Span { get; init; }
885912

886-
public static CSharpDirective? Parse(ImmutableArray<SimpleDiagnostic>.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText)
913+
public static Named? Parse(ImmutableArray<SimpleDiagnostic>.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText)
887914
{
888915
return directiveKind switch
889916
{
890917
"sdk" => Sdk.Parse(errors, sourceFile, span, directiveKind, directiveText),
891918
"property" => Property.Parse(errors, sourceFile, span, directiveKind, directiveText),
892919
"package" => Package.Parse(errors, sourceFile, span, directiveKind, directiveText),
893-
_ => ReportError<CSharpDirective>(errors, sourceFile, span, string.Format(CliCommandStrings.UnrecognizedDirective, directiveKind, sourceFile.GetLocationString(span))),
920+
_ => ReportError<Named>(errors, sourceFile, span, string.Format(CliCommandStrings.UnrecognizedDirective, directiveKind, sourceFile.GetLocationString(span))),
894921
};
895922
}
896923

@@ -933,14 +960,18 @@ private static (string, string?)? ParseOptionalTwoParts(ImmutableArray<SimpleDia
933960
/// </summary>
934961
public sealed class Shebang : CSharpDirective;
935962

963+
public abstract class Named : CSharpDirective
964+
{
965+
public required string Name { get; init; }
966+
}
967+
936968
/// <summary>
937969
/// <c>#:sdk</c> directive.
938970
/// </summary>
939-
public sealed class Sdk : CSharpDirective
971+
public sealed class Sdk : Named
940972
{
941973
private Sdk() { }
942974

943-
public required string Name { get; init; }
944975
public string? Version { get; init; }
945976

946977
public static new Sdk? Parse(ImmutableArray<SimpleDiagnostic>.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText)
@@ -967,11 +998,10 @@ public string ToSlashDelimitedString()
967998
/// <summary>
968999
/// <c>#:property</c> directive.
9691000
/// </summary>
970-
public sealed class Property : CSharpDirective
1001+
public sealed class Property : Named
9711002
{
9721003
private Property() { }
9731004

974-
public required string Name { get; init; }
9751005
public required string Value { get; init; }
9761006

9771007
public static new Property? Parse(ImmutableArray<SimpleDiagnostic>.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText)
@@ -1007,13 +1037,12 @@ private Property() { }
10071037
/// <summary>
10081038
/// <c>#:package</c> directive.
10091039
/// </summary>
1010-
public sealed class Package : CSharpDirective
1040+
public sealed class Package : Named
10111041
{
10121042
private static readonly SearchValues<char> s_separators = SearchValues.Create(' ', '@');
10131043

10141044
private Package() { }
10151045

1016-
public required string Name { get; init; }
10171046
public string? Version { get; init; }
10181047

10191048
public static new Package? Parse(ImmutableArray<SimpleDiagnostic>.Builder? errors, SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText)
@@ -1033,6 +1062,33 @@ private Package() { }
10331062
}
10341063
}
10351064

1065+
/// <summary>
1066+
/// Used for deduplication - compares directives by their type and name (ignoring case).
1067+
/// </summary>
1068+
internal sealed class NamedDirectiveComparer : IEqualityComparer<CSharpDirective.Named>
1069+
{
1070+
public static readonly NamedDirectiveComparer Instance = new();
1071+
1072+
private NamedDirectiveComparer() { }
1073+
1074+
public bool Equals(CSharpDirective.Named? x, CSharpDirective.Named? y)
1075+
{
1076+
if (ReferenceEquals(x, y)) return true;
1077+
1078+
if (x is null || y is null) return false;
1079+
1080+
return x.GetType() == y.GetType() &&
1081+
string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
1082+
}
1083+
1084+
public int GetHashCode(CSharpDirective.Named obj)
1085+
{
1086+
return HashCode.Combine(
1087+
obj.GetType().GetHashCode(),
1088+
obj.Name.GetHashCode(StringComparison.OrdinalIgnoreCase));
1089+
}
1090+
}
1091+
10361092
internal sealed class SimpleDiagnostic
10371093
{
10381094
public required Position Location { get; init; }

src/Cli/dotnet/Commands/Tool/Install/ToolInstallCommandParser.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ internal static class ToolInstallCommandParser
4949
public static readonly Option<bool> CreateManifestIfNeededOption = new("--create-manifest-if-needed")
5050
{
5151
Description = CliCommandStrings.CreateManifestIfNeededOptionDescription,
52-
Arity = ArgumentArity.Zero
52+
Arity = ArgumentArity.ZeroOrOne,
53+
DefaultValueFactory = _ => true,
5354
};
5455

5556
public static readonly Option<bool> AllowPackageDowngradeOption = new("--allow-downgrade")

0 commit comments

Comments
 (0)