Skip to content

Conversation

@lakshsidhu04
Copy link

Added an option to enable users to include duplicate headers which match with certain regexes

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot
Copy link
Member

llvmbot commented Nov 7, 2025

@llvm/pr-subscribers-clang-tidy

@llvm/pr-subscribers-clang-tools-extra

Author: Lakshdeep Singh (lakshsidhu04)

Changes

Added an option to enable users to include duplicate headers which match with certain regexes


Full diff: https://github.com/llvm/llvm-project/pull/167046.diff

3 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp (+1-1)
  • (modified) clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp (+80-11)
  • (modified) clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.h (+7-3)
diff --git a/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp b/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp
index a0e7ac19ab2d5..bf0d9a7598679 100644
--- a/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp
+++ b/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp
@@ -107,7 +107,7 @@ class CyclicDependencyCallbacks : public PPCallbacks {
       Check.diag(Loc, "direct self-inclusion of header file '%0'") << FileName;
       return;
     }
-
+    
     Check.diag(Loc, "circular header file dependency detected while including "
                     "'%0', please check the include path")
         << FileName;
diff --git a/clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp b/clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp
index 0237c057afed5..5ed783bcef824 100644
--- a/clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp
+++ b/clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp
@@ -11,7 +11,12 @@
 #include "clang/Lex/Preprocessor.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/SmallVector.h"
+#include "llvm/Support/Regex.h"
+#include "llvm/Support/Path.h"
+#include "../utils/OptionsUtils.h"
+#include "llvm/ADT/StringExtras.h"
 #include <memory>
+#include <vector>
 
 namespace clang::tidy::readability {
 
@@ -33,12 +38,9 @@ using FileList = SmallVector<StringRef>;
 class DuplicateIncludeCallbacks : public PPCallbacks {
 public:
   DuplicateIncludeCallbacks(DuplicateIncludeCheck &Check,
-                            const SourceManager &SM)
-      : Check(Check), SM(SM) {
-    // The main file doesn't participate in the FileChanged notification.
-    Files.emplace_back();
-  }
-
+                            const SourceManager &SM, 
+                            const std::vector<std::string> &AllowedStrings);
+  
   void FileChanged(SourceLocation Loc, FileChangeReason Reason,
                    SrcMgr::CharacteristicKind FileType,
                    FileID PrevFID) override;
@@ -62,14 +64,28 @@ class DuplicateIncludeCallbacks : public PPCallbacks {
   SmallVector<FileList> Files;
   DuplicateIncludeCheck &Check;
   const SourceManager &SM;
+  std::vector<llvm::Regex> AllowedDuplicateRegex;
+
+  bool IsAllowedDuplicateInclude(StringRef TokenName, OptionalFileEntryRef File,
+                                 StringRef RelativePath);
 };
 
 } // namespace
 
