diff --git a/CSharpier.sln.DotSettings b/CSharpier.sln.DotSettings index 96ebbd074..b81e41832 100644 --- a/CSharpier.sln.DotSettings +++ b/CSharpier.sln.DotSettings @@ -1,2 +1,3 @@  + True True \ No newline at end of file diff --git a/Src/CSharpier.Tests/CommandLineFormatterTests.cs b/Src/CSharpier.Tests/CommandLineFormatterTests.cs index 31f84887c..b42f35dad 100644 --- a/Src/CSharpier.Tests/CommandLineFormatterTests.cs +++ b/Src/CSharpier.Tests/CommandLineFormatterTests.cs @@ -115,6 +115,21 @@ public void File_In_Ignore_Skips_Formatting(string fileName, string ignoreConten lines.FirstOrDefault(o => o.StartsWith("Total files")).Should().Be("Total files: 0 "); } + [TestCase("SubFolder/File.cs", "*.cs", "SubFolder")] + public void File_In_Ignore_Skips_Formatting( + string fileName, + string ignoreContents, + string rootPath + ) { + var unformattedFilePath = fileName; + WhenThereExists(unformattedFilePath, UnformattedClass); + WhenThereExists(".csharpierignore", ignoreContents); + + var (_, lines) = this.Format(baseDirectoryPath: Path.Combine(GetRootPath(), rootPath)); + + lines.FirstOrDefault(o => o.StartsWith("Total files")).Should().Be("Total files: 0 "); + } + [Test] public void Ignore_Reports_Errors() { @@ -132,20 +147,20 @@ public void Ignore_Reports_Errors() } private (int exitCode, IList lines) Format( - string rootPath = null, + string baseDirectoryPath = null, bool skipWrite = false, bool check = false ) { - if (rootPath == null) + if (baseDirectoryPath == null) { - rootPath = GetRootPath(); + baseDirectoryPath = GetRootPath(); } var commandLineFormatter = new TestableCommandLineFormatter( - rootPath, + baseDirectoryPath, new CommandLineOptions { - DirectoryOrFile = rootPath, + DirectoryOrFile = baseDirectoryPath, SkipWrite = skipWrite, Check = check }, @@ -154,7 +169,7 @@ public void Ignore_Reports_Errors() ); return ( - commandLineFormatter.Format(CancellationToken.None).Result, + commandLineFormatter.FormatFiles(CancellationToken.None).Result, commandLineFormatter.Lines ); } @@ -181,12 +196,12 @@ private class TestableCommandLineFormatter : CommandLineFormatter public readonly IList Lines = new List(); public TestableCommandLineFormatter( - string rootPath, + string baseDirectoryPath, CommandLineOptions commandLineOptions, PrinterOptions printerOptions, IFileSystem fileSystem ) - : base(rootPath, commandLineOptions, printerOptions, fileSystem) { } + : base(baseDirectoryPath, commandLineOptions, printerOptions, fileSystem) { } protected override void WriteLine(string line = null) { diff --git a/Src/CSharpier.Tests/ConfigurationFileOptionsTests.cs b/Src/CSharpier.Tests/ConfigurationFileOptionsTests.cs index 26a9bf611..2b4b030df 100644 --- a/Src/CSharpier.Tests/ConfigurationFileOptionsTests.cs +++ b/Src/CSharpier.Tests/ConfigurationFileOptionsTests.cs @@ -65,6 +65,20 @@ public void Should_Read_ExtensionLess_File(string contents) result.PrintWidth.Should().Be(10); } + [TestCase("", "printWidth: 10")] + [TestCase("", "{ \"printWidth\": 10 }")] + [TestCase(".yml", "printWidth: 10")] + [TestCase(".yaml", "printWidth: 10")] + [TestCase(".json", "{ \"printWidth\": 10 }")] + public void Should_Find_Configuration_In_Parent_Directory(string extension, string contents) + { + WhenThereExists($"c:/test/.csharpierrc{extension}", contents); + + var result = CreateConfigurationOptions("c:/test/subfolder"); + + result.PrintWidth.Should().Be(10); + } + [Test] public void Should_Prefer_No_Extension() { @@ -166,9 +180,9 @@ private void ShouldHaveDefaultOptions(ConfigurationFileOptions configurationFile configurationFileOptions.EndOfLine.Should().Be(EndOfLine.Auto); } - private ConfigurationFileOptions CreateConfigurationOptions(string rootPath) + private ConfigurationFileOptions CreateConfigurationOptions(string baseDirectoryPath) { - return ConfigurationFileOptions.Create(rootPath, fileSystem); + return ConfigurationFileOptions.Create(baseDirectoryPath, fileSystem); } private void WhenThereExists(string path, string contents) diff --git a/Src/CSharpier.Tests/LineEndingTests.cs b/Src/CSharpier.Tests/LineEndingTests.cs index 4856b4a53..0d4e09e68 100644 --- a/Src/CSharpier.Tests/LineEndingTests.cs +++ b/Src/CSharpier.Tests/LineEndingTests.cs @@ -53,7 +53,8 @@ public void Escaped_LineEndings_In_Verbatim_String_Should_Remain( string escapedNewLine, EndOfLine endOfLine ) { - var code = @$"class ClassName + var code = + @$"class ClassName {{ string value = @""one{escapedNewLine}two""; }} diff --git a/Src/CSharpier/CommandLineFormatter.cs b/Src/CSharpier/CommandLineFormatter.cs index ac0f31594..f627232de 100644 --- a/Src/CSharpier/CommandLineFormatter.cs +++ b/Src/CSharpier/CommandLineFormatter.cs @@ -20,21 +20,21 @@ public class CommandLineFormatter protected readonly Stopwatch Stopwatch; - protected readonly string RootPath; + protected readonly string BaseDirectoryPath; protected readonly CommandLineOptions CommandLineOptions; protected readonly PrinterOptions PrinterOptions; protected readonly IFileSystem FileSystem; - public global::Ignore.Ignore Ignore { get; set; } + public global::Ignore.Ignore Ignore { get; init; } - public CommandLineFormatter( - string rootPath, + protected CommandLineFormatter( + string baseDirectoryPath, CommandLineOptions commandLineOptions, PrinterOptions printerOptions, IFileSystem fileSystem ) { - this.RootPath = rootPath; + this.BaseDirectoryPath = baseDirectoryPath; this.PrinterOptions = printerOptions; this.CommandLineOptions = commandLineOptions; this.Stopwatch = Stopwatch.StartNew(); @@ -42,7 +42,45 @@ IFileSystem fileSystem this.Ignore = new global::Ignore.Ignore(); } - public async Task Format(CancellationToken cancellationToken) + public static async Task Format( + CommandLineOptions commandLineOptions, + IFileSystem fileSystem, + CancellationToken cancellationToken + ) { + var baseDirectoryPath = File.Exists(commandLineOptions.DirectoryOrFile) + ? Path.GetDirectoryName(commandLineOptions.DirectoryOrFile) + : commandLineOptions.DirectoryOrFile; + + if (baseDirectoryPath == null) + { + throw new Exception( + $"The path of {commandLineOptions.DirectoryOrFile} does not appear to point to a directory or a file." + ); + } + + var configurationFileOptions = ConfigurationFileOptions.Create( + baseDirectoryPath, + fileSystem + ); + + var printerOptions = new PrinterOptions + { + TabWidth = configurationFileOptions.TabWidth, + UseTabs = configurationFileOptions.UseTabs, + Width = configurationFileOptions.PrintWidth, + EndOfLine = configurationFileOptions.EndOfLine + }; + + var commandLineFormatter = new CommandLineFormatter( + baseDirectoryPath, + commandLineOptions, + printerOptions, + fileSystem + ); + return await commandLineFormatter.FormatFiles(cancellationToken); + } + + public async Task FormatFiles(CancellationToken cancellationToken) { var ignoreExitCode = await this.ParseIgnoreFile(cancellationToken); if (ignoreExitCode != 0) @@ -57,7 +95,7 @@ public async Task Format(CancellationToken cancellationToken) else { var tasks = this.FileSystem.Directory.EnumerateFiles( - this.RootPath, + this.BaseDirectoryPath, "*.cs", SearchOption.AllDirectories ) @@ -83,30 +121,44 @@ public async Task Format(CancellationToken cancellationToken) private async Task ParseIgnoreFile(CancellationToken cancellationToken) { - var ignoreFilePath = Path.Combine(this.RootPath, ".csharpierignore"); - if (this.FileSystem.File.Exists(ignoreFilePath)) + var directoryInfo = this.FileSystem.DirectoryInfo.FromDirectoryName( + this.BaseDirectoryPath + ); + var ignoreFilePath = this.FileSystem.Path.Combine( + directoryInfo.FullName, + ".csharpierignore" + ); + while (!this.FileSystem.File.Exists(ignoreFilePath)) { - foreach ( - var line in await this.FileSystem.File.ReadAllLinesAsync( - ignoreFilePath, - cancellationToken - ) - ) { - try - { - this.Ignore.Add(line); - } - catch (Exception ex) - { - WriteLine( - "The .csharpierignore file at " - + ignoreFilePath - + " could not be parsed due to the following line:" - ); - WriteLine(line); - WriteLine("Exception: " + ex.Message); - return 1; - } + directoryInfo = directoryInfo.Parent; + if (directoryInfo == null) + { + return 0; + } + ignoreFilePath = this.FileSystem.Path.Combine( + directoryInfo.FullName, + ".csharpierignore" + ); + } + + foreach ( + var line in await this.FileSystem.File.ReadAllLinesAsync( + ignoreFilePath, + cancellationToken + ) + ) { + try + { + this.Ignore.Add(line); + } + catch (Exception ex) + { + WriteLine( + $"The .csharpierignore file at {ignoreFilePath} could not be parsed due to the following line:" + ); + WriteLine(line); + WriteLine($"Exception: {ex.Message}"); + return 1; } } @@ -228,7 +280,7 @@ private async Task FormatFile(string file, CancellationToken cancellationToken) private string GetPath(string file) { - return PadToSize(file.Substring(this.RootPath.Length)); + return PadToSize(file.Substring(this.BaseDirectoryPath.Length)); } private void PrintResults() @@ -284,7 +336,7 @@ private bool IgnoreFile(string filePath) } var normalizedFilePath = filePath.Replace("\\", "/") - .Substring(this.RootPath.Length + 1); + .Substring(this.BaseDirectoryPath.Length + 1); return this.Ignore.IsIgnored(normalizedFilePath); } diff --git a/Src/CSharpier/ConfigurationFileOptions.cs b/Src/CSharpier/ConfigurationFileOptions.cs index e822d0443..63dc4698b 100644 --- a/Src/CSharpier/ConfigurationFileOptions.cs +++ b/Src/CSharpier/ConfigurationFileOptions.cs @@ -17,58 +17,68 @@ public class ConfigurationFileOptions public bool UseTabs { get; init; } public EndOfLine EndOfLine { get; init; } - public static ConfigurationFileOptions Create(string rootPath, IFileSystem fileSystem) - { - ConfigurationFileOptions ReadJson(string path) - { - return JsonConvert.DeserializeObject( - fileSystem.File.ReadAllText(path) - ); - } - - ConfigurationFileOptions ReadYaml(string path) + public static ConfigurationFileOptions Create( + string baseDirectoryPath, + IFileSystem fileSystem + ) { + while (true) { - var deserializer = new DeserializerBuilder().WithNamingConvention( - CamelCaseNamingConvention.Instance - ) - .Build(); + var potentialPath = fileSystem.Path.Combine(baseDirectoryPath, ".csharpierrc"); - return deserializer.Deserialize( - fileSystem.File.ReadAllText(path) - ); - } + if (fileSystem.File.Exists(potentialPath)) + { + var contents = fileSystem.File.ReadAllText(potentialPath); + if (contents.TrimStart().StartsWith("{")) + { + return ReadJson(potentialPath, fileSystem); + } - var potentialPath = fileSystem.Path.Combine(rootPath, ".csharpierrc"); + return ReadYaml(potentialPath, fileSystem); + } - if (fileSystem.File.Exists(potentialPath)) - { - var contents = fileSystem.File.ReadAllText(potentialPath); - if (contents.TrimStart().StartsWith("{")) + var jsonExtensionPath = potentialPath + ".json"; + if (fileSystem.File.Exists(jsonExtensionPath)) { - return ReadJson(potentialPath); + return ReadJson(jsonExtensionPath, fileSystem); } - return ReadYaml(potentialPath); - } + var yamlExtensionPaths = new[] { potentialPath + ".yaml", potentialPath + ".yml" }; + foreach (var yamlExtensionPath in yamlExtensionPaths) + { + if (!fileSystem.File.Exists(yamlExtensionPath)) + { + continue; + } - var jsonExtensionPath = potentialPath + ".json"; - if (fileSystem.File.Exists(jsonExtensionPath)) - { - return ReadJson(jsonExtensionPath); - } + return ReadYaml(yamlExtensionPath, fileSystem); + } - var yamlExtensionPaths = new[] { potentialPath + ".yaml", potentialPath + ".yml" }; - foreach (var yamlExtensionPath in yamlExtensionPaths) - { - if (!fileSystem.File.Exists(yamlExtensionPath)) + var directoryInfo = fileSystem.DirectoryInfo.FromDirectoryName(baseDirectoryPath); + if (directoryInfo.Parent == null) { - continue; + return new ConfigurationFileOptions(); } - - return ReadYaml(yamlExtensionPath); + baseDirectoryPath = directoryInfo.Parent.FullName; } + } + + private static ConfigurationFileOptions ReadJson(string path, IFileSystem fileSystem) + { + return JsonConvert.DeserializeObject( + fileSystem.File.ReadAllText(path) + ); + } + + private static ConfigurationFileOptions ReadYaml(string path, IFileSystem fileSystem) + { + var deserializer = new DeserializerBuilder().WithNamingConvention( + CamelCaseNamingConvention.Instance + ) + .Build(); - return new ConfigurationFileOptions(); + return deserializer.Deserialize( + fileSystem.File.ReadAllText(path) + ); } } } diff --git a/Src/CSharpier/Program.cs b/Src/CSharpier/Program.cs index 45465954d..fc0847907 100644 --- a/Src/CSharpier/Program.cs +++ b/Src/CSharpier/Program.cs @@ -44,41 +44,11 @@ CancellationToken cancellationToken SkipWrite = skipWrite }; - var rootPath = File.Exists(directoryOrFile) - ? Path.GetDirectoryName(directoryOrFile) - : directoryOrFile; - - if (rootPath == null) - { - throw new Exception( - "The path of " - + directoryOrFile - + " does not appear to point to a directory or a file." - ); - } - - var configurationFileOptions = ConfigurationFileOptions.Create( - rootPath, - new FileSystem() - ); - - var printerOptions = new PrinterOptions - { - TabWidth = configurationFileOptions.TabWidth, - UseTabs = configurationFileOptions.UseTabs, - Width = configurationFileOptions.PrintWidth, - EndOfLine = configurationFileOptions.EndOfLine - }; - - // TODO most parameters should probably be combined into an object - // TODO also the entry point should just be a static method call, hide the constructor and the other call - var commandLineFormatter = new CommandLineFormatter( - rootPath, + return await CommandLineFormatter.Format( commandLineOptions, - printerOptions, - new FileSystem() + new FileSystem(), + cancellationToken ); - return await commandLineFormatter.Format(cancellationToken); } } }