File: src\Analyzers\CSharp\Analyzers\NewLines\ConsecutiveBracePlacement\ConsecutiveBracePlacementDiagnosticAnalyzer.cs
Web Access
Project: src\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.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Diagnostics;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CSharp.NewLines.ConsecutiveBracePlacement;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class ConsecutiveBracePlacementDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
    public ConsecutiveBracePlacementDiagnosticAnalyzer()
        : base(IDEDiagnosticIds.ConsecutiveBracePlacementDiagnosticId,
               EnforceOnBuildValues.ConsecutiveBracePlacement,
               CSharpCodeStyleOptions.AllowBlankLinesBetweenConsecutiveBraces,
               new LocalizableResourceString(
                   nameof(CSharpAnalyzersResources.Consecutive_braces_must_not_have_a_blank_between_them), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
    {
    }
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis;
 
    protected override void InitializeWorker(AnalysisContext context)
        => context.RegisterCompilationStartAction(context =>
            context.RegisterSyntaxTreeAction(treeContext => AnalyzeTree(treeContext, context.Compilation.Options)));
 
    private void AnalyzeTree(SyntaxTreeAnalysisContext context, CompilationOptions compilationOptions)
    {
        var option = context.GetCSharpAnalyzerOptions().AllowBlankLinesBetweenConsecutiveBraces;
        if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification))
            return;
 
        using var _ = ArrayBuilder<SyntaxNode>.GetInstance(out var stack);
        Recurse(context, option.Notification, stack);
    }
 
    private void Recurse(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, ArrayBuilder<SyntaxNode> stack)
    {
        var tree = context.Tree;
        var cancellationToken = context.CancellationToken;
 
        var root = context.GetAnalysisRoot(findInTrivia: false);
        var text = tree.GetText(cancellationToken);
 
        stack.Add(root);
        while (stack.Count > 0)
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            var current = stack.Last();
            stack.RemoveLast();
 
            // Don't bother analyzing nodes that have syntax errors in them.
            if (current.ContainsDiagnostics && current.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error))
                continue;
 
            foreach (var child in current.ChildNodesAndTokens())
            {
                if (!context.ShouldAnalyzeSpan(child.FullSpan))
                    continue;
 
                if (child.IsNode)
                    stack.Add(child.AsNode()!);
                else if (child.IsToken)
                    ProcessToken(context, notificationOption, text, child.AsToken());
            }
        }
    }
 
    private void ProcessToken(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SourceText text, SyntaxToken token)
    {
        if (!HasExcessBlankLinesAfter(text, token, out var secondBrace, out _))
            return;
 
        context.ReportDiagnostic(DiagnosticHelper.Create(
            this.Descriptor,
            secondBrace.GetLocation(),
            notificationOption,
            context.Options,
            additionalLocations: null,
            properties: null));
    }
 
    public static bool HasExcessBlankLinesAfter(
        SourceText text, SyntaxToken token,
        out SyntaxToken secondBrace,
        out SyntaxTrivia endOfLineTrivia)
    {
        secondBrace = default;
        endOfLineTrivia = default;
        if (!token.IsKind(SyntaxKind.CloseBraceToken))
            return false;
 
        var nextToken = token.GetNextToken();
        if (!nextToken.IsKind(SyntaxKind.CloseBraceToken))
            return false;
 
        var firstBrace = token;
        secondBrace = nextToken;
 
        // two } tokens.  They need to be on the same line, or if they are not on subsequent lines, then there needs
        // to be more than whitespace between them.
        var lines = text.Lines;
        var firstBraceLine = lines.GetLineFromPosition(firstBrace.SpanStart).LineNumber;
        var secondBraceLine = lines.GetLineFromPosition(secondBrace.SpanStart).LineNumber;
 
        var lineCount = secondBraceLine - firstBraceLine;
 
        // if they're both on the same line, or one line apart, then there's no problem.
        if (lineCount <= 1)
            return false;
 
        // they're multiple lines apart.  This i not ok if those lines are all whitespace.
        for (var currentLine = firstBraceLine + 1; currentLine < secondBraceLine; currentLine++)
        {
            if (!IsAllWhitespace(lines[currentLine]))
                return false;
        }
 
        endOfLineTrivia = secondBrace.LeadingTrivia.Last(t => t.IsKind(SyntaxKind.EndOfLineTrivia));
        return endOfLineTrivia != default;
    }
 
    private static bool IsAllWhitespace(TextLine textLine)
    {
        var text = textLine.Text!;
        for (var i = textLine.Start; i < textLine.End; i++)
        {
            if (!SyntaxFacts.IsWhitespace(text[i]))
                return false;
        }
 
        return true;
    }
}