Skip to content

Conversation

5chmidti
Copy link
Contributor

@5chmidti 5chmidti commented Oct 9, 2025

This check finds all functions and variables that can be declared as constexpr, using the specified standard version to check if the requirements are met.

This is part 1/N, adding only the C++11 rule-set. The implementation for C++11-C++23 is done. I'll start looking into C++26 and C23 next.

Fixes #115622

This check finds all functions and variables that can be declared as
`constexpr`, using the specified standard version to check if the
requirements are met.

This is part 1/N, adding only the C++11 rule-set.

Fixes llvm#115622
@llvmbot
Copy link
Member

llvmbot commented Oct 9, 2025

@llvm/pr-subscribers-clang-tidy

Author: Julian Schmidt (5chmidti)

Changes

This check finds all functions and variables that can be declared as constexpr, using the specified standard version to check if the requirements are met.

This is part 1/N, adding only the C++11 rule-set.

Fixes #115622


Patch is 51.72 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/162741.diff

13 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/modernize/CMakeLists.txt (+1)
  • (modified) clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp (+2)
  • (added) clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.cpp (+517)
  • (added) clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.h (+45)
  • (modified) clang-tools-extra/clang-tidy/utils/ASTUtils.cpp (+17)
  • (modified) clang-tools-extra/clang-tidy/utils/ASTUtils.h (+3)
  • (modified) clang-tools-extra/docs/ReleaseNotes.rst (+5)
  • (modified) clang-tools-extra/docs/clang-tidy/checks/list.rst (+1)
  • (added) clang-tools-extra/docs/clang-tidy/checks/modernize/use-constexpr.rst (+73)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-constexpr-cxx20.cpp (+36)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-constexpr-locals.cpp (+20)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-constexpr-replacement-opts.cpp (+32)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-constexpr.cpp (+461)
