diff --git a/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp b/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp --- a/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp +++ b/clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp @@ -33,6 +33,7 @@ #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" @@ -156,10 +157,18 @@ }); std::vector Unused; + llvm::StringSet<> AlreadyIncludedHeaders; for (const include_cleaner::Include &I : RecordedPreprocessor.Includes.all()) { - if (Used.contains(&I) || !I.Resolved) + if (!I.Resolved) continue; + if (Used.contains(&I)) { + if (!AlreadyIncludedHeaders.contains(I.Spelled)) + AlreadyIncludedHeaders.insert(I.Spelled); + else + Unused.push_back(&I); + continue; + } if (RecordedPI.shouldKeep(*I.Resolved)) continue; // Check if main file is the public interface for a private header. If so @@ -199,6 +208,7 @@ tooling::HeaderIncludes HeaderIncludes(getCurrentMainFile(), Code, FileStyle->IncludeStyle); + llvm::StringSet<> AlreadyInserted; for (const auto &Inc : Missing) { std::string Spelling = include_cleaner::spellHeader( {Inc.Missing, PP->getHeaderSearchInfo(), MainFile}); @@ -209,14 +219,18 @@ // main file. if (auto Replacement = HeaderIncludes.insert(llvm::StringRef{Spelling}.trim("\"<>"), - Angled, tooling::IncludeDirective::Include)) - diag(SM->getSpellingLoc(Inc.SymRef.RefLocation), - "no header providing \"%0\" is directly included") - << Inc.SymRef.Target.name() - << FixItHint::CreateInsertion( - SM->getComposedLoc(SM->getMainFileID(), - Replacement->getOffset()), - Replacement->getReplacementText()); + Angled, tooling::IncludeDirective::Include)) { + DiagnosticBuilder DB = + diag(SM->getSpellingLoc(Inc.SymRef.RefLocation), + "no header providing \"%0\" is directly included") + << Inc.SymRef.Target.name(); + if (!AlreadyInserted.contains(Inc.Missing.resolvedPath())) { + DB << FixItHint::CreateInsertion( + SM->getComposedLoc(SM->getMainFileID(), Replacement->getOffset()), + Replacement->getReplacementText()); + AlreadyInserted.insert(Inc.Missing.resolvedPath()); + } + } } } diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/baz.h b/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/baz.h --- a/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/baz.h +++ b/clang-tools-extra/test/clang-tidy/checkers/misc/Inputs/baz.h @@ -1,2 +1,3 @@ #pragma once +#define BAZ 10 int baz(); diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/include-cleaner.cpp b/clang-tools-extra/test/clang-tidy/checkers/misc/include-cleaner.cpp --- a/clang-tools-extra/test/clang-tidy/checkers/misc/include-cleaner.cpp +++ b/clang-tools-extra/test/clang-tidy/checkers/misc/include-cleaner.cpp @@ -1,5 +1,8 @@ // RUN: %check_clang_tidy %s misc-include-cleaner %t -- -- -I%S/Inputs -isystem%S/Inputs/system #include "bar.h" +#include "bar.h" +// CHECK-MESSAGES: :[[@LINE-1]]:1: warning: included header bar.h is not used directly [misc-include-cleaner] +// CHECK-FIXES: {{^}} // CHECK-FIXES: {{^}}#include "baz.h"{{$}} #include "foo.h" // CHECK-MESSAGES: :[[@LINE-1]]:1: warning: included header foo.h is not used directly [misc-include-cleaner] @@ -11,6 +14,8 @@ int BarResult = bar(); int BazResult = baz(); // CHECK-MESSAGES: :[[@LINE-1]]:17: warning: no header providing "baz" is directly included [misc-include-cleaner] +int BazResultAgain = BAZ; // Header should not be inserted more than once +// CHECK-MESSAGES: :[[@LINE-1]]:22: warning: no header providing "BAZ" is directly included [misc-include-cleaner] std::string HelloString; // CHECK-MESSAGES: :[[@LINE-1]]:6: warning: no header providing "std::string" is directly included [misc-include-cleaner] int FooBarResult = foobar();