Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 149 additions & 20 deletions Src/CSharpier.Cli/CommandLineFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ CancellationToken cancellationToken
writer = new FileSystemFormattedFileWriter(fileSystem);
}

var pendingFiles = new List<(string DirectoryName, string ActualPath, string OriginalPath)>();

for (var x = 0; x < commandLineOptions.DirectoryOrFilePaths.Length; x++)
{
var directoryOrFilePath = commandLineOptions.DirectoryOrFilePaths[x];
Expand All @@ -193,11 +195,26 @@ CancellationToken cancellationToken
return 1;
}

var directoryName = isFile
? fileSystem.Path.GetDirectoryName(directoryOrFilePath)
: directoryOrFilePath;
var originalDirectoryOrFile = commandLineOptions.OriginalDirectoryOrFilePaths[x];

if (!Path.IsPathRooted(originalDirectoryOrFile))
{
if (!originalDirectoryOrFile.StartsWith('.'))
{
originalDirectoryOrFile =
"." + Path.DirectorySeparatorChar + originalDirectoryOrFile;
}
}

if (isFile)
{
var fileDirectoryName = fileSystem.Path.GetDirectoryName(directoryOrFilePath);
ArgumentNullException.ThrowIfNull(fileDirectoryName);
pendingFiles.Add((fileDirectoryName, directoryOrFilePath, originalDirectoryOrFile));
continue;
}

ArgumentNullException.ThrowIfNull(directoryName);
var directoryName = directoryOrFilePath;

var optionsProvider = await OptionsProvider.Create(
directoryName,
Expand All @@ -208,24 +225,13 @@ CancellationToken cancellationToken
cancellationToken
);

var originalDirectoryOrFile = commandLineOptions.OriginalDirectoryOrFilePaths[x];

var formattingCache = await FormattingCacheFactory.InitializeAsync(
commandLineOptions,
optionsProvider,
fileSystem,
cancellationToken
);

if (!Path.IsPathRooted(originalDirectoryOrFile))
{
if (!originalDirectoryOrFile.StartsWith('.'))
{
originalDirectoryOrFile =
"." + Path.DirectorySeparatorChar + originalDirectoryOrFile;
}
}

async IAsyncEnumerable<string> EnumerateNonignoredFiles(string directory)
{
foreach (var file in fileSystem.Directory.EnumerateFiles(directory))
Expand Down Expand Up @@ -295,11 +301,7 @@ await FormatPhysicalFile(
}
}

