Skip to content

[clang-tidy] Add option to keep virtual in 'modernize-use-override' #144916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

fwinkl
Copy link

@fwinkl fwinkl commented Jun 19, 2025

Add a new AllowVirtual option to 'modernize-use-override', which does not remove the virtual specifier. This is useful because some coding guidelines may suggest to use virtual ... override for clarity. See the new unit test for examples.

Add a new `AllowVirtual` option to 'modernize-use-override', which does
not remove the `virtual` specifier. This is useful because some coding
guidelines may suggest to use `virtual ... override` for clarity.
Copy link

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.

@@ -226,7 +232,8 @@ void UseOverrideCheck::check(const MatchFinder::MatchResult &Result) {
CharSourceRange::getTokenRange(OverrideLoc, OverrideLoc));
}

if (HasVirtual) {
// Remove virtual unless requested otherwise
Copy link
Author

@fwinkl fwinkl Jun 19, 2025

Choose a reason for hiding this comment

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

Note that even when AllowVirtual is used, the check does not add any missing virtual keywords. If this is preferred one could instead add a UseVirtualAndOverride option which would then also add a missing virtual specifier.

But as this is a bit more work I would first like to judge the interest in that feature in general.

@llvmbot
Copy link
Member

llvmbot commented Jun 19, 2025

@llvm/pr-subscribers-clang-tidy

Author: Frank Winklmeier (fwinkl)

Changes

Add a new AllowVirtual option to 'modernize-use-override', which does not remove the virtual specifier. This is useful because some coding guidelines may suggest to use virtual ... override for clarity. See the new unit test for examples.


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

4 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.cpp (+10-3)
  • (modified) clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.h (+1)
  • (modified) clang-tools-extra/docs/clang-tidy/checks/modernize/use-override.rst (+7-1)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-override-allow-virtual.cpp (+25)
diff --git a/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.cpp
index fd5bd9f0b181b..c18bd68a99b37 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.cpp
@@ -22,6 +22,7 @@ UseOverrideCheck::UseOverrideCheck(StringRef Name, ClangTidyContext *Context)
       IgnoreTemplateInstantiations(
           Options.get("IgnoreTemplateInstantiations", false)),
       AllowOverrideAndFinal(Options.get("AllowOverrideAndFinal", false)),
+      AllowVirtual(Options.get("AllowVirtual", false)),
       OverrideSpelling(Options.get("OverrideSpelling", "override")),
       FinalSpelling(Options.get("FinalSpelling", "final")) {}
 
@@ -30,6 +31,7 @@ void UseOverrideCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
   Options.store(Opts, "IgnoreTemplateInstantiations",
                 IgnoreTemplateInstantiations);
   Options.store(Opts, "AllowOverrideAndFinal", AllowOverrideAndFinal);
+  Options.store(Opts, "AllowVirtual", AllowVirtual);
   Options.store(Opts, "OverrideSpelling", OverrideSpelling);
   Options.store(Opts, "FinalSpelling", FinalSpelling);
 }
