//===-- ApplyReplacements.cpp - Apply and deduplicate replacements --------===//
//
// 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
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file provides the implementation for deduplicating, detecting
/// conflicts in, and applying collections of Replacements.
///
/// FIXME: Use Diagnostics for output instead of llvm::errs().
///
//===----------------------------------------------------------------------===//
#include "clang-apply-replacements/Tooling/ApplyReplacements.h"
#include "clang/Basic/LangOptions.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Format/Format.h"
#include "clang/Lex/Lexer.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/Core/Diagnostic.h"
#include "clang/Tooling/DiagnosticsYaml.h"
#include "clang/Tooling/ReplacementsYaml.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
#include <array>
#include <optional>

using namespace llvm;
using namespace clang;

static void eatDiagnostics(const SMDiagnostic &, void *) {}

namespace clang {
namespace replace {

namespace detail {

static constexpr std::array<StringRef, 2> AllowedExtensions = {".yaml", ".yml"};

template <typename TranslationUnits>
static std::error_code collectReplacementsFromDirectory(
    const llvm::StringRef Directory, TranslationUnits &TUs,
    TUReplacementFiles &TUFiles, clang::DiagnosticsEngine &Diagnostics) {
  using namespace llvm::sys::fs;
  using namespace llvm::sys::path;

  std::error_code ErrorCode;

  for (recursive_directory_iterator I(Directory, ErrorCode), E;
       I != E && !ErrorCode; I.increment(ErrorCode)) {
    if (filename(I->path())[0] == '.') {
      // Indicate not to descend into directories beginning with '.'
      I.no_push();
      continue;
    }

    if (!is_contained(AllowedExtensions, extension(I->path())))
      continue;

    TUFiles.push_back(I->path());

    ErrorOr<std::unique_ptr<MemoryBuffer>> Out =
        MemoryBuffer::getFile(I->path());
    if (std::error_code BufferError = Out.getError()) {
      errs() << "Error reading " << I->path() << ": " << BufferError.message()
             << "\n";
      continue;
    }

    yaml::Input YIn(Out.get()->getBuffer(), nullptr, &eatDiagnostics);
    typename TranslationUnits::value_type TU;
    YIn >> TU;
    if (YIn.error()) {
      // File doesn't appear to be a header change description. Ignore it.
      continue;
    }

    // Only keep files that properly parse.
    TUs.push_back(TU);
  }

  return ErrorCode;
}
} // namespace detail

template <>
std::error_code collectReplacementsFromDirectory(
    const llvm::StringRef Directory, TUReplacements &TUs,
    TUReplacementFiles &TUFiles, clang::DiagnosticsEngine &Diagnostics) {
  return detail::collectReplacementsFromDirectory(Directory, TUs, TUFiles,
                                                  Diagnostics);
}

template <>
std::error_code collectReplacementsFromDirectory(
    const llvm::StringRef Directory, TUDiagnostics &TUs,
    TUReplacementFiles &TUFiles, clang::DiagnosticsEngine &Diagnostics) {
  return detail::collectReplacementsFromDirectory(Directory, TUs, TUFiles,
                                                  Diagnostics);
}

/// Extract replacements from collected TranslationUnitReplacements and
/// TranslationUnitDiagnostics and group them per file. Identical replacements
/// from diagnostics are deduplicated.
///
/// \param[in] TUs Collection of all found and deserialized
/// TranslationUnitReplacements.
/// \param[in] TUDs Collection of all found and deserialized
/// TranslationUnitDiagnostics.
/// \param[in] SM Used to deduplicate paths.
///
/// \returns A map mapping FileEntry to a set of Replacement targeting that
/// file.
static llvm::DenseMap<FileEntryRef, std::vector<tooling::Replacement>>
groupReplacements(const TUReplacements &TUs, const TUDiagnostics &TUDs,
                  const clang::SourceManager &SM) {
  llvm::StringSet<> Warned;
  llvm::DenseMap<FileEntryRef, std::vector<tooling::Replacement>>
      GroupedReplacements;

  // Deduplicate identical replacements in diagnostics unless they are from the
  // same TU.
  // FIXME: Find an efficient way to deduplicate on diagnostics level.
  llvm::DenseMap<const FileEntry *,
                 std::map<tooling::Replacement,
                          const tooling::TranslationUnitDiagnostics *>>
      DiagReplacements;

  auto AddToGroup = [&](const tooling::Replacement &R,
                        const tooling::TranslationUnitDiagnostics *SourceTU,
                        const std::optional<std::string> BuildDir) {
    // Use the file manager to deduplicate paths. FileEntries are
    // automatically canonicalized. Since relative paths can come from different
    // build directories, make them absolute immediately.
    SmallString<128> Path = R.getFilePath();
    if (BuildDir)
      llvm::sys::fs::make_absolute(*BuildDir, Path);
    else
      SM.getFileManager().makeAbsolutePath(Path);

    if (auto Entry = SM.getFileManager().getOptionalFileRef(Path)) {
      if (SourceTU) {
        auto [It, Inserted] = DiagReplacements[*Entry].try_emplace(R, SourceTU);
        if (!Inserted && It->second != SourceTU)
          // This replacement is a duplicate of one suggested by another TU.
          return;
      }
      GroupedReplacements[*Entry].push_back(R);
    } else if (Warned.insert(Path).second) {
      errs() << "Described file '" << R.getFilePath()
             << "' doesn't exist. Ignoring...\n";
    }
  };

  for (const auto &TU : TUs)
    for (const tooling::Replacement &R : TU.Replacements)
      AddToGroup(R, nullptr, {});

  for (const auto &TU : TUDs)
    for (const auto &D : TU.Diagnostics)
      if (const auto *ChoosenFix = tooling::selectFirstFix(D)) {
        for (const auto &Fix : *ChoosenFix)
          for (const tooling::Replacement &R : Fix.second)
            AddToGroup(R, &TU, D.BuildDirectory);
      }

  // Sort replacements per file to keep consistent behavior when
  // clang-apply-replacements run on differents machine.
  for (auto &FileAndReplacements : GroupedReplacements) {
    llvm::sort(FileAndReplacements.second);
  }

  return GroupedReplacements;
}

bool mergeAndDeduplicate(const TUReplacements &TUs, const TUDiagnostics &TUDs,
                         FileToChangesMap &FileChanges,
                         clang::SourceManager &SM, bool IgnoreInsertConflict) {
  auto GroupedReplacements = groupReplacements(TUs, TUDs, SM);
  bool ConflictDetected = false;

  // To report conflicting replacements on corresponding file, all replacements
  // are stored into 1 big AtomicChange.
  for (const auto &FileAndReplacements : GroupedReplacements) {
    FileEntryRef Entry = FileAndReplacements.first;
    const SourceLocation BeginLoc =
        SM.getLocForStartOfFile(SM.getOrCreateFileID(Entry, SrcMgr::C_User));
    tooling::AtomicChange FileChange(Entry.getName(), Entry.getName());
    for (const auto &R : FileAndReplacements.second) {
      llvm::Error Err =
          FileChange.replace(SM, BeginLoc.getLocWithOffset(R.getOffset()),
                             R.getLength(), R.getReplacementText());
      if (Err) {
        // FIXME: This will report conflicts by pair using a file+offset format
        // which is not so much human readable.
        // A first improvement could be to translate offset to line+col. For
        // this and without loosing error message some modifications around
        // `tooling::ReplacementError` are need (access to
        // `getReplacementErrString`).
        // A better strategy could be to add a pretty printer methods for
        // conflict reporting. Methods that could be parameterized to report a
        // conflict in different format, file+offset, file+line+col, or even
        // more human readable using VCS conflict markers.
        // For now, printing directly the error reported by `AtomicChange` is
        // the easiest solution.
        errs() << llvm::toString(std::move(Err)) << "\n";
        if (IgnoreInsertConflict) {
          tooling::Replacements &Replacements = FileChange.getReplacements();
          unsigned NewOffset =
              Replacements.getShiftedCodePosition(R.getOffset());
          unsigned NewLength = Replacements.getShiftedCodePosition(
                                   R.getOffset() + R.getLength()) -
                               NewOffset;
          if (NewLength == R.getLength()) {
            tooling::Replacement RR = tooling::Replacement(
                R.getFilePath(), NewOffset, NewLength, R.getReplacementText());
            Replacements = Replacements.merge(tooling::Replacements(RR));
          } else {
            llvm::errs()
                << "Can't resolve conflict, skipping the replacement.\n";
            ConflictDetected = true;
          }
        } else
          ConflictDetected = true;
      }
    }
    FileChanges.try_emplace(Entry,
                            std::vector<tooling::AtomicChange>{FileChange});
  }

  return !ConflictDetected;
}

llvm::Expected<std::string>
applyChanges(StringRef File, const std::vector<tooling::AtomicChange> &Changes,
             const tooling::ApplyChangesSpec &Spec,
             DiagnosticsEngine &Diagnostics) {
  FileManager Files((FileSystemOptions()));
  SourceManager SM(Diagnostics, Files);

  llvm::ErrorOr<std::unique_ptr<MemoryBuffer>> Buffer =
      SM.getFileManager().getBufferForFile(File);
  if (!Buffer)
    return errorCodeToError(Buffer.getError());
  return tooling::applyAtomicChanges(File, Buffer.get()->getBuffer(), Changes,
                                     Spec);
}

bool deleteReplacementFiles(const TUReplacementFiles &Files,
                            clang::DiagnosticsEngine &Diagnostics) {
  bool Success = true;
  for (const auto &Filename : Files) {
    std::error_code Error = llvm::sys::fs::remove(Filename);
    if (Error) {
      Success = false;
      // FIXME: Use Diagnostics for outputting errors.
      errs() << "Error deleting file: " << Filename << "\n";
      errs() << Error.message() << "\n";
      errs() << "Please delete the file manually\n";
    }
  }
  return Success;
}

} // end namespace replace
} // end namespace clang
