//===----------------------------------------------------------------------===//
//
// 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 "InvalidEnumDefaultInitializationCheck.h"
#include "../utils/Matchers.h"
#include "../utils/OptionsUtils.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/TypeVisitor.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include <algorithm>

using namespace clang::ast_matchers;

namespace clang::tidy::bugprone {

namespace {

// Preserve same name as AST_MATCHER(isCompleteAndHasNoZeroValue)
// NOLINTNEXTLINE(llvm-prefer-static-over-anonymous-namespace)
bool isCompleteAndHasNoZeroValue(const EnumDecl *D) {
  const EnumDecl *Definition = D->getDefinition();
  return Definition && Definition->isComplete() &&
         !Definition->enumerators().empty() &&
         llvm::none_of(Definition->enumerators(),
                       [](const EnumConstantDecl *Value) {
                         return Value->getInitVal().isZero();
                       });
}

AST_MATCHER(EnumDecl, isCompleteAndHasNoZeroValue) {
  return isCompleteAndHasNoZeroValue(&Node);
}

// Find an initialization which initializes the value (if it has enum type) to a
// default zero value.
AST_MATCHER(Expr, isEmptyInit) {
  if (isa<CXXScalarValueInitExpr, ImplicitValueInitExpr>(&Node))
    return true;
  if (const auto *Init = dyn_cast<InitListExpr>(&Node)) {
    if (Init->getNumInits() == 0)
      return true;
  }
  return false;
}

AST_MATCHER(InitListExpr, hasArrayFiller) { return Node.hasArrayFiller(); }

// Check if any type has a "child" type that is an enum without zero value.
// The "child" type can be an array element type or member type of a record
// type (or a recursive combination of these). In this case, if the "root" type
// is statically initialized, the enum component is initialized to zero.
class FindEnumMember : public TypeVisitor<FindEnumMember, bool> {
public:
  const EnumType *FoundEnum = nullptr;

  bool VisitType(const Type *T) {
    const Type *DesT = T->getUnqualifiedDesugaredType();
    if (DesT != T)
      return Visit(DesT);
    return false;
  }
  bool VisitArrayType(const ArrayType *T) {
    return Visit(T->getElementType().getTypePtr());
  }
  bool VisitConstantArrayType(const ConstantArrayType *T) {
    return Visit(T->getElementType().getTypePtr());
  }
  bool VisitEnumType(const EnumType *T) {
    if (isCompleteAndHasNoZeroValue(T->getDecl())) {
      FoundEnum = T;
      return true;
    }
    return false;
  }
  bool VisitRecordType(const RecordType *T) {
    const RecordDecl *RD = T->getDecl()->getDefinition();
    if (!RD || RD->isUnion())
      return false;
    auto VisitField = [this](const FieldDecl *F) {
      return Visit(F->getType().getTypePtr());
    };
    return llvm::any_of(RD->fields(), VisitField);
  }
};

} // namespace

InvalidEnumDefaultInitializationCheck::InvalidEnumDefaultInitializationCheck(
    StringRef Name, ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      IgnoredEnums(
          utils::options::parseStringList(Options.get("IgnoredEnums", ""))) {
  IgnoredEnums.emplace_back("::std::errc");
}

void InvalidEnumDefaultInitializationCheck::storeOptions(
    ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "IgnoredEnums",
                utils::options::serializeStringList(IgnoredEnums));
}

void InvalidEnumDefaultInitializationCheck::registerMatchers(
    MatchFinder *Finder) {
  auto EnumWithoutZeroValue = enumType(hasDeclaration(
      enumDecl(isCompleteAndHasNoZeroValue(),
               unless(matchers::matchesAnyListedRegexName(IgnoredEnums)))
          .bind("enum")));
  auto EnumOrArrayOfEnum = qualType(hasUnqualifiedDesugaredType(
      anyOf(EnumWithoutZeroValue,
            arrayType(hasElementType(qualType(
                hasUnqualifiedDesugaredType(EnumWithoutZeroValue)))))));
  Finder->addMatcher(
      expr(isEmptyInit(), hasType(EnumOrArrayOfEnum)).bind("expr"), this);

  // Array initialization can contain an "array filler" for the (syntactically)
  // unspecified elements. This expression is not found by AST matchers and can
  // have any type (the array's element type). This is an implicitly generated
  // initialization, so if the type contains somewhere an enum without zero
  // enumerator, the zero initialization applies here. We search this array
  // element type for the specific enum type manually when this matcher matches.
  Finder->addMatcher(initListExpr(hasArrayFiller()).bind("array_filler_expr"),
                     this);
}

void InvalidEnumDefaultInitializationCheck::check(
    const MatchFinder::MatchResult &Result) {
  const auto *InitExpr = Result.Nodes.getNodeAs<Expr>("expr");
  const auto *Enum = Result.Nodes.getNodeAs<EnumDecl>("enum");
  if (!InitExpr) {
    const auto *InitList =
        Result.Nodes.getNodeAs<InitListExpr>("array_filler_expr");
    // Initialization of omitted array elements with array filler was found.
    // Check the type for enum without zero value.
    // FIXME: In this way only one enum-typed value is found, not all of these.
    FindEnumMember Finder;
    if (!Finder.Visit(InitList->getArrayFiller()->getType().getTypePtr()))
      return;
    InitExpr = InitList;
    Enum = Finder.FoundEnum->getDecl();
  }

  if (!InitExpr || !Enum)
    return;

  ASTContext &ACtx = Enum->getASTContext();
  SourceLocation Loc = InitExpr->getExprLoc();
  if (Loc.isInvalid()) {
    if (isa<ImplicitValueInitExpr, InitListExpr>(InitExpr)) {
      const DynTypedNodeList Parents = ACtx.getParents(*InitExpr);
      if (Parents.empty())
        return;

      if (const auto *Ctor = Parents[0].get<CXXConstructorDecl>()) {
        // Try to find member initializer with the found expression and get the
        // source location from it.
        CXXCtorInitializer *const *CtorInit = std::find_if(
            Ctor->init_begin(), Ctor->init_end(),
            [InitExpr](const CXXCtorInitializer *Init) {
              return Init->isMemberInitializer() && Init->getInit() == InitExpr;
            });
        if (!CtorInit)
          return;
        Loc = (*CtorInit)->getLParenLoc();
      } else if (const auto *InitList = Parents[0].get<InitListExpr>()) {
        // The expression may be implicitly generated for an initialization.
        // Search for a parent initialization list with valid source location.
        while (InitList->getExprLoc().isInvalid()) {
          const DynTypedNodeList Parents = ACtx.getParents(*InitList);
          if (Parents.empty())
            return;
          InitList = Parents[0].get<InitListExpr>();
          if (!InitList)
            return;
        }
        Loc = InitList->getExprLoc();
      }
    }
    // If still not found a source location, omit the warning.
    // Ideally all such cases (if they exist) should be handled to make the
    // check more precise.
    if (Loc.isInvalid())
      return;
  }
  diag(Loc, "enum value of type %0 initialized with invalid value of 0, "
            "enum doesn't have a zero-value enumerator")
      << Enum;
  diag(Enum->getLocation(), "enum is defined here", DiagnosticIDs::Note);
}

} // namespace clang::tidy::bugprone