-void DuplicateIncludeCallbacks::FileChanged(SourceLocation Loc,
-                                            FileChangeReason Reason,
-                                            SrcMgr::CharacteristicKind FileType,
-                                            FileID PrevFID) {
+DuplicateIncludeCallbacks::DuplicateIncludeCallbacks(
+    DuplicateIncludeCheck &Check, const SourceManager &SM,
+    const std::vector<std::string> &AllowedStrings) : Check(Check), SM(SM) {
+  // The main file doesn't participate in the FileChanged notification.
+  Files.emplace_back();
+  AllowedDuplicateRegex.reserve(AllowedStrings.size());
+  for (const std::string &str : AllowedStrings) {
+    AllowedDuplicateRegex.emplace_back(str);
+  }
+}
+
+void DuplicateIncludeCallbacks::FileChanged(
+        SourceLocation Loc, FileChangeReason Reason,
+        SrcMgr::CharacteristicKind FileType, FileID PrevFID) {
   if (Reason == EnterFile)
     Files.emplace_back();
   else if (Reason == ExitFile)
@@ -85,6 +101,14 @@ void DuplicateIncludeCallbacks::InclusionDirective(
   if (FilenameRange.getBegin().isMacroID() ||
       FilenameRange.getEnd().isMacroID())
     return;
+  
+  // if duplicate allowed, record and return
+  if(IsAllowedDuplicateInclude(FileName, File, RelativePath))
+  {
+      Files.back().push_back(FileName);
+      return;
+  }
+
   if (llvm::is_contained(Files.back(), FileName)) {
     // We want to delete the entire line, so make sure that [Start,End] covers
     // everything.
@@ -109,9 +133,54 @@ void DuplicateIncludeCallbacks::MacroUndefined(const Token &MacroNameTok,
   Files.back().clear();
 }
 
+bool DuplicateIncludeCallbacks::IsAllowedDuplicateInclude(StringRef TokenName,
+                                                OptionalFileEntryRef File,
+                                                StringRef RelativePath) {
+  SmallVector<StringRef, 3> matchArguments;
+  matchArguments.push_back(TokenName);
+  
+  if (!RelativePath.empty())
+    matchArguments.push_back(llvm::sys::path::filename(RelativePath));
+  
+  if (File) {
+    StringRef RealPath = File->getFileEntry().tryGetRealPathName();
+    if (!RealPath.empty())
+      matchArguments.push_back(llvm::sys::path::filename(RealPath));
+  }
+  
+  // try to match with each regex
+  for (const llvm::Regex &reg : AllowedDuplicateRegex) {
+    for (StringRef arg : matchArguments) {
+      if (reg.match(arg))
+        return true;
+    }
+  }
+  return false;
+}
+
+DuplicateIncludeCheck::DuplicateIncludeCheck(StringRef Name,
+                                             ClangTidyContext *Context)
+    : ClangTidyCheck(Name, Context) {
+  std::string Raw = Options.get("AllowedDuplicateIncludes", "").str();
+  if (!Raw.empty()) {
+    SmallVector<StringRef, 4> StringParts;
+    StringRef(Raw).split(StringParts, ',', -1, false);
+    
+    for (StringRef Part : StringParts) {
+      Part = Part.trim(); 
+      if (!Part.empty())
+        AllowedDuplicateIncludes.push_back(Part.str());
+    }
+  }
+}
+
 void DuplicateIncludeCheck::registerPPCallbacks(
     const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) {
-  PP->addPPCallbacks(std::make_unique<DuplicateIncludeCallbacks>(*this, SM));
+  PP->addPPCallbacks(std::make_unique<DuplicateIncludeCallbacks>(*this, SM, AllowedDuplicateIncludes));
 }
 
+void DuplicateIncludeCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
+  Options.store(Opts, "AllowedDuplicateIncludes",
+                llvm::join(AllowedDuplicateIncludes, ","));
+}
 } // namespace clang::tidy::readability
diff --git a/clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.h b/clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.h
index 297999cf4f921..688c2ad4d30f1 100644
--- a/clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.h
+++ b/clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.h
@@ -10,7 +10,8 @@
 #define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_DUPLICATE_INCLUDE_CHECK_H
 
 #include "../ClangTidyCheck.h"
-
+#include <vector>
+#include <string>
 namespace clang::tidy::readability {
 
 /// \brief Find and remove duplicate #include directives.
@@ -19,11 +20,14 @@ namespace clang::tidy::readability {
 /// directives between them are analyzed.
 class DuplicateIncludeCheck : public ClangTidyCheck {
 public:
-  DuplicateIncludeCheck(StringRef Name, ClangTidyContext *Context)
-      : ClangTidyCheck(Name, Context) {}
+  DuplicateIncludeCheck(StringRef Name, ClangTidyContext *Context);
 
   void registerPPCallbacks(const SourceManager &SM, Preprocessor *PP,
                            Preprocessor *ModuleExpanderPP) override;
+  
+  void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
+private:
+  std::vector<std::string> AllowedDuplicateIncludes;
 };
 
 } // namespace clang::tidy::readability

@lakshsidhu04
Copy link
Author

Hi, i am relatively new to open source. Can you tell me if I need to make documentation changes if any?

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff origin/main HEAD --extensions h,cpp -- clang-tools-extra/test/clang-tidy/checkers/readability/Inputs/duplicate-include-allowed/other.h clang-tools-extra/test/clang-tidy/checkers/readability/Inputs/duplicate-include-allowed/pack_begin.h clang-tools-extra/test/clang-tidy/checkers/readability/Inputs/duplicate-include-allowed/pack_end.h clang-tools-extra/test/clang-tidy/checkers/readability/duplicate-include-allowed.cpp clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.h --diff_from_common_commit

⚠️
The reproduction instructions above might return results for more than one PR
in a stack if you are using a stacked PR workflow. You can limit the results by
changing origin/main to the base branch/commit you want to compare against.
⚠️

View the diff from clang-format here.
diff --git a/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp b/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp
index bf0d9a759..a0e7ac19a 100644
--- a/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp
+++ b/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp
@@ -107,7 +107,7 @@ public:
       Check.diag(Loc, "direct self-inclusion of header file '%0'") << FileName;
       return;
     }
-    
+
     Check.diag(Loc, "circular header file dependency detected while including "
                     "'%0', please check the include path")
         << FileName;

@vbvictor
Copy link
Contributor

vbvictor commented Nov 7, 2025

Hi, i am relatively new to open source. Can you tell me if I need to make documentation changes if any?

Hi, yes: you need to add 1)release notes, 2)tests and 3)documentation about new option.

General advice: look at how other people do PRs that got merged - there you can find answers by yourself.
For a recent concrete example: #164827.

Please try to make robust test cases and cover all edge-cases that you can think of!

@github-actions
Copy link

