//===----------------------------------------------------------------------===//
//
// 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 "UseScopedLockCheck.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
#include "clang/AST/Stmt.h"
#include "clang/AST/Type.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Lex/Lexer.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/Twine.h"

using namespace clang::ast_matchers;

namespace clang::tidy::modernize {

static bool isLockGuardDecl(const NamedDecl *Decl) {
  return Decl->getDeclName().isIdentifier() &&
         Decl->getName() == "lock_guard" && Decl->isInStdNamespace();
}

static bool isLockGuard(const QualType &Type) {
  if (const auto *Record = Type->getAsCanonical<RecordType>())
    if (const RecordDecl *Decl = Record->getDecl())
      return isLockGuardDecl(Decl);

  if (const auto *TemplateSpecType = Type->getAs<TemplateSpecializationType>())
    if (const TemplateDecl *Decl =
            TemplateSpecType->getTemplateName().getAsTemplateDecl())
      return isLockGuardDecl(Decl);

  return false;
}

static llvm::SmallVector<const VarDecl *>
getLockGuardsFromDecl(const DeclStmt *DS) {
  llvm::SmallVector<const VarDecl *> LockGuards;

  for (const Decl *Decl : DS->decls()) {
    if (const auto *VD = dyn_cast<VarDecl>(Decl)) {
      const QualType Type =
          VD->getType().getCanonicalType().getUnqualifiedType();
      if (isLockGuard(Type))
        LockGuards.push_back(VD);
    }
  }

  return LockGuards;
}

// Scans through the statements in a block and groups consecutive
// 'std::lock_guard' variable declarations together.
static llvm::SmallVector<llvm::SmallVector<const VarDecl *>>
findLocksInCompoundStmt(const CompoundStmt *Block,
                        const ast_matchers::MatchFinder::MatchResult &Result) {
  // store groups of consecutive 'std::lock_guard' declarations
  llvm::SmallVector<llvm::SmallVector<const VarDecl *>> LockGuardGroups;
  llvm::SmallVector<const VarDecl *> CurrentLockGuardGroup;

  auto AddAndClearCurrentGroup = [&]() {
    if (!CurrentLockGuardGroup.empty()) {
      LockGuardGroups.push_back(CurrentLockGuardGroup);
      CurrentLockGuardGroup.clear();
    }
  };

  for (const Stmt *Stmt : Block->body()) {
    if (const auto *DS = dyn_cast<DeclStmt>(Stmt)) {
      const llvm::SmallVector<const VarDecl *> LockGuards =
          getLockGuardsFromDecl(DS);

      if (!LockGuards.empty()) {
        CurrentLockGuardGroup.append(LockGuards);
        continue;
      }
    }
    AddAndClearCurrentGroup();
  }

  AddAndClearCurrentGroup();

  return LockGuardGroups;
}

// Find the exact source range of the 'lock_guard' token
static SourceRange getLockGuardRange(const TypeSourceInfo *SourceInfo) {
  const TypeLoc LockGuardTypeLoc = SourceInfo->getTypeLoc();

  return {LockGuardTypeLoc.getBeginLoc(), LockGuardTypeLoc.getEndLoc()};
}

// Find the exact source range of the 'lock_guard' name token
static SourceRange getLockGuardNameRange(const TypeSourceInfo *SourceInfo) {
  const auto TemplateLoc =
      SourceInfo->getTypeLoc().getAs<TemplateSpecializationTypeLoc>();
  if (!TemplateLoc)
    return {};

  return {TemplateLoc.getTemplateNameLoc(),
          TemplateLoc.getLAngleLoc().getLocWithOffset(-1)};
}

const static StringRef UseScopedLockMessage =
    "use 'std::scoped_lock' instead of 'std::lock_guard'";

UseScopedLockCheck::UseScopedLockCheck(StringRef Name,
                                       ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      WarnOnSingleLocks(Options.get("WarnOnSingleLocks", true)),
      WarnOnUsingAndTypedef(Options.get("WarnOnUsingAndTypedef", true)) {}

void UseScopedLockCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "WarnOnSingleLocks", WarnOnSingleLocks);
  Options.store(Opts, "WarnOnUsingAndTypedef", WarnOnUsingAndTypedef);
}

