//===--- DefinitionBlockSeparator.cpp ---------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file implements DefinitionBlockSeparator, a TokenAnalyzer that inserts
/// or removes empty lines separating definition blocks like classes, structs,
/// functions, enums, and namespaces in between.
///
//===----------------------------------------------------------------------===//

#include "DefinitionBlockSeparator.h"
#define DEBUG_TYPE "definition-block-separator"

namespace clang {
namespace format {
std::pair<tooling::Replacements, unsigned> DefinitionBlockSeparator::analyze(
    TokenAnnotator &Annotator, SmallVectorImpl<AnnotatedLine *> &AnnotatedLines,
    FormatTokenLexer &Tokens) {
  assert(Style.SeparateDefinitionBlocks != FormatStyle::SDS_Leave);
  AffectedRangeMgr.computeAffectedLines(AnnotatedLines);
  tooling::Replacements Result;
  separateBlocks(AnnotatedLines, Result, Tokens);
  return {Result, 0};
}

void DefinitionBlockSeparator::separateBlocks(
    SmallVectorImpl<AnnotatedLine *> &Lines, tooling::Replacements &Result,
    FormatTokenLexer &Tokens) {
  const bool IsNeverStyle =
      Style.SeparateDefinitionBlocks == FormatStyle::SDS_Never;
  const AdditionalKeywords &ExtraKeywords = Tokens.getKeywords();
  auto GetBracketLevelChange = [](const FormatToken *Tok) {
    if (Tok->isOneOf(tok::l_brace, tok::l_paren, tok::l_square))
      return 1;
    if (Tok->isOneOf(tok::r_brace, tok::r_paren, tok::r_square))
      return -1;
    return 0;
  };
  auto LikelyDefinition = [&](const AnnotatedLine *Line,
                              bool ExcludeEnum = false) {
    if ((Line->MightBeFunctionDecl && Line->mightBeFunctionDefinition()) ||
        Line->startsWithNamespace()) {
      return true;
    }
    int BracketLevel = 0;
    for (const FormatToken *CurrentToken = Line->First; CurrentToken;
         CurrentToken = CurrentToken->Next) {
      if (BracketLevel == 0) {
        if (CurrentToken->isOneOf(tok::kw_class, tok::kw_struct,
                                  tok::kw_union) ||
            (Style.isJavaScript() &&
             CurrentToken->is(ExtraKeywords.kw_function))) {
          return true;
        }
        if (!ExcludeEnum && CurrentToken->is(tok::kw_enum))
          return true;
      }
      BracketLevel += GetBracketLevelChange(CurrentToken);
    }
    return false;
  };
  unsigned NewlineCount =
      (Style.SeparateDefinitionBlocks == FormatStyle::SDS_Always ? 1 : 0) + 1;
  WhitespaceManager Whitespaces(
      Env.getSourceManager(), Style,
      Style.LineEnding > FormatStyle::LE_CRLF
          ? WhitespaceManager::inputUsesCRLF(
                Env.getSourceManager().getBufferData(Env.getFileID()),
                Style.LineEnding == FormatStyle::LE_DeriveCRLF)
          : Style.LineEnding == FormatStyle::LE_CRLF);
  for (unsigned I = 0; I < Lines.size(); ++I) {
    const auto &CurrentLine = Lines[I];
    if (CurrentLine->InPPDirective)
      continue;
    FormatToken *TargetToken = nullptr;
    AnnotatedLine *TargetLine;
    auto OpeningLineIndex = CurrentLine->MatchingOpeningBlockLineIndex;
    AnnotatedLine *OpeningLine = nullptr;
    const auto IsAccessSpecifierToken = [](const FormatToken *Token) {
      return Token->isAccessSpecifier() || Token->isObjCAccessSpecifier();
    };
    const auto InsertReplacement = [&](const int NewlineToInsert) {
      assert(TargetLine);
      assert(TargetToken);

      // Do not handle EOF newlines.
      if (TargetToken->is(tok::eof))
        return;
      if (IsAccessSpecifierToken(TargetToken) ||
          (OpeningLineIndex > 0 &&
           IsAccessSpecifierToken(Lines[OpeningLineIndex - 1]->First))) {
        return;
      }
      if (!TargetLine->Affected)
        return;
      Whitespaces.replaceWhitespace(*TargetToken, NewlineToInsert,
                                    TargetToken->OriginalColumn,
                                    TargetToken->OriginalColumn);
    };
    const auto IsPPConditional = [&](const size_t LineIndex) {
      const auto &Line = Lines[LineIndex];
      return Line->First->is(tok::hash) && Line->First->Next &&
             Line->First->Next->isOneOf(tok::pp_if, tok::pp_ifdef, tok::pp_else,
                                        tok::pp_ifndef, tok::pp_elifndef,
                                        tok::pp_elifdef, tok::pp_elif,
                                        tok::pp_endif);
    };
    const auto FollowingOtherOpening = [&]() {
      return OpeningLineIndex == 0 ||
             Lines[OpeningLineIndex - 1]->Last->opensScope() ||
             IsPPConditional(OpeningLineIndex - 1);
    };
    const auto HasEnumOnLine = [&]() {
      bool FoundEnumKeyword = false;
      int BracketLevel = 0;
      for (const FormatToken *CurrentToken = CurrentLine->First; CurrentToken;
           CurrentToken = CurrentToken->Next) {
        if (BracketLevel == 0) {
          if (CurrentToken->is(tok::kw_enum))
            FoundEnumKeyword = true;
          else if (FoundEnumKeyword && CurrentToken->is(tok::l_brace))
            return true;
        }
        BracketLevel += GetBracketLevelChange(CurrentToken);
      }
      return FoundEnumKeyword && I + 1 < Lines.size() &&
             Lines[I + 1]->First->is(tok::l_brace);
    };

    bool IsDefBlock = false;
    const auto MayPrecedeDefinition = [&](const int Direction = -1) {
      assert(Direction >= -1);
      assert(Direction <= 1);

      if (Lines[OpeningLineIndex]->First->is(TT_CSharpGenericTypeConstraint))
        return true;

      const size_t OperateIndex = OpeningLineIndex + Direction;
      assert(OperateIndex < Lines.size());
      const auto &OperateLine = Lines[OperateIndex];
      if (LikelyDefinition(OperateLine))
        return false;

      if (const auto *Tok = OperateLine->First;
          Tok->is(tok::comment) && !isClangFormatOn(Tok->TokenText)) {
        return true;
      }

      // A single line identifier that is not in the last line.
      if (OperateLine->First->is(tok::identifier) &&
          OperateLine->First == OperateLine->Last &&
          OperateIndex + 1 < Lines.size()) {
        // UnwrappedLineParser's recognition of free-standing macro like
        // Q_OBJECT may also recognize some uppercased type names that may be
        // used as return type as that kind of macros, which is a bit hard to
        // distinguish one from another purely from token patterns. Here, we
        // try not to add new lines below those identifiers.
        AnnotatedLine *NextLine = Lines[OperateIndex + 1];
        if (NextLine->MightBeFunctionDecl &&
            NextLine->mightBeFunctionDefinition() &&
            NextLine->First->NewlinesBefore == 1 &&
            OperateLine->First->is(TT_FunctionLikeOrFreestandingMacro)) {
          return true;
        }
      }

      if (Style.isCSharp() && OperateLine->First->is(TT_AttributeLSquare))
        return true;
      return false;
    };

    if (HasEnumOnLine() &&
        !LikelyDefinition(CurrentLine, /*ExcludeEnum=*/true)) {
      // We have no scope opening/closing information for enum.
      IsDefBlock = true;
      OpeningLineIndex = I;
      while (OpeningLineIndex > 0 && MayPrecedeDefinition())
        --OpeningLineIndex;
      OpeningLine = Lines[OpeningLineIndex];
      TargetLine = OpeningLine;
      TargetToken = TargetLine->First;
      if (!FollowingOtherOpening())
        InsertReplacement(NewlineCount);
      else if (IsNeverStyle)
        InsertReplacement(OpeningLineIndex != 0);
      TargetLine = CurrentLine;
      TargetToken = TargetLine->First;
      while (TargetToken && TargetToken->isNot(tok::r_brace))
        TargetToken = TargetToken->Next;
      if (!TargetToken)
        while (I < Lines.size() && Lines[I]->First->isNot(tok::r_brace))
          ++I;
    } else if (CurrentLine->First->closesScope()) {
      if (OpeningLineIndex > Lines.size())
        continue;
      // Handling the case that opening brace has its own line, with checking
      // whether the last line already had an opening brace to guard against
      // misrecognition.
      if (OpeningLineIndex > 0 &&
          Lines[OpeningLineIndex]->First->is(tok::l_brace) &&
          Lines[OpeningLineIndex - 1]->Last->isNot(tok::l_brace)) {
        --OpeningLineIndex;
      }
      OpeningLine = Lines[OpeningLineIndex];
      // Closing a function definition.
      if (LikelyDefinition(OpeningLine)) {
        IsDefBlock = true;
        while (OpeningLineIndex > 0 && MayPrecedeDefinition())
          --OpeningLineIndex;
        OpeningLine = Lines[OpeningLineIndex];
        TargetLine = OpeningLine;
        TargetToken = TargetLine->First;
        if (!FollowingOtherOpening()) {
          // Avoid duplicated replacement.
          if (TargetToken->isNot(tok::l_brace))
            InsertReplacement(NewlineCount);
        } else if (IsNeverStyle) {
          InsertReplacement(OpeningLineIndex != 0);
        }
      }
    }

    // Not the last token.
    if (IsDefBlock && I + 1 < Lines.size()) {
      OpeningLineIndex = I + 1;
      TargetLine = Lines[OpeningLineIndex];
      TargetToken = TargetLine->First;

      // No empty line for continuously closing scopes. The token will be
      // handled in another case if the line following is opening a
      // definition.
      if (!TargetToken->closesScope() && !IsPPConditional(OpeningLineIndex)) {
        // Check whether current line may precede a definition line.
        while (OpeningLineIndex + 1 < Lines.size() &&
               MayPrecedeDefinition(/*Direction=*/0)) {
          ++OpeningLineIndex;
        }
        TargetLine = Lines[OpeningLineIndex];
        if (!LikelyDefinition(TargetLine)) {
          OpeningLineIndex = I + 1;
          TargetLine = Lines[I + 1];
          TargetToken = TargetLine->First;
          InsertReplacement(NewlineCount);
        }
      } else if (IsNeverStyle) {
        InsertReplacement(/*NewlineToInsert=*/1);
      }
    }
  }
  for (const auto &R : Whitespaces.generateReplacements()) {
    // The add method returns an Error instance which simulates program exit
    // code through overloading boolean operator, thus false here indicates
    // success.
    if (Result.add(R))
      return;
  }
}
} // namespace format
} // namespace clang
