//===--- ClangTidyDiagnosticConsumer.h - clang-tidy -------------*- C++ -*-===//
//
// 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
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CLANGTIDYDIAGNOSTICCONSUMER_H
#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CLANGTIDYDIAGNOSTICCONSUMER_H

#include "ClangTidyOptions.h"
#include "ClangTidyProfiling.h"
#include "FileExtensionsSet.h"
#include "NoLintDirectiveHandler.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/Tooling/Core/Diagnostic.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Support/Regex.h"
#include <optional>

namespace clang {

class ASTContext;
class SourceManager;

namespace tidy {
class CachedGlobList;

/// A detected error complete with information to display diagnostic and
/// automatic fix.
///
/// This is used as an intermediate format to transport Diagnostics without a
/// dependency on a SourceManager.
///
/// FIXME: Make Diagnostics flexible enough to support this directly.
struct ClangTidyError : tooling::Diagnostic {
  ClangTidyError(StringRef CheckName, Level DiagLevel, StringRef BuildDirectory,
                 bool IsWarningAsError);

  bool IsWarningAsError;
  std::vector<std::string> EnabledDiagnosticAliases;
};

/// Contains displayed and ignored diagnostic counters for a ClangTidy run.
struct ClangTidyStats {
  unsigned ErrorsDisplayed = 0;
  unsigned ErrorsIgnoredCheckFilter = 0;
  unsigned ErrorsIgnoredNOLINT = 0;
  unsigned ErrorsIgnoredNonUserCode = 0;
  unsigned ErrorsIgnoredLineFilter = 0;

  unsigned errorsIgnored() const {
    return ErrorsIgnoredNOLINT + ErrorsIgnoredCheckFilter +
           ErrorsIgnoredNonUserCode + ErrorsIgnoredLineFilter;
  }
};

/// Every \c ClangTidyCheck reports errors through a \c DiagnosticsEngine
/// provided by this context.
///
/// A \c ClangTidyCheck always has access to the active context to report
/// warnings like:
/// \code
/// Context->Diag(Loc, "Single-argument constructors must be explicit")
///     << FixItHint::CreateInsertion(Loc, "explicit ");
/// \endcode
class ClangTidyContext {
public:
  /// Initializes \c ClangTidyContext instance.
  ClangTidyContext(std::unique_ptr<ClangTidyOptionsProvider> OptionsProvider,
                   bool AllowEnablingAnalyzerAlphaCheckers = false,
                   bool EnableModuleHeadersParsing = false);
  /// Sets the DiagnosticsEngine that diag() will emit diagnostics to.
  // FIXME: this is required initialization, and should be a constructor param.
  // Fix the context -> diag engine -> consumer -> context initialization cycle.
  void setDiagnosticsEngine(std::unique_ptr<DiagnosticOptions> DiagOpts,
                            DiagnosticsEngine *DiagEngine) {
    this->DiagOpts = std::move(DiagOpts);
    this->DiagEngine = DiagEngine;
  }

  ~ClangTidyContext();

  ClangTidyContext(const ClangTidyContext &) = delete;
  ClangTidyContext &operator=(const ClangTidyContext &) = delete;

  /// Report any errors detected using this method.
  ///
  /// This is still under heavy development and will likely change towards using
  /// tablegen'd diagnostic IDs.
  /// FIXME: Figure out a way to manage ID spaces.
  DiagnosticBuilder diag(StringRef CheckName, SourceLocation Loc,
                         StringRef Description,
                         DiagnosticIDs::Level Level = DiagnosticIDs::Warning);

  DiagnosticBuilder diag(StringRef CheckName, StringRef Description,
                         DiagnosticIDs::Level Level = DiagnosticIDs::Warning);

  DiagnosticBuilder diag(const tooling::Diagnostic &Error);

  /// Report any errors to do with reading the configuration using this method.
  DiagnosticBuilder
  configurationDiag(StringRef Message,
                    DiagnosticIDs::Level Level = DiagnosticIDs::Warning);

