From 1a23be01e74da2e25d644aff2e7b37e58c20fe6e Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Tue, 18 Mar 2025 20:08:08 +0100 Subject: [PATCH 1/8] Process C# directives in file-based programs --- .../convert/ProjectConvertCommand.cs | 26 +- .../dotnet-run/LocalizableStrings.resx | 16 + .../dotnet/commands/dotnet-run/RunCommand.cs | 2 +- .../VirtualProjectBuildingCommand.cs | 512 ++++++++++++++++-- .../dotnet-run/xlf/LocalizableStrings.cs.xlf | 20 + .../dotnet-run/xlf/LocalizableStrings.de.xlf | 20 + .../dotnet-run/xlf/LocalizableStrings.es.xlf | 20 + .../dotnet-run/xlf/LocalizableStrings.fr.xlf | 20 + .../dotnet-run/xlf/LocalizableStrings.it.xlf | 20 + .../dotnet-run/xlf/LocalizableStrings.ja.xlf | 20 + .../dotnet-run/xlf/LocalizableStrings.ko.xlf | 20 + .../dotnet-run/xlf/LocalizableStrings.pl.xlf | 20 + .../xlf/LocalizableStrings.pt-BR.xlf | 20 + .../dotnet-run/xlf/LocalizableStrings.ru.xlf | 20 + .../dotnet-run/xlf/LocalizableStrings.tr.xlf | 20 + .../xlf/LocalizableStrings.zh-Hans.xlf | 20 + .../xlf/LocalizableStrings.zh-Hant.xlf | 20 + src/Cli/dotnet/dotnet.csproj | 1 + .../DotnetProjectConvertTests.cs | 389 ++++++++++++- test/dotnet-run.Tests/RunFileTests.cs | 22 + 20 files changed, 1162 insertions(+), 66 deletions(-) diff --git a/src/Cli/dotnet/commands/dotnet-project/convert/ProjectConvertCommand.cs b/src/Cli/dotnet/commands/dotnet-project/convert/ProjectConvertCommand.cs index 812504b447cb..906aa9fb4551 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.CreateSourceFile(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..42b75f086669 100644 --- a/src/Cli/dotnet/commands/dotnet-run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/commands/dotnet-run/VirtualProjectBuildingCommand.cs @@ -3,6 +3,12 @@ #nullable enable +using System; +using System.Buffers; +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 +16,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 +29,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 +61,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 +131,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 = CreateSourceFile(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 +178,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, virtualProjectFile: 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, virtualProjectFile: false, targetFilePath: null); + } - public static string GetNonVirtualProjectFileText() + private static void WriteProjectFile(TextWriter writer, ImmutableArray directives, bool virtualProjectFile, 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 (virtualProjectFile) + { + writer.WriteLine($""" + + + + + """); + } + else + { + writer.WriteLine($""" + + + """); + } + + foreach (var sdk in sdkDirectives.Skip(1)) + { + if (virtualProjectFile) + { + 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 (virtualProjectFile) + { + 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 (virtualProjectFile) + { + 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 CreateSourceFile(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 +460,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 record CSharpDirective +{ + private static readonly SearchValues s_separators = SearchValues.Create('/', '='); + + 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 name, string value) + { + return name switch + { + "sdk" => Sdk.Parse(sourceFile, span, name, value), + "property" => Property.Parse(sourceFile, span, name, value), + "package" => Package.Parse(sourceFile, span, name, value), + _ => throw new GracefulException(LocalizableStrings.UnrecognizedDirective, name, sourceFile.GetLocationString(span)), + }; + } + + private static (string, string?) ParseNameAndOptionalVersion(SourceFile sourceFile, TextSpan span, string name, string value) + { + var i = value.AsSpan().IndexOfAny(s_separators); + if (i < 0) + { + return (checkFirstPart(value), null); + } + + return (checkFirstPart(value[..i]), value[(i + 1)..]); + + string checkFirstPart(string firstPart) + { + if (string.IsNullOrWhiteSpace(firstPart)) + { + throw new GracefulException(LocalizableStrings.MissingDirectiveName, name, sourceFile.GetLocationString(span)); + } + + return firstPart; + } + } + + /// + /// #! directive. + /// + public sealed record Shebang : CSharpDirective; + + /// + /// #:sdk directive. + /// + public sealed record 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 name, string value) + { + var (sdkName, sdkVersion) = ParseNameAndOptionalVersion(sourceFile, span, name, value); + + return new Sdk + { + Span = span, + Name = sdkName, + Version = sdkVersion, + }; + } + + public string ToSlashDelimitedString() + { + return Version is null ? Name : $"{Name}/{Version}"; + } + } + + /// + /// #:property directive. + /// + public sealed record 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 name, string value) + { + var (propertyName, propertyValue) = ParseNameAndOptionalVersion(sourceFile, span, name, value); + + 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 record 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 name, string value) + { + var (packageName, packageVersion) = ParseNameAndOptionalVersion(sourceFile, span, name, value); + + 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..20d9bad01f9d 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,353 @@ 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("=1.0", "")] 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 Name=Value + """, + expectedProject: """ + + + + Exe + net10.0 + enable + enable + + + + Value + + + + + """, + expectedCSharp: """ + # ! /test + # :property Name=Value + """); + } + + [Fact] + public void Directives_Whitespace_Invalid() + { + VerifyConversionThrows( + inputCSharp: """ + #: property Name = Value + """, + expectedWildcardPattern: "Invalid property name at /app/Program.cs:1. The ' ' character, hexadecimal value 0x20, 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) + { + 0.Invoking(delegate { Convert(inputCSharp, out _, out _); }) + .Should().Throw().WithMessage(expectedWildcardPattern); + } } diff --git a/test/dotnet-run.Tests/RunFileTests.cs b/test/dotnet-run.Tests/RunFileTests.cs index 8aa2e57e72ee..74419797e733 100644 --- a/test/dotnet-run.Tests/RunFileTests.cs +++ b/test/dotnet-run.Tests/RunFileTests.cs @@ -775,4 +775,26 @@ 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 + """); + } } From 428b4731136438faa4548d2ee921b5b351b0aa19 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 24 Mar 2025 19:14:30 +0100 Subject: [PATCH 2/8] Remove unnecessary `record`s --- .../commands/Run/VirtualProjectBuildingCommand.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs index 42b75f086669..66bbba03a977 100644 --- a/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs @@ -482,7 +482,7 @@ internal static partial class Patterns /// /// Represents a C# directive starting with #:. Those are ignored by the language but recognized by us. /// -internal abstract record CSharpDirective +internal abstract class CSharpDirective { private static readonly SearchValues s_separators = SearchValues.Create('/', '='); @@ -528,12 +528,12 @@ string checkFirstPart(string firstPart) /// /// #! directive. /// - public sealed record Shebang : CSharpDirective; + public sealed class Shebang : CSharpDirective; /// /// #:sdk directive. /// - public sealed record Sdk : CSharpDirective + public sealed class Sdk : CSharpDirective { private Sdk() { } @@ -561,7 +561,7 @@ public string ToSlashDelimitedString() /// /// #:property directive. /// - public sealed record Property : CSharpDirective + public sealed class Property : CSharpDirective { private Property() { } @@ -598,7 +598,7 @@ private Property() { } /// /// #:package directive. /// - public sealed record Package : CSharpDirective + public sealed class Package : CSharpDirective { private Package() { } From ae49c7633c11871800405c4e3c7088b400f5bf6f Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Tue, 25 Mar 2025 19:02:53 +0100 Subject: [PATCH 3/8] Change separator to a space --- documentation/general/dotnet-run-file.md | 3 +- .../Run/VirtualProjectBuildingCommand.cs | 12 ++--- .../DotnetProjectConvertTests.cs | 52 +++++++++---------- test/dotnet-run.Tests/RunFileTests.cs | 2 +- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index 6305e5293361..cb0d711dacd3 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -190,8 +190,7 @@ Other directives result in a warning, reserving them for future use. ``` The value must be separated from the name of the directive by white space and any leading and trailing white space is not considered part of the value. -Any value can optionally have two parts separated by `=` or `/` -(the former is consistent with how properties are usually passed, e.g., `/p:Prop=Value`, and the latter is what the `` 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/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs index 66bbba03a977..cb9932c9dc27 100644 --- a/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs @@ -484,8 +484,6 @@ internal static partial class Patterns /// internal abstract class CSharpDirective { - private static readonly SearchValues s_separators = SearchValues.Create('/', '='); - private CSharpDirective() { } /// @@ -506,13 +504,15 @@ public static CSharpDirective Parse(SourceFile sourceFile, TextSpan span, string private static (string, string?) ParseNameAndOptionalVersion(SourceFile sourceFile, TextSpan span, string name, string value) { - var i = value.AsSpan().IndexOfAny(s_separators); - if (i < 0) + var i = value.IndexOf(' ', StringComparison.Ordinal); + var firstPart = checkFirstPart(i < 0 ? value : value[..i]); + var secondPart = i < 0 ? [] : value.AsSpan((i + 1)..).TrimStart(); + if (i < 0 || secondPart.IsWhiteSpace()) { - return (checkFirstPart(value), null); + return (firstPart, null); } - return (checkFirstPart(value[..i]), value[(i + 1)..]); + return (firstPart, secondPart.ToString()); string checkFirstPart(string firstPart) { diff --git a/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs b/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs index 20d9bad01f9d..051c7613f971 100644 --- a/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs +++ b/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs @@ -323,10 +323,10 @@ public void Directives() 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 + #: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: """ @@ -363,8 +363,8 @@ public void Directives_Variable() { VerifyConversion( inputCSharp: """ - #:package MyPackage=$(MyProp) - #:property MyProp=MyValue + #:package MyPackage $(MyProp) + #:property MyProp MyValue """, expectedProject: """ @@ -395,13 +395,13 @@ 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 + #: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: """ @@ -459,11 +459,11 @@ public void Directives_Empty() [Theory, CombinatorialData] public void Directives_EmptyName( [CombinatorialValues("sdk", "property", "package")] string directive, - [CombinatorialValues("=1.0", "")] string value) + [CombinatorialValues(" ", "")] string value) { VerifyConversionThrows( inputCSharp: $""" - #:{directive} {value} + #:{directive}{value} """, expectedWildcardPattern: $"Missing name of '{directive}' at /app/Program.cs:1."); } @@ -483,7 +483,7 @@ public void Directives_InvalidPropertyName() { VerifyConversionThrows( inputCSharp: """ - #:property Name"=Value + #:property Name" Value """, expectedWildcardPattern: """ Invalid property name at /app/Program.cs:1. The '"' character, hexadecimal value 0x22, cannot be included in a name. @@ -495,9 +495,9 @@ public void Directives_Escaping() { VerifyConversion( inputCSharp: """ - #:property Prop= - #:sdk /="<>test - #:package /="<>test + #:property Prop + #:sdk ="<>test + #:package ="<>test """, expectedProject: """ @@ -531,8 +531,8 @@ public void Directives_Whitespace() # ! /test #! /program x #: sdk TestSdk - #:property Name= Value - # :property Name=Value + #:property Name Value + # :property Name Value """, expectedProject: """ @@ -545,7 +545,7 @@ public void Directives_Whitespace() - Value + Value @@ -553,7 +553,7 @@ public void Directives_Whitespace() """, expectedCSharp: """ # ! /test - # :property Name=Value + # :property Name Value """); } @@ -561,10 +561,10 @@ public void Directives_Whitespace() public void Directives_Whitespace_Invalid() { VerifyConversionThrows( - inputCSharp: """ - #: property Name = Value + inputCSharp: $""" + #: property Name{'\t'} Value """, - expectedWildcardPattern: "Invalid property name at /app/Program.cs:1. The ' ' character, hexadecimal value 0x20, cannot be included in a name."); + 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) diff --git a/test/dotnet-run.Tests/RunFileTests.cs b/test/dotnet-run.Tests/RunFileTests.cs index 74419797e733..905d3096a79a 100644 --- a/test/dotnet-run.Tests/RunFileTests.cs +++ b/test/dotnet-run.Tests/RunFileTests.cs @@ -781,7 +781,7 @@ public void PackageReference() { var testInstance = _testAssetsManager.CreateTestDirectory(); File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ - #:package System.CommandLine=2.0.0-beta4.22272.1 + #:package System.CommandLine 2.0.0-beta4.22272.1 using System.CommandLine; var rootCommand = new RootCommand("Sample app for System.CommandLine"); From 10c00d299c227f0cd986785dd1ae76fd94b51951 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Tue, 25 Mar 2025 19:07:45 +0100 Subject: [PATCH 4/8] Test package with central version --- test/dotnet-run.Tests/RunFileTests.cs | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/dotnet-run.Tests/RunFileTests.cs b/test/dotnet-run.Tests/RunFileTests.cs index 905d3096a79a..43ee3de38dc6 100644 --- a/test/dotnet-run.Tests/RunFileTests.cs +++ b/test/dotnet-run.Tests/RunFileTests.cs @@ -797,4 +797,36 @@ public void PackageReference() 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 + """); + } } From a9dd09af9aeaec5b7ffd7031ffc4da7a462db665 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Tue, 25 Mar 2025 19:08:02 +0100 Subject: [PATCH 5/8] Improve code --- .../dotnet/commands/Project/convert/ProjectConvertCommand.cs | 2 +- src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs | 4 ++-- .../dotnet-project-convert.Tests/DotnetProjectConvertTests.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Cli/dotnet/commands/Project/convert/ProjectConvertCommand.cs b/src/Cli/dotnet/commands/Project/convert/ProjectConvertCommand.cs index 906aa9fb4551..64d13ff9a2c1 100644 --- a/src/Cli/dotnet/commands/Project/convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/commands/Project/convert/ProjectConvertCommand.cs @@ -38,7 +38,7 @@ public override int Execute() } // Find directives (this can fail, so do this before creating the target directory). - var sourceFile = VirtualProjectBuildingCommand.CreateSourceFile(file); + var sourceFile = VirtualProjectBuildingCommand.LoadSourceFile(file); var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile); Directory.CreateDirectory(targetDirectory); diff --git a/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs index cb9932c9dc27..51ba986ec5d9 100644 --- a/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs @@ -138,7 +138,7 @@ public VirtualProjectBuildingCommand PrepareProjectInstance() { Debug.Assert(_directives.IsDefault && _targetFilePath is null, $"{nameof(PrepareProjectInstance)} should not be called multiple times."); - var sourceFile = CreateSourceFile(EntryPointFileFullPath); + var sourceFile = LoadSourceFile(EntryPointFileFullPath); _directives = FindDirectives(sourceFile); // If there were any `#:` directives, remove them from the file. @@ -421,7 +421,7 @@ public static ImmutableArray FindDirectives(SourceFile sourceFi return builder.ToImmutable(); } - public static SourceFile CreateSourceFile(string filePath) + public static SourceFile LoadSourceFile(string filePath) { using var stream = File.OpenRead(filePath); return new SourceFile(filePath, SourceText.From(stream, Encoding.UTF8)); diff --git a/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs b/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs index 051c7613f971..a0ff0d09f22f 100644 --- a/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs +++ b/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs @@ -589,7 +589,7 @@ private static void VerifyConversion(string inputCSharp, string expectedProject, private static void VerifyConversionThrows(string inputCSharp, string expectedWildcardPattern) { - 0.Invoking(delegate { Convert(inputCSharp, out _, out _); }) - .Should().Throw().WithMessage(expectedWildcardPattern); + var convert = () => Convert(inputCSharp, out _, out _); + convert.Should().Throw().WithMessage(expectedWildcardPattern); } } From 27c5ccfa665ac31f7db4d08e64ae17a3c9b89ea2 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Tue, 25 Mar 2025 20:28:50 +0100 Subject: [PATCH 6/8] Improve code --- .../Run/VirtualProjectBuildingCommand.cs | 49 +++++++++---------- .../DotnetProjectConvertTests.cs | 2 + 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs index 51ba986ec5d9..8d0d4e41c1b1 100644 --- a/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/commands/Run/VirtualProjectBuildingCommand.cs @@ -4,7 +4,6 @@ #nullable enable using System; -using System.Buffers; using System.Collections.Immutable; using System.Diagnostics; using System.Security; @@ -185,7 +184,7 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj"); var projectFileWriter = new StringWriter(); - WriteProjectFile(projectFileWriter, _directives, virtualProjectFile: true, targetFilePath: _targetFilePath); + WriteProjectFile(projectFileWriter, _directives, isVirtualProject: true, targetFilePath: _targetFilePath); var projectFileText = projectFileWriter.ToString(); using var reader = new StringReader(projectFileText); @@ -198,10 +197,10 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) public static void WriteProjectFile(TextWriter writer, ImmutableArray directives) { - WriteProjectFile(writer, directives, virtualProjectFile: false, targetFilePath: null); + WriteProjectFile(writer, directives, isVirtualProject: false, targetFilePath: null); } - private static void WriteProjectFile(TextWriter writer, ImmutableArray directives, bool virtualProjectFile, string? targetFilePath) + private static void WriteProjectFile(TextWriter writer, ImmutableArray directives, bool isVirtualProject, string? targetFilePath) { int processedDirectives = 0; @@ -217,7 +216,7 @@ private static void WriteProjectFile(TextWriter writer, ImmutableArray @@ -236,7 +235,7 @@ private static void WriteProjectFile(TextWriter writer, ImmutableArray @@ -273,7 +272,7 @@ private static void WriteProjectFile(TextWriter writer, ImmutableArray """); - if (virtualProjectFile) + if (isVirtualProject) { writer.WriteLine(""" @@ -332,7 +331,7 @@ private static void WriteProjectFile(TextWriter writer, ImmutableArray().Count() == directives.Length); - if (virtualProjectFile) + if (isVirtualProject) { Debug.Assert(targetFilePath is not null); @@ -491,22 +490,22 @@ private CSharpDirective() { } /// public required TextSpan Span { get; init; } - public static CSharpDirective Parse(SourceFile sourceFile, TextSpan span, string name, string value) + public static CSharpDirective Parse(SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText) { - return name switch + return directiveKind switch { - "sdk" => Sdk.Parse(sourceFile, span, name, value), - "property" => Property.Parse(sourceFile, span, name, value), - "package" => Package.Parse(sourceFile, span, name, value), - _ => throw new GracefulException(LocalizableStrings.UnrecognizedDirective, name, sourceFile.GetLocationString(span)), + "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?) ParseNameAndOptionalVersion(SourceFile sourceFile, TextSpan span, string name, string value) + private static (string, string?) ParseOptionalTwoParts(SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText) { - var i = value.IndexOf(' ', StringComparison.Ordinal); - var firstPart = checkFirstPart(i < 0 ? value : value[..i]); - var secondPart = i < 0 ? [] : value.AsSpan((i + 1)..).TrimStart(); + 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); @@ -518,7 +517,7 @@ string checkFirstPart(string firstPart) { if (string.IsNullOrWhiteSpace(firstPart)) { - throw new GracefulException(LocalizableStrings.MissingDirectiveName, name, sourceFile.GetLocationString(span)); + throw new GracefulException(LocalizableStrings.MissingDirectiveName, directiveKind, sourceFile.GetLocationString(span)); } return firstPart; @@ -540,9 +539,9 @@ private Sdk() { } public required string Name { get; init; } public string? Version { get; init; } - public static new Sdk Parse(SourceFile sourceFile, TextSpan span, string name, string value) + public static new Sdk Parse(SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText) { - var (sdkName, sdkVersion) = ParseNameAndOptionalVersion(sourceFile, span, name, value); + var (sdkName, sdkVersion) = ParseOptionalTwoParts(sourceFile, span, directiveKind, directiveText); return new Sdk { @@ -568,9 +567,9 @@ private Property() { } public required string Name { get; init; } public required string Value { get; init; } - public static new Property Parse(SourceFile sourceFile, TextSpan span, string name, string value) + public static new Property Parse(SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText) { - var (propertyName, propertyValue) = ParseNameAndOptionalVersion(sourceFile, span, name, value); + var (propertyName, propertyValue) = ParseOptionalTwoParts(sourceFile, span, directiveKind, directiveText); if (propertyValue is null) { @@ -605,9 +604,9 @@ private Package() { } public required string Name { get; init; } public string? Version { get; init; } - public static new Package Parse(SourceFile sourceFile, TextSpan span, string name, string value) + public static new Package Parse(SourceFile sourceFile, TextSpan span, string directiveKind, string directiveText) { - var (packageName, packageVersion) = ParseNameAndOptionalVersion(sourceFile, span, name, value); + var (packageName, packageVersion) = ParseOptionalTwoParts(sourceFile, span, directiveKind, directiveText); return new Package { diff --git a/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs b/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs index a0ff0d09f22f..dfa42b5c641a 100644 --- a/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs +++ b/test/dotnet-project-convert.Tests/DotnetProjectConvertTests.cs @@ -532,6 +532,7 @@ public void Directives_Whitespace() #! /program x #: sdk TestSdk #:property Name Value + #:property NugetPackageDescription "My package with spaces" # :property Name Value """, expectedProject: """ @@ -546,6 +547,7 @@ public void Directives_Whitespace() Value + "My package with spaces" From 821b3abfb2c91defa03569beae9ca6caa56d1537 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Tue, 25 Mar 2025 20:51:23 +0100 Subject: [PATCH 7/8] Use spaces in an example --- documentation/general/dotnet-run-file.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index cb0d711dacd3..741383ca8925 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -184,9 +184,9 @@ Other directives result in a warning, reserving them for future use. ```cs #:sdk Microsoft.NET.Sdk.Web -#:property TargetFramework=net11.0 -#:property LangVersion=preview -#:package System.CommandLine=2.0.0-* +#:property TargetFramework net11.0 +#:property LangVersion preview +#:package System.CommandLine 2.0.0-* ``` The value must be separated from the name of the directive by white space and any leading and trailing white space is not considered part of the value. From 1f66c228dcab3aa94a7ddc89b26b5fb22e0a15bf Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Wed, 26 Mar 2025 08:36:07 +0100 Subject: [PATCH 8/8] Simplify example --- documentation/general/dotnet-run-file.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index 741383ca8925..a3458a90f72d 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -183,10 +183,10 @@ Directives `sdk`, `package`, and `property` are translated into `