//===----------------------------------------------------------------------===//
//
// 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 "ProTypeMemberInitCheck.h"
#include "../utils/LexerUtils.h"
#include "../utils/Matchers.h"
#include "../utils/TypeTraits.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Lex/Lexer.h"
#include "llvm/ADT/SmallPtrSet.h"

using namespace clang::ast_matchers;
using namespace clang::tidy::matchers;
using llvm::SmallPtrSet;
using llvm::SmallPtrSetImpl;

namespace clang::tidy::cppcoreguidelines {

namespace {

AST_MATCHER(CXXRecordDecl, hasDefaultConstructor) {
  return Node.hasDefaultConstructor();
}

} // namespace

// Iterate over all the fields in a record type, both direct and indirect (e.g.
// if the record contains an anonymous struct).
template <typename T, typename Func>
static void forEachField(const RecordDecl &Record, const T &Fields,
                         const Func &Fn) {
  for (const FieldDecl *F : Fields) {
    if (F->isAnonymousStructOrUnion()) {
      if (const CXXRecordDecl *R = F->getType()->getAsCXXRecordDecl())
        forEachField(*R, R->fields(), Fn);
    } else {
      Fn(F);
    }
  }
}

template <typename T, typename Func>
static void forEachFieldWithFilter(const RecordDecl &Record, const T &Fields,
                                   bool &AnyMemberHasInitPerUnion,
                                   const Func &Fn) {
  for (const FieldDecl *F : Fields) {
    if (F->isAnonymousStructOrUnion()) {
      if (const CXXRecordDecl *R = F->getType()->getAsCXXRecordDecl()) {
        AnyMemberHasInitPerUnion = false;
        forEachFieldWithFilter(*R, R->fields(), AnyMemberHasInitPerUnion, Fn);
      }
    } else {
      Fn(F);
    }
    if (Record.isUnion() && AnyMemberHasInitPerUnion)
      break;
  }
}

static void
removeFieldInitialized(const FieldDecl *M,
                       SmallPtrSetImpl<const FieldDecl *> &FieldDecls) {
  const RecordDecl *R = M->getParent();
  if (R && R->isUnion()) {
    // Erase all members in a union if any member of it is initialized.
    for (const auto *F : R->fields())
      FieldDecls.erase(F);
  } else {
    FieldDecls.erase(M);
  }
}

static void
removeFieldsInitializedInBody(const Stmt &Stmt, ASTContext &Context,
                              SmallPtrSetImpl<const FieldDecl *> &FieldDecls) {
  auto Matches =
      match(findAll(binaryOperator(
                hasOperatorName("="),
                hasLHS(memberExpr(member(fieldDecl().bind("fieldDecl")))))),
            Stmt, Context);
  for (const auto &Match : Matches)
    removeFieldInitialized(Match.getNodeAs<FieldDecl>("fieldDecl"), FieldDecls);
}

static StringRef getName(const FieldDecl *Field) { return Field->getName(); }

static StringRef getName(const RecordDecl *Record) {
  // Get the typedef name if this is a C-style anonymous struct and typedef.
  if (const TypedefNameDecl *Typedef = Record->getTypedefNameForAnonDecl())
    return Typedef->getName();
  return Record->getName();
}

// Creates comma separated list of decls requiring initialization in order of
// declaration.
template <typename R, typename T>
static std::string
toCommaSeparatedString(const R &OrderedDecls,
                       const SmallPtrSetImpl<const T *> &DeclsToInit) {
  SmallVector<StringRef, 16> Names;
  for (const T *Decl : OrderedDecls)
    if (DeclsToInit.contains(Decl))
      Names.emplace_back(getName(Decl));
  return llvm::join(Names.begin(), Names.end(), ", ");
}

static SourceLocation getLocationForEndOfToken(const ASTContext &Context,
                                               SourceLocation Location) {
  return Lexer::getLocForEndOfToken(Location, 0, Context.getSourceManager(),
                                    Context.getLangOpts());
}

namespace {

// There are 3 kinds of insertion placements:
enum class InitializerPlacement {
  // 1. The fields are inserted after an existing CXXCtorInitializer stored in
  // Where. This will be the case whenever there is a written initializer before
  // the fields available.
  After,

