Skip to content
Merged
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
21 changes: 16 additions & 5 deletions Src/CSharpier.Cli/FormattingCache.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Collections.Concurrent;
using System.IO.Abstractions;
using System.IO.Hashing;
using System.Text;
using System.Runtime.InteropServices;
using System.Text.Json;
using CSharpier.Cli.Options;
using CSharpier.Core.Utilities;
Expand Down Expand Up @@ -99,10 +99,9 @@ IFileSystem fileSystem

public bool CanSkipFormatting(FileToFormatInfo fileToFormatInfo)
{
var currentHash = Hash(fileToFormatInfo.FileContents) + this.optionsHash;
if (cacheDictionary.TryGetValue(fileToFormatInfo.Path, out var cachedHash))
{
if (currentHash == cachedHash)
if (this.HashMatches(cachedHash, fileToFormatInfo.FileContents))
{
return true;
}
Expand All @@ -113,6 +112,15 @@ public bool CanSkipFormatting(FileToFormatInfo fileToFormatInfo)
return false;
}

private bool HashMatches(string cachedHash, string fileContents)
{
var contentHash = Hash(fileContents);

return cachedHash.Length == contentHash.Length + this.optionsHash.Length
&& cachedHash.AsSpan(0, contentHash.Length).SequenceEqual(contentHash.AsSpan())
&& cachedHash.AsSpan(contentHash.Length).SequenceEqual(this.optionsHash.AsSpan());
}

public void CacheResult(string code, FileToFormatInfo fileToFormatInfo)
{
cacheDictionary[fileToFormatInfo.Path] = Hash(code) + this.optionsHash;
Expand All @@ -124,10 +132,13 @@ private static string GetOptionsHash(OptionsProvider optionsProvider)
return Hash($"{csharpierVersion}_${optionsProvider.Serialize()}");
}

// hashes the utf-16 payload in place - transcoding to ascii first would both copy the whole
// file and collapse every non-ascii character to '?', letting two different files collide
private static string Hash(string input)
{
var result = XxHash32.Hash(Encoding.ASCII.GetBytes(input));
return Convert.ToHexString(result);
Span<byte> destination = stackalloc byte[sizeof(uint)];
XxHash32.Hash(MemoryMarshal.AsBytes(input.AsSpan()), destination);
return Convert.ToHexString(destination);
}

public async Task ResolveAsync(CancellationToken cancellationToken)
Expand Down
5 changes: 3 additions & 2 deletions Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ out var parsedFormatter
?? PrinterOptions.GetXmlWhitespaceSensitivity(filePath)
)
{
IndentSize = matchingOverride.IndentSize,
IndentSize =
matchingOverride.IndentSize ?? (parsedFormatter == Formatter.XML ? 2 : 4),
UseTabs = matchingOverride.UseTabs,
Width = matchingOverride.PrintWidth,
EndOfLine = matchingOverride.EndOfLine,
Expand Down Expand Up @@ -81,7 +82,7 @@ internal class Override
private GlobMatcher? matcher;

public int PrintWidth { get; init; } = 100;
public int IndentSize { get; init; } = 4;
public int? IndentSize { get; init; }
public bool UseTabs { get; init; }

[JsonConverter(typeof(CaseInsensitiveEnumConverter<XmlWhitespaceSensitivity>))]
Expand Down
22 changes: 20 additions & 2 deletions Src/CSharpier.Cli/Server/CSharpierServiceImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,14 @@ CancellationToken cancellationToken
}

var printerOptions = await optionsProvider.GetPrinterOptionsForAsync(
formatFileParameter.fileName,
fileName,
cancellationToken
);
if (printerOptions == null || printerOptions.Formatter is Formatter.Unknown)
{
return new FormatFileResult(Status.UnsupportedFile);
}

// TODO #819 if there are compilation errors we need to do something here
var result = await CodeFormatter.FormatAsync(
formatFileParameter.fileContents,
printerOptions,
Expand All @@ -83,6 +82,25 @@ CancellationToken cancellationToken
};
}

if (string.IsNullOrEmpty(result.Code))
{
if (!string.IsNullOrEmpty(result.WarningMessage))
{
return new FormatFileResult(Status.Failed)
{
errorMessage = result.WarningMessage,
};
}

if (!string.IsNullOrEmpty(result.FailureMessage))
{
return new FormatFileResult(Status.Failed)
{
errorMessage = result.FailureMessage,
};
}
}

