File: src\Analyzers\Core\Analyzers\NewLines\MultipleBlankLines\AbstractMultipleBlankLinesDiagnosticAnalyzer.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.NewLines.MultipleBlankLines;
 
internal abstract class AbstractMultipleBlankLinesDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer
{
    private readonly ISyntaxFacts _syntaxFacts;
 
    protected AbstractMultipleBlankLinesDiagnosticAnalyzer(ISyntaxFacts syntaxFacts)
        : base(IDEDiagnosticIds.MultipleBlankLinesDiagnosticId,
               EnforceOnBuildValues.MultipleBlankLines,
               CodeStyleOptions2.AllowMultipleBlankLines,
               new LocalizableResourceString(
                   nameof(AnalyzersResources.Avoid_multiple_blank_lines), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)))
    {
        _syntaxFacts = syntaxFacts;
    }
 
    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.GetAnalyzerOptions().AllowMultipleBlankLines;
        if (option.Value || ShouldSkipAnalysis(context, compilationOptions, option.Notification))
            return;
 
        Recurse(context, option.Notification, context.GetAnalysisRoot(findInTrivia: false), context.CancellationToken);
    }
 
    private void Recurse(
        SyntaxTreeAnalysisContext context,
        NotificationOption2 notificationOption,
        SyntaxNode node,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
 
        // Don't bother analyzing nodes that have syntax errors in them.
        if (node.ContainsDiagnostics)
            return;
 
        foreach (var child in node.ChildNodesAndTokens())
        {
            if (!context.ShouldAnalyzeSpan(child.FullSpan))
                continue;
 
            if (child.AsNode(out var childNode))
                Recurse(context, notificationOption, childNode, cancellationToken);
            else if (child.IsToken)
                CheckToken(context, notificationOption, child.AsToken());
        }
    }
 
    private void CheckToken(SyntaxTreeAnalysisContext context, NotificationOption2 notificationOption, SyntaxToken token)
    {
        if (token.ContainsDiagnostics)
            return;
 
        if (!ContainsMultipleBlankLines(token, out var badTrivia))
            return;
 
        context.ReportDiagnostic(DiagnosticHelper.Create(
            this.Descriptor,
            Location.Create(badTrivia.SyntaxTree!, new TextSpan(badTrivia.SpanStart, 0)),
            notificationOption,
            context.Options,
            additionalLocations: ImmutableArray.Create(token.GetLocation()),
            properties: null));
    }
 
    private bool ContainsMultipleBlankLines(SyntaxToken token, out SyntaxTrivia firstBadTrivia)
    {
        var leadingTrivia = token.LeadingTrivia;
        for (var i = 0; i < leadingTrivia.Count; i++)
        {
            if (IsEndOfLine(leadingTrivia, i) &&
                IsEndOfLine(leadingTrivia, i + 1))
            {
                // Three cases that end up with two blank lines.
                //
                // 1. the token starts with two newlines.  This is definitely something to clean up.
                // 2. we have two newlines after structured trivia (which itself ends with an newline).
                // 3. we have three newlines (following non-structured trivia).
 
                if (i == 0 ||
                    leadingTrivia[i - 1].HasStructure)
                {
                    firstBadTrivia = leadingTrivia[i];
                    return true;
                }
 
                if (IsEndOfLine(leadingTrivia, i + 2))
                {
                    // Report on the second newline.  This is for cases like:
                    //
                    //      // comment
                    //
                    //
                    //      public
                    //
                    // The first newline follows the comment.  But we want to report the issue on the start of the
                    // next line.
                    firstBadTrivia = leadingTrivia[i + 1];
                    return true;
                }
            }
        }
 
        firstBadTrivia = default;
        return false;
    }
 
    private bool IsEndOfLine(SyntaxTriviaList triviaList, int index)
    {
        if (index >= triviaList.Count)
            return false;
 
        var trivia = triviaList[index];
        return _syntaxFacts.IsEndOfLineTrivia(trivia);
    }
}