https://github.com/hjanuschka created
https://github.com/llvm/llvm-project/pull/182023
Add new clang-tidy check that suggests changing signed integer variables to
`size_t` when they are initialized from unsigned sources and only used in
unsigned-compatible contexts.
Storing a `size_t` value in a signed `int` can cause implicit narrowing
conversions and sign-comparison warnings. Using `size_t` directly avoids these
issues.
For example:
```cpp
std::vector<int> v;
int n = v.size(); // -> size_t n = v.size();
v.resize(n);
int len = s.length(); // -> size_t len = s.length();
for (int i = 0; i < len; ++i) { s[i]; }
```
The check only triggers when the variable is initialized from an unsigned
expression (e.g. `.size()`, `.length()`) and every use is in an
unsigned-compatible context (comparisons, function args expecting unsigned,
array subscripts). Variables used in signed arithmetic are not transformed.
>From 303546ea0f30c2b6fb5489fd55c0e3cfcdcfb4bf Mon Sep 17 00:00:00 2001
From: Helmut Januschka <[email protected]>
Date: Wed, 18 Feb 2026 14:30:08 +0100
Subject: [PATCH] [clang-tidy] Add modernize-use-size-type check
Suggests changing signed integer variables to size_t when they are initialized
from unsigned sources and only used in unsigned-compatible contexts.
---
.../clang-tidy/modernize/CMakeLists.txt | 1 +
.../modernize/ModernizeTidyModule.cpp | 2 +
.../clang-tidy/modernize/UseSizeTypeCheck.cpp | 160 ++++++++++++++++++
.../clang-tidy/modernize/UseSizeTypeCheck.h | 36 ++++
clang-tools-extra/docs/ReleaseNotes.rst | 7 +
.../docs/clang-tidy/checks/list.rst | 1 +
.../checks/modernize/use-size-type.rst | 38 +++++
.../checkers/modernize/use-size-type.cpp | 78 +++++++++
8 files changed, 323 insertions(+)
create mode 100644 clang-tools-extra/clang-tidy/modernize/UseSizeTypeCheck.cpp
create mode 100644 clang-tools-extra/clang-tidy/modernize/UseSizeTypeCheck.h
create mode 100644
clang-tools-extra/docs/clang-tidy/checks/modernize/use-size-type.rst
create mode 100644
clang-tools-extra/test/clang-tidy/checkers/modernize/use-size-type.cpp
diff --git a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
index cc4cc7a02b594..22ebd20a66211 100644
--- a/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/modernize/CMakeLists.txt
@@ -46,6 +46,7 @@ add_clang_library(clangTidyModernizeModule STATIC
UseOverrideCheck.cpp
UseRangesCheck.cpp
UseScopedLockCheck.cpp
+ UseSizeTypeCheck.cpp
UseStartsEndsWithCheck.cpp
UseStdFormatCheck.cpp
UseStdNumbersCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
index fcb860d8c5298..38e539adaeec8 100644
--- a/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/modernize/ModernizeTidyModule.cpp
@@ -46,6 +46,7 @@
#include "UseOverrideCheck.h"
#include "UseRangesCheck.h"
#include "UseScopedLockCheck.h"
+#include "UseSizeTypeCheck.h"
#include "UseStartsEndsWithCheck.h"
#include "UseStdFormatCheck.h"
#include "UseStdNumbersCheck.h"
@@ -94,6 +95,7 @@ class ModernizeModule : public ClangTidyModule {
CheckFactories.registerCheck<UseRangesCheck>("modernize-use-ranges");
CheckFactories.registerCheck<UseScopedLockCheck>(
"modernize-use-scoped-lock");
+ CheckFactories.registerCheck<UseSizeTypeCheck>("modernize-use-size-type");
CheckFactories.registerCheck<UseStartsEndsWithCheck>(
"modernize-use-starts-ends-with");
CheckFactories.registerCheck<UseStdFormatCheck>("modernize-use-std-format");
diff --git a/clang-tools-extra/clang-tidy/modernize/UseSizeTypeCheck.cpp
b/clang-tools-extra/clang-tidy/modernize/UseSizeTypeCheck.cpp
new file mode 100644
index 0000000000000..f4d9a5c0cd509
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseSizeTypeCheck.cpp
@@ -0,0 +1,160 @@
+//===--- UseSizeTypeCheck.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 "UseSizeTypeCheck.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::modernize {
+
+void UseSizeTypeCheck::registerMatchers(MatchFinder *Finder) {
+ // Match local variables with signed integer type initialized from an
+ // unsigned expression (via implicit cast).
+ Finder->addMatcher(
+ varDecl(hasLocalStorage(),
+ hasType(qualType(hasCanonicalType(isSignedInteger()))),
+ hasInitializer(ignoringImplicit(expr(hasType(
+ qualType(hasCanonicalType(isUnsignedInteger())))))),
+ unless(isConstexpr()),
+ hasParent(declStmt(hasParent(
+ compoundStmt().bind("scope")))))
+ .bind("var"),
+ this);
+}
+
+/// Return true if every use of \p VD within \p Scope is in a context that
+/// expects or is compatible with an unsigned / size_t type.
+static bool allUsesAreUnsignedCompatible(const VarDecl *VD,
+ const CompoundStmt *Scope,
+ ASTContext &Ctx) {
+ auto Refs = match(
+ findAll(declRefExpr(to(varDecl(equalsNode(VD)))).bind("ref")),
+ *Scope, Ctx);
+
+ if (Refs.empty())
+ return false;
+
+ for (const auto &Ref : Refs) {
+ const auto *DRE = Ref.getNodeAs<DeclRefExpr>("ref");
+ if (!DRE)
+ return false;
+
+ const auto Parents = Ctx.getParents(*DRE);
+ if (Parents.empty())
+ return false;
+
+ // Walk up through implicit casts to find the "real" parent context.
+ // The DeclRefExpr is typically wrapped in LValueToRValue and
+ // IntegralCast implicit casts.
+ const Expr *Current = DRE;
+ DynTypedNodeList CurrentParents = Parents;
+ while (CurrentParents.size() == 1) {
+ if (const auto *ICE = CurrentParents[0].get<ImplicitCastExpr>()) {
+ Current = ICE;
+ CurrentParents = Ctx.getParents(*ICE);
+ continue;
+ }
+ break;
+ }
+
+ bool UsageOk = false;
+ for (const auto &Parent : CurrentParents) {
+ // Used in binary comparison with unsigned operand.
+ if (const auto *BO = Parent.get<BinaryOperator>()) {
+ if (BO->isComparisonOp()) {
+ UsageOk = true;
+ break;
+ }
+ // Also accept arithmetic where the result feeds into an
+ // unsigned context (but not standalone).
+ }
+
+ // Used as function argument (CallExpr or CXXMemberCallExpr).
+ if (const auto *CE = Parent.get<CallExpr>()) {
+ for (unsigned I = 0; I < CE->getNumArgs(); ++I) {
+ if (CE->getArg(I)->IgnoreParenImpCasts() == DRE) {
+ if (const auto *FD = CE->getDirectCallee()) {
+ if (I < FD->getNumParams()) {
+ const QualType PT =
+ FD->getParamDecl(I)->getType().getCanonicalType();
+ if (PT->isUnsignedIntegerType())
+ UsageOk = true;
+ }
+ }
+ break;
+ }
+ }
+ if (UsageOk)
+ break;
+ }
+
+ // Used as array subscript index.
+ if (const auto *ASE = Parent.get<ArraySubscriptExpr>()) {
+ if (ASE->getIdx()->IgnoreParenImpCasts() == DRE) {
+ UsageOk = true;
+ break;
+ }
+ }
+
+ // Used in operator[] (CXXOperatorCallExpr).
+ if (const auto *OCE = Parent.get<CXXOperatorCallExpr>()) {
+ if (OCE->getOperator() == OO_Subscript &&
+ OCE->getNumArgs() > 1 &&
+ OCE->getArg(1)->IgnoreParenImpCasts() == DRE) {
+ UsageOk = true;
+ break;
+ }
+ if (UsageOk)
+ break;
+ }
+ }
+
+ if (!UsageOk)
+ return false;
+ }
+
+ return true;
+}
+
+void UseSizeTypeCheck::check(const MatchFinder::MatchResult &Result) {
+ const auto *VD = Result.Nodes.getNodeAs<VarDecl>("var");
+ const auto *Scope = Result.Nodes.getNodeAs<CompoundStmt>("scope");
+ if (!VD || !Scope)
+ return;
+
+ // Skip dependent types.
+ if (VD->getType()->isDependentType())
+ return;
+
+ // Skip macros.
+ if (VD->getLocation().isMacroID())
+ return;
+
+ // Check all uses are unsigned-compatible.
+ if (!allUsesAreUnsignedCompatible(VD, Scope, *Result.Context))
+ return;
+
+ // Get the type specifier source range to replace.
+ const SourceLocation TypeStart = VD->getTypeSpecStartLoc();
+ const SourceLocation TypeEnd = VD->getTypeSpecEndLoc();
+ if (TypeStart.isInvalid() || TypeEnd.isInvalid())
+ return;
+ if (TypeStart.isMacroID() || TypeEnd.isMacroID())
+ return;
+
+ diag(VD->getLocation(),
+ "variable %0 is of signed type %1 but is initialized from and "
+ "used as an unsigned value; consider using 'size_t'")
+ << VD << VD->getType()
+ << FixItHint::CreateReplacement(
+ CharSourceRange::getTokenRange(TypeStart, TypeEnd), "size_t");
+}
+
+} // namespace clang::tidy::modernize
diff --git a/clang-tools-extra/clang-tidy/modernize/UseSizeTypeCheck.h
b/clang-tools-extra/clang-tidy/modernize/UseSizeTypeCheck.h
new file mode 100644
index 0000000000000..ce466e18a847b
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/modernize/UseSizeTypeCheck.h
@@ -0,0 +1,36 @@
+//===--- UseSizeTypeCheck.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_USESIZETYPECHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESIZETYPECHECK_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::modernize {
+
+/// Finds local variables declared as signed integer types that are initialized
+/// from an unsigned/``size_t`` source (e.g. ``container.size()``) and only
used
+/// in contexts expecting unsigned types, and suggests changing the type to
+/// ``size_t``.
+///
+/// For the user-facing documentation see:
+/// https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-size-type.html
+class UseSizeTypeCheck : public ClangTidyCheck {
+public:
+ UseSizeTypeCheck(StringRef Name, ClangTidyContext *Context)
+ : ClangTidyCheck(Name, Context) {}
+ void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+ void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+ bool isLanguageVersionSupported(const LangOptions &LangOpts) const override {
+ return LangOpts.CPlusPlus11;
+ }
+};
+
+} // namespace clang::tidy::modernize
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_MODERNIZE_USESIZETYPECHECK_H
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst
b/clang-tools-extra/docs/ReleaseNotes.rst
index 68bab88146241..0dc391644e88e 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -121,6 +121,13 @@ New checks
``llvm::to_vector(llvm::make_filter_range(...))`` that can be replaced with
``llvm::map_to_vector`` and ``llvm::filter_to_vector``.
+- New :doc:`modernize-use-size-type
+ <clang-tidy/checks/modernize/use-size-type>` check.
+
+ Finds local variables declared as signed integer types initialized
+ from unsigned sources and only used in unsigned contexts, and
+ suggests changing the type to ``size_t``.
+
- New :doc:`modernize-use-string-view
<clang-tidy/checks/modernize/use-string-view>` check.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst
b/clang-tools-extra/docs/clang-tidy/checks/list.rst
index c475870ed7b31..a4138f3c282f3 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -326,6 +326,7 @@ Clang-Tidy Checks
:doc:`modernize-use-override <modernize/use-override>`, "Yes"
:doc:`modernize-use-ranges <modernize/use-ranges>`, "Yes"
:doc:`modernize-use-scoped-lock <modernize/use-scoped-lock>`, "Yes"
+ :doc:`modernize-use-size-type <modernize/use-size-type>`, "Yes"
:doc:`modernize-use-starts-ends-with <modernize/use-starts-ends-with>`,
"Yes"
:doc:`modernize-use-std-format <modernize/use-std-format>`, "Yes"
:doc:`modernize-use-std-numbers <modernize/use-std-numbers>`, "Yes"
diff --git
a/clang-tools-extra/docs/clang-tidy/checks/modernize/use-size-type.rst
b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-size-type.rst
new file mode 100644
index 0000000000000..9fc7b780ae016
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/modernize/use-size-type.rst
@@ -0,0 +1,38 @@
+.. title:: clang-tidy - modernize-use-size-type
+
+modernize-use-size-type
+=======================
+
+Finds local variables declared as signed integer types that are
+initialized from an unsigned/``size_t`` source (e.g.
+``container.size()``) and only used in contexts expecting unsigned
+types, and suggests changing the type to ``size_t``.
+
+Storing a ``size_t`` value in a signed ``int`` can cause implicit
+narrowing conversions and sign-comparison warnings. Using ``size_t``
+directly avoids these issues.
+
+For example:
+
+.. code-block:: c++
+
+ std::vector<int> v;
+ int n = v.size();
+ v.resize(n);
+
+ // transforms to:
+
+ std::vector<int> v;
+ size_t n = v.size();
+ v.resize(n);
+
+The check only triggers when all of the following are true:
+
+- The variable is a local, non-static variable.
+- The variable has a signed integer type (e.g. ``int``).
+- The initializer is an unsigned integer expression.
+- Every use of the variable is in an unsigned-compatible context
+ (comparison, function argument expecting unsigned, array
+ subscript, or implicit cast to unsigned).
+- The variable is not ``constexpr``.
+- The declaration is not in a macro.
diff --git
a/clang-tools-extra/test/clang-tidy/checkers/modernize/use-size-type.cpp
b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-size-type.cpp
new file mode 100644
index 0000000000000..da23ba1b630af
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/modernize/use-size-type.cpp
@@ -0,0 +1,78 @@
+// RUN: %check_clang_tidy -std=c++17-or-later %s modernize-use-size-type %t
+
+using size_t = decltype(sizeof(0));
+
+namespace std {
+template <typename T>
+struct vector {
+ size_t size() const;
+ T &operator[](size_t);
+ const T &operator[](size_t) const;
+ void resize(size_t);
+};
+
+template <typename T>
+struct basic_string {
+ size_t size() const;
+ size_t length() const;
+ char &operator[](size_t);
+};
+using string = basic_string<char>;
+} // namespace std
+
+// Positive: int from .size(), used in comparison and subscript
+void test_size_comparison(std::vector<int> &v) {
+ int n = v.size();
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: variable 'n' is of signed type
'int' but is initialized from and used as an unsigned value; consider using
'size_t' [modernize-use-size-type]
+ // CHECK-FIXES: size_t n = v.size();
+ for (int i = 0; i < n; ++i) {
+ }
+}
+
+// Positive: int from .size(), used in resize()
+void test_size_resize(std::vector<int> &v) {
+ int s = v.size();
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: variable 's' is of signed type
'int'
+ // CHECK-FIXES: size_t s = v.size();
+ v.resize(s);
+}
+
+// Positive: int from .length(), used in subscript
+void test_length_subscript(std::string &s) {
+ int len = s.length();
+ // CHECK-MESSAGES: :[[@LINE-1]]:7: warning: variable 'len' is of signed type
'int'
+ // CHECK-FIXES: size_t len = s.length();
+ for (int i = 0; i < len; ++i) {
+ char c = s[i];
+ }
+}
+
+// Negative: variable used in signed context (subtraction producing
+// potentially negative value)
+void test_signed_arithmetic(std::vector<int> &v) {
+ int n = v.size();
+ int x = n - 10; // Used in signed arithmetic
+}
+
+// Negative: variable not initialized from unsigned
+void test_signed_init() {
+ int n = 42;
+ size_t s = n;
+}
+
+// Negative: variable is already unsigned
+void test_already_unsigned(std::vector<int> &v) {
+ size_t n = v.size();
+}
+
+// Negative: variable used in a context expecting signed
+void negative_signed_param(int x);
+void test_signed_param(std::vector<int> &v) {
+ int n = v.size();
+ negative_signed_param(n);
+}
+
+// Negative: unused variable (no uses to check)
+void test_unused(std::vector<int> &v) {
+ int n = v.size();
+}
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits