[clang] [clang-tools-extra] [clang][Analysis] Handle const-qualified pointer refs in `ExprMutationAnalyzer` (PR #190421)
Daniil Dudkin via cfe-commits
cfe-commits at lists.llvm.org
Sun Apr 19 14:04:44 PDT 2026
https://github.com/unterumarmung updated https://github.com/llvm/llvm-project/pull/190421
>From 2d2994c07aa1473253ef0e8a5414212cf8a6e23e Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Sat, 4 Apr 2026 01:33:29 +0300
Subject: [PATCH 1/2] [clang][Analysis] Handle const-qualified pointer refs in
`ExprMutationAnalyzer`
Teach ExprMutationAnalyzer to recognize references to const-qualified
pointer objects, such as `T *const &`, as non-const pointee sinks when the
pointee type itself is non-const.
Fixes #190218
Assisted by Codex.
---
clang-tools-extra/docs/ReleaseNotes.rst | 3 +
.../const-correctness-pointer-as-pointers.cpp | 55 +++++++++++++++++++
clang/lib/Analysis/ExprMutationAnalyzer.cpp | 24 +++++++-
.../Analysis/ExprMutationAnalyzerTest.cpp | 33 +++++++++++
4 files changed, 112 insertions(+), 3 deletions(-)
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 95ed0061d654c..673e563ce3eee 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -376,6 +376,9 @@ Changes in existing checks
- Fixed false positive where a pointer used with placement new was
incorrectly diagnosed as allowing the pointee to be made ``const``.
+ - Fixed false positives when pointers were later passed or bound through
+ const-qualified pointer references.
+
- Improved :doc:`misc-multiple-inheritance
<clang-tidy/checks/misc/multiple-inheritance>` by avoiding false positives when
virtual inheritance causes concrete bases to be counted more than once.
diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/const-correctness-pointer-as-pointers.cpp b/clang-tools-extra/test/clang-tidy/checkers/misc/const-correctness-pointer-as-pointers.cpp
index 4c42743af8f11..6d91de9c6fa2b 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/misc/const-correctness-pointer-as-pointers.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/misc/const-correctness-pointer-as-pointers.cpp
@@ -10,6 +10,8 @@
// RUN: }}' \
// RUN: -- -fno-delayed-template-parsing
+#include <vector>
+
void pointee_to_const() {
int a[] = {1, 2};
int *p_local0 = &a[0];
@@ -123,3 +125,56 @@ void pointer_in_emplacement_new() {
// CHECK-NOT: warning
new(ptr) int {123};
}
+
+void takesConstPointerRef(int *const &);
+void takesConstPointerRRef(int *const &&);
+using IntPtrAlias = int *;
+typedef int *IntPtrTypedef;
+void takesAliasConstPointerRef(IntPtrAlias const &);
+void takesTypedefConstPointerRef(IntPtrTypedef const &);
+
+void ignore_const_pointer_reference_sinks() {
+ int value = 0;
+
+ int *p_local0 = &value;
+ // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local0'
+ // CHECK-FIXES: int *p_local0 = &value;
+ takesConstPointerRef(p_local0);
+
+ int *p_local1 = &value;
+ // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local1'
+ // CHECK-FIXES: int *p_local1 = &value;
+ int *const &ref = p_local1;
+ (void)ref;
+
+ IntPtrAlias p_local2 = &value;
+ // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local2'
+ // CHECK-FIXES: IntPtrAlias p_local2 = &value;
+ takesAliasConstPointerRef(p_local2);
+
+ int *p_local3 = &value;
+ // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local3'
+ // CHECK-FIXES: int *p_local3 = &value;
+ takesConstPointerRRef(static_cast<int *const &&>(p_local3));
+
+ IntPtrTypedef p_local4 = &value;
+ // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local4'
+ // CHECK-FIXES: IntPtrTypedef p_local4 = &value;
+ takesTypedefConstPointerRef(p_local4);
+
+ int *p_local5 = &value;
+ // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local5'
+ // CHECK-FIXES: int *p_local5 = &value;
+ int *volatile &volatile_ref = p_local5;
+ (void)volatile_ref;
+}
+
+void ignore_range_for_pointer_reference_sink() {
+ int value = 0;
+ std::vector<int *> const source = {&value};
+ std::vector<int *> sink;
+ // CHECK-FIXES: for (int *element : source)
+ for (int *element : source)
+ // CHECK-MESSAGES-NOT: warning: pointee of variable 'element'
+ sink.push_back(element);
+}
diff --git a/clang/lib/Analysis/ExprMutationAnalyzer.cpp b/clang/lib/Analysis/ExprMutationAnalyzer.cpp
index 5def6ba3cac5a..224df4d2e9be2 100644
--- a/clang/lib/Analysis/ExprMutationAnalyzer.cpp
+++ b/clang/lib/Analysis/ExprMutationAnalyzer.cpp
@@ -145,9 +145,20 @@ class ExprPointeeResolve {
// explicit cast will be checked in `findPointeeToNonConst`
const CastKind kind = ICE->getCastKind();
if (kind == CK_LValueToRValue || kind == CK_DerivedToBase ||
- kind == CK_UncheckedDerivedToBase ||
- (kind == CK_NoOp && (ICE->getType() == ICE->getSubExpr()->getType())))
+ kind == CK_UncheckedDerivedToBase)
return resolveExpr(ICE->getSubExpr());
+ if (kind == CK_NoOp) {
+ // Binding `T *` to `T *const &` only adds top-level qualifiers to the
+ // pointer object, so this `CK_NoOp` still refers to the same pointer.
+ const QualType CastType =
+ ICE->getType().getLocalUnqualifiedType().getCanonicalType();
+ const QualType SubExprType = ICE->getSubExpr()
+ ->getType()
+ .getLocalUnqualifiedType()
+ .getCanonicalType();
+ if (CastType == SubExprType)
+ return resolveExpr(ICE->getSubExpr());
+ }
return false;
}
@@ -227,6 +238,12 @@ const auto nonConstReferenceType = [] {
referenceType(pointee(unless(isConstQualified()))));
};
+const auto referenceToPointerWithNonConstPointeeType = [] {
+ return hasUnqualifiedDesugaredType(
+ referenceType(pointee(hasUnqualifiedDesugaredType(
+ pointerType(pointee(unless(isConstQualified())))))));
+};
+
const auto nonConstPointerType = [] {
return hasUnqualifiedDesugaredType(
pointerType(pointee(unless(isConstQualified()))));
@@ -767,7 +784,8 @@ ExprMutationAnalyzer::Analyzer::findPointeeMemberMutation(const Expr *Exp) {
const Stmt *
ExprMutationAnalyzer::Analyzer::findPointeeToNonConst(const Expr *Exp) {
const auto NonConstPointerOrNonConstRefOrDependentType = type(
- anyOf(nonConstPointerType(), nonConstReferenceType(), isDependentType()));
+ anyOf(nonConstPointerType(), nonConstReferenceType(),
+ referenceToPointerWithNonConstPointeeType(), isDependentType()));
// assign
const auto InitToNonConst =
diff --git a/clang/unittests/Analysis/ExprMutationAnalyzerTest.cpp b/clang/unittests/Analysis/ExprMutationAnalyzerTest.cpp
index c63479dc26e0b..0304d2f527ca2 100644
--- a/clang/unittests/Analysis/ExprMutationAnalyzerTest.cpp
+++ b/clang/unittests/Analysis/ExprMutationAnalyzerTest.cpp
@@ -1756,6 +1756,14 @@ TEST(ExprMutationAnalyzerTest, PointeeMutatedByInitToNonConst) {
match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
EXPECT_TRUE(isPointeeMutated(Results, AST.get()));
}
+ {
+ const std::string Code =
+ "void f() { int* x = nullptr; int* const& b = x; }";
+ auto AST = buildASTFromCodeWithArgs(Code, {});
+ auto Results =
+ match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
+ EXPECT_TRUE(isPointeeMutated(Results, AST.get()));
+ }
}
TEST(ExprMutationAnalyzerTest, PointeeMutatedByAssignToNonConst) {
@@ -1801,6 +1809,31 @@ TEST(ExprMutationAnalyzerTest, PointeeMutatedByPassAsArgument) {
match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
EXPECT_TRUE(isPointeeMutated(Results, AST.get()));
}
+ {
+ const std::string Code =
+ "void b(int *const &); void f() { int* x = nullptr; b(x); }";
+ auto AST = buildASTFromCodeWithArgs(Code, {});
+ auto Results =
+ match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
+ EXPECT_TRUE(isPointeeMutated(Results, AST.get()));
+ }
+ {
+ const std::string Code =
+ "void b(int *const &&); void f() { int* x = nullptr; "
+ "b(static_cast<int *const &&>(x)); }";
+ auto AST = buildASTFromCodeWithArgs(Code, {});
+ auto Results =
+ match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
+ EXPECT_TRUE(isPointeeMutated(Results, AST.get()));
+ }
+ {
+ const std::string Code = "using IntPtr = int *; void b(IntPtr const &); "
+ "void f() { int* x = nullptr; b(x); }";
+ auto AST = buildASTFromCodeWithArgs(Code, {});
+ auto Results =
+ match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
+ EXPECT_TRUE(isPointeeMutated(Results, AST.get()));
+ }
{
const std::string Code =
"namespace std { typedef decltype(sizeof(int)) size_t; }"
>From 2c46ec6c699b347993bf805d856a43492eff999f Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <unterumarmung at yandex.ru>
Date: Mon, 20 Apr 2026 00:04:24 +0300
Subject: [PATCH 2/2] address review comments
---
clang-tools-extra/docs/ReleaseNotes.rst | 2 +-
.../const-correctness-pointer-as-pointers.cpp | 18 +++++++------
clang/lib/Analysis/ExprMutationAnalyzer.cpp | 25 ++++++++++---------
.../Analysis/ExprMutationAnalyzerTest.cpp | 24 ++++++++++++++++++
4 files changed, 49 insertions(+), 20 deletions(-)
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 673e563ce3eee..3e5091ee83ea3 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -377,7 +377,7 @@ Changes in existing checks
incorrectly diagnosed as allowing the pointee to be made ``const``.
- Fixed false positives when pointers were later passed or bound through
- const-qualified pointer references.
+ ``const``-qualified pointer references.
- Improved :doc:`misc-multiple-inheritance
<clang-tidy/checks/misc/multiple-inheritance>` by avoiding false positives when
diff --git a/clang-tools-extra/test/clang-tidy/checkers/misc/const-correctness-pointer-as-pointers.cpp b/clang-tools-extra/test/clang-tidy/checkers/misc/const-correctness-pointer-as-pointers.cpp
index 6d91de9c6fa2b..4c0050b22bb05 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/misc/const-correctness-pointer-as-pointers.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/misc/const-correctness-pointer-as-pointers.cpp
@@ -10,6 +10,7 @@
// RUN: }}' \
// RUN: -- -fno-delayed-template-parsing
+#include <memory>
#include <vector>
void pointee_to_const() {
@@ -137,33 +138,27 @@ void ignore_const_pointer_reference_sinks() {
int value = 0;
int *p_local0 = &value;
- // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local0'
// CHECK-FIXES: int *p_local0 = &value;
takesConstPointerRef(p_local0);
int *p_local1 = &value;
- // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local1'
// CHECK-FIXES: int *p_local1 = &value;
int *const &ref = p_local1;
(void)ref;
IntPtrAlias p_local2 = &value;
- // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local2'
// CHECK-FIXES: IntPtrAlias p_local2 = &value;
takesAliasConstPointerRef(p_local2);
int *p_local3 = &value;
- // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local3'
// CHECK-FIXES: int *p_local3 = &value;
takesConstPointerRRef(static_cast<int *const &&>(p_local3));
IntPtrTypedef p_local4 = &value;
- // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local4'
// CHECK-FIXES: IntPtrTypedef p_local4 = &value;
takesTypedefConstPointerRef(p_local4);
int *p_local5 = &value;
- // CHECK-MESSAGES-NOT: warning: pointee of variable 'p_local5'
// CHECK-FIXES: int *p_local5 = &value;
int *volatile &volatile_ref = p_local5;
(void)volatile_ref;
@@ -175,6 +170,15 @@ void ignore_range_for_pointer_reference_sink() {
std::vector<int *> sink;
// CHECK-FIXES: for (int *element : source)
for (int *element : source)
- // CHECK-MESSAGES-NOT: warning: pointee of variable 'element'
sink.push_back(element);
}
+
+struct UniquePtrData {};
+
+void ignore_unique_ptr_emplace_sink() {
+ /*const*/ UniquePtrData *const newdata = new UniquePtrData;
+ // CHECK-FIXES: /*const*/ UniquePtrData *const newdata = new UniquePtrData;
+
+ std::vector<std::unique_ptr<UniquePtrData>> data;
+ data.emplace_back(newdata);
+}
diff --git a/clang/lib/Analysis/ExprMutationAnalyzer.cpp b/clang/lib/Analysis/ExprMutationAnalyzer.cpp
index 224df4d2e9be2..dd394a8c8d93c 100644
--- a/clang/lib/Analysis/ExprMutationAnalyzer.cpp
+++ b/clang/lib/Analysis/ExprMutationAnalyzer.cpp
@@ -150,12 +150,13 @@ class ExprPointeeResolve {
if (kind == CK_NoOp) {
// Binding `T *` to `T *const &` only adds top-level qualifiers to the
// pointer object, so this `CK_NoOp` still refers to the same pointer.
+ const auto GetLocallyUnqualifiedCanonicalType = [](QualType Type) {
+ return Type.getLocalUnqualifiedType().getCanonicalType();
+ };
const QualType CastType =
- ICE->getType().getLocalUnqualifiedType().getCanonicalType();
- const QualType SubExprType = ICE->getSubExpr()
- ->getType()
- .getLocalUnqualifiedType()
- .getCanonicalType();
+ GetLocallyUnqualifiedCanonicalType(ICE->getType());
+ const QualType SubExprType =
+ GetLocallyUnqualifiedCanonicalType(ICE->getSubExpr()->getType());
if (CastType == SubExprType)
return resolveExpr(ICE->getSubExpr());
}
@@ -238,10 +239,10 @@ const auto nonConstReferenceType = [] {
referenceType(pointee(unless(isConstQualified()))));
};
-const auto referenceToPointerWithNonConstPointeeType = [] {
- return hasUnqualifiedDesugaredType(
- referenceType(pointee(hasUnqualifiedDesugaredType(
- pointerType(pointee(unless(isConstQualified())))))));
+const auto constReferenceToPointerWithNonConstPointeeType = [] {
+ return hasUnqualifiedDesugaredType(referenceType(pointee(qualType(
+ isConstQualified(), hasUnqualifiedDesugaredType(pointerType(
+ pointee(unless(isConstQualified()))))))));
};
const auto nonConstPointerType = [] {
@@ -783,9 +784,9 @@ ExprMutationAnalyzer::Analyzer::findPointeeMemberMutation(const Expr *Exp) {
const Stmt *
ExprMutationAnalyzer::Analyzer::findPointeeToNonConst(const Expr *Exp) {
- const auto NonConstPointerOrNonConstRefOrDependentType = type(
- anyOf(nonConstPointerType(), nonConstReferenceType(),
- referenceToPointerWithNonConstPointeeType(), isDependentType()));
+ const auto NonConstPointerOrNonConstRefOrDependentType = type(anyOf(
+ nonConstPointerType(), nonConstReferenceType(),
+ constReferenceToPointerWithNonConstPointeeType(), isDependentType()));
// assign
const auto InitToNonConst =
diff --git a/clang/unittests/Analysis/ExprMutationAnalyzerTest.cpp b/clang/unittests/Analysis/ExprMutationAnalyzerTest.cpp
index 0304d2f527ca2..142ab27af448f 100644
--- a/clang/unittests/Analysis/ExprMutationAnalyzerTest.cpp
+++ b/clang/unittests/Analysis/ExprMutationAnalyzerTest.cpp
@@ -1764,6 +1764,14 @@ TEST(ExprMutationAnalyzerTest, PointeeMutatedByInitToNonConst) {
match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
EXPECT_TRUE(isPointeeMutated(Results, AST.get()));
}
+ {
+ const std::string Code =
+ "void f() { const int* x = nullptr; const int* const& b = x; }";
+ auto AST = buildASTFromCodeWithArgs(Code, {});
+ auto Results =
+ match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
+ EXPECT_FALSE(isPointeeMutated(Results, AST.get()));
+ }
}
TEST(ExprMutationAnalyzerTest, PointeeMutatedByAssignToNonConst) {
@@ -1834,6 +1842,22 @@ TEST(ExprMutationAnalyzerTest, PointeeMutatedByPassAsArgument) {
match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
EXPECT_TRUE(isPointeeMutated(Results, AST.get()));
}
+ {
+ const std::string Code = "typedef int *IntPtr; void b(IntPtr const &); "
+ "void f() { int* x = nullptr; b(x); }";
+ auto AST = buildASTFromCodeWithArgs(Code, {});
+ auto Results =
+ match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
+ EXPECT_TRUE(isPointeeMutated(Results, AST.get()));
+ }
+ {
+ const std::string Code = "template<class T> void b(T* const&); "
+ "void f() { int* x = nullptr; b(x); }";
+ auto AST = buildASTFromCodeWithArgs(Code, {});
+ auto Results =
+ match(withEnclosingCompound(declRefTo("x")), AST->getASTContext());
+ EXPECT_TRUE(isPointeeMutated(Results, AST.get()));
+ }
{
const std::string Code =
"namespace std { typedef decltype(sizeof(int)) size_t; }"
More information about the cfe-commits
mailing list