//===--- UseInternalLinkageCheck.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 "UseInternalLinkageCheck.h"
#include "../utils/FileExtensionsUtils.h"
#include "clang/AST/Decl.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchersMacros.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Basic/Specifiers.h"
#include "clang/Lex/Token.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVector.h"

using namespace clang::ast_matchers;

namespace clang::tidy {

template <>
struct OptionEnumMapping<misc::UseInternalLinkageCheck::FixModeKind> {
  static llvm::ArrayRef<
      std::pair<misc::UseInternalLinkageCheck::FixModeKind, StringRef>>
  getEnumMapping() {
    static constexpr std::pair<misc::UseInternalLinkageCheck::FixModeKind,
                               StringRef>
        Mapping[] = {
            {misc::UseInternalLinkageCheck::FixModeKind::None, "None"},
            {misc::UseInternalLinkageCheck::FixModeKind::UseStatic,
             "UseStatic"},
        };
    return {Mapping};
  }
};

} // namespace clang::tidy

namespace clang::tidy::misc {

namespace {

AST_MATCHER(Decl, isFirstDecl) { return Node.isFirstDecl(); }

AST_MATCHER(FunctionDecl, hasBody) { return Node.hasBody(); }

static bool isInMainFile(SourceLocation L, SourceManager &SM,
                         const FileExtensionsSet &HeaderFileExtensions) {
  for (;;) {
    if (utils::isExpansionLocInHeaderFile(L, SM, HeaderFileExtensions))
      return false;
    if (SM.isInMainFile(L))
      return true;
    // not in header file but not in main file
    L = SM.getIncludeLoc(SM.getFileID(L));
    if (L.isValid())
      continue;
    // Conservative about the unknown
    return false;
  }
}

AST_MATCHER_P(Decl, isAllRedeclsInMainFile, FileExtensionsSet,
              HeaderFileExtensions) {
  return llvm::all_of(Node.redecls(), [&](const Decl *D) {
    return isInMainFile(D->getLocation(),
                        Finder->getASTContext().getSourceManager(),
                        HeaderFileExtensions);
  });
}

AST_POLYMORPHIC_MATCHER(isExternStorageClass,
                        AST_POLYMORPHIC_SUPPORTED_TYPES(FunctionDecl,
                                                        VarDecl)) {
  return Node.getStorageClass() == SC_Extern;
}

AST_MATCHER(FunctionDecl, isAllocationOrDeallocationOverloadedFunction) {
  // [basic.stc.dynamic.allocation]
  // An allocation function that is not a class member function shall belong to
  // the global scope and not have a name with internal linkage.
  // [basic.stc.dynamic.deallocation]
  // A deallocation function that is not a class member function shall belong to
  // the global scope and not have a name with internal linkage.
  static const llvm::DenseSet<OverloadedOperatorKind> OverloadedOperators{
      OverloadedOperatorKind::OO_New,
      OverloadedOperatorKind::OO_Array_New,
      OverloadedOperatorKind::OO_Delete,
      OverloadedOperatorKind::OO_Array_Delete,
  };
  return OverloadedOperators.contains(Node.getOverloadedOperator());
}

} // namespace

UseInternalLinkageCheck::UseInternalLinkageCheck(StringRef Name,
                                                 ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      HeaderFileExtensions(Context->getHeaderFileExtensions()),
      FixMode(Options.get("FixMode", FixModeKind::UseStatic)) {}

void UseInternalLinkageCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "FixMode", FixMode);
}

void UseInternalLinkageCheck::registerMatchers(MatchFinder *Finder) {
  auto Common =
      allOf(isFirstDecl(), isAllRedeclsInMainFile(HeaderFileExtensions),
            unless(anyOf(
                // 1. internal linkage
                isStaticStorageClass(), isInAnonymousNamespace(),
                // 2. explicit external linkage
                isExternStorageClass(), isExternC(),
                // 3. template
                isExplicitTemplateSpecialization(),
                hasAncestor(decl(anyOf(
                    // 4. friend
                    friendDecl(),
                    // 5. module export decl
                    exportDecl()))))));
  Finder->addMatcher(
      functionDecl(Common, hasBody(),
                   unless(anyOf(cxxMethodDecl(), isConsteval(),
                                isAllocationOrDeallocationOverloadedFunction(),
                                isMain())))
          .bind("fn"),
      this);
  Finder->addMatcher(
      varDecl(Common, hasGlobalStorage(), unless(hasThreadStorageDuration()))
          .bind("var"),
      this);
}

static constexpr StringRef Message =
    "%0 %1 can be made static or moved into an anonymous namespace "
    "to enforce internal linkage";

void UseInternalLinkageCheck::check(const MatchFinder::MatchResult &Result) {
  if (const auto *FD = Result.Nodes.getNodeAs<FunctionDecl>("fn")) {
    DiagnosticBuilder DB = diag(FD->getLocation(), Message) << "function" << FD;
    const SourceLocation FixLoc = FD->getInnerLocStart();
    if (FixLoc.isInvalid() || FixLoc.isMacroID())
      return;
    if (FixMode == FixModeKind::UseStatic)
      DB << FixItHint::CreateInsertion(FixLoc, "static ");
    return;
  }
  if (const auto *VD = Result.Nodes.getNodeAs<VarDecl>("var")) {
    // In C++, const variables at file scope have implicit internal linkage,
    // so we should not warn there. This is not the case in C.
    // https://eel.is/c++draft/diff#basic-3
    if (getLangOpts().CPlusPlus && VD->getType().isConstQualified())
      return;

    DiagnosticBuilder DB = diag(VD->getLocation(), Message) << "variable" << VD;
    const SourceLocation FixLoc = VD->getInnerLocStart();
    if (FixLoc.isInvalid() || FixLoc.isMacroID())
      return;
    if (FixMode == FixModeKind::UseStatic)
      DB << FixItHint::CreateInsertion(FixLoc, "static ");
    return;
  }
  llvm_unreachable("");
}

} // namespace clang::tidy::misc
