Skip to content

Commit bfbb4ca

Browse files
committed
Issue #2801: SA1500 fires for the while clause of do/while statement
Provide new configuration setting, `layoutRules.allowDoWhileOnClosingBrace` such that when enabled, the following will be allowed: ``` do { Console.WriteLine("test"); } while (false); ```
1 parent 441ef97 commit bfbb4ca

File tree

7 files changed

+203
-9
lines changed

7 files changed

+203
-9
lines changed

StyleCop.Analyzers/StyleCop.Analyzers.CodeFixes/LayoutRules/SA1500CodeFixProvider.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen
5757

5858
var settings = SettingsHelper.GetStyleCopSettings(document.Project.AnalyzerOptions, cancellationToken);
5959
var braceToken = syntaxRoot.FindToken(diagnostic.Location.SourceSpan.Start);
60-
var tokenReplacements = GenerateBraceFixes(settings.Indentation, ImmutableArray.Create(braceToken));
60+
var tokenReplacements = GenerateBraceFixes(settings, ImmutableArray.Create(braceToken));
6161

6262
var newSyntaxRoot = syntaxRoot.ReplaceTokens(tokenReplacements.Keys, (originalToken, rewrittenToken) => tokenReplacements[originalToken]);
6363
return document.WithSyntaxRoot(newSyntaxRoot);
6464
}
6565

66-
private static Dictionary<SyntaxToken, SyntaxToken> GenerateBraceFixes(IndentationSettings indentationSettings, ImmutableArray<SyntaxToken> braceTokens)
66+
private static Dictionary<SyntaxToken, SyntaxToken> GenerateBraceFixes(StyleCopSettings settings, ImmutableArray<SyntaxToken> braceTokens)
6767
{
6868
var tokenReplacements = new Dictionary<SyntaxToken, SyntaxToken>();
6969

@@ -72,7 +72,7 @@ private static Dictionary<SyntaxToken, SyntaxToken> GenerateBraceFixes(Indentati
7272
var braceLine = LocationHelpers.GetLineSpan(braceToken).StartLinePosition.Line;
7373
var braceReplacementToken = braceToken;
7474

75-
var indentationSteps = DetermineIndentationSteps(indentationSettings, braceToken);
75+
var indentationSteps = DetermineIndentationSteps(settings.Indentation, braceToken);
7676

7777
var previousToken = braceToken.GetPreviousToken();
7878

@@ -102,19 +102,23 @@ private static Dictionary<SyntaxToken, SyntaxToken> GenerateBraceFixes(Indentati
102102
AddReplacement(tokenReplacements, previousToken, previousToken.WithTrailingTrivia(previousTokenNewTrailingTrivia));
103103
}
104104

105-
braceReplacementToken = braceReplacementToken.WithLeadingTrivia(IndentationHelper.GenerateWhitespaceTrivia(indentationSettings, indentationSteps));
105+
braceReplacementToken = braceReplacementToken.WithLeadingTrivia(IndentationHelper.GenerateWhitespaceTrivia(settings.Indentation, indentationSteps));
106106
}
107107

108108
// Check if we need to apply a fix after the brace. No fix is needed when:
109109
// - The closing brace is followed by a semi-colon or closing paren
110110
// - The closing brace is the last token in the file
111+
// - The closing brace is followed by the while expression of a do/while loop and the
112+
// allowDoWhileOnClosingBrace setting is enabled.
111113
var nextToken = braceToken.GetNextToken();
112114
var nextTokenLine = nextToken.IsKind(SyntaxKind.None) ? -1 : LocationHelpers.GetLineSpan(nextToken).StartLinePosition.Line;
113115
var isMultiDimensionArrayInitializer = braceToken.IsKind(SyntaxKind.OpenBraceToken) && braceToken.Parent.IsKind(SyntaxKind.ArrayInitializerExpression) && braceToken.Parent.Parent.IsKind(SyntaxKind.ArrayInitializerExpression);
116+
var allowDoWhileOnClosingBrace = settings.LayoutRules.AllowDoWhileOnClosingBrace && nextToken.IsKind(SyntaxKind.WhileKeyword) && (braceToken.Parent?.IsKind(SyntaxKind.Block) ?? false) && (braceToken.Parent.Parent?.IsKind(SyntaxKind.DoStatement) ?? false);
114117

115118
if ((nextTokenLine == braceLine) &&
116119
(!braceToken.IsKind(SyntaxKind.CloseBraceToken) || !IsValidFollowingToken(nextToken)) &&
117-
!isMultiDimensionArrayInitializer)
120+
!isMultiDimensionArrayInitializer &&
121+
!allowDoWhileOnClosingBrace)
118122
{
119123
var sharedTrivia = nextToken.LeadingTrivia.WithoutTrailingWhitespace();
120124
var newTrailingTrivia = braceReplacementToken.TrailingTrivia
@@ -135,7 +139,7 @@ private static Dictionary<SyntaxToken, SyntaxToken> GenerateBraceFixes(Indentati
135139
newIndentationSteps = Math.Max(0, newIndentationSteps - 1);
136140
}
137141

138-
AddReplacement(tokenReplacements, nextToken, nextToken.WithLeadingTrivia(IndentationHelper.GenerateWhitespaceTrivia(indentationSettings, newIndentationSteps)));
142+
AddReplacement(tokenReplacements, nextToken, nextToken.WithLeadingTrivia(IndentationHelper.GenerateWhitespaceTrivia(settings.Indentation, newIndentationSteps)));
139143
}
140144

