//===--- ContainerDataPointerCheck.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 "ContainerDataPointerCheck.h"

#include "../utils/Matchers.h"
#include "../utils/OptionsUtils.h"
#include "clang/Lex/Lexer.h"
#include "llvm/ADT/StringRef.h"

using namespace clang::ast_matchers;

namespace clang::tidy::readability {

constexpr llvm::StringLiteral ContainerExprName = "container-expr";
constexpr llvm::StringLiteral DerefContainerExprName = "deref-container-expr";
constexpr llvm::StringLiteral AddrOfContainerExprName =
    "addr-of-container-expr";
constexpr llvm::StringLiteral AddressOfName = "address-of";

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

ContainerDataPointerCheck::ContainerDataPointerCheck(StringRef Name,
                                                     ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      IgnoredContainers(utils::options::parseStringList(
          Options.get("IgnoredContainers", ""))) {}

void ContainerDataPointerCheck::registerMatchers(MatchFinder *Finder) {
  const auto Record =
      cxxRecordDecl(
          unless(matchers::matchesAnyListedName(IgnoredContainers)),
          isSameOrDerivedFrom(
              namedDecl(
                  has(cxxMethodDecl(isPublic(), hasName("data")).bind("data")))
                  .bind("container")))
          .bind("record");

  const auto NonTemplateContainerType =
      qualType(hasUnqualifiedDesugaredType(recordType(hasDeclaration(Record))));
  const auto TemplateContainerType =
      qualType(hasUnqualifiedDesugaredType(templateSpecializationType(
          hasDeclaration(classTemplateDecl(has(Record))))));

  const auto Container =
      qualType(anyOf(NonTemplateContainerType, TemplateContainerType));

  const auto ContainerExpr = anyOf(
      unaryOperator(
          hasOperatorName("*"),
          hasUnaryOperand(
              expr(hasType(pointsTo(Container))).bind(DerefContainerExprName)))
          .bind(ContainerExprName),
      unaryOperator(hasOperatorName("&"),
                    hasUnaryOperand(expr(anyOf(hasType(Container),
                                               hasType(references(Container))))
                                        .bind(AddrOfContainerExprName)))
          .bind(ContainerExprName),
      expr(anyOf(hasType(Container), hasType(pointsTo(Container)),
                 hasType(references(Container))))
          .bind(ContainerExprName));

  const auto Zero = integerLiteral(equals(0));

  const auto SubscriptOperator = callee(cxxMethodDecl(hasName("operator[]")));

  Finder->addMatcher(
      unaryOperator(
          unless(isExpansionInSystemHeader()), hasOperatorName("&"),
          hasUnaryOperand(expr(
              anyOf(cxxOperatorCallExpr(SubscriptOperator, argumentCountIs(2),
                                        hasArgument(0, ContainerExpr),
                                        hasArgument(1, Zero)),
                    cxxMemberCallExpr(SubscriptOperator, on(ContainerExpr),
                                      argumentCountIs(1), hasArgument(0, Zero)),
                    arraySubscriptExpr(hasLHS(ContainerExpr), hasRHS(Zero))))))
          .bind(AddressOfName),
      this);
}

void ContainerDataPointerCheck::check(const MatchFinder::MatchResult &Result) {
  const auto *UO = Result.Nodes.getNodeAs<UnaryOperator>(AddressOfName);
  const auto *CE = Result.Nodes.getNodeAs<Expr>(ContainerExprName);
  const auto *DCE = Result.Nodes.getNodeAs<Expr>(DerefContainerExprName);
  const auto *ACE = Result.Nodes.getNodeAs<Expr>(AddrOfContainerExprName);

  if (!UO || !CE)
    return;

  if (DCE && !CE->getType()->isPointerType())
    CE = DCE;
  else if (ACE)
    CE = ACE;

  SourceRange SrcRange = CE->getSourceRange();

  std::string ReplacementText{
      Lexer::getSourceText(CharSourceRange::getTokenRange(SrcRange),
                           *Result.SourceManager, getLangOpts())};

  if (!isa<DeclRefExpr, ArraySubscriptExpr, CXXOperatorCallExpr, CallExpr,
           MemberExpr>(CE))
    ReplacementText = "(" + ReplacementText + ")";

  if (CE->getType()->isPointerType())
    ReplacementText += "->data()";
  else
    ReplacementText += ".data()";

  FixItHint Hint =
      FixItHint::CreateReplacement(UO->getSourceRange(), ReplacementText);
  diag(UO->getBeginLoc(),
       "'data' should be used for accessing the data pointer instead of taking "
       "the address of the 0-th element")
      << Hint;
}
} // namespace clang::tidy::readability
