//===--- IncludeCleanerCheck.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 "IncludeCleanerCheck.h"
#include "../ClangTidyCheck.h"
#include "../ClangTidyDiagnosticConsumer.h"
#include "../ClangTidyOptions.h"
#include "../utils/OptionsUtils.h"
#include "clang-include-cleaner/Analysis.h"
#include "clang-include-cleaner/IncludeSpeller.h"
#include "clang-include-cleaner/Record.h"
#include "clang-include-cleaner/Types.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclBase.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/FileEntry.h"
#include "clang/Basic/LLVM.h"
#include "clang/Basic/LangOptions.h"
#include "clang/Basic/SourceLocation.h"
#include "clang/Format/Format.h"
#include "clang/Lex/Preprocessor.h"
#include "clang/Tooling/Core/Replacement.h"
#include "clang/Tooling/Inclusions/HeaderIncludes.h"
#include "clang/Tooling/Inclusions/StandardLibrary.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Regex.h"
#include <optional>
#include <string>
#include <vector>

using namespace clang::ast_matchers;

namespace clang::tidy::misc {

namespace {
struct MissingIncludeInfo {
  include_cleaner::SymbolReference SymRef;
  include_cleaner::Header Missing;
};
} // namespace

IncludeCleanerCheck::IncludeCleanerCheck(StringRef Name,
                                         ClangTidyContext *Context)
    : ClangTidyCheck(Name, Context),
      IgnoreHeaders(
          utils::options::parseStringList(Options.get("IgnoreHeaders", ""))),
      DeduplicateFindings(Options.get("DeduplicateFindings", true)),
      UnusedIncludes(Options.get("UnusedIncludes", true)),
      MissingIncludes(Options.get("MissingIncludes", true)) {
  for (const auto &Header : IgnoreHeaders) {
    if (!llvm::Regex{Header}.isValid())
      configurationDiag("Invalid ignore headers regex '%0'") << Header;
    std::string HeaderSuffix{Header.str()};
    if (!Header.ends_with("$"))
      HeaderSuffix += "$";
    IgnoreHeadersRegex.emplace_back(HeaderSuffix);
  }

  if (UnusedIncludes == false && MissingIncludes == false)
    this->configurationDiag("The check 'misc-include-cleaner' will not "
                            "perform any analysis because 'UnusedIncludes' and "
                            "'MissingIncludes' are both false.");
}

void IncludeCleanerCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
  Options.store(Opts, "IgnoreHeaders",
                utils::options::serializeStringList(IgnoreHeaders));
  Options.store(Opts, "DeduplicateFindings", DeduplicateFindings);
  Options.store(Opts, "UnusedIncludes", UnusedIncludes);
  Options.store(Opts, "MissingIncludes", MissingIncludes);
}

bool IncludeCleanerCheck::isLanguageVersionSupported(
    const LangOptions &LangOpts) const {
  return !LangOpts.ObjC;
}

void IncludeCleanerCheck::registerMatchers(MatchFinder *Finder) {
  Finder->addMatcher(translationUnitDecl().bind("top"), this);
}

void IncludeCleanerCheck::registerPPCallbacks(const SourceManager &SM,
                                              Preprocessor *PP,
                                              Preprocessor *ModuleExpanderPP) {
  PP->addPPCallbacks(RecordedPreprocessor.record(*PP));
  this->PP = PP;
  RecordedPI.record(*PP);
}

bool IncludeCleanerCheck::shouldIgnore(const include_cleaner::Header &H) {
  return llvm::any_of(IgnoreHeadersRegex, [&H](const llvm::Regex &R) {
    switch (H.kind()) {
    case include_cleaner::Header::Standard:
      // We don't trim angle brackets around standard library headers
      // deliberately, so that they are only matched as <vector>, otherwise
      // having just `.*/vector` might yield false positives.
      return R.match(H.standard().name());
    case include_cleaner::Header::Verbatim:
      return R.match(H.verbatim().trim("<>\""));
    case include_cleaner::Header::Physical:
      return R.match(H.physical().getFileEntry().tryGetRealPathName());
    }
    llvm_unreachable("Unknown Header kind.");
  });
}