  /// Check whether a given diagnostic should be suppressed due to the presence
  /// of a "NOLINT" suppression comment.
  /// This is exposed so that other tools that present clang-tidy diagnostics
  /// (such as clangd) can respect the same suppression rules as clang-tidy.
  /// This does not handle suppression of notes following a suppressed
  /// diagnostic; that is left to the caller as it requires maintaining state in
  /// between calls to this function.
  /// If any NOLINT is malformed, e.g. a BEGIN without a subsequent END, output
  /// \param NoLintErrors will return an error about it.
  /// If \param AllowIO is false, the function does not attempt to read source
  /// files from disk which are not already mapped into memory; such files are
  /// treated as not containing a suppression comment.
  /// \param EnableNoLintBlocks controls whether to honor NOLINTBEGIN/NOLINTEND
  /// blocks; if false, only considers line-level disabling.
  bool
  shouldSuppressDiagnostic(DiagnosticsEngine::Level DiagLevel,
                           const Diagnostic &Info,
                           SmallVectorImpl<tooling::Diagnostic> &NoLintErrors,
                           bool AllowIO = true, bool EnableNoLintBlocks = true);

  /// Sets the \c SourceManager of the used \c DiagnosticsEngine.
  ///
  /// This is called from the \c ClangTidyCheck base class.
  void setSourceManager(SourceManager *SourceMgr);

  /// Should be called when starting to process new translation unit.
  void setCurrentFile(StringRef File);

  /// Returns the main file name of the current translation unit.
  StringRef getCurrentFile() const { return CurrentFile; }

  /// Sets ASTContext for the current translation unit.
  void setASTContext(ASTContext *Context);

  /// Gets the language options from the AST context.
  const LangOptions &getLangOpts() const { return LangOpts; }

  /// Returns the name of the clang-tidy check which produced this
  /// diagnostic ID.
  std::string getCheckName(unsigned DiagnosticID) const;

  /// Returns \c true if the check is enabled for the \c CurrentFile.
  ///
  /// The \c CurrentFile can be changed using \c setCurrentFile.
  bool isCheckEnabled(StringRef CheckName) const;

  /// Returns \c true if the check should be upgraded to error for the
  /// \c CurrentFile.
  bool treatAsError(StringRef CheckName) const;

  /// Returns global options.
  const ClangTidyGlobalOptions &getGlobalOptions() const;

  /// Returns options for \c CurrentFile.
  ///
  /// The \c CurrentFile can be changed using \c setCurrentFile.
  const ClangTidyOptions &getOptions() const;

  /// Returns options for \c File. Does not change or depend on
  /// \c CurrentFile.
  ClangTidyOptions getOptionsForFile(StringRef File) const;

  const FileExtensionsSet &getHeaderFileExtensions() const {
    return HeaderFileExtensions;
  }

  const FileExtensionsSet &getImplementationFileExtensions() const {
    return ImplementationFileExtensions;
  }

  /// Returns \c ClangTidyStats containing issued and ignored diagnostic
  /// counters.
  const ClangTidyStats &getStats() const { return Stats; }

  /// Control profile collection in clang-tidy.
  void setEnableProfiling(bool Profile);
  bool getEnableProfiling() const { return Profile; }

  /// Control storage of profile date.
  void setProfileStoragePrefix(StringRef ProfilePrefix);
  std::optional<ClangTidyProfiling::StorageParams>
  getProfileStorageParams() const;

  /// Should be called when starting to process new translation unit.
  void setCurrentBuildDirectory(StringRef BuildDirectory) {
    CurrentBuildDirectory = std::string(BuildDirectory);
  }

  /// Returns build directory of the current translation unit.
  const std::string &getCurrentBuildDirectory() const {
    return CurrentBuildDirectory;
  }

  /// If the experimental alpha checkers from the static analyzer can be
  /// enabled.
  bool canEnableAnalyzerAlphaCheckers() const {
    return AllowEnablingAnalyzerAlphaCheckers;
  }

  // This method determines whether preprocessor-level module header parsing is
  // enabled using the `--experimental-enable-module-headers-parsing` option.
  bool canEnableModuleHeadersParsing() const {
    return EnableModuleHeadersParsing;
  }

  void setSelfContainedDiags(bool Value) { SelfContainedDiags = Value; }

  bool areDiagsSelfContained() const { return SelfContainedDiags; }

  using DiagLevelAndFormatString = std::pair<DiagnosticIDs::Level, std::string>;
  DiagLevelAndFormatString getDiagLevelAndFormatString(unsigned DiagnosticID,
                                                       SourceLocation Loc) {
    return {static_cast<DiagnosticIDs::Level>(
                DiagEngine->getDiagnosticLevel(DiagnosticID, Loc)),
            std::string(
                DiagEngine->getDiagnosticIDs()->getDescription(DiagnosticID))};
  }

  void setOptionsCollector(llvm::StringSet<> *Collector) {
    OptionsCollector = Collector;
  }
  llvm::StringSet<> *getOptionsCollector() const { return OptionsCollector; }

private:
  // Writes to Stats.
  friend class ClangTidyDiagnosticConsumer;

