https://github.com/hjanuschka created 
https://github.com/llvm/llvm-project/pull/182068

Add new clang-tidy check that finds function parameters that are pointers but 
are always dereferenced without null checks, suggesting they should be 
references instead.

Using references for non-nullable parameters makes the contract explicit and 
improves readability.

```cpp
// Before -- pointer that is never null-checked
void process(Foo *P) {
  P->bar();
  P->X = 42;
}

// Suggested -- use a reference
void process(Foo &P) {
  P.bar();
  P.X = 42;
}
```

The check skips parameters that are:
- null-checked (`if(ptr)`, `ptr != nullptr`, etc.)
- passed to other functions as pointers
- used in pointer arithmetic or array subscript
- void pointers or pointers to incomplete types
- parameters of virtual methods
- function pointers
- unnamed or never dereferenced

No fix-its are provided since changing the signature requires updating all call 
sites, which may span translation units.

>From 50bd3088930e916990aae8c02a4a46bb6abc686d Mon Sep 17 00:00:00 2001
From: Helmut Januschka <[email protected]>
Date: Wed, 18 Feb 2026 18:13:54 +0100
Subject: [PATCH] [clang-tidy] Add readability-pointer-to-ref check

Add new clang-tidy check that finds function parameters that are pointers
but are always dereferenced without null checks, suggesting they should be
references instead.

Using references for non-nullable parameters makes the contract explicit
and improves readability.

For example:
```cpp
// Before
void process(Foo *P) {
  P->bar();
  P->X = 42;
}

// Suggested -- use a reference
void process(Foo &P) {
  P.bar();
  P.X = 42;
}
```

The check skips parameters that are:
- null-checked (if(ptr), ptr != nullptr, etc.)
- passed to other functions as pointers
- used in pointer arithmetic or array subscript
- void pointers or pointers to incomplete types
- parameters of virtual methods
- function pointers
- unnamed or never dereferenced

No fix-its are provided since changing the signature requires updating
all call sites, which may span translation units.
---
 .../clang-tidy/readability/CMakeLists.txt     |   1 +
 .../readability/PointerToRefCheck.cpp         | 204 ++++++++++++++++++
 .../readability/PointerToRefCheck.h           |  31 +++
 .../readability/ReadabilityTidyModule.cpp     |   3 +
 clang-tools-extra/docs/ReleaseNotes.rst       |   7 +
 .../docs/clang-tidy/checks/list.rst           |   1 +
 .../checks/readability/pointer-to-ref.rst     |  43 ++++
 .../checkers/readability/pointer-to-ref.cpp   |  86 ++++++++
 8 files changed, 376 insertions(+)
 create mode 100644 
clang-tools-extra/clang-tidy/readability/PointerToRefCheck.cpp
 create mode 100644 clang-tools-extra/clang-tidy/readability/PointerToRefCheck.h
 create mode 100644 
clang-tools-extra/docs/clang-tidy/checks/readability/pointer-to-ref.rst
 create mode 100644 
clang-tools-extra/test/clang-tidy/checkers/readability/pointer-to-ref.cpp