  // 2. The fields are inserted before the first existing initializer stored in
  // Where.
  Before,

  // 3. There are no written initializers and the fields will be inserted before
  // the constructor's body creating a new initializer list including the ':'.
  New
};

// An InitializerInsertion contains a list of fields and/or base classes to
// insert into the initializer list of a constructor. We use this to ensure
// proper absolute ordering according to the class declaration relative to the
// (perhaps improper) ordering in the existing initializer list, if any.
struct InitializerInsertion {
  InitializerInsertion(InitializerPlacement Placement,
                       const CXXCtorInitializer *Where)
      : Placement(Placement), Where(Where) {}

  SourceLocation getLocation(const ASTContext &Context,
                             const CXXConstructorDecl &Constructor) const {
    assert((Where != nullptr || Placement == InitializerPlacement::New) &&
           "Location should be relative to an existing initializer or this "
           "insertion represents a new initializer list.");
    SourceLocation Location;
    switch (Placement) {
    case InitializerPlacement::New: {
      const std::optional<Token> Tok = utils::lexer::getPreviousToken(
          Constructor.getBody()->getBeginLoc(), Context.getSourceManager(),
          Context.getLangOpts());
      Location = Tok ? Tok->getLocation() : SourceLocation{};
      break;
    }
    case InitializerPlacement::Before: {
      const std::optional<Token> Tok = utils::lexer::getPreviousToken(
          Where->getSourceRange().getBegin(), Context.getSourceManager(),
          Context.getLangOpts());
      Location = Tok ? Tok->getLocation() : SourceLocation{};
      break;
    }
    case InitializerPlacement::After:
      Location = Where->getRParenLoc();
      break;
    }
    return getLocationForEndOfToken(Context, Location);
  }

  std::string codeToInsert() const {
    assert(!Initializers.empty() && "No initializers to insert");
    std::string Code;
    llvm::raw_string_ostream Stream(Code);
    const std::string Joined =
        llvm::join(Initializers.begin(), Initializers.end(), "(), ");
    switch (Placement) {
    case InitializerPlacement::New:
      Stream << " : " << Joined << "()";
      break;
    case InitializerPlacement::Before:
      Stream << " " << Joined << "(),";
      break;
    case InitializerPlacement::After:
      Stream << ", " << Joined << "()";
      break;
    }
    return Stream.str();
  }

