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
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
<PackageVersion Include="BenchmarkDotNet.Annotations" Version="0.13.5" />
<PackageVersion Include="CliWrap" Version="3.3.3" />
<PackageVersion Include="DiffEngine" Version="6.5.7" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.3.2" />
<PackageVersion Include="Ignore" Version="0.1.48" />
<PackageVersion Include="ini-parser-netstandard" Version="2.5.2" />
<PackageVersion Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.6.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" 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
2 changes: 2 additions & 0 deletions Src/CSharpier.Cli/CSharpier.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
<PublicKey>002400000480000094000000060200000024000052534131000400000100010049d266ea1aeae09c0abfce28b8728314d4e4807126ee8bc56155a7ddc765997ed3522908b469ae133fc49ef0bfa957df36082c1c2e0ec8cdc05a4ca4dbd4e1bea6c17fc1008555e15af13a8fc871a04ffc38f5e60e6203bfaf01d16a2a283b90572ade79135801c1675bf38b7a5a60ec8353069796eb53a26ffdddc9ee1273be</PublicKey>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNet.Glob" />
<PackageReference Include="Ignore" />
<PackageReference Include="ini-parser-netstandard" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="System.CommandLine" />
<PackageReference Include="System.IO.Abstractions" />
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 IReadOnlyCollection<Section> Sections { get; init; }
public bool IsRoot { get; init; }
}
24 changes: 24 additions & 0 deletions Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs
Original file line number Diff line number Diff line change
@@ -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<Section>();
foreach (var section in configData.Sections)
{
sections.Add(new Section(section, directory));
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: I feel like this whole loop could have been a .Select().

}
return new ConfigFile { IsRoot = configData.Global["root"] == "true", Sections = sections };
}
}
84 changes: 84 additions & 0 deletions Src/CSharpier.Cli/EditorConfig/EditorConfigParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.IO.Abstractions;

namespace CSharpier.Cli.EditorConfig;

internal static class EditorConfigParser
{
/// <summary>Finds all configs above the given directory as well as within the subtree of this directory</summary>
public static List<EditorConfigSections> FindForDirectoryName(
string directoryName,
IFileSystem fileSystem
)
{
if (directoryName is "")
{
return new List<EditorConfigSections>();
}

var directoryInfo = fileSystem.DirectoryInfo.FromDirectoryName(directoryName);
var editorConfigFiles = directoryInfo
Copy link
Contributor

Choose a reason for hiding this comment

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

Thus only a part of the .editorconfig of the project is considered. Since the project system / Visual Studio with <EditorConfigFiles /> allows to load further files. In addition Visual Studios extension the Global AnalyzerConfig.

Copy link
Owner Author

@belav belav Aug 31, 2023

Choose a reason for hiding this comment

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

This comes from the doc on AnalyzerConfig, so I don't think there need to be any changes to support it.

global configuration files can't be used to configure editor style settings for IDEs, such as indent size or whether to trim trailing whitespace. Instead, they are designed purely for specifying project-level analyzer configuration options.

I wasn't aware of EditorConfigFiles but I'll look into it.

Copy link
Owner Author

Choose a reason for hiding this comment

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

It sounds like usage of EditorConfigFiles is not recommended and Global AnalyzerConfig should be used instead. https://github.com/MicrosoftDocs/visualstudio-docs/issues/6051#issuecomment-1092262347 Which makes it sounds like this is more intended to be used for configuring analyzers.

There is an easy workaround of just adding an .editorconfig or .csahrpierrc to the root, so it doesn't seem critical to support EditorConfigFiles.

.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<Section> FindSections(string filePath, IFileSystem fileSystem)
{
return ParseConfigFiles(fileSystem.Path.GetDirectoryName(filePath), fileSystem)
.Reverse()
.SelectMany(configFile => configFile.Sections)
.ToList();
belav marked this conversation as resolved.
Show resolved Hide resolved
}

private static IEnumerable<ConfigFile> 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;
}
}
}
Loading
Loading