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;