@@ -111,15 +113,19 @@ void UseOverrideCheck::check(const MatchFinder::MatchResult &Result) {
   unsigned KeywordCount = HasVirtual + HasOverride + HasFinal;
 
   if ((!OnlyVirtualSpecified && KeywordCount == 1) ||
+      (HasVirtual && HasOverride && AllowVirtual) ||
       (!HasVirtual && HasOverride && HasFinal && AllowOverrideAndFinal))
     return; // Nothing to do.
 
   std::string Message;
   if (OnlyVirtualSpecified) {
-    Message = "prefer using '%0' or (rarely) '%1' instead of 'virtual'";
+    if (AllowVirtual)
+      Message = "add '%0' or (rarely) '%1' for 'virtual' function";
+    else
+      Message = "prefer using '%0' or (rarely) '%1' instead of 'virtual'";
   } else if (KeywordCount == 0) {
     Message = "annotate this function with '%0' or (rarely) '%1'";
-  } else {
+  } else if (!AllowVirtual) {
     StringRef Redundant =
         HasVirtual ? (HasOverride && HasFinal && !AllowOverrideAndFinal
                           ? "'virtual' and '%0' are"
@@ -226,7 +232,8 @@ void UseOverrideCheck::check(const MatchFinder::MatchResult &Result) {
         CharSourceRange::getTokenRange(OverrideLoc, OverrideLoc));
   }
 
-  if (HasVirtual) {
+  // Remove virtual unless requested otherwise
+  if (HasVirtual && !AllowVirtual) {
     for (Token Tok : Tokens) {
       if (Tok.is(tok::kw_virtual)) {
         std::optional<Token> NextToken =
diff --git a/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.h b/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.h
index 2c624f48fcc85..6879221ab4284 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.h
+++ b/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.h
@@ -29,6 +29,7 @@ class UseOverrideCheck : public ClangTidyCheck {
   const bool IgnoreDestructors;
   const bool IgnoreTemplateInstantiations;
   const bool AllowOverrideAndFinal;
+  const bool AllowVirtual;
   const StringRef OverrideSpelling;
   const StringRef FinalSpelling;
 };
diff --git a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-override.rst b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-override.rst
index f8f34794af749..d3ed73ef35a51 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-override.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-override.rst
@@ -4,7 +4,8 @@ modernize-use-override
 ======================
 
 Adds ``override`` (introduced in C++11) to overridden virtual functions and
-removes ``virtual`` from those functions as it is not required.
+removes ``virtual`` from those functions (unless the `AllowVirtual` option is
+used).
 
 ``virtual`` on non base class implementations was used to help indicate to the
 user that a function was virtual. C++ compilers did not use the presence of
@@ -40,6 +41,11 @@ Options
    members, such as ``gcc -Wsuggest-override``/``gcc -Werror=suggest-override``.
    Default is `false`.
 
+.. option:: AllowVirtual
+
+   If set to `true`, ``virtual`` will not be removed by this check when adding
+   ``override`` or ``final``. Default is `false`.
+
 .. option:: OverrideSpelling
 
    Specifies a macro to use instead of ``override``. This is useful when
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-override-allow-virtual.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-override-allow-virtual.cpp
new file mode 100644
index 0000000000000..17be170e0d9cd
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-override-allow-virtual.cpp
@@ -0,0 +1,25 @@
+// RUN: %check_clang_tidy %s modernize-use-override %t -- \
+// RUN:   -config="{CheckOptions: {modernize-use-override.AllowVirtual: true}}"
+
+struct Base {
+  virtual ~Base();
+  virtual void a();
+  virtual void b();
+  virtual void c();
+};
+
+struct Derived : public Base {
+  virtual ~Derived() override;
+
+  virtual void a() override;
+  // CHECK-MESSAGES-NOT: warning:
+  // CHECK-FIXES: {{^}}  virtual void a() override;
+
+  virtual void b();
+  // CHECK-MESSAGES: :[[@LINE-1]]:16: warning: add 'override'
+  // CHECK-FIXES: {{^}}  virtual void b() override;
+
+  void c();
+  // CHECK-MESSAGES: :[[@LINE-1]]:8: warning: annotate
+  // CHECK-FIXES: {{^}}  void c() override;
+};

@llvmbot
Copy link
Member

llvmbot commented Jun 19, 2025

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

Author: Frank Winklmeier (fwinkl)

Changes

Add a new AllowVirtual option to 'modernize-use-override', which does not remove the virtual specifier. This is useful because some coding guidelines may suggest to use virtual ... override for clarity. See the new unit test for examples.


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

4 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.cpp (+10-3)
  • (modified) clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.h (+1)
  • (modified) clang-tools-extra/docs/clang-tidy/checks/modernize/use-override.rst (+7-1)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-override-allow-virtual.cpp (+25)
diff --git a/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.cpp
index fd5bd9f0b181b..c18bd68a99b37 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.cpp
@@ -22,6 +22,7 @@ UseOverrideCheck::UseOverrideCheck(StringRef Name, ClangTidyContext *Context)
       IgnoreTemplateInstantiations(
           Options.get("IgnoreTemplateInstantiations", false)),
       AllowOverrideAndFinal(Options.get("AllowOverrideAndFinal", false)),
+      AllowVirtual(Options.get("AllowVirtual", false)),
       OverrideSpelling(Options.get("OverrideSpelling", "override")),
       FinalSpelling(Options.get("FinalSpelling", "final")) {}
 
@@ -30,6 +31,7 @@ void UseOverrideCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
   Options.store(Opts, "IgnoreTemplateInstantiations",
                 IgnoreTemplateInstantiations);
   Options.store(Opts, "AllowOverrideAndFinal", AllowOverrideAndFinal);
+  Options.store(Opts, "AllowVirtual", AllowVirtual);
   Options.store(Opts, "OverrideSpelling", OverrideSpelling);
   Options.store(Opts, "FinalSpelling", FinalSpelling);
 }
@@ -111,15 +113,19 @@ void UseOverrideCheck::check(const MatchFinder::MatchResult &Result) {
   unsigned KeywordCount = HasVirtual + HasOverride + HasFinal;
 
   if ((!OnlyVirtualSpecified && KeywordCount == 1) ||
+      (HasVirtual && HasOverride && AllowVirtual) ||
       (!HasVirtual && HasOverride && HasFinal && AllowOverrideAndFinal))
     return; // Nothing to do.
 
   std::string Message;
   if (OnlyVirtualSpecified) {
-    Message = "prefer using '%0' or (rarely) '%1' instead of 'virtual'";
+    if (AllowVirtual)
+      Message = "add '%0' or (rarely) '%1' for 'virtual' function";
+    else
+      Message = "prefer using '%0' or (rarely) '%1' instead of 'virtual'";
   } else if (KeywordCount == 0) {
     Message = "annotate this function with '%0' or (rarely) '%1'";
-  } else {
+  } else if (!AllowVirtual) {
     StringRef Redundant =
         HasVirtual ? (HasOverride && HasFinal && !AllowOverrideAndFinal
                           ? "'virtual' and '%0' are"
@@ -226,7 +232,8 @@ void UseOverrideCheck::check(const MatchFinder::MatchResult &Result) {
         CharSourceRange::getTokenRange(OverrideLoc, OverrideLoc));
   }
 
-  if (HasVirtual) {
+  // Remove virtual unless requested otherwise
+  if (HasVirtual && !AllowVirtual) {
     for (Token Tok : Tokens) {
       if (Tok.is(tok::kw_virtual)) {
         std::optional<Token> NextToken =
diff --git a/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.h b/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.h
index 2c624f48fcc85..6879221ab4284 100644
--- a/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.h
+++ b/clang-tools-extra/clang-tidy/modernize/UseOverrideCheck.h
@@ -29,6 +29,7 @@ class UseOverrideCheck : public ClangTidyCheck {
   const bool IgnoreDestructors;
   const bool IgnoreTemplateInstantiations;
   const bool AllowOverrideAndFinal;
+  const bool AllowVirtual;
   const StringRef OverrideSpelling;
   const StringRef FinalSpelling;
 };
diff --git a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-override.rst b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-override.rst
index f8f34794af749..d3ed73ef35a51 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-override.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-override.rst
@@ -4,7 +4,8 @@ modernize-use-override
 ======================
 
 Adds ``override`` (introduced in C++11) to overridden virtual functions and
-removes ``virtual`` from those functions as it is not required.
+removes ``virtual`` from those functions (unless the `AllowVirtual` option is
+used).
 
 ``virtual`` on non base class implementations was used to help indicate to the
 user that a function was virtual. C++ compilers did not use the presence of
@@ -40,6 +41,11 @@ Options
    members, such as ``gcc -Wsuggest-override``/``gcc -Werror=suggest-override``.
    Default is `false`.
 
+.. option:: AllowVirtual
+
+   If set to `true`, ``virtual`` will not be removed by this check when adding
+   ``override`` or ``final``. Default is `false`.
+
 .. option:: OverrideSpelling
 
    Specifies a macro to use instead of ``override``. This is useful when
diff --git a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-override-allow-virtual.cpp b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-override-allow-virtual.cpp
new file mode 100644
index 0000000000000..17be170e0d9cd
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-override-allow-virtual.cpp
@@ -0,0 +1,25 @@
+// RUN: %check_clang_tidy %s modernize-use-override %t -- \
+// RUN:   -config="{CheckOptions: {modernize-use-override.AllowVirtual: true}}"
+
+struct Base {
+  virtual ~Base();
+  virtual void a();
+  virtual void b();
+  virtual void c();
+};
+
+struct Derived : public Base {
+  virtual ~Derived() override;
+
+  virtual void a() override;
+  // CHECK-MESSAGES-NOT: warning:
+  // CHECK-FIXES: {{^}}  virtual void a() override;
+
+  virtual void b();
+  // CHECK-MESSAGES: :[[@LINE-1]]:16: warning: add 'override'
+  // CHECK-FIXES: {{^}}  virtual void b() override;
+
+  void c();
+  // CHECK-MESSAGES: :[[@LINE-1]]:8: warning: annotate
+  // CHECK-FIXES: {{^}}  void c() override;
+};

Copy link
Contributor

@vbvictor vbvictor left a comment

Choose a reason for hiding this comment

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

Please add your change to ReleaseNotes.rst

Comment on lines +15 to +16
// CHECK-MESSAGES-NOT: warning:
// CHECK-FIXES: {{^}} virtual void a() override;
Copy link
Contributor

Choose a reason for hiding this comment

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

You should just write nothing. By default, tests will fail if there will be a message


virtual void b();
// CHECK-MESSAGES: :[[@LINE-1]]:16: warning: add 'override'
// CHECK-FIXES: {{^}} virtual void b() override;
Copy link
Contributor

Choose a reason for hiding this comment

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

don't use {{^}}, just match string as a whole


void c();
// CHECK-MESSAGES: :[[@LINE-1]]:8: warning: annotate
// CHECK-FIXES: {{^}} void c() override;
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto {{^}}

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