141145
braceReplacementToken = braceReplacementToken.WithTrailingTrivia(newTrailingTrivia);
@@ -284,7 +288,7 @@ protected override async Task<SyntaxNode> FixAllInDocumentAsync(FixAllContext fi
284288

285289
var settings = SettingsHelper.GetStyleCopSettings(document.Project.AnalyzerOptions, fixAllContext.CancellationToken);
286290

287-
var tokenReplacements = GenerateBraceFixes(settings.Indentation, tokens);
291+
var tokenReplacements = GenerateBraceFixes(settings, tokens);
288292

289293
return syntaxRoot.ReplaceTokens(tokenReplacements.Keys, (originalToken, rewrittenToken) => tokenReplacements[originalToken]);
290294
}

StyleCop.Analyzers/StyleCop.Analyzers.Test/LayoutRules/SA1500/SA1500UnitTests.DoWhiles.cs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,5 +305,154 @@ private void Bar()
305305

306306
await VerifyCSharpFixAsync(testCode, expectedDiagnostics, fixedTestCode, CancellationToken.None).ConfigureAwait(false);
307307
}
308+
309+
/// <summary>
310+
/// Verifies that no diagnostics are reported for the do/while loop when the <see langword="while"/>
311+
/// expression is on the same line as the closing brace and the setting is enabled.
312+
/// </summary>
313+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
314+
[Fact]
315+
public async Task TestDoWhileOnClosingBraceWithAllowSettingAsync()
316+
{
317+
var testSettings = @"
318+
{
319+
""settings"": {
320+
""layoutRules"": {
321+
""allowDoWhileOnClosingBrace"": true
322+
}
323+
}
324+
}";
325+
326+
var testCode = @"public class Foo
327+
{
328+
private void Bar()
329+
{
330+
var x = 0;
331+
332+
do
333+
{
334+
x = 1;
335+
} while (x == 0);
336+
}
337+
}";
338+
339+
var test = new CSharpTest
340+
{
341+
TestCode = testCode,
342+
Settings = testSettings,
343+
};
344+
345+
await test.RunAsync(CancellationToken.None).ConfigureAwait(false);
346+
}
347+
348+
/// <summary>
349+
/// Verifies that diagnostics will be reported for the invalid <see langword="while"/> loop that
350+
/// is on the same line as the closing brace which is not part of a <c>do/while</c> loop. This
351+
/// ensures that the <c>allowDoWhileOnClosingBrace</c> setting is only applicable to a <c>do/while</c>
352+
/// loop and will not mistakenly allow a plain <see langword="while"/> loop after any arbitrary
353+
/// closing brace.
354+
/// </summary>
355+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
356+
[Fact]
357+
public async Task TestJustWhileLoopOnClosingBraceWithAllowDoWhileOnClosingBraceSettingAsync()
358+
{
359+
var testSettings = @"
360+
{
361+
""settings"": {
362+
""layoutRules"": {
363+
""allowDoWhileOnClosingBrace"": true
364+
}
365+
}
366+
}";
367+
368+
var testCode = @"public class Foo
369+
{
370+
private void Bar()
371+
{
372+
var x = 0;
373+
374+
while (x == 0)
375+
{
376+
x = 1;
377+
} while (x == 0)
378+
{
379+
x = 1;
380+
}
381+
}
382+
}";
383+
384+
var test = new CSharpTest
385+
{
386+
TestCode = testCode,
387+
ExpectedDiagnostics =
388+
{
389+
Diagnostic().WithLocation(10, 9),
390+
},
391+
Settings = testSettings,
392+
};
393+
394+
await test.RunAsync(CancellationToken.None).ConfigureAwait(false);
395+
}
396+
397+
/// <summary>
398+
/// Verifies that no diagnostics are reported for the do/while loop when the <see langword="while"/>
399+
/// expression is on the same line as the closing brace and the setting is allowed.
400+
/// </summary>
401+
/// <remarks>
402+
/// <para>The "Invalid do ... while #6" code in the <see cref="TestDoWhileInvalidAsync"/> unit test
403+
/// should account for the proper fix when the <c>allowDoWhileOnClosingBrace</c> is <see langword="false"/>,
404+
/// which is the default.</para>
405+
/// </remarks>
406+
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
407+
[Fact]
408+
public async Task TestFixDoWhileOnClosingBraceWithAllowSettingAsync()
409+
{
410+
var testSettings = @"
411+
{
412+
""settings"": {
413+
""layoutRules"": {
414+
""allowDoWhileOnClosingBrace"": true
415+
}
416+
}
417+
}";
418+
419+
var testCode = @"public class Foo
420+
{
421+
private void Bar()
422+
{
423+
var x = 0;
424+
425+
do
426+
{
427+
x = 1; } while (x == 0);
428+
}
429+
}";
430+
431+
var fixedTestCode = @"public class Foo
432+
{
433+
private void Bar()
434+
{
435+
var x = 0;
436+
437+
do
438+
{
439+
x = 1;
440+
} while (x == 0);
441+
}
442+
}";
443+
444+
var test = new CSharpTest
445+
{
446+
TestCode = testCode,
447+
ExpectedDiagnostics =
448+
{
449+
Diagnostic().WithLocation(9, 20),
450+
},
451+
FixedCode = fixedTestCode,
452+
Settings = testSettings,
453+
};
454+
455+
await test.RunAsync(CancellationToken.None).ConfigureAwait(false);
456+
}
308457
}
309458
}

