diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index 6305e5293361..a3458a90f72d 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -183,15 +183,14 @@ Directives `sdk`, `package`, and `property` are translated into `` attribute uses). +Any value can optionally have two parts separated by a space (more whitespace characters could be allowed in the future). The value of the first `#:sdk` is injected into `` with the separator (if any) replaced with `/`, and the subsequent `#:sdk` directive values are split by the separator and injected as `` elements (or without the `Version` attribute if there is no separator). It is an error if the first part (name) is empty (the version is allowed to be empty, but that results in empty `Version=""`). diff --git a/src/Cli/dotnet/commands/dotnet-project/convert/ProjectConvertCommand.cs b/src/Cli/dotnet/commands/dotnet-project/convert/ProjectConvertCommand.cs index 812504b447cb..64d13ff9a2c1 100644 --- a/src/Cli/dotnet/commands/dotnet-project/convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-project/convert/ProjectConvertCommand.cs @@ -3,7 +3,9 @@ #nullable enable +using System.Collections.Immutable; using System.CommandLine; +using System.IO; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.Utils; using Microsoft.TemplateEngine.Cli.Commands; @@ -35,13 +37,29 @@ public override int Execute() throw new GracefulException(LocalizableStrings.DirectoryAlreadyExists, targetDirectory); } + // Find directives (this can fail, so do this before creating the target directory). + var sourceFile = VirtualProjectBuildingCommand.LoadSourceFile(file); + var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile); + Directory.CreateDirectory(targetDirectory); - string projectFile = Path.Join(targetDirectory, Path.GetFileNameWithoutExtension(file) + ".csproj"); - string projectFileText = VirtualProjectBuildingCommand.GetNonVirtualProjectFileText(); - File.WriteAllText(path: projectFile, contents: projectFileText); + var targetFile = Path.Join(targetDirectory, Path.GetFileName(file)); + + // If there were any directives, remove them from the file. + if (directives.Length != 0) + { + VirtualProjectBuildingCommand.RemoveDirectivesFromFile(directives, sourceFile.Text, targetFile); + File.Delete(file); + } + else + { + File.Move(file, targetFile); + } - File.Move(file, Path.Join(targetDirectory, Path.GetFileName(file))); + string projectFile = Path.Join(targetDirectory, Path.GetFileNameWithoutExtension(file) + ".csproj"); + using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write); + using var writer = new StreamWriter(stream, Encoding.UTF8); + VirtualProjectBuildingCommand.WriteProjectFile(writer, directives); return 0; } diff --git a/src/Cli/dotnet/commands/dotnet-run/LocalizableStrings.resx b/src/Cli/dotnet/commands/dotnet-run/LocalizableStrings.resx index 7835c0542477..1be704f47749 100644 --- a/src/Cli/dotnet/commands/dotnet-run/LocalizableStrings.resx +++ b/src/Cli/dotnet/commands/dotnet-run/LocalizableStrings.resx @@ -244,4 +244,20 @@ Make the profile names distinct. A launch profile with the name '{0}' doesn't exist. + + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + + + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + + + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + diff --git a/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs b/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs index 12a90ef6b70d..aa201693443d 100644 --- a/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-run/RunCommand.cs @@ -110,7 +110,7 @@ public int Execute() projectFactory = new VirtualProjectBuildingCommand { EntryPointFileFullPath = EntryPointFileFullPath, - }.CreateProjectInstance; + }.PrepareProjectInstance().CreateProjectInstance; } try diff --git a/src/Cli/dotnet/commands/dotnet-run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/commands/dotnet-run/VirtualProjectBuildingCommand.cs index a55a79ba9439..8d0d4e41c1b1 100644 --- a/src/Cli/dotnet/commands/dotnet-run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-run/VirtualProjectBuildingCommand.cs @@ -3,6 +3,11 @@ #nullable enable +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Security; +using System.Text.RegularExpressions; using System.Xml; using Microsoft.Build.Construction; using Microsoft.Build.Definition; @@ -10,8 +15,11 @@ using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Logging; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.Utils; +using LocalizableStrings = Microsoft.DotNet.Tools.Run.LocalizableStrings; namespace Microsoft.DotNet.Tools; @@ -20,6 +28,9 @@ namespace Microsoft.DotNet.Tools; /// internal sealed class VirtualProjectBuildingCommand { + private ImmutableArray _directives; + private string? _targetFilePath; + public Dictionary GlobalProperties { get; } = new(StringComparer.OrdinalIgnoreCase); public required string EntryPointFileFullPath { get; init; } @@ -49,6 +60,8 @@ public int Execute(string[] binaryLoggerArgs, ILogger consoleLogger) }; BuildManager.DefaultBuildManager.BeginBuild(parameters); + PrepareProjectInstance(); + // Do a restore first (equivalent to MSBuild's "implicit restore", i.e., `/restore`). // See https://github.com/dotnet/msbuild/blob/a1c2e7402ef0abe36bf493e395b04dd2cb1b3540/src/MSBuild/XMake.cs#L1838 // and https://github.com/dotnet/msbuild/issues/11519. @@ -117,6 +130,31 @@ public int Execute(string[] binaryLoggerArgs, ILogger consoleLogger) } } + /// + /// Needs to be called before the first call to . + /// + public VirtualProjectBuildingCommand PrepareProjectInstance() + { + Debug.Assert(_directives.IsDefault && _targetFilePath is null, $"{nameof(PrepareProjectInstance)} should not be called multiple times."); + + var sourceFile = LoadSourceFile(EntryPointFileFullPath); + _directives = FindDirectives(sourceFile); + + // If there were any `#:` directives, remove them from the file. + // (This is temporary until Roslyn is updated to ignore them.) + _targetFilePath = EntryPointFileFullPath; + if (_directives.Length != 0) + { + var targetDirectory = Path.Join(Path.GetDirectoryName(_targetFilePath), "obj"); + Directory.CreateDirectory(targetDirectory); + _targetFilePath = Path.Join(targetDirectory, Path.GetFileName(_targetFilePath)); + + RemoveDirectivesFromFile(_directives, sourceFile.Text, _targetFilePath); + } + + return this; + } + public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection) { return CreateProjectInstance(projectCollection, addGlobalProperties: null); @@ -139,82 +177,281 @@ private ProjectInstance CreateProjectInstance( { GlobalProperties = globalProperties, }); + + ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) + { + Debug.Assert(!_directives.IsDefault && _targetFilePath is not null, $"{nameof(PrepareProjectInstance)} should have been called first."); + + var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj"); + var projectFileWriter = new StringWriter(); + WriteProjectFile(projectFileWriter, _directives, isVirtualProject: true, targetFilePath: _targetFilePath); + var projectFileText = projectFileWriter.ToString(); + + using var reader = new StringReader(projectFileText); + using var xmlReader = XmlReader.Create(reader); + var projectRoot = ProjectRootElement.Create(xmlReader, projectCollection); + projectRoot.FullPath = projectFileFullPath; + return projectRoot; + } } - // Kept in sync with the default `dotnet new console` project file (enforced by `DotnetProjectAddTests.SameAsTemplate`). - private const string CommonProjectProperties = """ - Exe - net10.0 - enable - enable - """; + public static void WriteProjectFile(TextWriter writer, ImmutableArray directives) + { + WriteProjectFile(writer, directives, isVirtualProject: false, targetFilePath: null); + } - public static string GetNonVirtualProjectFileText() + private static void WriteProjectFile(TextWriter writer, ImmutableArray directives, bool isVirtualProject, string? targetFilePath) { - return $""" - + int processedDirectives = 0; + var sdkDirectives = directives.OfType(); + var propertyDirectives = directives.OfType(); + var packageDirectives = directives.OfType(); + + string sdkValue = "Microsoft.NET.Sdk"; + + if (sdkDirectives.FirstOrDefault() is { } firstSdk) + { + sdkValue = firstSdk.ToSlashDelimitedString(); + processedDirectives++; + } + + if (isVirtualProject) + { + writer.WriteLine($""" + + + + + """); + } + else + { + writer.WriteLine($""" + + + """); + } + + foreach (var sdk in sdkDirectives.Skip(1)) + { + if (isVirtualProject) + { + writer.WriteLine($""" + + """); + } + else if (sdk.Version is null) + { + writer.WriteLine($""" + + """); + } + else + { + writer.WriteLine($""" + + """); + } + + processedDirectives++; + } + + if (processedDirectives > 1) + { + writer.WriteLine(); + } + + // Kept in sync with the default `dotnet new console` project file (enforced by `DotnetProjectAddTests.SameAsTemplate`). + writer.WriteLine($""" - {CommonProjectProperties} + Exe + net10.0 + enable + enable + """); + + if (isVirtualProject) + { + writer.WriteLine(""" + + + false + + """); + } + + if (propertyDirectives.Any()) + { + writer.WriteLine(""" + + + """); + + foreach (var property in propertyDirectives) + { + writer.WriteLine($""" + <{property.Name}>{EscapeValue(property.Value)} + """); + + processedDirectives++; + } + + writer.WriteLine(" "); + } + + if (packageDirectives.Any()) + { + writer.WriteLine(""" + + + """); + + foreach (var package in packageDirectives) + { + if (package.Version is null) + { + writer.WriteLine($""" + + """); + } + else + { + writer.WriteLine($""" + + """); + } + + processedDirectives++; + } + + writer.WriteLine(" "); + } + + Debug.Assert(processedDirectives + directives.OfType().Count() == directives.Length); + + if (isVirtualProject) + { + Debug.Assert(targetFilePath is not null); + + writer.WriteLine($""" + + + + + + """); + + foreach (var sdk in sdkDirectives) + { + writer.WriteLine($""" + + """); + } + + if (!sdkDirectives.Any()) + { + Debug.Assert(sdkValue == "Microsoft.NET.Sdk"); + writer.WriteLine(""" + + """); + } + + writer.WriteLine(""" + + + + + + + + + + + + <_RestoreProjectPathItems Include="@(FilteredRestoreGraphProjectInputItems)" /> + + + + + + + """); + } + + writer.WriteLine(""" + """); - """; + static string EscapeValue(string value) => SecurityElement.Escape(value); } - private ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) + public static ImmutableArray FindDirectives(SourceFile sourceFile) { - var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj"); - var projectFileText = $""" - - - + var builder = ImmutableArray.CreateBuilder(); - - {CommonProjectProperties} + // NOTE: When Roslyn is updated to support "ignored directives", we should use its SyntaxTokenParser instead. + foreach (var line in sourceFile.Text.Lines) + { + var lineText = sourceFile.Text.ToString(line.Span); - false - + if (Patterns.Shebang.IsMatch(lineText)) + { + builder.Add(new CSharpDirective.Shebang { Span = line.SpanIncludingLineBreak }); + } + else if (Patterns.Directive.Match(lineText) is { Success: true } match) + { + builder.Add(CSharpDirective.Parse(sourceFile, line.SpanIncludingLineBreak, match.Groups[1].Value, match.Groups[2].Value)); + } + } - - - - - - - - - - - - - <_RestoreProjectPathItems Include="@(FilteredRestoreGraphProjectInputItems)" /> - - - - - - - - """; - ProjectRootElement projectRoot; - using (var xmlReader = XmlReader.Create(new StringReader(projectFileText))) + // The result should be ordered by source location, RemoveDirectivesFromFile depends on that. + return builder.ToImmutable(); + } + + public static SourceFile LoadSourceFile(string filePath) + { + using var stream = File.OpenRead(filePath); + return new SourceFile(filePath, SourceText.From(stream, Encoding.UTF8)); + } + + public static SourceText? RemoveDirectivesFromFile(ImmutableArray directives, SourceText text) + { + if (directives.Length == 0) + { + return null; + } + + Debug.Assert(directives.OrderBy(d => d.Span.Start).SequenceEqual(directives), "Directives should be ordered by source location."); + + for (int i = directives.Length - 1; i >= 0; i--) + { + var directive = directives[i]; + text = text.Replace(directive.Span, string.Empty); + } + + return text; + } + + public static void RemoveDirectivesFromFile(ImmutableArray directives, SourceText text, string filePath) + { + if (RemoveDirectivesFromFile(directives, text) is { } modifiedText) { - projectRoot = ProjectRootElement.Create(xmlReader, projectCollection); + using var stream = File.Open(filePath, FileMode.Create, FileAccess.Write); + using var writer = new StreamWriter(stream, Encoding.UTF8); + modifiedText.Write(writer); } - projectRoot.AddItem(itemType: "Compile", include: EntryPointFileFullPath); - projectRoot.FullPath = projectFileFullPath; - return projectRoot; } public static bool IsValidEntryPointPath(string entryPointFilePath) @@ -222,3 +459,161 @@ public static bool IsValidEntryPointPath(string entryPointFilePath) return entryPointFilePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) && File.Exists(entryPointFilePath); } } + +internal readonly record struct SourceFile(string Path, SourceText Text) +{ + public string GetLocationString(TextSpan span) + { + var positionSpan = new FileLinePositionSpan(Path, Text.Lines.GetLinePositionSpan(span)); + return $"{positionSpan.Path}:{positionSpan.StartLinePosition.Line + 1}"; + } +} + +internal static partial class Patterns +{ + [GeneratedRegex("""^\s*#:\s*(\w*)\s*(.*?)\s*$""")] + public static partial Regex Directive { get; } + + [GeneratedRegex("""^\s*#!.*$""")] + public static partial Regex Shebang { get; } +} + +/// +/// Represents a C# directive starting with #:. Those are ignored by the language but recognized by us. +/// +internal abstract class CSharpDirective +{ + private CSharpDirective() { } + + /// + /// Span of the full line including the trailing line break. + /// + public required TextSpan Span { get; init; } + + public static CSharpDirective Parse(SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText) + { + return directiveKind switch + { + "sdk" => Sdk.Parse(sourceFile, span, directiveKind, directiveText), + "property" => Property.Parse(sourceFile, span, directiveKind, directiveText), + "package" => Package.Parse(sourceFile, span, directiveKind, directiveText), + _ => throw new GracefulException(LocalizableStrings.UnrecognizedDirective, directiveKind, sourceFile.GetLocationString(span)), + }; + } + + private static (string, string?) ParseOptionalTwoParts(SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText) + { + var i = directiveText.IndexOf(' ', StringComparison.Ordinal); + var firstPart = checkFirstPart(i < 0 ? directiveText : directiveText[..i]); + var secondPart = i < 0 ? [] : directiveText.AsSpan((i + 1)..).TrimStart(); + if (i < 0 || secondPart.IsWhiteSpace()) + { + return (firstPart, null); + } + + return (firstPart, secondPart.ToString()); + + string checkFirstPart(string firstPart) + { + if (string.IsNullOrWhiteSpace(firstPart)) + { + throw new GracefulException(LocalizableStrings.MissingDirectiveName, directiveKind, sourceFile.GetLocationString(span)); + } + + return firstPart; + } + } + + /// + /// #! directive. + /// + public sealed class Shebang : CSharpDirective; + + /// + /// #:sdk directive. + /// + public sealed class Sdk : CSharpDirective + { + private Sdk() { } + + public required string Name { get; init; } + public string? Version { get; init; } + + public static new Sdk Parse(SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText) + { + var (sdkName, sdkVersion) = ParseOptionalTwoParts(sourceFile, span, directiveKind, directiveText); + + return new Sdk + { + Span = span, + Name = sdkName, + Version = sdkVersion, + }; + } + + public string ToSlashDelimitedString() + { + return Version is null ? Name : $"{Name}/{Version}"; + } + } + + /// + /// #:property directive. + /// + public sealed class Property : CSharpDirective + { + private Property() { } + + public required string Name { get; init; } + public required string Value { get; init; } + + public static new Property Parse(SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText) + { + var (propertyName, propertyValue) = ParseOptionalTwoParts(sourceFile, span, directiveKind, directiveText); + + if (propertyValue is null) + { + throw new GracefulException(LocalizableStrings.PropertyDirectiveMissingParts, sourceFile.GetLocationString(span)); + } + + try + { + propertyName = XmlConvert.VerifyName(propertyName); + } + catch (XmlException ex) + { + throw new GracefulException(string.Format(LocalizableStrings.PropertyDirectiveInvalidName, sourceFile.GetLocationString(span), ex.Message), ex); + } + + return new Property + { + Span = span, + Name = propertyName, + Value = propertyValue, + }; + } + } + + /// + /// #:package directive. + /// + public sealed class Package : CSharpDirective + { + private Package() { } + + public required string Name { get; init; } + public string? Version { get; init; } + + public static new Package Parse(SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText) + { + var (packageName, packageVersion) = ParseOptionalTwoParts(sourceFile, span, directiveKind, directiveText); + + return new Package + { + Span = span, + Name = packageName, + Version = packageVersion, + }; + } + } +} diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.cs.xlf index 59ada52ff3af..004b695765f4 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.cs.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.cs.xlf @@ -51,11 +51,26 @@ Nastavte odlišné názvy profilů. Profil spuštění s názvem {0} neexistuje. + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. Pomocí možnosti -p je možné v jednu chvíli zadat pouze jeden projekt. {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. Vlastnosti, které mají být předány nástroji MSBuild @@ -151,6 +166,11 @@ Aktuální {1} je {2}. {0} není platný soubor projektu. + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... Použití nastavení spuštění z {0}... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.de.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.de.xlf index ecec47ebfcae..36658c85e829 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.de.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.de.xlf @@ -51,11 +51,26 @@ Erstellen Sie eindeutige Profilnamen. Es ist kein Startprofil mit dem Namen "{0}" vorhanden. + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. Nur jeweils ein Projekt kann mithilfe der Option „-p“ angegeben werden. {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. Eigenschaften, die an MSBuild übergeben werden sollen. @@ -151,6 +166,11 @@ Ein ausführbares Projekt muss ein ausführbares TFM (z. B. net5.0) und den Outp "{0}" ist keine gültige Projektdatei. + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... Die Starteinstellungen von {0} werden verwendet… diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.es.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.es.xlf index 63695e47e069..b1b620cea61e 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.es.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.es.xlf @@ -51,11 +51,26 @@ Defina nombres de perfiles distintos. No existe ningún perfil de inicio con el nombre "{0}". + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. Solo se puede especificar un proyecto a la vez mediante la opción -p. {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. Propiedades que se van a pasar a MSBuild. @@ -151,6 +166,11 @@ El valor actual de {1} es "{2}". "{0}" no es un archivo de proyecto válido. + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... Usando la configuración de inicio de {0}... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.fr.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.fr.xlf index 286a7a541634..c9f29dd9fded 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.fr.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.fr.xlf @@ -51,11 +51,26 @@ faites en sorte que les noms de profil soient distincts. Un profil de lancement avec le nom '{0}' n'existe pas. + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. Vous ne pouvez spécifier qu’un seul projet à la fois à l’aide de l’option -p. {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. Propriétés à passer à MSBuild @@ -151,6 +166,11 @@ Le {1} actuel est '{2}'. '{0}' n'est pas un fichier projet valide. + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... Utilisation des paramètres de lancement à partir de {0}... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.it.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.it.xlf index 5f52c5e48fae..b2342bb37998 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.it.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.it.xlf @@ -51,11 +51,26 @@ Rendi distinti i nomi dei profili. Non esiste un profilo di avvio con il nome '{0}'. + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. È possibile specificare un solo progetto alla volta utilizzando l'opzione -p. {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. Proprietà da passare a MSBuild. @@ -151,6 +166,11 @@ Il valore corrente di {1} è '{2}'. '{0}' non è un file di progetto valido. + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... Uso delle impostazioni di avvio di {0}... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ja.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ja.xlf index ab29ec8d15c3..af2846b5de11 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ja.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ja.xlf @@ -51,11 +51,26 @@ Make the profile names distinct. '{0} ' という名前の起動プロファイルは存在しません。 + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. -p オプションを使用して一度に指定できるプロジェクトは 1 つだけです。 {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. MSBuild に渡されるプロパティ。 @@ -151,6 +166,11 @@ The current {1} is '{2}'. '{0}' は有効なプロジェクト ファイルではありません。 + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... {0} からの起動設定を使用中... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ko.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ko.xlf index a2b77c1bc8bf..045b3666c091 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ko.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ko.xlf @@ -51,11 +51,26 @@ Make the profile names distinct. 이름이 '{0}'인 시작 프로필이 없습니다. + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. -p 옵션을 사용하여 한 번에 하나의 프로젝트만 지정할 수 있습니다. {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. MSBuild에 전달할 속성입니다. @@ -151,6 +166,11 @@ The current {1} is '{2}'. '{0}'은(는) 유효한 프로젝트 파일이 아닙니다. + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... {0}의 시작 설정을 사용하는 중... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.pl.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.pl.xlf index d013b5a9a62f..ec68e4ea4b84 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.pl.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.pl.xlf @@ -51,11 +51,26 @@ Rozróżnij nazwy profilów. Profil uruchamiania o nazwie „{0}” nie istnieje. + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. Jednocześnie można określić tylko jeden projekt przy użyciu opcji -p. {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. Właściwości do przekazania do programu MSBuild. @@ -151,6 +166,11 @@ Bieżący element {1}: „{2}”. „{0}” nie jest prawidłowym plikiem projektu. + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... Używanie ustawień uruchamiania z profilu {0}... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.pt-BR.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.pt-BR.xlf index 258d2670d7e7..6814e4245ddd 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.pt-BR.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.pt-BR.xlf @@ -51,11 +51,26 @@ Diferencie os nomes dos perfis. Um perfil de lançamento com o nome '{0}' não existe. + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. Somente um projeto pode ser especificado por vez usando a opção -p. {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. Propriedades a serem passadas para o MSBuild. @@ -151,6 +166,11 @@ O {1} atual é '{2}'. '{0}' não é um arquivo de projeto válido. + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... Usando as configurações de inicialização de {0}... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ru.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ru.xlf index 78de4b5ebcd1..8869a15391ca 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ru.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.ru.xlf @@ -51,11 +51,26 @@ Make the profile names distinct. Профиль запуска с именем "{0}" не существует. + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. С помощью параметра -p можно указать только один проект за раз. {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. Свойства, которые необходимо передать в MSBuild. @@ -151,6 +166,11 @@ The current {1} is '{2}'. "{0}" не является допустимым файлом проекта. + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... Используются параметры запуска из {0}... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.tr.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.tr.xlf index 167786e6277a..fe58dc6dea4d 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.tr.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.tr.xlf @@ -51,11 +51,26 @@ Make the profile names distinct. '{0}' adlı bir başlatma profili yok. + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. -p seçeneği kullanılarak tek seferde yalnızca bir proje belirtilebilir. {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. MSBuild'e geçirilecek özellikler. @@ -151,6 +166,11 @@ Geçerli {1}: '{2}'. '{0}' geçerli bir proje dosyası değil. + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... {0} içindeki başlatma ayarları kullanılıyor... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.zh-Hans.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.zh-Hans.xlf index 799e30f616a5..8efa7b5ee5be 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.zh-Hans.xlf @@ -51,11 +51,26 @@ Make the profile names distinct. 名为“{0}”的启动配置文件不存在。 + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. 使用 -p 选项时一次只能指定一个项目。 {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. 要传递到 MSBuild 的属性。 @@ -151,6 +166,11 @@ The current {1} is '{2}'. “{0}”不是有效的项目文件。 + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... 从 {0} 使用启动设置... diff --git a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.zh-Hant.xlf b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.zh-Hant.xlf index cec07d6b8f89..67af330485b2 100644 --- a/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/commands/dotnet-run/xlf/LocalizableStrings.zh-Hant.xlf @@ -51,11 +51,26 @@ Make the profile names distinct. 名稱為 '{0}' 的啟動設定檔不存在。 + + Missing name of '{0}' at {1}. + Missing name of '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Only one project can be specified at a time using the -p option. 使用 -p 選項時,一次只能指定一個專案。 {Locked="-p"} + + Invalid property name at {0}. {1} + Invalid property name at {0}. {1} + {0} is the file path and line number. {1} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': {0} + {0} is the file path and line number. {Locked="="} + Properties to be passed to MSBuild. 要傳送至 MSBuild 的屬性。 @@ -151,6 +166,11 @@ The current {1} is '{2}'. '{0}' 並非有效的專案名稱。 + + Unrecognized directive '{0}' at {1}. + Unrecognized directive '{0}' at {1}. + {0} is the directive name like 'package' or 'sdk', {1} is the file path and line number. + Using launch settings from {0}... 使用來自 {0} 的啟動設定... diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index 03afec6eb2aa..f3eeca630d30 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -121,6 +121,7 @@ + diff --git a/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs b/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs index 0e3155a6b7cf..dfa42b5c641a 100644 --- a/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs +++ b/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs @@ -1,6 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.CodeAnalysis.Text; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Tools; + namespace Microsoft.DotNet.Cli.Project.Convert.Tests; public sealed class DotnetProjectConvertTests(ITestOutputHelper log) : SdkTest(log) @@ -25,7 +29,15 @@ public void SameAsTemplate() .Execute() .Should().Pass(); - var dotnetProjectConvertProject = Directory.EnumerateFiles(Path.Join(dotnetProjectConvert, "Program"), "*.csproj").Single(); + new DirectoryInfo(dotnetProjectConvert) + .EnumerateFileSystemInfos().Select(d => d.Name).Order() + .Should().BeEquivalentTo(["Program"]); + + new DirectoryInfo(Path.Join(dotnetProjectConvert, "Program")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program.csproj", "Program.cs"]); + + var dotnetProjectConvertProject = Path.Join(dotnetProjectConvert, "Program", "Program.csproj"); Path.GetFileName(dotnetProjectConvertProject).Should().Be("Program.csproj"); @@ -57,6 +69,10 @@ public void DirectoryAlreadyExists() .Execute() .Should().Fail() .And.HaveStdErrContaining("The target directory already exists"); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(d => d.Name).Order() + .Should().BeEquivalentTo(["MyApp", "MyApp.cs"]); } [Fact] @@ -95,6 +111,10 @@ public void OutputOption_DirectoryAlreadyExists() .Execute() .Should().Fail() .And.HaveStdErrContaining("The target directory already exists"); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(d => d.Name).Order() + .Should().BeEquivalentTo(["MyApp.cs", "SomeOutput"]); } [Fact] @@ -110,8 +130,8 @@ public void MultipleEntryPointFiles() .Should().Pass(); new DirectoryInfo(testInstance.Path) - .EnumerateDirectories().Select(d => d.Name).Order() - .Should().BeEquivalentTo(["Program1"]); + .EnumerateFileSystemInfos().Select(d => d.Name).Order() + .Should().BeEquivalentTo(["Program1", "Program2.cs"]); new DirectoryInfo(Path.Join(testInstance.Path, "Program1")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -128,6 +148,9 @@ public void NoFileArgument() .Execute() .Should().Fail() .And.HaveStdErrContaining("Required argument missing for command"); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Should().BeEmpty(); } [Fact] @@ -140,6 +163,9 @@ public void NonExistentFile() .Execute() .Should().Fail() .And.HaveStdErrContaining("The specified file must exist"); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Should().BeEmpty(); } [Fact] @@ -153,6 +179,10 @@ public void NonCSharpFile() .Execute() .Should().Fail() .And.HaveStdErrContaining("The specified file must exist and have '.cs' file extension"); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program.vb"]); } [Fact] @@ -166,6 +196,10 @@ public void ExtensionCasing() .Execute() .Should().Pass(); + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program"]); + new DirectoryInfo(Path.Join(testInstance.Path, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() .Should().BeEquivalentTo(["Program.csproj", "Program.CS"]); @@ -184,6 +218,10 @@ public void FileContent(string content) .Execute() .Should().Pass(); + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program"]); + new DirectoryInfo(Path.Join(testInstance.Path, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() .Should().BeEquivalentTo(["Program.csproj", "Program.cs"]); @@ -205,8 +243,355 @@ public void NestedDirectory() .Execute() .Should().Pass(); + new DirectoryInfo(Path.Join(testInstance.Path, "app")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program"]); + new DirectoryInfo(Path.Join(testInstance.Path, "app", "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() .Should().BeEquivalentTo(["Program.csproj", "Program.cs"]); } + + /// + /// When processing fails due to invalid directives, no conversion should be performed + /// (e.g., the target directory should not be created). + /// + [Fact] + public void ProcessingFails() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), "#:invalid"); + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdErrContaining("Unrecognized directive 'invalid' at"); + + new DirectoryInfo(Path.Join(testInstance.Path)) + .EnumerateDirectories().Should().BeEmpty(); + } + + /// + /// End-to-end test of directive processing. More cases are covered by faster unit tests below. + /// + [Fact] + public void ProcessingSucceeds() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #:sdk Aspire.Hosting.Sdk/9.1.0 + Console.WriteLine(); + """); + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DirectoryInfo(testInstance.Path) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program"]); + + new DirectoryInfo(Path.Join(testInstance.Path, "Program")) + .EnumerateFileSystemInfos().Select(f => f.Name).Order() + .Should().BeEquivalentTo(["Program.csproj", "Program.cs"]); + + File.ReadAllText(Path.Join(testInstance.Path, "Program", "Program.cs")) + .Should().Be("Console.WriteLine();"); + + File.ReadAllText(Path.Join(testInstance.Path, "Program", "Program.csproj")) + .Should().Be(""" + + + + Exe + net10.0 + enable + enable + + + + + """); + } + + [Fact] + public void Directives() + { + VerifyConversion( + inputCSharp: """ + #!/program + #:sdk Microsoft.NET.Sdk + #:sdk Aspire.Hosting.Sdk 9.1.0 + #:property TargetFramework net11.0 + #:package System.CommandLine 2.0.0-beta4.22272.1 + #:property LangVersion preview + Console.WriteLine(); + """, + expectedProject: """ + + + + + + Exe + net10.0 + enable + enable + + + + net11.0 + preview + + + + + + + + + """, + expectedCSharp: """ + Console.WriteLine(); + """); + } + + [Fact] + public void Directives_Variable() + { + VerifyConversion( + inputCSharp: """ + #:package MyPackage $(MyProp) + #:property MyProp MyValue + """, + expectedProject: """ + + + + Exe + net10.0 + enable + enable + + + + MyValue + + + + + + + + + """, + expectedCSharp: ""); + } + + [Fact] + public void Directives_Separators() + { + VerifyConversion( + inputCSharp: """ + #:property Prop1 One=a/b + #:property Prop2 Two/a=b + #:sdk First 1.0=a/b + #:sdk Second 2.0/a=b + #:sdk Third 3.0=a/b + #:package P1 1.0/a=b + #:package P2 2.0/a=b + """, + expectedProject: """ + + + + + + + Exe + net10.0 + enable + enable + + + + One=a/b + Two/a=b + + + + + + + + + + """, + expectedCSharp: ""); + } + + [Theory] + [InlineData("invalid")] + [InlineData("SDK")] + public void Directives_Unknown(string directive) + { + VerifyConversionThrows( + inputCSharp: $""" + #:sdk Test + #:{directive} Test + """, + expectedWildcardPattern: $"Unrecognized directive '{directive}' at /app/Program.cs:2."); + } + + [Fact] + public void Directives_Empty() + { + VerifyConversionThrows( + inputCSharp: """ + #: + #:sdk Test + """, + expectedWildcardPattern: "Unrecognized directive '' at /app/Program.cs:1."); + } + + [Theory, CombinatorialData] + public void Directives_EmptyName( + [CombinatorialValues("sdk", "property", "package")] string directive, + [CombinatorialValues(" ", "")] string value) + { + VerifyConversionThrows( + inputCSharp: $""" + #:{directive}{value} + """, + expectedWildcardPattern: $"Missing name of '{directive}' at /app/Program.cs:1."); + } + + [Fact] + public void Directives_MissingPropertyValue() + { + VerifyConversionThrows( + inputCSharp: """ + #:property Test + """, + expectedWildcardPattern: "The property directive needs to have two parts separated by '=' like 'PropertyName=PropertyValue': /app/Program.cs:1"); + } + + [Fact] + public void Directives_InvalidPropertyName() + { + VerifyConversionThrows( + inputCSharp: """ + #:property Name" Value + """, + expectedWildcardPattern: """ + Invalid property name at /app/Program.cs:1. The '"' character, hexadecimal value 0x22, cannot be included in a name. + """); + } + + [Fact] + public void Directives_Escaping() + { + VerifyConversion( + inputCSharp: """ + #:property Prop + #:sdk ="<>test + #:package ="<>test + """, + expectedProject: """ + + + + Exe + net10.0 + enable + enable + + + + <test"> + + + + + + + + + """, + expectedCSharp: ""); + } + + [Fact] + public void Directives_Whitespace() + { + VerifyConversion( + inputCSharp: """ + # ! /test + #! /program x + #: sdk TestSdk + #:property Name Value + #:property NugetPackageDescription "My package with spaces" + # :property Name Value + """, + expectedProject: """ + + + + Exe + net10.0 + enable + enable + + + + Value + "My package with spaces" + + + + + """, + expectedCSharp: """ + # ! /test + # :property Name Value + """); + } + + [Fact] + public void Directives_Whitespace_Invalid() + { + VerifyConversionThrows( + inputCSharp: $""" + #: property Name{'\t'} Value + """, + expectedWildcardPattern: "Invalid property name at /app/Program.cs:1. The '\t' character, hexadecimal value 0x09, cannot be included in a name."); + } + + private static void Convert(string inputCSharp, out string actualProject, out string? actualCSharp) + { + var sourceFile = new SourceFile("/app/Program.cs", SourceText.From(inputCSharp, Encoding.UTF8)); + var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile); + var projectWriter = new StringWriter(); + VirtualProjectBuildingCommand.WriteProjectFile(projectWriter, directives); + actualProject = projectWriter.ToString(); + actualCSharp = VirtualProjectBuildingCommand.RemoveDirectivesFromFile(directives, sourceFile.Text)?.ToString(); + } + + /// + /// means the conversion should not touch the C# content. + /// + private static void VerifyConversion(string inputCSharp, string expectedProject, string? expectedCSharp) + { + Convert(inputCSharp, out var actualProject, out var actualCSharp); + actualProject.Should().Be(expectedProject); + actualCSharp.Should().Be(expectedCSharp); + } + + private static void VerifyConversionThrows(string inputCSharp, string expectedWildcardPattern) + { + var convert = () => Convert(inputCSharp, out _, out _); + convert.Should().Throw().WithMessage(expectedWildcardPattern); + } } diff --git a/test/dotnet-run.Tests/RunFileTests.cs b/test/dotnet-run.Tests/RunFileTests.cs index 8aa2e57e72ee..43ee3de38dc6 100644 --- a/test/dotnet-run.Tests/RunFileTests.cs +++ b/test/dotnet-run.Tests/RunFileTests.cs @@ -775,4 +775,58 @@ public void Define_02() .Should().Fail() .And.HaveStdOutContaining("error CS5001:"); // Program does not contain a static 'Main' method suitable for an entry point } + + [Fact] + public void PackageReference() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #:package System.CommandLine 2.0.0-beta4.22272.1 + using System.CommandLine; + + var rootCommand = new RootCommand("Sample app for System.CommandLine"); + return await rootCommand.InvokeAsync(args); + """); + + new DotnetCommand(Log, "run", "Program.cs", "--", "--help") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining(""" + Description: + Sample app for System.CommandLine + """); + } + + [Fact] + public void PackageReference_CentralVersion() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Packages.props"), """ + + + true + + + + + + """); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + #:package System.CommandLine + using System.CommandLine; + + var rootCommand = new RootCommand("Sample app for System.CommandLine"); + return await rootCommand.InvokeAsync(args); + """); + + new DotnetCommand(Log, "run", "Program.cs", "--", "--help") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOutContaining(""" + Description: + Sample app for System.CommandLine + """); + } }