File: src\roslyn\src\Analyzers\CSharp\Analyzers\AddBraces\CSharpAddBracesDiagnosticAnalyzer.cs
Web Access
Project: src\roslyn\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle)
// 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 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 ||
            ShouldSkipAnalysis(context, option.Notification))
        {
            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,
            context.Options,
            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;
    }
}