diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/.editorconfig b/src/Cli/Microsoft.DotNet.FileBasedPrograms/.editorconfig new file mode 100644 index 000000000000..4694508c4da5 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/.editorconfig @@ -0,0 +1,6 @@ +[*.cs] + +# IDE0240: Remove redundant nullable directive +# The directive needs to be included since all sources in a source package are considered generated code +# when referenced from a project via package reference. +dotnet_diagnostic.IDE0240.severity = none \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/ExternalHelpers.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/ExternalHelpers.cs new file mode 100644 index 000000000000..629e4901a7e0 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/ExternalHelpers.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable +using System; +using System.IO; + +namespace Microsoft.DotNet.FileBasedPrograms; + +/// +/// When targeting netstandard2.0, the user of the source package must "implement" certain methods by declaring members in this type. +/// +partial class ExternalHelpers +{ + public static partial int CombineHashCodes(int value1, int value2); + public static partial string GetRelativePath(string relativeTo, string path); + + public static partial bool IsPathFullyQualified(string path); + +#if NET + public static partial int CombineHashCodes(int value1, int value2) + => HashCode.Combine(value1, value2); + + public static partial string GetRelativePath(string relativeTo, string path) + => Path.GetRelativePath(relativeTo, path); + + public static partial bool IsPathFullyQualified(string path) + => Path.IsPathFullyQualified(path); + +#elif FILE_BASED_PROGRAMS_SOURCE_PACKAGE_BUILD + // This path should only be used when we are verifying that the source package itself builds under netstandard2.0. + public static partial int CombineHashCodes(int value1, int value2) + => throw new NotImplementedException(); + + public static partial string GetRelativePath(string relativeTo, string path) + => throw new NotImplementedException(); + + public static partial bool IsPathFullyQualified(string path) + => throw new NotImplementedException(); + +#endif +} + +#if FILE_BASED_PROGRAMS_SOURCE_PACKAGE_GRACEFUL_EXCEPTION +internal class GracefulException : Exception +{ + public GracefulException() + { + } + + public GracefulException(string? message) : base(message) + { + } + + public GracefulException(string format, string arg) : this(string.Format(format, arg)) + { + } + + public GracefulException(string? message, Exception? innerException) : base(message, innerException) + { + } +} +#endif diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx new file mode 100644 index 000000000000..1161700f99d9 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileBasedProgramsResources.resx @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Could not find any project in `{0}`. + + + Could not find project or directory `{0}`. + + + Found more than one project in `{0}`. Specify which one to use. + + + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + error + Used when reporting directive errors like "file(location): error: message". + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + Directives currently cannot contain double quotes ("). + + + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs new file mode 100644 index 000000000000..7a534b1c9fe3 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs @@ -0,0 +1,619 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +#if !FILE_BASED_PROGRAMS_SOURCE_PACKAGE_GRACEFUL_EXCEPTION +using Microsoft.DotNet.Cli.Utils; +#endif + +using Roslyn.Utilities; + +namespace Microsoft.DotNet.FileBasedPrograms; + +internal static class FileLevelDirectiveHelpers +{ + public static SyntaxTokenParser CreateTokenizer(SourceText text) + { + return SyntaxFactory.CreateTokenParser(text, + CSharpParseOptions.Default.WithFeatures([new("FileBasedProgram", "true")])); + } + + /// + /// If , the whole is parsed to find diagnostics about every app directive. + /// Otherwise, only directives up to the first C# token is checked. + /// The former is useful for dotnet project convert where we want to report all errors because it would be difficult to fix them up after the conversion. + /// The latter is useful for dotnet run file.cs where if there are app directives after the first token, + /// compiler reports anyway, so we speed up success scenarios by not parsing the whole file up front in the SDK CLI. + /// + public static ImmutableArray FindDirectives(SourceFile sourceFile, bool reportAllErrors, DiagnosticBag diagnostics) + { + var builder = ImmutableArray.CreateBuilder(); + var tokenizer = CreateTokenizer(sourceFile.Text); + + var result = tokenizer.ParseLeadingTrivia(); + var triviaList = result.Token.LeadingTrivia; + + FindLeadingDirectives(sourceFile, triviaList, diagnostics, builder); + + // In conversion mode, we want to report errors for any invalid directives in the rest of the file + // so users don't end up with invalid directives in the converted project. + if (reportAllErrors) + { + tokenizer.ResetTo(result); + + do + { + result = tokenizer.ParseNextToken(); + + foreach (var trivia in result.Token.LeadingTrivia) + { + ReportErrorFor(trivia); + } + + foreach (var trivia in result.Token.TrailingTrivia) + { + ReportErrorFor(trivia); + } + } + while (!result.Token.IsKind(SyntaxKind.EndOfFileToken)); + } + + void ReportErrorFor(SyntaxTrivia trivia) + { + if (trivia.ContainsDiagnostics && trivia.IsKind(SyntaxKind.IgnoredDirectiveTrivia)) + { + diagnostics.AddError(sourceFile, trivia.Span, FileBasedProgramsResources.CannotConvertDirective); + } + } + + // The result should be ordered by source location, RemoveDirectivesFromFile depends on that. + return builder.ToImmutable(); + } + + /// Finds file-level directives in the leading trivia list of a compilation unit and reports diagnostics on them. + /// The builder to store the parsed directives in, or null if the parsed directives are not needed. + public static void FindLeadingDirectives( + SourceFile sourceFile, + SyntaxTriviaList triviaList, + DiagnosticBag diagnostics, + ImmutableArray.Builder? builder) + { + Debug.Assert(triviaList.Span.Start == 0); + + var deduplicated = new Dictionary(NamedDirectiveComparer.Instance); + TextSpan previousWhiteSpaceSpan = default; + + for (var index = 0; index < triviaList.Count; index++) + { + var trivia = triviaList[index]; + // Stop when the trivia contains an error (e.g., because it's after #if). + if (trivia.ContainsDiagnostics) + { + break; + } + + if (trivia.IsKind(SyntaxKind.WhitespaceTrivia)) + { + Debug.Assert(previousWhiteSpaceSpan.IsEmpty); + previousWhiteSpaceSpan = trivia.FullSpan; + continue; + } + + if (trivia.IsKind(SyntaxKind.ShebangDirectiveTrivia)) + { + TextSpan span = GetFullSpan(previousWhiteSpaceSpan, trivia); + + var whiteSpace = GetWhiteSpaceInfo(triviaList, index); + var info = new CSharpDirective.ParseInfo + { + Span = span, + LeadingWhiteSpace = whiteSpace.Leading, + TrailingWhiteSpace = whiteSpace.Trailing, + }; + builder?.Add(new CSharpDirective.Shebang(info)); + } + else if (trivia.IsKind(SyntaxKind.IgnoredDirectiveTrivia)) + { + TextSpan span = GetFullSpan(previousWhiteSpaceSpan, trivia); + + var message = trivia.GetStructure() is IgnoredDirectiveTriviaSyntax { Content: { RawKind: (int)SyntaxKind.StringLiteralToken } content } + ? content.Text.AsSpan().Trim() + : ""; + // TODO: original impl was using more recent span-oriented APIs. Optimal sharing may be tricky. + // var parts = Patterns.Whitespace.EnumerateSplits(message, 2); + var parts = Patterns.Whitespace.Split(message.ToString(), 2); + var name = parts.Length > 0 ? parts[0] : ""; + var value = parts.Length > 1 ? parts[1] : ""; + Debug.Assert(!(parts.Length > 2)); + + var whiteSpace = GetWhiteSpaceInfo(triviaList, index); + var context = new CSharpDirective.ParseContext + { + Info = new() + { + Span = span, + LeadingWhiteSpace = whiteSpace.Leading, + TrailingWhiteSpace = whiteSpace.Trailing, + }, + Diagnostics = diagnostics, + SourceFile = sourceFile, + DirectiveKind = name, + DirectiveText = value, + }; + + // Block quotes now so we can later support quoted values without a breaking change. https://github.com/dotnet/sdk/issues/49367 + if (value.Contains('"')) + { + diagnostics.AddError(sourceFile, context.Info.Span, FileBasedProgramsResources.QuoteInDirective); + } + + if (CSharpDirective.Parse(context) is { } directive) + { + // If the directive is already present, report an error. + if (deduplicated.ContainsKey(directive)) + { + var existingDirective = deduplicated[directive]; + var typeAndName = $"#:{existingDirective.GetType().Name.ToLowerInvariant()} {existingDirective.Name}"; + diagnostics.AddError(sourceFile, directive.Info.Span, string.Format(FileBasedProgramsResources.DuplicateDirective, typeAndName)); + } + else + { + deduplicated.Add(directive, directive); + } + + builder?.Add(directive); + } + } + + previousWhiteSpaceSpan = default; + } + + return; + + static TextSpan GetFullSpan(TextSpan previousWhiteSpaceSpan, SyntaxTrivia trivia) + { + // Include the preceding whitespace in the span, i.e., span will be the whole line. + return previousWhiteSpaceSpan.IsEmpty ? trivia.FullSpan : TextSpan.FromBounds(previousWhiteSpaceSpan.Start, trivia.FullSpan.End); + } + + static (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) GetWhiteSpaceInfo(in SyntaxTriviaList triviaList, int index) + { + (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) result = default; + + for (int i = index - 1; i >= 0; i--) + { + if (!Fill(ref result.Leading, triviaList, i)) break; + } + + for (int i = index + 1; i < triviaList.Count; i++) + { + if (!Fill(ref result.Trailing, triviaList, i)) break; + } + + return result; + + static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int index) + { + var trivia = triviaList[index]; + if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)) + { + info.LineBreaks += 1; + info.TotalLength += trivia.FullSpan.Length; + return true; + } + + if (trivia.IsKind(SyntaxKind.WhitespaceTrivia)) + { + info.TotalLength += trivia.FullSpan.Length; + return true; + } + + return false; + } + } + } +} + +internal readonly record struct SourceFile(string Path, SourceText Text) +{ + public static SourceFile Load(string filePath) + { + using var stream = File.OpenRead(filePath); + return new SourceFile(filePath, SourceText.From(stream, Encoding.UTF8)); + } + + public SourceFile WithText(SourceText newText) + { + return new SourceFile(Path, newText); + } + + public void Save() + { + using var stream = File.Open(Path, FileMode.Create, FileAccess.Write); + using var writer = new StreamWriter(stream, Encoding.UTF8); + Text.Write(writer); + } + + public FileLinePositionSpan GetFileLinePositionSpan(TextSpan span) + { + return new FileLinePositionSpan(Path, Text.Lines.GetLinePositionSpan(span)); + } + + public string GetLocationString(TextSpan span) + { + var positionSpan = GetFileLinePositionSpan(span); + return $"{positionSpan.Path}({positionSpan.StartLinePosition.Line + 1})"; + } +} + +internal static partial class Patterns +{ + public static Regex Whitespace { get; } = new Regex("""\s+""", RegexOptions.Compiled); + + public static Regex DisallowedNameCharacters { get; } = new Regex("""[\s@=/]""", RegexOptions.Compiled); + + public static Regex EscapedCompilerOption { get; } = new Regex("""^/\w+:".*"$""", RegexOptions.Compiled | RegexOptions.Singleline); +} + +internal struct WhiteSpaceInfo +{ + public int LineBreaks; + public int TotalLength; +} + +/// +/// Represents a C# directive starting with #: (a.k.a., "file-level directive"). +/// Those are ignored by the language but recognized by us. +/// +internal abstract class CSharpDirective(in CSharpDirective.ParseInfo info) +{ + public ParseInfo Info { get; } = info; + + public readonly struct ParseInfo + { + /// + /// Span of the full line including the trailing line break. + /// + public required TextSpan Span { get; init; } + public required WhiteSpaceInfo LeadingWhiteSpace { get; init; } + public required WhiteSpaceInfo TrailingWhiteSpace { get; init; } + } + + public readonly struct ParseContext + { + public required ParseInfo Info { get; init; } + public required DiagnosticBag Diagnostics { get; init; } + public required SourceFile SourceFile { get; init; } + public required string DirectiveKind { get; init; } + public required string DirectiveText { get; init; } + } + + public static Named? Parse(in ParseContext context) + { + return context.DirectiveKind switch + { + "sdk" => Sdk.Parse(context), + "property" => Property.Parse(context), + "package" => Package.Parse(context), + "project" => Project.Parse(context), + var other => context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.UnrecognizedDirective, other)), + }; + } + + private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, char separator) + { + var separatorIndex = context.DirectiveText.IndexOf(separator); + var firstPart = (separatorIndex < 0 ? context.DirectiveText : context.DirectiveText.AsSpan(0, separatorIndex)).TrimEnd(); + + string directiveKind = context.DirectiveKind; + if (firstPart.IsWhiteSpace()) + { + return context.Diagnostics.AddError<(string, string?)?>(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + } + + // If the name contains characters that resemble separators, report an error to avoid any confusion. + if (Patterns.DisallowedNameCharacters.Match(context.DirectiveText, beginning: 0, length: separatorIndex).Success) + { + return context.Diagnostics.AddError<(string, string?)?>(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.InvalidDirectiveName, directiveKind, separator)); + } + + if (separatorIndex < 0) + { + return (firstPart.ToString(), null); + } + + var secondPart = context.DirectiveText.AsSpan(separatorIndex + 1).TrimStart(); + if (secondPart.IsWhiteSpace()) + { + Debug.Assert(secondPart.Length == 0, + "We have trimmed the second part, so if it's white space, it should be actually empty."); + + return (firstPart.ToString(), string.Empty); + } + + return (firstPart.ToString(), secondPart.ToString()); + } + + public abstract override string ToString(); + + /// + /// #! directive. + /// + public sealed class Shebang(in ParseInfo info) : CSharpDirective(info) + { + public override string ToString() => "#!"; + } + + public abstract class Named(in ParseInfo info) : CSharpDirective(info) + { + public required string Name { get; init; } + } + + /// + /// #:sdk directive. + /// + public sealed class Sdk(in ParseInfo info) : Named(info) + { + public string? Version { get; init; } + + public static new Sdk? Parse(in ParseContext context) + { + if (ParseOptionalTwoParts(context, separator: '@') is not var (sdkName, sdkVersion)) + { + return null; + } + + return new Sdk(context.Info) + { + Name = sdkName, + Version = sdkVersion, + }; + } + + public override string ToString() => Version is null ? $"#:sdk {Name}" : $"#:sdk {Name}@{Version}"; + } + + /// + /// #:property directive. + /// + public sealed class Property(in ParseInfo info) : Named(info) + { + public required string Value { get; init; } + + public static new Property? Parse(in ParseContext context) + { + if (ParseOptionalTwoParts(context, separator: '=') is not var (propertyName, propertyValue)) + { + return null; + } + + if (propertyValue is null) + { + return context.Diagnostics.AddError(context.SourceFile, context.Info.Span, FileBasedProgramsResources.PropertyDirectiveMissingParts); + } + + try + { + propertyName = XmlConvert.VerifyName(propertyName); + } + catch (XmlException ex) + { + return context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, ex.Message), ex); + } + + if (propertyName.Equals("RestoreUseStaticGraphEvaluation", StringComparison.OrdinalIgnoreCase) && + MSBuildUtilities.ConvertStringToBool(propertyValue)) + { + context.Diagnostics.AddError(context.SourceFile, context.Info.Span, FileBasedProgramsResources.StaticGraphRestoreNotSupported); + } + + return new Property(context.Info) + { + Name = propertyName, + Value = propertyValue, + }; + } + + public override string ToString() => $"#:property {Name}={Value}"; + } + + /// + /// #:package directive. + /// + public sealed class Package(in ParseInfo info) : Named(info) + { + public string? Version { get; init; } + + public static new Package? Parse(in ParseContext context) + { + if (ParseOptionalTwoParts(context, separator: '@') is not var (packageName, packageVersion)) + { + return null; + } + + return new Package(context.Info) + { + Name = packageName, + Version = packageVersion, + }; + } + + public override string ToString() => Version is null ? $"#:package {Name}" : $"#:package {Name}@{Version}"; + } + + /// + /// #:project directive. + /// + public sealed class Project(in ParseInfo info) : Named(info) + { + public static new Project? Parse(in ParseContext context) + { + var directiveText = context.DirectiveText; + if (directiveText.IsWhiteSpace()) + { + string directiveKind = context.DirectiveKind; + return context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + } + + try + { + // If the path is a directory like '../lib', transform it to a project file path like '../lib/lib.csproj'. + // Also normalize blackslashes to forward slashes to ensure the directive works on all platforms. + var sourceDirectory = Path.GetDirectoryName(context.SourceFile.Path) ?? "."; + var resolvedProjectPath = Path.Combine(sourceDirectory, directiveText.Replace('\\', '/')); + if (Directory.Exists(resolvedProjectPath)) + { + var fullFilePath = GetProjectFileFromDirectory(resolvedProjectPath).FullName; + + // Keep a relative path only if the original directive was a relative path. + directiveText = ExternalHelpers.IsPathFullyQualified(directiveText) + ? fullFilePath + : ExternalHelpers.GetRelativePath(relativeTo: sourceDirectory, fullFilePath); + } + else if (!File.Exists(resolvedProjectPath)) + { + throw new GracefulException(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, resolvedProjectPath); + } + } + catch (GracefulException e) + { + context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.InvalidProjectDirective, e.Message), e); + } + + return new Project(context.Info) + { + Name = directiveText, + }; + } + + public Project WithName(string name) + { + return new Project(Info) { Name = name }; + } + + public static FileInfo GetProjectFileFromDirectory(string projectDirectory) + { + DirectoryInfo dir; + try + { + dir = new DirectoryInfo(projectDirectory); + } + catch (ArgumentException) + { + throw new GracefulException(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, projectDirectory); + } + + if (!dir.Exists) + { + throw new GracefulException(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, projectDirectory); + } + + FileInfo[] files = dir.GetFiles("*proj"); + if (files.Length == 0) + { + throw new GracefulException( + FileBasedProgramsResources.CouldNotFindAnyProjectInDirectory, + projectDirectory); + } + + if (files.Length > 1) + { + throw new GracefulException(FileBasedProgramsResources.MoreThanOneProjectInDirectory, projectDirectory); + } + + return files.First(); + } + + public override string ToString() => $"#:project {Name}"; + } +} + +/// +/// Used for deduplication - compares directives by their type and name (ignoring case). +/// +internal sealed class NamedDirectiveComparer : IEqualityComparer +{ + public static readonly NamedDirectiveComparer Instance = new(); + + private NamedDirectiveComparer() { } + + public bool Equals(CSharpDirective.Named? x, CSharpDirective.Named? y) + { + if (ReferenceEquals(x, y)) return true; + + if (x is null || y is null) return false; + + return x.GetType() == y.GetType() && + StringComparer.OrdinalIgnoreCase.Equals(x.Name, y.Name); + } + + public int GetHashCode(CSharpDirective.Named obj) + { + return ExternalHelpers.CombineHashCodes( + obj.GetType().GetHashCode(), + StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name)); + } +} + +internal sealed class SimpleDiagnostic +{ + public required Position Location { get; init; } + public required string Message { get; init; } + + /// + /// An adapter of that ensures we JSON-serialize only the necessary fields. + /// + public readonly struct Position + { + public required TextSpan TextSpan { get; init; } + public required LinePositionSpan Span { get; init; } + public required string Path { get; init; } + } +} + +internal readonly struct DiagnosticBag +{ + public bool IgnoreDiagnostics { get; private init; } + + /// + /// If and is , the first diagnostic is thrown as . + /// + public ImmutableArray.Builder? Builder { get; private init; } + + public static DiagnosticBag ThrowOnFirst() => default; + public static DiagnosticBag Collect(out ImmutableArray.Builder builder) => new() { Builder = builder = ImmutableArray.CreateBuilder() }; + public static DiagnosticBag Ignore() => new() { IgnoreDiagnostics = true, Builder = null }; + + public void AddError(SourceFile sourceFile, TextSpan textSpan, string message, Exception? inner = null) + { + if (Builder != null) + { + Debug.Assert(!IgnoreDiagnostics); + Builder.Add(new SimpleDiagnostic { Location = new SimpleDiagnostic.Position() { Path = sourceFile.Path, TextSpan = textSpan, Span = sourceFile.GetFileLinePositionSpan(textSpan).Span }, Message = message }); + } + else if (!IgnoreDiagnostics) + { + throw new GracefulException($"{sourceFile.GetLocationString(textSpan)}: {FileBasedProgramsResources.DirectiveError}: {message}", inner); + } + } + + public T? AddError(SourceFile sourceFile, TextSpan span, string message, Exception? inner = null) + { + AddError(sourceFile, span, message, inner); + return default; + } +} diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/MSBuildUtilities.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/MSBuildUtilities.cs new file mode 100644 index 000000000000..17884509a0ab --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/MSBuildUtilities.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// TODO: consider if this extra copy of the file can be avoided. +// It's really not that much code, so, if we can't solve the reuse issue, we can live with that. +#nullable enable +using System; + +namespace Microsoft.DotNet.FileBasedPrograms +{ + /// + /// Internal utilities copied from microsoft/MSBuild repo. + /// + class MSBuildUtilities + { + /// + /// Converts a string to a bool. We consider "true/false", "on/off", and + /// "yes/no" to be valid boolean representations in the XML. + /// Modified from its original version to not throw, but return a default value. + /// + /// The string to convert. + /// Boolean true or false, corresponding to the string. + internal static bool ConvertStringToBool(string? parameterValue, bool defaultValue = false) + { + if (string.IsNullOrEmpty(parameterValue)) + { + return defaultValue; + } + else if (ValidBooleanTrue(parameterValue)) + { + return true; + } + else if (ValidBooleanFalse(parameterValue)) + { + return false; + } + else + { + // Unsupported boolean representation. + return defaultValue; + } + } + + /// + /// Returns true if the string represents a valid MSBuild boolean true value, + /// such as "on", "!false", "yes" + /// + private static bool ValidBooleanTrue(string? parameterValue) + { + return ((string.Compare(parameterValue, "true", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(parameterValue, "on", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(parameterValue, "yes", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(parameterValue, "!false", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(parameterValue, "!off", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(parameterValue, "!no", StringComparison.OrdinalIgnoreCase) == 0)); + } + + /// + /// Returns true if the string represents a valid MSBuild boolean false value, + /// such as "!on" "off" "no" "!true" + /// + private static bool ValidBooleanFalse(string? parameterValue) + { + return ((string.Compare(parameterValue, "false", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(parameterValue, "off", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(parameterValue, "no", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(parameterValue, "!true", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(parameterValue, "!on", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(parameterValue, "!yes", StringComparison.OrdinalIgnoreCase) == 0)); + } + } +} diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.Package.csproj b/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.Package.csproj new file mode 100644 index 000000000000..3438f8231ca3 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.Package.csproj @@ -0,0 +1,64 @@ + + + + + $(VisualStudioServiceTargetFramework);netstandard2.0 + false + none + false + preview + + + true + true + Microsoft.DotNet.FileBasedPrograms + false + Package containing sources for file-based programs support. + + $(NoWarn);NU5128 + false + $(DefineConstants);FILE_BASED_PROGRAMS_SOURCE_PACKAGE_BUILD;FILE_BASED_PROGRAMS_SOURCE_PACKAGE_GRACEFUL_EXCEPTION + + disable + + + + + + + + + + + + + + + External\%(NuGetPackageId)\%(Link) + + + + + + + + + true + contentFiles\cs\any\FileBasedProgramsResources.resx + + + true + contentFiles\cs\any\xlf + + + + + + + <_PackageFiles Remove="@(_PackageFiles)" Condition="$([System.String]::Copy('%(_PackageFiles.Identity)').EndsWith('FileBasedProgramsResources.cs'))" /> + + + + diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.projitems b/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.projitems new file mode 100644 index 000000000000..31ce6cd9eabc --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.projitems @@ -0,0 +1,15 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + 374C251E-BF99-45B2-A58E-40229ED8AACA + + + Microsoft.DotNet.FileBasedPrograms + + + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.shproj b/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.shproj new file mode 100644 index 000000000000..68cb2e509ef5 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/Microsoft.DotNet.FileBasedPrograms.shproj @@ -0,0 +1,13 @@ + + + + 374C251E-BF99-45B2-A58E-40229ED8AACA + 14.0 + + + + + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/README.md b/src/Cli/Microsoft.DotNet.FileBasedPrograms/README.md new file mode 100644 index 000000000000..adfe2440e4e4 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/README.md @@ -0,0 +1,16 @@ +# Microsoft.DotNet.FileBasedPrograms Source Package + +This is a source package containing shared code for [file-based programs](../../../documentation/general/dotnet-run-file.md) support. This package is intended only for internal use by .NET components. + +## Usage in Consuming Projects + +To use this package in your project, add the following to your `.csproj` file: + +```xml + + + + +``` diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf new file mode 100644 index 000000000000..f07fc11a51a0 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf new file mode 100644 index 000000000000..42ffde6cc3e1 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf new file mode 100644 index 000000000000..2e96d37d210f --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf new file mode 100644 index 000000000000..20b54f93808a --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf new file mode 100644 index 000000000000..462162003052 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf new file mode 100644 index 000000000000..ea875f298625 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf new file mode 100644 index 000000000000..40f63bc13679 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf new file mode 100644 index 000000000000..0da6669901ea --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf new file mode 100644 index 000000000000..10dead880ffe --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf new file mode 100644 index 000000000000..b1172f076b24 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf new file mode 100644 index 000000000000..a441afa030a2 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf new file mode 100644 index 000000000000..d44714e24f94 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf new file mode 100644 index 000000000000..7fcdd70a7ac7 --- /dev/null +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf @@ -0,0 +1,77 @@ + + + + + + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + Some directives cannot be converted. Run the file to see all compilation errors. Specify '--force' to convert anyway. + {Locked="--force"} + + + Could not find any project in `{0}`. + Could not find any project in `{0}`. + + + + Could not find project or directory `{0}`. + Could not find project or directory `{0}`. + + + + error + error + Used when reporting directive errors like "file(location): error: message". + + + Duplicate directives are not supported: {0} + Duplicate directives are not supported: {0} + {0} is the directive type and name. + + + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. + {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + + The '#:project' directive is invalid: {0} + The '#:project' directive is invalid: {0} + {0} is the inner error message. + + + Missing name of '{0}'. + Missing name of '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + Found more than one project in `{0}`. Specify which one to use. + Found more than one project in `{0}`. Specify which one to use. + + + + Invalid property name: {0} + Invalid property name: {0} + {0} is an inner exception message. + + + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. + {Locked="#:property"} + + + Directives currently cannot contain double quotes ("). + Directives currently cannot contain double quotes ("). + + + + Static graph restore is not supported for file-based apps. Remove the '#:property'. + Static graph restore is not supported for file-based apps. Remove the '#:property'. + {Locked="#:property"} + + + Unrecognized directive '{0}'. + Unrecognized directive '{0}'. + {0} is the directive name like 'package' or 'sdk'. + + + + \ No newline at end of file diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs index dc94f892abd1..93bb43ef8c24 100644 --- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs +++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommand.cs @@ -11,6 +11,7 @@ using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.FileBasedPrograms; using NuGet.ProjectModel; namespace Microsoft.DotNet.Cli.Commands.Package.Add; diff --git a/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs b/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs index 2706bed56b21..6f3ed29dad68 100644 --- a/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs +++ b/src/Cli/dotnet/Commands/Package/Remove/PackageRemoveCommand.cs @@ -7,6 +7,7 @@ using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.FileBasedPrograms; namespace Microsoft.DotNet.Cli.Commands.Package.Remove; diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs index f1bc3ab7d655..e41367a34fd0 100644 --- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs @@ -7,6 +7,7 @@ using Microsoft.Build.Evaluation; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.FileBasedPrograms; using Microsoft.TemplateEngine.Cli.Commands; namespace Microsoft.DotNet.Cli.Commands.Project.Convert; @@ -30,7 +31,7 @@ public override int Execute() // Find directives (this can fail, so do this before creating the target directory). var sourceFile = SourceFile.Load(file); - var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !_force, DiagnosticBag.ThrowOnFirst()); + var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: !_force, DiagnosticBag.ThrowOnFirst()); // Create a project instance for evaluation. var projectCollection = new ProjectCollection(); diff --git a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs index 286a7972f6f3..7cc8e6bddd86 100644 --- a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs +++ b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs @@ -8,6 +8,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.FileBasedPrograms; namespace Microsoft.DotNet.Cli.Commands.Run.Api; @@ -64,7 +65,7 @@ public sealed class GetProject : RunApiInput public override RunApiOutput Execute() { var sourceFile = SourceFile.Load(EntryPointFileFullPath); - var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: true, DiagnosticBag.Collect(out var diagnostics)); + var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: true, DiagnosticBag.Collect(out var diagnostics)); string artifactsPath = ArtifactsPath ?? VirtualProjectBuildingCommand.GetArtifactsPath(EntryPointFileFullPath); var csprojWriter = new StringWriter(); @@ -160,6 +161,9 @@ public sealed class RunCommand : RunApiOutput } } +// TODO: getting the following error, which is not suppressible with pragma. +// error SYSLIB1225: The type 'Encoding' includes the ref like property, field or constructor parameter 'Preamble'. No source code will be generated for the property, field or constructor. (https://learn.microsoft.com/dotnet/fundamentals/syslib-diagnostics/syslib1225) +// I have no idea how the type 'Encoding' is ending up getting used as a result of my change. [JsonSerializable(typeof(RunApiInput))] [JsonSerializable(typeof(RunApiOutput))] internal partial class RunFileApiJsonSerializerContext : JsonSerializerContext; diff --git a/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs index 45fa0380aeea..0422f389a824 100644 --- a/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs +++ b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using Microsoft.DotNet.FileBasedPrograms; namespace Microsoft.DotNet.Cli.Commands.Run; @@ -33,7 +34,7 @@ public ImmutableArray Directives { if (field.IsDefault) { - field = VirtualProjectBuildingCommand.FindDirectives(SourceFile, reportAllErrors: false, DiagnosticBag.Ignore()); + field = FileLevelDirectiveHelpers.FindDirectives(SourceFile, reportAllErrors: false, DiagnosticBag.Ignore()); Debug.Assert(!field.IsDefault); } @@ -125,7 +126,7 @@ existingDirective is CSharpDirective.Named existingNamed && // Otherwise, we will add the directive to the top of the file. int start = 0; - var tokenizer = VirtualProjectBuildingCommand.CreateTokenizer(SourceFile.Text); + var tokenizer = FileLevelDirectiveHelpers.CreateTokenizer(SourceFile.Text); var result = tokenizer.ParseNextToken(); var leadingTrivia = result.Token.LeadingTrivia; diff --git a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs index 1e58bdd27c05..3a42cd94c06a 100644 --- a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs +++ b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs @@ -6,6 +6,7 @@ using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.FileBasedPrograms; namespace Microsoft.DotNet.Cli.Commands.Run; diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index bdd97d2402b7..7a4ff1295838 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -26,6 +26,7 @@ using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; +using Microsoft.DotNet.FileBasedPrograms; namespace Microsoft.DotNet.Cli.Commands.Run; @@ -171,7 +172,7 @@ public ImmutableArray Directives if (field.IsDefault) { var sourceFile = SourceFile.Load(EntryPointFileFullPath); - field = FindDirectives(sourceFile, reportAllErrors: false, DiagnosticBag.ThrowOnFirst()); + field = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: false, DiagnosticBag.ThrowOnFirst()); Debug.Assert(!field.IsDefault); } @@ -1431,186 +1432,6 @@ static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk s } } - public static SyntaxTokenParser CreateTokenizer(SourceText text) - { - return SyntaxFactory.CreateTokenParser(text, - CSharpParseOptions.Default.WithFeatures([new("FileBasedProgram", "true")])); - } - - /// - /// If , the whole is parsed to find diagnostics about every app directive. - /// Otherwise, only directives up to the first C# token is checked. - /// The former is useful for dotnet project convert where we want to report all errors because it would be difficult to fix them up after the conversion. - /// The latter is useful for dotnet run file.cs where if there are app directives after the first token, - /// compiler reports anyway, so we speed up success scenarios by not parsing the whole file up front in the SDK CLI. - /// - public static ImmutableArray FindDirectives(SourceFile sourceFile, bool reportAllErrors, DiagnosticBag diagnostics) - { - var deduplicated = new HashSet(NamedDirectiveComparer.Instance); - var builder = ImmutableArray.CreateBuilder(); - var tokenizer = CreateTokenizer(sourceFile.Text); - - var result = tokenizer.ParseLeadingTrivia(); - TextSpan previousWhiteSpaceSpan = default; - var triviaList = result.Token.LeadingTrivia; - foreach (var (index, trivia) in triviaList.Index()) - { - // Stop when the trivia contains an error (e.g., because it's after #if). - if (trivia.ContainsDiagnostics) - { - break; - } - - if (trivia.IsKind(SyntaxKind.WhitespaceTrivia)) - { - Debug.Assert(previousWhiteSpaceSpan.IsEmpty); - previousWhiteSpaceSpan = trivia.FullSpan; - continue; - } - - if (trivia.IsKind(SyntaxKind.ShebangDirectiveTrivia)) - { - TextSpan span = GetFullSpan(previousWhiteSpaceSpan, trivia); - - var whiteSpace = GetWhiteSpaceInfo(triviaList, index); - var info = new CSharpDirective.ParseInfo - { - Span = span, - LeadingWhiteSpace = whiteSpace.Leading, - TrailingWhiteSpace = whiteSpace.Trailing, - }; - builder.Add(new CSharpDirective.Shebang(info)); - } - else if (trivia.IsKind(SyntaxKind.IgnoredDirectiveTrivia)) - { - TextSpan span = GetFullSpan(previousWhiteSpaceSpan, trivia); - - var message = trivia.GetStructure() is IgnoredDirectiveTriviaSyntax { Content: { RawKind: (int)SyntaxKind.StringLiteralToken } content } - ? content.Text.AsSpan().Trim() - : ""; - var parts = Patterns.Whitespace.EnumerateSplits(message, 2); - var name = parts.MoveNext() ? message[parts.Current] : default; - var value = parts.MoveNext() ? message[parts.Current] : default; - Debug.Assert(!parts.MoveNext()); - - var whiteSpace = GetWhiteSpaceInfo(triviaList, index); - var context = new CSharpDirective.ParseContext - { - Info = new() - { - Span = span, - LeadingWhiteSpace = whiteSpace.Leading, - TrailingWhiteSpace = whiteSpace.Trailing, - }, - Diagnostics = diagnostics, - SourceFile = sourceFile, - DirectiveKind = name.ToString(), - DirectiveText = value.ToString(), - }; - - // Block quotes now so we can later support quoted values without a breaking change. https://github.com/dotnet/sdk/issues/49367 - if (value.Contains('"')) - { - diagnostics.AddError(sourceFile, context.Info.Span, CliCommandStrings.QuoteInDirective); - } - - if (CSharpDirective.Parse(context) is { } directive) - { - // If the directive is already present, report an error. - if (deduplicated.TryGetValue(directive, out var existingDirective)) - { - var typeAndName = $"#:{existingDirective.GetType().Name.ToLowerInvariant()} {existingDirective.Name}"; - diagnostics.AddError(sourceFile, directive.Info.Span, string.Format(CliCommandStrings.DuplicateDirective, typeAndName)); - } - else - { - deduplicated.Add(directive); - } - - builder.Add(directive); - } - } - - previousWhiteSpaceSpan = default; - } - - // In conversion mode, we want to report errors for any invalid directives in the rest of the file - // so users don't end up with invalid directives in the converted project. - if (reportAllErrors) - { - tokenizer.ResetTo(result); - - do - { - result = tokenizer.ParseNextToken(); - - foreach (var trivia in result.Token.LeadingTrivia) - { - ReportErrorFor(trivia); - } - - foreach (var trivia in result.Token.TrailingTrivia) - { - ReportErrorFor(trivia); - } - } - while (!result.Token.IsKind(SyntaxKind.EndOfFileToken)); - } - - // The result should be ordered by source location, RemoveDirectivesFromFile depends on that. - return builder.ToImmutable(); - - static TextSpan GetFullSpan(TextSpan previousWhiteSpaceSpan, SyntaxTrivia trivia) - { - // Include the preceding whitespace in the span, i.e., span will be the whole line. - return previousWhiteSpaceSpan.IsEmpty ? trivia.FullSpan : TextSpan.FromBounds(previousWhiteSpaceSpan.Start, trivia.FullSpan.End); - } - - void ReportErrorFor(SyntaxTrivia trivia) - { - if (trivia.ContainsDiagnostics && trivia.IsKind(SyntaxKind.IgnoredDirectiveTrivia)) - { - diagnostics.AddError(sourceFile, trivia.Span, CliCommandStrings.CannotConvertDirective); - } - } - - static (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) GetWhiteSpaceInfo(in SyntaxTriviaList triviaList, int index) - { - (WhiteSpaceInfo Leading, WhiteSpaceInfo Trailing) result = default; - - for (int i = index - 1; i >= 0; i--) - { - if (!Fill(ref result.Leading, triviaList, i)) break; - } - - for (int i = index + 1; i < triviaList.Count; i++) - { - if (!Fill(ref result.Trailing, triviaList, i)) break; - } - - return result; - - static bool Fill(ref WhiteSpaceInfo info, in SyntaxTriviaList triviaList, int index) - { - var trivia = triviaList[index]; - if (trivia.IsKind(SyntaxKind.EndOfLineTrivia)) - { - info.LineBreaks += 1; - info.TotalLength += trivia.FullSpan.Length; - return true; - } - - if (trivia.IsKind(SyntaxKind.WhitespaceTrivia)) - { - info.TotalLength += trivia.FullSpan.Length; - return true; - } - - return false; - } - } - } - public static SourceText? RemoveDirectivesFromFile(ImmutableArray directives, SourceText text) { if (directives.Length == 0) @@ -1664,371 +1485,6 @@ public static bool IsValidEntryPointPath(string entryPointFilePath) } } -internal readonly record struct SourceFile(string Path, SourceText Text) -{ - public static SourceFile Load(string filePath) - { - using var stream = File.OpenRead(filePath); - return new SourceFile(filePath, SourceText.From(stream, Encoding.UTF8)); - } - - public SourceFile WithText(SourceText newText) - { - return new SourceFile(Path, newText); - } - - public void Save() - { - using var stream = File.Open(Path, FileMode.Create, FileAccess.Write); - using var writer = new StreamWriter(stream, Encoding.UTF8); - Text.Write(writer); - } - - public FileLinePositionSpan GetFileLinePositionSpan(TextSpan span) - { - return new FileLinePositionSpan(Path, Text.Lines.GetLinePositionSpan(span)); - } - - public string GetLocationString(TextSpan span) - { - var positionSpan = GetFileLinePositionSpan(span); - return $"{positionSpan.Path}({positionSpan.StartLinePosition.Line + 1})"; - } -} - -internal static partial class Patterns -{ - [GeneratedRegex("""\s+""")] - public static partial Regex Whitespace { get; } - - [GeneratedRegex("""[\s@=/]""")] - public static partial Regex DisallowedNameCharacters { get; } - - [GeneratedRegex("""^/\w+:".*"$""", RegexOptions.Singleline)] - public static partial Regex EscapedCompilerOption { get; } -} - -internal struct WhiteSpaceInfo -{ - public int LineBreaks; - public int TotalLength; -} - -/// -/// Represents a C# directive starting with #: (a.k.a., "file-level directive"). -/// Those are ignored by the language but recognized by us. -/// -internal abstract class CSharpDirective(in CSharpDirective.ParseInfo info) -{ - public ParseInfo Info { get; } = info; - - public readonly struct ParseInfo - { - /// - /// Span of the full line including the trailing line break. - /// - public required TextSpan Span { get; init; } - public required WhiteSpaceInfo LeadingWhiteSpace { get; init; } - public required WhiteSpaceInfo TrailingWhiteSpace { get; init; } - } - - public readonly struct ParseContext - { - public required ParseInfo Info { get; init; } - public required DiagnosticBag Diagnostics { get; init; } - public required SourceFile SourceFile { get; init; } - public required string DirectiveKind { get; init; } - public required string DirectiveText { get; init; } - } - - public static Named? Parse(in ParseContext context) - { - return context.DirectiveKind switch - { - "sdk" => Sdk.Parse(context), - "property" => Property.Parse(context), - "package" => Package.Parse(context), - "project" => Project.Parse(context), - var other => context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(CliCommandStrings.UnrecognizedDirective, other)), - }; - } - - private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, char separator) - { - var separatorIndex = context.DirectiveText.IndexOf(separator, StringComparison.Ordinal); - var firstPart = (separatorIndex < 0 ? context.DirectiveText : context.DirectiveText.AsSpan(..separatorIndex)).TrimEnd(); - - string directiveKind = context.DirectiveKind; - if (firstPart.IsWhiteSpace()) - { - return context.Diagnostics.AddError<(string, string?)?>(context.SourceFile, context.Info.Span, string.Format(CliCommandStrings.MissingDirectiveName, directiveKind)); - } - - // If the name contains characters that resemble separators, report an error to avoid any confusion. - if (Patterns.DisallowedNameCharacters.IsMatch(firstPart)) - { - return context.Diagnostics.AddError<(string, string?)?>(context.SourceFile, context.Info.Span, string.Format(CliCommandStrings.InvalidDirectiveName, directiveKind, separator)); - } - - if (separatorIndex < 0) - { - return (firstPart.ToString(), null); - } - - var secondPart = context.DirectiveText.AsSpan((separatorIndex + 1)..).TrimStart(); - if (secondPart.IsWhiteSpace()) - { - Debug.Assert(secondPart.Length == 0, - "We have trimmed the second part, so if it's white space, it should be actually empty."); - - return (firstPart.ToString(), string.Empty); - } - - return (firstPart.ToString(), secondPart.ToString()); - } - - public abstract override string ToString(); - - /// - /// #! directive. - /// - public sealed class Shebang(in ParseInfo info) : CSharpDirective(info) - { - public override string ToString() => "#!"; - } - - public abstract class Named(in ParseInfo info) : CSharpDirective(info) - { - public required string Name { get; init; } - } - - /// - /// #:sdk directive. - /// - public sealed class Sdk(in ParseInfo info) : Named(info) - { - public string? Version { get; init; } - - public static new Sdk? Parse(in ParseContext context) - { - if (ParseOptionalTwoParts(context, separator: '@') is not var (sdkName, sdkVersion)) - { - return null; - } - - return new Sdk(context.Info) - { - Name = sdkName, - Version = sdkVersion, - }; - } - - public override string ToString() => Version is null ? $"#:sdk {Name}" : $"#:sdk {Name}@{Version}"; - } - - /// - /// #:property directive. - /// - public sealed class Property(in ParseInfo info) : Named(info) - { - public required string Value { get; init; } - - public static new Property? Parse(in ParseContext context) - { - if (ParseOptionalTwoParts(context, separator: '=') is not var (propertyName, propertyValue)) - { - return null; - } - - if (propertyValue is null) - { - return context.Diagnostics.AddError(context.SourceFile, context.Info.Span, CliCommandStrings.PropertyDirectiveMissingParts); - } - - try - { - propertyName = XmlConvert.VerifyName(propertyName); - } - catch (XmlException ex) - { - return context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(CliCommandStrings.PropertyDirectiveInvalidName, ex.Message), ex); - } - - if (propertyName.Equals("RestoreUseStaticGraphEvaluation", StringComparison.OrdinalIgnoreCase) && - MSBuildUtilities.ConvertStringToBool(propertyValue)) - { - context.Diagnostics.AddError(context.SourceFile, context.Info.Span, CliCommandStrings.StaticGraphRestoreNotSupported); - } - - return new Property(context.Info) - { - Name = propertyName, - Value = propertyValue, - }; - } - - public override string ToString() => $"#:property {Name}={Value}"; - } - - /// - /// #:package directive. - /// - public sealed class Package(in ParseInfo info) : Named(info) - { - public string? Version { get; init; } - - public static new Package? Parse(in ParseContext context) - { - if (ParseOptionalTwoParts(context, separator: '@') is not var (packageName, packageVersion)) - { - return null; - } - - return new Package(context.Info) - { - Name = packageName, - Version = packageVersion, - }; - } - - public override string ToString() => Version is null ? $"#:package {Name}" : $"#:package {Name}@{Version}"; - } - - /// - /// #:project directive. - /// - public sealed class Project(in ParseInfo info) : Named(info) - { - public static new Project? Parse(in ParseContext context) - { - var directiveText = context.DirectiveText; - if (directiveText.IsWhiteSpace()) - { - string directiveKind = context.DirectiveKind; - return context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(CliCommandStrings.MissingDirectiveName, directiveKind)); - } - - try - { - // If the path is a directory like '../lib', transform it to a project file path like '../lib/lib.csproj'. - // Also normalize blackslashes to forward slashes to ensure the directive works on all platforms. - var sourceDirectory = Path.GetDirectoryName(context.SourceFile.Path) ?? "."; - var resolvedProjectPath = Path.Combine(sourceDirectory, directiveText.Replace('\\', '/')); - if (Directory.Exists(resolvedProjectPath)) - { - var fullFilePath = MsbuildProject.GetProjectFileFromDirectory(resolvedProjectPath).FullName; - - // Keep a relative path only if the original directive was a relative path. - directiveText = Path.IsPathFullyQualified(directiveText) - ? fullFilePath - : Path.GetRelativePath(relativeTo: sourceDirectory, fullFilePath); - } - else if (!File.Exists(resolvedProjectPath)) - { - throw new GracefulException(CliStrings.CouldNotFindProjectOrDirectory, resolvedProjectPath); - } - } - catch (GracefulException e) - { - context.Diagnostics.AddError(context.SourceFile, context.Info.Span, string.Format(CliCommandStrings.InvalidProjectDirective, e.Message), e); - } - - return new Project(context.Info) - { - Name = directiveText, - }; - } - - public Project WithName(string name) - { - return new Project(Info) { Name = name }; - } - - public override string ToString() => $"#:project {Name}"; - } -} - -/// -/// Used for deduplication - compares directives by their type and name (ignoring case). -/// -internal sealed class NamedDirectiveComparer : IEqualityComparer -{ - public static readonly NamedDirectiveComparer Instance = new(); - - private NamedDirectiveComparer() { } - - public bool Equals(CSharpDirective.Named? x, CSharpDirective.Named? y) - { - if (ReferenceEquals(x, y)) return true; - - if (x is null || y is null) return false; - - return x.GetType() == y.GetType() && - string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase); - } - - public int GetHashCode(CSharpDirective.Named obj) - { - return HashCode.Combine( - obj.GetType().GetHashCode(), - obj.Name.GetHashCode(StringComparison.OrdinalIgnoreCase)); - } -} - -internal sealed class SimpleDiagnostic -{ - public required Position Location { get; init; } - public required string Message { get; init; } - - /// - /// An adapter of that ensures we JSON-serialize only the necessary fields. - /// - public readonly struct Position - { - public string Path { get; init; } - public LinePositionSpan Span { get; init; } - - public static implicit operator Position(FileLinePositionSpan fileLinePositionSpan) => new() - { - Path = fileLinePositionSpan.Path, - Span = fileLinePositionSpan.Span, - }; - } -} - -internal readonly struct DiagnosticBag -{ - public bool IgnoreDiagnostics { get; private init; } - - /// - /// If and is , the first diagnostic is thrown as . - /// - public ImmutableArray.Builder? Builder { get; private init; } - - public static DiagnosticBag ThrowOnFirst() => default; - public static DiagnosticBag Collect(out ImmutableArray.Builder builder) => new() { Builder = builder = ImmutableArray.CreateBuilder() }; - public static DiagnosticBag Ignore() => new() { IgnoreDiagnostics = true, Builder = null }; - - public void AddError(SourceFile sourceFile, TextSpan span, string message, Exception? inner = null) - { - if (Builder != null) - { - Debug.Assert(!IgnoreDiagnostics); - Builder.Add(new SimpleDiagnostic { Location = sourceFile.GetFileLinePositionSpan(span), Message = message }); - } - else if (!IgnoreDiagnostics) - { - throw new GracefulException($"{sourceFile.GetLocationString(span)}: {CliCommandStrings.DirectiveError}: {message}", inner); - } - } - - public T? AddError(SourceFile sourceFile, TextSpan span, string message, Exception? inner = null) - { - AddError(sourceFile, span, message, inner); - return default; - } -} - internal sealed class RunFileBuildCacheEntry { private static StringComparer GlobalPropertiesComparer => StringComparer.OrdinalIgnoreCase; diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index 2e63f9a3c5a8..d72287d41799 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -24,7 +24,6 @@ - @@ -107,4 +106,5 @@ OverwriteReadOnlyFiles="true" /> + diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs index 72a600549d62..82043b75c004 100644 --- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs +++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs @@ -9,6 +9,7 @@ using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Run.Tests; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.FileBasedPrograms; namespace Microsoft.DotNet.Cli.Project.Convert.Tests; @@ -1621,7 +1622,7 @@ private static void Convert(string inputCSharp, out string actualProject, out st var sourceFile = new SourceFile(filePath ?? programPath, SourceText.From(inputCSharp, Encoding.UTF8)); actualDiagnostics = null; var diagnosticBag = collectDiagnostics ? DiagnosticBag.Collect(out actualDiagnostics) : DiagnosticBag.ThrowOnFirst(); - var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !force, diagnosticBag); + var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: !force, diagnosticBag); var projectWriter = new StringWriter(); VirtualProjectBuildingCommand.WriteProjectFile(projectWriter, directives, isVirtualProject: false); actualProject = projectWriter.ToString(); @@ -1650,7 +1651,7 @@ private static void VerifyConversionThrows(string inputCSharp, string expectedWi private static void VerifyDirectiveConversionErrors(string inputCSharp, IEnumerable<(int LineNumber, string Message)> expectedErrors) { var sourceFile = new SourceFile(programPath, SourceText.From(inputCSharp, Encoding.UTF8)); - VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: true, DiagnosticBag.Collect(out var diagnostics)); + FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: true, DiagnosticBag.Collect(out var diagnostics)); VerifyErrors(diagnostics, expectedErrors); } diff --git a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs index 371b29d7f314..f6b181615b72 100644 --- a/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/FileBasedAppSourceEditorTests.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.FileBasedPrograms; namespace Microsoft.DotNet.Cli.Run.Tests; diff --git a/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs b/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs index 08d323f2cc28..edaf986abdfa 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunTelemetryTests.cs @@ -7,6 +7,7 @@ using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.FileBasedPrograms; namespace Microsoft.DotNet.Cli.Run.Tests;