From df793cade361b34d8240e1187035cde210c382d4 Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Fri, 7 Jul 2023 15:07:33 -0500 Subject: [PATCH 01/10] Adding support for reading editorconfigs closes #630 --- Directory.Packages.props | 2 + Src/CSharpier.Cli.Tests/CliTests.cs | 1 + Src/CSharpier.Cli/CSharpier.Cli.csproj | 1 + Src/CSharpier.Cli/CommandLineFormatter.cs | 76 ++-- Src/CSharpier.Cli/EditorConfig/ConfigFile.cs | 7 + .../EditorConfig/ConfigFileParser.cs | 78 ++++ .../EditorConfig/EditorConfigParser.cs | 69 ++++ .../EditorConfig/EditorConfigSections.cs | 82 ++++ Src/CSharpier.Cli/EditorConfig/Section.cs | 51 +++ Src/CSharpier.Cli/FormattingCache.cs | 15 +- .../HasMismatchedCliAndMsBuildVersions.cs | 4 +- Src/CSharpier.Cli/IgnoreFile.cs | 1 - .../Options/CSharpierConfigData.cs | 3 + .../{ => Options}/ConfigurationFileOptions.cs | 61 ++- Src/CSharpier.Cli/Options/OptionsProvider.cs | 96 +++++ .../ConfigurationFileOptionsTests.cs | 211 ---------- Src/CSharpier.Tests/OptionsProviderTests.cs | 391 ++++++++++++++++++ Src/CSharpier/PrinterOptions.cs | 6 +- 18 files changed, 861 insertions(+), 294 deletions(-) create mode 100644 Src/CSharpier.Cli/EditorConfig/ConfigFile.cs create mode 100644 Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs create mode 100644 Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs create mode 100644 Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs create mode 100644 Src/CSharpier.Cli/EditorConfig/Section.cs create mode 100644 Src/CSharpier.Cli/Options/CSharpierConfigData.cs rename Src/CSharpier.Cli/{ => Options}/ConfigurationFileOptions.cs (65%) create mode 100644 Src/CSharpier.Cli/Options/OptionsProvider.cs delete mode 100644 Src/CSharpier.Tests/ConfigurationFileOptionsTests.cs create mode 100644 Src/CSharpier.Tests/OptionsProviderTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 9e132982b..73989fb3a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,9 @@ + + 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..675066cce 100644 --- a/Src/CSharpier.Cli/CSharpier.Cli.csproj +++ b/Src/CSharpier.Cli/CSharpier.Cli.csproj @@ -11,6 +11,7 @@ 002400000480000094000000060200000024000052534131000400000100010049d266ea1aeae09c0abfce28b8728314d4e4807126ee8bc56155a7ddc765997ed3522908b469ae133fc49ef0bfa957df36082c1c2e0ec8cdc05a4ca4dbd4e1bea6c17fc1008555e15af13a8fc871a04ffc38f5e60e6203bfaf01d16a2a283b90572ade79135801c1675bf38b7a5a60ec8353069796eb53a26ffdddc9ee1273be + diff --git a/Src/CSharpier.Cli/CommandLineFormatter.cs b/Src/CSharpier.Cli/CommandLineFormatter.cs index 95384f2c6..10c554330 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,31 @@ CancellationToken cancellationToken for (var x = 0; x < commandLineOptions.DirectoryOrFilePaths.Length; x++) { - var directoryOrFile = commandLineOptions.DirectoryOrFilePaths[x].Replace("\\", "/"); - var originalDirectoryOrFile = commandLineOptions.OriginalDirectoryOrFilePaths[ - x - ].Replace("\\", "/"); - - var (ignoreFile, printerOptions) = await GetIgnoreFileAndPrinterOptions( - directoryOrFile, + var directoryOrFilePath = commandLineOptions.DirectoryOrFilePaths[x]; + var isFile = fileSystem.File.Exists(directoryOrFilePath); + var isDirectory = fileSystem.Directory.Exists(directoryOrFilePath); + var directoryName = isFile + ? fileSystem.Path.GetDirectoryName(directoryOrFilePath) + : isDirectory + ? directoryOrFilePath + : string.Empty; + // TODO 1 if single file don't look in subtree? just look for the one for this file + var optionsProvider = await OptionsProvider.Create( + directoryName, commandLineOptions.ConfigPath, fileSystem, logger, cancellationToken ); + var directoryOrFile = directoryOrFilePath.Replace("\\", "/"); + var originalDirectoryOrFile = commandLineOptions.OriginalDirectoryOrFilePaths[ + x + ].Replace("\\", "/"); + var formattingCache = await FormattingCacheFactory.InitializeAsync( commandLineOptions, - printerOptions, + optionsProvider, fileSystem, cancellationToken ); @@ -159,9 +168,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,11 +191,11 @@ await FormatPhysicalFile( ); } - if (fileSystem.File.Exists(directoryOrFile)) + if (isFile) { await FormatFile(directoryOrFile, originalDirectoryOrFile); } - else if (fileSystem.Directory.Exists(directoryOrFile)) + else if (isDirectory) { if ( !commandLineOptions.NoMSBuildCheck @@ -232,38 +242,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..5ec84f19d --- /dev/null +++ b/Src/CSharpier.Cli/EditorConfig/ConfigFile.cs @@ -0,0 +1,7 @@ +namespace CSharpier.Cli.EditorConfig; + +internal class ConfigFile +{ + public required List
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..b12c7604b --- /dev/null +++ b/Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs @@ -0,0 +1,78 @@ +using System.IO.Abstractions; +using System.Text.RegularExpressions; + +namespace CSharpier.Cli.EditorConfig; + +internal static class ConfigFileParser +{ + private static readonly Regex SectionRegex = new(@"^\s*\[(([^#;]|\\#|\\;)+)\]\s*([#;].*)?$"); + private static readonly Regex CommentRegex = new(@"^\s*[#;]"); + private static readonly Regex PropertyRegex = + new(@"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*([#;].*)?$"); + + private static readonly HashSet KnownProperties = + new( + new[] { "indent_style", "indent_size", "tab_width", "max_line_length", "root", }, + StringComparer.OrdinalIgnoreCase + ); + + public static ConfigFile Parse(string filePath, IFileSystem fileSystem) + { + var lines = fileSystem.File.ReadLines(filePath); + + var isRoot = false; + var propertiesBySection = new Dictionary>(); + var sectionName = string.Empty; + foreach (var line in lines) + { + if (string.IsNullOrWhiteSpace(line) || CommentRegex.IsMatch(line)) + { + continue; + } + + var propertyMatches = PropertyRegex.Matches(line); + if (propertyMatches.Count > 0) + { + var key = propertyMatches[0].Groups[1].Value.Trim().ToLowerInvariant(); + var value = propertyMatches[0].Groups[2].Value.Trim(); + + if (KnownProperties.Contains(key)) + { + value = value.ToLowerInvariant(); + } + + if (sectionName is "") + { + if (key == "root" && bool.TryParse(value, out var parsedValue)) + { + isRoot = parsedValue; + } + } + else + { + propertiesBySection[sectionName][key] = value; + } + } + else + { + var sectionMatches = SectionRegex.Matches(line); + if (sectionMatches.Count <= 0) + { + continue; + } + + sectionName = sectionMatches[0].Groups[1].Value; + propertiesBySection[sectionName] = new Dictionary(); + } + } + + var directory = fileSystem.Path.GetDirectoryName(filePath); + return new ConfigFile + { + IsRoot = isRoot, + Sections = propertiesBySection + .Select(o => new Section(o.Key, directory, o.Value)) + .ToList() + }; + } +} diff --git a/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs b/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs new file mode 100644 index 000000000..8cab97e2d --- /dev/null +++ b/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs @@ -0,0 +1,69 @@ +using System.IO.Abstractions; + +namespace CSharpier.Cli.EditorConfig; + +internal static class EditorConfigParser +{ + public static List GetAllForDirectory( + string directoryName, + IFileSystem fileSystem + ) + { + if (directoryName is "") + { + return new List(); + } + + var editorConfigFiles = fileSystem.DirectoryInfo + .FromDirectoryName(directoryName) + .EnumerateFiles(".editorconfig", SearchOption.AllDirectories); + + return editorConfigFiles + .Select( + o => + new EditorConfigSections + { + DirectoryName = fileSystem.Path.GetDirectoryName(o.FullName), + SectionsIncludingParentFiles = FindSections(o.FullName, fileSystem) + } + ) + .OrderBy(o => o.DirectoryName) + .ToList(); + } + + private static List
FindSections(string filePath, IFileSystem fileSystem) + { + var editorConfigFiles = ParseConfigFiles( + fileSystem.Path.GetDirectoryName(filePath), + fileSystem + ) + .Reverse() + .ToList(); + return editorConfigFiles.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..6c32e7f52 --- /dev/null +++ b/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs @@ -0,0 +1,82 @@ +namespace CSharpier.Cli.EditorConfig; + +internal class EditorConfigSections +{ + public required string DirectoryName { get; init; } + public required List
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..a8757e443 --- /dev/null +++ b/Src/CSharpier.Cli/EditorConfig/Section.cs @@ -0,0 +1,51 @@ +namespace CSharpier.Cli.EditorConfig; + +using GlobExpressions; + +public class Section +{ + private readonly Glob matcher; + public string Glob { get; } + public string? IndentStyle { get; } + public string? IndentSize { get; } + public string? TabWidth { get; } + public string? MaxLineLength { get; } + + public Section(string name, string directory, Dictionary properties) + { + this.Glob = FixGlob(name, directory); + this.matcher = new Glob(name); + this.IndentStyle = properties.TryGetValue("indent_style", out var indentStyle) + ? indentStyle + : null; + this.IndentSize = properties.TryGetValue("indent_size", out var indentSize) + ? indentSize + : null; + this.TabWidth = properties.TryGetValue("tab_width", out var tabWidth) ? tabWidth : null; + this.MaxLineLength = properties.TryGetValue("max_line_length", out var maxLineLenght) + ? maxLineLenght + : null; + } + + 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..e01e872f2 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,11 @@ 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)}"); + // TODO 1 figure out how to really hash the optionsProvider + return Hash($"{csharpierVersion}_${JsonSerializer.Serialize(optionsProvider)}"); } 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 65% rename from Src/CSharpier.Cli/ConfigurationFileOptions.cs rename to Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs index 793646b35..2b6242480 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,38 @@ ConfigurationFileOptions configurationFileOptions }; } - public static ConfigurationFileOptions FindForDirectory( - string baseDirectoryPath, + internal static List FindForDirectory2( + 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 +80,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..49bee06d1 --- /dev/null +++ b/Src/CSharpier.Cli/Options/OptionsProvider.cs @@ -0,0 +1,96 @@ +namespace CSharpier.Cli.Options; + +using System.IO.Abstractions; +using CSharpier.Cli.EditorConfig; +using Microsoft.Extensions.Logging; +using PrinterOptions = CSharpier.PrinterOptions; + +internal class OptionsProvider +{ + private readonly List configs; + private readonly List csharpierConfigs; + private readonly IgnoreFile ignoreFile; + private readonly PrinterOptions? specifiedPrinterOptions; + private readonly IFileSystem fileSystem; + + private OptionsProvider( + List configs, + List csharpierConfigs, + IgnoreFile ignoreFile, + PrinterOptions? specifiedPrinterOptions, + IFileSystem fileSystem + ) + { + this.configs = configs; + 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.FindForDirectory2(directoryName, fileSystem, logger) + : Array.Empty().ToList(); + + var editorConfigSections = EditorConfigParser.GetAllForDirectory(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.configs.FirstOrDefault( + o => o.DirectoryName.StartsWith(directoryName) + ); + var resolvedCSharpierConfig = this.csharpierConfigs.FirstOrDefault( + o => o.DirectoryName.StartsWith(directoryName) + ); + + if (resolvedEditorConfig is null && resolvedCSharpierConfig is null) + { + return new PrinterOptions(); + } + + if ( + (resolvedCSharpierConfig?.DirectoryName.Length ?? int.MaxValue) + < (resolvedEditorConfig?.DirectoryName.Length ?? int.MaxValue) + ) + { + return ConfigurationFileOptions.ConvertToPrinterOptions( + resolvedCSharpierConfig!.CSharpierConfig + ); + } + + return resolvedEditorConfig!.ConvertToPrinterOptions(filePath); + } + + public bool IsIgnored(string actualFilePath) + { + return this.ignoreFile.IsIgnored(actualFilePath); + } +} 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/OptionsProviderTests.cs b/Src/CSharpier.Tests/OptionsProviderTests.cs new file mode 100644 index 000000000..d64a74986 --- /dev/null +++ b/Src/CSharpier.Tests/OptionsProviderTests.cs @@ -0,0 +1,391 @@ +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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + + ShouldHaveDefaultOptions(result); + } + + [Test] + public async Task Should_Return_Default_Options_With_No_File() + { + var context = new TestContext(); + var optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test/subfolder"); + var result = optionsProvider.GetPrinterOptionsFor("c:/test/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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("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/sub1/.editorConfig", + @" + [*] + indent_size = 1 + " + ); + + context.WhenAFileExists( + "c:/test/.editorConfig", + @" + [*] + indent_size = 2 + max_line_length = 10 + " + ); + + var optionsProvider = await context.CreateOptionsProvider("c:/test/sub1"); + var result = optionsProvider.GetPrinterOptionsFor("c:/test/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/sub1/.editorConfig", + @" + [*] + indent_size = unset + " + ); + + context.WhenAFileExists( + "c:/test/.editorConfig", + @" + [*] + indent_size = 2 + " + ); + + var optionsProvider = await context.CreateOptionsProvider("c:/test/sub1"); + var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + result.TabWidth.Should().Be(4); + } + + // TODO 1 fix this + // TODO 1 write some tests that check for csharpier vs editorconfig + [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 optionsProvider = await context.CreateOptionsProvider("c:/test"); + var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + result.TabWidth.Should().Be(2); + } + + 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) + { + this.fileSystem.AddFile(path, new MockFileData(contents)); + } + + public Task CreateOptionsProvider(string directoryName) + { + this.fileSystem.AddDirectory(directoryName); + return OptionsProvider.Create( + directoryName, + null, + this.fileSystem, + NullLogger.Instance, + CancellationToken.None + ); + } + } +} 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; From e942cbfbd060851ca9c9dbb422cbfa732f3fed57 Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Fri, 14 Jul 2023 15:05:24 -0500 Subject: [PATCH 02/10] Some self review --- Directory.Packages.props | 1 - Src/CSharpier.Cli/CSharpier.Cli.csproj | 1 + Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs | 4 +++- Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs | 3 ++- Src/CSharpier.Cli/Options/OptionsProvider.cs | 7 +++++-- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 73989fb3a..b39fe49f4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,6 @@ - diff --git a/Src/CSharpier.Cli/CSharpier.Cli.csproj b/Src/CSharpier.Cli/CSharpier.Cli.csproj index 675066cce..570f9cdc6 100644 --- a/Src/CSharpier.Cli/CSharpier.Cli.csproj +++ b/Src/CSharpier.Cli/CSharpier.Cli.csproj @@ -8,6 +8,7 @@ true ../../Nuget/csharpier.snk True + 002400000480000094000000060200000024000052534131000400000100010049d266ea1aeae09c0abfce28b8728314d4e4807126ee8bc56155a7ddc765997ed3522908b469ae133fc49ef0bfa957df36082c1c2e0ec8cdc05a4ca4dbd4e1bea6c17fc1008555e15af13a8fc871a04ffc38f5e60e6203bfaf01d16a2a283b90572ade79135801c1675bf38b7a5a60ec8353069796eb53a26ffdddc9ee1273be diff --git a/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs b/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs index 8cab97e2d..66deeb2bf 100644 --- a/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs +++ b/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs @@ -4,11 +4,13 @@ namespace CSharpier.Cli.EditorConfig; internal static class EditorConfigParser { - public static List GetAllForDirectory( + /// Finds all configs above the given directory as well as within the subtree of this directory + public static List FindForDirectoryName( string directoryName, IFileSystem fileSystem ) { + // TODO 1 this may not actually find things above the current directory if (directoryName is "") { return new List(); diff --git a/Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs b/Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs index 2b6242480..012b477a5 100644 --- a/Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs +++ b/Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs @@ -38,7 +38,8 @@ ConfigurationFileOptions configurationFileOptions }; } - internal static List FindForDirectory2( + /// 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 diff --git a/Src/CSharpier.Cli/Options/OptionsProvider.cs b/Src/CSharpier.Cli/Options/OptionsProvider.cs index 49bee06d1..e591ea685 100644 --- a/Src/CSharpier.Cli/Options/OptionsProvider.cs +++ b/Src/CSharpier.Cli/Options/OptionsProvider.cs @@ -41,10 +41,13 @@ CancellationToken cancellationToken : null; var csharpierConfigs = configPath is null - ? ConfigurationFileOptions.FindForDirectory2(directoryName, fileSystem, logger) + ? ConfigurationFileOptions.FindForDirectoryName(directoryName, fileSystem, logger) : Array.Empty().ToList(); - var editorConfigSections = EditorConfigParser.GetAllForDirectory(directoryName, fileSystem); + var editorConfigSections = EditorConfigParser.FindForDirectoryName( + directoryName, + fileSystem + ); var ignoreFile = await IgnoreFile.Create(directoryName, fileSystem, cancellationToken); return new OptionsProvider( From cb6cb931ce092b0054ec018c08ce53242838c5cc Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Sat, 15 Jul 2023 10:23:14 -0500 Subject: [PATCH 03/10] Fixing some issues. Switching to strong name signed glob --- Directory.Packages.props | 2 +- Src/CSharpier.Cli/CSharpier.Cli.csproj | 3 +- Src/CSharpier.Cli/CommandLineFormatter.cs | 2 +- .../EditorConfig/EditorConfigParser.cs | 26 ++- Src/CSharpier.Cli/EditorConfig/Section.cs | 8 +- Src/CSharpier.Cli/Options/OptionsProvider.cs | 8 +- Src/CSharpier.Tests/OptionsProviderTests.cs | 166 +++++++++++++----- 7 files changed, 150 insertions(+), 65 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b39fe49f4..8e9be8a8b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + diff --git a/Src/CSharpier.Cli/CSharpier.Cli.csproj b/Src/CSharpier.Cli/CSharpier.Cli.csproj index 570f9cdc6..c4a982fa5 100644 --- a/Src/CSharpier.Cli/CSharpier.Cli.csproj +++ b/Src/CSharpier.Cli/CSharpier.Cli.csproj @@ -8,11 +8,10 @@ true ../../Nuget/csharpier.snk True - 002400000480000094000000060200000024000052534131000400000100010049d266ea1aeae09c0abfce28b8728314d4e4807126ee8bc56155a7ddc765997ed3522908b469ae133fc49ef0bfa957df36082c1c2e0ec8cdc05a4ca4dbd4e1bea6c17fc1008555e15af13a8fc871a04ffc38f5e60e6203bfaf01d16a2a283b90572ade79135801c1675bf38b7a5a60ec8353069796eb53a26ffdddc9ee1273be - + diff --git a/Src/CSharpier.Cli/CommandLineFormatter.cs b/Src/CSharpier.Cli/CommandLineFormatter.cs index 10c554330..cfe5c9fcb 100644 --- a/Src/CSharpier.Cli/CommandLineFormatter.cs +++ b/Src/CSharpier.Cli/CommandLineFormatter.cs @@ -137,7 +137,7 @@ CancellationToken cancellationToken : isDirectory ? directoryOrFilePath : string.Empty; - // TODO 1 if single file don't look in subtree? just look for the one for this file + var optionsProvider = await OptionsProvider.Create( directoryName, commandLineOptions.ConfigPath, diff --git a/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs b/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs index 66deeb2bf..73f76ac81 100644 --- a/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs +++ b/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs @@ -10,15 +10,31 @@ public static List FindForDirectoryName( IFileSystem fileSystem ) { - // TODO 1 this may not actually find things above the current directory if (directoryName is "") { return new List(); } - var editorConfigFiles = fileSystem.DirectoryInfo - .FromDirectoryName(directoryName) - .EnumerateFiles(".editorconfig", SearchOption.AllDirectories); + 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( @@ -29,7 +45,7 @@ IFileSystem fileSystem SectionsIncludingParentFiles = FindSections(o.FullName, fileSystem) } ) - .OrderBy(o => o.DirectoryName) + .OrderByDescending(o => o.DirectoryName.Length) .ToList(); } diff --git a/Src/CSharpier.Cli/EditorConfig/Section.cs b/Src/CSharpier.Cli/EditorConfig/Section.cs index a8757e443..d577dbdeb 100644 --- a/Src/CSharpier.Cli/EditorConfig/Section.cs +++ b/Src/CSharpier.Cli/EditorConfig/Section.cs @@ -1,11 +1,11 @@ namespace CSharpier.Cli.EditorConfig; -using GlobExpressions; +using DotNet.Globbing; public class Section { private readonly Glob matcher; - public string Glob { get; } + public string Pattern { get; } public string? IndentStyle { get; } public string? IndentSize { get; } public string? TabWidth { get; } @@ -13,8 +13,8 @@ public class Section public Section(string name, string directory, Dictionary properties) { - this.Glob = FixGlob(name, directory); - this.matcher = new Glob(name); + this.Pattern = FixGlob(name, directory); + this.matcher = Glob.Parse(this.Pattern); this.IndentStyle = properties.TryGetValue("indent_style", out var indentStyle) ? indentStyle : null; diff --git a/Src/CSharpier.Cli/Options/OptionsProvider.cs b/Src/CSharpier.Cli/Options/OptionsProvider.cs index e591ea685..5e587b628 100644 --- a/Src/CSharpier.Cli/Options/OptionsProvider.cs +++ b/Src/CSharpier.Cli/Options/OptionsProvider.cs @@ -68,10 +68,10 @@ public PrinterOptions GetPrinterOptionsFor(string filePath) var directoryName = this.fileSystem.Path.GetDirectoryName(filePath); var resolvedEditorConfig = this.configs.FirstOrDefault( - o => o.DirectoryName.StartsWith(directoryName) + o => directoryName.StartsWith(o.DirectoryName) ); var resolvedCSharpierConfig = this.csharpierConfigs.FirstOrDefault( - o => o.DirectoryName.StartsWith(directoryName) + o => directoryName.StartsWith(o.DirectoryName) ); if (resolvedEditorConfig is null && resolvedCSharpierConfig is null) @@ -80,8 +80,8 @@ public PrinterOptions GetPrinterOptionsFor(string filePath) } if ( - (resolvedCSharpierConfig?.DirectoryName.Length ?? int.MaxValue) - < (resolvedEditorConfig?.DirectoryName.Length ?? int.MaxValue) + (resolvedCSharpierConfig?.DirectoryName.Length ?? int.MinValue) + >= (resolvedEditorConfig?.DirectoryName.Length ?? int.MinValue) ) { return ConfigurationFileOptions.ConvertToPrinterOptions( diff --git a/Src/CSharpier.Tests/OptionsProviderTests.cs b/Src/CSharpier.Tests/OptionsProviderTests.cs index d64a74986..5b7fbd58f 100644 --- a/Src/CSharpier.Tests/OptionsProviderTests.cs +++ b/Src/CSharpier.Tests/OptionsProviderTests.cs @@ -16,8 +16,7 @@ public async Task Should_Return_Default_Options_With_Empty_Json() var context = new TestContext(); context.WhenAFileExists("c:/test/.csharpierrc", "{}"); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); ShouldHaveDefaultOptions(result); } @@ -26,8 +25,7 @@ public async Task Should_Return_Default_Options_With_Empty_Json() public async Task Should_Return_Default_Options_With_No_File() { var context = new TestContext(); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); ShouldHaveDefaultOptions(result); } @@ -39,8 +37,7 @@ public async Task Should_Return_Default_Options_With_Empty_File(string fileName) { var context = new TestContext(); context.WhenAFileExists($"c:/test/{fileName}", string.Empty); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); ShouldHaveDefaultOptions(result); } @@ -57,8 +54,7 @@ public async Task Should_Return_Json_Extension_Options() }" ); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.Width.Should().Be(10); } @@ -78,8 +74,7 @@ public async Task Should_Return_Yaml_Extension_Options(string extension) " ); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.Width.Should().Be(10); } @@ -91,8 +86,7 @@ public async Task Should_Read_ExtensionLess_File(string contents) var context = new TestContext(); context.WhenAFileExists($"c:/test/.csharpierrc", contents); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.Width.Should().Be(10); } @@ -110,8 +104,10 @@ string contents var context = new TestContext(); context.WhenAFileExists($"c:/test/.csharpierrc{extension}", contents); - var optionsProvider = await context.CreateOptionsProvider("c:/test/subfolder"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test/subfolder", + "c:/test/subfolder/test.cs" + ); result.Width.Should().Be(10); } @@ -125,8 +121,7 @@ public async Task Should_Prefer_No_Extension() context.WhenAFileExists("c:/test/.csharpierrc.json", "{ \"printWidth\": 2 }"); context.WhenAFileExists("c:/test/.csharpierrc.yaml", "printWidth: 3"); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.Width.Should().Be(1); } @@ -137,8 +132,7 @@ public async Task Should_Return_PrintWidth_With_Json() var context = new TestContext(); context.WhenAFileExists("c:/test/.csharpierrc", "{ \"printWidth\": 10 }"); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.Width.Should().Be(10); } @@ -149,8 +143,7 @@ public async Task Should_Return_TabWidth_With_Json() var context = new TestContext(); context.WhenAFileExists("c:/test/.csharpierrc", "{ \"tabWidth\": 10 }"); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.TabWidth.Should().Be(10); } @@ -161,8 +154,7 @@ public async Task Should_Return_UseTabs_With_Json() var context = new TestContext(); context.WhenAFileExists("c:/test/.csharpierrc", "{ \"useTabs\": true }"); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.UseTabs.Should().BeTrue(); } @@ -173,8 +165,7 @@ public async Task Should_Return_PrintWidth_With_Yaml() var context = new TestContext(); context.WhenAFileExists("c:/test/.csharpierrc", "printWidth: 10"); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.Width.Should().Be(10); } @@ -185,8 +176,7 @@ public async Task Should_Return_TabWidth_With_Yaml() var context = new TestContext(); context.WhenAFileExists("c:/test/.csharpierrc", "tabWidth: 10"); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.TabWidth.Should().Be(10); } @@ -197,8 +187,7 @@ public async Task Should_Return_UseTabs_With_Yaml() var context = new TestContext(); context.WhenAFileExists("c:/test/.csharpierrc", "useTabs: true"); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.UseTabs.Should().BeTrue(); } @@ -217,8 +206,7 @@ public async Task Should_Support_EditorConfig_Basic() " ); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.UseTabs.Should().BeFalse(); result.TabWidth.Should().Be(2); @@ -239,8 +227,7 @@ public async Task Should_Support_EditorConfig_Tabs(string propertyName) " ); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.UseTabs.Should().BeTrue(); result.TabWidth.Should().Be(2); @@ -260,8 +247,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Tab_Width() " ); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.UseTabs.Should().BeTrue(); result.TabWidth.Should().Be(3); @@ -280,8 +266,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Indent_Size_Tab() " ); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.TabWidth.Should().Be(3); } @@ -291,7 +276,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files() { var context = new TestContext(); context.WhenAFileExists( - "c:/test/sub1/.editorConfig", + "c:/test/subfolder/.editorConfig", @" [*] indent_size = 1 @@ -307,8 +292,10 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files() " ); - var optionsProvider = await context.CreateOptionsProvider("c:/test/sub1"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test/subfolder", + "c:/test/subfolder/test.cs" + ); result.TabWidth.Should().Be(1); result.Width.Should().Be(10); } @@ -318,7 +305,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files_And_Unset { var context = new TestContext(); context.WhenAFileExists( - "c:/test/sub1/.editorConfig", + "c:/test/subfolder/.editorConfig", @" [*] indent_size = unset @@ -333,13 +320,13 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files_And_Unset " ); - var optionsProvider = await context.CreateOptionsProvider("c:/test/sub1"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + var result = await context.CreateProviderAndGetOptionsFor( + "c:/test/subfolder", + "c:/test/subfolder/test.cs" + ); result.TabWidth.Should().Be(4); } - // TODO 1 fix this - // TODO 1 write some tests that check for csharpier vs editorconfig [Test] public async Task Should_Support_EditorConfig_Tabs_With_Globs() { @@ -355,11 +342,89 @@ public async Task Should_Support_EditorConfig_Tabs_With_Globs() " ); - var optionsProvider = await context.CreateOptionsProvider("c:/test"); - var result = optionsProvider.GetPrinterOptionsFor("c:/test/test.cs"); + 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); @@ -376,16 +441,21 @@ public void WhenAFileExists(string path, string contents) this.fileSystem.AddFile(path, new MockFileData(contents)); } - public Task CreateOptionsProvider(string directoryName) + public async Task CreateProviderAndGetOptionsFor( + string directoryName, + string filePath + ) { this.fileSystem.AddDirectory(directoryName); - return OptionsProvider.Create( + var provider = await OptionsProvider.Create( directoryName, null, this.fileSystem, NullLogger.Instance, CancellationToken.None ); + + return provider.GetPrinterOptionsFor(filePath); } } } From cfa09168aaaa04366c4cc4590e811e53c65395d9 Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Sat, 15 Jul 2023 10:41:29 -0500 Subject: [PATCH 04/10] Finishing up code changes --- Src/CSharpier.Cli/CommandLineFormatter.cs | 22 ++++++++++++-------- Src/CSharpier.Cli/FormattingCache.cs | 3 +-- Src/CSharpier.Cli/Options/OptionsProvider.cs | 21 +++++++++++++++---- Src/CSharpier.Cli/Program.cs | 3 --- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/Src/CSharpier.Cli/CommandLineFormatter.cs b/Src/CSharpier.Cli/CommandLineFormatter.cs index cfe5c9fcb..651f50803 100644 --- a/Src/CSharpier.Cli/CommandLineFormatter.cs +++ b/Src/CSharpier.Cli/CommandLineFormatter.cs @@ -130,13 +130,25 @@ CancellationToken cancellationToken for (var x = 0; x < commandLineOptions.DirectoryOrFilePaths.Length; x++) { var directoryOrFilePath = commandLineOptions.DirectoryOrFilePaths[x]; + var directoryOrFile = directoryOrFilePath.Replace("\\", "/"); var isFile = fileSystem.File.Exists(directoryOrFilePath); var isDirectory = fileSystem.Directory.Exists(directoryOrFilePath); + + if (!isFile && !isDirectory) + { + console.WriteErrorLine( + "There was no file or directory found at " + directoryOrFile + ); + return 1; + } + var directoryName = isFile ? fileSystem.Path.GetDirectoryName(directoryOrFilePath) : isDirectory ? directoryOrFilePath - : string.Empty; + : throw new InvalidOperationException( + "This should never happen because if there is no file or directory we shouldn't get this far" + ); var optionsProvider = await OptionsProvider.Create( directoryName, @@ -146,7 +158,6 @@ CancellationToken cancellationToken cancellationToken ); - var directoryOrFile = directoryOrFilePath.Replace("\\", "/"); var originalDirectoryOrFile = commandLineOptions.OriginalDirectoryOrFilePaths[ x ].Replace("\\", "/"); @@ -228,13 +239,6 @@ await FormatPhysicalFile( } } } - else - { - console.WriteErrorLine( - "There was no file or directory found at " + directoryOrFile - ); - return 1; - } await formattingCache.ResolveAsync(cancellationToken); } diff --git a/Src/CSharpier.Cli/FormattingCache.cs b/Src/CSharpier.Cli/FormattingCache.cs index e01e872f2..d8418f34c 100644 --- a/Src/CSharpier.Cli/FormattingCache.cs +++ b/Src/CSharpier.Cli/FormattingCache.cs @@ -134,8 +134,7 @@ public void CacheResult(string code, FileToFormatInfo fileToFormatInfo) private static string GetOptionsHash(OptionsProvider optionsProvider) { var csharpierVersion = typeof(FormattingCache).Assembly.GetName().Version; - // TODO 1 figure out how to really hash the optionsProvider - return Hash($"{csharpierVersion}_${JsonSerializer.Serialize(optionsProvider)}"); + return Hash($"{csharpierVersion}_${optionsProvider.Serialize()}"); } private static string Hash(string input) diff --git a/Src/CSharpier.Cli/Options/OptionsProvider.cs b/Src/CSharpier.Cli/Options/OptionsProvider.cs index 5e587b628..699ef24ba 100644 --- a/Src/CSharpier.Cli/Options/OptionsProvider.cs +++ b/Src/CSharpier.Cli/Options/OptionsProvider.cs @@ -1,27 +1,28 @@ 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 configs; + private readonly List editorConfigs; private readonly List csharpierConfigs; private readonly IgnoreFile ignoreFile; private readonly PrinterOptions? specifiedPrinterOptions; private readonly IFileSystem fileSystem; private OptionsProvider( - List configs, + List editorConfigs, List csharpierConfigs, IgnoreFile ignoreFile, PrinterOptions? specifiedPrinterOptions, IFileSystem fileSystem ) { - this.configs = configs; + this.editorConfigs = editorConfigs; this.csharpierConfigs = csharpierConfigs; this.ignoreFile = ignoreFile; this.specifiedPrinterOptions = specifiedPrinterOptions; @@ -67,7 +68,7 @@ public PrinterOptions GetPrinterOptionsFor(string filePath) } var directoryName = this.fileSystem.Path.GetDirectoryName(filePath); - var resolvedEditorConfig = this.configs.FirstOrDefault( + var resolvedEditorConfig = this.editorConfigs.FirstOrDefault( o => directoryName.StartsWith(o.DirectoryName) ); var resolvedCSharpierConfig = this.csharpierConfigs.FirstOrDefault( @@ -96,4 +97,16 @@ 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; } From 06b885caf9f7cd8737b122575b3515ef8bd2fe55 Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Sat, 15 Jul 2023 10:47:04 -0500 Subject: [PATCH 05/10] Some self review --- Src/CSharpier.Cli/CommandLineFormatter.cs | 6 +----- Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs | 4 ++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Src/CSharpier.Cli/CommandLineFormatter.cs b/Src/CSharpier.Cli/CommandLineFormatter.cs index 651f50803..4d0aef50a 100644 --- a/Src/CSharpier.Cli/CommandLineFormatter.cs +++ b/Src/CSharpier.Cli/CommandLineFormatter.cs @@ -144,11 +144,7 @@ CancellationToken cancellationToken var directoryName = isFile ? fileSystem.Path.GetDirectoryName(directoryOrFilePath) - : isDirectory - ? directoryOrFilePath - : throw new InvalidOperationException( - "This should never happen because if there is no file or directory we shouldn't get this far" - ); + : directoryOrFilePath; var optionsProvider = await OptionsProvider.Create( directoryName, diff --git a/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs b/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs index 6c32e7f52..ceffff9ee 100644 --- a/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs +++ b/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs @@ -1,5 +1,9 @@ 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; } From b8d3ad2712b0daed94cc273a051c65e511d36098 Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Tue, 25 Jul 2023 18:55:08 -0500 Subject: [PATCH 06/10] Fixing tests when run on linux --- Scripts/RunLinuxTests.ps1 | 2 +- Src/CSharpier.Tests/OptionsProviderTests.cs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) 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.Tests/OptionsProviderTests.cs b/Src/CSharpier.Tests/OptionsProviderTests.cs index 5b7fbd58f..dfc3e6443 100644 --- a/Src/CSharpier.Tests/OptionsProviderTests.cs +++ b/Src/CSharpier.Tests/OptionsProviderTests.cs @@ -438,6 +438,11 @@ private class TestContext public void WhenAFileExists(string path, string contents) { + if (!OperatingSystem.IsWindows()) + { + path = path.Replace("c:", string.Empty); + } + this.fileSystem.AddFile(path, new MockFileData(contents)); } @@ -446,6 +451,12 @@ public async Task CreateProviderAndGetOptionsFor( 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, From a0c5c5d8d6cd39b7d4bb07f55ca56d1580eb9cef Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Tue, 25 Jul 2023 19:48:54 -0500 Subject: [PATCH 07/10] And more issues fixed --- .gitignore | 1 + Src/CSharpier.Tests/DocPrinterTests.cs | 5 + Src/CSharpier.Tests/DocSerializerTests.cs | 106 +++++++++--------- Src/CSharpier.Tests/OptionsProviderTests.cs | 26 ++--- .../SyntaxNodeComparerTests.cs | 13 +-- 5 files changed, 77 insertions(+), 74 deletions(-) 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/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 index dfc3e6443..7ac56291f 100644 --- a/Src/CSharpier.Tests/OptionsProviderTests.cs +++ b/Src/CSharpier.Tests/OptionsProviderTests.cs @@ -197,7 +197,7 @@ public async Task Should_Support_EditorConfig_Basic() { var context = new TestContext(); context.WhenAFileExists( - "c:/test/.editorConfig", + "c:/test/.editorconfig", @" [*] indent_style = space @@ -219,7 +219,7 @@ public async Task Should_Support_EditorConfig_Tabs(string propertyName) { var context = new TestContext(); context.WhenAFileExists( - "c:/test/.editorConfig", + "c:/test/.editorconfig", $@" [*] indent_style = tab @@ -238,7 +238,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Tab_Width() { var context = new TestContext(); context.WhenAFileExists( - "c:/test/.editorConfig", + "c:/test/.editorconfig", @" [*] indent_style = tab @@ -258,7 +258,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Indent_Size_Tab() { var context = new TestContext(); context.WhenAFileExists( - "c:/test/.editorConfig", + "c:/test/.editorconfig", @" [*] indent_size = tab @@ -276,7 +276,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files() { var context = new TestContext(); context.WhenAFileExists( - "c:/test/subfolder/.editorConfig", + "c:/test/subfolder/.editorconfig", @" [*] indent_size = 1 @@ -284,7 +284,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files() ); context.WhenAFileExists( - "c:/test/.editorConfig", + "c:/test/.editorconfig", @" [*] indent_size = 2 @@ -305,7 +305,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files_And_Unset { var context = new TestContext(); context.WhenAFileExists( - "c:/test/subfolder/.editorConfig", + "c:/test/subfolder/.editorconfig", @" [*] indent_size = unset @@ -313,7 +313,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files_And_Unset ); context.WhenAFileExists( - "c:/test/.editorConfig", + "c:/test/.editorconfig", @" [*] indent_size = 2 @@ -332,7 +332,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Globs() { var context = new TestContext(); context.WhenAFileExists( - "c:/test/.editorConfig", + "c:/test/.editorconfig", @" [*] indent_size = 1 @@ -351,7 +351,7 @@ public async Task Should_Find_EditorConfig_In_Parent_Directory() { var context = new TestContext(); context.WhenAFileExists( - "c:/test/.editorConfig", + "c:/test/.editorconfig", @" [*.cs] indent_size = 2 @@ -370,7 +370,7 @@ public async Task Should_Prefer_CSharpierrc_In_SameFolder() { var context = new TestContext(); context.WhenAFileExists( - "c:/test/.editorConfig", + "c:/test/.editorconfig", @" [*.cs] indent_size = 2 @@ -390,7 +390,7 @@ public async Task Should_Prefer_Closer_EditorConfig() { var context = new TestContext(); context.WhenAFileExists( - "c:/test/subfolder/.editorConfig", + "c:/test/subfolder/.editorconfig", @" [*.cs] indent_size = 2 @@ -410,7 +410,7 @@ public async Task Should_Prefer_Closer_CSharpierrc() { var context = new TestContext(); context.WhenAFileExists( - "c:/test/.editorConfig", + "c:/test/.editorconfig", @" [*.cs] indent_size = 2 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] From b40eef51b8a62a9f1eab5a33719eac60b70d851a Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Tue, 25 Jul 2023 21:05:11 -0500 Subject: [PATCH 08/10] Fixing last linux issue --- Src/CSharpier.Cli/CommandLineFormatter.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Src/CSharpier.Cli/CommandLineFormatter.cs b/Src/CSharpier.Cli/CommandLineFormatter.cs index 4d0aef50a..b8d509c07 100644 --- a/Src/CSharpier.Cli/CommandLineFormatter.cs +++ b/Src/CSharpier.Cli/CommandLineFormatter.cs @@ -129,15 +129,14 @@ CancellationToken cancellationToken for (var x = 0; x < commandLineOptions.DirectoryOrFilePaths.Length; x++) { - var directoryOrFilePath = commandLineOptions.DirectoryOrFilePaths[x]; - var directoryOrFile = directoryOrFilePath.Replace("\\", "/"); + var directoryOrFilePath = commandLineOptions.DirectoryOrFilePaths[x].Replace("\\", "/"); var isFile = fileSystem.File.Exists(directoryOrFilePath); var isDirectory = fileSystem.Directory.Exists(directoryOrFilePath); if (!isFile && !isDirectory) { console.WriteErrorLine( - "There was no file or directory found at " + directoryOrFile + "There was no file or directory found at " + directoryOrFilePath ); return 1; } @@ -200,26 +199,30 @@ await FormatPhysicalFile( if (isFile) { - await FormatFile(directoryOrFile, originalDirectoryOrFile); + await FormatFile(directoryOrFilePath, originalDirectoryOrFile); } 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(); From e6158407974471eb38a96b71fe53a13a875a3209 Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Fri, 25 Aug 2023 14:36:49 -0500 Subject: [PATCH 09/10] Use ini-parser for editorconfig. Clean up a couple things --- Directory.Packages.props | 1 + Src/CSharpier.Cli/CSharpier.Cli.csproj | 1 + Src/CSharpier.Cli/EditorConfig/ConfigFile.cs | 2 +- .../EditorConfig/ConfigFileParser.cs | 74 +++---------------- .../EditorConfig/EditorConfigParser.cs | 7 +- .../EditorConfig/EditorConfigSections.cs | 2 +- Src/CSharpier.Cli/EditorConfig/Section.cs | 19 ++--- Src/CSharpier.Tests/OptionsProviderTests.cs | 29 ++++++++ 8 files changed, 52 insertions(+), 83 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8e9be8a8b..90c3ed93d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,7 @@ + diff --git a/Src/CSharpier.Cli/CSharpier.Cli.csproj b/Src/CSharpier.Cli/CSharpier.Cli.csproj index c4a982fa5..485396140 100644 --- a/Src/CSharpier.Cli/CSharpier.Cli.csproj +++ b/Src/CSharpier.Cli/CSharpier.Cli.csproj @@ -13,6 +13,7 @@ + diff --git a/Src/CSharpier.Cli/EditorConfig/ConfigFile.cs b/Src/CSharpier.Cli/EditorConfig/ConfigFile.cs index 5ec84f19d..847d5bc6c 100644 --- a/Src/CSharpier.Cli/EditorConfig/ConfigFile.cs +++ b/Src/CSharpier.Cli/EditorConfig/ConfigFile.cs @@ -2,6 +2,6 @@ internal class ConfigFile { - public required List
Sections { get; init; } + 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 index b12c7604b..d1462a40a 100644 --- a/Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs +++ b/Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs @@ -1,78 +1,24 @@ using System.IO.Abstractions; -using System.Text.RegularExpressions; +using IniParser; namespace CSharpier.Cli.EditorConfig; internal static class ConfigFileParser { - private static readonly Regex SectionRegex = new(@"^\s*\[(([^#;]|\\#|\\;)+)\]\s*([#;].*)?$"); - private static readonly Regex CommentRegex = new(@"^\s*[#;]"); - private static readonly Regex PropertyRegex = - new(@"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*([#;].*)?$"); - - private static readonly HashSet KnownProperties = - new( - new[] { "indent_style", "indent_size", "tab_width", "max_line_length", "root", }, - StringComparer.OrdinalIgnoreCase - ); - public static ConfigFile Parse(string filePath, IFileSystem fileSystem) { - var lines = fileSystem.File.ReadLines(filePath); - - var isRoot = false; - var propertiesBySection = new Dictionary>(); - var sectionName = string.Empty; - foreach (var line in lines) - { - if (string.IsNullOrWhiteSpace(line) || CommentRegex.IsMatch(line)) - { - continue; - } - - var propertyMatches = PropertyRegex.Matches(line); - if (propertyMatches.Count > 0) - { - var key = propertyMatches[0].Groups[1].Value.Trim().ToLowerInvariant(); - var value = propertyMatches[0].Groups[2].Value.Trim(); + var parser = new FileIniDataParser(); - if (KnownProperties.Contains(key)) - { - value = value.ToLowerInvariant(); - } - - if (sectionName is "") - { - if (key == "root" && bool.TryParse(value, out var parsedValue)) - { - isRoot = parsedValue; - } - } - else - { - propertiesBySection[sectionName][key] = value; - } - } - else - { - var sectionMatches = SectionRegex.Matches(line); - if (sectionMatches.Count <= 0) - { - continue; - } - - sectionName = sectionMatches[0].Groups[1].Value; - propertiesBySection[sectionName] = new Dictionary(); - } - } + using var stream = fileSystem.File.OpenRead(filePath); + using var streamReader = new StreamReader(stream); + var configData = parser.ReadData(streamReader); var directory = fileSystem.Path.GetDirectoryName(filePath); - return new ConfigFile + var sections = new List
(); + foreach (var section in configData.Sections) { - IsRoot = isRoot, - Sections = propertiesBySection - .Select(o => new Section(o.Key, directory, o.Value)) - .ToList() - }; + 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 index 73f76ac81..b609e37ac 100644 --- a/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs +++ b/Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs @@ -51,13 +51,10 @@ IFileSystem fileSystem private static List
FindSections(string filePath, IFileSystem fileSystem) { - var editorConfigFiles = ParseConfigFiles( - fileSystem.Path.GetDirectoryName(filePath), - fileSystem - ) + return ParseConfigFiles(fileSystem.Path.GetDirectoryName(filePath), fileSystem) .Reverse() + .SelectMany(configFile => configFile.Sections) .ToList(); - return editorConfigFiles.SelectMany(configFile => configFile.Sections).ToList(); } private static IEnumerable ParseConfigFiles( diff --git a/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs b/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs index ceffff9ee..5fe3a2bb0 100644 --- a/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs +++ b/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs @@ -7,7 +7,7 @@ namespace CSharpier.Cli.EditorConfig; internal class EditorConfigSections { public required string DirectoryName { get; init; } - public required List
SectionsIncludingParentFiles { get; init; } + public required IReadOnlyCollection
SectionsIncludingParentFiles { get; init; } public PrinterOptions ConvertToPrinterOptions(string filePath) { diff --git a/Src/CSharpier.Cli/EditorConfig/Section.cs b/Src/CSharpier.Cli/EditorConfig/Section.cs index d577dbdeb..bdf4641db 100644 --- a/Src/CSharpier.Cli/EditorConfig/Section.cs +++ b/Src/CSharpier.Cli/EditorConfig/Section.cs @@ -1,6 +1,7 @@ namespace CSharpier.Cli.EditorConfig; using DotNet.Globbing; +using IniParser.Model; public class Section { @@ -11,20 +12,14 @@ public class Section public string? TabWidth { get; } public string? MaxLineLength { get; } - public Section(string name, string directory, Dictionary properties) + public Section(SectionData section, string directory) { - this.Pattern = FixGlob(name, directory); + this.Pattern = FixGlob(section.SectionName, directory); this.matcher = Glob.Parse(this.Pattern); - this.IndentStyle = properties.TryGetValue("indent_style", out var indentStyle) - ? indentStyle - : null; - this.IndentSize = properties.TryGetValue("indent_size", out var indentSize) - ? indentSize - : null; - this.TabWidth = properties.TryGetValue("tab_width", out var tabWidth) ? tabWidth : null; - this.MaxLineLength = properties.TryGetValue("max_line_length", out var maxLineLenght) - ? maxLineLenght - : null; + 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) diff --git a/Src/CSharpier.Tests/OptionsProviderTests.cs b/Src/CSharpier.Tests/OptionsProviderTests.cs index 7ac56291f..3d2c64c7d 100644 --- a/Src/CSharpier.Tests/OptionsProviderTests.cs +++ b/Src/CSharpier.Tests/OptionsProviderTests.cs @@ -327,6 +327,35 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files_And_Unset 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() { From 2ae81a969fbd3728d8a00c59ea820f8cdd4598fe Mon Sep 17 00:00:00 2001 From: Bela VanderVoort Date: Fri, 25 Aug 2023 14:38:14 -0500 Subject: [PATCH 10/10] get these sorted --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 094825f3f..37383771f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,8 +7,8 @@ - +