Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for editorconfig #927

Merged
merged 14 commits into from
Sep 1, 2023
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<PackageVersion Include="CliWrap" Version="3.3.3" />
<PackageVersion Include="DiffEngine" Version="6.5.7" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="Ignore" Version="0.1.48" />
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.6.0" />
Expand Down
2 changes: 1 addition & 1 deletion Scripts/RunLinuxTests.ps1
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions Src/CSharpier.Cli.Tests/CliTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions Src/CSharpier.Cli/CSharpier.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PublicKey>002400000480000094000000060200000024000052534131000400000100010049d266ea1aeae09c0abfce28b8728314d4e4807126ee8bc56155a7ddc765997ed3522908b469ae133fc49ef0bfa957df36082c1c2e0ec8cdc05a4ca4dbd4e1bea6c17fc1008555e15af13a8fc871a04ffc38f5e60e6203bfaf01d16a2a283b90572ade79135801c1675bf38b7a5a60ec8353069796eb53a26ffdddc9ee1273be</PublicKey>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNet.Glob" />
<PackageReference Include="Ignore" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="System.CommandLine" />
Expand Down
99 changes: 40 additions & 59 deletions Src/CSharpier.Cli/CommandLineFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System.Text;
using CSharpier.Cli.Options;
using System.Diagnostics;
using System.IO.Abstractions;
using CSharpier.Utilities;
using Microsoft.Extensions.Logging;

namespace CSharpier.Cli;

using System.Text;

internal static class CommandLineFormatter
{
public static async Task<int> Format(
Expand All @@ -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,
Expand All @@ -41,7 +41,7 @@ CancellationToken cancellationToken

if (
!GeneratedCodeUtilities.IsGeneratedCodeFile(filePath)
&& !ignoreFile.IsIgnored(filePath)
&& !optionsProvider.IsIgnored(filePath)
)
{
var fileIssueLogger = new FileIssueLogger(
Expand All @@ -54,7 +54,7 @@ await PerformFormattingSteps(
new StdOutFormattedFileWriter(console),
commandLineFormatterResult,
fileIssueLogger,
printerOptions,
optionsProvider.GetPrinterOptionsFor(filePath),
commandLineOptions,
FormattingCacheFactory.NullCache,
cancellationToken
Expand Down Expand Up @@ -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
);
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -218,52 +238,13 @@ await FormatPhysicalFile(
}
}
}
else
{
console.WriteErrorLine(
"There was no file or directory found at " + directoryOrFile
);
return 1;
}

await formattingCache.ResolveAsync(cancellationToken);
}

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,
Expand Down
7 changes: 7 additions & 0 deletions Src/CSharpier.Cli/EditorConfig/ConfigFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace CSharpier.Cli.EditorConfig;

internal class ConfigFile
{
public required List<Section> Sections { get; init; }
belav marked this conversation as resolved.
Show resolved Hide resolved
public bool IsRoot { get; init; }
}
78 changes: 78 additions & 0 deletions Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs
Original file line number Diff line number Diff line change
@@ -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*([#;].*)?$");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I'm not a super big fan of parsing source using regex.
People have built libraries that use actual parsers and lexers to handle all the weird corner cases properly.
editorconfig.org recommends:
https://github.com/editorconfig/editorconfig-core-net#readme

However, editorconfig is actually an INI file and there are plenty of well known libraries for those.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was originally using that project, but it didn't support IFileSystem - https://github.com/editorconfig/editorconfig-core-net/pulls

I also realized it was built to find a editorconfig for a given file, which would be too slow for what csharpier needs.

That regex comes out of editorconfig-core-net, I did find https://github.com/rickyah/ini-parser just now. I will see if it is easy to pull in.

private static readonly Regex CommentRegex = new(@"^\s*[#;]");
private static readonly Regex PropertyRegex =
new(@"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*([#;].*)?$");

private static readonly HashSet<string> 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<string, Dictionary<string, string?>>();
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<string, string?>();
}
}

var directory = fileSystem.Path.GetDirectoryName(filePath);
return new ConfigFile
{
IsRoot = isRoot,
Sections = propertiesBySection
.Select(o => new Section(o.Key, directory, o.Value))
.ToList()
};
}
}
Loading