Skip to content

Issue #2801: SA1500 fires for the while clause of do/while statement #3196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 19, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ private static async Task<Document> GetTransformedDocumentAsync(Document documen

var settings = SettingsHelper.GetStyleCopSettings(document.Project.AnalyzerOptions, syntaxRoot.SyntaxTree, cancellationToken);
var braceToken = syntaxRoot.FindToken(diagnostic.Location.SourceSpan.Start);
var tokenReplacements = GenerateBraceFixes(settings.Indentation, ImmutableArray.Create(braceToken));
var tokenReplacements = GenerateBraceFixes(settings, ImmutableArray.Create(braceToken));

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

private static Dictionary<SyntaxToken, SyntaxToken> GenerateBraceFixes(IndentationSettings indentationSettings, ImmutableArray<SyntaxToken> braceTokens)
private static Dictionary<SyntaxToken, SyntaxToken> GenerateBraceFixes(StyleCopSettings settings, ImmutableArray<SyntaxToken> braceTokens)
{
var tokenReplacements = new Dictionary<SyntaxToken, SyntaxToken>();

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

var indentationSteps = DetermineIndentationSteps(indentationSettings, braceToken);
var indentationSteps = DetermineIndentationSteps(settings.Indentation, braceToken);

var previousToken = braceToken.GetPreviousToken();

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

braceReplacementToken = braceReplacementToken.WithLeadingTrivia(IndentationHelper.GenerateWhitespaceTrivia(indentationSettings, indentationSteps));
braceReplacementToken = braceReplacementToken.WithLeadingTrivia(IndentationHelper.GenerateWhitespaceTrivia(settings.Indentation, indentationSteps));
}

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

@Kevin-Andrew Kevin-Andrew Sep 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line shows only partial code coverage from the Unit Tests and would be the reason for the decrease in Codecov.

image

The reason is because there isn't a test in which the braceToken.Parent is null. I'm not completely familiar with the flow of execution and what is possible and what isn't, so I wrote this just being safe. I suppose, given the SyntaxTree, it may be impossible for braceToken.Parent to be null, but I just don't know. I tried to write some Unit Tests that had malformed syntax and they would fail with a compiler error.

If someone is sure that braceToken.Parent can never be null, then I'd be happy to change it. I know the line of code right above this also uses braceToken.Parent without the Null-Conditional operator, but I wasn't sure if the fact that it is applicable to a Multi-Dimensional Array Initializer, that the syntax may be different. So I just wanted to be extra safe.

But we can remove it if we want the Codcov percentage to go back up.


if ((nextTokenLine == braceLine) &&
(!braceToken.IsKind(SyntaxKind.CloseBraceToken) || !IsValidFollowingToken(nextToken)) &&
!isMultiDimensionArrayInitializer)
!isMultiDimensionArrayInitializer &&
!allowDoWhileOnClosingBrace)
{
var sharedTrivia = nextToken.LeadingTrivia.WithoutTrailingWhitespace();
var newTrailingTrivia = braceReplacementToken.TrailingTrivia
Expand All @@ -135,7 +139,7 @@ private static Dictionary<SyntaxToken, SyntaxToken> GenerateBraceFixes(Indentati
newIndentationSteps = Math.Max(0, newIndentationSteps - 1);
}

AddReplacement(tokenReplacements, nextToken, nextToken.WithLeadingTrivia(IndentationHelper.GenerateWhitespaceTrivia(indentationSettings, newIndentationSteps)));
AddReplacement(tokenReplacements, nextToken, nextToken.WithLeadingTrivia(IndentationHelper.GenerateWhitespaceTrivia(settings.Indentation, newIndentationSteps)));
}

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

var settings = SettingsHelper.GetStyleCopSettings(document.Project.AnalyzerOptions, syntaxRoot.SyntaxTree, fixAllContext.CancellationToken);

var tokenReplacements = GenerateBraceFixes(settings.Indentation, tokens);
var tokenReplacements = GenerateBraceFixes(settings, tokens);

return syntaxRoot.ReplaceTokens(tokenReplacements.Keys, (originalToken, rewrittenToken) => tokenReplacements[originalToken]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,5 +304,164 @@ private void Bar()

await VerifyCSharpFixAsync(testCode, expectedDiagnostics, fixedTestCode, CancellationToken.None).ConfigureAwait(false);
}