diff --git a/clang-tools-extra/clang-tidy/readability/CMakeLists.txt 
b/clang-tools-extra/clang-tidy/readability/CMakeLists.txt
index f1f3cde32feff..8f591ada34ed9 100644
--- a/clang-tools-extra/clang-tidy/readability/CMakeLists.txt
+++ b/clang-tools-extra/clang-tidy/readability/CMakeLists.txt
@@ -37,6 +37,7 @@ add_clang_library(clangTidyReadabilityModule STATIC
   NamespaceCommentCheck.cpp
   NonConstParameterCheck.cpp
   OperatorsRepresentationCheck.cpp
+  PointerToRefCheck.cpp
   QualifiedAutoCheck.cpp
   ReadabilityTidyModule.cpp
   RedundantAccessSpecifiersCheck.cpp
diff --git a/clang-tools-extra/clang-tidy/readability/PointerToRefCheck.cpp 
b/clang-tools-extra/clang-tidy/readability/PointerToRefCheck.cpp
new file mode 100644
index 0000000000000..12c03f3067681
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/readability/PointerToRefCheck.cpp
@@ -0,0 +1,204 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "PointerToRefCheck.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang::tidy::readability {
+
+namespace {
+
+/// Visitor that classifies how a pointer parameter is used in a function body.
+/// It detects:
+///   - Dereferences (operator*, operator->)
+///   - Null checks (if(ptr), ptr != nullptr, assert(ptr), etc.)
+///   - Pointer arithmetic
+///   - Being passed to other functions as a pointer
+///   - Address-of or array subscript usage
+class PointerUsageVisitor
+    : public RecursiveASTVisitor<PointerUsageVisitor> {
+public:
+  explicit PointerUsageVisitor(const ParmVarDecl *Param) : Param(Param) {}
+
+  bool isDereferenced() const { return Dereferenced; }
+  bool isNullChecked() const { return NullChecked; }
+  bool isUsedAsPointer() const { return UsedAsPointer; }
+
+  bool VisitUnaryOperator(const UnaryOperator *UO) {
+    if (UO->getOpcode() == UO_Deref && refersToParam(UO->getSubExpr()))
+      Dereferenced = true;
+    return true;
+  }
+
+  bool VisitMemberExpr(const MemberExpr *ME) {
+    if (ME->isArrow() && refersToParam(ME->getBase()))
+      Dereferenced = true;
+    return true;
+  }
+
+  // Detect null checks: if(ptr), ptr == nullptr, !ptr, etc.
+  bool VisitImplicitCastExpr(const ImplicitCastExpr *ICE) {
+    if (ICE->getCastKind() == CK_PointerToBoolean &&
+        refersToParam(ICE->getSubExpr()))
+      NullChecked = true;
+    return true;
+  }
+
+  bool VisitBinaryOperator(const BinaryOperator *BO) {
+    const Expr *LHS = BO->getLHS()->IgnoreImplicit();
+    const Expr *RHS = BO->getRHS()->IgnoreImplicit();
+
+    // ptr == nullptr, nullptr == ptr, ptr != nullptr, etc.
+    if (BO->isEqualityOp()) {
+      if ((refersToParam(LHS) && isNullExpr(RHS)) ||
+          (refersToParam(RHS) && isNullExpr(LHS)))
+        NullChecked = true;
+    }
+
+    // Pointer comparison (relational).
+    if (BO->isRelationalOp() &&
+        (refersToParam(LHS) || refersToParam(RHS)))
+      UsedAsPointer = true;
+
+    // Pointer arithmetic: ptr + n, ptr - n.
+    if ((BO->getOpcode() == BO_Add || BO->getOpcode() == BO_Sub) &&
+        (refersToParam(LHS) || refersToParam(RHS)))
+      UsedAsPointer = true;
+
+    // Reassignment: ptr = something.
+    if (BO->getOpcode() == BO_Assign && refersToParam(LHS))
+      UsedAsPointer = true;
+
+    return true;
+  }
+
+  // Detect being passed to a function expecting a pointer.
+  bool VisitCallExpr(const CallExpr *CE) {
+    for (unsigned I = 0; I < CE->getNumArgs(); ++I) {
+      if (refersToParam(CE->getArg(I)->IgnoreImplicit()))
+        UsedAsPointer = true;
+    }
+    return true;
+  }
+
+  // Detect array subscript: ptr[i]
+  bool VisitArraySubscriptExpr(const ArraySubscriptExpr *ASE) {
+    if (refersToParam(ASE->getBase()->IgnoreImplicit()))
+      UsedAsPointer = true;
+    return true;
+  }
+
+  // Detect address-taken or assigned to another pointer.
+  bool VisitDeclRefExpr(const DeclRefExpr *DRE) {
+    // We handle specific patterns above; just track raw usage for
+    // cases we might miss (e.g. storing to another variable).
+    return true;
+  }
+
+private:
+  bool refersToParam(const Expr *E) const {
+    if (!E)
+      return false;
+    const auto *DRE = dyn_cast<DeclRefExpr>(E->IgnoreImplicit());
+    return DRE && DRE->getDecl() == Param;
+  }
+
+  static bool isNullExpr(const Expr *E) {
+    if (!E)
+      return false;
+    E = E->IgnoreImplicit();
+    if (isa<GNUNullExpr>(E))
+      return true;
+    if (const auto *IL = dyn_cast<IntegerLiteral>(E))
+      return IL->getValue() == 0;
+    if (isa<CXXNullPtrLiteralExpr>(E))
+      return true;
+    return false;
+  }
+
+  const ParmVarDecl *Param;
+  bool Dereferenced = false;
+  bool NullChecked = false;
+  bool UsedAsPointer = false;
+};
+
+} // namespace
+
+void PointerToRefCheck::registerMatchers(MatchFinder *Finder) {
+  // Match function definitions with at least one pointer parameter.
+  // Match each pointer parameter individually.
+  Finder->addMatcher(
+      parmVarDecl(
+          hasType(pointerType()),
+          hasAncestor(functionDecl(isDefinition(),
+                                   unless(isImplicit()),
+                                   unless(isDeleted()))
+                          .bind("func")))
+          .bind("param"),
+      this);
+}
+
+void PointerToRefCheck::check(const MatchFinder::MatchResult &Result) {
+  const auto *Func = Result.Nodes.getNodeAs<FunctionDecl>("func");
+  const auto *Param = Result.Nodes.getNodeAs<ParmVarDecl>("param");
+  if (!Func || !Param || !Func->hasBody())
+    return;
+
+  // Skip system headers.
+  if (Result.SourceManager->isInSystemHeader(Param->getLocation()))
+    return;
+
+  // Skip variadic functions, main, and operator overloads.
+  if (Func->isVariadic() || Func->isMain())
+    return;
+  if (isa<CXXMethodDecl>(Func) &&
+      cast<CXXMethodDecl>(Func)->isOverloadedOperator())
+    return;
+
+  // Skip if the parameter is unnamed (can't be used).
+  if (!Param->getIdentifier())
+    return;
+
+  // Skip void pointers (too generic).
+  const auto *PT = Param->getType()->getAs<PointerType>();
+  if (!PT || PT->getPointeeType()->isVoidType())
+    return;
+
+  // Skip pointers to incomplete types.
+  if (PT->getPointeeType()->isIncompleteType())
+    return;
+
+  // Skip virtual methods (changing signature breaks polymorphism).
+  if (const auto *MD = dyn_cast<CXXMethodDecl>(Func))
+    if (MD->isVirtual())
+      return;
+
+  // Skip callbacks and function pointers (typedef params).
+  if (Param->getType()->isFunctionPointerType())
+    return;
+
+  // Analyze usage in the body.
+  PointerUsageVisitor Visitor(Param);
+  Visitor.TraverseStmt(const_cast<Stmt *>(Func->getBody()));
+
+  // Only flag if the pointer is dereferenced but never null-checked
+  // and not used as a raw pointer (arithmetic, passed to functions, etc.).
+  if (Visitor.isDereferenced() && !Visitor.isNullChecked() &&
+      !Visitor.isUsedAsPointer()) {
+    diag(Param->getLocation(),
+         "pointer parameter '%0' can be a reference; it is always "
+         "dereferenced but never checked for null")
+        << Param->getName();
+  }
+}
+
+} // namespace clang::tidy::readability
diff --git a/clang-tools-extra/clang-tidy/readability/PointerToRefCheck.h 
b/clang-tools-extra/clang-tidy/readability/PointerToRefCheck.h
new file mode 100644
index 0000000000000..cc6da410f507f
--- /dev/null
+++ b/clang-tools-extra/clang-tidy/readability/PointerToRefCheck.h
@@ -0,0 +1,31 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_READABILITY_POINTERTOREFCHECK_H
+#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_POINTERTOREFCHECK_H
+
+#include "../ClangTidyCheck.h"
+
+namespace clang::tidy::readability {
+
+/// Finds function parameters that are pointers but are always dereferenced
+/// without null checks, suggesting they should be references instead.
+///
+/// For the user-facing documentation see:
+/// 
https://clang.llvm.org/extra/clang-tidy/checks/readability/pointer-to-ref.html
+class PointerToRefCheck : public ClangTidyCheck {
+public:
+  PointerToRefCheck(StringRef Name, ClangTidyContext *Context)
+      : ClangTidyCheck(Name, Context) {}
+  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+};
+
+} // namespace clang::tidy::readability
+
+#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_READABILITY_POINTERTOREFCHECK_H
diff --git a/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp 
b/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp
index c582dc98eac6b..c0dff0822e4fc 100644
--- a/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp
+++ b/clang-tools-extra/clang-tidy/readability/ReadabilityTidyModule.cpp
@@ -39,6 +39,7 @@
 #include "NamedParameterCheck.h"
 #include "NonConstParameterCheck.h"
 #include "OperatorsRepresentationCheck.h"
