//===--- ConstCorrectnessCheck.cpp - clang-tidy -----------------*- C++ -*-===//
//
// 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 "ConstCorrectnessCheck.h"
#include "../utils/FixItHintUtils.h"
#include "../utils/Matchers.h"
#include "../utils/OptionsUtils.h"
#include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include <cassert>

using namespace clang::ast_matchers;

namespace clang::tidy::misc {

namespace {
// FIXME: This matcher exists in some other code-review as well.
// It should probably move to ASTMatchers.
AST_MATCHER(VarDecl, isLocal) { return Node.isLocalVarDecl(); }
AST_MATCHER_P(DeclStmt, containsAnyDeclaration,
              ast_matchers::internal::Matcher<Decl>, InnerMatcher) {
  return ast_matchers::internal::matchesFirstInPointerRange(
             InnerMatcher, Node.decl_begin(), Node.decl_end(), Finder,
             Builder) != Node.decl_end();
}
AST_MATCHER(ReferenceType, isSpelledAsLValue) {
  return Node.isSpelledAsLValue();
}
AST_MATCHER(Type, isDependentType) { return Node.isDependentType(); }
} // namespace

ConstCorrectnessCheck::ConstCorrectnessCheck(StringRef Name,
                                             ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      AnalyzePointers(Options.get("AnalyzePointers", true)),
      AnalyzeReferences(Options.get("AnalyzeReferences", true)),
      AnalyzeValues(Options.get("AnalyzeValues", true)),

      WarnPointersAsPointers(Options.get("WarnPointersAsPointers", true)),
      WarnPointersAsValues(Options.get("WarnPointersAsValues", false)),

      TransformPointersAsPointers(
          Options.get("TransformPointersAsPointers", true)),
      TransformPointersAsValues(
          Options.get("TransformPointersAsValues", false)),
      TransformReferences(Options.get("TransformReferences", true)),
      TransformValues(Options.get("TransformValues", true)),

      AllowedTypes(
          utils::options::parseStringList(Options.get("AllowedTypes", ""))) {
  if (AnalyzeValues == false && AnalyzeReferences == false &&
      AnalyzePointers == false)
    this->configurationDiag(
        "The check 'misc-const-correctness' will not "
        "perform any analysis because 'AnalyzeValues', "
        "'AnalyzeReferences' and 'AnalyzePointers' are false.");
}

void ConstCorrectnessCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "AnalyzePointers", AnalyzePointers);
  Options.store(Opts, "AnalyzeReferences", AnalyzeReferences);
  Options.store(Opts, "AnalyzeValues", AnalyzeValues);

  Options.store(Opts, "WarnPointersAsPointers", WarnPointersAsPointers);
  Options.store(Opts, "WarnPointersAsValues", WarnPointersAsValues);

  Options.store(Opts, "TransformPointersAsPointers",
                TransformPointersAsPointers);
  Options.store(Opts, "TransformPointersAsValues", TransformPointersAsValues);
  Options.store(Opts, "TransformReferences", TransformReferences);
  Options.store(Opts, "TransformValues", TransformValues);

  Options.store(Opts, "AllowedTypes",
                utils::options::serializeStringList(AllowedTypes));
}

void ConstCorrectnessCheck::registerMatchers(MatchFinder *Finder) {
  const auto ConstType =
      hasType(qualType(isConstQualified(),
                       // pointee check will check the constness of pointer
                       unless(pointerType())));

  const auto ConstReference = hasType(references(isConstQualified()));
  const auto RValueReference = hasType(
      referenceType(anyOf(rValueReferenceType(), unless(isSpelledAsLValue()))));

  const auto TemplateType = anyOf(
      hasType(hasCanonicalType(templateTypeParmType())),
      hasType(substTemplateTypeParmType()), hasType(isDependentType()),
      // References to template types, their substitutions or typedefs to
      // template types need to be considered as well.
      hasType(referenceType(pointee(hasCanonicalType(templateTypeParmType())))),
      hasType(referenceType(pointee(substTemplateTypeParmType()))));

  const auto AllowedType = hasType(qualType(anyOf(
      hasDeclaration(namedDecl(matchers::matchesAnyListedName(AllowedTypes))),
      references(namedDecl(matchers::matchesAnyListedName(AllowedTypes))),
      pointerType(pointee(hasDeclaration(
          namedDecl(matchers::matchesAnyListedName(AllowedTypes))))))));

  const auto AutoTemplateType = varDecl(
      anyOf(hasType(autoType()), hasType(referenceType(pointee(autoType()))),
            hasType(pointerType(pointee(autoType())))));

  const auto FunctionPointerRef =
      hasType(hasCanonicalType(referenceType(pointee(functionType()))));

  // Match local variables which could be 'const' if not modified later.
  // Example: `int i = 10` would match `int i`.
  const auto LocalValDecl = varDecl(
      isLocal(), hasInitializer(anything()),
      unless(anyOf(ConstType, ConstReference, TemplateType,
                   hasInitializer(isInstantiationDependent()), AutoTemplateType,
                   RValueReference, FunctionPointerRef,
                   hasType(cxxRecordDecl(isLambda())), isImplicit(),
                   AllowedType)));

  // Match the function scope for which the analysis of all local variables
  // shall be run.
  const auto FunctionScope =
      functionDecl(
          hasBody(stmt(forEachDescendant(
                           declStmt(containsAnyDeclaration(
                                        LocalValDecl.bind("local-value")),
                                    unless(has(decompositionDecl())))
                               .bind("decl-stmt")))
                      .bind("scope")))
          .bind("function-decl");

  Finder->addMatcher(FunctionScope, this);
}

