|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using FormattingRangeHelper = Microsoft.CodeAnalysis.CSharp.Utilities.FormattingRangeHelper;
namespace Microsoft.CodeAnalysis.CSharp.Diagnostics.AddBraces
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpAddBracesDiagnosticAnalyzer :
AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
public CSharpAddBracesDiagnosticAnalyzer()
: base(IDEDiagnosticIds.AddBracesDiagnosticId,
EnforceOnBuildValues.AddBraces,
CSharpCodeStyleOptions.PreferBraces,
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Add_braces), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
new LocalizableResourceString(nameof(CSharpAnalyzersResources.Add_braces_to_0_statement), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
{
}
protected override void InitializeWorker(AnalysisContext context)
=> context.RegisterSyntaxNodeAction(AnalyzeNode,
SyntaxKind.IfStatement,
SyntaxKind.ElseClause,
SyntaxKind.ForStatement,
SyntaxKind.ForEachStatement,
SyntaxKind.ForEachVariableStatement,
SyntaxKind.WhileStatement,
SyntaxKind.DoStatement,
SyntaxKind.UsingStatement,
SyntaxKind.LockStatement,
SyntaxKind.FixedStatement);
public override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
public void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
var statement = context.Node;
var option = context.GetCSharpAnalyzerOptions().PreferBraces;
if (option.Value == PreferBracesPreference.None)
{
return;
}
var embeddedStatement = statement.GetEmbeddedStatement();
Contract.ThrowIfNull(embeddedStatement);
switch (embeddedStatement.Kind())
{
case SyntaxKind.Block:
// The embedded statement already has braces, which is always allowed.
return;
case SyntaxKind.IfStatement when statement.Kind() == SyntaxKind.ElseClause:
// Constructs like the following are always allowed:
//
// if (something)
// {
// }
// else if (somethingElse) // <-- 'if' nested in an 'else' clause
// {
// }
return;
case SyntaxKind.LockStatement:
case SyntaxKind.UsingStatement:
case SyntaxKind.FixedStatement:
// If we have something like this:
//
// using (...)
// using (...)
// {
// }
//
// The first statement needs no block as it formatted with the same indentation.
if (statement.Kind() == embeddedStatement.Kind())
{
return;
}
break;
}
if (option.Value == PreferBracesPreference.WhenMultiline
&& !IsConsideredMultiLine(statement, embeddedStatement)
&& !RequiresBracesToMatchContext(statement))
{
return;
}
if (ContainsInterleavedDirective(statement, embeddedStatement, context.CancellationToken))
{
return;
}
var firstToken = statement.GetFirstToken();
context.ReportDiagnostic(DiagnosticHelper.Create(
Descriptor,
firstToken.GetLocation(),
option.Notification.Severity,
additionalLocations: null,
properties: null,
SyntaxFacts.GetText(firstToken.Kind())));
}
/// <summary>
/// Check if there are interleaved directives on the statement.
/// Handles special case with if/else.
/// </summary>
private static bool ContainsInterleavedDirective(SyntaxNode statement, StatementSyntax embeddedStatement, CancellationToken cancellationToken)
{
if (statement is IfStatementSyntax ifStatementNode)
{
var elseNode = ifStatementNode.Else;
if (elseNode != null && !embeddedStatement.IsMissing)
{
// For IF/ELSE statements, only the IF part should be checked for interleaved directives when the diagnostic is triggered on the IF.
// A separate diagnostic will be triggered to handle the ELSE part.
var ifStatementSpanWithoutElse = TextSpan.FromBounds(statement.Span.Start, embeddedStatement.Span.End);
return statement.ContainsInterleavedDirective(ifStatementSpanWithoutElse, cancellationToken);
}
}
return statement.ContainsInterleavedDirective(cancellationToken);
}
/// <summary>
/// <para>In general, statements are considered multiline if any of the following span more than one line:</para>
/// <list type="bullet">
/// <item><description>The part of the statement preceding the embedded statement</description></item>
/// <item><description>The embedded statement itself</description></item>
/// <item><description>The part of the statement following the embedded statement, for example the
/// <c>while (...);</c> portion of a <c>do ... while (...);</c> statement</description></item>
/// </list>
/// <para>The third condition is not checked for <c>else</c> clauses because they are only considered multiline
/// when their embedded statement is multiline.</para>
/// </summary>
private static bool IsConsideredMultiLine(SyntaxNode statement, SyntaxNode embeddedStatement)
{
// Early return if syntax errors prevent analysis
if (embeddedStatement.IsMissing)
{
// The embedded statement was added by the compiler during recovery from a syntax error
return false;
}
// Early return if the entire statement fits on one line
if (FormattingRangeHelper.AreTwoTokensOnSameLine(statement.GetFirstToken(), statement.GetLastToken()))
{
// The entire statement fits on one line. Examples:
//
// if (something) return;
//
// while (true) something();
return false;
}
// Check the part of the statement preceding the embedded statement (bullet 1)
var lastTokenBeforeEmbeddedStatement = embeddedStatement.GetFirstToken().GetPreviousToken();
if (!FormattingRangeHelper.AreTwoTokensOnSameLine(statement.GetFirstToken(), lastTokenBeforeEmbeddedStatement))
{
// The part of the statement preceding the embedded statement does not fit on one line. Examples:
//
// for (int i = 0; // <-- The initializer/condition/increment are on separate lines
// i < 10;
// i++)
// SomeMethod();
return true;
}
// Check the embedded statement itself (bullet 2)
if (!FormattingRangeHelper.AreTwoTokensOnSameLine(embeddedStatement.GetFirstToken(), embeddedStatement.GetLastToken()))
{
// The embedded statement does not fit on one line. Examples:
//
// if (something)
// obj.Method( // <-- This embedded statement spans two lines.
// arg);
return true;
}
// Check the part of the statement following the embedded statement, but only if it exists and is not an
// 'else' clause (bullet 3)
if (statement.GetLastToken() != embeddedStatement.GetLastToken())
{
if (statement is IfStatementSyntax ifStatement && ifStatement.Statement == embeddedStatement)
{
// The embedded statement is followed by an 'else' clause, which may span multiple lines without
// triggering a braces requirement, such as this:
//
// if (true)
// return;
// else // <-- this else clause is two lines, but is not considered a multiline context
// return;
//
// ---
// INTENTIONAL FALLTHROUGH
}
else
{
var firstTokenAfterEmbeddedStatement = embeddedStatement.GetLastToken().GetNextToken();
if (!FormattingRangeHelper.AreTwoTokensOnSameLine(firstTokenAfterEmbeddedStatement, statement.GetLastToken()))
{
// The part of the statement following the embedded statement does not fit on one line. Examples:
//
// do
// SomeMethod();
// while (x < 0 || // <-- This condition is split across multiple lines.
// x > 10);
return true;
}
}
}
return false;
}
/// <summary>
/// Determines whether <paramref name="statement"/> should use braces under a
/// <see cref="PreferBracesPreference.WhenMultiline"/> preference due to the presence of braces on one or more
/// sibling statements (the "context").
/// </summary>
private static bool RequiresBracesToMatchContext(SyntaxNode statement)
{
if (statement.Kind() is not (SyntaxKind.IfStatement or SyntaxKind.ElseClause))
{
// 'if' statements are the only statements that can have multiple embedded statements which are
// considered relative to each other.
return false;
}
var outermostIfStatement = GetOutermostIfStatementOfSequence(statement);
if (AnyPartOfIfSequenceUsesBraces(outermostIfStatement))
{
return true;
}
return false;
}
/// <summary>
/// Gets the top-most <see cref="IfStatementSyntax"/> for which <paramref name="ifStatementOrElseClause"/> is
/// part of the <c>if</c>/<c>else if</c>/<c>else</c> sequence.
/// </summary>
/// <remarks>
/// <para>For the purpose of brace usage analysis, the embedded statements of an <c>if</c>/<c>else if</c>/<c>else</c>
/// sequence are considered sibling statements, even though they don't appear as immediate siblings in the
/// syntax tree. This method walks up the syntax tree to find the <c>if</c> statement that starts the
/// sequence.</para>
/// </remarks>
private static IfStatementSyntax GetOutermostIfStatementOfSequence(SyntaxNode ifStatementOrElseClause)
{
IfStatementSyntax result;
if (ifStatementOrElseClause.IsKind(SyntaxKind.ElseClause))
{
result = (IfStatementSyntax)ifStatementOrElseClause.GetRequiredParent();
}
else
{
Debug.Assert(ifStatementOrElseClause.IsKind(SyntaxKind.IfStatement));
result = (IfStatementSyntax)ifStatementOrElseClause;
}
while (result.IsParentKind(SyntaxKind.ElseClause))
result = (IfStatementSyntax)result.GetRequiredParent().GetRequiredParent();
return result;
}
/// <summary>
/// Determines if any embedded statement of an <c>if</c>/<c>else if</c>/<c>else</c> sequence uses braces. Only
/// the embedded statements falling <em>immediately</em> under one of these nodes are checked.
/// </summary>
private static bool AnyPartOfIfSequenceUsesBraces(IfStatementSyntax? statement)
{
// Iterative instead of recursive to avoid stack depth problems
while (statement != null)
{
if (statement.Statement.IsKind(SyntaxKind.Block))
return true;
var elseStatement = statement.Else?.Statement;
if (elseStatement.IsKind(SyntaxKind.Block))
return true;
statement = elseStatement as IfStatementSyntax;
}
return false;
}
}
}
|