nkakuev created this revision.
nkakuev added reviewers: malcolm.parsons, alexfh.
nkakuev added a subscriber: cfe-commits.

Currently clang-tidy doesn't provide a practical way to suppress diagnostics 
from headers you don't have control over. If using a function defined in such 
header causes a false positive, there is no easy way to deal with this issue.

Since you //cannot// modify a header, you //cannot// add NOLINT (or any other 
source annotations) to it. And adding NOLINT to each invocation of such 
function is either impossible or hugely impractical if invocations are 
ubiquitous.

Using the '-header-filter' doesn't help to address this issue as well. Unless 
your own headers are strategically named, it is virtually impossible to craft a 
regex that will filter out only the third-party ones.

The '-line-filter' can be used to suppress such warnings, but it's not very 
convenient. Instead of excluding a line that produces a warning you have to 
include all other lines. Plus, it provides no way to suppress only certain 
warnings from a file.

The option I propose solves aforementioned problems. It allows a user to 
specify a set of regular expressions to suppress diagnostics from files whose 
names match these expressions. Additionally, a set of checks can be specified 
to suppress only certain diagnostics.

The option can be used in the following way:
`clang-tidy 
-suppress-checks-filter='[{"name":"falty_header.h"},{"name":"third-party/","checks":"google-explicit-constructor"}]'
 source.cpp -- -c`


https://reviews.llvm.org/D26418

Files:
  clang-tidy/ClangTidyDiagnosticConsumer.cpp
  clang-tidy/ClangTidyDiagnosticConsumer.h
  clang-tidy/ClangTidyOptions.cpp
  clang-tidy/ClangTidyOptions.h
  clang-tidy/tool/ClangTidyMain.cpp
  test/clang-tidy/Inputs/suppress-checks-filter/1/header1.h
  test/clang-tidy/Inputs/suppress-checks-filter/1/header2.h
  test/clang-tidy/Inputs/suppress-checks-filter/2/header3.h
  test/clang-tidy/Inputs/suppress-checks-filter/2/header4.h
  test/clang-tidy/suppress-checks-filter.cpp

Index: test/clang-tidy/suppress-checks-filter.cpp
===================================================================
--- test/clang-tidy/suppress-checks-filter.cpp
+++ test/clang-tidy/suppress-checks-filter.cpp
@@ -0,0 +1,17 @@
+// RUN: clang-tidy -checks='-*,google-explicit-constructor,google-readability-casting' -header-filter='header*' -suppress-checks-filter='[{"name":"header1.h"},{"name":"header2.h","checks":"google-explicit-constructor"},{"name":"2/"}]' %s -- -I %S/Inputs/suppress-checks-filter 2>&1 | FileCheck %s
+
+#include "1/header1.h"
+// CHECK-NOT: header1.h:{{.*}} warning
+
+#include "1/header2.h"
+// CHECK-NOT: 1/header2.h:{{.*}} warning: single-argument constructors {{.*}}
+// CHECK: 1/header2.h:{{.*}} warning: redundant cast to the same type {{.*}}
+
+#include "2/header3.h"
+// CHECK-NOT: 2/header3.h:{{.*}} warning: single-argument constructors
+
+#include "2/header4.h"
+// CHECK-NOT: 2/header4.h:{{.*}} warning: single-argument constructors
+
+// CHECK: Suppressed 4 warnings (4 with suppressed checks filters)
+
Index: test/clang-tidy/Inputs/suppress-checks-filter/2/header4.h
===================================================================
--- test/clang-tidy/Inputs/suppress-checks-filter/2/header4.h
+++ test/clang-tidy/Inputs/suppress-checks-filter/2/header4.h
@@ -0,0 +1,2 @@
+class A4 { A4(int); };
+
Index: test/clang-tidy/Inputs/suppress-checks-filter/2/header3.h
===================================================================
--- test/clang-tidy/Inputs/suppress-checks-filter/2/header3.h
+++ test/clang-tidy/Inputs/suppress-checks-filter/2/header3.h
@@ -0,0 +1,2 @@
+class A3 { A3(int); };
+
Index: test/clang-tidy/Inputs/suppress-checks-filter/1/header2.h
===================================================================
--- test/clang-tidy/Inputs/suppress-checks-filter/1/header2.h
+++ test/clang-tidy/Inputs/suppress-checks-filter/1/header2.h
@@ -0,0 +1,6 @@
+class A2 { A2(int); };
+
+class B2 {
+  B2(int &In, int &Out) { Out = (int)In; }
+};
+
Index: test/clang-tidy/Inputs/suppress-checks-filter/1/header1.h
===================================================================
--- test/clang-tidy/Inputs/suppress-checks-filter/1/header1.h
+++ test/clang-tidy/Inputs/suppress-checks-filter/1/header1.h
@@ -0,0 +1,2 @@
+class A1 { A1(int); };
+
Index: clang-tidy/tool/ClangTidyMain.cpp
===================================================================
--- clang-tidy/tool/ClangTidyMain.cpp
+++ clang-tidy/tool/ClangTidyMain.cpp
@@ -105,6 +105,20 @@
                                        cl::init(""),
                                        cl::cat(ClangTidyCategory));
 
