//===--- DeprecatedHeadersCheck.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 "DeprecatedHeadersCheck.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Lex/PPCallbacks.h"
#include "clang/Lex/Preprocessor.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringSet.h"

#include <algorithm>
#include <vector>

using IncludeMarker =
    clang::tidy::modernize::DeprecatedHeadersCheck::IncludeMarker;
namespace clang::tidy::modernize {
namespace {

class IncludeModernizePPCallbacks : public PPCallbacks {
public:
  explicit IncludeModernizePPCallbacks(
      std::vector<IncludeMarker> &IncludesToBeProcessed, LangOptions LangOpts,
      const SourceManager &SM, bool CheckHeaderFile);

  void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok,
                          StringRef FileName, bool IsAngled,
                          CharSourceRange FilenameRange,
                          OptionalFileEntryRef File, StringRef SearchPath,
                          StringRef RelativePath, const Module *Imported,
                          SrcMgr::CharacteristicKind FileType) override;

private:
  std::vector<IncludeMarker> &IncludesToBeProcessed;
  LangOptions LangOpts;
  llvm::StringMap<std::string> CStyledHeaderToCxx;
  llvm::StringSet<> DeleteHeaders;
  const SourceManager &SM;
  bool CheckHeaderFile;
};

class ExternCRefutationVisitor
    : public RecursiveASTVisitor<ExternCRefutationVisitor> {
  std::vector<IncludeMarker> &IncludesToBeProcessed;
  const SourceManager &SM;

public:
  ExternCRefutationVisitor(std::vector<IncludeMarker> &IncludesToBeProcessed,
                           SourceManager &SM)
      : IncludesToBeProcessed(IncludesToBeProcessed), SM(SM) {}
  bool shouldWalkTypesOfTypeLocs() const { return false; }
  bool shouldVisitLambdaBody() const { return false; }

  bool VisitLinkageSpecDecl(LinkageSpecDecl *LinkSpecDecl) const {
    if (LinkSpecDecl->getLanguage() != LinkageSpecDecl::lang_c ||
        !LinkSpecDecl->hasBraces())
      return true;

    auto ExternCBlockBegin = LinkSpecDecl->getBeginLoc();
    auto ExternCBlockEnd = LinkSpecDecl->getEndLoc();
    auto IsWrapped = [=, &SM = SM](const IncludeMarker &Marker) -> bool {
      return SM.isBeforeInTranslationUnit(ExternCBlockBegin, Marker.DiagLoc) &&
             SM.isBeforeInTranslationUnit(Marker.DiagLoc, ExternCBlockEnd);
    };

    llvm::erase_if(IncludesToBeProcessed, IsWrapped);
    return true;
  }
};
} // namespace