  InitializerPlacement Placement;
  const CXXCtorInitializer *Where;
  SmallVector<std::string, 4> Initializers;
};

} // namespace

// Convenience utility to get a RecordDecl from a QualType.
static const RecordDecl *getCanonicalRecordDecl(const QualType &Type) {
  if (const auto *RT = Type->getAsCanonical<RecordType>())
    return RT->getDecl();
  return nullptr;
}

template <typename R, typename T>
static SmallVector<InitializerInsertion, 16>
computeInsertions(const CXXConstructorDecl::init_const_range &Inits,
                  const R &OrderedDecls,
                  const SmallPtrSetImpl<const T *> &DeclsToInit) {
  SmallVector<InitializerInsertion, 16> Insertions;
  Insertions.emplace_back(InitializerPlacement::New, nullptr);

  typename R::const_iterator Decl = std::begin(OrderedDecls);
  for (const CXXCtorInitializer *Init : Inits) {
    if (Init->isWritten()) {
      if (Insertions.size() == 1)
        Insertions.emplace_back(InitializerPlacement::Before, Init);

      // Gets either the field or base class being initialized by the provided
      // initializer.
      const auto *InitDecl =
          Init->isAnyMemberInitializer()
              ? static_cast<const NamedDecl *>(Init->getAnyMember())
              : Init->getBaseClass()->getAsCXXRecordDecl();

      // Add all fields between current field up until the next initializer.
      for (; Decl != std::end(OrderedDecls) && *Decl != InitDecl; ++Decl) {
        if (const auto *D = dyn_cast<T>(*Decl)) {
          if (DeclsToInit.contains(D))
            Insertions.back().Initializers.emplace_back(getName(D));
        }
      }

      Insertions.emplace_back(InitializerPlacement::After, Init);
    }
  }

  // Add remaining decls that require initialization.
  for (; Decl != std::end(OrderedDecls); ++Decl) {
    if (const auto *D = dyn_cast<T>(*Decl)) {
      if (DeclsToInit.contains(D))
        Insertions.back().Initializers.emplace_back(getName(D));
    }
  }
  return Insertions;
}

// Gets the list of bases and members that could possibly be initialized, in
// order as they appear in the class declaration.
static void
getInitializationsInOrder(const CXXRecordDecl &ClassDecl,
                          SmallVectorImpl<const NamedDecl *> &Decls) {
  Decls.clear();
  for (const auto &Base : ClassDecl.bases()) {
    // Decl may be null if the base class is a template parameter.
    if (const NamedDecl *Decl = getCanonicalRecordDecl(Base.getType()))
      Decls.emplace_back(Decl);
  }
  forEachField(ClassDecl, ClassDecl.fields(),
               [&](const FieldDecl *F) { Decls.push_back(F); });
}

template <typename T>
static void fixInitializerList(const ASTContext &Context,
                               DiagnosticBuilder &Diag,
                               const CXXConstructorDecl *Ctor,
                               const SmallPtrSetImpl<const T *> &DeclsToInit) {
  // Do not propose fixes in macros since we cannot place them correctly.
  if (Ctor->getBeginLoc().isMacroID())
    return;

  SmallVector<const NamedDecl *, 16> OrderedDecls;
  getInitializationsInOrder(*Ctor->getParent(), OrderedDecls);

  for (const auto &Insertion :
       computeInsertions(Ctor->inits(), OrderedDecls, DeclsToInit)) {
    if (!Insertion.Initializers.empty())
      Diag << FixItHint::CreateInsertion(Insertion.getLocation(Context, *Ctor),
                                         Insertion.codeToInsert());
  }
}

ProTypeMemberInitCheck::ProTypeMemberInitCheck(StringRef Name,
                                               ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      IgnoreArrays(Options.get("IgnoreArrays", false)),
      UseAssignment(Options.get("UseAssignment", false)) {}

void ProTypeMemberInitCheck::registerMatchers(MatchFinder *Finder) {
  auto IsUserProvidedNonDelegatingConstructor =
      allOf(isUserProvided(), unless(isInstantiated()),
            unless(isDelegatingConstructor()),
            ofClass(cxxRecordDecl().bind("parent")),
            unless(hasAnyConstructorInitializer(cxxCtorInitializer(
                isWritten(), unless(isMemberInitializer()),
                hasTypeLoc(loc(
                    qualType(hasDeclaration(equalsBoundNode("parent")))))))));

  auto IsNonTrivialDefaultConstructor = allOf(
      isDefaultConstructor(), unless(isUserProvided()),
      hasParent(cxxRecordDecl(unless(isTriviallyDefaultConstructible()))));
  Finder->addMatcher(
      cxxConstructorDecl(isDefinition(),
                         anyOf(IsUserProvidedNonDelegatingConstructor,
                               IsNonTrivialDefaultConstructor))
          .bind("ctor"),
      this);

  // Match classes with a default constructor that is defaulted or is not in the
  // AST.
  Finder->addMatcher(
      cxxRecordDecl(
          isDefinition(), unless(isInstantiated()), hasDefaultConstructor(),
          anyOf(has(cxxConstructorDecl(isDefaultConstructor(), isDefaulted(),
                                       unless(isImplicit()))),
                unless(has(cxxConstructorDecl()))),
          unless(isTriviallyDefaultConstructible()))
          .bind("record"),
      this);

  auto HasDefaultConstructor = hasInitializer(
      cxxConstructExpr(unless(requiresZeroInitialization()),
                       hasDeclaration(cxxConstructorDecl(
                           isDefaultConstructor(), unless(isUserProvided())))));
  Finder->addMatcher(
      varDecl(isDefinition(), HasDefaultConstructor,
              hasAutomaticStorageDuration(),
              hasType(recordDecl(has(fieldDecl()),
                                 isTriviallyDefaultConstructible())))
          .bind("var"),
      this);
}

void ProTypeMemberInitCheck::check(const MatchFinder::MatchResult &Result) {
  if (const auto *Ctor = Result.Nodes.getNodeAs<CXXConstructorDecl>("ctor")) {
    // Skip declarations delayed by late template parsing without a body.
    if (!Ctor->getBody())
      return;
    // Skip out-of-band explicitly defaulted special member functions
    // (except the default constructor).
    if (Ctor->isExplicitlyDefaulted() && !Ctor->isDefaultConstructor())
      return;
    checkMissingMemberInitializer(*Result.Context, *Ctor->getParent(), Ctor);
    checkMissingBaseClassInitializer(*Result.Context, *Ctor->getParent(), Ctor);
  } else if (const auto *Record =
                 Result.Nodes.getNodeAs<CXXRecordDecl>("record")) {
    assert(Record->hasDefaultConstructor() &&
           "Matched record should have a default constructor");
    checkMissingMemberInitializer(*Result.Context, *Record, nullptr);
    checkMissingBaseClassInitializer(*Result.Context, *Record, nullptr);
  } else if (const auto *Var = Result.Nodes.getNodeAs<VarDecl>("var")) {
    checkUninitializedTrivialType(*Result.Context, Var);
  }
}

void ProTypeMemberInitCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "IgnoreArrays", IgnoreArrays);
  Options.store(Opts, "UseAssignment", UseAssignment);
}

