diff --git a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs index 681843c6cbc8..3ccfbc535f8f 100644 --- a/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs +++ b/src/Cli/Microsoft.DotNet.FileBasedPrograms/FileLevelDirectiveHelpers.cs @@ -36,7 +36,7 @@ public static SyntaxTokenParser CreateTokenizer(SourceText text) /// 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, ErrorReporter reportError) + public static ImmutableArray FindDirectives(SourceFile sourceFile, bool reportAllErrors, ErrorReporter errorReporter) { var builder = ImmutableArray.CreateBuilder(); var tokenizer = CreateTokenizer(sourceFile.Text); @@ -44,7 +44,7 @@ public static ImmutableArray FindDirectives(SourceFile sourceFi var result = tokenizer.ParseLeadingTrivia(); var triviaList = result.Token.LeadingTrivia; - FindLeadingDirectives(sourceFile, triviaList, reportError, builder); + FindLeadingDirectives(sourceFile, triviaList, errorReporter, 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. @@ -73,7 +73,7 @@ void ReportErrorFor(SyntaxTrivia trivia) { if (trivia.ContainsDiagnostics && trivia.IsKind(SyntaxKind.IgnoredDirectiveTrivia)) { - reportError(sourceFile, trivia.Span, FileBasedProgramsResources.CannotConvertDirective); + errorReporter(sourceFile.Text, sourceFile.Path, trivia.Span, FileBasedProgramsResources.CannotConvertDirective); } } @@ -86,7 +86,7 @@ void ReportErrorFor(SyntaxTrivia trivia) public static void FindLeadingDirectives( SourceFile sourceFile, SyntaxTriviaList triviaList, - ErrorReporter reportError, + ErrorReporter errorReporter, ImmutableArray.Builder? builder) { Debug.Assert(triviaList.Span.Start == 0); @@ -144,7 +144,7 @@ public static void FindLeadingDirectives( LeadingWhiteSpace = whiteSpace.Leading, TrailingWhiteSpace = whiteSpace.Trailing, }, - ReportError = reportError, + ErrorReporter = errorReporter, SourceFile = sourceFile, DirectiveKind = name, DirectiveText = value, @@ -153,7 +153,7 @@ public static void FindLeadingDirectives( // Block quotes now so we can later support quoted values without a breaking change. https://github.com/dotnet/sdk/issues/49367 if (value.Contains('"')) { - reportError(sourceFile, context.Info.Span, FileBasedProgramsResources.QuoteInDirective); + context.ReportError(FileBasedProgramsResources.QuoteInDirective); } if (CSharpDirective.Parse(context) is { } directive) @@ -162,7 +162,7 @@ public static void FindLeadingDirectives( if (deduplicated.TryGetValue(directive, out var existingDirective)) { var typeAndName = $"#:{existingDirective.GetType().Name.ToLowerInvariant()} {existingDirective.Name}"; - reportError(sourceFile, directive.Info.Span, string.Format(FileBasedProgramsResources.DuplicateDirective, typeAndName)); + context.ReportError(directive.Info.Span, string.Format(FileBasedProgramsResources.DuplicateDirective, typeAndName)); } else { @@ -231,11 +231,6 @@ public static SourceFile Load(string filePath) return new SourceFile(filePath, SourceText.From(stream, encoding: null)); } - public SourceFile WithText(SourceText newText) - { - return new SourceFile(Path, newText); - } - public void Save() { using var stream = File.Open(Path, FileMode.Create, FileAccess.Write); @@ -244,17 +239,6 @@ public void Save() using var writer = new StreamWriter(stream, encoding); 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 @@ -293,10 +277,16 @@ public readonly struct ParseInfo public readonly struct ParseContext { public required ParseInfo Info { get; init; } - public required ErrorReporter ReportError { get; init; } + public required ErrorReporter ErrorReporter { get; init; } public required SourceFile SourceFile { get; init; } public required string DirectiveKind { get; init; } public required string DirectiveText { get; init; } + + public void ReportError(string message) + => ErrorReporter(SourceFile.Text, SourceFile.Path, Info.Span, message); + + public void ReportError(TextSpan span, string message) + => ErrorReporter(SourceFile.Text, SourceFile.Path, span, message); } public static Named? Parse(in ParseContext context) @@ -308,7 +298,7 @@ public readonly struct ParseContext case "package": return Package.Parse(context); case "project": return Project.Parse(context); default: - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.UnrecognizedDirective, context.DirectiveKind)); + context.ReportError(string.Format(FileBasedProgramsResources.UnrecognizedDirective, context.DirectiveKind)); return null; }; } @@ -321,14 +311,14 @@ private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, string directiveKind = context.DirectiveKind; if (firstPart.IsWhiteSpace()) { - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); return null; } // If the name contains characters that resemble separators, report an error to avoid any confusion. if (Patterns.DisallowedNameCharacters.Match(context.DirectiveText, beginning: 0, length: firstPart.Length).Success) { - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.InvalidDirectiveName, directiveKind, separator)); + context.ReportError(string.Format(FileBasedProgramsResources.InvalidDirectiveName, directiveKind, separator)); return null; } @@ -404,7 +394,7 @@ public sealed class Property(in ParseInfo info) : Named(info) if (propertyValue is null) { - context.ReportError(context.SourceFile, context.Info.Span, FileBasedProgramsResources.PropertyDirectiveMissingParts); + context.ReportError(FileBasedProgramsResources.PropertyDirectiveMissingParts); return null; } @@ -414,14 +404,14 @@ public sealed class Property(in ParseInfo info) : Named(info) } catch (XmlException ex) { - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, ex.Message)); + context.ReportError(string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, ex.Message)); return null; } if (propertyName.Equals("RestoreUseStaticGraphEvaluation", StringComparison.OrdinalIgnoreCase) && MSBuildUtilities.ConvertStringToBool(propertyValue)) { - context.ReportError(context.SourceFile, context.Info.Span, FileBasedProgramsResources.StaticGraphRestoreNotSupported); + context.ReportError(FileBasedProgramsResources.StaticGraphRestoreNotSupported); } return new Property(context.Info) @@ -493,8 +483,7 @@ public Project(in ParseInfo info, string name) : base(info) var directiveText = context.DirectiveText; if (directiveText.IsWhiteSpace()) { - string directiveKind = context.DirectiveKind; - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, context.DirectiveKind)); return null; } @@ -532,14 +521,15 @@ public Project WithName(string name, NameKind kind) /// /// If the directive points to a directory, returns a new directive pointing to the corresponding project file. /// - public Project EnsureProjectFilePath(SourceFile sourceFile, ErrorReporter reportError) + public Project EnsureProjectFilePath(SourceFile sourceFile, ErrorReporter errorReporter) { var resolvedName = Name; + var sourcePath = sourceFile.Path; // If the path is a directory like '../lib', transform it to a project file path like '../lib/lib.csproj'. // Also normalize backslashes to forward slashes to ensure the directive works on all platforms. - var sourceDirectory = Path.GetDirectoryName(sourceFile.Path) - ?? throw new InvalidOperationException($"Source file path '{sourceFile.Path}' does not have a containing directory."); + var sourceDirectory = Path.GetDirectoryName(sourcePath) + ?? throw new InvalidOperationException($"Source file path '{sourcePath}' does not have a containing directory."); var resolvedProjectPath = Path.Combine(sourceDirectory, resolvedName.Replace('\\', '/')); if (Directory.Exists(resolvedProjectPath)) @@ -553,16 +543,18 @@ public Project EnsureProjectFilePath(SourceFile sourceFile, ErrorReporter report } else { - reportError(sourceFile, Info.Span, string.Format(FileBasedProgramsResources.InvalidProjectDirective, error)); + ReportError(string.Format(FileBasedProgramsResources.InvalidProjectDirective, error)); } } else if (!File.Exists(resolvedProjectPath)) { - reportError(sourceFile, Info.Span, - string.Format(FileBasedProgramsResources.InvalidProjectDirective, string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, resolvedProjectPath))); + ReportError(string.Format(FileBasedProgramsResources.InvalidProjectDirective, string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, resolvedProjectPath))); } return WithName(resolvedName, NameKind.ProjectFilePath); + + void ReportError(string message) + => errorReporter(sourceFile.Text, sourcePath, Info.Span, message); } public override string ToString() => $"#:project {Name}"; @@ -617,25 +609,25 @@ public readonly struct Position } } -internal delegate void ErrorReporter(SourceFile sourceFile, TextSpan textSpan, string message); +internal delegate void ErrorReporter(SourceText text, string path, TextSpan textSpan, string message); internal static partial class ErrorReporters { public static readonly ErrorReporter IgnoringReporter = - static (_, _, _) => { }; + static (_, _, _, _) => { }; public static ErrorReporter CreateCollectingReporter(out ImmutableArray.Builder builder) { var capturedBuilder = builder = ImmutableArray.CreateBuilder(); - return (sourceFile, textSpan, message) => + return (text, path, textSpan, message) => capturedBuilder.Add(new SimpleDiagnostic { Location = new SimpleDiagnostic.Position() { - Path = sourceFile.Path, + Path = path, TextSpan = textSpan, - Span = sourceFile.GetFileLinePositionSpan(textSpan).Span + Span = text.Lines.GetLinePositionSpan(textSpan) }, Message = message }); diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs index 3c0952b37688..73bad41342b3 100644 --- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs @@ -43,7 +43,7 @@ public override int Execute() // Create a project instance for evaluation. var projectCollection = new ProjectCollection(); - var builder = new VirtualProjectBuilder(file, VirtualProjectBuildingCommand.TargetFrameworkVersion); + var builder = new VirtualProjectBuilder(file, VirtualProjectBuildingCommand.TargetFramework); builder.CreateProjectInstance( projectCollection, diff --git a/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs index 27ac23b56687..764aeec88f95 100644 --- a/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs +++ b/src/Cli/dotnet/Commands/Run/FileBasedAppSourceEditor.cs @@ -80,7 +80,7 @@ static string GetNewLine(SourceText text) public void Add(CSharpDirective directive) { var change = DetermineAddChange(directive); - SourceFile = SourceFile.WithText(SourceFile.Text.WithChanges([change])); + SourceFile = SourceFile with { Text = SourceFile.Text.WithChanges([change]) }; } private TextChange DetermineAddChange(CSharpDirective directive) @@ -231,7 +231,7 @@ public void Remove(CSharpDirective directive) var span = directive.Info.Span; var start = span.Start; var length = span.Length + DetermineTrailingLengthToRemove(directive); - SourceFile = SourceFile.WithText(SourceFile.Text.Replace(start: start, length: length, newText: string.Empty)); + SourceFile = SourceFile with { Text = SourceFile.Text.Replace(start: start, length: length, newText: string.Empty) }; } private static int DetermineTrailingLengthToRemove(CSharpDirective directive) diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index 967cdc7fd884..6b97511dc75a 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -12,6 +12,7 @@ using Microsoft.Build.Logging; using Microsoft.Build.Logging.SimpleErrorLogger; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; using Microsoft.DotNet.Cli.Commands.Clean.FileBasedAppArtifacts; using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Utils; @@ -82,6 +83,7 @@ internal sealed class VirtualProjectBuildingCommand : CommandBase ]; public static string TargetFrameworkVersion => Product.TargetFrameworkVersion; + public static string TargetFramework => $"net{Product.TargetFrameworkVersion}"; public bool NoRestore { get; init; } @@ -139,7 +141,7 @@ public VirtualProjectBuildingCommand( } .AsReadOnly()); - Builder = new VirtualProjectBuilder(entryPointFileFullPath, TargetFrameworkVersion, MSBuildArgs.GetResolvedTargets(), artifactsPath); + Builder = new VirtualProjectBuilder(entryPointFileFullPath, TargetFramework, MSBuildArgs.GetResolvedTargets(), artifactsPath); } public override int Execute() @@ -1076,7 +1078,8 @@ public static void CreateTempSubdirectory(string path) } public static readonly ErrorReporter ThrowingReporter = - static (sourceFile, textSpan, message) => throw new GracefulException($"{sourceFile.GetLocationString(textSpan)}: {FileBasedProgramsResources.DirectiveError}: {message}"); + static (text, path, textSpan, message) => + throw new GracefulException($"{path}({text.Lines.GetLinePositionSpan(textSpan).Start.Line + 1}): {FileBasedProgramsResources.DirectiveError}: {message}"); } internal sealed class RunFileBuildCacheEntry diff --git a/src/Microsoft.DotNet.ProjectTools/Microsoft.DotNet.ProjectTools.csproj b/src/Microsoft.DotNet.ProjectTools/Microsoft.DotNet.ProjectTools.csproj index 9de97043c331..c1156c178c0b 100644 --- a/src/Microsoft.DotNet.ProjectTools/Microsoft.DotNet.ProjectTools.csproj +++ b/src/Microsoft.DotNet.ProjectTools/Microsoft.DotNet.ProjectTools.csproj @@ -18,8 +18,8 @@ + - diff --git a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs index ace65c8c4726..cc6b5f8010c5 100644 --- a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs +++ b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs @@ -21,7 +21,7 @@ internal sealed class VirtualProjectBuilder public string EntryPointFileFullPath { get; } - public SourceFile EntryPointSourceFile + internal SourceFile EntryPointSourceFile { get { @@ -41,7 +41,7 @@ public string ArtifactsPath public VirtualProjectBuilder( string entryPointFileFullPath, - string targetFrameworkVersion, + string targetFramework, string[]? requestedTargets = null, string? artifactsPath = null) { @@ -50,16 +50,16 @@ public VirtualProjectBuilder( EntryPointFileFullPath = entryPointFileFullPath; RequestedTargets = requestedTargets; ArtifactsPath = artifactsPath; - _defaultProperties = GetDefaultProperties(targetFrameworkVersion); + _defaultProperties = GetDefaultProperties(targetFramework); } /// /// Kept in sync with the default dotnet new console project file (enforced by DotnetProjectConvertTests.SameAsTemplate). /// - public static IEnumerable<(string name, string value)> GetDefaultProperties(string targetFrameworkVersion) => + public static IEnumerable<(string name, string value)> GetDefaultProperties(string targetFramework) => [ ("OutputType", "Exe"), - ("TargetFramework", $"net{targetFrameworkVersion}"), + ("TargetFramework", targetFramework), ("ImplicitUsings", "enable"), ("Nullable", "enable"), ("PublishAot", "true"), @@ -76,6 +76,9 @@ public static string GetArtifactsPath(string entryPointFileFullPath) return GetTempSubpath(directoryName); } + public static string GetVirtualProjectPath(string entryPointFilePath) + => Path.ChangeExtension(entryPointFilePath, ".csproj"); + /// /// Obtains a temporary subdirectory for file-based app artifacts, e.g., /tmp/dotnet/runfile/. /// @@ -154,7 +157,18 @@ internal static ImmutableArray EvaluateDirectives( return directives; } - public void CreateProjectInstance( + public ProjectInstance CreateProjectInstance(ProjectCollection projectCollection, ErrorReporter errorReporter) + { + CreateProjectInstance( + projectCollection, + errorReporter, + out var projectInstance, + out _); + + return projectInstance; + } + + internal void CreateProjectInstance( ProjectCollection projectCollection, ErrorReporter errorReporter, out ProjectInstance project, @@ -199,7 +213,7 @@ private ProjectInstance CreateProjectInstance( ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) { - var projectFileFullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj"); + var projectFileFullPath = GetVirtualProjectPath(EntryPointFileFullPath); var projectFileWriter = new StringWriter(); WriteProjectFile( @@ -221,7 +235,7 @@ ProjectRootElement CreateProjectRootElement(ProjectCollection projectCollection) } } - public static void WriteProjectFile( + internal static void WriteProjectFile( TextWriter writer, ImmutableArray directives, IEnumerable<(string name, string value)> defaultProperties, @@ -531,7 +545,7 @@ static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk s } } - public static SourceText? RemoveDirectivesFromFile(ImmutableArray directives, SourceText text) + internal static SourceText? RemoveDirectivesFromFile(ImmutableArray directives, SourceText text) { if (directives.Length == 0) { @@ -549,7 +563,7 @@ static void WriteImport(TextWriter writer, string project, CSharpDirective.Sdk s return text; } - public static void RemoveDirectivesFromFile(ImmutableArray directives, SourceText text, string filePath) + internal static void RemoveDirectivesFromFile(ImmutableArray directives, SourceText text, string filePath) { if (RemoveDirectivesFromFile(directives, text) is { } modifiedText) { diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs index 1b800a103c8b..7b6365d8733e 100644 --- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs +++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs @@ -1746,7 +1746,7 @@ private static void Convert(string inputCSharp, out string actualProject, out st var directives = FileLevelDirectiveHelpers.FindDirectives(sourceFile, reportAllErrors: !force, diagnosticBag); directives = VirtualProjectBuilder.EvaluateDirectives(project: null, directives, sourceFile, diagnosticBag); var projectWriter = new StringWriter(); - VirtualProjectBuilder.WriteProjectFile(projectWriter, directives, VirtualProjectBuilder.GetDefaultProperties(VirtualProjectBuildingCommand.TargetFrameworkVersion), isVirtualProject: false); + VirtualProjectBuilder.WriteProjectFile(projectWriter, directives, VirtualProjectBuilder.GetDefaultProperties(VirtualProjectBuildingCommand.TargetFramework), isVirtualProject: false); actualProject = projectWriter.ToString(); actualCSharp = VirtualProjectBuilder.RemoveDirectivesFromFile(directives, sourceFile.Text)?.ToString(); }