void UseScopedLockCheck::registerMatchers(MatchFinder *Finder) {
  const auto LockGuardClassDecl =
      namedDecl(hasName("lock_guard"), isInStdNamespace());

  const auto LockGuardType =
      qualType(anyOf(hasUnqualifiedDesugaredType(
                         recordType(hasDeclaration(LockGuardClassDecl))),
                     hasUnqualifiedDesugaredType(templateSpecializationType(
                         hasDeclaration(LockGuardClassDecl)))));

  const auto LockVarDecl = varDecl(hasType(LockGuardType));

  if (WarnOnSingleLocks) {
    Finder->addMatcher(
        compoundStmt(
            unless(isExpansionInSystemHeader()),
            has(declStmt(has(LockVarDecl)).bind("lock-decl-single")),
            unless(has(declStmt(unless(equalsBoundNode("lock-decl-single")),
                                has(LockVarDecl))))),
        this);
  }

  Finder->addMatcher(
      compoundStmt(unless(isExpansionInSystemHeader()),
                   has(declStmt(has(LockVarDecl)).bind("lock-decl-multiple")),
                   has(declStmt(unless(equalsBoundNode("lock-decl-multiple")),
                                has(LockVarDecl))))
          .bind("block-multiple"),
      this);

  if (WarnOnUsingAndTypedef) {
    // Match 'typedef std::lock_guard<std::mutex> Lock'
    Finder->addMatcher(typedefDecl(unless(isExpansionInSystemHeader()),
                                   hasType(hasUnderlyingType(LockGuardType)))
                           .bind("lock-guard-typedef"),
                       this);

    // Match 'using Lock = std::lock_guard<std::mutex>'
    Finder->addMatcher(typeAliasDecl(unless(isExpansionInSystemHeader()),
                                     hasType(templateSpecializationType(
                                         hasDeclaration(LockGuardClassDecl))))
                           .bind("lock-guard-using-alias"),
                       this);

    // Match 'using std::lock_guard'
    Finder->addMatcher(
        usingDecl(unless(isExpansionInSystemHeader()),
                  hasAnyUsingShadowDecl(hasTargetDecl(LockGuardClassDecl)))
            .bind("lock-guard-using-decl"),
        this);
  }
}

void UseScopedLockCheck::check(const MatchFinder::MatchResult &Result) {
  if (const auto *DS = Result.Nodes.getNodeAs<DeclStmt>("lock-decl-single")) {
    const llvm::SmallVector<const VarDecl *> Decls = getLockGuardsFromDecl(DS);
    diagOnMultipleLocks({Decls}, Result);
    return;
  }

  if (const auto *Compound =
          Result.Nodes.getNodeAs<CompoundStmt>("block-multiple")) {
    diagOnMultipleLocks(findLocksInCompoundStmt(Compound, Result), Result);
    return;
  }

  if (const auto *Typedef =
          Result.Nodes.getNodeAs<TypedefDecl>("lock-guard-typedef")) {
    diagOnSourceInfo(Typedef->getTypeSourceInfo(), Result);
    return;
  }

  if (const auto *UsingAlias =
          Result.Nodes.getNodeAs<TypeAliasDecl>("lock-guard-using-alias")) {
    diagOnSourceInfo(UsingAlias->getTypeSourceInfo(), Result);
    return;
  }

  if (const auto *Using =
          Result.Nodes.getNodeAs<UsingDecl>("lock-guard-using-decl")) {
    diagOnUsingDecl(Using, Result);
  }
}

