Skip to content

Commit acbb547

Browse files
authored
Fix and test more dotnet file precedence cases (#49510)
1 parent 474a661 commit acbb547

File tree

3 files changed

+186
-38
lines changed

3 files changed

+186
-38
lines changed

src/Cli/dotnet/CommandFactory/CommandFactoryUsingResolver.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ public static Command Create(
7474
applicationName: applicationName,
7575
currentWorkingDirectory: currentWorkingDirectory);
7676

77+
return CreateOrThrow(commandName, commandSpec);
78+
}
79+
80+
#nullable enable
81+
public static Command CreateOrThrow(string commandName, CommandSpec? commandSpec)
82+
{
7783
if (commandSpec == null)
7884
{
7985
if (_knownCommandsAvailableAsDotNetTool.Contains(commandName, StringComparer.OrdinalIgnoreCase))
@@ -90,6 +96,7 @@ public static Command Create(
9096

9197
return command;
9298
}
99+
#nullable disable
93100

94101
public static Command Create(CommandSpec commandSpec)
95102
{

src/Cli/dotnet/Program.cs

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.CommandLine.Parsing;
88
using System.Diagnostics;
99
using Microsoft.DotNet.Cli.CommandFactory;
10+
using Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
1011
using Microsoft.DotNet.Cli.Commands.Run;
1112
using Microsoft.DotNet.Cli.Commands.Workload;
1213
using Microsoft.DotNet.Cli.Extensions;
@@ -129,22 +130,6 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime)
129130
using (new PerformanceMeasurement(performanceData, "Parse Time"))
130131
{
131132
parseResult = Parser.Instance.Parse(args);
132-
// If we didn't match any built-in commands, and a C# file path is the first argument,
133-
// parse as `dotnet run file.cs ..rest_of_args` instead.
134-
if (parseResult.CommandResult.Command is RootCommand
135-
&& parseResult.GetValue(Parser.DotnetSubCommand) is { } unmatchedCommandOrFile
136-
&& VirtualProjectBuildingCommand.IsValidEntryPointPath(unmatchedCommandOrFile))
137-
{
138-
List<string> otherTokens = new(parseResult.Tokens.Count - 1);
139-
foreach (var token in parseResult.Tokens)
140-
{
141-
if (token.Type != TokenType.Argument || token.Value != unmatchedCommandOrFile)
142-
{
143-
otherTokens.Add(token.Value);
144-
}
145-
}
146-
parseResult = Parser.Instance.Parse(["run", unmatchedCommandOrFile, .. otherTokens]);
147-
}
148133

149134
// Avoid create temp directory with root permission and later prevent access in non sudo
150135
// This method need to be run very early before temp folder get created
@@ -251,36 +236,35 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime)
251236
int exitCode;
252237
if (parseResult.CanBeInvoked())
253238
{
254-
PerformanceLogEventSource.Log.BuiltInCommandStart();
255-
256-
try
257-
{
258-
exitCode = parseResult.Invoke();
259-
exitCode = AdjustExitCode(parseResult, exitCode);
260-
}
261-
catch (Exception exception)
262-
{
263-
exitCode = Parser.ExceptionHandler(exception, parseResult);
264-
}
265-
266-
PerformanceLogEventSource.Log.BuiltInCommandStop();
239+
InvokeBuiltInCommand(parseResult, out exitCode);
267240
}
268241
else
269242
{
270243
PerformanceLogEventSource.Log.ExtensibleCommandResolverStart();
271244
try
272245
{
273-
var resolvedCommand = CommandFactoryUsingResolver.Create(
274-
"dotnet-" + parseResult.GetValue(Parser.DotnetSubCommand),
275-
args.GetSubArguments(),
276-
FrameworkConstants.CommonFrameworks.NetStandardApp15);
277-
PerformanceLogEventSource.Log.ExtensibleCommandResolverStop();
246+
string commandName = "dotnet-" + parseResult.GetValue(Parser.DotnetSubCommand);
247+
var resolvedCommandSpec = CommandResolver.TryResolveCommandSpec(
248+
new DefaultCommandResolverPolicy(),
249+
commandName,
250+
args.GetSubArguments(),
251+
FrameworkConstants.CommonFrameworks.NetStandardApp15);
252+
253+
if (resolvedCommandSpec is null && TryRunFileBasedApp(parseResult) is { } fileBasedAppExitCode)
254+
{
255+
exitCode = fileBasedAppExitCode;
256+
}
257+
else
258+
{
259+
var resolvedCommand = CommandFactoryUsingResolver.CreateOrThrow(commandName, resolvedCommandSpec);
260+
PerformanceLogEventSource.Log.ExtensibleCommandResolverStop();
278261

279-
PerformanceLogEventSource.Log.ExtensibleCommandStart();
280-
var result = resolvedCommand.Execute();
281-
PerformanceLogEventSource.Log.ExtensibleCommandStop();
262+
PerformanceLogEventSource.Log.ExtensibleCommandStart();
263+
var result = resolvedCommand.Execute();
264+
PerformanceLogEventSource.Log.ExtensibleCommandStop();
282265

283-
exitCode = result.ExitCode;
266+
exitCode = result.ExitCode;
267+
}
284268
}
285269
catch (CommandUnknownException e)
286270
{
@@ -297,6 +281,50 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime)
297281
TelemetryClient.Dispose();
298282

299283
return exitCode;
284+
285+
static int? TryRunFileBasedApp(ParseResult parseResult)
286+
{
287+
// If we didn't match any built-in commands, and a C# file path is the first argument,
288+
// parse as `dotnet run file.cs ..rest_of_args` instead.
289+
if (parseResult.CommandResult.Command is RootCommand
290+
&& parseResult.GetValue(Parser.DotnetSubCommand) is { } unmatchedCommandOrFile
291+
&& VirtualProjectBuildingCommand.IsValidEntryPointPath(unmatchedCommandOrFile))
292+
{
293+
List<string> otherTokens = new(parseResult.Tokens.Count - 1);
294+
foreach (var token in parseResult.Tokens)
295+
{
296+
if (token.Type != TokenType.Argument || token.Value != unmatchedCommandOrFile)
297+
{
298+
otherTokens.Add(token.Value);
299+
}
300+
}
301+
parseResult = Parser.Instance.Parse(["run", unmatchedCommandOrFile, .. otherTokens]);
302+
303+
InvokeBuiltInCommand(parseResult, out var exitCode);
304+
return exitCode;
305+
}
306+
307+
return null;
308+
}
309+
310+
static void InvokeBuiltInCommand(ParseResult parseResult, out int exitCode)
311+
{
312+
Debug.Assert(parseResult.CanBeInvoked());
313+
314+
PerformanceLogEventSource.Log.BuiltInCommandStart();
315+
316+
try
317+
{
318+
exitCode = parseResult.Invoke();
319+
exitCode = AdjustExitCode(parseResult, exitCode);
320+
}
321+
catch (Exception exception)
322+
{
323+
exitCode = Parser.ExceptionHandler(exception, parseResult);
324+
}
325+
326+
PerformanceLogEventSource.Log.BuiltInCommandStop();
327+
}
300328
}
301329

302330
private static int AdjustExitCode(ParseResult parseResult, int exitCode)

test/dotnet.Tests/CommandTests/Run/RunFileTests.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,119 @@ public void FilePath_AsProjectArgument()
220220
.And.HaveStdErrContaining(CliCommandStrings.RunCommandException);
221221
}
222222

223+
/// <summary>
224+
/// Even if there is a file-based app <c>./build</c>, <c>dotnet build</c> should not execute that.
225+
/// </summary>
226+
[Theory]
227+
// error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file.
228+
[InlineData("build", "MSB1003")]
229+
// dotnet watch: Could not find a MSBuild project file in '...'. Specify which project to use with the --project option.
230+
[InlineData("watch", "--project")]
231+
public void Precedence_BuiltInCommand(string cmd, string error)
232+
{
233+
var testInstance = _testAssetsManager.CreateTestDirectory();
234+
File.WriteAllText(Path.Join(testInstance.Path, cmd), """
235+
#!/usr/bin/env dotnet
236+
Console.WriteLine("hello 1");
237+
""");
238+
File.WriteAllText(Path.Join(testInstance.Path, $"dotnet-{cmd}"), """
239+
#!/usr/bin/env dotnet
240+
Console.WriteLine("hello 2");
241+
""");
242+
243+
// dotnet build -> built-in command
244+
new DotnetCommand(Log, cmd)
245+
.WithWorkingDirectory(testInstance.Path)
246+
.Execute()
247+
.Should().Fail()
248+
.And.HaveStdOutContaining(error);
249+
250+
// dotnet ./build -> file-based app
251+
new DotnetCommand(Log, $"./{cmd}")
252+
.WithWorkingDirectory(testInstance.Path)
253+
.Execute()
254+
.Should().Pass()
255+
.And.HaveStdOut("hello 1");
256+
257+
// dotnet run build -> file-based app
258+
new DotnetCommand(Log, "run", cmd)
259+
.WithWorkingDirectory(testInstance.Path)
260+
.Execute()
261+
.Should().Pass()
262+
.And.HaveStdOut("hello 1");
263+
}
264+
265+
/// <summary>
266+
/// Even if there is a file-based app <c>./test.dll</c>, <c>dotnet test.dll</c> should not execute that.
267+
/// </summary>
268+
[Theory]
269+
[InlineData("test.dll")]
270+
[InlineData("./test.dll")]
271+
public void Precedence_Dll(string arg)
272+
{
273+
var testInstance = _testAssetsManager.CreateTestDirectory();
274+
File.WriteAllText(Path.Join(testInstance.Path, "test.dll"), """
275+
#!/usr/bin/env dotnet
276+
Console.WriteLine("hello world");
277+
""");
278+
279+
// dotnet [./]test.dll -> exec the dll
280+
new DotnetCommand(Log, arg)
281+
.WithWorkingDirectory(testInstance.Path)
282+
.Execute()
283+
.Should().Fail()
284+
// A fatal error was encountered. The library 'hostpolicy.dll' required to execute the application was not found in ...
285+
.And.HaveStdErrContaining("hostpolicy");
286+
287+
// dotnet run [./]test.dll -> file-based app
288+
new DotnetCommand(Log, "run", arg)
289+
.WithWorkingDirectory(testInstance.Path)
290+
.Execute()
291+
.Should().Pass()
292+
.And.HaveStdOut("hello world");
293+
}
294+
295+
[Fact]
296+
public void Precedence_NuGetTool()
297+
{
298+
var testInstance = _testAssetsManager.CreateTestDirectory();
299+
File.WriteAllText(Path.Join(testInstance.Path, "complog"), """
300+
#!/usr/bin/env dotnet
301+
Console.WriteLine("hello world");
302+
""");
303+
304+
new DotnetCommand(Log, "new", "tool-manifest")
305+
.WithWorkingDirectory(testInstance.Path)
306+
.Execute()
307+
.Should().Pass();
308+
309+
new DotnetCommand(Log, "tool", "install", "[email protected]")
310+
.WithWorkingDirectory(testInstance.Path)
311+
.Execute()
312+
.Should().Pass();
313+
314+
// dotnet complog -> NuGet tool
315+
new DotnetCommand(Log, "complog")
316+
.WithWorkingDirectory(testInstance.Path)
317+
.Execute()
318+
.Should().Pass()
319+
.And.HaveStdOutContaining("complog");
320+
321+
// dotnet ./complog -> file-based app
322+
new DotnetCommand(Log, "./complog")
323+
.WithWorkingDirectory(testInstance.Path)
324+
.Execute()
325+
.Should().Pass()
326+
.And.HaveStdOut("hello world");
327+
328+
// dotnet run complog -> file-based app
329+
new DotnetCommand(Log, "run", "complog")
330+
.WithWorkingDirectory(testInstance.Path)
331+
.Execute()
332+
.Should().Pass()
333+
.And.HaveStdOut("hello world");
334+
}
335+
223336
/// <summary>
224337
/// <c>dotnet run -</c> reads the C# code from stdin.
225338
/// </summary>

0 commit comments

Comments
 (0)