[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