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

using namespace clang::ast_matchers;
using namespace clang;

namespace {

AST_MATCHER(NamedDecl, isOperatorDecl) {
  const DeclarationName::NameKind NK = Node.getDeclName().getNameKind();
  return NK != DeclarationName::Identifier &&
         NK != DeclarationName::CXXConstructorName &&
         NK != DeclarationName::CXXDestructorName;
}

} // namespace

namespace clang::tidy {

template <>
struct OptionEnumMapping<
    misc::OverrideWithDifferentVisibilityCheck::ChangeKind> {
  static llvm::ArrayRef<std::pair<
      misc::OverrideWithDifferentVisibilityCheck::ChangeKind, StringRef>>
  getEnumMapping() {
    static constexpr std::pair<
        misc::OverrideWithDifferentVisibilityCheck::ChangeKind, StringRef>
        Mapping[] = {
            {misc::OverrideWithDifferentVisibilityCheck::ChangeKind::Any,
             "any"},
            {misc::OverrideWithDifferentVisibilityCheck::ChangeKind::Widening,
             "widening"},
            {misc::OverrideWithDifferentVisibilityCheck::ChangeKind::Narrowing,
             "narrowing"},
        };
    return {Mapping};
  }
};

namespace misc {

OverrideWithDifferentVisibilityCheck::OverrideWithDifferentVisibilityCheck(
    StringRef Name, ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      DetectVisibilityChange(
          Options.get("DisallowedVisibilityChange", ChangeKind::Any)),
      CheckDestructors(Options.get("CheckDestructors", false)),
      CheckOperators(Options.get("CheckOperators", false)),
      IgnoredFunctions(utils::options::parseStringList(
          Options.get("IgnoredFunctions", ""))) {}

void OverrideWithDifferentVisibilityCheck::storeOptions(
    ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "DisallowedVisibilityChange", DetectVisibilityChange);
  Options.store(Opts, "CheckDestructors", CheckDestructors);
  Options.store(Opts, "CheckOperators", CheckOperators);
  Options.store(Opts, "IgnoredFunctions",
                utils::options::serializeStringList(IgnoredFunctions));
}

void OverrideWithDifferentVisibilityCheck::registerMatchers(
    MatchFinder *Finder) {
  const auto IgnoredDecl =
      namedDecl(matchers::matchesAnyListedName(IgnoredFunctions));
  const auto FilterDestructors =
      CheckDestructors ? decl() : decl(unless(cxxDestructorDecl()));
  const auto FilterOperators =
      CheckOperators ? namedDecl() : namedDecl(unless(isOperatorDecl()));
  Finder->addMatcher(
      cxxMethodDecl(
          isVirtual(), FilterDestructors, FilterOperators,
          ofClass(
              cxxRecordDecl(unless(isExpansionInSystemHeader())).bind("class")),
          forEachOverridden(cxxMethodDecl(ofClass(cxxRecordDecl().bind("base")),
                                          unless(IgnoredDecl))
                                .bind("base_func")))
          .bind("func"),
      this);
}

void OverrideWithDifferentVisibilityCheck::check(
    const MatchFinder::MatchResult &Result) {
  const auto *const MatchedFunction =
      Result.Nodes.getNodeAs<FunctionDecl>("func");
  if (!MatchedFunction->isCanonicalDecl())
    return;

  const auto *const ParentClass =
      Result.Nodes.getNodeAs<CXXRecordDecl>("class");
  const auto *const BaseClass = Result.Nodes.getNodeAs<CXXRecordDecl>("base");
  CXXBasePaths Paths;
  if (!ParentClass->isDerivedFrom(BaseClass, Paths))
    return;

  const auto *const OverriddenFunction =
      Result.Nodes.getNodeAs<FunctionDecl>("base_func");
  const AccessSpecifier ActualAccess = MatchedFunction->getAccess();
  AccessSpecifier OverriddenAccess = OverriddenFunction->getAccess();

  const CXXBaseSpecifier *InheritanceWithStrictVisibility = nullptr;
  for (const CXXBasePath &Path : Paths) {
    for (const CXXBasePathElement &Elem : Path) {
      if (Elem.Base->getAccessSpecifier() > OverriddenAccess) {
        OverriddenAccess = Elem.Base->getAccessSpecifier();
        InheritanceWithStrictVisibility = Elem.Base;
      }
    }
  }

  if (ActualAccess != OverriddenAccess) {
    if (DetectVisibilityChange == ChangeKind::Widening &&
        ActualAccess > OverriddenAccess)
      return;
    if (DetectVisibilityChange == ChangeKind::Narrowing &&
        ActualAccess < OverriddenAccess)
      return;

    if (InheritanceWithStrictVisibility) {
      diag(MatchedFunction->getLocation(),
           "visibility of function %0 is changed from %1 (through %1 "
           "inheritance of class %2) to %3")
          << MatchedFunction << OverriddenAccess
          << InheritanceWithStrictVisibility->getType() << ActualAccess;
      diag(InheritanceWithStrictVisibility->getBeginLoc(),
           "%0 is inherited as %1 here", DiagnosticIDs::Note)
          << InheritanceWithStrictVisibility->getType() << OverriddenAccess;
    } else {
      diag(MatchedFunction->getLocation(),
           "visibility of function %0 is changed from %1 in class %2 to %3")
          << MatchedFunction << OverriddenAccess << BaseClass << ActualAccess;
    }
    diag(OverriddenFunction->getLocation(), "function declared here as %0",
         DiagnosticIDs::Note)
        << OverriddenFunction->getAccess();
  }
}

} // namespace misc

} // namespace clang::tidy