+#include "PointerToRefCheck.h"
 #include "QualifiedAutoCheck.h"
 #include "RedundantAccessSpecifiersCheck.h"
 #include "RedundantCastingCheck.h"
@@ -134,6 +135,8 @@ class ReadabilityModule : public ClangTidyModule {
         "readability-misplaced-array-index");
     CheckFactories.registerCheck<OperatorsRepresentationCheck>(
         "readability-operators-representation");
+    CheckFactories.registerCheck<PointerToRefCheck>(
+        "readability-pointer-to-ref");
     CheckFactories.registerCheck<QualifiedAutoCheck>(
         "readability-qualified-auto");
     CheckFactories.registerCheck<RedundantAccessSpecifiersCheck>(
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst 
b/clang-tools-extra/docs/ReleaseNotes.rst
index 68bab88146241..78f482aa34f61 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -139,6 +139,13 @@ New checks
   Finds and removes redundant conversions from 
``std::[w|u8|u16|u32]string_view`` to
   ``std::[...]string`` in call expressions expecting ``std::[...]string_view``.
 
+- New :doc:`readability-pointer-to-ref
+  <clang-tidy/checks/readability/pointer-to-ref>` check.
+
+  Finds function parameters that are pointers but are always
+  dereferenced without null checks, suggesting they should be
+  references instead.
+
 - New :doc:`readability-trailing-comma
   <clang-tidy/checks/readability/trailing-comma>` 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..7d01e5f0343ff 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/list.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst
@@ -405,6 +405,7 @@ Clang-Tidy Checks
    :doc:`readability-named-parameter <readability/named-parameter>`, "Yes"
    :doc:`readability-non-const-parameter <readability/non-const-parameter>`, 
"Yes"
    :doc:`readability-operators-representation 
<readability/operators-representation>`, "Yes"
+   :doc:`readability-pointer-to-ref <readability/pointer-to-ref>`,
    :doc:`readability-qualified-auto <readability/qualified-auto>`, "Yes"
    :doc:`readability-redundant-access-specifiers 
<readability/redundant-access-specifiers>`, "Yes"
    :doc:`readability-redundant-casting <readability/redundant-casting>`, "Yes"
diff --git 
a/clang-tools-extra/docs/clang-tidy/checks/readability/pointer-to-ref.rst 
b/clang-tools-extra/docs/clang-tidy/checks/readability/pointer-to-ref.rst
new file mode 100644
index 0000000000000..aa6fd9589ac12
--- /dev/null
+++ b/clang-tools-extra/docs/clang-tidy/checks/readability/pointer-to-ref.rst
@@ -0,0 +1,43 @@
+.. title:: clang-tidy - readability-pointer-to-ref
+
+readability-pointer-to-ref
+==========================
+
+Finds function parameters that are pointers but are always
+dereferenced without null checks, suggesting they should be
+references instead.
+
+Using references instead of pointers for non-nullable parameters
+makes the contract explicit: the caller must provide a valid object.
+This improves readability and can prevent null pointer bugs.
+
+.. code-block:: c++
+
+  // Before -- pointer that is never null-checked
+  void process(Foo *P) {
+    P->bar();
+    P->X = 42;
+  }
+
+  // After -- use a reference instead
+  void process(Foo &P) {
+    P.bar();
+    P.X = 42;
+  }
+
+The check will not flag a parameter if it:
+
+- is checked for null (``if (P)``, ``P != nullptr``, etc.),
+- is passed to another function as a pointer argument,
+- is used in pointer arithmetic or array subscript,
+- is a ``void`` pointer or pointer to an incomplete type,
+- is a parameter of a virtual method,
+- is a function pointer,
+- is unnamed, or
+- is never dereferenced in the function body.
+
+.. note::
+
+   This check does not provide fix-its because changing a
+   parameter from pointer to reference requires updating all
+   call sites, which may span multiple translation units.
diff --git 
a/clang-tools-extra/test/clang-tidy/checkers/readability/pointer-to-ref.cpp 
b/clang-tools-extra/test/clang-tidy/checkers/readability/pointer-to-ref.cpp
new file mode 100644
index 0000000000000..dd5604ec8c6eb
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/readability/pointer-to-ref.cpp
@@ -0,0 +1,86 @@
+// RUN: %check_clang_tidy %s readability-pointer-to-ref %t
+
+struct Foo {
+  int X;
+  void bar();
+};
+
+// Positive: always dereferenced, never null-checked.
+void derefArrow(Foo *P) {
+// CHECK-MESSAGES: :[[@LINE-1]]:22: warning: pointer parameter 'P' can be a 
reference
+  P->bar();
+  P->X = 42;
+}
+
+// Positive: dereference via unary operator.
+int derefStar(int *P) {
+// CHECK-MESSAGES: :[[@LINE-1]]:20: warning: pointer parameter 'P' can be a 
reference
+  return *P + 1;
+}
+
+// Positive: multiple parameters, only the dereferenced one flagged.
+void twoParams(int *A, int *B) {
+// CHECK-MESSAGES: :[[@LINE-1]]:21: warning: pointer parameter 'A' can be a 
reference
+  *A = 10;
+  if (B)
+    *B = 20;
+}
+
+// Negative: null-checked before use.
+void nullChecked(Foo *P) {
+  if (P)
+    P->bar();
+}
+
+// Negative: compared to nullptr.
+void comparedToNull(Foo *P) {
+  if (P != nullptr)
+    P->bar();
+}
+
+// Negative: passed to another function (used as raw pointer).
+void takePtr(Foo *);
+void passedAlong(Foo *P) {
+  P->bar();
+  takePtr(P);
+}
+
+// Negative: used as array (subscript).
+void asArray(int *P) {
+  P[0] = 1;
+  P[1] = 2;
+}
+
+// Negative: void pointer (too generic).
+void voidPtr(void *P) {
+}
+
+// Negative: unnamed parameter.
+void unnamed(int *) {
+}
+
+// Negative: no dereference at all.
+void noDereference(int *P) {
+}
+
+// Negative: parameter reassigned.
+void reassigned(int *P) {
+  int X = 0;
+  P = &X;
+  *P = 42;
+}
+
+// Negative: virtual method (signature must match base).
+struct Base {
+  virtual void vmethod(int *P);
+};
+
+// Negative: function pointer parameter.
+void funcPtr(void (*P)(int)) {
+  P(42);
+}
+
+// Negative: pointer arithmetic.
+void arithmetic(int *P) {
+  *(P + 1) = 0;
+}

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to