void IncludeCleanerCheck::check(const MatchFinder::MatchResult &Result) {
  const SourceManager *SM = Result.SourceManager;
  const FileEntry *MainFile = SM->getFileEntryForID(SM->getMainFileID());
  llvm::DenseSet<const include_cleaner::Include *> Used;
  std::vector<MissingIncludeInfo> Missing;
  llvm::SmallVector<Decl *> MainFileDecls;
  for (Decl *D : Result.Nodes.getNodeAs<TranslationUnitDecl>("top")->decls()) {
    if (!SM->isWrittenInMainFile(SM->getExpansionLoc(D->getLocation())))
      continue;
    // FIXME: Filter out implicit template specializations.
    MainFileDecls.push_back(D);
  }
  llvm::DenseSet<include_cleaner::Symbol> SeenSymbols;
  OptionalDirectoryEntryRef ResourceDir =
      PP->getHeaderSearchInfo().getModuleMap().getBuiltinDir();
  // FIXME: Find a way to have less code duplication between include-cleaner
  // analysis implementation and the below code.
  walkUsed(MainFileDecls, RecordedPreprocessor.MacroReferences, &RecordedPI,
           *PP,
           [&](const include_cleaner::SymbolReference &Ref,
               llvm::ArrayRef<include_cleaner::Header> Providers) {
             // Process each symbol once to reduce noise in the findings.
             // Tidy checks are used in two different workflows:
             // - Ones that show all the findings for a given file. For such
             // workflows there is not much point in showing all the occurences,
             // as one is enough to indicate the issue.
             // - Ones that show only the findings on changed pieces. For such
             // workflows it's useful to show findings on every reference of a
             // symbol as otherwise tools might give incosistent results
             // depending on the parts of the file being edited. But it should
             // still help surface findings for "new violations" (i.e.
             // dependency did not exist in the code at all before).
             if (DeduplicateFindings && !SeenSymbols.insert(Ref.Target).second)
               return;
             bool Satisfied = false;
             for (const include_cleaner::Header &H : Providers) {
               if (H.kind() == include_cleaner::Header::Physical &&
                   (H.physical() == MainFile ||
                    H.physical().getDir() == ResourceDir)) {
                 Satisfied = true;
                 continue;
               }

               for (const include_cleaner::Include *I :
                    RecordedPreprocessor.Includes.match(H)) {
                 Used.insert(I);
                 Satisfied = true;
               }
             }
             if (!Satisfied && !Providers.empty() &&
                 Ref.RT == include_cleaner::RefType::Explicit &&
                 !shouldIgnore(Providers.front()))
               Missing.push_back({Ref, Providers.front()});
           });

  std::vector<const include_cleaner::Include *> Unused;
  for (const include_cleaner::Include &I :
       RecordedPreprocessor.Includes.all()) {
    if (Used.contains(&I) || !I.Resolved || I.Resolved->getDir() == ResourceDir)
      continue;
    if (RecordedPI.shouldKeep(*I.Resolved))
      continue;
    // Check if main file is the public interface for a private header. If so
    // we shouldn't diagnose it as unused.
    if (auto PHeader = RecordedPI.getPublic(*I.Resolved); !PHeader.empty()) {
      PHeader = PHeader.trim("<>\"");
      // Since most private -> public mappings happen in a verbatim way, we
      // check textually here. This might go wrong in presence of symlinks or
      // header mappings. But that's not different than rest of the places.
      if (getCurrentMainFile().ends_with(PHeader))
        continue;
    }
    auto StdHeader = tooling::stdlib::Header::named(
        I.quote(), PP->getLangOpts().CPlusPlus ? tooling::stdlib::Lang::CXX
                                               : tooling::stdlib::Lang::C);
    if (StdHeader && shouldIgnore(*StdHeader))
      continue;
    if (shouldIgnore(*I.Resolved))
      continue;
    Unused.push_back(&I);
  }

  llvm::StringRef Code = SM->getBufferData(SM->getMainFileID());
  auto FileStyle =
      format::getStyle(format::DefaultFormatStyle, getCurrentMainFile(),
                       format::DefaultFallbackStyle, Code,
                       &SM->getFileManager().getVirtualFileSystem());
  if (!FileStyle)
    FileStyle = format::getLLVMStyle();

  if (UnusedIncludes) {
    for (const auto *Inc : Unused) {
      diag(Inc->HashLocation, "included header %0 is not used directly")
          << llvm::sys::path::filename(Inc->Spelled,
                                       llvm::sys::path::Style::posix)
          << FixItHint::CreateRemoval(CharSourceRange::getCharRange(
                 SM->translateLineCol(SM->getMainFileID(), Inc->Line, 1),
                 SM->translateLineCol(SM->getMainFileID(), Inc->Line + 1, 1)));
    }
  }

  if (MissingIncludes) {
    tooling::HeaderIncludes HeaderIncludes(getCurrentMainFile(), Code,
                                           FileStyle->IncludeStyle);
    // Deduplicate insertions when running in bulk fix mode.
    llvm::StringSet<> InsertedHeaders{};
    for (const auto &Inc : Missing) {
      std::string Spelling = include_cleaner::spellHeader(
          {Inc.Missing, PP->getHeaderSearchInfo(), MainFile});
      bool Angled = llvm::StringRef{Spelling}.starts_with("<");
      // We might suggest insertion of an existing include in edge cases, e.g.,
      // include is present in a PP-disabled region, or spelling of the header
      // turns out to be the same as one of the unresolved includes in the
      // main file.
      if (auto Replacement = HeaderIncludes.insert(
              llvm::StringRef{Spelling}.trim("\"<>"), Angled,
              tooling::IncludeDirective::Include)) {
        DiagnosticBuilder DB =
            diag(SM->getSpellingLoc(Inc.SymRef.RefLocation),
                 "no header providing \"%0\" is directly included")
            << Inc.SymRef.Target.name();
        if (areDiagsSelfContained() ||
            InsertedHeaders.insert(Replacement->getReplacementText()).second) {
          DB << FixItHint::CreateInsertion(
              SM->getComposedLoc(SM->getMainFileID(), Replacement->getOffset()),
              Replacement->getReplacementText());
        }
      }
    }
  }
}

} // namespace clang::tidy::misc