diff --git a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
index 882f2dc9fb4d8..f7dbc1d7312ee 100644
--- a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
@@ -32,6 +32,7 @@ add_clang_library(clangTidyModernizeModule STATIC
   UnaryStaticAssertCheck.cpp
   UseAutoCheck.cpp
   UseBoolLiteralsCheck.cpp
+  UseConstexprCheck.cpp
   UseConstraintsCheck.cpp
   UseDefaultMemberInitCheck.cpp
   UseDesignatedInitializersCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
index 360e2b8434d0c..1b54adeef9281 100644
--- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
@@ -33,6 +33,7 @@
 #include "UnaryStaticAssertCheck.h"
 #include "UseAutoCheck.h"
 #include "UseBoolLiteralsCheck.h"
+#include "UseConstexprCheck.h"
 #include "UseConstraintsCheck.h"
 #include "UseDefaultMemberInitCheck.h"
 #include "UseDesignatedInitializersCheck.h"
@@ -82,6 +83,7 @@ class ModernizeModule : public ClangTidyModule {
     CheckFactories.registerCheck<MinMaxUseInitializerListCheck>(
         "modernize-min-max-use-initializer-list");
     CheckFactories.registerCheck<PassByValueCheck>("modernize-pass-by-value");
+    CheckFactories.registerCheck<UseConstexprCheck>("modernize-use-constexpr");
     CheckFactories.registerCheck<UseDesignatedInitializersCheck>(
         "modernize-use-designated-initializers");
     CheckFactories.registerCheck<UseIntegerSignComparisonCheck>(
diff --git a/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.cpp
new file mode 100644
index 0000000000000..9238605b3cb4a
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.cpp
@@ -0,0 +1,517 @@
+//===--- UseConstexprCheck.cpp - clang-tidy--------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "UseConstexprCheck.h"
+#include "../utils/ASTUtils.h"
+#include "../utils/LexerUtils.h"
+#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallPtrSet.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::modernize {
+
+namespace {
+AST_MATCHER(FunctionDecl, locationPermitsConstexpr) {
+  const bool IsInMainFile =
+      Finder->getASTContext().getSourceManager().isInMainFile(
+          Node.getLocation());
+
+  if (IsInMainFile && Node.hasExternalFormalLinkage())
+    return false;
+  if (!IsInMainFile && !Node.isInlined())
+    return false;
+
+  return true;
+}
+
+AST_MATCHER(Expr, isCXX11ConstantExpr) {
+  return !Node.isValueDependent() &&
+         Node.isCXX11ConstantExpr(Finder->getASTContext());
+}
+
+AST_MATCHER(DeclaratorDecl, isInMacro) {
+  const SourceRange R =
+      SourceRange(Node.getTypeSpecStartLoc(), Node.getLocation());
+
+  return Node.getLocation().isMacroID() || Node.getEndLoc().isMacroID() ||
+         utils::rangeContainsMacroExpansion(
+             R, &Finder->getASTContext().getSourceManager()) ||
+         utils::rangeIsEntirelyWithinMacroArgument(
+             R, &Finder->getASTContext().getSourceManager());
+}
+
+AST_MATCHER(Decl, hasNoRedecl) {
+  // There is always the actual declaration
+  return !Node.redecls().empty() &&
+         std::next(Node.redecls_begin()) == Node.redecls_end();
+}
+
+AST_MATCHER(Decl, allRedeclsInSameFile) {
+  const SourceManager &SM = Finder->getASTContext().getSourceManager();
+  const SourceLocation L = Node.getLocation();
+  for (const Decl *ReDecl : Node.redecls()) {
+    if (!SM.isWrittenInSameFile(L, ReDecl->getLocation()))
+      return false;
+  }
+  return true;
+}
+} // namespace
+
+static bool
+satisfiesConstructorPropertiesUntil20(const CXXConstructorDecl *Ctor,
+                                      ASTContext &Ctx) {
+  const CXXRecordDecl *Rec = Ctor->getParent();
+  llvm::SmallPtrSet<const RecordDecl *, 8> Bases{};
+  for (const CXXBaseSpecifier Base : Rec->bases())
+    Bases.insert(Base.getType()->getAsRecordDecl());
+
+  llvm::SmallPtrSet<const FieldDecl *, 8> Fields{Rec->field_begin(),
+                                                 Rec->field_end()};
+  llvm::SmallPtrSet<const FieldDecl *, 4> Indirects{};
+
+  for (const CXXCtorInitializer *const Init : Ctor->inits()) {
+    const Type *InitType = Init->getBaseClass();
+    if (InitType && InitType->isRecordType()) {
+      const auto *ConstructingInit =
+          llvm::dyn_cast<CXXConstructExpr>(Init->getInit());
+      if (ConstructingInit &&
+          !ConstructingInit->getConstructor()->isConstexprSpecified())
+        return false;
+    }
+
+    if (Init->isBaseInitializer()) {
+      Bases.erase(Init->getBaseClass()->getAsRecordDecl());
+      continue;
+    }
+
+    if (Init->isMemberInitializer()) {
+      const FieldDecl *Field = Init->getMember();
+
+      if (Field->isAnonymousStructOrUnion())
+        Indirects.insert(Field);
+
+      Fields.erase(Field);
+      continue;
+    }
+  }
+
+  for (const auto &Match :
+       match(cxxRecordDecl(forEach(indirectFieldDecl().bind("indirect"))), *Rec,
+             Ctx)) {
+    const auto *IField = Match.getNodeAs<IndirectFieldDecl>("indirect");
+
+    size_t NumInitializations = false;
+    for (const NamedDecl *ND : IField->chain())
+      NumInitializations += Indirects.erase(llvm::dyn_cast<FieldDecl>(ND));
+
+    if (NumInitializations != 1)
+      return false;
+
+    for (const NamedDecl *ND : IField->chain())
+      Fields.erase(llvm::dyn_cast<FieldDecl>(ND));
+  }
+
+  if (!Fields.empty())
+    return false;
+
+  return true;
+}
+
+static bool isLiteralType(QualType QT, const ASTContext &Ctx,
+                          bool ConservativeLiteralType);
+
+static bool isLiteralType(const Type *T, const ASTContext &Ctx,
+                          const bool ConservativeLiteralType) {
+  if (!T)
+    return false;
+
+  if (!T->isLiteralType(Ctx))
+    return false;
+
+  if (!ConservativeLiteralType)
+    return T->isLiteralType(Ctx) && !T->isVoidType();
+
+  if (T->isIncompleteType() || T->isIncompleteArrayType())
+    return false;
+
+  T = utils::unwrapPointee(T);
+  if (!T)
+    return false;
+
+  assert(!T->isPointerOrReferenceType());
+
+  if (T->isIncompleteType() || T->isIncompleteArrayType())
+    return false;
+
+  if (T->isLiteralType(Ctx))
+    return true;
+
+  if (const CXXRecordDecl *Rec = T->getAsCXXRecordDecl()) {
+    if (llvm::any_of(Rec->ctors(), [](const CXXConstructorDecl *Ctor) {
+          return !Ctor->isCopyOrMoveConstructor() &&
+                 Ctor->isConstexprSpecified();
+        }))
+      return false;
+
+    for (const CXXBaseSpecifier Base : Rec->bases()) {
+      if (!isLiteralType(Base.getType(), Ctx, ConservativeLiteralType))
+        return false;
+    }
+  }
+
+  if (const Type *ArrayElementType = T->getArrayElementTypeNoTypeQual())
+    return isLiteralType(ArrayElementType, Ctx, ConservativeLiteralType);
+
+  return false;
+}
+
+static bool isLiteralType(QualType QT, const ASTContext &Ctx,
+                          const bool ConservativeLiteralType) {
+  return !QT.isVolatileQualified() &&
+         isLiteralType(QT.getTypePtr(), Ctx, ConservativeLiteralType);
+}
+
+static bool satisfiesProperties11(
+    const FunctionDecl *FDecl, ASTContext &Ctx,
+    const bool ConservativeLiteralType,
+    const bool AddConstexprToMethodOfClassWithoutConstexprConstructor) {
+  if (FDecl->isConstexprSpecified())
+    return true;
+
+  const LangOptions LO = Ctx.getLangOpts();
+  const auto *Method = llvm::dyn_cast<CXXMethodDecl>(FDecl);
+  if (Method && !Method->isStatic() &&
+      !Method->getParent()->hasConstexprNonCopyMoveConstructor() &&
+      !AddConstexprToMethodOfClassWithoutConstexprConstructor)
+    return false;
+
+  if (Method &&
+      (Method->isVirtual() ||
+       !match(cxxMethodDecl(hasBody(cxxTryStmt())), *Method, Ctx).empty()))
+    return false;
+
+  if (const auto *Ctor = llvm::dyn_cast<CXXConstructorDecl>(FDecl);
+      Ctor && (!satisfiesConstructorPropertiesUntil20(Ctor, Ctx) ||
+               llvm::any_of(Ctor->getParent()->bases(),
+                            [](const CXXBaseSpecifier &Base) {
+                              return Base.isVirtual();
+                            })))
+    return false;
+
+  if (const auto *Dtor = llvm::dyn_cast<CXXDestructorDecl>(FDecl);
+      Dtor && !Dtor->isTrivial())
+    return false;
+
+  if (!isLiteralType(FDecl->getReturnType(), Ctx, ConservativeLiteralType))
+    return false;
+
+  for (const ParmVarDecl *Param : FDecl->parameters())
+    if (!isLiteralType(Param->getType(), Ctx, ConservativeLiteralType))
+      return false;
+
+  class Visitor11 : public clang::RecursiveASTVisitor<Visitor11> {
+  public:
+    using Base = clang::RecursiveASTVisitor<Visitor11>;
+    bool shouldVisitImplicitCode() const { return true; }
+
+    Visitor11(ASTContext &Ctx, bool ConservativeLiteralType)
+        : Ctx(Ctx), ConservativeLiteralType(ConservativeLiteralType) {}
+
+    bool WalkUpFromNullStmt(NullStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool WalkUpFromDeclStmt(DeclStmt *DS) {
+      for (const Decl *D : DS->decls())
+        if (!llvm::isa<StaticAssertDecl, TypedefNameDecl, UsingDecl,
+                       UsingDirectiveDecl>(D)) {
+          Possible = false;
+          return false;
+        }
+      return true;
+    }
+
+    bool WalkUpFromExpr(Expr *) { return true; }
+    bool WalkUpFromCompoundStmt(CompoundStmt *S) {
+      for (const DynTypedNode &Node : Ctx.getParents(*S))
+        if (Node.get<FunctionDecl>() != nullptr)
+          return true;
+
+      Possible = false;
+      return false;
+    }
+    bool WalkUpFromStmt(Stmt *) {
+      Possible = false;
+      return false;
+    }
+
+    bool WalkUpFromReturnStmt(ReturnStmt *) {
+      ++NumReturns;
+      if (NumReturns != 1U) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool WalkUpFromCastExpr(CastExpr *CE) {
+      if (llvm::is_contained(
+              {
+                  CK_LValueBitCast,
+                  CK_IntegralToPointer,
+                  CK_PointerToIntegral,
+              },
+              CE->getCastKind())) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXDynamicCastExpr(CXXDynamicCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseCXXReinterpretCastExpr(CXXReinterpretCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseType(QualType QT, const bool TraverseQualifier = true) {
+      if (QT.isNull())
+        return true;
+      if (!isLiteralType(QT, Ctx, ConservativeLiteralType)) {
+        Possible = false;
+        return false;
+      }
+      return Base::TraverseType(QT, TraverseQualifier);
+    }
+
+    bool WalkUpFromCXXConstructExpr(CXXConstructExpr *CE) {
+      if (const CXXConstructorDecl *Ctor = CE->getConstructor();
+          Ctor && !Ctor->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+
+      return true;
+    }
+    bool WalkUpFromCallExpr(CallExpr *CE) {
+      if (const auto *FDecl =
+              llvm::dyn_cast_if_present<FunctionDecl>(CE->getCalleeDecl());
+          FDecl && !FDecl->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXNewExpr(CXXNewExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseDeclRefExpr(DeclRefExpr *DRef) {
+      if (DRef->getType().isVolatileQualified()) {
+        Possible = false;
+        return false;
+      }
+      return Base::TraverseDeclRefExpr(DRef);
+    }
+
+    ASTContext &Ctx;
+    const bool ConservativeLiteralType;
+    bool Possible = true;
+    size_t NumReturns = 0;
+  };
+
+  Visitor11 V{Ctx, ConservativeLiteralType};
+  V.TraverseDecl(const_cast<FunctionDecl *>(FDecl));
+  if (!V.Possible)
+    return false;
+
+  return true;
+}
+
+namespace {
+AST_MATCHER_P2(FunctionDecl, satisfiesProperties, bool, ConservativeLiteralType,
+               bool, AddConstexprToMethodOfClassWithoutConstexprConstructor) {
+  ASTContext &Ctx = Finder->getASTContext();
+  const LangOptions LO = Ctx.getLangOpts();
+
+  if (LO.CPlusPlus11)
+    return satisfiesProperties11(
+        &Node, Ctx, ConservativeLiteralType,
+        AddConstexprToMethodOfClassWithoutConstexprConstructor);
+
+  return false;
+}
+
+AST_MATCHER_P(VarDecl, satisfiesVariableProperties, bool,
+              ConservativeLiteralType) {
+  ASTContext &Ctx = Finder->getASTContext();
+
+  const QualType QT = Node.getType();
+  const Type *T = QT.getTypePtr();
+  if (!T)
+    return false;
+
+  if (!isLiteralType(QT, Ctx, ConservativeLiteralType))
+    return false;
+
+  const bool IsDeclaredInsideConstexprFunction = std::invoke([&Node]() {
+    const auto *Func = llvm::dyn_cast<FunctionDecl>(Node.getDeclContext());
+    if (!Func)
+      return false;
+    return Func->isConstexpr();
+  });
+
+  if (Node.isStaticLocal() && IsDeclaredInsideConstexprFunction)
+    return false;
+
+  if (!Ctx.getLangOpts().CPlusPlus20)
+    return true;
+
+  const CXXRecordDecl *RDecl = T->getAsCXXRecordDecl();
+  const Type *const ArrayOrPtrElement = T->getPointeeOrArrayElementType();
+  if (ArrayOrPtrElement)
+    RDecl = ArrayOrPtrElement->getAsCXXRecordDecl();
+
+  if (RDecl && (!RDecl->hasDefinition() || !RDecl->hasConstexprDestructor()))
+    return false;
+
+  return true;
+}
+} // namespace
+
+void UseConstexprCheck::registerMatchers(MatchFinder *Finder) {
+  Finder->addMatcher(
+      functionDecl(
+          isDefinition(),
+          unless(anyOf(isConstexpr(), isImplicit(), hasExternalFormalLinkage(),
+                       isInMacro(), isMain(), isInStdNamespace(),
+                       isExpansionInSystemHeader(), isExternC(),
+                       cxxMethodDecl(ofClass(cxxRecordDecl(isLambda()))))),
+          locationPermitsConstexpr(), allRedeclsInSameFile(),
+          satisfiesProperties(
+              ConservativeLiteralType,
+              AddConstexprToMethodOfClassWithoutConstexprConstructor))
+          .bind("func"),
+      this);
+
+  const auto CallToNonConstexprFunction =
+      callExpr(callee(functionDecl(unless(isConstexpr()))));
+
+  const auto VarSupportingConstexpr =
+      varDecl(
+          unless(anyOf(parmVarDecl(), isImplicit(), isInStdNamespace(),
+                       isExpansionInSystemHeader(), isConstexpr(), isExternC(),
+                       hasExternalFormalLinkage(), isInMacro())),
+          hasNoRedecl(), hasType(qualType(isConstQualified())),
+          satisfiesVariableProperties(ConservativeLiteralType),
+          hasInitializer(
+              expr(isCXX11ConstantExpr(), unless(CallToNonConstexprFunction),
+                   unless(hasDescendant(CallToNonConstexprFunction)))))
+          .bind("var");
+  Finder->addMatcher(mapAnyOf(translationUnitDecl, namespaceDecl)
+                         .with(forEach(VarSupportingConstexpr)),
+                     this);
+  Finder->addMatcher(declStmt(hasSingleDecl(VarSupportingConstexpr)), this);
+}
+
+static const FunctionDecl *
+maybeResolveToTemplateDecl(const FunctionDecl *Func) {
+  if (Func && Func->isTemplateInstantiation())
+    Func = Func->getTemplateInstantiationPattern();
+  return Func;
+}
+
+void UseConstexprCheck::check(const MatchFinder::MatchResult &Result) {
+  if (const auto *Func = Result.Nodes.getNodeAs<FunctionDecl>("func")) {
+    Func = maybeResolveToTemplateDecl(Func);
+    if (Func)
+      Functions.insert(Func);
+    return;
+  }
+
+  if (const auto *Var = Result.Nodes.getNodeAs<VarDecl>("var")) {
+    if (const VarDecl *VarTemplate = Var->getTemplateInstantiationPattern())
+      Var = VarTemplate;
+
+    Variables.insert(Var);
+    return;
+  }
+}
+
+void UseConstexprCheck::onEndOfTranslationUnit() {
+  const std::string FunctionReplacement = ConstexprString + " ";
+
+  for (const FunctionDecl *Func : Functions) {
+    const SourceRange R =
+        SourceRange(Func->getTypeSpecStartLoc(), Func->getLocation());
+    auto Diag = diag(Func->getLocation(), "declare function %0 as 'constexpr'")
+                << Func << R;
+
+    for (const Decl *D : Func->redecls())
+      if (const auto *FDecl = llvm::dyn_cast<FunctionDecl>(D))
+        Diag << FixItHint::CreateInsertion(FDecl->getTypeSpecStartLoc(),
+                                           FunctionReplacement);
+  }
+
+  const auto MaybeRemoveConst = [&, this](DiagnosticBuilder &Diag,
+                                          const VarDecl *Var) {
+    // Since either of the locs can be in a macro, use `makeFileCharRange` to be
+    // sure that we have a consistent `CharSourceRange`, located entirely in the
+    // source file.
+    const CharSourceRange FileRange = Lexer::makeFileCharRange(
+        CharSourceRange::getCharRange(Var->getInnerLocStart(),
+                                      Var->getLocation()),
+        Var->getASTContext().getSourceManager(), getLangOpts());
+    if (const std::optional<Token> ConstToken =
+            utils::lexer::getQualifyingToken(
+                tok::TokenKind::kw_const, FileRange, Var->getASTContext(),
+                Var->getASTContext().getSourceManager())) {
+      Diag << FixItHint::CreateRemoval(ConstToken->getLocation());
+    }
+  };
+
+  for (const auto *Var : Variables) {
+    const SourceRange R =
+        SourceRange(Var->getTypeSpecStartLoc(), Var->getLocation());
+    auto Diag = diag(Var->getLocation(), "declare variable %0 as 'constexpr'")
+                << Var << R
+                << FixItHint::CreateInsertion(Var->getTypeSpecStartLoc(),
+                                              FunctionReplacement);
+    MaybeRemoveConst(Diag, Var);
+  }
+
+  Functions.clear();
+  Variables.clear();
+}
+
+UseConstexprCheck::UseConstexprCheck(StringRef Name, ClangTidyContext *Context)
+    : ClangTidyCheck(Name, Context),
+      ConservativeLiteralType(Options.get("ConservativeLiteralType", true)),
+      AddConstexprToMethodOfClassWithoutConstexprConstructor(Options.get(
+          "AddConstexprToMethodOfClassWithoutConstexprConstructor", false)),
+      ConstexprString(Options.get("ConstexprString", "constexpr")),
+      StaticConstexprString(
+          Options.get("StaticConstexprString", "static " + ConstexprString)) {}
+
+void UseConstexprCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
+  Options.store(Opts, "ConservativeLiteralType", ConservativeLiteralType);
+  Options.store(Opts, "AddConstexprToMethodOfClassWithoutConstexprConstructor",
+                AddConstexprToMethodOfClassWithoutConstexprConstructor);
+  Options.store(Opts, "ConstexprString", ConstexprString);
+  Options.store(Opts, "StaticConstexprString", StaticConstexprString);
+}
+} // namespace clang::tidy::modernize
diff --git a/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.h b/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.h
new file mode 100644
index 0000000000000..bfb206d40ba47
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.h
@@ -0,0 +1,45 @@
+//===--- UseUseConstexprCheck.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_MODERNIZE_USECONSTEXPRCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USECONSTEXPRCHECK_H
+
+#include "../ClangTidyCheck.h"
+#include "clang/AST/Decl.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/SmallPtrSet.h"
+
+namespace clang::tidy::modernize {
+
+/// Finds functions and variables that can be declared 'constexpr'.
+///
+/// For the user-facing documentation see:
+/// http://clang.llvm.org/extra/clang-...
[truncated]

@llvmbot
Copy link
Member

llvmbot commented Oct 9, 2025

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

Author: Julian Schmidt (5chmidti)

Changes

This check finds all functions and variables that can be declared as constexpr, using the specified standard version to check if the requirements are met.

This is part 1/N, adding only the C++11 rule-set.

Fixes #115622


Patch is 51.72 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/162741.diff

13 Files Affected:

  • (modified) clang-tools-extra/clang-tidy/modernize/CMakeLists.txt (+1)
  • (modified) clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp (+2)
  • (added) clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.cpp (+517)
  • (added) clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.h (+45)
  • (modified) clang-tools-extra/clang-tidy/utils/ASTUtils.cpp (+17)
  • (modified) clang-tools-extra/clang-tidy/utils/ASTUtils.h (+3)
  • (modified) clang-tools-extra/docs/ReleaseNotes.rst (+5)
  • (modified) clang-tools-extra/docs/clang-tidy/checks/list.rst (+1)
  • (added) clang-tools-extra/docs/clang-tidy/checks/modernize/use-constexpr.rst (+73)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-constexpr-cxx20.cpp (+36)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-constexpr-locals.cpp (+20)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-constexpr-replacement-opts.cpp (+32)
  • (added) clang-tools-extra/test/clang-tidy/checkers/modernize/use-constexpr.cpp (+461)
diff --git a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
index 882f2dc9fb4d8..f7dbc1d7312ee 100644
--- a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
@@ -32,6 +32,7 @@ add_clang_library(clangTidyModernizeModule STATIC
   UnaryStaticAssertCheck.cpp
   UseAutoCheck.cpp
   UseBoolLiteralsCheck.cpp
+  UseConstexprCheck.cpp
   UseConstraintsCheck.cpp
   UseDefaultMemberInitCheck.cpp
   UseDesignatedInitializersCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
index 360e2b8434d0c..1b54adeef9281 100644
--- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
@@ -33,6 +33,7 @@
 #include "UnaryStaticAssertCheck.h"
 #include "UseAutoCheck.h"
 #include "UseBoolLiteralsCheck.h"
+#include "UseConstexprCheck.h"
 #include "UseConstraintsCheck.h"
 #include "UseDefaultMemberInitCheck.h"
 #include "UseDesignatedInitializersCheck.h"
@@ -82,6 +83,7 @@ class ModernizeModule : public ClangTidyModule {
     CheckFactories.registerCheck<MinMaxUseInitializerListCheck>(
         "modernize-min-max-use-initializer-list");
     CheckFactories.registerCheck<PassByValueCheck>("modernize-pass-by-value");
+    CheckFactories.registerCheck<UseConstexprCheck>("modernize-use-constexpr");
     CheckFactories.registerCheck<UseDesignatedInitializersCheck>(
         "modernize-use-designated-initializers");
     CheckFactories.registerCheck<UseIntegerSignComparisonCheck>(
diff --git a/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.cpp
new file mode 100644
index 0000000000000..9238605b3cb4a
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.cpp
@@ -0,0 +1,517 @@
+//===--- UseConstexprCheck.cpp - clang-tidy--------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "UseConstexprCheck.h"
+#include "../utils/ASTUtils.h"
+#include "../utils/LexerUtils.h"
+#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallPtrSet.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::modernize {
+
+namespace {
+AST_MATCHER(FunctionDecl, locationPermitsConstexpr) {
+  const bool IsInMainFile =
+      Finder->getASTContext().getSourceManager().isInMainFile(
+          Node.getLocation());
+
+  if (IsInMainFile && Node.hasExternalFormalLinkage())
+    return false;
+  if (!IsInMainFile && !Node.isInlined())
+    return false;
+
+  return true;
+}
+
+AST_MATCHER(Expr, isCXX11ConstantExpr) {
+  return !Node.isValueDependent() &&
+         Node.isCXX11ConstantExpr(Finder->getASTContext());
+}
+
+AST_MATCHER(DeclaratorDecl, isInMacro) {
+  const SourceRange R =
+      SourceRange(Node.getTypeSpecStartLoc(), Node.getLocation());
+
+  return Node.getLocation().isMacroID() || Node.getEndLoc().isMacroID() ||
+         utils::rangeContainsMacroExpansion(
+             R, &Finder->getASTContext().getSourceManager()) ||
+         utils::rangeIsEntirelyWithinMacroArgument(
+             R, &Finder->getASTContext().getSourceManager());
+}
+
+AST_MATCHER(Decl, hasNoRedecl) {
+  // There is always the actual declaration
+  return !Node.redecls().empty() &&
+         std::next(Node.redecls_begin()) == Node.redecls_end();
+}
+
+AST_MATCHER(Decl, allRedeclsInSameFile) {
+  const SourceManager &SM = Finder->getASTContext().getSourceManager();
+  const SourceLocation L = Node.getLocation();
+  for (const Decl *ReDecl : Node.redecls()) {
+    if (!SM.isWrittenInSameFile(L, ReDecl->getLocation()))
+      return false;
+  }
+  return true;
+}
+} // namespace
+
+static bool
+satisfiesConstructorPropertiesUntil20(const CXXConstructorDecl *Ctor,
+                                      ASTContext &Ctx) {
+  const CXXRecordDecl *Rec = Ctor->getParent();
+  llvm::SmallPtrSet<const RecordDecl *, 8> Bases{};
+  for (const CXXBaseSpecifier Base : Rec->bases())
+    Bases.insert(Base.getType()->getAsRecordDecl());
+
+  llvm::SmallPtrSet<const FieldDecl *, 8> Fields{Rec->field_begin(),
+                                                 Rec->field_end()};
+  llvm::SmallPtrSet<const FieldDecl *, 4> Indirects{};
+
+  for (const CXXCtorInitializer *const Init : Ctor->inits()) {
+    const Type *InitType = Init->getBaseClass();
+    if (InitType && InitType->isRecordType()) {
+      const auto *ConstructingInit =
+          llvm::dyn_cast<CXXConstructExpr>(Init->getInit());
+      if (ConstructingInit &&
+          !ConstructingInit->getConstructor()->isConstexprSpecified())
+        return false;
+    }
+
+    if (Init->isBaseInitializer()) {
+      Bases.erase(Init->getBaseClass()->getAsRecordDecl());
+      continue;
+    }
+
+    if (Init->isMemberInitializer()) {
+      const FieldDecl *Field = Init->getMember();
+
+      if (Field->isAnonymousStructOrUnion())
+        Indirects.insert(Field);
+
+      Fields.erase(Field);
+      continue;
+    }
+  }
+
+  for (const auto &Match :
+       match(cxxRecordDecl(forEach(indirectFieldDecl().bind("indirect"))), *Rec,
+             Ctx)) {
+    const auto *IField = Match.getNodeAs<IndirectFieldDecl>("indirect");
+
+    size_t NumInitializations = false;
+    for (const NamedDecl *ND : IField->chain())
+      NumInitializations += Indirects.erase(llvm::dyn_cast<FieldDecl>(ND));
+
+    if (NumInitializations != 1)
+      return false;
+
+    for (const NamedDecl *ND : IField->chain())
+      Fields.erase(llvm::dyn_cast<FieldDecl>(ND));
+  }
+
+  if (!Fields.empty())
+    return false;
+
+  return true;
+}
+
+static bool isLiteralType(QualType QT, const ASTContext &Ctx,
+                          bool ConservativeLiteralType);
+
+static bool isLiteralType(const Type *T, const ASTContext &Ctx,
+                          const bool ConservativeLiteralType) {
+  if (!T)
+    return false;
+
+  if (!T->isLiteralType(Ctx))
+    return false;
+
+  if (!ConservativeLiteralType)
+    return T->isLiteralType(Ctx) && !T->isVoidType();
+
+  if (T->isIncompleteType() || T->isIncompleteArrayType())
+    return false;
+
+  T = utils::unwrapPointee(T);
+  if (!T)
+    return false;
+
+  assert(!T->isPointerOrReferenceType());
+
+  if (T->isIncompleteType() || T->isIncompleteArrayType())
+    return false;
+
+  if (T->isLiteralType(Ctx))
+    return true;
+
+  if (const CXXRecordDecl *Rec = T->getAsCXXRecordDecl()) {
+    if (llvm::any_of(Rec->ctors(), [](const CXXConstructorDecl *Ctor) {
+          return !Ctor->isCopyOrMoveConstructor() &&
+                 Ctor->isConstexprSpecified();
+        }))
+      return false;
+
+    for (const CXXBaseSpecifier Base : Rec->bases()) {
+      if (!isLiteralType(Base.getType(), Ctx, ConservativeLiteralType))
+        return false;
+    }
+  }
+
+  if (const Type *ArrayElementType = T->getArrayElementTypeNoTypeQual())
+    return isLiteralType(ArrayElementType, Ctx, ConservativeLiteralType);
+
+  return false;
+}
+
+static bool isLiteralType(QualType QT, const ASTContext &Ctx,
+                          const bool ConservativeLiteralType) {
+  return !QT.isVolatileQualified() &&
+         isLiteralType(QT.getTypePtr(), Ctx, ConservativeLiteralType);
+}
+
+static bool satisfiesProperties11(
+    const FunctionDecl *FDecl, ASTContext &Ctx,
+    const bool ConservativeLiteralType,
+    const bool AddConstexprToMethodOfClassWithoutConstexprConstructor) {
+  if (FDecl->isConstexprSpecified())
+    return true;
+
+  const LangOptions LO = Ctx.getLangOpts();
+  const auto *Method = llvm::dyn_cast<CXXMethodDecl>(FDecl);
+  if (Method && !Method->isStatic() &&
+      !Method->getParent()->hasConstexprNonCopyMoveConstructor() &&
+      !AddConstexprToMethodOfClassWithoutConstexprConstructor)
+    return false;
+
+  if (Method &&
+      (Method->isVirtual() ||
+       !match(cxxMethodDecl(hasBody(cxxTryStmt())), *Method, Ctx).empty()))
+    return false;
+
+  if (const auto *Ctor = llvm::dyn_cast<CXXConstructorDecl>(FDecl);
+      Ctor && (!satisfiesConstructorPropertiesUntil20(Ctor, Ctx) ||
+               llvm::any_of(Ctor->getParent()->bases(),
+                            [](const CXXBaseSpecifier &Base) {
+                              return Base.isVirtual();
+                            })))
+    return false;
+
+  if (const auto *Dtor = llvm::dyn_cast<CXXDestructorDecl>(FDecl);
+      Dtor && !Dtor->isTrivial())
+    return false;
+
+  if (!isLiteralType(FDecl->getReturnType(), Ctx, ConservativeLiteralType))
+    return false;
+
+  for (const ParmVarDecl *Param : FDecl->parameters())
+    if (!isLiteralType(Param->getType(), Ctx, ConservativeLiteralType))
+      return false;
+
+  class Visitor11 : public clang::RecursiveASTVisitor<Visitor11> {
+  public:
+    using Base = clang::RecursiveASTVisitor<Visitor11>;
+    bool shouldVisitImplicitCode() const { return true; }
+
+    Visitor11(ASTContext &Ctx, bool ConservativeLiteralType)
+        : Ctx(Ctx), ConservativeLiteralType(ConservativeLiteralType) {}
+
+    bool WalkUpFromNullStmt(NullStmt *) {
+      Possible = false;
+      return false;
+    }
+    bool WalkUpFromDeclStmt(DeclStmt *DS) {
+      for (const Decl *D : DS->decls())
+        if (!llvm::isa<StaticAssertDecl, TypedefNameDecl, UsingDecl,
+                       UsingDirectiveDecl>(D)) {
+          Possible = false;
+          return false;
+        }
+      return true;
+    }
+
+    bool WalkUpFromExpr(Expr *) { return true; }
+    bool WalkUpFromCompoundStmt(CompoundStmt *S) {
+      for (const DynTypedNode &Node : Ctx.getParents(*S))
+        if (Node.get<FunctionDecl>() != nullptr)
+          return true;
+
+      Possible = false;
+      return false;
+    }
+    bool WalkUpFromStmt(Stmt *) {
+      Possible = false;
+      return false;
+    }
+
+    bool WalkUpFromReturnStmt(ReturnStmt *) {
+      ++NumReturns;
+      if (NumReturns != 1U) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool WalkUpFromCastExpr(CastExpr *CE) {
+      if (llvm::is_contained(
+              {
+                  CK_LValueBitCast,
+                  CK_IntegralToPointer,
+                  CK_PointerToIntegral,
+              },
+              CE->getCastKind())) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXDynamicCastExpr(CXXDynamicCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseCXXReinterpretCastExpr(CXXReinterpretCastExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseType(QualType QT, const bool TraverseQualifier = true) {
+      if (QT.isNull())
+        return true;
+      if (!isLiteralType(QT, Ctx, ConservativeLiteralType)) {
+        Possible = false;
+        return false;
+      }
+      return Base::TraverseType(QT, TraverseQualifier);
+    }
+
+    bool WalkUpFromCXXConstructExpr(CXXConstructExpr *CE) {
+      if (const CXXConstructorDecl *Ctor = CE->getConstructor();
+          Ctor && !Ctor->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+
+      return true;
+    }
+    bool WalkUpFromCallExpr(CallExpr *CE) {
+      if (const auto *FDecl =
+              llvm::dyn_cast_if_present<FunctionDecl>(CE->getCalleeDecl());
+          FDecl && !FDecl->isConstexprSpecified()) {
+        Possible = false;
+        return false;
+      }
+      return true;
+    }
+
+    bool TraverseCXXNewExpr(CXXNewExpr *) {
+      Possible = false;
+      return false;
+    }
+
+    bool TraverseDeclRefExpr(DeclRefExpr *DRef) {
+      if (DRef->getType().isVolatileQualified()) {
+        Possible = false;
+        return false;
+      }
+      return Base::TraverseDeclRefExpr(DRef);
+    }
+
+    ASTContext &Ctx;
+    const bool ConservativeLiteralType;
+    bool Possible = true;
+    size_t NumReturns = 0;
+  };
+
+  Visitor11 V{Ctx, ConservativeLiteralType};
+  V.TraverseDecl(const_cast<FunctionDecl *>(FDecl));
+  if (!V.Possible)
+    return false;
+
+  return true;
+}
+
+namespace {
+AST_MATCHER_P2(FunctionDecl, satisfiesProperties, bool, ConservativeLiteralType,
+               bool, AddConstexprToMethodOfClassWithoutConstexprConstructor) {
+  ASTContext &Ctx = Finder->getASTContext();
+  const LangOptions LO = Ctx.getLangOpts();
+
+  if (LO.CPlusPlus11)
+    return satisfiesProperties11(
+        &Node, Ctx, ConservativeLiteralType,
+        AddConstexprToMethodOfClassWithoutConstexprConstructor);
+
+  return false;
+}
+
+AST_MATCHER_P(VarDecl, satisfiesVariableProperties, bool,
+              ConservativeLiteralType) {
+  ASTContext &Ctx = Finder->getASTContext();
+
+  const QualType QT = Node.getType();
+  const Type *T = QT.getTypePtr();
+  if (!T)
+    return false;
+
+  if (!isLiteralType(QT, Ctx, ConservativeLiteralType))
+    return false;
+
+  const bool IsDeclaredInsideConstexprFunction = std::invoke([&Node]() {
+    const auto *Func = llvm::dyn_cast<FunctionDecl>(Node.getDeclContext());
+    if (!Func)
+      return false;
+    return Func->isConstexpr();
+  });
+
+  if (Node.isStaticLocal() && IsDeclaredInsideConstexprFunction)
+    return false;
+
+  if (!Ctx.getLangOpts().CPlusPlus20)
+    return true;
+
+  const CXXRecordDecl *RDecl = T->getAsCXXRecordDecl();
+  const Type *const ArrayOrPtrElement = T->getPointeeOrArrayElementType();
+  if (ArrayOrPtrElement)
+    RDecl = ArrayOrPtrElement->getAsCXXRecordDecl();
+
+  if (RDecl && (!RDecl->hasDefinition() || !RDecl->hasConstexprDestructor()))
+    return false;
+
+  return true;
+}
+} // namespace
+
+void UseConstexprCheck::registerMatchers(MatchFinder *Finder) {
+  Finder->addMatcher(
+      functionDecl(
+          isDefinition(),
+          unless(anyOf(isConstexpr(), isImplicit(), hasExternalFormalLinkage(),
+                       isInMacro(), isMain(), isInStdNamespace(),
+                       isExpansionInSystemHeader(), isExternC(),
+                       cxxMethodDecl(ofClass(cxxRecordDecl(isLambda()))))),
+          locationPermitsConstexpr(), allRedeclsInSameFile(),
+          satisfiesProperties(
+              ConservativeLiteralType,
+              AddConstexprToMethodOfClassWithoutConstexprConstructor))
+          .bind("func"),
+      this);
+
+  const auto CallToNonConstexprFunction =
+      callExpr(callee(functionDecl(unless(isConstexpr()))));
+
+  const auto VarSupportingConstexpr =
+      varDecl(
+          unless(anyOf(parmVarDecl(), isImplicit(), isInStdNamespace(),
+                       isExpansionInSystemHeader(), isConstexpr(), isExternC(),
+                       hasExternalFormalLinkage(), isInMacro())),
+          hasNoRedecl(), hasType(qualType(isConstQualified())),
+          satisfiesVariableProperties(ConservativeLiteralType),
+          hasInitializer(
+              expr(isCXX11ConstantExpr(), unless(CallToNonConstexprFunction),
+                   unless(hasDescendant(CallToNonConstexprFunction)))))
+          .bind("var");
+  Finder->addMatcher(mapAnyOf(translationUnitDecl, namespaceDecl)
+                         .with(forEach(VarSupportingConstexpr)),
+                     this);
+  Finder->addMatcher(declStmt(hasSingleDecl(VarSupportingConstexpr)), this);
+}
+
+static const FunctionDecl *
+maybeResolveToTemplateDecl(const FunctionDecl *Func) {
+  if (Func && Func->isTemplateInstantiation())
+    Func = Func->getTemplateInstantiationPattern();
+  return Func;
+}
+
+void UseConstexprCheck::check(const MatchFinder::MatchResult &Result) {
+  if (const auto *Func = Result.Nodes.getNodeAs<FunctionDecl>("func")) {
+    Func = maybeResolveToTemplateDecl(Func);
+    if (Func)
+      Functions.insert(Func);
+    return;
+  }
+
+  if (const auto *Var = Result.Nodes.getNodeAs<VarDecl>("var")) {
+    if (const VarDecl *VarTemplate = Var->getTemplateInstantiationPattern())
+      Var = VarTemplate;
+
+    Variables.insert(Var);
+    return;
+  }
+}
+
+void UseConstexprCheck::onEndOfTranslationUnit() {
+  const std::string FunctionReplacement = ConstexprString + " ";
+
+  for (const FunctionDecl *Func : Functions) {
+    const SourceRange R =
+        SourceRange(Func->getTypeSpecStartLoc(), Func->getLocation());
+    auto Diag = diag(Func->getLocation(), "declare function %0 as 'constexpr'")
+                << Func << R;
+
+    for (const Decl *D : Func->redecls())
+      if (const auto *FDecl = llvm::dyn_cast<FunctionDecl>(D))
+        Diag << FixItHint::CreateInsertion(FDecl->getTypeSpecStartLoc(),
+                                           FunctionReplacement);
+  }
+
+  const auto MaybeRemoveConst = [&, this](DiagnosticBuilder &Diag,
+                                          const VarDecl *Var) {
+    // Since either of the locs can be in a macro, use `makeFileCharRange` to be
+    // sure that we have a consistent `CharSourceRange`, located entirely in the
+    // source file.
+    const CharSourceRange FileRange = Lexer::makeFileCharRange(
+        CharSourceRange::getCharRange(Var->getInnerLocStart(),
+                                      Var->getLocation()),
+        Var->getASTContext().getSourceManager(), getLangOpts());
+    if (const std::optional<Token> ConstToken =
+            utils::lexer::getQualifyingToken(
+                tok::TokenKind::kw_const, FileRange, Var->getASTContext(),
+                Var->getASTContext().getSourceManager())) {
+      Diag << FixItHint::CreateRemoval(ConstToken->getLocation());
+    }
+  };
+
+  for (const auto *Var : Variables) {
+    const SourceRange R =
+        SourceRange(Var->getTypeSpecStartLoc(), Var->getLocation());
+    auto Diag = diag(Var->getLocation(), "declare variable %0 as 'constexpr'")
+                << Var << R
+                << FixItHint::CreateInsertion(Var->getTypeSpecStartLoc(),
+                                              FunctionReplacement);
+    MaybeRemoveConst(Diag, Var);
+  }
+
+  Functions.clear();
+  Variables.clear();
+}
+
+UseConstexprCheck::UseConstexprCheck(StringRef Name, ClangTidyContext *Context)
+    : ClangTidyCheck(Name, Context),
+      ConservativeLiteralType(Options.get("ConservativeLiteralType", true)),
+      AddConstexprToMethodOfClassWithoutConstexprConstructor(Options.get(
+          "AddConstexprToMethodOfClassWithoutConstexprConstructor", false)),
+      ConstexprString(Options.get("ConstexprString", "constexpr")),
+      StaticConstexprString(
+          Options.get("StaticConstexprString", "static " + ConstexprString)) {}
+
+void UseConstexprCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) {
+  Options.store(Opts, "ConservativeLiteralType", ConservativeLiteralType);
+  Options.store(Opts, "AddConstexprToMethodOfClassWithoutConstexprConstructor",
+                AddConstexprToMethodOfClassWithoutConstexprConstructor);
+  Options.store(Opts, "ConstexprString", ConstexprString);
+  Options.store(Opts, "StaticConstexprString", StaticConstexprString);
+}
+} // namespace clang::tidy::modernize
diff --git a/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.h b/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.h
new file mode 100644
index 0000000000000..bfb206d40ba47
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseConstexprCheck.h
@@ -0,0 +1,45 @@
+//===--- UseUseConstexprCheck.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_MODERNIZE_USECONSTEXPRCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USECONSTEXPRCHECK_H
+
+#include "../ClangTidyCheck.h"
+#include "clang/AST/Decl.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/SmallPtrSet.h"
+
+namespace clang::tidy::modernize {
+
+/// Finds functions and variables that can be declared 'constexpr'.
+///
+/// For the user-facing documentation see:
+/// http://clang.llvm.org/extra/clang-...
[truncated]

@5chmidti
Copy link
Contributor Author

5chmidti commented Oct 9, 2025

One issue that I have found when I ran this on VTK:
When specializing a function template, the check will add constexpr to the non-specialized template for each specialization. I have left this in for now, but maybe the check should instead not add constexpr to the non-specialized re-decl for every specialization. WDYT?

template <typename T>
- int foo();
+ constexpr constexpr int foo(); 

template <>
- int foo<int>() { return 10; }
+ constexpr int foo<int>() { return 10; }

template <>
- int foo<unsigned int>() { return 0; }
+ constexpr int foo<unsigned int>() { return 0; }

@5chmidti 5chmidti changed the title [clang-tidy] add misc-constexpr check [clang-tidy] add modernize-use-constexpr check Oct 9, 2025
Copy link
Contributor

@zwuis zwuis left a comment

Choose a reason for hiding this comment

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

Thank you for your hard work!

Comment on lines +27 to +32
if (IsInMainFile && Node.hasExternalFormalLinkage())
return false;
if (!IsInMainFile && !Node.isInlined())
return false;

return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

if (IsInMainFile)
  return !Node.hasExternalFormalLinkage();

// not in main file
return Node.isInlined();

I think it's more clear.

Comment on lines +60 to +64
for (const Decl *ReDecl : Node.redecls()) {
if (!SM.isWrittenInSameFile(L, ReDecl->getLocation()))
return false;
}
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use llvm::all_of?

Comment on lines +81 to +82
const Type *InitType = Init->getBaseClass();
if (InitType && InitType->isRecordType()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if (const Type *InitType = ...;
    InitType && ...) {
  // ...
}

Indirects.insert(Field);

Fields.erase(Field);
continue;
Copy link
Contributor

Choose a reason for hiding this comment

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

Unnecessary continue?

Comment on lines +371 to +376
const bool IsDeclaredInsideConstexprFunction = std::invoke([&Node]() {
const auto *Func = llvm::dyn_cast<FunctionDecl>(Node.getDeclContext());
if (!Func)
return false;
return Func->isConstexpr();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

I think calling lambda immediately is better than using std::invoke. It is already clear that lambda is a function object.

Comment on lines +373 to +375
if (!Func)
return false;
return Func->isConstexpr();
Copy link
Contributor

Choose a reason for hiding this comment

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

return Func && Func->isConstexpr();

Comment on lines +389 to +390
if (RDecl && (!RDecl->hasDefinition() || !RDecl->hasConstexprDestructor()))
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

if (RDecl)
  return RDecl->hasDefinition() && RDecl->hasConstexprDestructor();

Comment on lines +400 to +403
unless(anyOf(isConstexpr(), isImplicit(), hasExternalFormalLinkage(),
isInMacro(), isMain(), isInStdNamespace(),
isExpansionInSystemHeader(), isExternC(),
cxxMethodDecl(ofClass(cxxRecordDecl(isLambda()))))),
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add isDeleted to unless? It seems meaningless to suggest adding constexpr to functions with = delete.

FunctionReplacement);
}

const auto MaybeRemoveConst = [&, this](DiagnosticBuilder &Diag,
Copy link
Contributor

Choose a reason for hiding this comment

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

*this is already implicitly captured by reference if the capture-default is &.

};

This type is a literal type, but can not be constructed at compile-time.
With `ConservativeLiteralType` equal to `true`, variables or funtions
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
With `ConservativeLiteralType` equal to `true`, variables or funtions
With :option:`ConservativeLiteralType` equal to `true`, variables or functions

.. option:: ConservativeLiteralType

With this option enabled, only literal types that can be constructed at
compile-time are considered to supoprt ``constexpr``.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
compile-time are considered to supoprt ``constexpr``.
compile-time are considered to support ``constexpr``.

Comment on lines +45 to +49
struct NonLiteral{
NonLiteral();
~NonLiteral();
int &ref;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Only struct with constexpr constructor can be marked as constexpr. even ConservativeLiteralType is false, NonLiteral cannot be marked as constexpr IMO.
This code block will confuse me how to make this NonLiteral constexpr.

Copy link
Contributor

@zwuis zwuis left a comment

Choose a reason for hiding this comment

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

Can you add more test cases with anonymous union members and constructors?


I realise that suggesting llvm::any_of or llvm::all_of on short for loops is my personal preference. It is not listed in llvm coding standards. Please ignore these suggestions if you don't like them.

satisfiesConstructorPropertiesUntil20(const CXXConstructorDecl *Ctor,
ASTContext &Ctx) {
const CXXRecordDecl *Rec = Ctor->getParent();
llvm::SmallPtrSet<const RecordDecl *, 8> Bases{};
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused variable?


if (Method &&
(Method->isVirtual() ||
!match(cxxMethodDecl(hasBody(cxxTryStmt())), *Method, Ctx).empty()))
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we remove this? It seems that function try blocks are correctly handled by Visitor11.

Comment on lines +203 to +206
llvm::any_of(Ctor->getParent()->bases(),
[](const CXXBaseSpecifier &Base) {
return Base.isVirtual();
})))
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this case handled correctly

struct A {};
struct B : virtual A {};
struct C : B {
  /* constexpr? */ C() {}
};

? Perhaps using CXXRecordDecl::getNumVBases is better.

Comment on lines +216 to +218
for (const ParmVarDecl *Param : FDecl->parameters())
if (!isLiteralType(Param->getType(), Ctx, ConservativeLiteralType))
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use llvm::all_of?

Comment on lines +381 to +382
if (!Ctx.getLangOpts().CPlusPlus20)
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

C++20 should support more constexpr cases, Is this condition reversed?

}
}

void UseConstexprCheck::onEndOfTranslationUnit() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we put diag in UseConstexprCheck::check?

@vbvictor
Copy link
Contributor

I realise that suggesting llvm::any_of or llvm::all_of on short for loops is my personal preference. It is not listed in llvm coding standards. Please ignore these suggestions if you don't like them.

I think that any_of/all_of is preferred to code readability and there is effort to enable clang-tidy rule for it: #156161. (At least I'd like it to be enabled, but we will discuss usefulness when PR is done)

Copy link
Collaborator

@shafik shafik left a comment

Choose a reason for hiding this comment

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

Will this recommend constexpr for arrays? What about globals?

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.

[clang-tidy] suggest adding constexpr when it is possible

7 participants