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
2 changes: 1 addition & 1 deletion docs/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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`"
---

<!-- generated by Nullean.Curb.OptionDocs -->

# `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

Expand Down
14 changes: 13 additions & 1 deletion src/Nullean.Curb.Core/FormatOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,19 @@ public FormatOptions() { }
/// member, type, namespace, enum and switch bodies. A one-line <c>if (a) { return; }</c> is
/// therefore kept by this option even with the block option off.
/// </remarks>
public bool PreserveSingleLineStatements { get; init; } = true;
public bool PreserveSingleLineStatements => PreserveSingleLineStatementsOption ?? true;

/// <summary>
/// <c>csharp_preserve_single_line_statements</c> exactly as written, or null when it was not mentioned.
/// </summary>
/// <remarks>
/// Kept separately from <see cref="PreserveSingleLineStatements"/> because one shape has to tell an
/// absent key apart from an explicit true: a chain of <c>using</c> 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.
/// </remarks>
public bool? PreserveSingleLineStatementsOption { get; init; }

/// <summary>
/// <c>csharp_keep_existing_linebreaks</c> exactly as written, or null when it was not mentioned.
Expand Down
2 changes: 1 addition & 1 deletion src/Nullean.Curb.Core/Options/OptionDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
21 changes: 18 additions & 3 deletions src/Nullean.Curb.Core/Printing/CSharp/Printers.Statements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Nullean.Curb.EditorConfig/EditorConfigOptionsBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ public static FormatOptions Bind(FileConfiguration configuration, ICollection<Cu
// accessor-list printer reads it as a plain option rather than through the source.
if (TryBool(properties, "csharp_preserve_single_line_statements", diagnostics, out var preserveStatements))
{
options = options with { PreserveSingleLineStatements = preserveStatements };
options = options with { PreserveSingleLineStatementsOption = preserveStatements };
ReportIfDeterministic(
"csharp_preserve_single_line_statements",
"every body, label and statement takes its own line");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,10 @@ public async Task M()
""");

[Test]
public Task Chained_using_statements_stay_on_one_line() => 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
{
Expand All @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion tools/Nullean.Curb.OptionDocs/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@
var sb = new StringBuilder();
sb.AppendLine(GroupedHeader);
sb.AppendLine();
sb.AppendLine($"# {title}");

Check warning on line 167 in tools/Nullean.Curb.OptionDocs/Program.cs

View workflow job for this annotation

GitHub Actions / Corpus and conformance

The behavior of 'StringBuilder.AppendLine(ref StringBuilder.AppendInterpolatedStringHandler)' could vary based on the current user's locale settings. Replace this call in 'Program.Main(string[])' with a call to 'StringBuilder.AppendLine(IFormatProvider, ref StringBuilder.AppendInterpolatedStringHandler)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
sb.AppendLine();
sb.AppendLine(GroupDescriptions[group]);

Expand All @@ -181,7 +181,7 @@
sb.AppendLine();
sb.AppendLine("# Option Reference");
sb.AppendLine();
sb.AppendLine($"Curb implements {OptionDescriptor.All.Count} `.editorconfig` keys. Every key has a before/after example generated by running Curb.");

Check warning on line 184 in tools/Nullean.Curb.OptionDocs/Program.cs

View workflow job for this annotation

GitHub Actions / Corpus and conformance

The behavior of 'StringBuilder.AppendLine(ref StringBuilder.AppendInterpolatedStringHandler)' could vary based on the current user's locale settings. Replace this call in 'Program.Main(string[])' with a call to 'StringBuilder.AppendLine(IFormatProvider, ref StringBuilder.AppendInterpolatedStringHandler)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
sb.AppendLine();
sb.AppendLine("Use the filter above to search by name. Each group below links to its own index.");
sb.AppendLine();
Expand All @@ -189,10 +189,10 @@
{
var slug = GroupSlugs[group];
var title = GroupTitles[group];
sb.AppendLine($"## [{title}]({slug}/index.md)");

Check warning on line 192 in tools/Nullean.Curb.OptionDocs/Program.cs

View workflow job for this annotation

GitHub Actions / Corpus and conformance

The behavior of 'StringBuilder.AppendLine(ref StringBuilder.AppendInterpolatedStringHandler)' could vary based on the current user's locale settings. Replace this call in 'Program.Main(string[])' with a call to 'StringBuilder.AppendLine(IFormatProvider, ref StringBuilder.AppendInterpolatedStringHandler)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
sb.AppendLine();
foreach (var d in descriptors)
sb.AppendLine($"- [{d.Key}]({slug}/{d.Key}.md) — {d.Summary}");

Check warning on line 195 in tools/Nullean.Curb.OptionDocs/Program.cs

View workflow job for this annotation

GitHub Actions / Corpus and conformance

The behavior of 'StringBuilder.AppendLine(ref StringBuilder.AppendInterpolatedStringHandler)' could vary based on the current user's locale settings. Replace this call in 'Program.Main(string[])' with a call to 'StringBuilder.AppendLine(IFormatProvider, ref StringBuilder.AppendInterpolatedStringHandler)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
sb.AppendLine();
}
var path = Path.Combine(outputRoot, "index.md");
Expand All @@ -214,13 +214,13 @@
var slug = GroupSlugs[descriptor.Group];
var sb = new StringBuilder();
sb.AppendLine("---");
sb.AppendLine($"listing: {slug}");

Check warning on line 217 in tools/Nullean.Curb.OptionDocs/Program.cs

View workflow job for this annotation

GitHub Actions / Corpus and conformance

The behavior of 'StringBuilder.AppendLine(ref StringBuilder.AppendInterpolatedStringHandler)' could vary based on the current user's locale settings. Replace this call in 'Program.BuildPage(OptionDescriptor, string, CSharpFormatter, FormatOptions, IEnumerable<Program.Divergence>)' with a call to 'StringBuilder.AppendLine(IFormatProvider, ref StringBuilder.AppendInterpolatedStringHandler)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
sb.AppendLine($"description: \"{descriptor.Summary} Defaults to `{descriptor.Default}`\"");

Check warning on line 218 in tools/Nullean.Curb.OptionDocs/Program.cs

View workflow job for this annotation

GitHub Actions / Corpus and conformance

The behavior of 'StringBuilder.AppendLine(ref StringBuilder.AppendInterpolatedStringHandler)' could vary based on the current user's locale settings. Replace this call in 'Program.BuildPage(OptionDescriptor, string, CSharpFormatter, FormatOptions, IEnumerable<Program.Divergence>)' with a call to 'StringBuilder.AppendLine(IFormatProvider, ref StringBuilder.AppendInterpolatedStringHandler)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
sb.AppendLine("---");
sb.AppendLine();
sb.AppendLine(GroupedHeader);
sb.AppendLine();
sb.AppendLine($"# `{descriptor.Key}`");

Check warning on line 223 in tools/Nullean.Curb.OptionDocs/Program.cs

View workflow job for this annotation

GitHub Actions / Corpus and conformance

The behavior of 'StringBuilder.AppendLine(ref StringBuilder.AppendInterpolatedStringHandler)' could vary based on the current user's locale settings. Replace this call in 'Program.BuildPage(OptionDescriptor, string, CSharpFormatter, FormatOptions, IEnumerable<Program.Divergence>)' with a call to 'StringBuilder.AppendLine(IFormatProvider, ref StringBuilder.AppendInterpolatedStringHandler)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
sb.AppendLine();
sb.AppendLine(descriptor.Summary);
sb.AppendLine();
Expand All @@ -233,7 +233,7 @@
foreach (var v in descriptor.Values)
{
var note = v.StartsWith('<') && v.EndsWith('>') ? "Any integer." : "";
sb.AppendLine($"| `{v}` | {note} |");

Check warning on line 236 in tools/Nullean.Curb.OptionDocs/Program.cs

View workflow job for this annotation

GitHub Actions / Corpus and conformance

The behavior of 'StringBuilder.AppendLine(ref StringBuilder.AppendInterpolatedStringHandler)' could vary based on the current user's locale settings. Replace this call in 'Program.BuildPage(OptionDescriptor, string, CSharpFormatter, FormatOptions, IEnumerable<Program.Divergence>)' with a call to 'StringBuilder.AppendLine(IFormatProvider, ref StringBuilder.AppendInterpolatedStringHandler)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
}
sb.AppendLine();
sb.AppendLine($"**Default:** `{descriptor.Default}`");
Expand Down Expand Up @@ -315,10 +315,10 @@

private static void EmitValueSection(StringBuilder sb, string key, string value, string before, string after)
{
sb.AppendLine($"### `{key} = {value}`");

Check warning on line 318 in tools/Nullean.Curb.OptionDocs/Program.cs

View workflow job for this annotation

GitHub Actions / Corpus and conformance

The behavior of 'StringBuilder.AppendLine(ref StringBuilder.AppendInterpolatedStringHandler)' could vary based on the current user's locale settings. Replace this call in 'Program.EmitValueSection(StringBuilder, string, string, string, string)' with a call to 'StringBuilder.AppendLine(IFormatProvider, ref StringBuilder.AppendInterpolatedStringHandler)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
sb.AppendLine();
sb.AppendLine("```ini");
sb.AppendLine($"{key} = {value}");

Check warning on line 321 in tools/Nullean.Curb.OptionDocs/Program.cs

View workflow job for this annotation

GitHub Actions / Corpus and conformance

The behavior of 'StringBuilder.AppendLine(ref StringBuilder.AppendInterpolatedStringHandler)' could vary based on the current user's locale settings. Replace this call in 'Program.EmitValueSection(StringBuilder, string, string, string, string)' with a call to 'StringBuilder.AppendLine(IFormatProvider, ref StringBuilder.AppendInterpolatedStringHandler)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1305)
sb.AppendLine("```");
sb.AppendLine();
sb.AppendLine("::::{tab-set}");
Expand Down Expand Up @@ -455,7 +455,7 @@
_ => (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 },
Expand Down
Loading