/// <summary>
/// Verifies that no diagnostics are reported for the do/while loop when the <see langword="while"/>
/// expression is on the same line as the closing brace and the setting is enabled.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task TestDoWhileOnClosingBraceWithAllowSettingAsync()
{
var testSettings = @"
{
""settings"": {
""layoutRules"": {
""allowDoWhileOnClosingBrace"": true
}
}
}";

var testCode = @"public class Foo
{
private void Bar()
{
var x = 0;

do
{
x = 1;
} while (x == 0);
}
}";

var test = new CSharpTest
{
TestCode = testCode,
Settings = testSettings,
};

await test.RunAsync(CancellationToken.None).ConfigureAwait(false);
}

/// <summary>
/// Verifies that diagnostics will be reported for the invalid <see langword="while"/> loop that
/// is on the same line as the closing brace which is not part of a <c>do/while</c> loop. This
/// ensures that the <c>allowDoWhileOnClosingBrace</c> setting is only applicable to a <c>do/while</c>
/// loop and will not mistakenly allow a plain <see langword="while"/> loop after any arbitrary
/// closing brace.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task TestJustWhileLoopOnClosingBraceWithAllowDoWhileOnClosingBraceSettingAsync()
{
var testSettings = @"
{
""settings"": {
""layoutRules"": {
""allowDoWhileOnClosingBrace"": true
}
}
}";

var testCode = @"public class Foo
{
private void Bar()
{
var x = 0;

while (x == 0)
{
x = 1;
[|}|] while (x == 0)
{
x = 1;
}
}
}";

var fixedCode = @"public class Foo
{
private void Bar()
{
var x = 0;

while (x == 0)
{
x = 1;
}
while (x == 0)
{
x = 1;
}
}
}";

var test = new CSharpTest
{
TestCode = testCode,
FixedCode = fixedCode,
Settings = testSettings,
};

await test.RunAsync(CancellationToken.None).ConfigureAwait(false);
}

