//===--- NonConstReferences.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 "NonConstReferences.h"
#include "../utils/OptionsUtils.h"
#include "clang/AST/DeclBase.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"

using namespace clang::ast_matchers;

namespace clang {
namespace tidy {
namespace google {
namespace runtime {

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

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

void NonConstReferences::registerMatchers(MatchFinder *Finder) {
  if (!getLangOpts().CPlusPlus)
    return;

  Finder->addMatcher(
      parmVarDecl(
          unless(isInstantiated()),
          hasType(references(
              qualType(unless(isConstQualified())).bind("referenced_type"))),
          unless(hasType(rValueReferenceType())))
          .bind("param"),
      this);
}

void NonConstReferences::check(const MatchFinder::MatchResult &Result) {
  const auto *Parameter = Result.Nodes.getNodeAs<ParmVarDecl>("param");
  const auto *Function =
      dyn_cast_or_null<FunctionDecl>(Parameter->getParentFunctionOrMethod());

  if (Function == nullptr || Function->isImplicit())
    return;

  if (Function->getLocation().isMacroID())
    return;

  if (!Function->isCanonicalDecl())
    return;

  if (const auto *Method = dyn_cast<CXXMethodDecl>(Function)) {
    // Don't warn on implementations of an interface using references.
    if (Method->begin_overridden_methods() != Method->end_overridden_methods())
      return;
    // Don't warn on lambdas, as they frequently have to conform to the
    // interface defined elsewhere.
    if (Method->getParent()->isLambda())
      return;
  }

  auto ReferencedType = *Result.Nodes.getNodeAs<QualType>("referenced_type");

  if (std::find_if(WhiteListTypes.begin(), WhiteListTypes.end(),
                   [&](llvm::StringRef WhiteListType) {
                     return ReferencedType.getCanonicalType().getAsString(
                                Result.Context->getPrintingPolicy()) ==
                            WhiteListType;
                   }) != WhiteListTypes.end())
    return;

  // Don't warn on function references, they shouldn't be constant.
  if (ReferencedType->isFunctionProtoType())
    return;

  // Don't warn on dependent types in templates.
  if (ReferencedType->isDependentType())
    return;

  if (Function->isOverloadedOperator()) {
    switch (Function->getOverloadedOperator()) {
      case clang::OO_LessLess:
      case clang::OO_PlusPlus:
      case clang::OO_MinusMinus:
      case clang::OO_PlusEqual:
      case clang::OO_MinusEqual:
      case clang::OO_StarEqual:
      case clang::OO_SlashEqual:
      case clang::OO_PercentEqual:
      case clang::OO_LessLessEqual:
      case clang::OO_GreaterGreaterEqual:
      case clang::OO_PipeEqual:
      case clang::OO_CaretEqual:
      case clang::OO_AmpEqual:
        // Don't warn on the first parameter of operator<<(Stream&, ...),
        // operator++, operator-- and operation+assignment operators.
        if (Function->getParamDecl(0) == Parameter)
          return;
        break;
      case clang::OO_GreaterGreater: {
        auto isNonConstRef = [](clang::QualType T) {
          return T->isReferenceType() &&
                 !T.getNonReferenceType().isConstQualified();
        };
        // Don't warn on parameters of stream extractors:
        //   Stream& operator>>(Stream&, Value&);
        // Both parameters should be non-const references by convention.
        if (isNonConstRef(Function->getParamDecl(0)->getType()) &&
            (Function->getNumParams() < 2 || // E.g. member operator>>.
             isNonConstRef(Function->getParamDecl(1)->getType())) &&
            isNonConstRef(Function->getReturnType()))
          return;
        break;
      }
      default:
        break;
    }
  }

  // Some functions use references to comply with established standards.
  if (Function->getDeclName().isIdentifier() && Function->getName() == "swap")
    return;

  // iostream parameters are typically passed by non-const reference.
  if (StringRef(ReferencedType.getAsString()).endswith("stream"))
    return;

  if (Parameter->getName().empty()) {
    diag(Parameter->getLocation(), "non-const reference parameter at index %0, "
                                   "make it const or use a pointer")
        << Parameter->getFunctionScopeIndex();
  } else {
    diag(Parameter->getLocation(),
         "non-const reference parameter %0, make it const or use a pointer")
        << Parameter;
  }
}

} // namespace runtime
} // namespace google
} // namespace tidy
} // namespace clang
