[clang] [Clang][Sema] Diagnose invalid 'pure'/'const' attributes on writes (PR #217817)
via cfe-commits
cfe-commits at lists.llvm.org
Fri Aug 21 06:29:13 PDT 2026
https://github.com/AhmedKamel10 updated https://github.com/llvm/llvm-project/pull/217817
>From 3539ddfa3e677c39664a385e06b18b5e2dd4d480 Mon Sep 17 00:00:00 2001
From: ahmedkamel10 <amkzaher at gmail.com>
Date: Fri, 21 Aug 2026 08:07:04 +0300
Subject: [PATCH 1/2] [Clang][Sema] Diagnose invalid 'pure'/'const' attributes
on writes
Add -Winvalid-pure-attribute, which flags common violations of the
GCC-documented 'pure'/'const' attribute contract: direct writes
through a pointer or reference parameter, and writes to global or
static-local variables. This is a syntactic heuristic over the
function's own body, not a soundness proof (interprocedural analysis
for this is undecidable in general).
Fixes #215106
---
clang/include/clang/Basic/DiagnosticGroups.td | 1 +
.../clang/Basic/DiagnosticSemaKinds.td | 13 ++++
clang/lib/Sema/AnalysisBasedWarnings.cpp | 70 +++++++++++++++++++
clang/test/Sema/attr-const-pure.c | 66 +++++++++++++----
4 files changed, 137 insertions(+), 13 deletions(-)
diff --git a/clang/include/clang/Basic/DiagnosticGroups.td b/clang/include/clang/Basic/DiagnosticGroups.td
index 9ee0b61a96a32..5742be7a9e143 100644
--- a/clang/include/clang/Basic/DiagnosticGroups.td
+++ b/clang/include/clang/Basic/DiagnosticGroups.td
@@ -756,6 +756,7 @@ def ExpansionToDefined : DiagGroup<"expansion-to-defined">;
def FlagEnum : DiagGroup<"flag-enum">;
def IncrementBool : DiagGroup<"increment-bool", [DeprecatedIncrementBool]>;
def InfiniteRecursion : DiagGroup<"infinite-recursion">;
+def InvalidPureAttribute : DiagGroup<"invalid-pure-attribute">;
def PureVirtualCallFromCtorDtor: DiagGroup<"call-to-pure-virtual-from-ctor-dtor">;
def GNUImaginaryConstant : DiagGroup<"gnu-imaginary-constant">;
def IgnoredGCH : DiagGroup<"ignored-gch">;
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 22e65d3205808..ee31fe72a81b0 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -811,6 +811,19 @@ def warn_const_attr_with_pure_attr : Warning<
def warn_pure_function_returns_void : Warning<
"'%select{pure|const}0' attribute on function returning 'void'; attribute ignored">,
InGroup<IgnoredAttributes>;
+
+def warn_pure_function_writes_argument : Warning<
+ "function declared %select{'pure'|'const'}0 stores through "
+ "%select{pointer|reference}1 parameter %2; this violates the attribute's "
+ "contract and can cause miscompilation under optimization">,
+ InGroup<InvalidPureAttribute>;
+def warn_pure_function_writes_global : Warning<
+ "function declared %select{'pure'|'const'}0 stores to "
+ "%select{global|static local}1 variable %2; this violates the attribute's "
+ "contract and can cause miscompilation under optimization">,
+ InGroup<InvalidPureAttribute>;
+def note_pure_function_declared_here : Note<
+ "function declared %select{'pure'|'const'}0 here">;
def warn_suggest_noreturn_function : Warning<
"%select{function|method}0 %1 could be declared with attribute 'noreturn'">,
diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp
index d0500a6defd64..9d2e2fdd61729 100644
--- a/clang/lib/Sema/AnalysisBasedWarnings.cpp
+++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp
@@ -284,6 +284,76 @@ static bool checkForRecursiveFunctionCall(const FunctionDecl *FD, CFG *cfg) {
return foundRecursion;
}
+namespace {
+/// Walks a 'pure'/'const' function body looking for stores through pointer
+/// or reference parameters, and stores to global or static-local variables.
+/// This is a syntactic heuristic, not a proof: it only catches direct writes
+/// reachable via straightforward lvalue expressions in the function's own
+/// body. It intentionally does not attempt interprocedural analysis (that's
+/// undecidable in general), locally-allocated memory escaping, or writes
+/// mediated through a level of indirection the visitor doesn't unwrap.
+class PureConstWriteChecker : public DynamicRecursiveASTVisitor {
+public:
+ PureConstWriteChecker(Sema &sema, const FunctionDecl *functionDecl,
+ bool isConstAttr)
+ : S(sema), FD(functionDecl), IsConstAttr(isConstAttr) {}
+
+ bool VisitBinaryOperator(BinaryOperator *BO) override {
+ if (BO->isAssignmentOp() || BO->isCompoundAssignmentOp())
+ checkLvalue(BO->getLHS());
+
+ return true;
+ }
+
+private:
+ Sema &S;
+ const FunctionDecl *FD;
+ bool IsConstAttr;
+
+ void checkLvalue(const Expr *E) {
+ E = E->IgnoreParenImpCasts();
+ if (const auto *UO = dyn_cast<UnaryOperator>(E)) {
+ if (UO->getOpcode() == UO_Deref) {
+ checkPointerBase(UO->getSubExpr());
+ return;
+ }
+ }
+ if (const auto *DRE = dyn_cast<DeclRefExpr>(E)) {
+ if (const auto *VD = dyn_cast<VarDecl>(DRE->getDecl())) {
+ if (const auto *PVD = dyn_cast<ParmVarDecl>(VD)) {
+ if (PVD->getType()->isReferenceType() &&
+ !PVD->getType()->getPointeeType().isConstQualified()) {
+
+ S.Diag(DRE->getLocation(), diag::warn_pure_function_writes_argument)
+ << IsConstAttr << true << PVD;
+ S.Diag(FD->getLocation(), diag::note_pure_function_declared_here)
+ << IsConstAttr;
+ }
+ } else if (VD->hasGlobalStorage()) {
+ S.Diag(DRE->getLocation(), diag::warn_pure_function_writes_global)
+ << IsConstAttr << VD->isStaticLocal() << VD;
+ S.Diag(FD->getLocation(), diag::note_pure_function_declared_here)
+ << IsConstAttr;
+ }
+ }
+ }
+ }
+
+ void checkPointerBase(const Expr *Base) {
+ Base = Base->IgnoreParenCasts();
+ const auto *DRE = dyn_cast<DeclRefExpr>(Base);
+ if (!DRE)
+ return;
+ const auto *PVD = dyn_cast<ParmVarDecl>(DRE->getDecl());
+ if (!PVD)
+ return;
+ S.Diag(DRE->getLocation(), diag::warn_pure_function_writes_argument)
+ << IsConstAttr << false << PVD;
+ S.Diag(FD->getLocation(), diag::note_pure_function_declared_here)
+ << IsConstAttr;
+ }
+};
+} // namespace
static void checkRecursiveFunction(Sema &S, const FunctionDecl *FD,
const Stmt *Body, AnalysisDeclContext &AC) {
FD = FD->getCanonicalDecl();
diff --git a/clang/test/Sema/attr-const-pure.c b/clang/test/Sema/attr-const-pure.c
index 43e22eb34014d..ca8b057b30d0f 100644
--- a/clang/test/Sema/attr-const-pure.c
+++ b/clang/test/Sema/attr-const-pure.c
@@ -1,5 +1,5 @@
-// RUN: %clang_cc1 -fsyntax-only -verify %s
-// RUN: %clang_cc1 -fsyntax-only -verify -x c++ %s
+// RUN: %clang_cc1 -fsyntax-only -verify -Winvalid-pure-attribute %s
+// RUN: %clang_cc1 -fsyntax-only -verify -Winvalid-pure-attribute -x c++ %s
// The attributes apply to function declarations, nothing else.
__attribute__((const)) int func1(void);
@@ -10,16 +10,14 @@ __attribute__((pure)) int func2(void);
#ifdef __cplusplus
struct CppTest {
- // They are fine on member functions.
- __attribute__((const)) int func();
+// They are fine on member functions.
+__attribute__((const)) int func();
[[gnu::pure]] int other_func();
-
- int another();
-
- // Constructors and destructors are not allowed though because they
- // notionally return void.
+int another();
+// Constructors and destructors are not allowed though because they
+// notionally return void.
[[__gnu__::__const__]] CppTest(); // expected-warning {{'const' attribute on function returning 'void'; attribute ignored}}
- __attribute__((pure)) ~CppTest(); // expected-warning {{'pure' attribute on function returning 'void'; attribute ignored}}
+__attribute__((pure)) ~CppTest(); // expected-warning {{'pure' attribute on function returning 'void'; attribute ignored}}
};
// Including out-of-line member functions.
@@ -40,11 +38,11 @@ int (*fp1)(void) [[gnu::const]]; // expected-warning {{attribute 'gnu::const' ig
int (*fp2)(void) [[gnu::pure]]; // expected-warning {{attribute 'gnu::pure' ignored, because it cannot be applied to a type}}
struct __attribute__((const)) S1 { // expected-warning {{'const' attribute only applies to functions}}
- int x;
+int x;
};
struct __attribute__((pure)) S2 { // expected-warning {{'pure' attribute only applies to functions}}
- int x;
+int x;
};
// Or variables, etc.
@@ -61,6 +59,48 @@ __attribute__((const, pure)) int func7(void); // expected-warning {{'const' attr
// FIXME: this should also be diagnosed the same as func7.
__attribute__((pure)) int func8(void);
[[gnu::const]] int func8(void) {
- return 12;
+return 12;
}
+// Diagnosing invalid 'pure'/'const' attributes: writes through a pointer or
+// reference parameter, or to a global/static variable, violate the
+// attribute's no-visible-side-effects contract.
+
+int lookup(int key, int *value) __attribute__((__pure__));
+int lookup(int key, int *value) { // expected-note {{function declared 'pure' here}}
+ if (key == 42) {
+ *value = 47; // expected-warning {{function declared 'pure' stores through pointer parameter 'value'}}
+ return 0;
+ }
+ return -1;
+}
+
+static int global_cache;
+__attribute__((pure)) int writes_global(int x) { // expected-note {{function declared 'pure' here}}
+ global_cache = x; // expected-warning {{function declared 'pure' stores to global variable 'global_cache'}}
+ return global_cache;
+}
+
+__attribute__((pure)) int writes_static_local(int x) { // expected-note {{function declared 'pure' here}}
+ static int cache = 0;
+ cache += x; // expected-warning {{function declared 'pure' stores to static local variable 'cache'}}
+ return cache;
+}
+
+// No warning: local, non-static variable.
+__attribute__((pure)) int local_only(int x) {
+ int tmp = x * 2;
+ return tmp;
+}
+
+// No warning: read-only dereference of a const pointer.
+__attribute__((pure)) int reads_ptr_arg(const int *value) {
+ return *value;
+}
+
+#ifdef __cplusplus
+__attribute__((pure)) int writes_ref_arg(int &out) { // expected-note {{function declared 'pure' here}}
+ out = 1; // expected-warning {{function declared 'pure' stores through reference parameter 'out'}}
+ return 0;
+}
+#endif
\ No newline at end of file
>From 4018b85d45e8a28e820e7b76020448077f3e81cd Mon Sep 17 00:00:00 2001
From: ahmedkamel10 <amkzaher at gmail.com>
Date: Fri, 21 Aug 2026 16:28:30 +0300
Subject: [PATCH 2/2] Refactoring checkLvalue and bug fixing
---
clang/lib/Sema/AnalysisBasedWarnings.cpp | 62 ++++++++++++++++--------
clang/test/Sema/attr-const-pure.c | 2 +-
2 files changed, 43 insertions(+), 21 deletions(-)
diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp
index 9d2e2fdd61729..c0820190ec819 100644
--- a/clang/lib/Sema/AnalysisBasedWarnings.cpp
+++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp
@@ -49,7 +49,7 @@
#include "clang/Sema/ScopeInfo.h"
#include "clang/Sema/SemaInternal.h"
#include "llvm/ADT/ArrayRef.h"
-#include "llvm/ADT/BitVector.h"
+#include "llvm/ADT/BitVector.h"// Check for throw out of non-throwing function.
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/MapVector.h"
#include "llvm/ADT/PostOrderIterator.h"
@@ -312,30 +312,36 @@ class PureConstWriteChecker : public DynamicRecursiveASTVisitor {
void checkLvalue(const Expr *E) {
E = E->IgnoreParenImpCasts();
+
if (const auto *UO = dyn_cast<UnaryOperator>(E)) {
- if (UO->getOpcode() == UO_Deref) {
+ if (UO->getOpcode() == UO_Deref)
checkPointerBase(UO->getSubExpr());
- return;
- }
+ return;
}
- if (const auto *DRE = dyn_cast<DeclRefExpr>(E)) {
- if (const auto *VD = dyn_cast<VarDecl>(DRE->getDecl())) {
- if (const auto *PVD = dyn_cast<ParmVarDecl>(VD)) {
- if (PVD->getType()->isReferenceType() &&
- !PVD->getType()->getPointeeType().isConstQualified()) {
- S.Diag(DRE->getLocation(), diag::warn_pure_function_writes_argument)
- << IsConstAttr << true << PVD;
- S.Diag(FD->getLocation(), diag::note_pure_function_declared_here)
- << IsConstAttr;
- }
- } else if (VD->hasGlobalStorage()) {
- S.Diag(DRE->getLocation(), diag::warn_pure_function_writes_global)
- << IsConstAttr << VD->isStaticLocal() << VD;
- S.Diag(FD->getLocation(), diag::note_pure_function_declared_here)
- << IsConstAttr;
- }
+ const auto *DRE = dyn_cast<DeclRefExpr>(E);
+ if (!DRE)
+ return;
+ const auto *VD = dyn_cast<VarDecl>(DRE->getDecl());
+ if (!VD)
+ return;
+
+ if (const auto *PVD = dyn_cast<ParmVarDecl>(VD)) {
+ if (PVD->getType()->isReferenceType() &&
+ !PVD->getType()->getPointeeType().isConstQualified()) {
+ S.Diag(DRE->getLocation(), diag::warn_pure_function_writes_argument)
+ << IsConstAttr << true << PVD;
+ S.Diag(FD->getLocation(), diag::note_pure_function_declared_here)
+ << IsConstAttr;
}
+ return;
+ }
+
+ if (VD->hasGlobalStorage()) {
+ S.Diag(DRE->getLocation(), diag::warn_pure_function_writes_global)
+ << IsConstAttr << VD->isStaticLocal() << VD;
+ S.Diag(FD->getLocation(), diag::note_pure_function_declared_here)
+ << IsConstAttr;
}
}
@@ -353,6 +359,15 @@ class PureConstWriteChecker : public DynamicRecursiveASTVisitor {
<< IsConstAttr;
}
};
+
+static void checkPureConstFunctionWrites(Sema &S, const FunctionDecl *FD,
+ Stmt *Body) {
+ bool IsConstAttr = FD->hasAttr<ConstAttr>();
+ if (!IsConstAttr && !FD->hasAttr<PureAttr>())
+ return;
+ PureConstWriteChecker Checker(S, FD, IsConstAttr);
+ Checker.TraverseStmt(Body);
+}
} // namespace
static void checkRecursiveFunction(Sema &S, const FunctionDecl *FD,
const Stmt *Body, AnalysisDeclContext &AC) {
@@ -3386,6 +3401,13 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings(
checkRecursiveFunction(S, FD, Body, AC);
}
}
+ if (!Diags.isIgnored(diag::warn_pure_function_writes_argument,
+ D->getBeginLoc()) ||
+ !Diags.isIgnored(diag::warn_pure_function_writes_global,
+ D->getBeginLoc())) {
+ if (const FunctionDecl *FD = dyn_cast<FunctionDecl>(D))
+ checkPureConstFunctionWrites(S, FD, const_cast<Stmt *>(Body));
+ }
// Check for throw out of non-throwing function.
if (!Diags.isIgnored(diag::warn_throw_in_noexcept_func, D->getBeginLoc()))
diff --git a/clang/test/Sema/attr-const-pure.c b/clang/test/Sema/attr-const-pure.c
index ca8b057b30d0f..d9c1be809acc3 100644
--- a/clang/test/Sema/attr-const-pure.c
+++ b/clang/test/Sema/attr-const-pure.c
@@ -103,4 +103,4 @@ __attribute__((pure)) int writes_ref_arg(int &out) { // expected-note {{function
out = 1; // expected-warning {{function declared 'pure' stores through reference parameter 'out'}}
return 0;
}
-#endif
\ No newline at end of file
+#endif
More information about the cfe-commits
mailing list