DeprecatedHeadersCheck::DeprecatedHeadersCheck(StringRef Name,
                                               ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      CheckHeaderFile(Options.get("CheckHeaderFile", false)) {}

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

void DeprecatedHeadersCheck::registerPPCallbacks(
    const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) {
  PP->addPPCallbacks(std::make_unique<IncludeModernizePPCallbacks>(
      IncludesToBeProcessed, getLangOpts(), PP->getSourceManager(),
      CheckHeaderFile));
}
void DeprecatedHeadersCheck::registerMatchers(
    ast_matchers::MatchFinder *Finder) {
  // Even though the checker operates on a "preprocessor" level, we still need
  // to act on a "TranslationUnit" to acquire the AST where we can walk each
  // Decl and look for `extern "C"` blocks where we will suppress the report we
  // collected during the preprocessing phase.
  // The `onStartOfTranslationUnit()` won't suffice, since we need some handle
  // to the `ASTContext`.
  Finder->addMatcher(ast_matchers::translationUnitDecl().bind("TU"), this);
}

void DeprecatedHeadersCheck::onEndOfTranslationUnit() {
  IncludesToBeProcessed.clear();
}

void DeprecatedHeadersCheck::check(
    const ast_matchers::MatchFinder::MatchResult &Result) {
  SourceManager &SM = Result.Context->getSourceManager();

  // Suppress includes wrapped by `extern "C" { ... }` blocks.
  ExternCRefutationVisitor Visitor(IncludesToBeProcessed, SM);
  Visitor.TraverseAST(*Result.Context);

  // Emit all the remaining reports.
  for (const IncludeMarker &Marker : IncludesToBeProcessed) {
    if (Marker.Replacement.empty()) {
      diag(Marker.DiagLoc,
           "including '%0' has no effect in C++; consider removing it")
          << Marker.FileName
          << FixItHint::CreateRemoval(Marker.ReplacementRange);
    } else {
      diag(Marker.DiagLoc, "inclusion of deprecated C++ header "
                           "'%0'; consider using '%1' instead")
          << Marker.FileName << Marker.Replacement
          << FixItHint::CreateReplacement(
                 Marker.ReplacementRange,
                 (llvm::Twine("<") + Marker.Replacement + ">").str());
    }
  }
}

IncludeModernizePPCallbacks::IncludeModernizePPCallbacks(
    std::vector<IncludeMarker> &IncludesToBeProcessed, LangOptions LangOpts,
    const SourceManager &SM, bool CheckHeaderFile)
    : IncludesToBeProcessed(IncludesToBeProcessed), LangOpts(LangOpts), SM(SM),
      CheckHeaderFile(CheckHeaderFile) {
  for (const auto &KeyValue :
       std::vector<std::pair<llvm::StringRef, std::string>>(
           {{"assert.h", "cassert"},
            {"complex.h", "complex"},
            {"ctype.h", "cctype"},
            {"errno.h", "cerrno"},
            {"float.h", "cfloat"},
            {"limits.h", "climits"},
            {"locale.h", "clocale"},
            {"math.h", "cmath"},
            {"setjmp.h", "csetjmp"},
            {"signal.h", "csignal"},
            {"stdarg.h", "cstdarg"},
            {"stddef.h", "cstddef"},
            {"stdio.h", "cstdio"},
            {"stdlib.h", "cstdlib"},
            {"string.h", "cstring"},
            {"time.h", "ctime"},
            {"wchar.h", "cwchar"},
            {"wctype.h", "cwctype"}})) {
    CStyledHeaderToCxx.insert(KeyValue);
  }
  // Add C++ 11 headers.
  if (LangOpts.CPlusPlus11) {
    for (const auto &KeyValue :
         std::vector<std::pair<llvm::StringRef, std::string>>(
             {{"fenv.h", "cfenv"},
              {"stdint.h", "cstdint"},
              {"inttypes.h", "cinttypes"},
              {"tgmath.h", "ctgmath"},
              {"uchar.h", "cuchar"}})) {
      CStyledHeaderToCxx.insert(KeyValue);
    }
  }
  for (const auto &Key :
       std::vector<std::string>({"stdalign.h", "stdbool.h", "iso646.h"})) {
    DeleteHeaders.insert(Key);
  }
}

void IncludeModernizePPCallbacks::InclusionDirective(
    SourceLocation HashLoc, const Token &IncludeTok, StringRef FileName,
    bool IsAngled, CharSourceRange FilenameRange, OptionalFileEntryRef File,
    StringRef SearchPath, StringRef RelativePath, const Module *Imported,
    SrcMgr::CharacteristicKind FileType) {

  // If we don't want to warn for non-main file reports and this is one, skip
  // it.
  if (!CheckHeaderFile && !SM.isInMainFile(HashLoc))
    return;

  // Ignore system headers.
  if (SM.isInSystemHeader(HashLoc))
    return;

  // FIXME: Take care of library symbols from the global namespace.
  //
  // Reasonable options for the check:
  //
  // 1. Insert std prefix for every such symbol occurrence.
  // 2. Insert `using namespace std;` to the beginning of TU.
  // 3. Do nothing and let the user deal with the migration himself.
  SourceLocation DiagLoc = FilenameRange.getBegin();
  if (CStyledHeaderToCxx.count(FileName) != 0) {
    IncludesToBeProcessed.push_back(
        IncludeMarker{CStyledHeaderToCxx[FileName], FileName,
                      FilenameRange.getAsRange(), DiagLoc});
  } else if (DeleteHeaders.count(FileName) != 0) {
    IncludesToBeProcessed.push_back(
        IncludeMarker{std::string{}, FileName,
                      SourceRange{HashLoc, FilenameRange.getEnd()}, DiagLoc});
  }
}

} // namespace clang::tidy::modernize