github-actions bot commented Nov 7, 2025

⚠️ C/C++ code linter clang-tidy found issues in your code. ⚠️

You can test this locally with the following command:
git diff -U0 origin/main...HEAD -- clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.h |
python3 clang-tools-extra/clang-tidy/tool/clang-tidy-diff.py \
  -path build -p1 -quiet
View the output from clang-tidy here.

clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp:69:8: warning: invalid case style for function 'IsAllowedDuplicateInclude' [readability-identifier-naming]
   69 |   bool IsAllowedDuplicateInclude(StringRef TokenName, OptionalFileEntryRef File,
      |        ^~~~~~~~~~~~~~~~~~~~~~~~~
      |        isAllowedDuplicateInclude
clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp:82:27: warning: invalid case style for variable 'str' [readability-identifier-naming]
   82 |   for (const std::string &str : AllowedStrings) {
      |                           ^~~
      |                           Str
   83 |     AllowedDuplicateRegex.emplace_back(str);
      |                                        ~~~
      |                                        Str
clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp:139:29: warning: invalid case style for variable 'matchArguments' [readability-identifier-naming]
  139 |   SmallVector<StringRef, 3> matchArguments;
      |                             ^~~~~~~~~~~~~~
      |                             MatchArguments
  140 |   matchArguments.push_back(TokenName);
      |   ~~~~~~~~~~~~~~
      |   MatchArguments
  141 | 
  142 |   if (!RelativePath.empty())
  143 |     matchArguments.push_back(llvm::sys::path::filename(RelativePath));
      |     ~~~~~~~~~~~~~~
      |     MatchArguments
  144 | 
  145 |   if (File) {
  146 |     StringRef RealPath = File->getFileEntry().tryGetRealPathName();
  147 |     if (!RealPath.empty())
  148 |       matchArguments.push_back(llvm::sys::path::filename(RealPath));
      |       ~~~~~~~~~~~~~~
      |       MatchArguments
  149 |   }
  150 | 
  151 |   // try to match with each regex
  152 |   for (const llvm::Regex &reg : AllowedDuplicateRegex) {
  153 |     for (StringRef arg : matchArguments) {
      |                          ~~~~~~~~~~~~~~
      |                          MatchArguments
clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp:152:27: warning: invalid case style for variable 'reg' [readability-identifier-naming]
  152 |   for (const llvm::Regex &reg : AllowedDuplicateRegex) {
      |                           ^~~
      |                           Reg
  153 |     for (StringRef arg : matchArguments) {
  154 |       if (reg.match(arg))
      |           ~~~
      |           Reg
clang-tools-extra/clang-tidy/readability/DuplicateIncludeCheck.cpp:153:20: warning: invalid case style for variable 'arg' [readability-identifier-naming]
  153 |     for (StringRef arg : matchArguments) {
      |                    ^~~
      |                    Arg
  154 |       if (reg.match(arg))
      |                     ~~~
      |                     Arg

Comment on lines 37 to 53
Option: ``AllowedDuplicateIncludes``
------------------------------------

Headers listed in this option are exempt from warnings. For example:

.. code-block:: c++

-config='{CheckOptions: [{key: readability-duplicate-include.AllowedDuplicateIncludes, value: "pack_begin.h,pack_end.h"}]}'

This allows regex matches with ``pack_begin.h`` and ``pack_end.h`` to be included multiple times
without triggering diagnostics.

Notes
-----

- Only direct includes in the current translation unit are checked.
- Useful for removing redundant includes and improving compile times in large codebases.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong option formatting, please see how other documents with options are formatted

Option: ``AllowedDuplicateIncludes``
------------------------------------

Headers listed in this option are exempt from warnings. For example:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add default value

#include "assertion.h"
// ...code with assertions disabled

Option: ``AllowedDuplicateIncludes``
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's name it IgnoreHeaders, see https://clang.llvm.org/extra/clang-tidy/checks/misc/include-cleaner.html.

Just copy documentation from there

std::string Raw = Options.get("AllowedDuplicateIncludes", "").str();
if (!Raw.empty()) {
SmallVector<StringRef, 4> StringParts;
StringRef(Raw).split(StringParts, ',', -1, false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 142 to 149
if (!RelativePath.empty())
matchArguments.push_back(llvm::sys::path::filename(RelativePath));

if (File) {
StringRef RealPath = File->getFileEntry().tryGetRealPathName();
if (!RealPath.empty())
matchArguments.push_back(llvm::sys::path::filename(RealPath));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need 3 separate matchArguments to match?
To my understanding when we encounter:

#include "pack_begin.h"
#include "pack_begin.h" 

We only need to match only header name, what are other 2 matchArguments?

@lakshsidhu04 lakshsidhu04 force-pushed the duplicate-header-option branch from 0658b02 to b611038 Compare November 8, 2025 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants