//===--- MacroRepeatedSideEffectsCheck.cpp - clang-tidy--------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

#include "MacroRepeatedSideEffectsCheck.h"
#include "clang/Basic/Builtins.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Lex/MacroArgs.h"
#include "clang/Lex/PPCallbacks.h"
#include "clang/Lex/Preprocessor.h"
#include <stack>

namespace clang::tidy::bugprone {

namespace {
class MacroRepeatedPPCallbacks : public PPCallbacks {
public:
  MacroRepeatedPPCallbacks(ClangTidyCheck &Check, Preprocessor &PP)
      : Check(Check), PP(PP) {}

  void MacroExpands(const Token &MacroNameTok, const MacroDefinition &MD,
                    SourceRange Range, const MacroArgs *Args) override;

private:
  ClangTidyCheck &Check;
  Preprocessor &PP;

  unsigned countArgumentExpansions(const MacroInfo *MI,
                                   const IdentifierInfo *Arg) const;

  bool hasSideEffects(const Token *ResultArgToks) const;
};
} // End of anonymous namespace.

void MacroRepeatedPPCallbacks::MacroExpands(const Token &MacroNameTok,
                                            const MacroDefinition &MD,
                                            SourceRange Range,
                                            const MacroArgs *Args) {
  // Ignore macro argument expansions.
  if (!Range.getBegin().isFileID())
    return;

  const MacroInfo *MI = MD.getMacroInfo();

  // Bail out if the contents of the macro are containing keywords that are
  // making the macro too complex.
  if (llvm::any_of(MI->tokens(), [](const Token &T) {
        return T.isOneOf(tok::kw_if, tok::kw_else, tok::kw_switch, tok::kw_case,
                         tok::kw_break, tok::kw_while, tok::kw_do, tok::kw_for,
                         tok::kw_continue, tok::kw_goto, tok::kw_return);
      }))
    return;

  for (unsigned ArgNo = 0U; ArgNo < MI->getNumParams(); ++ArgNo) {
    const IdentifierInfo *Arg = *(MI->param_begin() + ArgNo);
    const Token *ResultArgToks = Args->getUnexpArgument(ArgNo);

    if (hasSideEffects(ResultArgToks) &&
        countArgumentExpansions(MI, Arg) >= 2) {
      Check.diag(ResultArgToks->getLocation(),
                 "side effects in the %ordinal0 macro argument %1 are "
                 "repeated in macro expansion")
          << (ArgNo + 1) << Arg;
      Check.diag(MI->getDefinitionLoc(), "macro %0 defined here",
                 DiagnosticIDs::Note)
          << MacroNameTok.getIdentifierInfo();
    }
  }
}

unsigned MacroRepeatedPPCallbacks::countArgumentExpansions(
    const MacroInfo *MI, const IdentifierInfo *Arg) const {
  // Current argument count. When moving forward to a different control-flow
  // path this can decrease.
  unsigned Current = 0;
  // Max argument count.
  unsigned Max = 0;
  bool SkipParen = false;
  int SkipParenCount = 0;
  // Has a __builtin_constant_p been found?
  bool FoundBuiltin = false;
  bool PrevTokenIsHash = false;
  // Count when "?" is reached. The "Current" will get this value when the ":"
  // is reached.
  std::stack<unsigned, SmallVector<unsigned, 8>> CountAtQuestion;
  for (const auto &T : MI->tokens()) {
    // The result of __builtin_constant_p(x) is 0 if x is a macro argument
    // with side effects. If we see a __builtin_constant_p(x) followed by a
    // "?" "&&" or "||", then we need to reason about control flow to report
    // warnings correctly. Until such reasoning is added, bail out when this
    // happens.
    if (FoundBuiltin && T.isOneOf(tok::question, tok::ampamp, tok::pipepipe))
      return Max;

    // Skip stringified tokens.
    if (T.is(tok::hash)) {
      PrevTokenIsHash = true;
      continue;
    }
    if (PrevTokenIsHash) {
      PrevTokenIsHash = false;
      continue;
    }

    // Handling of ? and :.
    if (T.is(tok::question)) {
      CountAtQuestion.push(Current);
    } else if (T.is(tok::colon)) {
      if (CountAtQuestion.empty())
        return 0;
      Current = CountAtQuestion.top();
      CountAtQuestion.pop();
    }

    // If current token is a parenthesis, skip it.
    if (SkipParen) {
      if (T.is(tok::l_paren))
        SkipParenCount++;
      else if (T.is(tok::r_paren))
        SkipParenCount--;
      SkipParen = (SkipParenCount != 0);
      if (SkipParen)
        continue;
    }

    IdentifierInfo *TII = T.getIdentifierInfo();
    // If not existent, skip it.
    if (TII == nullptr)
      continue;

    // If a __builtin_constant_p is found within the macro definition, don't
    // count arguments inside the parentheses and remember that it has been
    // seen in case there are "?", "&&" or "||" operators later.
    if (TII->getBuiltinID() == Builtin::BI__builtin_constant_p) {
      FoundBuiltin = true;
      SkipParen = true;
      continue;
    }

    // If another macro is found within the macro definition, skip the macro
    // and the eventual arguments.
    if (TII->hasMacroDefinition()) {
      const MacroInfo *M = PP.getMacroDefinition(TII).getMacroInfo();
      if (M != nullptr && M->isFunctionLike())
        SkipParen = true;
      continue;
    }

    // Count argument.
    if (TII == Arg) {
      Current++;
      Max = std::max(Max, Current);
    }
  }
  return Max;
}

bool MacroRepeatedPPCallbacks::hasSideEffects(
    const Token *ResultArgToks) const {
  for (; ResultArgToks->isNot(tok::eof); ++ResultArgToks) {
    if (ResultArgToks->isOneOf(tok::plusplus, tok::minusminus))
      return true;
  }
  return false;
}

void MacroRepeatedSideEffectsCheck::registerPPCallbacks(
    const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) {
  PP->addPPCallbacks(::std::make_unique<MacroRepeatedPPCallbacks>(*this, *PP));
}

} // namespace clang::tidy::bugprone
