diff --git a/docs/reference/index.md b/docs/reference/index.md index a332fac..ef00970 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -67,7 +67,7 @@ Use the filter above to search by name. Each group below links to its own index. - [csharp_preserve_single_line_blocks](wrapping/csharp_preserve_single_line_blocks.md) — Allow a block the author wrote on one line to stay on one line. - [csharp_empty_block_style](wrapping/csharp_empty_block_style.md) — How an empty method, constructor, accessor or control-flow body is laid out. together_same_line keeps { } on the signature's line. -- [csharp_preserve_single_line_statements](wrapping/csharp_preserve_single_line_statements.md) — Allow a control-flow body the author left on its header's line to stay there. +- [csharp_preserve_single_line_statements](wrapping/csharp_preserve_single_line_statements.md) — Allow a control-flow body the author left on its header's line to stay there. Chained using statements are the exception and take their own lines unless this key is written out and set to true. - [csharp_keep_existing_linebreaks](wrapping/csharp_keep_existing_linebreaks.md) — Preservation mode. Defaults to true when max_line_length is off. - [csharp_wrap_before_first_method_call](wrapping/csharp_wrap_before_first_method_call.md) — When a chain breaks, break before the first call too, leaving the receiver on its own line. - [csharp_wrap_before_first_type_parameter_constraint](wrapping/csharp_wrap_before_first_type_parameter_constraint.md) — Put a where clause on its own line. diff --git a/docs/reference/wrapping/csharp_preserve_single_line_statements.md b/docs/reference/wrapping/csharp_preserve_single_line_statements.md index 93d2464..856979e 100644 --- a/docs/reference/wrapping/csharp_preserve_single_line_statements.md +++ b/docs/reference/wrapping/csharp_preserve_single_line_statements.md @@ -1,13 +1,13 @@ --- listing: wrapping -description: "Allow a control-flow body the author left on its header's line to stay there. Defaults to `true`" +description: "Allow a control-flow body the author left on its header's line to stay there. Chained using statements are the exception and take their own lines unless this key is written out and set to true. Defaults to `true`" --- # `csharp_preserve_single_line_statements` -Allow a control-flow body the author left on its header's line to stay there. +Allow a control-flow body the author left on its header's line to stay there. Chained using statements are the exception and take their own lines unless this key is written out and set to true. ## Values diff --git a/src/Nullean.Curb.Core/FormatOptions.cs b/src/Nullean.Curb.Core/FormatOptions.cs index 72c9e32..5cdba4c 100644 --- a/src/Nullean.Curb.Core/FormatOptions.cs +++ b/src/Nullean.Curb.Core/FormatOptions.cs @@ -415,7 +415,19 @@ public FormatOptions() { } /// member, type, namespace, enum and switch bodies. A one-line if (a) { return; } is /// therefore kept by this option even with the block option off. /// - public bool PreserveSingleLineStatements { get; init; } = true; + public bool PreserveSingleLineStatements => PreserveSingleLineStatementsOption ?? true; + + /// + /// csharp_preserve_single_line_statements exactly as written, or null when it was not mentioned. + /// + /// + /// Kept separately from because one shape has to tell an + /// absent key apart from an explicit true: a chain of using statements. Joining those is a + /// rewrite nobody asked for, so an unset key leaves them on their own lines, and only somebody who + /// has written the option out gets the author's joining honoured. Everything else the option governs + /// reads the resolved value and so still defaults to preservation. + /// + public bool? PreserveSingleLineStatementsOption { get; init; } /// /// csharp_keep_existing_linebreaks exactly as written, or null when it was not mentioned. diff --git a/src/Nullean.Curb.Core/Options/OptionDescriptors.cs b/src/Nullean.Curb.Core/Options/OptionDescriptors.cs index 8a686d9..8e3b681 100644 --- a/src/Nullean.Curb.Core/Options/OptionDescriptors.cs +++ b/src/Nullean.Curb.Core/Options/OptionDescriptors.cs @@ -104,7 +104,7 @@ private static OptionDescriptor[] Build() => // ---- Wrapping (28) ------------------------------------------------------------------ new("csharp_preserve_single_line_blocks", OptionGroup.Wrapping, ["true", "false"], "true", "Allow a block the author wrote on one line to stay on one line.", "single_line_block"), new("csharp_empty_block_style", OptionGroup.Wrapping, ["multiline", "together", "together_same_line"], "(not set)", "How an empty method, constructor, accessor or control-flow body is laid out. together_same_line keeps { } on the signature's line.", "single_line_block"), - new("csharp_preserve_single_line_statements", OptionGroup.Wrapping, ["true", "false"], "true", "Allow a control-flow body the author left on its header's line to stay there.", "if_else"), + new("csharp_preserve_single_line_statements", OptionGroup.Wrapping, ["true", "false"], "true", "Allow a control-flow body the author left on its header's line to stay there. Chained using statements are the exception and take their own lines unless this key is written out and set to true.", "if_else"), new("csharp_keep_existing_linebreaks", OptionGroup.Wrapping, ["true", "false"], "(derived from max_line_length)", "Preservation mode. Defaults to true when max_line_length is off.", "long_chain"), new("csharp_wrap_before_first_method_call", OptionGroup.Wrapping, ["true", "false"], "(not set)", "When a chain breaks, break before the first call too, leaving the receiver on its own line.", "long_chain"), new("csharp_wrap_before_first_type_parameter_constraint", OptionGroup.Wrapping, ["true", "false"], "false", "Put a where clause on its own line.", "generic_constraint"), diff --git a/src/Nullean.Curb.Core/Printing/CSharp/Printers.Statements.cs b/src/Nullean.Curb.Core/Printing/CSharp/Printers.Statements.cs index e2cdecf..4547cf1 100644 --- a/src/Nullean.Curb.Core/Printing/CSharp/Printers.Statements.cs +++ b/src/Nullean.Curb.Core/Printing/CSharp/Printers.Statements.cs @@ -471,10 +471,25 @@ public static void UsingStatement(UsingStatementSyntax node, PrintContext contex Spacing.InsideControlFlowParens(context); TokenPrinter.Print(node.CloseParenToken, context); - // Chained `using (a) using (b) { }` keeps the inner using on the same line. - if (node.Statement is UsingStatementSyntax) + // Chained `using (a) using (b) { }` is a sequence, not a nesting: every using in the chain guards the + // same block, and none of them is the body of the one before it. So each takes its own line at the + // same indent unless somebody has asked, in as many words, for the author's line to be kept. + // + // That asking is csharp_preserve_single_line_statements written out and set to true, which is why this + // reads the raw option rather than the resolved one. Previously the chain joined unconditionally, so + // an explicit false was ignored (issue #89). Resolving the option would fix that much and still leave + // the larger half wrong: it defaults to true, so a repository with no .editorconfig — or one that + // never mentions this key — would go on having its usings pulled onto one line, which is a rewrite + // nobody asked for and not the shape IDE0055 expects. An absent key therefore splits, an explicit + // false splits, and only an explicit true hands the decision back to the author's own line breaks. + if (node.Statement is UsingStatementSyntax chained) { - arena.Synthetic(SyntheticText.Space); + if (context.Options.PreserveSingleLineStatementsOption == true + && context.AuthorJoined(node.CloseParenToken.Span.End, chained.SpanStart)) + arena.Synthetic(SyntheticText.Space); + else + arena.HardLine(); + Node.Print(node.Statement, context); return; } diff --git a/src/Nullean.Curb.EditorConfig/EditorConfigOptionsBinder.cs b/src/Nullean.Curb.EditorConfig/EditorConfigOptionsBinder.cs index 3963913..9fcc489 100644 --- a/src/Nullean.Curb.EditorConfig/EditorConfigOptionsBinder.cs +++ b/src/Nullean.Curb.EditorConfig/EditorConfigOptionsBinder.cs @@ -585,7 +585,7 @@ public static FormatOptions Bind(FileConfiguration configuration, ICollection Unchanged( + public Task Chained_using_statements_are_split_even_when_the_author_joined_them() => Formats( + // A chain is a sequence: both usings guard the same block and neither is the body of the other, so + // each gets its own line at the same indent. Nothing is configured here, which is the case that + // matters — joining is a rewrite, and an absent key is nobody having asked for one. """ public class C { @@ -433,6 +436,87 @@ public void M() } } } + """, + """ + public class C + { + public void M() + { + using (var first = Open()) + using (var second = Open()) + { + Call(); + } + } + } + """); + + [Test] + public Task Chained_using_statements_the_author_split_stay_split() => Unchanged( + """ + public class C + { + public void M() + { + using (var first = Open()) + using (var second = Open()) + { + Call(); + } + } + } + """); + + [Test] + public Task Chained_using_statements_stay_joined_when_the_option_is_written_out() => Unchanged( + // The one way back to the old behaviour. Splitting is what an absent key does, not an opinion Curb + // holds over the author's head: somebody who writes the option out and turns it on has asked for + // their own line to be kept, and gets it. Without this .editorconfig the same source splits, which + // is the case above. + """ + public class C + { + public void M() + { + using (var first = Open()) using (var second = Open()) + { + Call(); + } + } + } + """, + editorConfig: """ + [*.cs] + csharp_preserve_single_line_statements = true + """); + + [Test] + public Task Three_chained_usings_split_together() => Formats( + """ + public class C + { + public void M() + { + using (var a = Open()) using (var b = Open()) using (var c = Open()) + { + Call(); + } + } + } + """, + """ + public class C + { + public void M() + { + using (var a = Open()) + using (var b = Open()) + using (var c = Open()) + { + Call(); + } + } + } """); [Test] diff --git a/tools/Nullean.Curb.OptionDocs/Program.cs b/tools/Nullean.Curb.OptionDocs/Program.cs index 82d3733..59fd470 100644 --- a/tools/Nullean.Curb.OptionDocs/Program.cs +++ b/tools/Nullean.Curb.OptionDocs/Program.cs @@ -455,7 +455,7 @@ private static FormatOptions BuildOptions(OptionDescriptor d, string value) _ => (Nullean.Curb.EmptyBlockStyle?)null, } }, - "csharp_preserve_single_line_statements" => opts with { PreserveSingleLineStatements = b }, + "csharp_preserve_single_line_statements" => opts with { PreserveSingleLineStatementsOption = b }, "csharp_keep_existing_linebreaks" => opts with { KeepExistingLinebreaksOption = b }, "csharp_wrap_before_first_method_call" => opts with { WrapBeforeFirstMethodCall = b }, "csharp_wrap_before_first_type_parameter_constraint" => opts with { WrapBeforeFirstTypeParameterConstraint = b },