StyleCop.Analyzers/StyleCop.Analyzers.Test/Settings/SettingsUnitTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public void VerifySettingsDefaults()
3737

3838
Assert.NotNull(styleCopSettings.LayoutRules);
3939
Assert.Equal(OptionSetting.Allow, styleCopSettings.LayoutRules.NewlineAtEndOfFile);
40+
Assert.True(styleCopSettings.LayoutRules.AllowConsecutiveUsings);
41+
Assert.False(styleCopSettings.LayoutRules.AllowDoWhileOnClosingBrace);
4042

4143
Assert.NotNull(styleCopSettings.SpacingRules);
4244
Assert.NotNull(styleCopSettings.ReadabilityRules);

StyleCop.Analyzers/StyleCop.Analyzers/LayoutRules/SA1500BracesForMultiLineStatementsMustNotShareLine.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ private static void CheckBraces(SyntaxNodeAnalysisContext context, SyntaxToken o
230230
CheckBraceToken(context, openBraceToken);
231231
if (checkCloseBrace)
232232
{
233-
CheckBraceToken(context, closeBraceToken);
233+
CheckBraceToken(context, closeBraceToken, openBraceToken);
234234
}
235235
}
236236

@@ -242,7 +242,7 @@ private static bool InitializerExpressionSharesLine(InitializerExpressionSyntax
242242
return (index > 0) && (parent.Expressions[index - 1].GetEndLine() == parent.Expressions[index].GetLine());
243243
}
244244