+static cl::opt<std::string>
+    SuppressWarningsFromHeaders("suppress-checks-filter", cl::desc(R"(
+Suppress diagnostics from files whose names match
+provided regular expression. If a list of checks
+is specified, only diagnostics produces by these
+checks will be suppressed. The format of
+the argument is a JSON array of objects:
+[
+  {"name":"header1.h","checks":"boost-*,cert-*"},
+  {"name":"header2.h"}
+]
+)"),
+                                cl::init(""), cl::cat(ClangTidyCategory));
+
 static cl::opt<bool> Fix("fix", cl::desc(R"(
 Apply suggested fixes. Without -fix-errors
 clang-tidy will bail out if any compilation
@@ -201,9 +215,14 @@
       llvm::errs() << Separator << Stats.ErrorsIgnoredNOLINT << " NOLINT";
       Separator = ", ";
     }
-    if (Stats.ErrorsIgnoredCheckFilter)
+    if (Stats.ErrorsIgnoredCheckFilter) {
       llvm::errs() << Separator << Stats.ErrorsIgnoredCheckFilter
                    << " with check filters";
+      Separator = ", ";
+    }
+    if (Stats.ErrorsIgnoredSuppressChecksFilter)
+      llvm::errs() << Separator << Stats.ErrorsIgnoredSuppressChecksFilter
+                   << " with suppressed checks filters";
     llvm::errs() << ").\n";
     if (Stats.ErrorsIgnoredNonUserCode)
       llvm::errs() << "Use -header-filter=.* to display errors from all "
@@ -253,8 +272,21 @@
 
 static std::unique_ptr<ClangTidyOptionsProvider> createOptionsProvider() {
   ClangTidyGlobalOptions GlobalOptions;
+  bool ArgumentsAreValid = true;
   if (std::error_code Err = parseLineFilter(LineFilter, GlobalOptions)) {
-    llvm::errs() << "Invalid LineFilter: " << Err.message() << "\n\nUsage:\n";
+    llvm::errs() << "Invalid LineFilter: " << Err.message();
+    ArgumentsAreValid = false;
+  }
+
+  if (std::error_code Err = parseSuppressChecksFilter(
+          SuppressWarningsFromHeaders, GlobalOptions)) {
+    llvm::errs() << "Cannot parse '-suppress-checks-filter': "
+                 << Err.message();
+    ArgumentsAreValid = false;
+  }
+
+  if (!ArgumentsAreValid) {
+    llvm::errs() << "\n\nUsage:\n";
     llvm::cl::PrintHelpMessage(/*Hidden=*/false, /*Categorized=*/true);
     return nullptr;
   }
Index: clang-tidy/ClangTidyOptions.h
===================================================================
--- clang-tidy/ClangTidyOptions.h
+++ clang-tidy/ClangTidyOptions.h
@@ -36,12 +36,27 @@
   std::vector<LineRange> LineRanges;
 };
 
+/// \brief If a file name matches \c FileNameRegex, checks listed in \c Checks
+/// will be suppressed.
+struct SuppressChecksFilterEntry {
+  /// \brief File name pattern.
+  std::string FileNameRegex;
+
+  /// \brief List of checks to suppress.
+  std::string Checks;
+};
+
 /// \brief Global options. These options are neither stored nor read from
 /// configuration files.
 struct ClangTidyGlobalOptions {
   /// \brief Output warnings from certain line ranges of certain files only.
   /// If empty, no warnings will be filtered.
   std::vector<FileFilter> LineFilter;
+
+  /// \brief Suppress warnings from certain checks if a file name matches a
+  /// pattern. If no checks are specified, all warnings from a file will be
+  /// suppressed.
+  std::vector<SuppressChecksFilterEntry> SuppressChecksFilter;
 };
 
 /// \brief Contains options for clang-tidy. These options may be read from
@@ -247,6 +262,11 @@
 std::error_code parseLineFilter(llvm::StringRef LineFilter,
                                 ClangTidyGlobalOptions &Options);
 
+/// \brief Parses SuppressChecksFilter from JSON and stores it to the \p
+/// Options.
+std::error_code parseSuppressChecksFilter(llvm::StringRef SuppressChecksFilter,
+                                          ClangTidyGlobalOptions &Options);
+
 /// \brief Parses configuration from JSON and returns \c ClangTidyOptions or an
 /// error.
 llvm::ErrorOr<ClangTidyOptions> parseConfiguration(llvm::StringRef Config);
Index: clang-tidy/ClangTidyOptions.cpp
===================================================================
--- clang-tidy/ClangTidyOptions.cpp
+++ clang-tidy/ClangTidyOptions.cpp
@@ -23,10 +23,12 @@
 
 using clang::tidy::ClangTidyOptions;
 using clang::tidy::FileFilter;
+using clang::tidy::SuppressChecksFilterEntry;
 using OptionsSource = clang::tidy::ClangTidyOptionsProvider::OptionsSource;
 
 LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(FileFilter)
 LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(FileFilter::LineRange)
+LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(SuppressChecksFilterEntry)
 LLVM_YAML_IS_SEQUENCE_VECTOR(ClangTidyOptions::StringPair)
 LLVM_YAML_IS_SEQUENCE_VECTOR(std::string)
 
@@ -61,6 +63,18 @@
   }
 };
 
