diff --git a/.gitignore b/.gitignore index 40ccc06c3..dcbb4722e 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ Src/CSharpier.VSCode/.idea/prettier.xml .idea/.idea.CSharpier/.idea/riderMarkupCache.xml /Src/CSharpier.Benchmarks/BenchmarkDotNet.Artifacts/ +/Src/CSharpier.Tests/TestResults diff --git a/Directory.Packages.props b/Directory.Packages.props index fbb2233a5..37383771f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,9 +7,11 @@ + + diff --git a/Scripts/RunLinuxTests.ps1 b/Scripts/RunLinuxTests.ps1 index 2d4e6d337..5dc30aeb0 100644 --- a/Scripts/RunLinuxTests.ps1 +++ b/Scripts/RunLinuxTests.ps1 @@ -1,7 +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 # 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 +docker run --rm -v ${pwd}:/app -e "NormalizeLineEndings=1" -w /app/tests mcr.microsoft.com/dotnet/sdk:7.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 f3cd7eee1..40761786e 100644 --- a/Src/CSharpier.Cli.Tests/CliTests.cs +++ b/Src/CSharpier.Cli.Tests/CliTests.cs @@ -67,6 +67,7 @@ public async Task Should_Format_Basic_File(string lineEnding) var result = await new CsharpierProcess().WithArguments("BasicFile.cs").ExecuteAsync(); + result.ErrorOutput.Should().BeNullOrEmpty(); result.Output.Should().StartWith("Formatted 1 files in "); result.ExitCode.Should().Be(0); (await this.ReadAllTextAsync("BasicFile.cs")).Should().Be(formattedContent); diff --git a/Src/CSharpier.Cli/CSharpier.Cli.csproj b/Src/CSharpier.Cli/CSharpier.Cli.csproj index 2deec950e..485396140 100644 --- a/Src/CSharpier.Cli/CSharpier.Cli.csproj +++ b/Src/CSharpier.Cli/CSharpier.Cli.csproj @@ -11,7 +11,9 @@ 002400000480000094000000060200000024000052534131000400000100010049d266ea1aeae09c0abfce28b8728314d4e4807126ee8bc56155a7ddc765997ed3522908b469ae133fc49ef0bfa957df36082c1c2e0ec8cdc05a4ca4dbd4e1bea6c17fc1008555e15af13a8fc871a04ffc38f5e60e6203bfaf01d16a2a283b90572ade79135801c1675bf38b7a5a60ec8353069796eb53a26ffdddc9ee1273be + + diff --git a/Src/CSharpier.Cli/CommandLineFormatter.cs b/Src/CSharpier.Cli/CommandLineFormatter.cs index 95384f2c6..b8d509c07 100644 --- a/Src/CSharpier.Cli/CommandLineFormatter.cs +++ b/Src/CSharpier.Cli/CommandLineFormatter.cs @@ -1,3 +1,5 @@ +using System.Text; +using CSharpier.Cli.Options; using System.Diagnostics; using System.IO.Abstractions; using CSharpier.Utilities; @@ -5,8 +7,6 @@ namespace CSharpier.Cli; -using System.Text; - internal static class CommandLineFormatter { public static async Task Format( @@ -31,8 +31,8 @@ CancellationToken cancellationToken console.InputEncoding ); - var (ignoreFile, printerOptions) = await GetIgnoreFileAndPrinterOptions( - filePath, + var optionsProvider = await OptionsProvider.Create( + fileSystem.Path.GetDirectoryName(filePath), commandLineOptions.ConfigPath, fileSystem, logger, @@ -41,7 +41,7 @@ CancellationToken cancellationToken if ( !GeneratedCodeUtilities.IsGeneratedCodeFile(filePath) - && !ignoreFile.IsIgnored(filePath) + && !optionsProvider.IsIgnored(filePath) ) { var fileIssueLogger = new FileIssueLogger( @@ -54,7 +54,7 @@ await PerformFormattingSteps( new StdOutFormattedFileWriter(console), commandLineFormatterResult, fileIssueLogger, - printerOptions, + optionsProvider.GetPrinterOptionsFor(filePath), commandLineOptions, FormattingCacheFactory.NullCache, cancellationToken @@ -129,22 +129,37 @@ CancellationToken cancellationToken for (var x = 0; x < commandLineOptions.DirectoryOrFilePaths.Length; x++) { - var directoryOrFile = commandLineOptions.DirectoryOrFilePaths[x].Replace("\\", "/"); - var originalDirectoryOrFile = commandLineOptions.OriginalDirectoryOrFilePaths[ - x - ].Replace("\\", "/"); + var directoryOrFilePath = commandLineOptions.DirectoryOrFilePaths[x].Replace("\\", "/"); + var isFile = fileSystem.File.Exists(directoryOrFilePath); + var isDirectory = fileSystem.Directory.Exists(directoryOrFilePath); - var (ignoreFile, printerOptions) = await GetIgnoreFileAndPrinterOptions( - directoryOrFile, + if (!isFile && !isDirectory) + { + console.WriteErrorLine( + "There was no file or directory found at " + directoryOrFilePath + ); + return 1; + } + + var directoryName = isFile + ? fileSystem.Path.GetDirectoryName(directoryOrFilePath) + : directoryOrFilePath; + + var optionsProvider = await OptionsProvider.Create( + directoryName, commandLineOptions.ConfigPath, fileSystem, logger, cancellationToken ); + var originalDirectoryOrFile = commandLineOptions.OriginalDirectoryOrFilePaths[ + x + ].Replace("\\", "/"); + var formattingCache = await FormattingCacheFactory.InitializeAsync( commandLineOptions, - printerOptions, + optionsProvider, fileSystem, cancellationToken ); @@ -159,9 +174,10 @@ CancellationToken cancellationToken async Task FormatFile(string actualFilePath, string originalFilePath) { + var printerOptions = optionsProvider.GetPrinterOptionsFor(actualFilePath); if ( GeneratedCodeUtilities.IsGeneratedCodeFile(actualFilePath) - || ignoreFile.IsIgnored(actualFilePath) + || optionsProvider.IsIgnored(actualFilePath) ) { return; @@ -181,28 +197,32 @@ await FormatPhysicalFile( ); } - if (fileSystem.File.Exists(directoryOrFile)) + if (isFile) { - await FormatFile(directoryOrFile, originalDirectoryOrFile); + await FormatFile(directoryOrFilePath, originalDirectoryOrFile); } - else if (fileSystem.Directory.Exists(directoryOrFile)) + else if (isDirectory) { if ( !commandLineOptions.NoMSBuildCheck - && HasMismatchedCliAndMsBuildVersions.Check(directoryOrFile, fileSystem, logger) + && HasMismatchedCliAndMsBuildVersions.Check( + directoryOrFilePath, + fileSystem, + logger + ) ) { return 1; } var tasks = fileSystem.Directory - .EnumerateFiles(directoryOrFile, "*.cs", SearchOption.AllDirectories) + .EnumerateFiles(directoryOrFilePath, "*.cs", SearchOption.AllDirectories) .Select(o => { var normalizedPath = o.Replace("\\", "/"); return FormatFile( normalizedPath, - normalizedPath.Replace(directoryOrFile, originalDirectoryOrFile) + normalizedPath.Replace(directoryOrFilePath, originalDirectoryOrFile) ); }) .ToArray(); @@ -218,13 +238,6 @@ await FormatPhysicalFile( } } } - else - { - console.WriteErrorLine( - "There was no file or directory found at " + directoryOrFile - ); - return 1; - } await formattingCache.ResolveAsync(cancellationToken); } @@ -232,38 +245,6 @@ await FormatPhysicalFile( return 0; } - private static async Task<(IgnoreFile, PrinterOptions)> GetIgnoreFileAndPrinterOptions( - string directoryOrFile, - string? configPath, - IFileSystem fileSystem, - ILogger logger, - CancellationToken cancellationToken - ) - { - var isDirectory = fileSystem.Directory.Exists(directoryOrFile); - - var baseDirectoryPath = isDirectory - ? directoryOrFile - : fileSystem.Path.GetDirectoryName(directoryOrFile); - - var ignoreFile = await IgnoreFile.Create( - baseDirectoryPath, - fileSystem, - logger, - cancellationToken - ); - - var printerOptions = configPath is null - ? ConfigurationFileOptions.FindPrinterOptionsForDirectory( - baseDirectoryPath, - fileSystem, - logger - ) - : ConfigurationFileOptions.CreatePrinterOptionsFromPath(configPath, fileSystem, logger); - - return (ignoreFile, printerOptions); - } - private static async Task FormatPhysicalFile( string actualFilePath, string originalFilePath, diff --git a/Src/CSharpier.Cli/EditorConfig/ConfigFile.cs b/Src/CSharpier.Cli/EditorConfig/ConfigFile.cs new file mode 100644 index 000000000..847d5bc6c --- /dev/null +++ b/Src/CSharpier.Cli/EditorConfig/ConfigFile.cs @@ -0,0 +1,7 @@ +namespace CSharpier.Cli.EditorConfig; + +internal class ConfigFile +{ + public required IReadOnlyCollection
Sections { get; init; } + public bool IsRoot { get; init; } +} diff --git a/Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs b/Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs new file mode 100644 index 000000000..d1462a40a --- /dev/null +++ b/Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs @@ -0,0 +1,24 @@ +using System.IO.Abstractions; +using IniParser; + +namespace CSharpier.Cli.EditorConfig; + +internal static class ConfigFileParser +{ + public static ConfigFile Parse(string filePath, IFileSystem fileSystem) + { + var parser = new FileIniDataParser(); + + using var stream = fileSystem.File.OpenRead(filePath); + using var streamReader = new StreamReader(stream); + var configData = parser.ReadData(streamReader); + + var directory = fileSystem.Path.GetDirectoryName(filePath); + var sections = new List
(); + foreach (var section in configData.Sections) + { + sections.Add(new Section(section, directory)); + } + return new ConfigFile { IsRoot = configData.Global["root"] == "true", Sections = sections }; + } +} diff --git a/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs b/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs new file mode 100644 index 000000000..b609e37ac --- /dev/null +++ b/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs @@ -0,0 +1,84 @@ +using System.IO.Abstractions; + +namespace CSharpier.Cli.EditorConfig; + +internal static class EditorConfigParser +{ + /// Finds all configs above the given directory as well as within the subtree of this directory + public static List FindForDirectoryName( + string directoryName, + IFileSystem fileSystem + ) + { + if (directoryName is "") + { + return new List(); + } + + var directoryInfo = fileSystem.DirectoryInfo.FromDirectoryName(directoryName); + var editorConfigFiles = directoryInfo + .EnumerateFiles(".editorconfig", SearchOption.AllDirectories) + .ToList(); + + // already found any in this directory above + directoryInfo = directoryInfo.Parent; + + while (directoryInfo is not null) + { + var file = fileSystem.FileInfo.FromFileName( + fileSystem.Path.Combine(directoryInfo.FullName, ".editorconfig") + ); + if (file.Exists) + { + editorConfigFiles.Add(file); + } + + directoryInfo = directoryInfo.Parent; + } + + return editorConfigFiles + .Select( + o => + new EditorConfigSections + { + DirectoryName = fileSystem.Path.GetDirectoryName(o.FullName), + SectionsIncludingParentFiles = FindSections(o.FullName, fileSystem) + } + ) + .OrderByDescending(o => o.DirectoryName.Length) + .ToList(); + } + + private static List
FindSections(string filePath, IFileSystem fileSystem) + { + return ParseConfigFiles(fileSystem.Path.GetDirectoryName(filePath), fileSystem) + .Reverse() + .SelectMany(configFile => configFile.Sections) + .ToList(); + } + + private static IEnumerable ParseConfigFiles( + string directoryPath, + IFileSystem fileSystem + ) + { + var directory = fileSystem.DirectoryInfo.FromDirectoryName(directoryPath); + while (directory != null) + { + var potentialPath = fileSystem.Path.Combine(directory.FullName, ".editorconfig"); + if (fileSystem.File.Exists(potentialPath)) + { + var configFile = ConfigFileParser.Parse(potentialPath, fileSystem); + + DebugLogger.Log(potentialPath); + yield return configFile; + if (configFile.IsRoot) + { + yield break; + } + } + + directory = directory.Parent; + } + } +} diff --git a/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs b/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs new file mode 100644 index 000000000..5fe3a2bb0 --- /dev/null +++ b/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs @@ -0,0 +1,86 @@ +namespace CSharpier.Cli.EditorConfig; + +/// +/// This is a representation of the editorconfig for the given directory along with +/// sections from any parent files until a root file is found +/// +internal class EditorConfigSections +{ + public required string DirectoryName { get; init; } + public required IReadOnlyCollection
SectionsIncludingParentFiles { get; init; } + + public PrinterOptions ConvertToPrinterOptions(string filePath) + { + var sections = this.SectionsIncludingParentFiles.Where(o => o.IsMatch(filePath)).ToList(); + var resolvedConfiguration = new ResolvedConfiguration(sections); + var printerOptions = new PrinterOptions(); + + if (resolvedConfiguration.MaxLineLength is { } maxLineLength) + { + printerOptions.Width = maxLineLength; + } + + if (resolvedConfiguration.IndentStyle is "tab") + { + printerOptions.UseTabs = true; + } + + if (printerOptions.UseTabs) + { + printerOptions.TabWidth = resolvedConfiguration.TabWidth ?? printerOptions.TabWidth; + } + else + { + printerOptions.TabWidth = resolvedConfiguration.IndentSize ?? printerOptions.TabWidth; + } + + return printerOptions; + } + + private class ResolvedConfiguration + { + public string? IndentStyle { get; } + public int? IndentSize { get; } + public int? TabWidth { get; } + public int? MaxLineLength { get; } + + public ResolvedConfiguration(List
sections) + { + var indentStyle = sections.LastOrDefault(o => o.IndentStyle != null)?.IndentStyle; + if (indentStyle is "space" or "tab") + { + this.IndentStyle = indentStyle; + } + + var maxLineLength = sections.LastOrDefault(o => o.MaxLineLength != null)?.MaxLineLength; + if (int.TryParse(maxLineLength, out var maxLineLengthValue) && maxLineLengthValue > 0) + { + this.MaxLineLength = maxLineLengthValue; + } + + var indentSize = sections.LastOrDefault(o => o.IndentSize != null)?.IndentSize; + var tabWidth = sections.LastOrDefault(o => o.TabWidth != null)?.TabWidth; + + if (indentSize == "tab") + { + if (int.TryParse(tabWidth, out var tabWidthValue)) + { + this.TabWidth = tabWidthValue; + } + + this.IndentSize = this.TabWidth; + } + else + { + if (int.TryParse(indentSize, out var indentSizeValue)) + { + this.IndentSize = indentSizeValue; + } + + this.TabWidth = int.TryParse(tabWidth, out var tabWidthValue) + ? tabWidthValue + : this.IndentSize; + } + } + } +} diff --git a/Src/CSharpier.Cli/EditorConfig/Section.cs b/Src/CSharpier.Cli/EditorConfig/Section.cs new file mode 100644 index 000000000..bdf4641db --- /dev/null +++ b/Src/CSharpier.Cli/EditorConfig/Section.cs @@ -0,0 +1,46 @@ +namespace CSharpier.Cli.EditorConfig; + +using DotNet.Globbing; +using IniParser.Model; + +public class Section +{ + private readonly Glob matcher; + public string Pattern { get; } + public string? IndentStyle { get; } + public string? IndentSize { get; } + public string? TabWidth { get; } + public string? MaxLineLength { get; } + + public Section(SectionData section, string directory) + { + this.Pattern = FixGlob(section.SectionName, directory); + this.matcher = Glob.Parse(this.Pattern); + this.IndentStyle = section.Keys["indent_style"]; + this.IndentSize = section.Keys["indent_size"]; + this.TabWidth = section.Keys["tab_width"]; + this.MaxLineLength = section.Keys["max_line_length"]; + } + + public bool IsMatch(string fileName) + { + return this.matcher.IsMatch(fileName); + } + + private static string FixGlob(string glob, string directory) + { + glob = glob.IndexOf('/') switch + { + -1 => "**/" + glob, + 0 => glob[1..], + _ => glob + }; + directory = directory.Replace(@"\", "/"); + if (!directory.EndsWith("/")) + { + directory += "/"; + } + + return directory + glob; + } +} diff --git a/Src/CSharpier.Cli/FormattingCache.cs b/Src/CSharpier.Cli/FormattingCache.cs index 239ef4196..d8418f34c 100644 --- a/Src/CSharpier.Cli/FormattingCache.cs +++ b/Src/CSharpier.Cli/FormattingCache.cs @@ -7,6 +7,8 @@ namespace CSharpier.Cli; +using CSharpier.Cli.Options; + internal interface IFormattingCache { Task ResolveAsync(CancellationToken cancellationToken); @@ -26,7 +28,7 @@ internal static class FormattingCacheFactory public static async Task InitializeAsync( CommandLineOptions commandLineOptions, - PrinterOptions printerOptions, + OptionsProvider optionsProvider, IFileSystem fileSystem, CancellationToken cancellationToken ) @@ -84,7 +86,7 @@ CancellationToken cancellationToken } } - return new FormattingCache(printerOptions, CacheFilePath, cacheDictionary, fileSystem); + return new FormattingCache(optionsProvider, CacheFilePath, cacheDictionary, fileSystem); } private class FormattingCache : IFormattingCache @@ -95,13 +97,13 @@ private class FormattingCache : IFormattingCache private readonly IFileSystem fileSystem; public FormattingCache( - PrinterOptions printerOptions, + OptionsProvider optionsProvider, string cacheFile, ConcurrentDictionary cacheDictionary, IFileSystem fileSystem ) { - this.optionsHash = GetOptionsHash(printerOptions); + this.optionsHash = GetOptionsHash(optionsProvider); this.cacheFile = cacheFile; this.cacheDictionary = cacheDictionary; this.fileSystem = fileSystem; @@ -129,10 +131,10 @@ public void CacheResult(string code, FileToFormatInfo fileToFormatInfo) this.cacheDictionary[fileToFormatInfo.Path] = Hash(code) + this.optionsHash; } - private static string GetOptionsHash(PrinterOptions printerOptions) + private static string GetOptionsHash(OptionsProvider optionsProvider) { var csharpierVersion = typeof(FormattingCache).Assembly.GetName().Version; - return Hash($"{csharpierVersion}_${JsonSerializer.Serialize(printerOptions)}"); + return Hash($"{csharpierVersion}_${optionsProvider.Serialize()}"); } private static string Hash(string input) diff --git a/Src/CSharpier.Cli/HasMismatchedCliAndMsBuildVersions.cs b/Src/CSharpier.Cli/HasMismatchedCliAndMsBuildVersions.cs index 2fb06e5b7..892849fdd 100644 --- a/Src/CSharpier.Cli/HasMismatchedCliAndMsBuildVersions.cs +++ b/Src/CSharpier.Cli/HasMismatchedCliAndMsBuildVersions.cs @@ -7,10 +7,10 @@ namespace CSharpier.Cli; public static class HasMismatchedCliAndMsBuildVersions { - public static bool Check(string directory, IFileSystem fileSystem, ILogger logger) + public static bool Check(string directoryName, IFileSystem fileSystem, ILogger logger) { var csProjPaths = fileSystem.Directory - .EnumerateFiles(directory, "*.csproj", SearchOption.AllDirectories) + .EnumerateFiles(directoryName, "*.csproj", SearchOption.AllDirectories) .ToArray(); var versionOfDotnetTool = typeof(CommandLineFormatter).Assembly diff --git a/Src/CSharpier.Cli/IgnoreFile.cs b/Src/CSharpier.Cli/IgnoreFile.cs index c3fab19f8..62e584df0 100644 --- a/Src/CSharpier.Cli/IgnoreFile.cs +++ b/Src/CSharpier.Cli/IgnoreFile.cs @@ -43,7 +43,6 @@ public bool IsIgnored(string filePath) public static async Task Create( string baseDirectoryPath, IFileSystem fileSystem, - ILogger logger, CancellationToken cancellationToken ) { diff --git a/Src/CSharpier.Cli/Options/CSharpierConfigData.cs b/Src/CSharpier.Cli/Options/CSharpierConfigData.cs new file mode 100644 index 000000000..ead1fd204 --- /dev/null +++ b/Src/CSharpier.Cli/Options/CSharpierConfigData.cs @@ -0,0 +1,3 @@ +namespace CSharpier.Cli.Options; + +internal record CSharpierConfigData(string DirectoryName, ConfigurationFileOptions CSharpierConfig); diff --git a/Src/CSharpier.Cli/ConfigurationFileOptions.cs b/Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs similarity index 64% rename from Src/CSharpier.Cli/ConfigurationFileOptions.cs rename to Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs index 793646b35..012b477a5 100644 --- a/Src/CSharpier.Cli/ConfigurationFileOptions.cs +++ b/Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs @@ -1,4 +1,4 @@ -namespace CSharpier.Cli; +namespace CSharpier.Cli.Options; using System.IO.Abstractions; using System.Text.Json; @@ -14,19 +14,6 @@ public class ConfigurationFileOptions private static readonly string[] validExtensions = { ".csharpierrc", ".json", ".yml", ".yaml" }; - internal static PrinterOptions FindPrinterOptionsForDirectory( - string baseDirectoryPath, - IFileSystem fileSystem, - ILogger logger - ) - { - DebugLogger.Log("Creating printer options for " + baseDirectoryPath); - - var configurationFileOptions = FindForDirectory(baseDirectoryPath, fileSystem, logger); - - return ConvertToPrinterOptions(configurationFileOptions); - } - internal static PrinterOptions CreatePrinterOptionsFromPath( string configPath, IFileSystem fileSystem, @@ -38,7 +25,7 @@ ILogger logger return ConvertToPrinterOptions(configurationFileOptions); } - private static PrinterOptions ConvertToPrinterOptions( + internal static PrinterOptions ConvertToPrinterOptions( ConfigurationFileOptions configurationFileOptions ) { @@ -51,13 +38,39 @@ ConfigurationFileOptions configurationFileOptions }; } - public static ConfigurationFileOptions FindForDirectory( - string baseDirectoryPath, + /// Finds all configs above the given directory as well as within the subtree of this directory + internal static List FindForDirectoryName( + string directoryName, IFileSystem fileSystem, - ILogger? logger = null + ILogger logger ) { - var directoryInfo = fileSystem.DirectoryInfo.FromDirectoryName(baseDirectoryPath); + var results = new List(); + var directoryInfo = fileSystem.DirectoryInfo.FromDirectoryName(directoryName); + + var filesByDirectory = directoryInfo + .EnumerateFiles(".csharpierrc*", SearchOption.AllDirectories) + .GroupBy(o => o.DirectoryName); + + foreach (var group in filesByDirectory) + { + var firstFile = group + .Where(o => validExtensions.Contains(o.Extension, StringComparer.OrdinalIgnoreCase)) + .MinBy(o => o.Extension); + + if (firstFile != null) + { + results.Add( + new CSharpierConfigData( + firstFile.DirectoryName, + Create(firstFile.FullName, fileSystem, logger) + ) + ); + } + } + + // already found any in this directory above + directoryInfo = directoryInfo.Parent; while (directoryInfo is not null) { @@ -68,16 +81,21 @@ public static ConfigurationFileOptions FindForDirectory( if (file != null) { - return Create(file.FullName, fileSystem, logger); + results.Add( + new CSharpierConfigData( + file.DirectoryName, + Create(file.FullName, fileSystem, logger) + ) + ); } directoryInfo = directoryInfo.Parent; } - return new ConfigurationFileOptions(); + return results.OrderByDescending(o => o.DirectoryName.Length).ToList(); } - public static ConfigurationFileOptions Create( + private static ConfigurationFileOptions Create( string configPath, IFileSystem fileSystem, ILogger? logger = null diff --git a/Src/CSharpier.Cli/Options/OptionsProvider.cs b/Src/CSharpier.Cli/Options/OptionsProvider.cs new file mode 100644 index 000000000..699ef24ba --- /dev/null +++ b/Src/CSharpier.Cli/Options/OptionsProvider.cs @@ -0,0 +1,112 @@ +namespace CSharpier.Cli.Options; + +using System.IO.Abstractions; +using System.Text.Json; +using CSharpier.Cli.EditorConfig; +using Microsoft.Extensions.Logging; +using PrinterOptions = CSharpier.PrinterOptions; + +internal class OptionsProvider +{ + private readonly List editorConfigs; + private readonly List csharpierConfigs; + private readonly IgnoreFile ignoreFile; + private readonly PrinterOptions? specifiedPrinterOptions; + private readonly IFileSystem fileSystem; + + private OptionsProvider( + List editorConfigs, + List csharpierConfigs, + IgnoreFile ignoreFile, + PrinterOptions? specifiedPrinterOptions, + IFileSystem fileSystem + ) + { + this.editorConfigs = editorConfigs; + this.csharpierConfigs = csharpierConfigs; + this.ignoreFile = ignoreFile; + this.specifiedPrinterOptions = specifiedPrinterOptions; + this.fileSystem = fileSystem; + } + + public static async Task Create( + string directoryName, + string? configPath, + IFileSystem fileSystem, + ILogger logger, + CancellationToken cancellationToken + ) + { + var specifiedPrinterOptions = configPath is not null + ? ConfigurationFileOptions.CreatePrinterOptionsFromPath(configPath, fileSystem, logger) + : null; + + var csharpierConfigs = configPath is null + ? ConfigurationFileOptions.FindForDirectoryName(directoryName, fileSystem, logger) + : Array.Empty().ToList(); + + var editorConfigSections = EditorConfigParser.FindForDirectoryName( + directoryName, + fileSystem + ); + var ignoreFile = await IgnoreFile.Create(directoryName, fileSystem, cancellationToken); + + return new OptionsProvider( + editorConfigSections, + csharpierConfigs, + ignoreFile, + specifiedPrinterOptions, + fileSystem + ); + } + + public PrinterOptions GetPrinterOptionsFor(string filePath) + { + if (this.specifiedPrinterOptions is not null) + { + return this.specifiedPrinterOptions; + } + + var directoryName = this.fileSystem.Path.GetDirectoryName(filePath); + var resolvedEditorConfig = this.editorConfigs.FirstOrDefault( + o => directoryName.StartsWith(o.DirectoryName) + ); + var resolvedCSharpierConfig = this.csharpierConfigs.FirstOrDefault( + o => directoryName.StartsWith(o.DirectoryName) + ); + + if (resolvedEditorConfig is null && resolvedCSharpierConfig is null) + { + return new PrinterOptions(); + } + + if ( + (resolvedCSharpierConfig?.DirectoryName.Length ?? int.MinValue) + >= (resolvedEditorConfig?.DirectoryName.Length ?? int.MinValue) + ) + { + return ConfigurationFileOptions.ConvertToPrinterOptions( + resolvedCSharpierConfig!.CSharpierConfig + ); + } + + return resolvedEditorConfig!.ConvertToPrinterOptions(filePath); + } + + public bool IsIgnored(string actualFilePath) + { + return this.ignoreFile.IsIgnored(actualFilePath); + } + + public string Serialize() + { + return JsonSerializer.Serialize( + new + { + specified = this.specifiedPrinterOptions, + csharpierConfigs = this.csharpierConfigs, + editorConfigs = this.editorConfigs + } + ); + } +} diff --git a/Src/CSharpier.Cli/Program.cs b/Src/CSharpier.Cli/Program.cs index 83ae998e1..517d7d29a 100644 --- a/Src/CSharpier.Cli/Program.cs +++ b/Src/CSharpier.Cli/Program.cs @@ -35,7 +35,6 @@ CancellationToken cancellationToken // System.CommandLine passes string.empty instead of null when this isn't supplied even if we use string? var actualConfigPath = string.IsNullOrEmpty(configPath) ? null : configPath; - DebugLogger.Log("Starting"); var console = new SystemConsole(); var logger = new ConsoleLogger(console, logLevel); @@ -122,10 +121,8 @@ CancellationToken cancellationToken return exitCode; } var character = Convert.ToChar(value); - DebugLogger.Log("Got " + character); if (character == '\u0003') { - DebugLogger.Log("Got EOF"); break; } diff --git a/Src/CSharpier.Tests/ConfigurationFileOptionsTests.cs b/Src/CSharpier.Tests/ConfigurationFileOptionsTests.cs deleted file mode 100644 index 4b5007170..000000000 --- a/Src/CSharpier.Tests/ConfigurationFileOptionsTests.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System.Collections.Generic; -using System.IO.Abstractions.TestingHelpers; -using CSharpier.Cli; -using FluentAssertions; -using NUnit.Framework; - -namespace CSharpier.Tests; - -[TestFixture] -[Parallelizable(ParallelScope.Fixtures)] -public class ConfigurationFileOptionsTests -{ - [Test] - public void Should_Return_Default_Options_With_Empty_Json() - { - var context = new TestContext(); - context.WhenAFileExists("c:/test/.csharpierrc", "{}"); - - var result = context.CreateConfigurationOptions("c:/test"); - - ShouldHaveDefaultOptions(result); - } - - [Test] - public void Should_Return_Default_Options_With_No_File() - { - var context = new TestContext(); - var result = context.CreateConfigurationOptions("c:/test"); - - ShouldHaveDefaultOptions(result); - } - - [TestCase(".csharpierrc")] - [TestCase(".csharpierrc.json")] - [TestCase(".csharpierrc.yaml")] - public void Should_Return_Default_Options_With_Empty_File(string fileName) - { - var context = new TestContext(); - context.WhenAFileExists($"c:/test/{fileName}", string.Empty); - var result = context.CreateConfigurationOptions("c:/test"); - - ShouldHaveDefaultOptions(result); - } - - [Test] - public void Should_Return_Json_Extension_Options() - { - var context = new TestContext(); - context.WhenAFileExists( - "c:/test/.csharpierrc.json", - @"{ - ""printWidth"": 10, - ""preprocessorSymbolSets"": [""1,2"", ""3""] -}" - ); - - var result = context.CreateConfigurationOptions("c:/test"); - - result.PrintWidth.Should().Be(10); - } - - [TestCase("yaml")] - [TestCase("yml")] - public void Should_Return_Yaml_Extension_Options(string extension) - { - var context = new TestContext(); - context.WhenAFileExists( - $"c:/test/.csharpierrc.{extension}", - @" -printWidth: 10 -preprocessorSymbolSets: - - 1,2 - - 3 -" - ); - - var result = context.CreateConfigurationOptions("c:/test"); - - result.PrintWidth.Should().Be(10); - } - - [TestCase("{ \"printWidth\": 10 }")] - [TestCase("printWidth: 10")] - public void Should_Read_ExtensionLess_File(string contents) - { - var context = new TestContext(); - context.WhenAFileExists($"c:/test/.csharpierrc", contents); - - var result = context.CreateConfigurationOptions("c:/test"); - - 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) - { - var context = new TestContext(); - context.WhenAFileExists($"c:/test/.csharpierrc{extension}", contents); - - var result = context.CreateConfigurationOptions("c:/test/subfolder"); - - result.PrintWidth.Should().Be(10); - } - - [Test] - public void Should_Prefer_No_Extension() - { - var context = new TestContext(); - context.WhenAFileExists("c:/test/.csharpierrc", "{ \"printWidth\": 1 }"); - - context.WhenAFileExists("c:/test/.csharpierrc.json", "{ \"printWidth\": 2 }"); - context.WhenAFileExists("c:/test/.csharpierrc.yaml", "printWidth: 3"); - - var result = context.CreateConfigurationOptions("c:/test"); - - result.PrintWidth.Should().Be(1); - } - - [Test] - public void Should_Return_PrintWidth_With_Json() - { - var context = new TestContext(); - context.WhenAFileExists("c:/test/.csharpierrc", "{ \"printWidth\": 10 }"); - - var result = context.CreateConfigurationOptions("c:/test"); - - result.PrintWidth.Should().Be(10); - } - - [Test] - public void Should_Return_TabWidth_With_Json() - { - var context = new TestContext(); - context.WhenAFileExists("c:/test/.csharpierrc", "{ \"tabWidth\": 10 }"); - - var result = context.CreateConfigurationOptions("c:/test"); - - result.TabWidth.Should().Be(10); - } - - [Test] - public void Should_Return_UseTabs_With_Json() - { - var context = new TestContext(); - context.WhenAFileExists("c:/test/.csharpierrc", "{ \"useTabs\": true }"); - - var result = context.CreateConfigurationOptions("c:/test"); - - result.UseTabs.Should().BeTrue(); - } - - [Test] - public void Should_Return_PrintWidth_With_Yaml() - { - var context = new TestContext(); - context.WhenAFileExists("c:/test/.csharpierrc", "printWidth: 10"); - - var result = context.CreateConfigurationOptions("c:/test"); - - result.PrintWidth.Should().Be(10); - } - - [Test] - public void Should_Return_TabWidth_With_Yaml() - { - var context = new TestContext(); - context.WhenAFileExists("c:/test/.csharpierrc", "tabWidth: 10"); - - var result = context.CreateConfigurationOptions("c:/test"); - - result.TabWidth.Should().Be(10); - } - - [Test] - public void Should_Return_UseTabs_With_Yaml() - { - var context = new TestContext(); - context.WhenAFileExists("c:/test/.csharpierrc", "useTabs: true"); - - var result = context.CreateConfigurationOptions("c:/test"); - - result.UseTabs.Should().BeTrue(); - } - - private static void ShouldHaveDefaultOptions(ConfigurationFileOptions configurationFileOptions) - { - configurationFileOptions.PrintWidth.Should().Be(100); - configurationFileOptions.TabWidth.Should().Be(4); - configurationFileOptions.UseTabs.Should().BeFalse(); - } - - private class TestContext - { - private MockFileSystem fileSystem = new(); - - public ConfigurationFileOptions CreateConfigurationOptions(string baseDirectoryPath) - { - this.fileSystem.AddDirectory(baseDirectoryPath); - return ConfigurationFileOptions.FindForDirectory(baseDirectoryPath, this.fileSystem); - } - - public void WhenAFileExists(string path, string contents) - { - this.fileSystem.AddFile(path, new MockFileData(contents)); - } - } -} diff --git a/Src/CSharpier.Tests/DocPrinterTests.cs b/Src/CSharpier.Tests/DocPrinterTests.cs index e32ef98e8..0671c3c0c 100644 --- a/Src/CSharpier.Tests/DocPrinterTests.cs +++ b/Src/CSharpier.Tests/DocPrinterTests.cs @@ -739,6 +739,11 @@ private static void PrintedDocShouldBe( bool useTabs = false ) { + if (Environment.GetEnvironmentVariable("NormalizeLineEndings") != null) + { + expected = expected.Replace("\r\n", "\n"); + } + var result = Print(doc, width, trimInitialLines, useTabs); result.Should().Be(expected); diff --git a/Src/CSharpier.Tests/DocSerializerTests.cs b/Src/CSharpier.Tests/DocSerializerTests.cs index 91ebb452a..631fa8878 100644 --- a/Src/CSharpier.Tests/DocSerializerTests.cs +++ b/Src/CSharpier.Tests/DocSerializerTests.cs @@ -15,7 +15,7 @@ public void Should_Format_Directive() var actual = DocSerializer.Serialize(doc); - actual.Should().Be("Doc.Directive(\"1\")"); + ActualShouldBe(actual, "Doc.Directive(\"1\")"); } [Test] @@ -37,10 +37,9 @@ public void Should_Format_Basic_Types() var actual = DocSerializer.Serialize(doc); - actual - .Should() - .Be( - @"Doc.Concat( + ActualShouldBe( + actual, + @"Doc.Concat( Doc.Line, Doc.LiteralLine, Doc.HardLine, @@ -53,7 +52,7 @@ public void Should_Format_Basic_Types() Doc.BreakParent, ""1"" )" - ); + ); } [Test] @@ -63,14 +62,13 @@ public void Should_Print_Basic_Group() var actual = DocSerializer.Serialize(doc); - actual - .Should() - .Be( - @"Doc.Group( + ActualShouldBe( + actual, + @"Doc.Group( Doc.Null, Doc.Null )" - ); + ); } [Test] @@ -80,15 +78,14 @@ public void Should_Print_Group_With_Id() var actual = DocSerializer.Serialize(doc); - actual - .Should() - .Be( - @"Doc.GroupWithId( + ActualShouldBe( + actual, + @"Doc.GroupWithId( ""1"", Doc.Null, Doc.Null )" - ); + ); } [Test] @@ -101,10 +98,9 @@ public void Should_Print_ConditionalGroup() var actual = DocSerializer.Serialize(doc); - actual - .Should() - .Be( - @"Doc.ConditionalGroup( + ActualShouldBe( + actual, + @"Doc.ConditionalGroup( Doc.Concat( Doc.Line, Doc.Line @@ -114,7 +110,7 @@ public void Should_Print_ConditionalGroup() Doc.LiteralLine ) )" - ); + ); } [Test] @@ -124,15 +120,14 @@ public void Should_Print_Align() var actual = DocSerializer.Serialize(doc); - actual - .Should() - .Be( - @"Doc.Align( + ActualShouldBe( + actual, + @"Doc.Align( 2, Doc.Null, Doc.Null )" - ); + ); } [Test] @@ -142,14 +137,13 @@ public void Should_Print_ForceFlat() var actual = DocSerializer.Serialize(doc); - actual - .Should() - .Be( - @"Doc.ForceFlat( + ActualShouldBe( + actual, + @"Doc.ForceFlat( Doc.Null, Doc.Null )" - ); + ); } [Test] @@ -159,14 +153,13 @@ public void Should_Print_Indent() var actual = DocSerializer.Serialize(doc); - actual - .Should() - .Be( - @"Doc.Indent( + ActualShouldBe( + actual, + @"Doc.Indent( Doc.Null, Doc.Null )" - ); + ); } [Test] @@ -176,14 +169,13 @@ public void Should_Print_IndentIfBreak() var actual = DocSerializer.Serialize(doc); - actual - .Should() - .Be( - @"Doc.IndentIfBreak( + ActualShouldBe( + actual, + @"Doc.IndentIfBreak( Doc.Null, ""1"" )" - ); + ); } [Test] @@ -193,15 +185,14 @@ public void Should_Print_IfBreak_With_Id() var actual = DocSerializer.Serialize(doc); - actual - .Should() - .Be( - @"Doc.IfBreak( + ActualShouldBe( + actual, + @"Doc.IfBreak( Doc.Null, Doc.Line, ""1"" )" - ); + ); } [Test] @@ -211,14 +202,13 @@ public void Should_Print_IfBreak_Without_Id() var actual = DocSerializer.Serialize(doc); - actual - .Should() - .Be( - @"Doc.IfBreak( + ActualShouldBe( + actual, + @"Doc.IfBreak( Doc.Null, Doc.Line )" - ); + ); } [TestCase(CommentType.SingleLine)] @@ -229,7 +219,7 @@ public void Should_Print_LeadingComment(CommentType commentType) var actual = DocSerializer.Serialize(doc); - actual.Should().Be(@$"Doc.LeadingComment(""1"", CommentType.{commentType})"); + ActualShouldBe(actual, @$"Doc.LeadingComment(""1"", CommentType.{commentType})"); } [TestCase(CommentType.SingleLine)] @@ -240,6 +230,16 @@ public void Should_Print_TrailingComment(CommentType commentType) var actual = DocSerializer.Serialize(doc); - actual.Should().Be(@$"Doc.TrailingComment(""1"", CommentType.{commentType})"); + ActualShouldBe(actual, @$"Doc.TrailingComment(""1"", CommentType.{commentType})"); + } + + private static void ActualShouldBe(string result, string be) + { + if (Environment.GetEnvironmentVariable("NormalizeLineEndings") != null) + { + be = be.Replace("\r\n", "\n"); + } + + result.Should().Be(be); } } diff --git a/Src/CSharpier.Tests/OptionsProviderTests.cs b/Src/CSharpier.Tests/OptionsProviderTests.cs new file mode 100644 index 000000000..3d2c64c7d --- /dev/null +++ b/Src/CSharpier.Tests/OptionsProviderTests.cs @@ -0,0 +1,501 @@ +namespace CSharpier.Tests; + +using System.IO.Abstractions.TestingHelpers; +using CSharpier.Cli.Options; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using NUnit.Framework; + +[TestFixture] +[Parallelizable(ParallelScope.Fixtures)] +public class OptionsProviderTests +{ + [Test] + public async Task Should_Return_Default_Options_With_Empty_Json() + { + var context = new TestContext(); + context.WhenAFileExists("c:/test/.csharpierrc", "{}"); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + ShouldHaveDefaultOptions(result); + } + + [Test] + public async Task Should_Return_Default_Options_With_No_File() + { + var context = new TestContext(); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + ShouldHaveDefaultOptions(result); + } + + [TestCase(".csharpierrc")] + [TestCase(".csharpierrc.json")] + [TestCase(".csharpierrc.yaml")] + public async Task Should_Return_Default_Options_With_Empty_File(string fileName) + { + var context = new TestContext(); + context.WhenAFileExists($"c:/test/{fileName}", string.Empty); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + ShouldHaveDefaultOptions(result); + } + + [Test] + public async Task Should_Return_Json_Extension_Options() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/.csharpierrc.json", + @"{ + ""printWidth"": 10, + ""preprocessorSymbolSets"": [""1,2"", ""3""] +}" + ); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.Width.Should().Be(10); + } + + [TestCase("yaml")] + [TestCase("yml")] + public async Task Should_Return_Yaml_Extension_Options(string extension) + { + var context = new TestContext(); + context.WhenAFileExists( + $"c:/test/.csharpierrc.{extension}", + @" +printWidth: 10 +preprocessorSymbolSets: + - 1,2 + - 3 +" + ); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.Width.Should().Be(10); + } + + [TestCase("{ \"printWidth\": 10 }")] + [TestCase("printWidth: 10")] + public async Task Should_Read_ExtensionLess_File(string contents) + { + var context = new TestContext(); + context.WhenAFileExists($"c:/test/.csharpierrc", contents); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.Width.Should().Be(10); + } + + [TestCase("", "printWidth: 10")] + [TestCase("", "{ \"printWidth\": 10 }")] + [TestCase(".yml", "printWidth: 10")] + [TestCase(".yaml", "printWidth: 10")] + [TestCase(".json", "{ \"printWidth\": 10 }")] + public async Task Should_Find_Configuration_In_Parent_Directory( + string extension, + string contents + ) + { + var context = new TestContext(); + context.WhenAFileExists($"c:/test/.csharpierrc{extension}", contents); + + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test/subfolder", + "c:/test/subfolder/test.cs" + ); + + result.Width.Should().Be(10); + } + + [Test] + public async Task Should_Prefer_No_Extension() + { + var context = new TestContext(); + context.WhenAFileExists("c:/test/.csharpierrc", "{ \"printWidth\": 1 }"); + + context.WhenAFileExists("c:/test/.csharpierrc.json", "{ \"printWidth\": 2 }"); + context.WhenAFileExists("c:/test/.csharpierrc.yaml", "printWidth: 3"); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.Width.Should().Be(1); + } + + [Test] + public async Task Should_Return_PrintWidth_With_Json() + { + var context = new TestContext(); + context.WhenAFileExists("c:/test/.csharpierrc", "{ \"printWidth\": 10 }"); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.Width.Should().Be(10); + } + + [Test] + public async Task Should_Return_TabWidth_With_Json() + { + var context = new TestContext(); + context.WhenAFileExists("c:/test/.csharpierrc", "{ \"tabWidth\": 10 }"); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.TabWidth.Should().Be(10); + } + + [Test] + public async Task Should_Return_UseTabs_With_Json() + { + var context = new TestContext(); + context.WhenAFileExists("c:/test/.csharpierrc", "{ \"useTabs\": true }"); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.UseTabs.Should().BeTrue(); + } + + [Test] + public async Task Should_Return_PrintWidth_With_Yaml() + { + var context = new TestContext(); + context.WhenAFileExists("c:/test/.csharpierrc", "printWidth: 10"); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.Width.Should().Be(10); + } + + [Test] + public async Task Should_Return_TabWidth_With_Yaml() + { + var context = new TestContext(); + context.WhenAFileExists("c:/test/.csharpierrc", "tabWidth: 10"); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.TabWidth.Should().Be(10); + } + + [Test] + public async Task Should_Return_UseTabs_With_Yaml() + { + var context = new TestContext(); + context.WhenAFileExists("c:/test/.csharpierrc", "useTabs: true"); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.UseTabs.Should().BeTrue(); + } + + [Test] + public async Task Should_Support_EditorConfig_Basic() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/.editorconfig", + @" +[*] +indent_style = space +indent_size = 2 +max_line_length = 10 +" + ); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.UseTabs.Should().BeFalse(); + result.TabWidth.Should().Be(2); + result.Width.Should().Be(10); + } + + [TestCase("tab_width")] + [TestCase("indent_size")] + public async Task Should_Support_EditorConfig_Tabs(string propertyName) + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/.editorconfig", + $@" + [*] + indent_style = tab + {propertyName} = 2 + " + ); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.UseTabs.Should().BeTrue(); + result.TabWidth.Should().Be(2); + } + + [Test] + public async Task Should_Support_EditorConfig_Tabs_With_Tab_Width() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/.editorconfig", + @" + [*] + indent_style = tab + indent_size = 1 + tab_width = 3 + " + ); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.UseTabs.Should().BeTrue(); + result.TabWidth.Should().Be(3); + } + + [Test] + public async Task Should_Support_EditorConfig_Tabs_With_Indent_Size_Tab() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/.editorconfig", + @" + [*] + indent_size = tab + tab_width = 3 + " + ); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + + result.TabWidth.Should().Be(3); + } + + [Test] + public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/subfolder/.editorconfig", + @" + [*] + indent_size = 1 + " + ); + + context.WhenAFileExists( + "c:/test/.editorconfig", + @" + [*] + indent_size = 2 + max_line_length = 10 + " + ); + + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test/subfolder", + "c:/test/subfolder/test.cs" + ); + result.TabWidth.Should().Be(1); + result.Width.Should().Be(10); + } + + [Test] + public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files_And_Unset() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/subfolder/.editorconfig", + @" + [*] + indent_size = unset + " + ); + + context.WhenAFileExists( + "c:/test/.editorconfig", + @" + [*] + indent_size = 2 + " + ); + + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test/subfolder", + "c:/test/subfolder/test.cs" + ); + result.TabWidth.Should().Be(4); + } + + [Test] + public async Task Should_Support_EditorConfig_Root() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/subfolder/.editorconfig", + @" + root = true + + [*] + indent_size = 2 + " + ); + + context.WhenAFileExists( + "c:/test/.editorconfig", + @" + [*] + max_line_length = 2 + " + ); + + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test/subfolder", + "c:/test/subfolder/test.cs" + ); + result.Width.Should().Be(100); + } + + [Test] + public async Task Should_Support_EditorConfig_Tabs_With_Globs() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/.editorconfig", + @" +[*] +indent_size = 1 + +[*.cs] +indent_size = 2 +" + ); + + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); + result.TabWidth.Should().Be(2); + } + + [Test] + public async Task Should_Find_EditorConfig_In_Parent_Directory() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/.editorconfig", + @" +[*.cs] +indent_size = 2 +" + ); + + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test/subfolder", + "c:/test/subfolder/test.cs" + ); + result.TabWidth.Should().Be(2); + } + + [Test] + public async Task Should_Prefer_CSharpierrc_In_SameFolder() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/.editorconfig", + @" +[*.cs] +indent_size = 2 +" + ); + context.WhenAFileExists("c:/test/.csharpierrc", "tabWidth: 1"); + + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test/subfolder", + "c:/test/test.cs" + ); + result.TabWidth.Should().Be(1); + } + + [Test] + public async Task Should_Prefer_Closer_EditorConfig() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/subfolder/.editorconfig", + @" +[*.cs] +indent_size = 2 +" + ); + context.WhenAFileExists("c:/test/.csharpierrc", "tabWidth: 1"); + + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test", + "c:/test/subfolder/test.cs" + ); + result.TabWidth.Should().Be(2); + } + + [Test] + public async Task Should_Prefer_Closer_CSharpierrc() + { + var context = new TestContext(); + context.WhenAFileExists( + "c:/test/.editorconfig", + @" +[*.cs] +indent_size = 2 +" + ); + context.WhenAFileExists("c:/test/subfolder/.csharpierrc", "tabWidth: 1"); + + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test", + "c:/test/subfolder/test.cs" + ); + result.TabWidth.Should().Be(1); + } + + private static void ShouldHaveDefaultOptions(PrinterOptions printerOptions) + { + printerOptions.Width.Should().Be(100); + printerOptions.TabWidth.Should().Be(4); + printerOptions.UseTabs.Should().BeFalse(); + } + + private class TestContext + { + private readonly MockFileSystem fileSystem = new(); + + public void WhenAFileExists(string path, string contents) + { + if (!OperatingSystem.IsWindows()) + { + path = path.Replace("c:", string.Empty); + } + + this.fileSystem.AddFile(path, new MockFileData(contents)); + } + + public async Task CreateProviderAndGetOptionsFor( + string directoryName, + string filePath + ) + { + if (!OperatingSystem.IsWindows()) + { + directoryName = directoryName.Replace("c:", string.Empty); + filePath = filePath.Replace("c:", string.Empty); + } + + this.fileSystem.AddDirectory(directoryName); + var provider = await OptionsProvider.Create( + directoryName, + null, + this.fileSystem, + NullLogger.Instance, + CancellationToken.None + ); + + return provider.GetPrinterOptionsFor(filePath); + } + } +} diff --git a/Src/CSharpier.Tests/SyntaxNodeComparerTests.cs b/Src/CSharpier.Tests/SyntaxNodeComparerTests.cs index bebdd6da6..642889d1e 100644 --- a/Src/CSharpier.Tests/SyntaxNodeComparerTests.cs +++ b/Src/CSharpier.Tests/SyntaxNodeComparerTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading; using FluentAssertions; using NUnit.Framework; @@ -64,10 +62,9 @@ public ConstructorWithBase(string value) var result = AreEqual(left, right); - result - .Should() - .Be( - @"----------------------------- Original: Around Line 2 ----------------------------- + ResultShouldBe( + result, + @"----------------------------- Original: Around Line 2 ----------------------------- public class ConstructorWithBase { public ConstructorWithBase(string value) @@ -84,8 +81,8 @@ public ConstructorWithBase(string value) return; } } -" - ); +".ReplaceLineEndings() + ); } [Test] diff --git a/Src/CSharpier/PrinterOptions.cs b/Src/CSharpier/PrinterOptions.cs index a1f86b958..ca1d9f0fd 100644 --- a/Src/CSharpier/PrinterOptions.cs +++ b/Src/CSharpier/PrinterOptions.cs @@ -4,9 +4,9 @@ internal class PrinterOptions { public bool IncludeAST { get; init; } public bool IncludeDocTree { get; init; } - public bool UseTabs { get; init; } - public int TabWidth { get; init; } = 4; - public int Width { get; init; } = 100; + public bool UseTabs { get; set; } + public int TabWidth { get; set; } = 4; + public int Width { get; set; } = 100; public EndOfLine EndOfLine { get; init; } = EndOfLine.Auto; public bool TrimInitialLines { get; init; } = true;