245-
private static void CheckBraceToken(SyntaxNodeAnalysisContext context, SyntaxToken token)
245+
private static void CheckBraceToken(SyntaxNodeAnalysisContext context, SyntaxToken token, SyntaxToken openBraceToken = default)
246246
{
247247
if (token.IsMissing)
248248
{
@@ -279,6 +279,22 @@ private static void CheckBraceToken(SyntaxNodeAnalysisContext context, SyntaxTok
279279
// last token of this file
280280
return;
281281

282+
case SyntaxKind.WhileKeyword:
283+
// Because the default Visual Studio code completion snippet for a do-while loop
284+
// places the while expression on the same line as the closing brace, some users
285+
// may want to allow that and not have SA1500 report it as a style error.
286+
if (context.Options.GetStyleCopSettings(context.CancellationToken).LayoutRules.AllowDoWhileOnClosingBrace)
287+
{
288+
var openBracePreviousToken = openBraceToken.GetPreviousToken(includeZeroWidth: true);
289+
290+
if (openBracePreviousToken.IsKind(SyntaxKind.DoKeyword))
291+
{
292+
return;
293+
}
294+
}
295+
296+
break;
297+
282298
default:
283299
break;
284300
}

StyleCop.Analyzers/StyleCop.Analyzers/Settings/ObjectModel/LayoutSettings.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@ internal class LayoutSettings
1717
/// </summary>
1818
private readonly bool allowConsecutiveUsings;
1919

20+
/// <summary>
21+
/// This is the backing field of the <see cref="AllowDoWhileOnClosingBrace"/> property.
22+
/// </summary>
23+
private readonly bool allowDoWhileOnClosingBrace;
24+
2025
/// <summary>
2126
/// Initializes a new instance of the <see cref="LayoutSettings"/> class.
2227
/// </summary>
2328
protected internal LayoutSettings()
2429
{
2530
this.newlineAtEndOfFile = OptionSetting.Allow;
2631
this.allowConsecutiveUsings = true;
32+
this.allowDoWhileOnClosingBrace = false;
2733
}
2834

2935
/// <summary>
@@ -45,6 +51,10 @@ protected internal LayoutSettings(JsonObject layoutSettingsObject)
4551
this.allowConsecutiveUsings = kvp.ToBooleanValue();
4652
break;
4753

54+
case "allowDoWhileOnClosingBrace":
55+
this.allowDoWhileOnClosingBrace = kvp.ToBooleanValue();
56+
break;
57+
4858
default:
4959
break;
5060
}
@@ -56,5 +66,8 @@ protected internal LayoutSettings(JsonObject layoutSettingsObject)
5666

5767
public bool AllowConsecutiveUsings =>
5868
this.allowConsecutiveUsings;
69+
70+
public bool AllowDoWhileOnClosingBrace =>
71+
this.allowDoWhileOnClosingBrace;
5972
}
6073
}

documentation/Configuration.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ The following properties are used to configure layout rules in StyleCop Analyzer
418418
| --- | --- | --- | --- |
419419
| `newlineAtEndOfFile` | `"allow"` | 1.0.0 | Specifies the handling for newline characters which appear at the end of a file |
420420
| `allowConsecutiveUsings` | `true` | 1.1.0 | Specifies if SA1519 will allow consecutive using statements without braces |
421+
| `allowDoWhileOnClosingBrace` | `false` | >1.2.0 | Specifies if SA1500 will allow the `while` expression of a `do-while` loop to be on the same line as the closing brace, as is generated by the default code snippet of Visual Studio |
421422

422423
### Lines at End of File
423424

@@ -439,6 +440,13 @@ The `allowConsecutiveUsings` property specifies the behavior:
439440
This only allows omitting the braces for a using followed by another using statement. A using statement followed by any other type of statement will still
440441
require braces to used.
441442

443+
### Do-While Loop Placement
444+
445+
The behavior of [SA1500](SA1500.md) can be customized regarding the manner in which the `while` expression of a `do-while` loop is allowed to be placed. The `allowDoWhileOnClosingBrace` property specified the behavior:
446+
447+
* `true`: the `while` expression of a `do-while` loop may be placed on the same line as the closing brace
448+
* `false`: the `while` expression of a `do-while` loop must be on a separate line from the closing brace
449+
442450
## Documentation Rules
443451

444452
This section describes the features of documentation rules which can be configured in **stylecop.json**. Each of the described properties are configured in the `documentationRules` object, which is shown in the following sample file.

documentation/SA1500.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
The opening or closing brace within a C# statement, element, or expression is not placed on its own line.
2121

22+
> :memo: The behavior of this rule can change based on the configuration of the `allowDoWhileOnClosingBrace` property in **stylecop.json**. See [Configuration.md](Configuration.md#Layout-Rules) for more information.
23+
2224
## Rule description
2325

2426
A violation of this rule occurs when the opening or closing brace within a statement, element, or expression is not placed on its own line. For example:

0 commit comments

Comments
 (0)