diff --git a/Src/CSharpier.Cli/CommandLineFormatter.cs b/Src/CSharpier.Cli/CommandLineFormatter.cs index fadb4ccb6..d45eb33ec 100644 --- a/Src/CSharpier.Cli/CommandLineFormatter.cs +++ b/Src/CSharpier.Cli/CommandLineFormatter.cs @@ -501,7 +501,10 @@ CancellationToken cancellationToken { IFormattingValidator? formattingValidator = null; - if (printerOptions.Formatter is Formatter.CSharp or Formatter.CSharpScript) + if ( + printerOptions.Formatter is Formatter.CSharp or Formatter.CSharpScript + && fileToFormatInfo.FileContents != codeFormattingResult.Code + ) { var sourceCodeKind = printerOptions.Formatter is Formatter.CSharpScript diff --git a/Src/CSharpier.Cli/EditorConfig/EditorConfigFileParser.cs b/Src/CSharpier.Cli/EditorConfig/CSharpierConfigParser.cs similarity index 100% rename from Src/CSharpier.Cli/EditorConfig/EditorConfigFileParser.cs rename to Src/CSharpier.Cli/EditorConfig/CSharpierConfigParser.cs diff --git a/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs b/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs index c6476769a..bd7e33ec4 100644 --- a/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs +++ b/Src/CSharpier.Cli/EditorConfig/EditorConfigSections.cs @@ -53,6 +53,11 @@ internal class EditorConfigSections printerOptions.EndOfLine = endOfLine; } + if (resolvedConfiguration.XmlWhitespaceSensitivity is { } xmlWhitespaceSensitivity) + { + printerOptions.XmlWhitespaceSensitivity = xmlWhitespaceSensitivity; + } + return printerOptions; } @@ -63,6 +68,7 @@ private class ResolvedConfiguration public int? TabWidth { get; } public int? MaxLineLength { get; } public EndOfLine? EndOfLine { get; } + public XmlWhitespaceSensitivity? XmlWhitespaceSensitivity { get; set; } public string? Formatter { get; } public ResolvedConfiguration(List
sections) @@ -104,9 +110,23 @@ public ResolvedConfiguration(List
sections) } var endOfLine = sections.LastOrDefault(o => o.EndOfLine != null)?.EndOfLine; - if (Enum.TryParse(endOfLine, true, out EndOfLine result)) + if (Enum.TryParse(endOfLine, true, out EndOfLine parsedEndOfLine)) + { + this.EndOfLine = parsedEndOfLine; + } + + var xmlWhitespaceSensitivity = sections + .LastOrDefault(o => o.XmlWhitespaceSensitivity != null) + ?.XmlWhitespaceSensitivity; + if ( + Enum.TryParse( + xmlWhitespaceSensitivity, + true, + out XmlWhitespaceSensitivity parsedXmlWhitespaceSensitivity + ) + ) { - this.EndOfLine = result; + this.XmlWhitespaceSensitivity = parsedXmlWhitespaceSensitivity; } this.Formatter = sections.LastOrDefault(o => o.Formatter is not null)?.Formatter; diff --git a/Src/CSharpier.Cli/EditorConfig/Section.cs b/Src/CSharpier.Cli/EditorConfig/Section.cs index e8f708831..156ccebbf 100644 --- a/Src/CSharpier.Cli/EditorConfig/Section.cs +++ b/Src/CSharpier.Cli/EditorConfig/Section.cs @@ -13,6 +13,8 @@ internal class Section(SectionData section, string directory) public string? MaxLineLength { get; } = section.Keys["max_line_length"]; public string? EndOfLine { get; } = section.Keys["end_of_line"]; public string? Formatter { get; } = section.Keys["csharpier_formatter"]; + public string? XmlWhitespaceSensitivity { get; } = + section.Keys["csharpier_xml_whitespace_sensitivity"]; public bool IsMatch(string fileName, bool ignoreDirectory) { diff --git a/Src/CSharpier.Cli/IgnoreFile.cs b/Src/CSharpier.Cli/IgnoreFile.cs index 89d2320bf..a64738e0c 100644 --- a/Src/CSharpier.Cli/IgnoreFile.cs +++ b/Src/CSharpier.Cli/IgnoreFile.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.IO.Abstractions; using CSharpier.Cli.DotIgnore; using CSharpier.Core; @@ -35,17 +36,27 @@ public bool IsIgnored(string filePath) string baseDirectoryPath, IFileSystem fileSystem, string? ignorePath, + ConcurrentDictionary? ignoreCache, CancellationToken cancellationToken ) { - Task CreateIgnore(string ignoreFilePath, string? overrideBasePath) + async Task CreateIgnore(string ignoreFilePath, string? overrideBasePath) { - return IgnoreList.CreateAsync( + if (ignoreCache is not null && ignoreCache.TryGetValue(ignoreFilePath, out var ignore)) + { + return ignore; + } + + ignore = await IgnoreList.CreateAsync( fileSystem, overrideBasePath ?? Path.GetDirectoryName(ignoreFilePath)!, ignoreFilePath, cancellationToken ); + + ignoreCache?[ignoreFilePath] = ignore; + + return ignore; } return await SharedFunc diff --git a/Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs b/Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs index ee85813cb..a83508c02 100644 --- a/Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs +++ b/Src/CSharpier.Cli/Options/ConfigurationFileOptions.cs @@ -10,6 +10,10 @@ internal class ConfigurationFileOptions public int? IndentSize { get; init; } public bool UseTabs { get; init; } + [JsonConverter(typeof(CaseInsensitiveEnumConverter))] + public XmlWhitespaceSensitivity XmlWhitespaceSensitivity { get; init; } = + XmlWhitespaceSensitivity.Strict; + [JsonConverter(typeof(CaseInsensitiveEnumConverter))] public EndOfLine EndOfLine { get; init; } @@ -38,6 +42,7 @@ out var parsedFormatter UseTabs = matchingOverride.UseTabs, Width = matchingOverride.PrintWidth, EndOfLine = matchingOverride.EndOfLine, + XmlWhitespaceSensitivity = matchingOverride.XmlWhitespaceSensitivity, }; } @@ -50,6 +55,7 @@ out var parsedFormatter UseTabs = this.UseTabs, Width = this.PrintWidth, EndOfLine = this.EndOfLine, + XmlWhitespaceSensitivity = this.XmlWhitespaceSensitivity, }; } @@ -73,6 +79,10 @@ internal class Override public int IndentSize { get; init; } = 4; public bool UseTabs { get; init; } + [JsonConverter(typeof(CaseInsensitiveEnumConverter))] + public XmlWhitespaceSensitivity XmlWhitespaceSensitivity { get; init; } = + XmlWhitespaceSensitivity.Strict; + [JsonConverter(typeof(CaseInsensitiveEnumConverter))] public EndOfLine EndOfLine { get; init; } diff --git a/Src/CSharpier.Cli/Options/OptionsProvider.cs b/Src/CSharpier.Cli/Options/OptionsProvider.cs index 0eb094614..153e1d20b 100644 --- a/Src/CSharpier.Cli/Options/OptionsProvider.cs +++ b/Src/CSharpier.Cli/Options/OptionsProvider.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.IO.Abstractions; using System.Text.Json; +using CSharpier.Cli.DotIgnore; using CSharpier.Cli.EditorConfig; using CSharpier.Core; using Microsoft.Extensions.Logging; @@ -15,6 +16,7 @@ private readonly ConcurrentDictionary< string, CSharpierConfigData? > csharpierConfigsByDirectory = new(); + private readonly ConcurrentDictionary ignoreWithPathCache = new(); private readonly ConcurrentDictionary ignoreFilesByDirectory = new(); private readonly ConfigurationFileOptions? specifiedConfigFile; private readonly EditorConfigSections? specifiedEditorConfig; @@ -60,6 +62,7 @@ CancellationToken cancellationToken directoryName, fileSystem, ignorePath, + null, cancellationToken ); @@ -204,7 +207,13 @@ CancellationToken cancellationToken Path.Combine(searchingDirectory, ".csharpierignore") ), (searchingDirectory) => - IgnoreFile.CreateAsync(searchingDirectory, this.fileSystem, null, cancellationToken) + IgnoreFile.CreateAsync( + searchingDirectory, + this.fileSystem, + null, + ignoreWithPathCache, + cancellationToken + ) ); #pragma warning disable IDE0270 diff --git a/Src/CSharpier.Core/CSharp/SyntaxNodeComparer.cs b/Src/CSharpier.Core/CSharp/SyntaxNodeComparer.cs index 7d5bed4a1..8675bfbf3 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxNodeComparer.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxNodeComparer.cs @@ -50,6 +50,7 @@ CancellationToken cancellationToken cSharpParseOptions, cancellationToken: cancellationToken ); + this.CompareFunc = Compare; } public string CompareSource() @@ -148,14 +149,16 @@ SyntaxNode formattedStart return Equal; } +#pragma warning disable CA1822 private CompareResult CompareLists( - IReadOnlyList originalList, - IReadOnlyList formattedList, - Func comparer, - Func getSpan, + T originalList, + T formattedList, + Func comparer, + Func getSpan, TextSpan originalParentSpan, TextSpan newParentSpan ) + where T : IReadOnlyList { for (var x = 0; x < originalList.Count || x < formattedList.Count; x++) { @@ -169,25 +172,71 @@ TextSpan newParentSpan return NotEqual(getSpan(originalList[x]), newParentSpan); } - if ( - originalList[x] is SyntaxNode originalNode - && formattedList[x] is SyntaxNode formattedNode - ) + var result = comparer(originalList[x], formattedList[x]); + if (result.IsInvalid) + { + return result; + } + } + + return Equal; + } +#pragma warning restore CA1822 + + private CompareResult CompareLists( + T originalList, + T formattedList, + Func comparer, + Func getSpan, + TextSpan originalParentSpan, + TextSpan newParentSpan + ) + where T : IReadOnlyList + { + for (var x = 0; x < originalList.Count || x < formattedList.Count; x++) + { + if (x == originalList.Count) { - this.originalStack.Push((originalNode, originalNode.Parent)); - this.formattedStack.Push((formattedNode, formattedNode.Parent)); + return NotEqual(originalParentSpan, getSpan(formattedList[x])); } - else + + if (x == formattedList.Count) { - var result = comparer(originalList[x], formattedList[x]); - if (result.IsInvalid) + return NotEqual(getSpan(originalList[x]), newParentSpan); + } + + var originalNode = originalList[x]; + var formattedNode = formattedList[x]; + this.originalStack.Push((originalNode, originalNode.Parent)); + this.formattedStack.Push((formattedNode, formattedNode.Parent)); + } + + return Equal; + } + + private static SyntaxToken[] AllSeparatorsButLast(in SeparatedSyntaxList list) + { + if (list.Count <= 1) + { + return []; + } + + var tokens = new SyntaxToken[list.Count - 1]; + var tokenIndex = 0; + + foreach (var element in list.GetWithSeparators()) + { + if (element.IsToken) + { + tokens[tokenIndex++] = element.AsToken(); + if (tokenIndex == tokens.Length) { - return result; + break; } } } - return Equal; + return tokens; } private static CompareResult NotEqual(SyntaxNode? originalNode, SyntaxNode? formattedNode) @@ -210,6 +259,8 @@ private static CompareResult NotEqual(TextSpan? originalSpan, TextSpan? formatte }; } + private Func CompareFunc { get; } + private CompareResult Compare(SyntaxToken originalToken, SyntaxToken formattedToken) { return this.Compare(originalToken, formattedToken, null, null); @@ -322,6 +373,17 @@ private CompareResult Compare(SyntaxTrivia originalTrivia, SyntaxTrivia formatte : NotEqual(originalTrivia.Span, formattedTrivia.Span); } + private bool CompareFullSpan(SyntaxNode originalStart, SyntaxNode formattedStart) + { + var originalSpan = OriginalSourceCode + .AsSpan() + .Slice(originalStart.FullSpan.Start, originalStart.FullSpan.Length); + var formattedSpan = NewSourceCode + .AsSpan() + .Slice(formattedStart.FullSpan.Start, formattedStart.FullSpan.Length); + return originalSpan == formattedSpan; + } + private static CompareResult CompareComment( string originalComment, string formattedComment, diff --git a/Src/CSharpier.Core/CSharp/SyntaxPrinter/MembersWithForcedLines.cs b/Src/CSharpier.Core/CSharp/SyntaxPrinter/MembersWithForcedLines.cs index 9e2b0d0dc..33a5b352f 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxPrinter/MembersWithForcedLines.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxPrinter/MembersWithForcedLines.cs @@ -20,7 +20,7 @@ public static List Print( ) where T : MemberDeclarationSyntax { - var result = new List(); + var result = new List(members.Count * 3); if (!skipFirstHardLine) { result.Add(Doc.HardLine); diff --git a/Src/CSharpier.Core/CSharp/SyntaxPrinter/Modifiers.cs b/Src/CSharpier.Core/CSharp/SyntaxPrinter/Modifiers.cs index 40ffc5d52..f966d6084 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxPrinter/Modifiers.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxPrinter/Modifiers.cs @@ -1,4 +1,5 @@ using CSharpier.Core.DocTypes; +using CSharpier.Core.Utilities; using Microsoft.CodeAnalysis; namespace CSharpier.Core.CSharp.SyntaxPrinter; diff --git a/Src/CSharpier.Core/CSharp/SyntaxPrinter/RightHandSide.cs b/Src/CSharpier.Core/CSharp/SyntaxPrinter/RightHandSide.cs index 272082060..734033ead 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxPrinter/RightHandSide.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxPrinter/RightHandSide.cs @@ -1,4 +1,5 @@ using CSharpier.Core.DocTypes; +using CSharpier.Core.Utilities; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; diff --git a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/Argument.cs b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/Argument.cs index f1d3aa223..c001e5298 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/Argument.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/Argument.cs @@ -9,7 +9,11 @@ internal static class Argument { public static Doc Print(ArgumentSyntax node, PrintingContext context) { - return Doc.Concat(PrintModifiers(node, context), Node.Print(node.Expression, context)); + var modifiers = PrintModifiers(node, context); + + return modifiers == Doc.Null + ? Node.Print(node.Expression, context) + : Doc.Concat(modifiers, Node.Print(node.Expression, context)); } public static Doc PrintModifiers(ArgumentSyntax node, PrintingContext context) diff --git a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/AttributeList.cs b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/AttributeList.cs index 2ffeacb90..b7165575d 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/AttributeList.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/AttributeList.cs @@ -33,7 +33,7 @@ node.Parent is CompilationUnitSyntax compilationUnitSyntax var printSeparatedSyntaxList = SeparatedSyntaxList.Print( node.Attributes, - (attributeNode, _) => + static (attributeNode, context) => { var name = Node.Print(attributeNode.Name, context); if (attributeNode.ArgumentList == null) @@ -60,7 +60,7 @@ is [ singleCollectionExpression ? Doc.Null : Doc.SoftLine, SeparatedSyntaxList.Print( attributeNode.ArgumentList.Arguments, - (attributeArgumentNode, _) => + static (attributeArgumentNode, context) => Doc.Concat( attributeArgumentNode.NameEquals != null ? NameEquals.Print( diff --git a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/FunctionPointerType.cs b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/FunctionPointerType.cs index d2fbe1ef6..b0495f9de 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/FunctionPointerType.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/FunctionPointerType.cs @@ -19,7 +19,7 @@ public static Doc Print(FunctionPointerTypeSyntax node, PrintingContext context) Doc.SoftLine, SeparatedSyntaxList.Print( node.ParameterList.Parameters, - (o, _) => + static (o, context) => Doc.Concat( AttributeLists.Print(o, o.AttributeLists, context), Modifiers.Print(o.Modifiers, context), @@ -46,7 +46,7 @@ PrintingContext context Token.Print(node.UnmanagedCallingConventionList.OpenBracketToken, context), SeparatedSyntaxList.Print( node.UnmanagedCallingConventionList.CallingConventions, - (o, _) => Token.Print(o.Name, context), + static (o, context) => Token.Print(o.Name, context), " ", context ), diff --git a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/OrderByClause.cs b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/OrderByClause.cs index c95f39303..169ccd1c2 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/OrderByClause.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/OrderByClause.cs @@ -12,7 +12,7 @@ public static Doc Print(OrderByClauseSyntax node, PrintingContext context) Token.Print(node.OrderByKeyword, context), SeparatedSyntaxList.Print( node.Orderings, - (orderingNode, _) => + static (orderingNode, context) => Doc.Concat( " ", Node.Print(orderingNode.Expression, context), diff --git a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/Parameter.cs b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/Parameter.cs index 4aba50361..a802a41e3 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/Parameter.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/Parameter.cs @@ -19,7 +19,7 @@ public static Doc Print(ParameterSyntax node, PrintingContext context) if ( node.AttributeLists.Count < 2 && ( - Enumerable.Any(node.GetLeadingTrivia(), o => o.IsComment()) + node.GetLeadingTrivia().Any(o => o.IsComment()) || node.Parent is ParameterListSyntax { Parameters.Count: 0 } ) ) diff --git a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/RecursivePattern.cs b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/RecursivePattern.cs index 2553ca007..92ce22e0d 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/RecursivePattern.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxPrinter/SyntaxNodePrinters/RecursivePattern.cs @@ -43,7 +43,7 @@ or BinaryPatternSyntax Doc.SoftLine, SeparatedSyntaxList.Print( node.PositionalPatternClause.Subpatterns, - (subpatternNode, _) => + static (subpatternNode, context) => Doc.Concat( subpatternNode.NameColon != null ? BaseExpressionColon.Print( @@ -78,9 +78,8 @@ or BinaryPatternSyntax result.Add( Doc.Group( node.Type != null - && !Enumerable.Any( - node.PropertyPatternClause.OpenBraceToken.LeadingTrivia, - o => o.IsDirective || o.IsComment() + && !node.PropertyPatternClause.OpenBraceToken.LeadingTrivia.Any(o => + o.IsDirective || o.IsComment() ) ? Doc.Line : Doc.Null, @@ -89,7 +88,7 @@ or BinaryPatternSyntax node.PropertyPatternClause.Subpatterns.Any() ? Doc.Line : Doc.Null, SeparatedSyntaxList.Print( node.PropertyPatternClause.Subpatterns, - (subpatternNode, _) => + static (subpatternNode, context) => Doc.Group( subpatternNode.ExpressionColon != null ? Node.Print(subpatternNode.ExpressionColon, context) diff --git a/Src/CSharpier.Core/CSharp/SyntaxPrinter/Token.cs b/Src/CSharpier.Core/CSharp/SyntaxPrinter/Token.cs index 579966134..18e11218b 100644 --- a/Src/CSharpier.Core/CSharp/SyntaxPrinter/Token.cs +++ b/Src/CSharpier.Core/CSharp/SyntaxPrinter/Token.cs @@ -131,7 +131,7 @@ or SyntaxKind.InterpolatedRawStringEndToken { if ( context.State.TrailingComma is not null - && Enumerable.FirstOrDefault(syntaxToken.TrailingTrivia, o => o.IsComment()) + && syntaxToken.TrailingTrivia.FirstOrDefault(o => o.IsComment()) == context.State.TrailingComma.TrailingComment ) { @@ -174,7 +174,8 @@ public static Doc PrintLeadingTrivia(SyntaxToken syntaxToken, PrintingContext co skipLastHardline: isClosingBrace ); - var hasDirective = Enumerable.Any(syntaxToken.LeadingTrivia, o => o.IsDirective); + var leadingTrivia = syntaxToken.LeadingTrivia; + var hasDirective = leadingTrivia.Any(o => o.IsDirective); if (hasDirective) { @@ -198,7 +199,7 @@ public static Doc PrintLeadingTrivia(SyntaxToken syntaxToken, PrintingContext co Doc extraNewLines = Doc.Null; - if (hasDirective || Enumerable.Any(syntaxToken.LeadingTrivia, o => o.IsComment())) + if (hasDirective || leadingTrivia.Any(o => o.IsComment())) { extraNewLines = ExtraNewLines.Print(syntaxToken.LeadingTrivia); } @@ -351,12 +352,7 @@ void AddLeadingComment(CommentType commentType) if (context.State.NextTriviaNeedsLine) { - if ( - Enumerable.Any( - leadingTrivia, - o => o.RawSyntaxKind() is SyntaxKind.IfDirectiveTrivia - ) - ) + if (leadingTrivia.Any(o => o.RawSyntaxKind() is SyntaxKind.IfDirectiveTrivia)) { docs.Insert(0, Doc.HardLineSkipBreakIfFirstInGroup); } @@ -430,17 +426,11 @@ private static Doc PrintTrailingTrivia(in SyntaxTriviaList trailingTrivia) public static bool HasComments(SyntaxToken syntaxToken) { - return Enumerable.Any( - syntaxToken.LeadingTrivia, - o => - o.RawSyntaxKind() - is not (SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia) + return syntaxToken.LeadingTrivia.Any(o => + o.RawSyntaxKind() is not (SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia) ) - || Enumerable.Any( - syntaxToken.TrailingTrivia, - o => - o.RawSyntaxKind() - is not (SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia) + || syntaxToken.TrailingTrivia.Any(o => + o.RawSyntaxKind() is not (SyntaxKind.WhitespaceTrivia or SyntaxKind.EndOfLineTrivia) ); } @@ -463,11 +453,8 @@ public static bool HasLeadingCommentMatching(SyntaxNode node, Regex regex) public static bool HasLeadingCommentMatching(SyntaxToken token, Regex regex) { - return Enumerable.Any( - token.LeadingTrivia, - o => - o.RawSyntaxKind() is SyntaxKind.SingleLineCommentTrivia - && regex.IsMatch(o.ToString()) + return token.LeadingTrivia.Any(o => + o.RawSyntaxKind() is SyntaxKind.SingleLineCommentTrivia && regex.IsMatch(o.ToString()) ); } } diff --git a/Src/CSharpier.Core/DocPrinter/PropagateBreaks.cs b/Src/CSharpier.Core/DocPrinter/PropagateBreaks.cs index e036ed204..24d2a0189 100644 --- a/Src/CSharpier.Core/DocPrinter/PropagateBreaks.cs +++ b/Src/CSharpier.Core/DocPrinter/PropagateBreaks.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using CSharpier.Core.DocTypes; namespace CSharpier.Core.DocPrinter; @@ -27,6 +28,7 @@ void BreakParentGroup() } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] bool OnEnter(Doc doc) { if (doc is ForceFlat) @@ -87,22 +89,23 @@ void OnExit(Doc doc) docsStack.Push(document); while (docsStack.Count > 0) { - var doc = docsStack.Pop(); + var doc = docsStack.Peek(); if (doc == TraverseDocOnExitStackMarker) { + docsStack.Pop(); OnExit(docsStack.Pop()); continue; } - docsStack.Push(doc); - docsStack.Push(TraverseDocOnExitStackMarker); - if (!OnEnter(doc)) { + OnExit(docsStack.Pop()); continue; } + docsStack.Push(TraverseDocOnExitStackMarker); + if (doc is Concat concat) { // push onto stack in reverse order so they are processed in the original order diff --git a/Src/CSharpier.Core/PrinterOptions.cs b/Src/CSharpier.Core/PrinterOptions.cs index 6e0b16da6..b82dbdde2 100644 --- a/Src/CSharpier.Core/PrinterOptions.cs +++ b/Src/CSharpier.Core/PrinterOptions.cs @@ -27,6 +27,8 @@ public int IndentSize public bool TrimInitialLines { get; init; } = true; public bool IncludeGenerated { get; set; } public Formatter Formatter { get; set; } = formatter; + public XmlWhitespaceSensitivity XmlWhitespaceSensitivity { get; set; } = + XmlWhitespaceSensitivity.Strict; public const int WidthUsedByTests = 100; diff --git a/Src/CSharpier.Core/XmlWhitespaceSensitivity.cs b/Src/CSharpier.Core/XmlWhitespaceSensitivity.cs new file mode 100644 index 000000000..829ddd777 --- /dev/null +++ b/Src/CSharpier.Core/XmlWhitespaceSensitivity.cs @@ -0,0 +1,8 @@ +namespace CSharpier.Core; + +internal enum XmlWhitespaceSensitivity +{ + Strict, + Xaml, + Ignore, +} diff --git a/Src/CSharpier.Generators/SyntaxNodeComparerGenerator.cs b/Src/CSharpier.Generators/SyntaxNodeComparerGenerator.cs index 4985e499e..c884f3d7c 100644 --- a/Src/CSharpier.Generators/SyntaxNodeComparerGenerator.cs +++ b/Src/CSharpier.Generators/SyntaxNodeComparerGenerator.cs @@ -137,10 +137,22 @@ private static void GenerateMethod(StringBuilder sourceBuilder, INamedTypeSymbol $$""" private CompareResult Compare{{type.Name}}({{type.Name}} originalNode, {{type.Name}} formattedNode) { - CompareResult result; + CompareResult result; """ ); + if ( + type.BaseType?.ToDisplayString() + == "Microsoft.CodeAnalysis.CSharp.Syntax.TypeDeclarationSyntax" + || type.ToDisplayString() + == "Microsoft.CodeAnalysis.CSharp.Syntax.MethodDeclarationSyntax" + ) + { + sourceBuilder.AppendLine( + $" if (CompareFullSpan(originalNode, formattedNode)) return Equal;" + ); + } + foreach (var propertySymbol in type.GetAllProperties().OrderBy(o => o.Name)) { var propertyName = propertySymbol.Name; @@ -228,10 +240,13 @@ private static void GenerateMethod(StringBuilder sourceBuilder, INamedTypeSymbol } else { - var compare = propertyType.Name == nameof(SyntaxTokenList) ? "Compare" : "null"; + var compare = + propertyType.Name == nameof(SyntaxTokenList) + ? "CompareFunc" + : "static (_, _) => default"; if (propertyName == "Modifiers") { - propertyName += ".OrderBy(o => o.Text).ToList()"; + propertyName += ".OrderBy(o => o.Text).ToArray()"; } sourceBuilder.AppendLine( @@ -249,13 +264,13 @@ private static void GenerateMethod(StringBuilder sourceBuilder, INamedTypeSymbol ) { sourceBuilder.AppendLine( - $" result = this.CompareLists(originalNode.{propertyName}, formattedNode.{propertyName}, null, o => o.Span, originalNode.Span, formattedNode.Span);" + $" result = this.CompareLists(originalNode.{propertyName}, formattedNode.{propertyName}, static (_, _) => default, o => o.Span, originalNode.Span, formattedNode.Span);" ); sourceBuilder.AppendLine(" if (result.IsInvalid) return result;"); // Omit the last separator when comparing the original node with the formatted node, as it legitimately may be added or removed sourceBuilder.AppendLine( - $" result = this.CompareLists(originalNode.{propertyName}.GetSeparators().Take(originalNode.{propertyName}.Count() - 1).ToList(), formattedNode.{propertyName}.GetSeparators().Take(formattedNode.{propertyName}.Count() - 1).ToList(), Compare, o => o.Span, originalNode.Span, formattedNode.Span);" + $" result = this.CompareLists(AllSeparatorsButLast(originalNode.{propertyName}), AllSeparatorsButLast(formattedNode.{propertyName}), CompareFunc, o => o.Span, originalNode.Span, formattedNode.Span);" ); sourceBuilder.AppendLine(" if (result.IsInvalid) return result;"); } diff --git a/Src/CSharpier.Tests/Cli/IgnoreFileTests.cs b/Src/CSharpier.Tests/Cli/IgnoreFileTests.cs index 75e3de8f3..7398711fe 100644 --- a/Src/CSharpier.Tests/Cli/IgnoreFileTests.cs +++ b/Src/CSharpier.Tests/Cli/IgnoreFileTests.cs @@ -621,7 +621,13 @@ private void GitBasedTest(string gitignore, string[] files) var gitIgnoredFiles = files.Where(o => !gitUntrackedFiles.Contains(o)); var ignoreFile = IgnoreFile - .CreateAsync(this.gitRepository.RepoPath, this.fileSystem, null, CancellationToken.None) + .CreateAsync( + this.gitRepository.RepoPath, + this.fileSystem, + null, + null, + CancellationToken.None + ) .GetAwaiter() .GetResult(); diff --git a/Src/CSharpier.Tests/Cli/Options/EditorConfigFileParserTests.cs b/Src/CSharpier.Tests/Cli/Options/CSharpierConfigParserTests.cs similarity index 97% rename from Src/CSharpier.Tests/Cli/Options/EditorConfigFileParserTests.cs rename to Src/CSharpier.Tests/Cli/Options/CSharpierConfigParserTests.cs index a143191ed..6315eda64 100644 --- a/Src/CSharpier.Tests/Cli/Options/EditorConfigFileParserTests.cs +++ b/Src/CSharpier.Tests/Cli/Options/CSharpierConfigParserTests.cs @@ -4,7 +4,7 @@ namespace CSharpier.Tests.Cli.Options; -public class EditorConfigFileParserTests +public class CSharpierConfigParserTests { [Test] public void Should_Parse_Yaml_With_Overrides() diff --git a/Src/CSharpier.Tests/OptionsProviderTests.cs b/Src/CSharpier.Tests/OptionsProviderTests.cs index 23ca9259e..b7e946150 100644 --- a/Src/CSharpier.Tests/OptionsProviderTests.cs +++ b/Src/CSharpier.Tests/OptionsProviderTests.cs @@ -97,16 +97,20 @@ public async Task Should_Return_Json_Extension_Options() var context = new TestContext(); context.WhenAFileExists( "c:/test/.csharpierrc.json", - @"{ - ""printWidth"": 10, - ""endOfLine"": ""crlf"" -}" + """ +{ + "printWidth": 10, + "endOfLine": "crlf", + "xmlWhitespaceSensitivity": "xaml" +} +""" ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.Width.Should().Be(10); result.EndOfLine.Should().Be(EndOfLine.CRLF); + result.XmlWhitespaceSensitivity.Should().Be(XmlWhitespaceSensitivity.Xaml); } [Test] @@ -117,16 +121,18 @@ public async Task Should_Return_Yaml_Extension_Options(string extension) var context = new TestContext(); context.WhenAFileExists( $"c:/test/.csharpierrc.{extension}", - @" + """ printWidth: 10 endOfLine: crlf -" +xmlWhitespaceSensitivity: xaml +""" ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); result.Width.Should().Be(10); result.EndOfLine.Should().Be(EndOfLine.CRLF); + result.XmlWhitespaceSensitivity.Should().Be(XmlWhitespaceSensitivity.Xaml); } [Test] @@ -251,11 +257,12 @@ public async Task Should_Return_IndentSize_For_Override_With_Yaml() context.WhenAFileExists( "c:/test/.csharpierrc", """ - overrides: - - files: "*.{override,another}" - formatter: "csharp" - indentSize: 2 - """ +overrides: + - files: "*.{override,another}" + formatter: "csharp" + indentSize: 2 + xmlWhitespaceSensitivity: ignore +""" ); var result = await context.CreateProviderAndGetOptionsFor( @@ -264,6 +271,7 @@ public async Task Should_Return_IndentSize_For_Override_With_Yaml() ); result.IndentSize.Should().Be(2); + result.XmlWhitespaceSensitivity.Should().Be(XmlWhitespaceSensitivity.Ignore); } [Test] @@ -273,16 +281,17 @@ public async Task Should_Return_IndentSize_For_Override_With_Json() context.WhenAFileExists( "c:/test/.csharpierrc", """ - { - "overrides": [ - { - "files": "*.{override,another}", - "formatter": "csharp", - "indentSize": 2 - } - ] - } - """ +{ + "overrides": [ + { + "files": "*.{override,another}", + "formatter": "csharp", + "indentSize": 2, + "xmlWhitespaceSensitivity": "ignore" + } + ] +} +""" ); var result = await context.CreateProviderAndGetOptionsFor( @@ -291,6 +300,7 @@ public async Task Should_Return_IndentSize_For_Override_With_Json() ); result.IndentSize.Should().Be(2); + result.XmlWhitespaceSensitivity.Should().Be(XmlWhitespaceSensitivity.Ignore); } [Test] @@ -332,13 +342,15 @@ public async Task Should_Support_EditorConfig_Basic() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[*] -indent_style = space -indent_size = 2 -max_line_length = 10 -end_of_line = crlf -" + """ + + [*] + indent_style = space + indent_size = 2 + max_line_length = 10 + end_of_line = crlf + csharpier_xml_whitespace_sensitivity = xaml + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -347,6 +359,7 @@ public async Task Should_Support_EditorConfig_Basic() result.IndentSize.Should().Be(2); result.Width.Should().Be(10); result.EndOfLine.Should().Be(EndOfLine.CRLF); + result.XmlWhitespaceSensitivity.Should().Be(XmlWhitespaceSensitivity.Xaml); } [Test] @@ -355,19 +368,21 @@ public async Task Should_Support_EditorConfig_With_Comments() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -# EditorConfig is awesome: https://EditorConfig.org + """ -# top-most EditorConfig file -root = true + # EditorConfig is awesome: https://EditorConfig.org -[*] -indent_style = space -indent_size = 2 -max_line_length = 10 -; Windows-style line endings -end_of_line = crlf -" + # top-most EditorConfig file + root = true + + [*] + indent_style = space + indent_size = 2 + max_line_length = 10 + ; Windows-style line endings + end_of_line = crlf + + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -384,13 +399,15 @@ public async Task Should_Support_EditorConfig_With_Duplicated_Sections() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[*] -indent_size = 2 + """ -[*] -indent_size = 4 -" + [*] + indent_size = 2 + + [*] + indent_size = 4 + + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -404,11 +421,13 @@ public async Task Should_Support_EditorConfig_With_Duplicated_Rules() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[*] -indent_size = 2 -indent_size = 4 -" + """ + + [*] + indent_size = 2 + indent_size = 4 + + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -422,10 +441,12 @@ public async Task Should_Not_Fail_With_Bad_EditorConfig() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[* -indent_size== -" + """ + + [* + indent_size== + + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -441,11 +462,13 @@ public async Task Should_Support_EditorConfig_Tabs(string propertyName) var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - $@" - [*] - indent_style = tab - {propertyName} = 2 - " + $""" + + [*] + indent_style = tab + {propertyName} = 2 + + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -460,12 +483,14 @@ public async Task Should_Support_EditorConfig_Tabs_With_Tab_Width() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" - [*] - indent_style = tab - indent_size = 1 - tab_width = 3 - " + """ + + [*] + indent_style = tab + indent_size = 1 + tab_width = 3 + + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -480,11 +505,13 @@ public async Task Should_Support_EditorConfig_Tabs_With_Indent_Size_Tab() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" - [*] - indent_size = tab - tab_width = 3 - " + """ + + [*] + indent_size = tab + tab_width = 3 + + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -498,19 +525,23 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files() var context = new TestContext(); context.WhenAFileExists( "c:/test/subfolder/.editorconfig", - @" - [*] - indent_size = 1 - " + """ + + [*] + indent_size = 1 + + """ ); context.WhenAFileExists( "c:/test/.editorconfig", - @" - [*] - indent_size = 2 - max_line_length = 10 - " + """ + + [*] + indent_size = 2 + max_line_length = 10 + + """ ); var result = await context.CreateProviderAndGetOptionsFor( @@ -527,18 +558,22 @@ public async Task Should_Support_EditorConfig_Tabs_With_Multiple_Files_And_Unset var context = new TestContext(); context.WhenAFileExists( "c:/test/subfolder/.editorconfig", - @" - [*] - indent_size = unset - " + """ + + [*] + indent_size = unset + + """ ); context.WhenAFileExists( "c:/test/.editorconfig", - @" - [*] - indent_size = 2 - " + """ + + [*] + indent_size = 2 + + """ ); var result = await context.CreateProviderAndGetOptionsFor( @@ -554,20 +589,24 @@ public async Task Should_Support_EditorConfig_Root() var context = new TestContext(); context.WhenAFileExists( "c:/test/subfolder/.editorconfig", - @" - root = true + """ - [*] - indent_size = 2 - " + root = true + + [*] + indent_size = 2 + + """ ); context.WhenAFileExists( "c:/test/.editorconfig", - @" - [*] - max_line_length = 2 - " + """ + + [*] + max_line_length = 2 + + """ ); var result = await context.CreateProviderAndGetOptionsFor( @@ -583,13 +622,15 @@ public async Task Should_Support_EditorConfig_Tabs_With_Globs() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[*] -indent_size = 1 + """ + + [*] + indent_size = 1 + + [*.cs] + indent_size = 2 -[*.cs] -indent_size = 2 -" + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -602,13 +643,15 @@ public async Task Should_Support_EditorConfig_Tabs_With_Glob_Braces() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[*] -indent_size = 1 + """ + + [*] + indent_size = 1 -[*.{cs}] -indent_size = 2 -" + [*.{cs}] + indent_size = 2 + + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -621,13 +664,15 @@ public async Task Should_Support_EditorConfig_Tabs_With_Glob_Braces_Multiples() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[*] -indent_size = 1 + """ + + [*] + indent_size = 1 + + [*.{csx,cs}] + indent_size = 2 -[*.{csx,cs}] -indent_size = 2 -" + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -635,7 +680,7 @@ public async Task Should_Support_EditorConfig_Tabs_With_Glob_Braces_Multiples() } [Test] - public async Task Should_Return_IndentSize_For_Formatter_In_Editorconfig() + public async Task Should_Return_Overrides_In_Editorconfig() { var context = new TestContext(); context.WhenAFileExists( @@ -644,12 +689,14 @@ public async Task Should_Return_IndentSize_For_Formatter_In_Editorconfig() [*.cst] indent_size = 2 csharpier_formatter = csharp + csharpier_xml_whitespace_sensitivity = ignore """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cst"); result.IndentSize.Should().Be(2); + result.XmlWhitespaceSensitivity.Should().Be(XmlWhitespaceSensitivity.Ignore); } [Test] @@ -658,10 +705,12 @@ public async Task Should_Find_EditorConfig_In_Parent_Directory() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[*.cs] -indent_size = 2 -" + """ + + [*.cs] + indent_size = 2 + + """ ); var result = await context.CreateProviderAndGetOptionsFor( @@ -677,10 +726,12 @@ public async Task Should_Prefer_CSharpierrc_In_SameFolder() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[*.cs] -indent_size = 2 -" + """ + + [*.cs] + indent_size = 2 + + """ ); context.WhenAFileExists("c:/test/.csharpierrc", "indentSize: 1"); @@ -694,10 +745,12 @@ public async Task Should_Not_Prefer_Closer_EditorConfig() var context = new TestContext(); context.WhenAFileExists( "c:/test/subfolder/.editorconfig", - @" -[*.cs] -indent_size = 2 -" + """ + + [*.cs] + indent_size = 2 + + """ ); context.WhenAFileExists("c:/test/.csharpierrc", "indentSize: 1"); @@ -714,11 +767,13 @@ public async Task Should_Ignore_Invalid_EditorConfig_Lines() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[*] -indent_size = 2 -INVALID -" + """ + + [*] + indent_size = 2 + INVALID + + """ ); var result = await context.CreateProviderAndGetOptionsFor("c:/test", "c:/test/test.cs"); @@ -732,18 +787,22 @@ public async Task Should_Not_Ignore_Ignored_EditorConfig() var context = new TestContext(); context.WhenAFileExists( "c:/test/subfolder/.editorconfig", - @" - [*] - indent_size = 2 - " + """ + + [*] + indent_size = 2 + + """ ); context.WhenAFileExists( "c:/test/.editorconfig", - @" - [*] - indent_size = 1 - " + """ + + [*] + indent_size = 1 + + """ ); context.WhenAFileExists("c:/test/.csharpierignore", "/subfolder/.editorconfig"); @@ -761,10 +820,12 @@ public async Task Should_Prefer_Closer_CSharpierrc() var context = new TestContext(); context.WhenAFileExists( "c:/test/.editorconfig", - @" -[*.cs] -indent_size = 2 -" + """ + + [*.cs] + indent_size = 2 + + """ ); context.WhenAFileExists("c:/test/subfolder/.csharpierrc", "indentSize: 1"); @@ -789,6 +850,7 @@ private static void ShouldHaveDefaultXmlOptions(PrinterOptions printerOptions) printerOptions.IndentSize.Should().Be(2); printerOptions.UseTabs.Should().BeFalse(); printerOptions.EndOfLine.Should().Be(EndOfLine.Auto); + printerOptions.XmlWhitespaceSensitivity.Should().Be(XmlWhitespaceSensitivity.Strict); } private sealed class TestContext