Skip to content

Commit

Permalink
Basics of new override ability by extension, which will also allow ot…
Browse files Browse the repository at this point in the history
…her file extensions to be formatted. (#1251)

closes #1220

I mostly went with how prettier works for overrides. Using a file glob,
a user can specify options to be used for a file. One of those options
is which formatter to use. Right now this means non-standard files can
be formatted with the csharp formatter. Some day the xml formatting PR
will finally get some attention.

---------

Co-authored-by: Lasath Fernando <devel@lasath.org>
  • Loading branch information
belav and shocklateboy92 authored Aug 16, 2024
1 parent 0df263d commit 28c9bc4
Show file tree
Hide file tree
Showing 22 changed files with 516 additions and 244 deletions.
25 changes: 25 additions & 0 deletions Src/CSharpier.Cli.Tests/CliTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ public async Task With_Check_Should_Write_Unformatted_File()
result.ExitCode.Should().Be(1);
}

// TODO overrides tests for piping files
[TestCase("\n")]
[TestCase("\r\n")]
public async Task Should_Format_Multiple_Piped_Files(string lineEnding)
Expand Down Expand Up @@ -312,6 +313,30 @@ public async Task Should_Support_Config_With_Multiple_Piped_Files()
result.Output.TrimEnd('\u0003').Should().Be("var myVariable =\n someLongValue;\n");
}

[Test]
public async Task Should_Support_Override_Config_With_Multiple_Piped_Files()
{
const string fileContent = "var myVariable = someLongValue;";
var fileName = Path.Combine(testFileDirectory, "TooWide.cst");
await this.WriteFileAsync(
".csharpierrc",
"""
overrides:
- files: "*.cst"
formatter: "csharp"
printWidth: 10
"""
);

var result = await new CsharpierProcess()
.WithArguments("--pipe-multiple-files")
.WithPipedInput($"{fileName}{'\u0003'}{fileContent}{'\u0003'}")
.ExecuteAsync();

result.ErrorOutput.Should().BeEmpty();
result.Output.TrimEnd('\u0003').Should().Be("var myVariable =\n someLongValue;\n");
}

[Test]
public async Task Should_Not_Fail_On_Empty_File()
{
Expand Down
80 changes: 42 additions & 38 deletions Src/CSharpier.Cli/CommandLineFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,21 @@ CancellationToken cancellationToken
);

var printerOptions = optionsProvider.GetPrinterOptionsFor(filePath);
printerOptions.IncludeGenerated = commandLineOptions.IncludeGenerated;

await PerformFormattingSteps(
fileToFormatInfo,
new StdOutFormattedFileWriter(console),
commandLineFormatterResult,
fileIssueLogger,
printerOptions,
commandLineOptions,
FormattingCacheFactory.NullCache,
cancellationToken
);
if (printerOptions is not null)
{
printerOptions.IncludeGenerated = commandLineOptions.IncludeGenerated;

await PerformFormattingSteps(
fileToFormatInfo,
new StdOutFormattedFileWriter(console),
commandLineFormatterResult,
fileIssueLogger,
printerOptions,
commandLineOptions,
FormattingCacheFactory.NullCache,
cancellationToken
);
}
}
}
else
Expand Down Expand Up @@ -193,7 +196,11 @@ CancellationToken cancellationToken
}
}