  std::unique_ptr<DiagnosticOptions> DiagOpts = nullptr;
  DiagnosticsEngine *DiagEngine = nullptr;
  std::unique_ptr<ClangTidyOptionsProvider> OptionsProvider;

  std::string CurrentFile;
  ClangTidyOptions CurrentOptions;

  std::unique_ptr<CachedGlobList> CheckFilter;
  std::unique_ptr<CachedGlobList> WarningAsErrorFilter;

  FileExtensionsSet HeaderFileExtensions;
  FileExtensionsSet ImplementationFileExtensions;

  LangOptions LangOpts;

  ClangTidyStats Stats;

  std::string CurrentBuildDirectory;

  llvm::DenseMap<unsigned, std::string> CheckNamesByDiagnosticID;

  bool Profile = false;
  std::string ProfilePrefix;

  bool AllowEnablingAnalyzerAlphaCheckers;
  bool EnableModuleHeadersParsing;

  bool SelfContainedDiags = false;

  NoLintDirectiveHandler NoLintHandler;
  llvm::StringSet<> *OptionsCollector = nullptr;
};

/// Gets the Fix attached to \p Diagnostic.
/// If there isn't a Fix attached to the diagnostic and \p AnyFix is true, Check
/// to see if exactly one note has a Fix and return it. Otherwise return
/// nullptr.
const llvm::StringMap<tooling::Replacements> *
getFixIt(const tooling::Diagnostic &Diagnostic, bool AnyFix);

/// A diagnostic consumer that turns each \c Diagnostic into a
/// \c SourceManager-independent \c ClangTidyError.
// FIXME: If we move away from unit-tests, this can be moved to a private
// implementation file.
class ClangTidyDiagnosticConsumer : public DiagnosticConsumer {
public:
  /// \param EnableNolintBlocks Enables diagnostic-disabling inside blocks of
  /// code, delimited by NOLINTBEGIN and NOLINTEND.
  ClangTidyDiagnosticConsumer(ClangTidyContext &Ctx,
                              DiagnosticsEngine *ExternalDiagEngine = nullptr,
                              bool RemoveIncompatibleErrors = true,
                              bool GetFixesFromNotes = false,
                              bool EnableNolintBlocks = true);

  // FIXME: The concept of converting between FixItHints and Replacements is
  // more generic and should be pulled out into a more useful Diagnostics
  // library.
  void HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
                        const Diagnostic &Info) override;

  void BeginSourceFile(const LangOptions &LangOpts,
                       const Preprocessor *PP = nullptr) override;

  void EndSourceFile() override;

  // Retrieve the diagnostics that were captured.
  std::vector<ClangTidyError> take();

private:
  void finalizeLastError();
  void removeIncompatibleErrors();
  void removeDuplicatedDiagnosticsOfAliasCheckers();

  /// Returns the \c HeaderFilter constructed for the options set in the
  /// context.
  llvm::Regex *getHeaderFilter();

  /// Returns the \c ExcludeHeaderFilter constructed for the options set in the
  /// context.
  llvm::Regex *getExcludeHeaderFilter();

  /// Updates \c LastErrorRelatesToUserCode and LastErrorPassesLineFilter
  /// according to the diagnostic \p Location.
  void checkFilters(SourceLocation Location, const SourceManager &Sources);
  bool passesLineFilter(StringRef FileName, unsigned LineNumber) const;

  void forwardDiagnostic(const Diagnostic &Info);

  ClangTidyContext &Context;
  DiagnosticsEngine *ExternalDiagEngine;
  bool RemoveIncompatibleErrors;
  bool GetFixesFromNotes;
  bool EnableNolintBlocks;
  std::vector<ClangTidyError> Errors;
  std::unique_ptr<llvm::Regex> HeaderFilter;
  std::unique_ptr<llvm::Regex> ExcludeHeaderFilter;
  bool LastErrorRelatesToUserCode = false;
  bool LastErrorPassesLineFilter = false;
  bool LastErrorWasIgnored = false;
  /// Tracks whether we're currently inside a
  /// `BeginSourceFile()/EndSourceFile()` pair. Outside of a source file, we
  /// should only receive diagnostics that have to source location, such as
  /// command-line warnings.
  bool InSourceFile = false;
};

} // end namespace tidy
} // end namespace clang

#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_CLANGTIDYDIAGNOSTICCONSUMER_H