// FIXME: Copied from clang/lib/Sema/SemaDeclCXX.cpp.
static bool isIncompleteOrZeroLengthArrayType(const ASTContext &Context,
                                              QualType T) {
  if (T->isIncompleteArrayType())
    return true;

  while (const ConstantArrayType *ArrayT = Context.getAsConstantArrayType(T)) {
    if (!ArrayT->getSize())
      return true;

    T = ArrayT->getElementType();
  }

  return false;
}

static bool isEmpty(const ASTContext &Context, const QualType &Type) {
  if (const CXXRecordDecl *ClassDecl = Type->getAsCXXRecordDecl())
    return ClassDecl->isEmpty();
  return isIncompleteOrZeroLengthArrayType(Context, Type);
}

static StringRef getInitializer(QualType QT, bool UseAssignment) {
  static constexpr StringRef DefaultInitializer = "{}";
  if (!UseAssignment)
    return DefaultInitializer;

  if (QT->isPointerType())
    return " = nullptr";

  const auto *BT = dyn_cast<BuiltinType>(QT.getCanonicalType().getTypePtr());
  if (!BT)
    return DefaultInitializer;

  switch (BT->getKind()) {
  case BuiltinType::Bool:
    return " = false";
  case BuiltinType::Float:
    return " = 0.0F";
  case BuiltinType::Double:
    return " = 0.0";
  case BuiltinType::LongDouble:
    return " = 0.0L";
  case BuiltinType::SChar:
  case BuiltinType::Char_S:
  case BuiltinType::WChar_S:
  case BuiltinType::Char16:
  case BuiltinType::Char32:
  case BuiltinType::Short:
  case BuiltinType::Int:
    return " = 0";
  case BuiltinType::UChar:
  case BuiltinType::Char_U:
  case BuiltinType::WChar_U:
  case BuiltinType::UShort:
  case BuiltinType::UInt:
    return " = 0U";
  case BuiltinType::Long:
    return " = 0L";
  case BuiltinType::ULong:
    return " = 0UL";
  case BuiltinType::LongLong:
    return " = 0LL";
  case BuiltinType::ULongLong:
    return " = 0ULL";

  default:
    return DefaultInitializer;
  }
}