return new FormatFileResult(Status.Formatted) { formattedFile = result.Code };
}
catch (Exception ex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private static Doc Print<T>(
if (x < list.SeparatorCount)
{
unFormattedCode.Append(list.GetSeparator(x).ToFullString().Trim());
unFormattedCode.Append(Environment.NewLine);
unFormattedCode.Append(context.LineEnding);
}

continue;
Expand Down
4 changes: 2 additions & 2 deletions Src/CSharpier.Core/DocTypes/Doc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ public static Doc Join(Doc separator, IEnumerable<Doc> enumerable)

public static ForceFlat ForceFlat(List<Doc> contents)
{
return new ForceFlat { Contents = contents.Count == 0 ? contents[0] : Concat(contents) };
return new ForceFlat { Contents = contents.Count == 1 ? contents[0] : Concat(contents) };
}

public static ForceFlat ForceFlat(params Doc[] contents)
{
return new ForceFlat { Contents = contents.Length == 0 ? contents[0] : Concat(contents) };
return new ForceFlat { Contents = contents.Length == 1 ? contents[0] : Concat(contents) };
}

public static Group Group(List<Doc> contents)
Expand Down
1 change: 0 additions & 1 deletion Src/CSharpier.Core/Xml/XNodePrinters/ElementChildren.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public static Doc Print(RawNode node, XmlPrintingContext context)
if (childNode.CSharpierIgnoreType is CSharpierIgnoreType.IgnoreEnd)
{
printIgnored = false;
x++;
}

if (printIgnored)
Expand Down
12 changes: 6 additions & 6 deletions Src/CSharpier.Core/Xml/XNodePrinters/Node.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,6 @@ private static Doc GetTextValue(RawNode rawNode, XmlPrintingContext context)
{
var textValue = rawNode.Value;

if (string.IsNullOrEmpty(textValue))
{
return Doc.Null;
}

if (rawNode.XmlWhitespaceSensitivity is XmlWhitespaceSensitivity.Ignore)
{
if (rawNode.PreviousNode is null)
Expand All @@ -118,14 +113,19 @@ private static Doc GetTextValue(RawNode rawNode, XmlPrintingContext context)
}
}

if (string.IsNullOrEmpty(textValue))
{
return Doc.Null;
}

if (rawNode.Parent?.Nodes.First() == rawNode)
{
if (textValue[0] is '\r')
{
textValue = textValue[1..];
}

if (textValue[0] is '\n')
if (textValue.Length > 0 && textValue[0] is '\n')
{
textValue = textValue[1..];
}
Expand Down
49 changes: 49 additions & 0 deletions Src/CSharpier.Tests/Cli/CSharpierServiceImplementationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using AwesomeAssertions;
using CSharpier.Cli.Server;
using Microsoft.Extensions.Logging.Abstractions;

namespace CSharpier.Tests.Cli;

internal sealed class CSharpierServiceImplementationTests
{
[Test]
public async Task Should_Report_Failure_When_Formatting_Produces_A_FailureMessage()
{
var service = new CSharpierServiceImplementation(NullLogger.Instance);

var result = await service.FormatFile(
new FormatFileParameter
{
fileName = Path.Combine(Path.GetTempPath(), "DeepRecursion.cs"),
fileContents = DeeplyConcatenatedString,
},
CancellationToken.None
);

result.status.Should().Be(Status.Failed);
result.errorMessage.Should().Contain("deep of recursion");
}

[Test]
public async Task Should_Not_Return_Empty_Content_When_Formatting_Fails()
{
var service = new CSharpierServiceImplementation(NullLogger.Instance);

var result = await service.FormatFile(
new FormatFileParameter
{
fileName = Path.Combine(Path.GetTempPath(), "DeepRecursion.cs"),
fileContents = DeeplyConcatenatedString,
},
CancellationToken.None
);

result.formattedFile.Should().BeNullOrEmpty();
result.status.Should().NotBe(Status.Formatted);
}

private static readonly string DeeplyConcatenatedString =
"public class ClassName\n{\n private string field = "
+ string.Join(" + ", Enumerable.Repeat("\"1\"", 200))
+ ";\n}\n";
}
64 changes: 64 additions & 0 deletions Src/CSharpier.Tests/Cli/FormattingCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.IO.Abstractions.TestingHelpers;
using System.Text;
using AwesomeAssertions;
using CSharpier.Cli;
using CSharpier.Cli.Options;
using Microsoft.Extensions.Logging.Abstractions;

namespace CSharpier.Tests.Cli;

internal sealed class FormattingCacheTests
{
[Test]
public async Task Should_Not_Skip_File_That_Differs_Only_By_Non_Ascii_Character()
{
var cache = await CreateCacheAsync();
var path = OperatingSystem.IsWindows() ? "c:/test/Class.cs" : "/test/Class.cs";

cache.CacheResult("public class Café { }\n", FileAt(path, "public class Café { }\n"));

var otherFile = FileAt(path, "public class Cafè { }\n");

cache.CanSkipFormatting(otherFile).Should().BeFalse();
}

[Test]
public async Task Should_Skip_File_With_Unchanged_Non_Ascii_Content()
{
var cache = await CreateCacheAsync();
var path = OperatingSystem.IsWindows() ? "c:/test/Class.cs" : "/test/Class.cs";
var contents = "public class Café { }\n";

cache.CacheResult(contents, FileAt(path, contents));

cache.CanSkipFormatting(FileAt(path, contents)).Should().BeTrue();
}

private static FileToFormatInfo FileAt(string path, string contents)
{
return FileToFormatInfo.Create(path, contents, Encoding.UTF8);
}

private static async Task<IFormattingCache> CreateCacheAsync()
{
var directory = OperatingSystem.IsWindows() ? "c:/test" : "/test";
var fileSystem = new MockFileSystem();
fileSystem.AddDirectory(directory);

var optionsProvider = await OptionsProvider.Create(
directory,
null,
null,
fileSystem,
NullLogger.Instance,
CancellationToken.None
);

return await FormattingCacheFactory.InitializeAsync(
new CommandLineOptions(),
optionsProvider,
fileSystem,
CancellationToken.None
);
}
}
25 changes: 25 additions & 0 deletions Src/CSharpier.Tests/LineEndingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,31 @@ EndOfLine endOfLine
result.Code.Should().NotContain($"one{newLine}two");
}

[Test]
[Arguments(EndOfLine.LF)]
[Arguments(EndOfLine.CRLF)]
public async Task Ignored_Range_In_Separated_List_Should_Respect_LineEnding(EndOfLine endOfLine)
{
var code = """
var value = new()
{
// csharpier-ignore-start
First = 1,
Second = 2
// csharpier-ignore-end
};

""";

var printerOptions = new PrinterOptions(Formatter.CSharp, XmlWhitespaceSensitivity.Strict)
{
EndOfLine = endOfLine,
};
var result = await CSharpFormatter.FormatAsync(code, printerOptions);

result.Code.Should().Be(code.ReplaceLineEndings(endOfLine == EndOfLine.LF ? "\n" : "\r\n"));
}

[Test]
[Arguments("\\r\\n", EndOfLine.LF)]
[Arguments("\\n", EndOfLine.CRLF)]
Expand Down
26 changes: 26 additions & 0 deletions Src/CSharpier.Tests/OptionsProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,32 @@ public async Task Should_Return_IndentSize_For_Override_With_Json()
result.XmlWhitespaceSensitivity.Should().Be(XmlWhitespaceSensitivity.Ignore);
}

[Test]
[Arguments("xml", 2)]
[Arguments("csharp", 4)]
public async Task Should_Return_Formatter_Default_IndentSize_For_Override_Without_IndentSize(
string formatter,
int expectedIndentSize
)
{
var context = new TestContext();
context.WhenAFileExists(
"c:/test/.csharpierrc",
$"""
overrides:
- files: "*.override"
formatter: "{formatter}"
"""
);

var result = await context.CreateProviderAndGetOptionsFor(
"c:/test",
"c:/test/test.override"
);

result.IndentSize.Should().Be(expectedIndentSize);
}

[Test]
[Arguments("cs")]
[Arguments("csx")]
Expand Down
Loading
Loading