if (isFile)
{
await FormatFile(directoryOrFilePath, originalDirectoryOrFile, true);
}
else if (isDirectory)
if (isDirectory)
{
if (
!commandLineOptions.NoMSBuildCheck
Expand Down Expand Up @@ -340,7 +342,111 @@ var file in EnumerateNonignoredFiles(directoryOrFilePath)
await formattingCache.ResolveAsync(cancellationToken);
}

if (pendingFiles.Count == 0)
{
return 0;
}

var commonRoot = pendingFiles[0].DirectoryName;
for (var i = 1; i < pendingFiles.Count; i++)
{
commonRoot = GetCommonAncestor(commonRoot, pendingFiles[i].DirectoryName);
}

var batchOptionsProvider = await OptionsProvider.Create(
commonRoot,
commandLineOptions.ConfigPath,
commandLineOptions.IgnorePath,
fileSystem,
logger,
cancellationToken
);

var batchFormattingCache = await FormattingCacheFactory.InitializeAsync(
commandLineOptions,
batchOptionsProvider,
fileSystem,
cancellationToken
);

var pendingTasks = new List<Task>();

foreach (var (_, actualPath, originalPath) in pendingFiles)
{
pendingTasks.Add(
FormatPendingFile(
actualPath,
originalPath,
batchOptionsProvider,
batchFormattingCache
)
);
}

try
{
await Task.WhenAll(pendingTasks).WaitAsync(cancellationToken);
}
catch (OperationCanceledException ex)
{
if (ex.CancellationToken != cancellationToken)
{
throw;
}
}

await batchFormattingCache.ResolveAsync(cancellationToken);

return 0;

async Task FormatPendingFile(
string actualFilePath,
string originalFilePath,
OptionsProvider optionsProvider,
IFormattingCache formattingCache
)
{
if (
(
!commandLineOptions.IncludeGenerated
&& GeneratedCodeUtilities.IsGeneratedCodeFile(actualFilePath)
) || await optionsProvider.IsIgnoredAsync(actualFilePath, cancellationToken)
)
{
return;
}

var printerOptions = await optionsProvider.GetPrinterOptionsForAsync(
actualFilePath,
cancellationToken
);

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

private static async Task FormatPhysicalFile(
Expand Down Expand Up @@ -386,6 +492,29 @@ await PerformFormattingSteps(
);
}

internal static string GetCommonAncestor(string pathA, string pathB)
{
var separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
var partsA = pathA.Split(separators);
var partsB = pathB.Split(separators);
var commonLength = 0;
for (var i = 0; i < Math.Min(partsA.Length, partsB.Length); i++)
{
if (string.Equals(partsA[i], partsB[i], StringComparison.Ordinal))
{
commonLength = i + 1;
}
else
{
break;
}
}

return commonLength > 0
? string.Join(Path.DirectorySeparatorChar, partsA.Take(commonLength))
: pathA;
}

private static int ReturnExitCode(
CommandLineOptions commandLineOptions,
CommandLineFormatterResult result
Expand Down
89 changes: 89 additions & 0 deletions Src/CSharpier.Tests/CommandLineFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,75 @@ public void Multiple_Files_Should_Use_Root_Ignore()
result.OutputLines.FirstOrDefault().Should().StartWith("Formatted 0 files in ");
}

[Test]
public void Multiple_Files_Should_Be_Formatted()
{
var context = new TestContext();
context.WhenAFileExists("SubFolder/1/File1.cs", UnformattedClassContent);
context.WhenAFileExists("SubFolder/2/File2.cs", UnformattedClassContent);

var result = Format(
context,
directoryOrFilePaths: ["SubFolder/1/File1.cs", "SubFolder/2/File2.cs"]
);

result.OutputLines.FirstOrDefault().Should().StartWith("Formatted 2 files in ");
context.GetFileContent("SubFolder/1/File1.cs").Should().Be(FormattedClassContent);
context.GetFileContent("SubFolder/2/File2.cs").Should().Be(FormattedClassContent);
}

[Test]
public void Multiple_Files_Should_Respect_Per_Directory_Config()
{
var context = new TestContext();
// file1 has a narrow printWidth, file2 uses default
context.WhenAFileExists("SubFolder/1/.csharpierrc", "printWidth: 10");
context.WhenAFileExists(
"SubFolder/1/File1.cs",
"var myVariable = someLongValue;"
);
context.WhenAFileExists(
"SubFolder/2/File2.cs",
"var myVariable = someLongValue;"
);

var result = Format(
context,
directoryOrFilePaths: ["SubFolder/1/File1.cs", "SubFolder/2/File2.cs"]
);

result.OutputLines.FirstOrDefault().Should().StartWith("Formatted 2 files in ");
// file1 should wrap due to printWidth: 10
context
.GetFileContent("SubFolder/1/File1.cs")
.Should()
.Be("var myVariable =\n someLongValue;\n");
// file2 should not wrap (default printWidth: 100)
context
.GetFileContent("SubFolder/2/File2.cs")
.Should()
.Be("var myVariable = someLongValue;\n");
}

[Test]
public void Mixed_Files_And_Directory_Should_Format_All()
{
var context = new TestContext();
context.WhenAFileExists("DirA/File1.cs", UnformattedClassContent);
context.WhenAFileExists("DirA/File2.cs", UnformattedClassContent);
context.WhenAFileExists("SingleFile.cs", UnformattedClassContent);

var result = Format(
context,
directoryOrFilePaths: ["DirA", "SingleFile.cs"]
);

result.OutputLines.FirstOrDefault().Should().StartWith("Formatted 3 files in ");
context.GetFileContent("DirA/File1.cs").Should().Be(FormattedClassContent);
context.GetFileContent("DirA/File2.cs").Should().Be(FormattedClassContent);
context.GetFileContent("SingleFile.cs").Should().Be(FormattedClassContent);
}

[Test]
public void Multiple_Files_Should_Use_Multiple_Ignores()
{
Expand Down Expand Up @@ -1091,6 +1160,26 @@ public string GetFileContent(string path)
}
}

[Test]
[Arguments("/a/b/c", "/a/b/d", "/a/b")]
[Arguments("/a/b", "/a/b", "/a/b")]
[Arguments("/a/b/c/d", "/a/b", "/a/b")]
[Arguments("/a/x/y", "/a/z/w", "/a")]
[Arguments("/x/y", "/z/w", "")]
public void GetCommonAncestor_Returns_Common_Path(
string pathA,
string pathB,
string expected
)
{
var sep = Path.DirectorySeparatorChar;
var result = CommandLineFormatter.GetCommonAncestor(
pathA.Replace('/', sep),
pathB.Replace('/', sep)
);
result.Should().Be(expected.Replace('/', sep));
}

private sealed record FormatResult(
int ExitCode,
IList<string> OutputLines,
Expand Down