diff --git a/Scripts/RunLinuxTests.ps1 b/Scripts/RunLinuxTests.ps1 index 019fa04f5..2d4e6d337 100644 --- a/Scripts/RunLinuxTests.ps1 +++ b/Scripts/RunLinuxTests.ps1 @@ -1,6 +1,7 @@ # running this seems to screw up the nuget restore, but provides a way to figure out why a test is failing on linux while working on windows. # you have to run this from the root, IE powershell ./Scripts/RunLinuxTests.ps1 -docker run --rm -v ${pwd}:/app -e "NormalizeLineEndings=1" -w /app/tests mcr.microsoft.com/dotnet/sdk:5.0 dotnet test /app/Src/CSharpier.Tests/CSharpier.Tests.csproj --logger:trx +# also a lot of these tests fail due to line endings in your local files being \r\n but the writeLine using \n +docker run --rm -v ${pwd}:/app -e "NormalizeLineEndings=1" -w /app/tests mcr.microsoft.com/dotnet/sdk:6.0 dotnet test /app/Src/CSharpier.Tests/CSharpier.Tests.csproj --logger:trx # gross way to run csharpier against the csharpier-repos #docker run --rm -v ${pwd}:/app -e "NormalizeLineEndings=1" -w /app mcr.microsoft.com/dotnet/sdk:5.0 dotnet ./csharpier/Src/CSharpier/bin/Debug/net6.0/dotnet-csharpier.dll csharpier-repos --skip-write diff --git a/Src/CSharpier.Cli.Tests/CliTests.cs b/Src/CSharpier.Cli.Tests/CliTests.cs index e223b95d7..0ef58bb96 100644 --- a/Src/CSharpier.Cli.Tests/CliTests.cs +++ b/Src/CSharpier.Cli.Tests/CliTests.cs @@ -124,7 +124,10 @@ public async Task With_Check_Should_Write_Unformatted_File() .WithArguments("CheckUnformatted.cs --check") .ExecuteAsync(); - result.Output.Should().StartWith("Warning /CheckUnformatted.cs - Was not formatted."); + result.Output + .Replace("\\", "/") + .Should() + .StartWith("Warning /CheckUnformatted.cs - Was not formatted."); result.ExitCode.Should().Be(1); } @@ -166,7 +169,7 @@ public async Task Should_Write_Error_With_Multiple_Piped_Files() result.ErrorOutput .Should() .Be( - $"Error {Path.DirectorySeparatorChar}InvalidFile.cs - Failed to compile so was not formatted.{Environment.NewLine}" + $"Error /InvalidFile.cs - Failed to compile so was not formatted.{Environment.NewLine}" ); result.ExitCode.Should().Be(1); } @@ -200,14 +203,19 @@ public async Task Should_Support_Config_With_Multiple_Piped_Files() .ExecuteAsync(); result.ErrorOutput.Should().BeEmpty(); - result.Output - .TrimEnd('\u0003') - .Should() - .Be( - @"var myVariable = - someLongValue; -" - ); + result.Output.TrimEnd('\u0003').Should().Be("var myVariable =\n someLongValue;\n"); + } + + [Test] + public async Task Should_Not_Fail_On_Empty_File() + { + await WriteFileAsync("BasicFile.cs", ""); + + var result = await new CsharpierProcess().WithArguments(".").ExecuteAsync(); + + result.Output.Should().StartWith("Total time:"); + result.ErrorOutput.Should().BeEmpty(); + result.ExitCode.Should().Be(0); } private async Task WriteFileAsync(string path, string content) diff --git a/Src/CSharpier.Cli/CommandLineFormatter.cs b/Src/CSharpier.Cli/CommandLineFormatter.cs index eae0b06ec..8d2606d69 100644 --- a/Src/CSharpier.Cli/CommandLineFormatter.cs +++ b/Src/CSharpier.Cli/CommandLineFormatter.cs @@ -1,47 +1,14 @@ +using System; using System.Diagnostics; using System.IO.Abstractions; -using System.Text; +using System.Linq; using CSharpier.Utilities; using Microsoft.Extensions.Logging; namespace CSharpier.Cli; -internal class CommandLineFormatter +internal static class CommandLineFormatter { - protected readonly CommandLineFormatterResult Result; - - protected readonly string BaseDirectoryPath; - protected readonly string PathToFileOrDirectory; - protected readonly CommandLineOptions CommandLineOptions; - protected readonly PrinterOptions PrinterOptions; - protected readonly IFileSystem FileSystem; - protected readonly IConsole Console; - protected readonly IgnoreFile IgnoreFile; - protected readonly ILogger Logger; - - protected CommandLineFormatter( - string baseDirectoryPath, - string pathToFileOrDirectory, - CommandLineOptions commandLineOptions, - PrinterOptions printerOptions, - IFileSystem fileSystem, - IConsole console, - IgnoreFile ignoreFile, - CommandLineFormatterResult result, - ILogger logger - ) - { - this.BaseDirectoryPath = baseDirectoryPath; - this.PathToFileOrDirectory = pathToFileOrDirectory; - this.PrinterOptions = printerOptions; - this.CommandLineOptions = commandLineOptions; - this.FileSystem = fileSystem; - this.Console = console; - this.IgnoreFile = ignoreFile; - this.Result = result; - this.Logger = logger; - } - public static async Task Format( CommandLineOptions commandLineOptions, IFileSystem fileSystem, @@ -50,172 +17,272 @@ public static async Task Format( CancellationToken cancellationToken ) { - var stopwatch = Stopwatch.StartNew(); - var result = new CommandLineFormatterResult(); - - async Task CreateFormatter(string path) + try { - var normalizedPath = path.Replace('\\', '/'); - var baseDirectoryPath = fileSystem.Directory.Exists(normalizedPath) - ? normalizedPath - : fileSystem.Path.GetDirectoryName(normalizedPath); + var stopwatch = Stopwatch.StartNew(); + var commandLineFormatterResult = new CommandLineFormatterResult(); - if (baseDirectoryPath == null) + if (commandLineOptions.StandardInFileContents != null) { - throw new Exception( - $"The path of {normalizedPath} does not appear to point to a directory or a file." + var filePath = commandLineOptions.DirectoryOrFilePaths[0]; + var fileToFormatInfo = FileToFormatInfo.Create( + filePath, + commandLineOptions.StandardInFileContents, + console.InputEncoding ); - } - var printerOptions = ConfigurationFileOptions.CreatePrinterOptions( - baseDirectoryPath, - fileSystem, - logger - ); + var loggerAndOptions = await GetLoggerAndOptions( + filePath, + filePath, + fileSystem, + logger, + cancellationToken + ); - var ignoreFile = await IgnoreFile.Create( - baseDirectoryPath, - fileSystem, - logger, - cancellationToken - ); - if (ignoreFile is null) - { - return null; + if (loggerAndOptions != null) + { + await PerformFormattingSteps( + fileToFormatInfo, + new StdOutFormattedFileWriter(console), + commandLineFormatterResult, + loggerAndOptions.Value.fileIssueLogger, + loggerAndOptions.Value.printerOptions, + commandLineOptions, + cancellationToken + ); + } } - - return new CommandLineFormatter( - baseDirectoryPath, - normalizedPath, - commandLineOptions, - printerOptions, - fileSystem, - console, - ignoreFile, - result, - logger - ); - } - - if (commandLineOptions.StandardInFileContents != null) - { - var path = commandLineOptions.DirectoryOrFilePaths[0]; - - var commandLineFormatter = await CreateFormatter(path); - if (commandLineFormatter == null) + else { - return 1; - } + IFormattedFileWriter? writer = null; + if (commandLineOptions.WriteStdout) + { + writer = new StdOutFormattedFileWriter(console); + } + else if (commandLineOptions.Check || commandLineOptions.SkipWrite) + { + writer = new NullFormattedFileWriter(); + } + else + { + writer = new FileSystemFormattedFileWriter(fileSystem); + } - await commandLineFormatter.FormatFile( - commandLineOptions.StandardInFileContents, - path, - console.InputEncoding, - cancellationToken - ); - } - else - { - foreach (var path in commandLineOptions.DirectoryOrFilePaths) - { - var commandLineFormatter = await CreateFormatter(path); - if (commandLineFormatter == null) + foreach ( + var directoryOrFile in commandLineOptions.DirectoryOrFilePaths.Select( + o => o.Replace("\\", "/") + ) + ) { - return 1; + async Task FormatFile(string filePath) + { + await FormatPhysicalFile( + filePath, + directoryOrFile, + fileSystem, + logger, + commandLineFormatterResult, + writer, + commandLineOptions, + cancellationToken + ); + } + + if (fileSystem.File.Exists(directoryOrFile)) + { + await FormatFile(directoryOrFile); + } + else + { + var tasks = fileSystem.Directory + .EnumerateFiles(directoryOrFile, "*.cs", SearchOption.AllDirectories) + .Select(FormatFile) + .ToArray(); + try + { + Task.WaitAll(tasks, cancellationToken); + } + catch (OperationCanceledException ex) + { + if (ex.CancellationToken != cancellationToken) + { + throw; + } + } + } } + } - await commandLineFormatter.FormatFiles(cancellationToken); + commandLineFormatterResult.ElapsedMilliseconds = stopwatch.ElapsedMilliseconds; + if (!commandLineOptions.WriteStdout) + { + ResultPrinter.PrintResults(commandLineFormatterResult, logger, commandLineOptions); } - } - result.ElapsedMilliseconds = stopwatch.ElapsedMilliseconds; - if (!commandLineOptions.WriteStdout) + return ReturnExitCode(commandLineOptions, commandLineFormatterResult); + } + catch (Exception ex) + when (ex is InvalidIgnoreFileException + || ex.InnerException is InvalidIgnoreFileException + ) { - ResultPrinter.PrintResults(result, logger, commandLineOptions); + var invalidIgnoreFileException = + ex is InvalidIgnoreFileException ? ex : ex.InnerException; + + logger.LogError( + invalidIgnoreFileException!.InnerException, + invalidIgnoreFileException.Message + ); + return 1; } - return ReturnExitCode(commandLineOptions, result); } - public async Task FormatFiles(CancellationToken cancellationToken) + private static async Task FormatPhysicalFile( + string filePath, + string directoryOrFile, + IFileSystem fileSystem, + ILogger logger, + CommandLineFormatterResult commandLineFormatterResult, + IFormattedFileWriter writer, + CommandLineOptions commandLineOptions, + CancellationToken cancellationToken + ) { - if (this.FileSystem.File.Exists(this.PathToFileOrDirectory)) + var fileToFormatInfo = await FileToFormatInfo.CreateFromFileSystem( + filePath, + fileSystem, + cancellationToken + ); + + var loggerAndOptions = await GetLoggerAndOptions( + directoryOrFile, + filePath, + fileSystem, + logger, + cancellationToken + ); + + if (loggerAndOptions == null) { - await FormatFileFromPath(this.PathToFileOrDirectory, cancellationToken); + return; } - else + + if (!filePath.EndsWithIgnoreCase(".cs") && !filePath.EndsWithIgnoreCase(".cst")) { - var tasks = this.FileSystem.Directory - .EnumerateFiles(this.PathToFileOrDirectory, "*.cs", SearchOption.AllDirectories) - .Select(o => FormatFileFromPath(o, cancellationToken)) - .ToArray(); - try - { - Task.WaitAll(tasks, cancellationToken); - } - catch (OperationCanceledException ex) - { - if (ex.CancellationToken != cancellationToken) - { - throw; - } - } + loggerAndOptions.Value.fileIssueLogger.WriteError("Is an unsupported file type."); + return; } + + await PerformFormattingSteps( + fileToFormatInfo, + writer, + commandLineFormatterResult, + loggerAndOptions.Value.fileIssueLogger, + loggerAndOptions.Value.printerOptions, + commandLineOptions, + cancellationToken + ); } - private async Task FormatFileFromPath(string filePath, CancellationToken cancellationToken) + private static async Task<(FileIssueLogger fileIssueLogger, PrinterOptions printerOptions)?> GetLoggerAndOptions( + string pathToDirectoryOrFile, + string pathToFile, + IFileSystem fileSystem, + ILogger logger, + CancellationToken cancellationToken + ) { - if (ShouldIgnoreFile(filePath)) - { - return; - } + var normalizedPath = pathToDirectoryOrFile.Replace('\\', '/'); + var baseDirectoryPath = fileSystem.Directory.Exists(normalizedPath) + ? normalizedPath + : fileSystem.Path.GetDirectoryName(normalizedPath); - if (!filePath.EndsWithIgnoreCase(".cs") && !filePath.EndsWithIgnoreCase(".cst")) + if (baseDirectoryPath == null) { - WriteError(filePath, "Is an unsupported file type."); - return; + throw new Exception( + $"The path of {normalizedPath} does not appear to point to a directory or a file." + ); } - cancellationToken.ThrowIfCancellationRequested(); + var printerOptions = ConfigurationFileOptions.CreatePrinterOptions( + baseDirectoryPath, + fileSystem, + logger + ); - var (encoding, fileContents, unableToDetectEncoding) = await FileReader.ReadFile( - filePath, - this.FileSystem, + var ignoreFile = await IgnoreFile.Create( + baseDirectoryPath, + fileSystem, + logger, cancellationToken ); - if (fileContents.Length == 0) + + if ( + GeneratedCodeUtilities.IsGeneratedCodeFile(pathToFile) + || ignoreFile.IsIgnored(pathToFile) + ) { - return; + return null; } - if (unableToDetectEncoding) + var filePathLogger = new FileIssueLogger( + pathToFile.Replace('\\', '/')[baseDirectoryPath.Length..], + logger + ); + + return (filePathLogger, printerOptions); + } + + private static int ReturnExitCode( + CommandLineOptions commandLineOptions, + CommandLineFormatterResult result + ) + { + if ( + (commandLineOptions.StandardInFileContents != null && result.FailedCompilation > 0) + || (commandLineOptions.Check && result.UnformattedFiles > 0) + || result.FailedSyntaxTreeValidation > 0 + || result.ExceptionsFormatting > 0 + || result.ExceptionsValidatingSource > 0 + ) { - WriteWarning(filePath, $"Unable to detect file encoding. Defaulting to {encoding}."); + return 1; } - await FormatFile(fileContents, filePath, encoding, cancellationToken); + return 0; } - private async Task FormatFile( - string fileContents, - string filePath, - Encoding encoding, + private static async Task PerformFormattingSteps( + FileToFormatInfo fileToFormatInfo, + IFormattedFileWriter formattedFileWriter, + CommandLineFormatterResult commandLineFormatterResult, + FileIssueLogger fileIssueLogger, + PrinterOptions printerOptions, + CommandLineOptions commandLineOptions, CancellationToken cancellationToken ) { - if (ShouldIgnoreFile(filePath)) + if (fileToFormatInfo.FileContents.Length == 0) { return; } + if (fileToFormatInfo.UnableToDetectEncoding) + { + fileIssueLogger.WriteWarning( + $"Unable to detect file encoding. Defaulting to {fileToFormatInfo.Encoding}." + ); + } + cancellationToken.ThrowIfCancellationRequested(); - CSharpierResult result; + CodeFormatterResult codeFormattingResult; try { - result = await CodeFormatter.FormatAsync( - fileContents, - this.PrinterOptions, + codeFormattingResult = await CodeFormatter.FormatAsync( + fileToFormatInfo.FileContents, + printerOptions, cancellationToken ); } @@ -225,49 +292,33 @@ CancellationToken cancellationToken } catch (Exception ex) { - Interlocked.Increment(ref this.Result.Files); - WriteError(filePath, "Threw exception while formatting.", ex); - Interlocked.Increment(ref this.Result.ExceptionsFormatting); + fileIssueLogger.WriteError("Threw exception while formatting.", ex); + Interlocked.Increment(ref commandLineFormatterResult.ExceptionsFormatting); return; } + finally + { + Interlocked.Increment(ref commandLineFormatterResult.Files); + } - if (result.Errors.Any()) + if (codeFormattingResult.Errors.Any()) { - Interlocked.Increment(ref this.Result.Files); - WriteError(filePath, "Failed to compile so was not formatted."); - Interlocked.Increment(ref this.Result.FailedCompilation); + fileIssueLogger.WriteError("Failed to compile so was not formatted."); + Interlocked.Increment(ref commandLineFormatterResult.FailedCompilation); return; } - if (!result.FailureMessage.IsBlank()) + if (!codeFormattingResult.FailureMessage.IsBlank()) { - Interlocked.Increment(ref this.Result.Files); - WriteError(filePath, result.FailureMessage); + fileIssueLogger.WriteError(codeFormattingResult.FailureMessage); return; } - await PerformSyntaxTreeValidation(filePath, fileContents, result, cancellationToken); - - PerformCheck(filePath, result, fileContents); - - cancellationToken.ThrowIfCancellationRequested(); - Interlocked.Increment(ref this.Result.Files); - - WriteFormattedResult(filePath, result, fileContents, encoding); - } - - private async Task PerformSyntaxTreeValidation( - string file, - string fileContents, - CSharpierResult result, - CancellationToken cancellationToken - ) - { - if (!this.CommandLineOptions.Fast) + if (!commandLineOptions.Fast) { var syntaxNodeComparer = new SyntaxNodeComparer( - fileContents, - result.Code, + fileToFormatInfo.FileContents, + codeFormattingResult.Code, cancellationToken ); @@ -276,107 +327,37 @@ CancellationToken cancellationToken var failure = await syntaxNodeComparer.CompareSourceAsync(cancellationToken); if (!string.IsNullOrEmpty(failure)) { - Interlocked.Increment(ref this.Result.FailedSyntaxTreeValidation); - WriteError(file, $"Failed syntax tree validation.\n{failure}"); + Interlocked.Increment( + ref commandLineFormatterResult.FailedSyntaxTreeValidation + ); + fileIssueLogger.WriteError($"Failed syntax tree validation.\n{failure}"); } } catch (Exception ex) { - Interlocked.Increment(ref this.Result.ExceptionsValidatingSource); - - WriteError(file, "Failed with exception during syntax tree validation.", ex); - } - } - } - - private void PerformCheck(string filePath, CSharpierResult result, string fileContents) - { - if ( - this.CommandLineOptions.Check - && !this.CommandLineOptions.WriteStdout - && result.Code != fileContents - ) - { - var difference = StringDiffer.PrintFirstDifference(result.Code, fileContents); - WriteWarning(filePath, $"Was not formatted.\n{difference}"); - Interlocked.Increment(ref this.Result.UnformattedFiles); - } - } + Interlocked.Increment(ref commandLineFormatterResult.ExceptionsValidatingSource); - private void WriteFormattedResult( - string filePath, - CSharpierResult result, - string? fileContents, - Encoding? encoding - ) - { - if (this.CommandLineOptions.WriteStdout) - { - this.Console.Write(result.Code); - } - else - { - if ( - !this.CommandLineOptions.Check - && !this.CommandLineOptions.SkipWrite - && result.Code != fileContents - ) - { - // purposely avoid async here, that way the file completely writes if the process gets cancelled while running. - this.FileSystem.File.WriteAllText(filePath, result.Code, encoding); + fileIssueLogger.WriteError( + "Failed with exception during syntax tree validation.", + ex + ); } } - } - private string GetPath(string file) - { - return file[this.BaseDirectoryPath.Length..]; - } - - private static int ReturnExitCode( - CommandLineOptions commandLineOptions, - CommandLineFormatterResult result - ) - { if ( - (commandLineOptions.StandardInFileContents != null && result.FailedCompilation > 0) - || (commandLineOptions.Check && result.UnformattedFiles > 0) - || result.FailedSyntaxTreeValidation > 0 - || result.ExceptionsFormatting > 0 - || result.ExceptionsValidatingSource > 0 + commandLineOptions.Check + && !commandLineOptions.WriteStdout + && codeFormattingResult.Code != fileToFormatInfo.FileContents ) { - return 1; + var difference = StringDiffer.PrintFirstDifference( + codeFormattingResult.Code, + fileToFormatInfo.FileContents + ); + fileIssueLogger.WriteWarning($"Was not formatted.\n{difference}"); + Interlocked.Increment(ref commandLineFormatterResult.UnformattedFiles); } - return 0; - } - - private bool ShouldIgnoreFile(string filePath) - { - return GeneratedCodeUtilities.IsGeneratedCodeFile(filePath) - || this.IgnoreFile.IsIgnored(filePath); - } - - private void WriteError(string filePath, string value, Exception? exception = null) - { - this.Logger.LogError(exception, $"{GetPath(filePath)} - {value}"); + formattedFileWriter.WriteResult(codeFormattingResult, fileToFormatInfo); } - - private void WriteWarning(string filePath, string value) - { - this.Logger.LogWarning($"{GetPath(filePath)} - {value}"); - } -} - -public class CommandLineFormatterResult -{ - // these are public fields so that Interlocked.Increment may be used on them. - public int FailedSyntaxTreeValidation; - public int FailedCompilation; - public int ExceptionsFormatting; - public int ExceptionsValidatingSource; - public int Files; - public int UnformattedFiles; - public long ElapsedMilliseconds; } diff --git a/Src/CSharpier.Cli/CommandLineFormatterResult.cs b/Src/CSharpier.Cli/CommandLineFormatterResult.cs new file mode 100644 index 000000000..d58f215c5 --- /dev/null +++ b/Src/CSharpier.Cli/CommandLineFormatterResult.cs @@ -0,0 +1,13 @@ +namespace CSharpier.Cli; + +public class CommandLineFormatterResult +{ + // these are fields instead of properties so that Interlocked.Increment may be used on them. + public int FailedSyntaxTreeValidation; + public int FailedCompilation; + public int ExceptionsFormatting; + public int ExceptionsValidatingSource; + public int Files; + public int UnformattedFiles; + public long ElapsedMilliseconds; +} diff --git a/Src/CSharpier.Cli/FileIssueLogger.cs b/Src/CSharpier.Cli/FileIssueLogger.cs new file mode 100644 index 000000000..5c4902af9 --- /dev/null +++ b/Src/CSharpier.Cli/FileIssueLogger.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; + +namespace CSharpier.Cli; + +internal class FileIssueLogger +{ + private readonly string filePath; + private readonly ILogger logger; + + public FileIssueLogger(string filePath, ILogger logger) + { + this.filePath = filePath; + this.logger = logger; + } + + public void WriteError(string value, Exception? exception = null) + { + this.logger.LogError(exception, $"{this.GetPath()} - {value}"); + } + + public void WriteWarning(string value) + { + this.logger.LogWarning($"{this.GetPath()} - {value}"); + } + + private string GetPath() + { + return this.filePath; + } +} diff --git a/Src/CSharpier.Cli/FileToFormatInfo.cs b/Src/CSharpier.Cli/FileToFormatInfo.cs new file mode 100644 index 000000000..f1cb75cf8 --- /dev/null +++ b/Src/CSharpier.Cli/FileToFormatInfo.cs @@ -0,0 +1,47 @@ +using System.IO.Abstractions; +using System.Text; + +namespace CSharpier.Cli; + +internal class FileToFormatInfo +{ + protected FileToFormatInfo() { } + + public string FileContents { get; init; } = string.Empty; + public string Path { get; init; } = string.Empty; + public Encoding Encoding { get; init; } = Encoding.Default; + public bool UnableToDetectEncoding { get; init; } + + public static async Task CreateFromFileSystem( + string path, + IFileSystem fileSystem, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + + var (encoding, fileContents, unableToDetectEncoding) = await FileReader.ReadFile( + path, + fileSystem, + cancellationToken + ); + + return new FileToFormatInfo + { + Path = path, + FileContents = fileContents, + Encoding = encoding, + UnableToDetectEncoding = unableToDetectEncoding + }; + } + + public static FileToFormatInfo Create(string path, string fileContents, Encoding encoding) + { + return new FileToFormatInfo + { + Path = path, + FileContents = fileContents, + Encoding = encoding + }; + } +} diff --git a/Src/CSharpier.Cli/IFormattedFileWriter.cs b/Src/CSharpier.Cli/IFormattedFileWriter.cs new file mode 100644 index 000000000..2d2999d13 --- /dev/null +++ b/Src/CSharpier.Cli/IFormattedFileWriter.cs @@ -0,0 +1,11 @@ +namespace CSharpier.Cli; + +internal interface IFormattedFileWriter +{ + void WriteResult(CodeFormatterResult result, FileToFormatInfo fileToFormatInfo); +} + +internal class NullFormattedFileWriter : IFormattedFileWriter +{ + public void WriteResult(CodeFormatterResult result, FileToFormatInfo fileToFormatInfo) { } +} diff --git a/Src/CSharpier.Cli/IgnoreFile.cs b/Src/CSharpier.Cli/IgnoreFile.cs index 633a45fb4..a3e29e1f1 100644 --- a/Src/CSharpier.Cli/IgnoreFile.cs +++ b/Src/CSharpier.Cli/IgnoreFile.cs @@ -35,7 +35,7 @@ public bool IsIgnored(string filePath) return this.Ignore.IsIgnored(normalizedFilePath); } - public static async Task Create( + public static async Task Create( string baseDirectoryPath, IFileSystem fileSystem, ILogger logger, @@ -59,13 +59,12 @@ var line in await fileSystem.File.ReadAllLinesAsync(ignoreFilePath, cancellation } catch (Exception ex) { - logger.LogError( - ex, + throw new InvalidIgnoreFileException( @$"The .csharpierignore file at {ignoreFilePath} could not be parsed due to the following line: {line} -" +", + ex ); - return null; } } @@ -92,3 +91,9 @@ var line in await fileSystem.File.ReadAllLinesAsync(ignoreFilePath, cancellation return null; } } + +public class InvalidIgnoreFileException : Exception +{ + public InvalidIgnoreFileException(string message, Exception exception) + : base(message, exception) { } +} diff --git a/Src/CSharpier.Cli/PhysicalFileInfoAndWriter.cs b/Src/CSharpier.Cli/PhysicalFileInfoAndWriter.cs new file mode 100644 index 000000000..6872be9b1 --- /dev/null +++ b/Src/CSharpier.Cli/PhysicalFileInfoAndWriter.cs @@ -0,0 +1,26 @@ +using System.IO.Abstractions; +using System.Text; + +namespace CSharpier.Cli; + +internal class FileSystemFormattedFileWriter : IFormattedFileWriter +{ + public IFileSystem FileSystem { get; } + + public FileSystemFormattedFileWriter(IFileSystem fileSystem) + { + this.FileSystem = fileSystem; + } + + public void WriteResult(CodeFormatterResult result, FileToFormatInfo fileToFormatInfo) + { + if (result.Code != fileToFormatInfo.FileContents) + { + this.FileSystem.File.WriteAllText( + fileToFormatInfo.Path, + result.Code, + fileToFormatInfo.Encoding + ); + } + } +} diff --git a/Src/CSharpier.Cli/StdOutFormattedFileWriter.cs b/Src/CSharpier.Cli/StdOutFormattedFileWriter.cs new file mode 100644 index 000000000..55803fcb1 --- /dev/null +++ b/Src/CSharpier.Cli/StdOutFormattedFileWriter.cs @@ -0,0 +1,15 @@ +namespace CSharpier.Cli; + +internal class StdOutFormattedFileWriter : IFormattedFileWriter +{ + private readonly IConsole console; + + public StdOutFormattedFileWriter(IConsole console) + { + this.console = console; + } + public void WriteResult(CodeFormatterResult result, FileToFormatInfo fileToFormatInfo) + { + this.console.Write(result.Code); + } +} diff --git a/Src/CSharpier.Tests/CommandLineFormatterTests.cs b/Src/CSharpier.Tests/CommandLineFormatterTests.cs index 23d919c01..5fdfa3833 100644 --- a/Src/CSharpier.Tests/CommandLineFormatterTests.cs +++ b/Src/CSharpier.Tests/CommandLineFormatterTests.cs @@ -35,9 +35,20 @@ public void Format_Writes_Failed_To_Compile() result.ErrorLines .First() .Should() - .Be( - $"Error {Path.DirectorySeparatorChar}Invalid.cs - Failed to compile so was not formatted." - ); + .Be("Error /Invalid.cs - Failed to compile so was not formatted."); + } + + [Test] + public void Format_Writes_Failed_To_Compile_With_Directory() + { + WhenAFileExists("Directory/Invalid.cs", "asdfasfasdf"); + + var result = this.Format(); + + result.ErrorLines + .First() + .Should() + .Be("Error /Directory/Invalid.cs - Failed to compile so was not formatted."); } [Test] @@ -54,7 +65,7 @@ public void Format_Writes_Unsupported() } [Test] - public void Format_Writes_File() + public void Format_Writes_File_With_Directory_Path() { const string unformattedFilePath = "Unformatted.cs"; WhenAFileExists(unformattedFilePath, UnformattedClassContent); @@ -64,6 +75,17 @@ public void Format_Writes_File() this.GetFileContent(unformattedFilePath).Should().Be(FormattedClassContent); } + [Test] + public void Format_Writes_File_With_File_Path() + { + const string unformattedFilePath = "Unformatted.cs"; + WhenAFileExists(unformattedFilePath, UnformattedClassContent); + + this.Format(directoryOrFilePaths: "Unformatted.cs"); + + this.GetFileContent(unformattedFilePath).Should().Be(FormattedClassContent); + } + [Test] public void Format_Supports_Skip_Write() { @@ -85,10 +107,7 @@ public void Format_Checks_Unformatted_File() result.ExitCode.Should().Be(1); this.GetFileContent(unformattedFilePath).Should().Be(UnformattedClassContent); - result.Lines - .First() - .Should() - .StartWith($"Warning {Path.DirectorySeparatorChar}Unformatted.cs - Was not formatted."); + result.Lines.First().Should().StartWith("Warning /Unformatted.cs - Was not formatted."); } [Test] @@ -217,6 +236,7 @@ public void Ignore_Should_Deal_With_Inconsistent_Slashes() [Test] public void Ignore_Reports_Errors() { + WhenAFileExists("Test.cs", UnformattedClassContent); WhenAFileExists(".csharpierignore", @"\Src\Uploads\*.cs"); var result = this.Format(); @@ -227,7 +247,7 @@ public void Ignore_Reports_Errors() result.ErrorLines .First() .Should() - .StartWith( + .Contain( $"Error The .csharpierignore file at {path} could not be parsed due to the following line:" ); result.ErrorLines.Skip(1).First().Should().Contain(@"\Src\Uploads\*.cs"); @@ -282,9 +302,7 @@ public void File_With_Compilation_Error_Should_Not_Lose_Code() result.ErrorLines .First() .Should() - .Be( - $"Error {Path.DirectorySeparatorChar}Invalid.cs - Failed to compile so was not formatted." - ); + .Be("Error /Invalid.cs - Failed to compile so was not formatted."); } [Test] @@ -339,7 +357,6 @@ public void Empty_Config_Files_Should_Log_Warning(string configFileName) result.Lines .First() - .Replace("\\", "/") .Should() .Be($"Warning The configuration file at {configPath} was empty."); } diff --git a/Src/CSharpier/CodeFormatter.cs b/Src/CSharpier/CodeFormatter.cs index 9b126d433..b9d7aaf0a 100644 --- a/Src/CSharpier/CodeFormatter.cs +++ b/Src/CSharpier/CodeFormatter.cs @@ -29,12 +29,12 @@ public static async Task FormatAsync( return result.Code; } - internal static CSharpierResult Format(string code, PrinterOptions printerOptions) + internal static CodeFormatterResult Format(string code, PrinterOptions printerOptions) { return FormatAsync(code, printerOptions, CancellationToken.None).Result; } - internal static async Task FormatAsync( + internal static async Task FormatAsync( string code, PrinterOptions printerOptions, CancellationToken cancellationToken @@ -69,10 +69,10 @@ SyntaxTree ParseText(string codeToFormat, params string[] preprocessorSymbols) if (GeneratedCodeUtilities.BeginsWithAutoGeneratedComment(rootNode)) { - return new CSharpierResult { Code = code }; + return new CodeFormatterResult { Code = code }; } - bool TryGetCompilationFailure(out CSharpierResult compilationResult) + bool TryGetCompilationFailure(out CodeFormatterResult compilationResult) { var diagnostics = syntaxTree! .GetDiagnostics(cancellationToken) @@ -80,7 +80,7 @@ bool TryGetCompilationFailure(out CSharpierResult compilationResult) .ToList(); if (diagnostics.Any()) { - compilationResult = new CSharpierResult + compilationResult = new CodeFormatterResult { Code = code, Errors = diagnostics, @@ -90,7 +90,7 @@ bool TryGetCompilationFailure(out CSharpierResult compilationResult) return true; } - compilationResult = CSharpierResult.Null; + compilationResult = CodeFormatterResult.Null; return false; } @@ -132,7 +132,7 @@ bool TryGetCompilationFailure(out CSharpierResult compilationResult) formattedCode = DocPrinter.DocPrinter.Print(document, printerOptions, lineEnding); } - return new CSharpierResult + return new CodeFormatterResult { Code = formattedCode, DocTree = printerOptions.IncludeDocTree @@ -143,7 +143,7 @@ bool TryGetCompilationFailure(out CSharpierResult compilationResult) } catch (InTooDeepException) { - return new CSharpierResult + return new CodeFormatterResult { FailureMessage = "We can't handle this deep of recursion yet." }; @@ -181,7 +181,7 @@ private static string PrintAST(CompilationUnitSyntax rootNode) } } -internal class CSharpierResult +internal class CodeFormatterResult { public string Code { get; init; } = string.Empty; public string DocTree { get; init; } = string.Empty; @@ -190,5 +190,5 @@ internal class CSharpierResult public string FailureMessage { get; init; } = string.Empty; - public static readonly CSharpierResult Null = new(); + public static readonly CodeFormatterResult Null = new(); }