/// <summary>
/// Verifies that no diagnostics are reported for the do/while loop when the <see langword="while"/>
/// expression is on the same line as the closing brace and the setting is allowed.
/// </summary>
/// <remarks>
/// <para>The "Invalid do ... while #6" code in the <see cref="TestDoWhileInvalidAsync"/> unit test
/// should account for the proper fix when the <c>allowDoWhileOnClosingBrace</c> is <see langword="false"/>,
/// which is the default.</para>
/// </remarks>
/// <returns>A <see cref="Task"/> representing the asynchronous unit test.</returns>
[Fact]
public async Task TestFixDoWhileOnClosingBraceWithAllowSettingAsync()
{
var testSettings = @"
{
""settings"": {
""layoutRules"": {
""allowDoWhileOnClosingBrace"": true
}
}
}";

var testCode = @"public class Foo
{
private void Bar()
{
var x = 0;

do
{
x = 1; [|}|] while (x == 0);
}
}";

var fixedTestCode = @"public class Foo
{
private void Bar()
{
var x = 0;

do
{
x = 1;
} while (x == 0);
}
}";

var test = new CSharpTest
{
TestCode = testCode,
FixedCode = fixedTestCode,
Settings = testSettings,
};

await test.RunAsync(CancellationToken.None).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public void VerifySettingsDefaults()

Assert.NotNull(styleCopSettings.LayoutRules);
Assert.Equal(OptionSetting.Allow, styleCopSettings.LayoutRules.NewlineAtEndOfFile);
Assert.True(styleCopSettings.LayoutRules.AllowConsecutiveUsings);
Assert.False(styleCopSettings.LayoutRules.AllowDoWhileOnClosingBrace);

Assert.NotNull(styleCopSettings.SpacingRules);
Assert.NotNull(styleCopSettings.ReadabilityRules);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ private static void CheckBraces(SyntaxNodeAnalysisContext context, SyntaxToken o
CheckBraceToken(context, openBraceToken);
if (checkCloseBrace)
{
CheckBraceToken(context, closeBraceToken);
CheckBraceToken(context, closeBraceToken, openBraceToken);
}
}

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

private static void CheckBraceToken(SyntaxNodeAnalysisContext context, SyntaxToken token)
private static void CheckBraceToken(SyntaxNodeAnalysisContext context, SyntaxToken token, SyntaxToken openBraceToken = default)
{
if (token.IsMissing)
{
Expand Down Expand Up @@ -284,6 +284,21 @@ private static void CheckBraceToken(SyntaxNodeAnalysisContext context, SyntaxTok
// last token of this file
return;

case SyntaxKind.WhileKeyword:
// Because the default Visual Studio code completion snippet for a do-while loop
// places the while expression on the same line as the closing brace, some users
// may want to allow that and not have SA1500 report it as a style error.
if (context.GetStyleCopSettings(context.CancellationToken).LayoutRules.AllowDoWhileOnClosingBrace)
{
if (openBraceToken.Parent.IsKind(SyntaxKind.Block)
&& openBraceToken.Parent.Parent.IsKind(SyntaxKind.DoStatement))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I don't know the syntax tree well enough, I just want to make sure, as I had similar code and this same question elsewhere, "Is using .Parent.Parent safe here?" We know that openBraceToken.Parent is not null and is of kind Block, but does that guarantee that .Parent.Parent will never be null? For example what if I have code like the following that uses some extra curly braces for scoping:

{ // Some braces for scoping
   var x = 0;
   do
   {
      x = 1;
   } while (x == 0);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsKind allows null, and openBraceToken.Parent.IsKind(SyntaxKind.Block) is only true if openBraceToken.Parent is not null.

{
return;
}
}

break;

default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ internal class LayoutSettings
/// </summary>
private readonly bool allowConsecutiveUsings;

/// <summary>
/// This is the backing field of the <see cref="AllowDoWhileOnClosingBrace"/> property.
/// </summary>
private readonly bool allowDoWhileOnClosingBrace;

/// <summary>
/// Initializes a new instance of the <see cref="LayoutSettings"/> class.
/// </summary>
protected internal LayoutSettings()
{
this.newlineAtEndOfFile = OptionSetting.Allow;
this.allowConsecutiveUsings = true;
this.allowDoWhileOnClosingBrace = false;
}

/// <summary>
Expand All @@ -37,6 +43,7 @@ protected internal LayoutSettings(JsonObject layoutSettingsObject, AnalyzerConfi
{
OptionSetting? newlineAtEndOfFile = null;
bool? allowConsecutiveUsings = null;
bool? allowDoWhileOnClosingBrace = null;

foreach (var kvp in layoutSettingsObject)
{
Expand All @@ -50,6 +57,10 @@ protected internal LayoutSettings(JsonObject layoutSettingsObject, AnalyzerConfi
allowConsecutiveUsings = kvp.ToBooleanValue();
break;

case "allowDoWhileOnClosingBrace":
allowDoWhileOnClosingBrace = kvp.ToBooleanValue();
break;

default:
break;
}
Expand All @@ -63,15 +74,20 @@ protected internal LayoutSettings(JsonObject layoutSettingsObject, AnalyzerConfi
};

allowConsecutiveUsings ??= AnalyzerConfigHelper.TryGetBooleanValue(analyzerConfigOptions, "stylecop.layout.allowConsecutiveUsings");
allowDoWhileOnClosingBrace ??= AnalyzerConfigHelper.TryGetBooleanValue(analyzerConfigOptions, "stylecop.layout.allowDoWhileOnClosingBrace");

this.newlineAtEndOfFile = newlineAtEndOfFile.GetValueOrDefault(OptionSetting.Allow);
this.allowConsecutiveUsings = allowConsecutiveUsings.GetValueOrDefault(true);
this.allowDoWhileOnClosingBrace = allowDoWhileOnClosingBrace.GetValueOrDefault(false);
}

public OptionSetting NewlineAtEndOfFile =>
this.newlineAtEndOfFile;

public bool AllowConsecutiveUsings =>
this.allowConsecutiveUsings;

public bool AllowDoWhileOnClosingBrace =>
this.allowDoWhileOnClosingBrace;
}
}
8 changes: 8 additions & 0 deletions documentation/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ The following properties are used to configure layout rules in StyleCop Analyzer
| --- | --- | --- | --- |
| `newlineAtEndOfFile` | `"allow"` | 1.0.0 | Specifies the handling for newline characters which appear at the end of a file |
| `allowConsecutiveUsings` | `true` | 1.1.0 | Specifies if SA1519 will allow consecutive using statements without braces |
| `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 |

### Lines at End of File

Expand All @@ -441,6 +442,13 @@ The `allowConsecutiveUsings` property specifies the behavior:
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
require braces to used.

### Do-While Loop Placement

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:

* `true`: the `while` expression of a `do`/`while` loop may be placed on the same line as the closing brace or on a separate line
* `false`: the `while` expression of a `do`/`while` loop must be on a separate line from the closing brace

## Documentation Rules

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.
Expand Down
2 changes: 2 additions & 0 deletions documentation/SA1500.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

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

> :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.

## Rule description

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:
Expand Down