diff --git a/Src/CSharpier.Benchmarks/Program.cs b/Src/CSharpier.Benchmarks/Program.cs index 714fd5d8c..124989fde 100644 --- a/Src/CSharpier.Benchmarks/Program.cs +++ b/Src/CSharpier.Benchmarks/Program.cs @@ -21,6 +21,7 @@ public void Default_SyntaxNodeComparer() this.code, this.code, false, + false, CancellationToken.None ); syntaxNodeComparer.CompareSource(); diff --git a/Src/CSharpier.Cli/CommandLineFormatter.cs b/Src/CSharpier.Cli/CommandLineFormatter.cs index ef3409be9..7b397496b 100644 --- a/Src/CSharpier.Cli/CommandLineFormatter.cs +++ b/Src/CSharpier.Cli/CommandLineFormatter.cs @@ -397,7 +397,8 @@ CancellationToken cancellationToken var syntaxNodeComparer = new SyntaxNodeComparer( fileToFormatInfo.FileContents, codeFormattingResult.Code, - codeFormattingResult.IgnoreDisabledText, + codeFormattingResult.ReorderedModifiers, + codeFormattingResult.ReorderedUsingsWithDisabledText, cancellationToken ); diff --git a/Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs b/Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs index df5915cb8..6583a68be 100644 --- a/Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs +++ b/Src/CSharpier.Cli/EditorConfig/ConfigFileParser.cs @@ -13,7 +13,7 @@ internal static class ConfigFileParser private static readonly Regex CommentRegex = new("^[;#].*$"); private static readonly IniParserConfiguration Configuration = - new() { CommentRegex = CommentRegex, }; + new() { CommentRegex = CommentRegex, AllowDuplicateKeys = true }; public static ConfigFile Parse(string filePath, IFileSystem fileSystem) { diff --git a/Src/CSharpier.FakeGenerators/SyntaxNodeComparerGenerator.cs b/Src/CSharpier.FakeGenerators/SyntaxNodeComparerGenerator.cs index d428dfbab..fc6570a7f 100644 --- a/Src/CSharpier.FakeGenerators/SyntaxNodeComparerGenerator.cs +++ b/Src/CSharpier.FakeGenerators/SyntaxNodeComparerGenerator.cs @@ -19,7 +19,8 @@ private static string GenerateSource() { var sourceBuilder = new StringBuilder(); sourceBuilder.AppendLine( - @"#pragma warning disable CS0168 + """ +#pragma warning disable CS0168 using System; using System.Linq; using Microsoft.CodeAnalysis; @@ -58,7 +59,8 @@ private CompareResult Compare( } switch (originalNode) - {" + { +""" ); var syntaxNodeTypes = ValidNodeTypes.Get(); @@ -67,22 +69,41 @@ private CompareResult Compare( { var lowerCaseName = syntaxNodeType.Name[0].ToString().ToLower() + syntaxNodeType.Name[1..]; - sourceBuilder.AppendLine( - $@" case {syntaxNodeType.Name} {lowerCaseName}: - return this.Compare{syntaxNodeType.Name}({lowerCaseName}, formattedNode as {syntaxNodeType.Name});" - ); + + if (syntaxNodeType == typeof(UsingDirectiveSyntax)) + { + sourceBuilder.AppendLine( + $""" + case {syntaxNodeType.Name} {lowerCaseName}: + if (this.IgnoreDisabledText) + return Equal; + return this.Compare{syntaxNodeType.Name}({lowerCaseName}, formattedNode as {syntaxNodeType.Name}); +""" + ); + } + else + { + sourceBuilder.AppendLine( + $""" + case {syntaxNodeType.Name} {lowerCaseName}: + return this.Compare{syntaxNodeType.Name}({lowerCaseName}, formattedNode as {syntaxNodeType.Name}); +""" + ); + } } sourceBuilder.AppendLine( - @" default: + """ + default: #if DEBUG - throw new Exception(""Can't handle "" + originalNode.GetType().Name); + throw new Exception("Can't handle " + originalNode.GetType().Name); #else return Equal; #endif } } - " + +""" ); foreach (var syntaxNodeType in syntaxNodeTypes) @@ -101,9 +122,11 @@ private CompareResult Compare( private static void GenerateMethod(StringBuilder sourceBuilder, Type type) { sourceBuilder.AppendLine( - @$" private CompareResult Compare{type.Name}({type.Name} originalNode, {type.Name} formattedNode) - {{ - CompareResult result;" + $$""" + private CompareResult Compare{{type.Name}}({{type.Name}} originalNode, {{type.Name}} formattedNode) + { + CompareResult result; +""" ); foreach (var propertyInfo in type.GetProperties().OrderBy(o => o.Name)) diff --git a/Src/CSharpier.FakeGenerators/ValidNodeTypes.cs b/Src/CSharpier.FakeGenerators/ValidNodeTypes.cs index d08545e32..b7d674421 100644 --- a/Src/CSharpier.FakeGenerators/ValidNodeTypes.cs +++ b/Src/CSharpier.FakeGenerators/ValidNodeTypes.cs @@ -5,7 +5,7 @@ namespace CSharpier.FakeGenerators; public static class ValidNodeTypes { - public static IEnumerable Get() + public static IList Get() { return typeof(CompilationUnitSyntax).Assembly .GetTypes() diff --git a/Src/CSharpier.Playground/ClientApp/src/AppContext.ts b/Src/CSharpier.Playground/ClientApp/src/AppContext.ts index 226f999ed..9244e3dee 100644 --- a/Src/CSharpier.Playground/ClientApp/src/AppContext.ts +++ b/Src/CSharpier.Playground/ClientApp/src/AppContext.ts @@ -75,7 +75,12 @@ export const useSetupAppContext = () => { formatCode: async () => { setIsLoading(true); - const { syntaxTree, formattedCode, doc, hasErrors } = await formatCode(enteredCode); + const { syntaxTree, formattedCode, doc, hasErrors, syntaxValidation } = await formatCode(enteredCode); + + if (syntaxValidation) { + console.log(syntaxValidation); + console.log(new Date()); + } setIsLoading(false); setSyntaxTree(syntaxTree); diff --git a/Src/CSharpier.Playground/ClientApp/src/FormatCode.ts b/Src/CSharpier.Playground/ClientApp/src/FormatCode.ts index f38347493..a1be5fc0e 100644 --- a/Src/CSharpier.Playground/ClientApp/src/FormatCode.ts +++ b/Src/CSharpier.Playground/ClientApp/src/FormatCode.ts @@ -24,6 +24,7 @@ export const formatCode = async (code: string) => { formattedCode: data.code, doc: data.doc, hasErrors: !!data.errors.length, + syntaxValidation: data.syntaxValidation }; } else { const text = await response.text(); diff --git a/Src/CSharpier.Playground/Controllers/FormatController.cs b/Src/CSharpier.Playground/Controllers/FormatController.cs index e0903dbf0..371c45f18 100644 --- a/Src/CSharpier.Playground/Controllers/FormatController.cs +++ b/Src/CSharpier.Playground/Controllers/FormatController.cs @@ -15,6 +15,7 @@ public class FormatResult public required string Json { get; set; } public required string Doc { get; set; } public required List Errors { get; set; } + public required string SyntaxValidation { get; set; } } public class FormatError @@ -53,12 +54,21 @@ public async Task Post([FromBody] string content) } ); + var comparer = new SyntaxNodeComparer( + content, + result.Code, + result.ReorderedModifiers, + result.ReorderedUsingsWithDisabledText, + CancellationToken.None + ); + return new FormatResult { Code = result.Code, Json = result.AST, Doc = result.DocTree, Errors = result.CompilationErrors.Select(this.ConvertError).ToList(), + SyntaxValidation = await comparer.CompareSourceAsync(CancellationToken.None) }; } diff --git a/Src/CSharpier.Tests/FormattingTests/BaseTest.cs b/Src/CSharpier.Tests/FormattingTests/BaseTest.cs index 4ea67a63c..678206f21 100644 --- a/Src/CSharpier.Tests/FormattingTests/BaseTest.cs +++ b/Src/CSharpier.Tests/FormattingTests/BaseTest.cs @@ -59,6 +59,7 @@ protected async Task RunTest(string fileName, string fileExtension, bool useTabs expectedCode, normalizedCode, false, + false, CancellationToken.None ); diff --git a/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/UsingDirectives_EdgeCase10.test b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/UsingDirectives_EdgeCase10.test new file mode 100644 index 000000000..22b3f4d07 --- /dev/null +++ b/Src/CSharpier.Tests/FormattingTests/TestFiles/cs/UsingDirectives_EdgeCase10.test @@ -0,0 +1,10 @@ +#if DEBUG +namespace Namespace; + +using System.Runtime.Remoting; +using System; + +class ClassName { } + +#endif + diff --git a/Src/CSharpier.Tests/Samples/Samples.cs b/Src/CSharpier.Tests/Samples/Samples.cs index 3cc5e1126..b1086835d 100644 --- a/Src/CSharpier.Tests/Samples/Samples.cs +++ b/Src/CSharpier.Tests/Samples/Samples.cs @@ -40,6 +40,7 @@ public static async Task RunTest(string fileName) code, result.Code, false, + false, CancellationToken.None ); diff --git a/Src/CSharpier.Tests/SyntaxNodeComparerTests.cs b/Src/CSharpier.Tests/SyntaxNodeComparerTests.cs index a43b1e68e..9535f8222 100644 --- a/Src/CSharpier.Tests/SyntaxNodeComparerTests.cs +++ b/Src/CSharpier.Tests/SyntaxNodeComparerTests.cs @@ -492,7 +492,7 @@ public void Usings_With_Directives_Pass_Validation(string content) {content} "; - var result = CompareSource(left, right, ignoreDisabledText: true); + var result = CompareSource(left, right, reorderedUsingsWithDisabledText: true); result.Should().BeEmpty(); } @@ -507,12 +507,17 @@ private static void ResultShouldBe(string result, string be) result.Should().Be(be); } - private static string CompareSource(string left, string right, bool ignoreDisabledText = false) + private static string CompareSource( + string left, + string right, + bool reorderedUsingsWithDisabledText = false + ) { var result = new SyntaxNodeComparer( left, right, - ignoreDisabledText, + false, + reorderedUsingsWithDisabledText, CancellationToken.None ).CompareSource(); diff --git a/Src/CSharpier/CSharpFormatter.cs b/Src/CSharpier/CSharpFormatter.cs index f7c534d12..5d49fb129 100644 --- a/Src/CSharpier/CSharpFormatter.cs +++ b/Src/CSharpier/CSharpFormatter.cs @@ -107,7 +107,8 @@ bool TryGetCompilationFailure(out CodeFormatterResult compilationResult) var formattingContext = new FormattingContext { LineEnding = lineEnding }; var document = Node.Print(rootNode, formattingContext); var formattedCode = DocPrinter.DocPrinter.Print(document, printerOptions, lineEnding); - var ignoreDisabledText = formattingContext.IgnoreDisabledText; + var reorderedModifiers = formattingContext.ReorderedModifiers; + var reorderedUsingsWithDisabledText = formattingContext.ReorderedUsingsWithDisabledText; foreach (var symbolSet in PreprocessorSymbols.GetSets(syntaxTree)) { @@ -124,7 +125,8 @@ await syntaxTree.GetRootAsync(cancellationToken), formattingContext2 ); formattedCode = DocPrinter.DocPrinter.Print(document, printerOptions, lineEnding); - ignoreDisabledText = ignoreDisabledText || formattingContext2.IgnoreDisabledText; + reorderedModifiers = reorderedModifiers || formattingContext2.ReorderedModifiers; + reorderedUsingsWithDisabledText = reorderedUsingsWithDisabledText || formattingContext2.ReorderedUsingsWithDisabledText; } return new CodeFormatterResult @@ -134,7 +136,8 @@ await syntaxTree.GetRootAsync(cancellationToken), ? DocSerializer.Serialize(document) : string.Empty, AST = printerOptions.IncludeAST ? PrintAST(rootNode) : string.Empty, - IgnoreDisabledText = ignoreDisabledText + ReorderedModifiers = reorderedModifiers, + ReorderedUsingsWithDisabledText = reorderedUsingsWithDisabledText, }; } catch (InTooDeepException) diff --git a/Src/CSharpier/CodeFormatterResult.cs b/Src/CSharpier/CodeFormatterResult.cs index 3eca17be0..9baea0e02 100644 --- a/Src/CSharpier/CodeFormatterResult.cs +++ b/Src/CSharpier/CodeFormatterResult.cs @@ -14,5 +14,6 @@ internal CodeFormatterResult() { } internal static readonly CodeFormatterResult Null = new(); - internal bool IgnoreDisabledText { get; init; } + internal bool ReorderedModifiers { get; init; } + internal bool ReorderedUsingsWithDisabledText { get; init; } } diff --git a/Src/CSharpier/SyntaxNodeComparer.cs b/Src/CSharpier/SyntaxNodeComparer.cs index 2b3cb437f..eb0151b10 100644 --- a/Src/CSharpier/SyntaxNodeComparer.cs +++ b/Src/CSharpier/SyntaxNodeComparer.cs @@ -8,20 +8,23 @@ internal partial class SyntaxNodeComparer protected string NewSourceCode { get; } protected SyntaxTree OriginalSyntaxTree { get; } protected SyntaxTree NewSyntaxTree { get; } - protected bool IgnoreDisabledText { get; } + protected bool ReorderedModifiers { get; } + protected bool ReorderedUsingsWithDisabledText { get; } private static readonly CompareResult Equal = new(); public SyntaxNodeComparer( string originalSourceCode, string newSourceCode, - bool ignoreDisabledText, + bool reorderedModifiers, + bool reorderedUsingsWithDisabledText, CancellationToken cancellationToken ) { this.OriginalSourceCode = originalSourceCode; this.NewSourceCode = newSourceCode; - this.IgnoreDisabledText = ignoreDisabledText; + this.ReorderedModifiers = reorderedModifiers; + this.ReorderedUsingsWithDisabledText = reorderedUsingsWithDisabledText; var cSharpParseOptions = new CSharpParseOptions(CSharpFormatter.LanguageVersion); this.OriginalSyntaxTree = CSharpSyntaxTree.ParseText( @@ -43,6 +46,14 @@ public string CompareSource() public async Task CompareSourceAsync(CancellationToken cancellationToken) { + // this seems almost impossible to figure out with the current way this is written + // the usings could be in disabled text on namespaces, or on the modifiers of any base types. + // parts of the #if or #endif could be leading trivia in different places + if (this.ReorderedUsingsWithDisabledText) + { + return string.Empty; + } + var result = this.AreEqualIgnoringWhitespace( await this.OriginalSyntaxTree.GetRootAsync(cancellationToken), await this.NewSyntaxTree.GetRootAsync(cancellationToken) @@ -199,7 +210,7 @@ private CompareResult Compare( ) { if ( - this.IgnoreDisabledText + this.ReorderedModifiers && ( ( formattedNode is NamespaceDeclarationSyntax nd @@ -249,7 +260,7 @@ private CompareResult Compare(SyntaxTrivia originalTrivia, SyntaxTrivia formatte { if (originalTrivia.RawSyntaxKind() is SyntaxKind.DisabledTextTrivia) { - if (this.IgnoreDisabledText) + if (this.ReorderedModifiers) { return Equal; } diff --git a/Src/CSharpier/SyntaxNodeComparer.generated.cs b/Src/CSharpier/SyntaxNodeComparer.generated.cs index 522dcb733..d098d25ce 100644 --- a/Src/CSharpier/SyntaxNodeComparer.generated.cs +++ b/Src/CSharpier/SyntaxNodeComparer.generated.cs @@ -473,6 +473,8 @@ private CompareResult Compare( case UnsafeStatementSyntax unsafeStatementSyntax: return this.CompareUnsafeStatementSyntax(unsafeStatementSyntax, formattedNode as UnsafeStatementSyntax); case UsingDirectiveSyntax usingDirectiveSyntax: + if (this.ReorderedModifiers) + return Equal; return this.CompareUsingDirectiveSyntax(usingDirectiveSyntax, formattedNode as UsingDirectiveSyntax); case UsingStatementSyntax usingStatementSyntax: return this.CompareUsingStatementSyntax(usingStatementSyntax, formattedNode as UsingStatementSyntax); diff --git a/Src/CSharpier/SyntaxPrinter/FormattingContext.cs b/Src/CSharpier/SyntaxPrinter/FormattingContext.cs index a103d29c5..6708314ce 100644 --- a/Src/CSharpier/SyntaxPrinter/FormattingContext.cs +++ b/Src/CSharpier/SyntaxPrinter/FormattingContext.cs @@ -9,7 +9,8 @@ internal class FormattingContext // we need to keep track if we reordered modifiers because when modifiers are moved inside // of an #if, then we can't compare the before and after disabled text in the source file - // we also need to keep track if we move around usings, because then the disabled text may end up on - // the first node after the usings, like namespace or class declaration - public bool IgnoreDisabledText { get; set; } + public bool ReorderedModifiers { get; set; } + + // we also need to keep track if we move around usings with disabledText + public bool ReorderedUsingsWithDisabledText { get; set; } } diff --git a/Src/CSharpier/SyntaxPrinter/Modifiers.cs b/Src/CSharpier/SyntaxPrinter/Modifiers.cs index 79b2ef219..290ae595e 100644 --- a/Src/CSharpier/SyntaxPrinter/Modifiers.cs +++ b/Src/CSharpier/SyntaxPrinter/Modifiers.cs @@ -101,7 +101,7 @@ Func, Doc> print && sortedModifiers.Zip(modifiers, (original, sorted) => original != sorted).Any() ) { - context.IgnoreDisabledText = true; + context.ReorderedModifiers = true; } return print(sortedModifiers.ToArray()); diff --git a/Src/CSharpier/SyntaxPrinter/UsingDirectives.cs b/Src/CSharpier/SyntaxPrinter/UsingDirectives.cs index 18109c67a..3bbf0e5b9 100644 --- a/Src/CSharpier/SyntaxPrinter/UsingDirectives.cs +++ b/Src/CSharpier/SyntaxPrinter/UsingDirectives.cs @@ -120,9 +120,9 @@ var groupOfUsingData in GroupUsings( } } - if (reorderedDirectives) + if (reorderedDirectives && usings.Any(o => o.ToFullString().Contains("#endif"))) { - context.IgnoreDisabledText = true; + context.ReorderedUsingsWithDisabledText = true; } return Doc.Concat(docs);