async Task FormatFile(string actualFilePath, string originalFilePath)
async Task FormatFile(
string actualFilePath,
string originalFilePath,
bool warnForUnsupported = false
)
{
if (
(
Expand All @@ -206,25 +213,33 @@ async Task FormatFile(string actualFilePath, string originalFilePath)
}

var printerOptions = optionsProvider.GetPrinterOptionsFor(actualFilePath);
printerOptions.IncludeGenerated = commandLineOptions.IncludeGenerated;

await FormatPhysicalFile(
actualFilePath,
originalFilePath,
fileSystem,
logger,
commandLineFormatterResult,
writer,
commandLineOptions,
printerOptions,
formattingCache,
cancellationToken
);
if (printerOptions is not null)
{
printerOptions.IncludeGenerated = commandLineOptions.IncludeGenerated;
await FormatPhysicalFile(
actualFilePath,
originalFilePath,
fileSystem,
logger,
commandLineFormatterResult,
writer,
commandLineOptions,
printerOptions,
formattingCache,
cancellationToken
);
}
else if (warnForUnsupported)
{
var fileIssueLogger = new FileIssueLogger(originalFilePath, logger);
fileIssueLogger.WriteWarning("Is an unsupported file type.");
}
}

if (isFile)
{
await FormatFile(directoryOrFilePath, originalDirectoryOrFile);
await FormatFile(directoryOrFilePath, originalDirectoryOrFile, true);
}
else if (isDirectory)
{
Expand All @@ -246,7 +261,6 @@ await FormatPhysicalFile(
"*.*",
SearchOption.AllDirectories
)
.Where(o => o.EndsWithIgnoreCase(".cs") || o.EndsWithIgnoreCase(".csx"))
.Select(o =>
{
var normalizedPath = o.Replace("\\", "/");
Expand Down Expand Up @@ -296,16 +310,6 @@ CancellationToken cancellationToken

var fileIssueLogger = new FileIssueLogger(originalFilePath, logger);

if (
!actualFilePath.EndsWithIgnoreCase(".cs")
&& !actualFilePath.EndsWithIgnoreCase(".cst")
&& !actualFilePath.EndsWithIgnoreCase(".csx")
)
{
fileIssueLogger.WriteWarning("Is an unsupported file type.");
return;
}

logger.LogDebug(
commandLineOptions.Check
? $"Checking - {originalFilePath}"
Expand Down
15 changes: 11 additions & 4 deletions Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ internal class EditorConfigSections
public required string DirectoryName { get; init; }
public required IReadOnlyCollection<Section> SectionsIncludingParentFiles { get; init; }

public PrinterOptions ConvertToPrinterOptions(string filePath)
public PrinterOptions? ConvertToPrinterOptions(string filePath)
{
if (!(filePath.EndsWith(".cs") || filePath.EndsWith(".csx")))
{
return null;
}

var sections = this.SectionsIncludingParentFiles.Where(o => o.IsMatch(filePath)).ToList();
var resolvedConfiguration = new ResolvedConfiguration(sections);
var printerOptions = new PrinterOptions();

var printerOptions = new PrinterOptions { Formatter = "csharp" };

if (resolvedConfiguration.MaxLineLength is { } maxLineLength)
{
Expand All @@ -27,11 +33,12 @@ public PrinterOptions ConvertToPrinterOptions(string filePath)

if (printerOptions.UseTabs)
{
printerOptions.TabWidth = resolvedConfiguration.TabWidth ?? printerOptions.TabWidth;
printerOptions.IndentSize = resolvedConfiguration.TabWidth ?? printerOptions.IndentSize;
}
else
{
printerOptions.TabWidth = resolvedConfiguration.IndentSize ?? printerOptions.TabWidth;
printerOptions.IndentSize =
resolvedConfiguration.IndentSize ?? printerOptions.IndentSize;
}

if (resolvedConfiguration.EndOfLine is { } endOfLine)
Expand Down
36 changes: 36 additions & 0 deletions Src/CSharpier.Cli/EditorConfig/Globber.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace CSharpier.Cli.EditorConfig;

internal static class Globber
{
private static readonly GlobMatcherOptions globOptions =
new()
{
MatchBase = true,
Dot = true,
AllowWindowsPaths = true,
AllowSingleBraceSets = true,
};

public static GlobMatcher Create(string files, string directory)
{
var pattern = FixGlob(files, directory);
return GlobMatcher.Create(pattern, globOptions);
}

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;
}
}
29 changes: 1 addition & 28 deletions Src/CSharpier.Cli/EditorConfig/Section.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@ namespace CSharpier.Cli.EditorConfig;

internal class Section
{
private static readonly GlobMatcherOptions globOptions =
new()
{
MatchBase = true,
Dot = true,
AllowWindowsPaths = true,
AllowSingleBraceSets = true,
};

private readonly GlobMatcher matcher;

public string? IndentStyle { get; }
Expand All @@ -23,8 +14,7 @@ internal class Section

public Section(SectionData section, string directory)
{
var pattern = FixGlob(section.SectionName, directory);
this.matcher = GlobMatcher.Create(pattern, globOptions);
this.matcher = Globber.Create(section.SectionName, directory);
this.IndentStyle = section.Keys["indent_style"];
this.IndentSize = section.Keys["indent_size"];
this.TabWidth = section.Keys["tab_width"];
Expand All @@ -36,21 +26,4 @@ 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;
}
}
119 changes: 119 additions & 0 deletions Src/CSharpier.Cli/Options/ConfigFileParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
namespace CSharpier.Cli.Options;

using System.IO.Abstractions;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

internal static class ConfigFileParser
{
private static readonly string[] validExtensions = { ".csharpierrc", ".json", ".yml", ".yaml" };

/// <summary>Finds all configs above the given directory as well as within the subtree of this directory</summary>
internal static List<CSharpierConfigData> FindForDirectoryName(
string directoryName,
IFileSystem fileSystem,
ILogger logger,
bool limitEditorConfigSearch
)
{
var results = new List<CSharpierConfigData>();
var directoryInfo = fileSystem.DirectoryInfo.New(directoryName);

var filesByDirectory = directoryInfo
.EnumerateFiles(
".csharpierrc*",
limitEditorConfigSearch
? SearchOption.TopDirectoryOnly
: 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)
{
var file = directoryInfo
.EnumerateFiles(".csharpierrc*", SearchOption.TopDirectoryOnly)
.Where(o => validExtensions.Contains(o.Extension, StringComparer.OrdinalIgnoreCase))
.MinBy(o => o.Extension);

if (file != null)
{
results.Add(
new CSharpierConfigData(
file.DirectoryName!,
Create(file.FullName, fileSystem, logger)
)
);
}

directoryInfo = directoryInfo.Parent;
}

return results.OrderByDescending(o => o.DirectoryName.Length).ToList();
}

internal static ConfigurationFileOptions Create(
string configPath,
IFileSystem fileSystem,
ILogger? logger = null
)
{
var directoryName = fileSystem.Path.GetDirectoryName(configPath)!;
var content = fileSystem.File.ReadAllText(configPath);

if (!string.IsNullOrWhiteSpace(content))
{
var configFile = CreateFromContent(content);
configFile.Init(directoryName);
return configFile;
}

logger?.LogWarning("The configuration file at " + configPath + " was empty.");

return new();
}

internal static ConfigurationFileOptions CreateFromContent(string content)
{
return content.TrimStart().StartsWith("{") ? ReadJson(content) : ReadYaml(content);
}

private static ConfigurationFileOptions ReadJson(string contents)
{
return JsonSerializer.Deserialize<ConfigurationFileOptions>(
contents,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
) ?? new();
}

private static ConfigurationFileOptions ReadYaml(string contents)
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();

return deserializer.Deserialize<ConfigurationFileOptions>(contents);
}
}
Loading

0 comments on commit 28c9bc4

Please sign in to comment.