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