static void
computeFieldsToInit(const ASTContext &Context, const RecordDecl &Record,
                    bool IgnoreArrays,
                    SmallPtrSetImpl<const FieldDecl *> &FieldsToInit) {
  bool AnyMemberHasInitPerUnion = false;
  forEachFieldWithFilter(
      Record, Record.fields(), AnyMemberHasInitPerUnion,
      [&](const FieldDecl *F) {
        if (IgnoreArrays && F->getType()->isArrayType())
          return;
        if (F->hasInClassInitializer() && F->getParent()->isUnion()) {
          AnyMemberHasInitPerUnion = true;
          removeFieldInitialized(F, FieldsToInit);
        }
        if (!F->hasInClassInitializer() &&
            utils::type_traits::isTriviallyDefaultConstructible(F->getType(),
                                                                Context) &&
            !isEmpty(Context, F->getType()) && !F->isUnnamedBitField() &&
            !AnyMemberHasInitPerUnion)
          FieldsToInit.insert(F);
      });
}

void ProTypeMemberInitCheck::checkMissingMemberInitializer(
    ASTContext &Context, const CXXRecordDecl &ClassDecl,
    const CXXConstructorDecl *Ctor) {
  const bool IsUnion = ClassDecl.isUnion();

  if (IsUnion && ClassDecl.hasInClassInitializer())
    return;

  // Gather all fields (direct and indirect) that need to be initialized.
  SmallPtrSet<const FieldDecl *, 16> FieldsToInit;
  computeFieldsToInit(Context, ClassDecl, IgnoreArrays, FieldsToInit);
  if (FieldsToInit.empty())
    return;

  if (Ctor) {
    for (const CXXCtorInitializer *Init : Ctor->inits()) {
      // Remove any fields that were explicitly written in the initializer list
      // or in-class.
      if (Init->isAnyMemberInitializer() && Init->isWritten()) {
        if (IsUnion)
          return; // We can only initialize one member of a union.
        removeFieldInitialized(Init->getAnyMember(), FieldsToInit);
      }
    }
    removeFieldsInitializedInBody(*Ctor->getBody(), Context, FieldsToInit);
  }

  // Collect all fields in order, both direct fields and indirect fields from
  // anonymous record types.
  SmallVector<const FieldDecl *, 16> OrderedFields;
  forEachField(ClassDecl, ClassDecl.fields(),
               [&](const FieldDecl *F) { OrderedFields.push_back(F); });

  // Collect all the fields we need to initialize, including indirect fields.
  // It only includes fields that have not been fixed
  SmallPtrSet<const FieldDecl *, 16> AllFieldsToInit;
  forEachField(ClassDecl, FieldsToInit, [&](const FieldDecl *F) {
    if (HasRecordClassMemberSet.insert(F).second)
      AllFieldsToInit.insert(F);
  });
  if (FieldsToInit.empty())
    return;

  DiagnosticBuilder Diag =
      diag(Ctor ? Ctor->getBeginLoc() : ClassDecl.getLocation(),
           "%select{|union }0constructor %select{does not|should}0 initialize "
           "%select{|one of }0these fields: %1")
      << IsUnion << toCommaSeparatedString(OrderedFields, FieldsToInit);

  if (AllFieldsToInit.empty())
    return;

  // Do not propose fixes for constructors in macros since we cannot place them
  // correctly.
  if (Ctor && Ctor->getBeginLoc().isMacroID())
    return;

  // Collect all fields but only suggest a fix for the first member of unions,
  // as initializing more than one union member is an error.
  SmallPtrSet<const FieldDecl *, 16> FieldsToFix;
  bool AnyMemberHasInitPerUnion = false;
  forEachFieldWithFilter(ClassDecl, ClassDecl.fields(),
                         AnyMemberHasInitPerUnion, [&](const FieldDecl *F) {
                           if (!FieldsToInit.contains(F))
                             return;
                           // Don't suggest fixes for enums because we don't
                           // know a good default. Don't suggest fixes for
                           // bitfields because in-class initialization is not
                           // possible until C++20.
                           if (F->getType()->isEnumeralType() ||
                               (!getLangOpts().CPlusPlus20 && F->isBitField()))
                             return;
                           FieldsToFix.insert(F);
                           AnyMemberHasInitPerUnion = true;
                         });
  if (FieldsToFix.empty())
    return;

  // Use in-class initialization if possible.
  if (Context.getLangOpts().CPlusPlus11) {
    for (const FieldDecl *Field : FieldsToFix) {
      Diag << FixItHint::CreateInsertion(
          getLocationForEndOfToken(Context, Field->getSourceRange().getEnd()),
          getInitializer(Field->getType(), UseAssignment));
    }
  } else if (Ctor) {
    // Otherwise, rewrite the constructor's initializer list.
    fixInitializerList(Context, Diag, Ctor, FieldsToFix);
  }
}