/// Classify for a variable in what the Const-Check is interested.
enum class VariableCategory { Value, Reference, Pointer };

void ConstCorrectnessCheck::check(const MatchFinder::MatchResult &Result) {
  const auto *LocalScope = Result.Nodes.getNodeAs<Stmt>("scope");
  const auto *Variable = Result.Nodes.getNodeAs<VarDecl>("local-value");
  const auto *Function = Result.Nodes.getNodeAs<FunctionDecl>("function-decl");
  const auto *VarDeclStmt = Result.Nodes.getNodeAs<DeclStmt>("decl-stmt");
  // It can not be guaranteed that the variable is declared isolated,
  // therefore a transformation might effect the other variables as well and
  // be incorrect.
  const bool CanBeFixIt = VarDeclStmt != nullptr && VarDeclStmt->isSingleDecl();

  /// If the variable was declared in a template it might be analyzed multiple
  /// times. Only one of those instantiations shall emit a warning. NOTE: This
  /// shall only deduplicate warnings for variables that are not instantiation
  /// dependent. Variables like 'int x = 42;' in a template that can become
  /// const emit multiple warnings otherwise.
  bool IsNormalVariableInTemplate = Function->isTemplateInstantiation();
  if (IsNormalVariableInTemplate &&
      TemplateDiagnosticsCache.contains(Variable->getBeginLoc()))
    return;

  VariableCategory VC = VariableCategory::Value;
  const QualType VT = Variable->getType();
  if (VT->isReferenceType()) {
    VC = VariableCategory::Reference;
  } else if (VT->isPointerType()) {
    VC = VariableCategory::Pointer;
  } else if (const auto *ArrayT = dyn_cast<ArrayType>(VT)) {
    if (ArrayT->getElementType()->isPointerType())
      VC = VariableCategory::Pointer;
  }

  auto CheckValue = [&]() {
    // The scope is only registered if the analysis shall be run.
    registerScope(LocalScope, Result.Context);

    // Offload const-analysis to utility function.
    if (ScopesCache[LocalScope]->isMutated(Variable))
      return;

    auto Diag = diag(Variable->getBeginLoc(),
                     "variable %0 of type %1 can be declared 'const'")
                << Variable << VT;
    if (IsNormalVariableInTemplate)
      TemplateDiagnosticsCache.insert(Variable->getBeginLoc());
    if (!CanBeFixIt)
      return;
    using namespace utils::fixit;
    if (VC == VariableCategory::Value && TransformValues) {
      Diag << addQualifierToVarDecl(*Variable, *Result.Context,
                                    Qualifiers::Const, QualifierTarget::Value,
                                    QualifierPolicy::Right);
      // FIXME: Add '{}' for default initialization if no user-defined default
      // constructor exists and there is no initializer.
      return;
    }

    if (VC == VariableCategory::Reference && TransformReferences) {
      Diag << addQualifierToVarDecl(*Variable, *Result.Context,
                                    Qualifiers::Const, QualifierTarget::Value,
                                    QualifierPolicy::Right);
      return;
    }

    if (VC == VariableCategory::Pointer && TransformPointersAsValues) {
      Diag << addQualifierToVarDecl(*Variable, *Result.Context,
                                    Qualifiers::Const, QualifierTarget::Value,
                                    QualifierPolicy::Right);
      return;
    }
  };

  auto CheckPointee = [&]() {
    assert(VC == VariableCategory::Pointer);
    registerScope(LocalScope, Result.Context);
    if (ScopesCache[LocalScope]->isPointeeMutated(Variable))
      return;
    auto Diag =
        diag(Variable->getBeginLoc(),
             "pointee of variable %0 of type %1 can be declared 'const'")
        << Variable << VT;
    if (IsNormalVariableInTemplate)
      TemplateDiagnosticsCache.insert(Variable->getBeginLoc());
    if (!CanBeFixIt)
      return;
    using namespace utils::fixit;
    if (TransformPointersAsPointers) {
      Diag << addQualifierToVarDecl(*Variable, *Result.Context,
                                    Qualifiers::Const, QualifierTarget::Pointee,
                                    QualifierPolicy::Right);
    }
  };

  // Each variable can only be in one category: Value, Pointer, Reference.
  // Analysis can be controlled for every category.
  if (VC == VariableCategory::Value && AnalyzeValues) {
    CheckValue();
    return;
  }
  if (VC == VariableCategory::Reference && AnalyzeReferences) {
    if (VT->getPointeeType()->isPointerType() && !WarnPointersAsValues)
      return;
    CheckValue();
    return;
  }
  if (VC == VariableCategory::Pointer && AnalyzePointers) {
    if (WarnPointersAsValues && !VT.isConstQualified())
      CheckValue();
    if (WarnPointersAsPointers) {
      if (const auto *PT = dyn_cast<PointerType>(VT)) {
        if (!PT->getPointeeType().isConstQualified())
          CheckPointee();
      }
      if (const auto *AT = dyn_cast<ArrayType>(VT)) {
        if (!AT->getElementType().isConstQualified()) {
          assert(AT->getElementType()->isPointerType());
          CheckPointee();
        }
      }
    }
    return;
  }
}

void ConstCorrectnessCheck::registerScope(const Stmt *LocalScope,
                                          ASTContext *Context) {
  auto &Analyzer = ScopesCache[LocalScope];
  if (!Analyzer)
    Analyzer = std::make_unique<ExprMutationAnalyzer>(*LocalScope, *Context);
}

} // namespace clang::tidy::misc