void UseScopedLockCheck::diagOnSingleLock(
    const VarDecl *LockGuard, const MatchFinder::MatchResult &Result) {
  auto Diag = diag(LockGuard->getBeginLoc(), UseScopedLockMessage);

  const SourceRange LockGuardTypeRange =
      getLockGuardRange(LockGuard->getTypeSourceInfo());

  if (LockGuardTypeRange.isInvalid())
    return;

  // Create Fix-its only if we can find the constructor call to properly handle
  // 'std::lock_guard l(m, std::adopt_lock)' case.
  const auto *CtorCall =
      dyn_cast_if_present<CXXConstructExpr>(LockGuard->getInit());
  if (!CtorCall)
    return;

  if (CtorCall->getNumArgs() == 1) {
    Diag << FixItHint::CreateReplacement(LockGuardTypeRange,
                                         "std::scoped_lock");
    return;
  }

  if (CtorCall->getNumArgs() == 2) {
    const Expr *const *CtorArgs = CtorCall->getArgs();

    const Expr *MutexArg = CtorArgs[0];
    const Expr *AdoptLockArg = CtorArgs[1];

    const StringRef MutexSourceText = Lexer::getSourceText(
        CharSourceRange::getTokenRange(MutexArg->getSourceRange()),
        *Result.SourceManager, Result.Context->getLangOpts());
    const StringRef AdoptLockSourceText = Lexer::getSourceText(
        CharSourceRange::getTokenRange(AdoptLockArg->getSourceRange()),
        *Result.SourceManager, Result.Context->getLangOpts());

    Diag << FixItHint::CreateReplacement(LockGuardTypeRange, "std::scoped_lock")
         << FixItHint::CreateReplacement(
                SourceRange(MutexArg->getBeginLoc(), AdoptLockArg->getEndLoc()),
                (llvm::Twine(AdoptLockSourceText) + ", " + MutexSourceText)
                    .str());
    return;
  }

  llvm_unreachable("Invalid argument number of std::lock_guard constructor");
}

void UseScopedLockCheck::diagOnMultipleLocks(
    const llvm::SmallVector<llvm::SmallVector<const VarDecl *>> &LockGroups,
    const ast_matchers::MatchFinder::MatchResult &Result) {
  for (const llvm::SmallVector<const VarDecl *> &Group : LockGroups) {
    if (Group.size() == 1) {
      if (WarnOnSingleLocks)
        diagOnSingleLock(Group[0], Result);
    } else {
      diag(Group[0]->getBeginLoc(),
           "use single 'std::scoped_lock' instead of multiple "
           "'std::lock_guard'");

      for (const VarDecl *Lock : llvm::drop_begin(Group))
        diag(Lock->getLocation(), "additional 'std::lock_guard' declared here",
             DiagnosticIDs::Note);
    }
  }
}

void UseScopedLockCheck::diagOnSourceInfo(
    const TypeSourceInfo *LockGuardSourceInfo,
    const ast_matchers::MatchFinder::MatchResult &Result) {
  const TypeLoc TL = LockGuardSourceInfo->getTypeLoc();

  if (const auto TTL = TL.getAs<TemplateSpecializationTypeLoc>()) {
    auto Diag = diag(TTL.getBeginLoc(), UseScopedLockMessage);

    const SourceRange LockGuardRange =
        getLockGuardNameRange(LockGuardSourceInfo);
    if (LockGuardRange.isInvalid())
      return;

    Diag << FixItHint::CreateReplacement(LockGuardRange, "scoped_lock");
  }
}

void UseScopedLockCheck::diagOnUsingDecl(
    const UsingDecl *UsingDecl,
    const ast_matchers::MatchFinder::MatchResult &Result) {
  diag(UsingDecl->getLocation(), UseScopedLockMessage)
      << FixItHint::CreateReplacement(UsingDecl->getLocation(), "scoped_lock");
}

} // namespace clang::tidy::modernize
