//===----------------------------------------------------------------------===//
//
// 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 "QualifiedAutoCheck.h"
#include "../utils/LexerUtils.h"
#include "../utils/Matchers.h"
#include "../utils/OptionsUtils.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "llvm/ADT/SmallVector.h"
#include <optional>

using namespace clang::ast_matchers;

namespace clang::tidy::readability {

namespace {

// FIXME move to ASTMatchers
AST_MATCHER_P(QualType, hasUnqualifiedType,
              ast_matchers::internal::Matcher<QualType>, InnerMatcher) {
  return InnerMatcher.matches(Node.getUnqualifiedType(), Finder, Builder);
}

enum class Qualifier { Const, Volatile, Restrict };

} // namespace

static std::optional<Token>
findQualToken(const VarDecl *Decl, Qualifier Qual,
              const MatchFinder::MatchResult &Result) {
  // Since either of the locs can be in a macro, use `makeFileCharRange` to be
  // sure that we have a consistent `CharSourceRange`, located entirely in the
  // source file.

  assert((Qual == Qualifier::Const || Qual == Qualifier::Volatile ||
          Qual == Qualifier::Restrict) &&
         "Invalid Qualifier");

  SourceLocation BeginLoc = Decl->getQualifierLoc().getBeginLoc();
  if (BeginLoc.isInvalid())
    BeginLoc = Decl->getBeginLoc();
  const SourceLocation EndLoc = Decl->getLocation();

  const CharSourceRange FileRange = Lexer::makeFileCharRange(
      CharSourceRange::getCharRange(BeginLoc, EndLoc), *Result.SourceManager,
      Result.Context->getLangOpts());

  if (FileRange.isInvalid())
    return std::nullopt;

  const tok::TokenKind Tok = Qual == Qualifier::Const      ? tok::kw_const
                             : Qual == Qualifier::Volatile ? tok::kw_volatile
                                                           : tok::kw_restrict;

  return utils::lexer::getQualifyingToken(Tok, FileRange, *Result.Context,
                                          *Result.SourceManager);
}

static std::optional<SourceRange>
getTypeSpecifierLocation(const VarDecl *Var,
                         const MatchFinder::MatchResult &Result) {
  SourceRange TypeSpecifier(
      Var->getTypeSpecStartLoc(),
      Var->getTypeSpecEndLoc().getLocWithOffset(Lexer::MeasureTokenLength(
          Var->getTypeSpecEndLoc(), *Result.SourceManager,
          Result.Context->getLangOpts())));

  if (TypeSpecifier.getBegin().isMacroID() ||
      TypeSpecifier.getEnd().isMacroID())
    return std::nullopt;
  return TypeSpecifier;
}

static std::optional<SourceRange>
mergeReplacementRange(SourceRange &TypeSpecifier, const Token &ConstToken) {
  if (TypeSpecifier.getBegin().getLocWithOffset(-1) == ConstToken.getEndLoc()) {
    TypeSpecifier.setBegin(ConstToken.getLocation());
    return std::nullopt;
  }
  if (TypeSpecifier.getEnd().getLocWithOffset(1) == ConstToken.getLocation()) {
    TypeSpecifier.setEnd(ConstToken.getEndLoc());
    return std::nullopt;
  }
  return SourceRange(ConstToken.getLocation(), ConstToken.getEndLoc());
}

static bool isPointerConst(QualType QType) {
  const QualType Pointee = QType->getPointeeType();
  assert(!Pointee.isNull() && "can't have a null Pointee");
  return Pointee.isConstQualified();
}

static bool isAutoPointerConst(QualType QType) {
  const QualType Pointee =
      cast<AutoType>(QType->getPointeeType().getTypePtr())->desugar();
  assert(!Pointee.isNull() && "can't have a null Pointee");
  return Pointee.isConstQualified();
}

QualifiedAutoCheck::QualifiedAutoCheck(StringRef Name,
                                       ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      AddConstToQualified(Options.get("AddConstToQualified", true)),
      AllowedTypes(
          utils::options::parseStringList(Options.get("AllowedTypes", ""))),
      IgnoreAliasing(Options.get("IgnoreAliasing", true)) {}

void QualifiedAutoCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "AddConstToQualified", AddConstToQualified);
  Options.store(Opts, "AllowedTypes",
                utils::options::serializeStringList(AllowedTypes));
  Options.store(Opts, "IgnoreAliasing", IgnoreAliasing);
}