+template <> struct MappingTraits<SuppressChecksFilterEntry> {
+  static void mapping(IO &IO, SuppressChecksFilterEntry &Filter) {
+    IO.mapRequired("name", Filter.FileNameRegex);
+    IO.mapOptional("checks", Filter.Checks);
+  }
+  static StringRef validate(IO &io, SuppressChecksFilterEntry &Filter) {
+    if (Filter.FileNameRegex.empty())
+      return "No file name pattern specified";
+    return StringRef();
+  }
+};
+
 template <> struct MappingTraits<ClangTidyOptions::StringPair> {
   static void mapping(IO &IO, ClangTidyOptions::StringPair &KeyValue) {
     IO.mapRequired("key", KeyValue.first);
@@ -315,6 +329,16 @@
   return Input.error();
 }
 
+/// \brief Parses -suppress-checks-filter option and stores it to the \c
+/// Options.
+std::error_code
+parseSuppressChecksFilter(StringRef SuppressChecksFilter,
+                          clang::tidy::ClangTidyGlobalOptions &Options) {
+  llvm::yaml::Input Input(SuppressChecksFilter);
+  Input >> Options.SuppressChecksFilter;
+  return Input.error();
+}
+
 llvm::ErrorOr<ClangTidyOptions> parseConfiguration(StringRef Config) {
   llvm::yaml::Input Input(Config);
   ClangTidyOptions Options;
Index: clang-tidy/ClangTidyDiagnosticConsumer.h
===================================================================
--- clang-tidy/ClangTidyDiagnosticConsumer.h
+++ clang-tidy/ClangTidyDiagnosticConsumer.h
@@ -15,6 +15,7 @@
 #include "clang/Basic/SourceManager.h"
 #include "clang/Tooling/Refactoring.h"
 #include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/Optional.h"
 #include "llvm/ADT/StringMap.h"
 #include "llvm/Support/Regex.h"
 #include "llvm/Support/Timer.h"
@@ -105,17 +106,20 @@
 struct ClangTidyStats {
   ClangTidyStats()
       : ErrorsDisplayed(0), ErrorsIgnoredCheckFilter(0), ErrorsIgnoredNOLINT(0),
-        ErrorsIgnoredNonUserCode(0), ErrorsIgnoredLineFilter(0) {}
+        ErrorsIgnoredNonUserCode(0), ErrorsIgnoredLineFilter(0),
+        ErrorsIgnoredSuppressChecksFilter(0) {}
 
   unsigned ErrorsDisplayed;
   unsigned ErrorsIgnoredCheckFilter;
   unsigned ErrorsIgnoredNOLINT;
   unsigned ErrorsIgnoredNonUserCode;
   unsigned ErrorsIgnoredLineFilter;
+  unsigned ErrorsIgnoredSuppressChecksFilter;
 
   unsigned errorsIgnored() const {
     return ErrorsIgnoredNOLINT + ErrorsIgnoredCheckFilter +
-           ErrorsIgnoredNonUserCode + ErrorsIgnoredLineFilter;
+           ErrorsIgnoredNonUserCode + ErrorsIgnoredLineFilter +
+           ErrorsIgnoredSuppressChecksFilter;
   }
 };
 
@@ -135,6 +139,10 @@
 /// \endcode
 class ClangTidyContext {
 public:
+  /// \brief Collection of pairs that map filenames to suppressed checks.
+  typedef std::vector<std::pair<llvm::Regex, llvm::Optional<GlobList>>>
+      SuppressChecksMap;
+
   /// \brief Initializes \c ClangTidyContext instance.
   ClangTidyContext(std::unique_ptr<ClangTidyOptionsProvider> OptionsProvider);
 
@@ -173,6 +181,9 @@
   /// The \c CurrentFile can be changed using \c setCurrentFile.
   GlobList &getChecksFilter();
 
+  /// \brief Returns \c SuppressChecksFilter.
+  SuppressChecksMap &getSuppressChecksFilter();
+
   /// \brief Returns check filter for the \c CurrentFile which
   /// selects checks for upgrade to error.
   GlobList &getWarningAsErrorFilter();
@@ -228,6 +239,9 @@
   /// \brief Store an \p Error.
   void storeError(const ClangTidyError &Error);
 
+  /// \brief Initializes \c SuppressChecksFilter from \c ClangTidyGlobalOptions.
+  void InitializeSuppressChecksFilter();
+
   std::vector<ClangTidyError> Errors;
   DiagnosticsEngine *DiagEngine;
   std::unique_ptr<ClangTidyOptionsProvider> OptionsProvider;
@@ -237,6 +251,8 @@
   std::unique_ptr<GlobList> CheckFilter;
   std::unique_ptr<GlobList> WarningAsErrorFilter;
 
+  SuppressChecksMap SuppressChecksFilter;
+
   LangOptions LangOpts;
 
   ClangTidyStats Stats;
@@ -280,6 +296,10 @@
   void checkFilters(SourceLocation Location);
   bool passesLineFilter(StringRef FileName, unsigned LineNumber) const;
 
+  /// \brief Returns true if diagnostic \c Info is matched by \c
+  /// SuppressChecksFilter.
+  bool diagnosticMatchesSuppressChecksFilter(const Diagnostic &Info) const;
+
   ClangTidyContext &Context;
   std::unique_ptr<DiagnosticsEngine> Diags;
   SmallVector<ClangTidyError, 8> Errors;
Index: clang-tidy/ClangTidyDiagnosticConsumer.cpp
===================================================================
--- clang-tidy/ClangTidyDiagnosticConsumer.cpp
+++ clang-tidy/ClangTidyDiagnosticConsumer.cpp
@@ -175,6 +175,7 @@
   // Before the first translation unit we can get errors related to command-line
   // parsing, use empty string for the file name in this case.
   setCurrentFile("");
+  InitializeSuppressChecksFilter();
 }
 
 DiagnosticBuilder ClangTidyContext::diag(
@@ -229,6 +230,11 @@
   return *CheckFilter;
 }
 
+ClangTidyContext::SuppressChecksMap &
+ClangTidyContext::getSuppressChecksFilter() {
+  return SuppressChecksFilter;
+};
+
 GlobList &ClangTidyContext::getWarningAsErrorFilter() {
   assert(WarningAsErrorFilter != nullptr);
   return *WarningAsErrorFilter;
@@ -239,6 +245,15 @@
   Errors.push_back(Error);
 }
 
+void ClangTidyContext::InitializeSuppressChecksFilter() {
+  for (const SuppressChecksFilterEntry &Entry :
+       getGlobalOptions().SuppressChecksFilter)
+    SuppressChecksFilter.emplace_back(llvm::Regex(Entry.FileNameRegex),
+                                      Entry.Checks.empty()
+                                          ? llvm::Optional<GlobList>()
+                                          : GlobList(Entry.Checks));
+}
+
 StringRef ClangTidyContext::getCheckName(unsigned DiagnosticID) const {
   llvm::DenseMap<unsigned, std::string>::const_iterator I =
       CheckNamesByDiagnosticID.find(DiagnosticID);
@@ -312,13 +327,20 @@
     return;
 
   if (Info.getLocation().isValid() && DiagLevel != DiagnosticsEngine::Error &&
-      DiagLevel != DiagnosticsEngine::Fatal &&
-      LineIsMarkedWithNOLINTinMacro(Diags->getSourceManager(),
-                                    Info.getLocation())) {
-    ++Context.Stats.ErrorsIgnoredNOLINT;
-    // Ignored a warning, should ignore related notes as well
-    LastErrorWasIgnored = true;
-    return;
+      DiagLevel != DiagnosticsEngine::Fatal) {
+    if (LineIsMarkedWithNOLINTinMacro(Diags->getSourceManager(),
+                                      Info.getLocation())) {
+      ++Context.Stats.ErrorsIgnoredNOLINT;
+      // Ignored a warning, should ignore related notes as well
+      LastErrorWasIgnored = true;
+      return;
+    }
+
+    if (diagnosticMatchesSuppressChecksFilter(Info)) {
+      ++Context.Stats.ErrorsIgnoredSuppressChecksFilter;
+      LastErrorWasIgnored = true;
+      return;
+    }
   }
 
   LastErrorWasIgnored = false;
@@ -402,6 +424,28 @@
   return false;
 }
 
+bool ClangTidyDiagnosticConsumer::diagnosticMatchesSuppressChecksFilter(
+    const Diagnostic &Info) const {
+  const SourceLocation &Location = Info.getLocation();
+  if (!Location.isValid() || !Location.isFileID() || !Info.hasSourceManager())
+    return false;
+
+  const SourceManager &SourceManager = Info.getSourceManager();
+  const StringRef FileName = SourceManager.getFilename(Location);
+  const StringRef CheckName = Context.getCheckName(Info.getID());
+
+  for (std::pair<llvm::Regex, llvm::Optional<GlobList>> &Entry :
+       Context.getSuppressChecksFilter()) {
+    if (Entry.first.match(FileName)) {
+      llvm::Optional<GlobList> &Checks = Entry.second;
+      if (!Checks.hasValue() || Checks.getValue().contains(CheckName))
+        return true;
+    }
+  }
+
+  return false;
+}
+
 void ClangTidyDiagnosticConsumer::checkFilters(SourceLocation Location) {
   // Invalid location may mean a diagnostic in a command line, don't skip these.
   if (!Location.isValid()) {
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to