void ProTypeMemberInitCheck::checkMissingBaseClassInitializer(
    const ASTContext &Context, const CXXRecordDecl &ClassDecl,
    const CXXConstructorDecl *Ctor) {
  // Gather any base classes that need to be initialized.
  SmallVector<const RecordDecl *, 4> AllBases;
  SmallPtrSet<const RecordDecl *, 4> BasesToInit;
  for (const CXXBaseSpecifier &Base : ClassDecl.bases()) {
    if (const auto *BaseClassDecl = getCanonicalRecordDecl(Base.getType())) {
      AllBases.emplace_back(BaseClassDecl);
      if (!BaseClassDecl->field_empty() &&
          utils::type_traits::isTriviallyDefaultConstructible(Base.getType(),
                                                              Context))
        BasesToInit.insert(BaseClassDecl);
    }
  }

  if (BasesToInit.empty())
    return;

  // Remove any bases that were explicitly written in the initializer list.
  if (Ctor) {
    if (Ctor->isImplicit())
      return;

    for (const CXXCtorInitializer *Init : Ctor->inits())
      if (Init->isBaseInitializer() && Init->isWritten()) {
        if (const CXXRecordDecl *CRD =
                Init->getBaseClass()->getAsCXXRecordDecl())
          BasesToInit.erase(CRD->getCanonicalDecl());
      }
  }

  if (BasesToInit.empty())
    return;

  DiagnosticBuilder Diag =
      diag(Ctor ? Ctor->getBeginLoc() : ClassDecl.getLocation(),
           "constructor does not initialize these bases: %0")
      << toCommaSeparatedString(AllBases, BasesToInit);

  if (Ctor)
    fixInitializerList(Context, Diag, Ctor, BasesToInit);
}

void ProTypeMemberInitCheck::checkUninitializedTrivialType(
    const ASTContext &Context, const VarDecl *Var) {
  // Verify that the record actually needs initialization
  const CXXRecordDecl *Record = Var->getType()->getAsCXXRecordDecl();
  if (!Record)
    return;

  SmallPtrSet<const FieldDecl *, 16> FieldsToInit;
  computeFieldsToInit(Context, *Record, IgnoreArrays, FieldsToInit);

  if (FieldsToInit.empty())
    return;

  const DiagnosticBuilder Diag =
      diag(Var->getBeginLoc(), "uninitialized record type: %0") << Var;

  Diag << FixItHint::CreateInsertion(
      getLocationForEndOfToken(Context, Var->getSourceRange().getEnd()),
      Context.getLangOpts().CPlusPlus11 ? "{}" : " = {}");
}

} // namespace clang::tidy::cppcoreguidelines