void QualifiedAutoCheck::registerMatchers(MatchFinder *Finder) {
  auto ExplicitSingleVarDecl =
      [](const ast_matchers::internal::Matcher<VarDecl> &InnerMatcher,
         StringRef ID) {
        return declStmt(
            unless(isInTemplateInstantiation()),
            hasSingleDecl(
                varDecl(unless(isImplicit()), InnerMatcher).bind(ID)));
      };
  auto ExplicitSingleVarDeclInTemplate =
      [](const ast_matchers::internal::Matcher<VarDecl> &InnerMatcher,
         StringRef ID) {
        return declStmt(
            isInTemplateInstantiation(),
            hasSingleDecl(
                varDecl(unless(isImplicit()), InnerMatcher).bind(ID)));
      };

  auto IsBoundToType = refersToType(equalsBoundNode("type"));
  auto UnlessFunctionType = unless(hasUnqualifiedDesugaredType(functionType()));

  auto IsPointerType = [this](const auto &...InnerMatchers) {
    if (this->IgnoreAliasing) {
      return qualType(
          hasUnqualifiedDesugaredType(pointerType(pointee(InnerMatchers...))));
    }
    return qualType(anyOf(qualType(pointerType(pointee(InnerMatchers...))),
                          qualType(substTemplateTypeParmType(hasReplacementType(
                              pointerType(pointee(InnerMatchers...)))))));
  };

  auto IsAutoDeducedToPointer =
      [IsPointerType](const std::vector<StringRef> &AllowedTypes,
                      const auto &...InnerMatchers) {
        return autoType(hasDeducedType(
            IsPointerType(InnerMatchers...),
            unless(hasUnqualifiedType(
                matchers::matchesAnyListedTypeName(AllowedTypes, false))),
            unless(pointerType(pointee(hasUnqualifiedType(
                matchers::matchesAnyListedTypeName(AllowedTypes, false)))))));
      };

  Finder->addMatcher(
      ExplicitSingleVarDecl(
          hasType(IsAutoDeducedToPointer(AllowedTypes, UnlessFunctionType)),
          "auto"),
      this);

  Finder->addMatcher(
      ExplicitSingleVarDeclInTemplate(
          allOf(hasType(IsAutoDeducedToPointer(
                    AllowedTypes, hasUnqualifiedType(qualType().bind("type")),
                    UnlessFunctionType)),
                anyOf(hasAncestor(
                          functionDecl(hasAnyTemplateArgument(IsBoundToType))),
                      hasAncestor(classTemplateSpecializationDecl(
                          hasAnyTemplateArgument(IsBoundToType))))),
          "auto"),
      this);
  if (!AddConstToQualified)
    return;
  Finder->addMatcher(ExplicitSingleVarDecl(
                         hasType(pointerType(pointee(autoType()))), "auto_ptr"),
                     this);
  Finder->addMatcher(
      ExplicitSingleVarDecl(hasType(lValueReferenceType(pointee(autoType()))),
                            "auto_ref"),
      this);
}

void QualifiedAutoCheck::check(const MatchFinder::MatchResult &Result) {
  if (const auto *Var = Result.Nodes.getNodeAs<VarDecl>("auto")) {
    SourceRange TypeSpecifier;
    if (std::optional<SourceRange> TypeSpec =
            getTypeSpecifierLocation(Var, Result)) {
      TypeSpecifier = *TypeSpec;
    } else {
      return;
    }

    SmallVector<SourceRange, 4> RemoveQualifiersRange;
    auto CheckQualifier = [&](bool IsPresent, Qualifier Qual) {
      if (IsPresent) {
        std::optional<Token> Token = findQualToken(Var, Qual, Result);
        if (!Token || Token->getLocation().isMacroID())
          return true; // Disregard this VarDecl.
        if (std::optional<SourceRange> Result =
                mergeReplacementRange(TypeSpecifier, *Token))
          RemoveQualifiersRange.push_back(*Result);
      }
      return false;
    };

    bool IsLocalConst = Var->getType().isLocalConstQualified();
    bool IsLocalVolatile = Var->getType().isLocalVolatileQualified();
    bool IsLocalRestrict = Var->getType().isLocalRestrictQualified();

    if (CheckQualifier(IsLocalConst, Qualifier::Const) ||
        CheckQualifier(IsLocalVolatile, Qualifier::Volatile) ||
        CheckQualifier(IsLocalRestrict, Qualifier::Restrict))
      return;

    // Check for bridging the gap between the asterisk and name.
    if (Var->getLocation() == TypeSpecifier.getEnd().getLocWithOffset(1))
      TypeSpecifier.setEnd(TypeSpecifier.getEnd().getLocWithOffset(1));

    const CharSourceRange FixItRange =
        CharSourceRange::getCharRange(TypeSpecifier);
    if (FixItRange.isInvalid())
      return;

    SourceLocation FixitLoc = FixItRange.getBegin();
    for (const SourceRange &Range : RemoveQualifiersRange)
      if (Range.getBegin() < FixitLoc)
        FixitLoc = Range.getBegin();

    const std::string ReplStr = [&] {
      const StringRef PtrConst = isPointerConst(Var->getType()) ? "const " : "";
      const StringRef LocalConst = IsLocalConst ? "const " : "";
      const StringRef LocalVol = IsLocalVolatile ? "volatile " : "";
      const StringRef LocalRestrict = IsLocalRestrict ? "__restrict " : "";
      return (PtrConst + "auto *" + LocalConst + LocalVol + LocalRestrict)
          .str();
    }();

    const DiagnosticBuilder Diag =
        diag(FixitLoc,
             "'%select{|const }0%select{|volatile }1%select{|__restrict }2auto "
             "%3' can be declared as '%4%3'")
        << IsLocalConst << IsLocalVolatile << IsLocalRestrict << Var->getName()
        << ReplStr;

    for (const SourceRange &Range : RemoveQualifiersRange)
      Diag << FixItHint::CreateRemoval(CharSourceRange::getCharRange(Range));

    Diag << FixItHint::CreateReplacement(FixItRange, ReplStr);
    return;
  }
  if (const auto *Var = Result.Nodes.getNodeAs<VarDecl>("auto_ptr")) {
    if (!isPointerConst(Var->getType()))
      return; // Pointer isn't const, no need to add const qualifier.
    if (!isAutoPointerConst(Var->getType()))
      return; // Const isn't wrapped in the auto type, so must be declared
              // explicitly.

    if (Var->getType().isLocalConstQualified()) {
      std::optional<Token> Token = findQualToken(Var, Qualifier::Const, Result);
      if (!Token || Token->getLocation().isMacroID())
        return;
    }
    if (Var->getType().isLocalVolatileQualified()) {
      std::optional<Token> Token =
          findQualToken(Var, Qualifier::Volatile, Result);
      if (!Token || Token->getLocation().isMacroID())
        return;
    }
    if (Var->getType().isLocalRestrictQualified()) {
      std::optional<Token> Token =
          findQualToken(Var, Qualifier::Restrict, Result);
      if (!Token || Token->getLocation().isMacroID())
        return;
    }

    if (std::optional<SourceRange> TypeSpec =
            getTypeSpecifierLocation(Var, Result)) {
      if (TypeSpec->isInvalid() || TypeSpec->getBegin().isMacroID() ||
          TypeSpec->getEnd().isMacroID())
        return;
      const SourceLocation InsertPos = TypeSpec->getBegin();
      diag(InsertPos,
           "'auto *%select{|const }0%select{|volatile }1%2' can be declared as "
           "'const auto *%select{|const }0%select{|volatile }1%2'")
          << Var->getType().isLocalConstQualified()
          << Var->getType().isLocalVolatileQualified() << Var->getName()
          << FixItHint::CreateInsertion(InsertPos, "const ");
    }
    return;
  }
  if (const auto *Var = Result.Nodes.getNodeAs<VarDecl>("auto_ref")) {
    if (!isPointerConst(Var->getType()))
      return; // Pointer isn't const, no need to add const qualifier.
    if (!isAutoPointerConst(Var->getType()))
      // Const isn't wrapped in the auto type, so must be declared explicitly.
      return;

    if (std::optional<SourceRange> TypeSpec =
            getTypeSpecifierLocation(Var, Result)) {
      if (TypeSpec->isInvalid() || TypeSpec->getBegin().isMacroID() ||
          TypeSpec->getEnd().isMacroID())
        return;
      const SourceLocation InsertPos = TypeSpec->getBegin();
      diag(InsertPos, "'auto &%0' can be declared as 'const auto &%0'")
          << Var->getName() << FixItHint::CreateInsertion(InsertPos, "const ");
    }
    return;
  }
}

} // namespace clang::tidy::readability
