Skip to content

Commit fd11cf4

Browse files
authored
[clang] Extend -Wunused-but-set-variable to static globals (#178342)
This PR extends the capability of -Wunused-but-set-variable to track and diagnose static global variables that are assigned values within a function but whose values are never used. This change complements -Wunused-variable, which detects static globals that are neither set nor used. I created this change with the help of claude for some initial guidance. Fixes #148361
1 parent a2c6b34 commit fd11cf4

File tree

12 files changed

+393
-22
lines changed

12 files changed

+393
-22
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ Attribute Changes in Clang
230230

231231
Improvements to Clang's diagnostics
232232
-----------------------------------
233+
- ``-Wunused-but-set-variable`` now diagnoses file-scope variables with
234+
internal linkage (``static`` storage class) that are assigned but never used. (#GH148361)
235+
233236
- Added ``-Wlifetime-safety`` to enable lifetime safety analysis,
234237
a CFG-based intra-procedural analysis that detects use-after-free and related
235238
temporal safety bugs. See the

clang/include/clang/AST/Decl.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1212,6 +1212,21 @@ class VarDecl : public DeclaratorDecl, public Redeclarable<VarDecl> {
12121212
&& !isFileVarDecl();
12131213
}
12141214

1215+
/// Returns true if this is a file-scope variable with internal linkage.
1216+
bool isInternalLinkageFileVar() const {
1217+
// Calling isExternallyVisible() can trigger linkage computation/caching,
1218+
// which may produce stale results when a decl's DeclContext changes after
1219+
// creation (e.g., OpenMP declare mapper variables), so here we determine
1220+
// it syntactically instead.
1221+
if (!isFileVarDecl())
1222+
return false;
1223+
// Linkage is determined by enclosing class/namespace for static data
1224+
// members.
1225+
if (getStorageClass() == SC_Static && !isStaticDataMember())
1226+
return true;
1227+
return isInAnonymousNamespace();
1228+
}
1229+
12151230
/// Returns true if a variable has extern or __private_extern__
12161231
/// storage.
12171232
bool hasExternalStorage() const {

clang/include/clang/Sema/Sema.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,10 @@ class Sema final : public SemaBase {
973973
/// unit because its type has no linkage and it's not extern "C".
974974
bool isExternalWithNoLinkageType(const ValueDecl *VD) const;
975975

976+
/// Determines whether the given source location is in the main file
977+
/// and we're in a context where we should warn about unused entities.
978+
bool isMainFileLoc(SourceLocation Loc) const;
979+
976980
/// Obtain a sorted list of functions that are undefined but ODR-used.
977981
void getUndefinedButUsed(
978982
SmallVectorImpl<std::pair<NamedDecl *, SourceLocation>> &Undefined);
@@ -7009,6 +7013,9 @@ class Sema final : public SemaBase {
70097013
/// Increment when we find a reference; decrement when we find an ignored
70107014
/// assignment. Ultimately the value is 0 if every reference is an ignored
70117015
/// assignment.
7016+
///
7017+
/// Uses canonical VarDecl as key so in-class decls and out-of-class defs of
7018+
/// static data members get tracked as a single entry.
70127019
llvm::DenseMap<const VarDecl *, int> RefsMinusAssignments;
70137020

70147021
/// Used to control the generation of ExprWithCleanups.

clang/lib/Sema/Sema.cpp

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,12 @@ bool Sema::isExternalWithNoLinkageType(const ValueDecl *VD) const {
956956
!isFunctionOrVarDeclExternC(VD);
957957
}
958958

959+
bool Sema::isMainFileLoc(SourceLocation Loc) const {
960+
if (TUKind != TU_Complete || getLangOpts().IsHeaderFile)
961+
return false;
962+
return SourceMgr.isInMainFile(Loc);
963+
}
964+
959965
/// Obtains a sorted list of functions and variables that are undefined but
960966
/// ODR-used.
961967
void Sema::getUndefinedButUsed(
@@ -1604,6 +1610,40 @@ void Sema::ActOnEndOfTranslationUnit() {
16041610
emitAndClearUnusedLocalTypedefWarnings();
16051611
}
16061612

1613+
if (!Diags.isIgnored(diag::warn_unused_but_set_variable, SourceLocation())) {
1614+
// Diagnose unused-but-set static globals in a deterministic order.
1615+
// Not tracking shadowing info for static globals; there's nothing to
1616+
// shadow.
1617+
struct LocAndDiag {
1618+
SourceLocation Loc;
1619+
PartialDiagnostic PD;
1620+
};
1621+
SmallVector<LocAndDiag, 16> DeclDiags;
1622+
auto addDiag = [&DeclDiags](SourceLocation Loc, PartialDiagnostic PD) {
1623+
DeclDiags.push_back(LocAndDiag{Loc, std::move(PD)});
1624+
};
1625+
1626+
// For -Wunused-but-set-variable we only care about variables that were
1627+
// referenced by the TU end.
1628+
for (const auto &Ref : RefsMinusAssignments) {
1629+
const VarDecl *VD = Ref.first;
1630+
// Only diagnose internal linkage file vars defined in the main file to
1631+
// match -Wunused-variable behavior and avoid false positives from
1632+
// headers.
1633+
if (VD->isInternalLinkageFileVar() && isMainFileLoc(VD->getLocation()))
1634+
DiagnoseUnusedButSetDecl(VD, addDiag);
1635+
}
1636+
1637+
llvm::sort(DeclDiags,
1638+
[](const LocAndDiag &LHS, const LocAndDiag &RHS) -> bool {
1639+
// Sorting purely for determinism; matches behavior in
1640+
// Sema::ActOnPopScope.
1641+
return LHS.Loc < RHS.Loc;
1642+
});
1643+
for (const LocAndDiag &D : DeclDiags)
1644+
Diag(D.Loc, D.PD);
1645+
}
1646+
16071647
if (!Diags.isIgnored(diag::warn_unused_private_field, SourceLocation())) {
16081648
// FIXME: Load additional unused private field candidates from the external
16091649
// source.

clang/lib/Sema/SemaDecl.cpp

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,14 +1912,6 @@ bool Sema::mightHaveNonExternalLinkage(const DeclaratorDecl *D) {
19121912
return !D->isExternallyVisible();
19131913
}
19141914

1915-
// FIXME: This needs to be refactored; some other isInMainFile users want
1916-
// these semantics.
1917-
static bool isMainFileLoc(const Sema &S, SourceLocation Loc) {
1918-
if (S.TUKind != TU_Complete || S.getLangOpts().IsHeaderFile)
1919-
return false;
1920-
return S.SourceMgr.isInMainFile(Loc);
1921-
}
1922-
19231915
bool Sema::ShouldWarnIfUnusedFileScopedDecl(const DeclaratorDecl *D) const {
19241916
assert(D);
19251917

@@ -1946,7 +1938,7 @@ bool Sema::ShouldWarnIfUnusedFileScopedDecl(const DeclaratorDecl *D) const {
19461938
return false;
19471939
} else {
19481940
// 'static inline' functions are defined in headers; don't warn.
1949-
if (FD->isInlined() && !isMainFileLoc(*this, FD->getLocation()))
1941+
if (FD->isInlined() && !isMainFileLoc(FD->getLocation()))
19501942
return false;
19511943
}
19521944

@@ -1957,7 +1949,7 @@ bool Sema::ShouldWarnIfUnusedFileScopedDecl(const DeclaratorDecl *D) const {
19571949
// Constants and utility variables are defined in headers with internal
19581950
// linkage; don't warn. (Unlike functions, there isn't a convenient marker
19591951
// like "inline".)
1960-
if (!isMainFileLoc(*this, VD->getLocation()))
1952+
if (!isMainFileLoc(VD->getLocation()))
19611953
return false;
19621954

19631955
if (Context.DeclMustBeEmitted(VD))
@@ -1971,7 +1963,7 @@ bool Sema::ShouldWarnIfUnusedFileScopedDecl(const DeclaratorDecl *D) const {
19711963
VD->getMemberSpecializationInfo() && !VD->isOutOfLine())
19721964
return false;
19731965

1974-
if (VD->isInline() && !isMainFileLoc(*this, VD->getLocation()))
1966+
if (VD->isInline() && !isMainFileLoc(VD->getLocation()))
19751967
return false;
19761968
} else {
19771969
return false;
@@ -2215,6 +2207,11 @@ void Sema::DiagnoseUnusedButSetDecl(const VarDecl *VD,
22152207
return;
22162208
}
22172209

2210+
// Don't warn on volatile file-scope variables. They are visible beyond their
2211+
// declaring function and writes to them could be observable side effects.
2212+
if (VD->getType().isVolatileQualified() && VD->isFileVarDecl())
2213+
return;
2214+
22182215
// Don't warn about __block Objective-C pointer variables, as they might
22192216
// be assigned in the block but not used elsewhere for the purpose of lifetime
22202217
// extension.
@@ -2227,7 +2224,7 @@ void Sema::DiagnoseUnusedButSetDecl(const VarDecl *VD,
22272224
if (VD->hasAttr<ObjCPreciseLifetimeAttr>() && Ty->isObjCObjectPointerType())
22282225
return;
22292226

2230-
auto iter = RefsMinusAssignments.find(VD);
2227+
auto iter = RefsMinusAssignments.find(VD->getCanonicalDecl());
22312228
if (iter == RefsMinusAssignments.end())
22322229
return;
22332230

@@ -2305,9 +2302,11 @@ void Sema::ActOnPopScope(SourceLocation Loc, Scope *S) {
23052302
DiagnoseUnusedDecl(D, addDiag);
23062303
if (const auto *RD = dyn_cast<RecordDecl>(D))
23072304
DiagnoseUnusedNestedTypedefs(RD, addDiag);
2308-
if (VarDecl *VD = dyn_cast<VarDecl>(D)) {
2305+
// Wait until end of TU to diagnose internal linkage file vars.
2306+
if (auto *VD = dyn_cast<VarDecl>(D);
2307+
VD && !VD->isInternalLinkageFileVar()) {
23092308
DiagnoseUnusedButSetDecl(VD, addDiag);
2310-
RefsMinusAssignments.erase(VD);
2309+
RefsMinusAssignments.erase(VD->getCanonicalDecl());
23112310
}
23122311
}
23132312

clang/lib/Sema/SemaExpr.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20477,8 +20477,15 @@ static void DoMarkVarDeclReferenced(
2047720477
bool UsableInConstantExpr =
2047820478
Var->mightBeUsableInConstantExpressions(SemaRef.Context);
2047920479

20480-
if (Var->isLocalVarDeclOrParm() && !Var->hasExternalStorage()) {
20481-
RefsMinusAssignments.insert({Var, 0}).first->getSecond()++;
20480+
// Only track variables with internal linkage or local scope.
20481+
// Use canonical decl so in-class declarations and out-of-class definitions
20482+
// of static data members in anonymous namespaces are tracked as a single
20483+
// entry.
20484+
const VarDecl *CanonVar = Var->getCanonicalDecl();
20485+
if ((CanonVar->isLocalVarDeclOrParm() ||
20486+
CanonVar->isInternalLinkageFileVar()) &&
20487+
!CanonVar->hasExternalStorage()) {
20488+
RefsMinusAssignments.insert({CanonVar, 0}).first->getSecond()++;
2048220489
}
2048320490

2048420491
// C++20 [expr.const]p12:

clang/lib/Sema/SemaExprCXX.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7504,7 +7504,7 @@ static void MaybeDecrementCount(
75047504
if ((IsCompoundAssign || isIncrementDecrementUnaryOp) &&
75057505
VD->getType().isVolatileQualified())
75067506
return;
7507-
auto iter = RefsMinusAssignments.find(VD);
7507+
auto iter = RefsMinusAssignments.find(VD->getCanonicalDecl());
75087508
if (iter == RefsMinusAssignments.end())
75097509
return;
75107510
iter->getSecond()--;

clang/test/C/C2y/n3622.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
// RUN: %clang_cc1 -verify=good -pedantic -Wall -std=c2y %s
2-
// RUN: %clang_cc1 -verify=compat,expected -pedantic -Wall -Wpre-c2y-compat -std=c2y %s
3-
// RUN: %clang_cc1 -verify=ped,expected -pedantic -Wall -std=c23 %s
4-
// RUN: %clang_cc1 -verify=ped,expected -pedantic -Wall -std=c17 %s
1+
// RUN: %clang_cc1 -verify=good -pedantic -Wall -Wno-unused-but-set-variable -std=c2y %s
2+
// RUN: %clang_cc1 -verify=compat,expected -pedantic -Wall -Wno-unused-but-set-variable -Wpre-c2y-compat -std=c2y %s
3+
// RUN: %clang_cc1 -verify=ped,expected -pedantic -Wall -Wno-unused-but-set-variable -std=c23 %s
4+
// RUN: %clang_cc1 -verify=ped,expected -pedantic -Wall -Wno-unused-but-set-variable -std=c17 %s
55
// good-no-diagnostics
66

77
/* WG14 N3622: Clang 22
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Header file for testing that header-defined static globals don't warn.
2+
static int header_set_unused = 0;
3+
static void header_init() { header_set_unused = 1; }
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// RUN: %clang_cc1 -fsyntax-only -Wunused-but-set-variable -I %S/Inputs -verify %s
2+
3+
// Test that header-defined static globals don't warn.
4+
#include "warn-unused-but-set-static-global-header.h"
5+
6+
#define NULL (void*)0
7+
8+
void *set(int size);
9+
void func_call(void *);
10+
11+
static int set_unused; // expected-warning {{variable 'set_unused' set but not used}}
12+
static int set_and_used;
13+
static int only_used;
14+
static int addr_taken;
15+
extern int external_var; // No warning (external linkage).
16+
extern int global_var; // No warning (not static).
17+
18+
void f1() {
19+
set_unused = 1;
20+
set_and_used = 2;
21+
22+
int x = set_and_used;
23+
(void)x;
24+
25+
int y = only_used;
26+
(void)y;
27+
28+
int *p = &addr_taken;
29+
(void)p;
30+
31+
external_var = 3;
32+
global_var = 4;
33+
}
34+
35+
// Test across multiple functions.
36+
static int set_used1;
37+
static int set_used2;
38+
39+
static int set1; // expected-warning {{variable 'set1' set but not used}}
40+
static int set2; // expected-warning {{variable 'set2' set but not used}}
41+
42+
void f2() {
43+
set1 = 1;
44+
set_used1 = 1;
45+
46+
int x = set_used2;
47+
(void)x;
48+
}
49+
50+
void f3() {
51+
set2 = 2;
52+
set_used2 = 2;
53+
54+
int x = set_used1;
55+
(void)x;
56+
}
57+
58+
static volatile int vol_set;
59+
void f4() {
60+
vol_set = 1;
61+
}
62+
63+
// Read and use
64+
static int compound; // expected-warning{{variable 'compound' set but not used}}
65+
static volatile int vol_compound;
66+
static int unary; // expected-warning{{variable 'unary' set but not used}}
67+
static volatile int vol_unary;
68+
void f5() {
69+
compound += 1;
70+
vol_compound += 1;
71+
unary++;
72+
vol_unary++;
73+
}
74+
75+
struct S {
76+
int i;
77+
};
78+
static struct S s_set; // expected-warning{{variable 's_set' set but not used}}
79+
static struct S s_used;
80+
void f6() {
81+
struct S t;
82+
s_set = t;
83+
t = s_used;
84+
}
85+
86+
// Multiple assignments
87+
static int multi; // expected-warning{{variable 'multi' set but not used}}
88+
void f7() {
89+
multi = 1;
90+
multi = 2;
91+
multi = 3;
92+
}
93+
94+
// Unused pointers
95+
static int *unused_ptr; // expected-warning{{variable 'unused_ptr' set but not used}}
96+
static char *str_ptr; // expected-warning{{variable 'str_ptr' set but not used}}
97+
void f8() {
98+
unused_ptr = set(5);
99+
str_ptr = "hello";
100+
}
101+
102+
// Used pointers
103+
void a(void *);
104+
static int *used_ptr;
105+
static int *param_ptr;
106+
static int *null_check_ptr;
107+
void f9() {
108+
used_ptr = set(5);
109+
*used_ptr = 5;
110+
111+
param_ptr = set(5);
112+
func_call(param_ptr);
113+
114+
null_check_ptr = set(5);
115+
if (null_check_ptr == NULL) {}
116+
}
117+
118+
// Function pointers (unused)
119+
static void (*unused_func_ptr)(); // expected-warning {{variable 'unused_func_ptr' set but not used}}
120+
void SetUnusedCallback(void (*f)()) {
121+
unused_func_ptr = f;
122+
}
123+
124+
// Function pointers (used)
125+
static void (*used_func_ptr)();
126+
void SetUsedCallback(void (*f)()) {
127+
used_func_ptr = f;
128+
}
129+
void CallUsedCallback() {
130+
if (used_func_ptr)
131+
used_func_ptr();
132+
}

0 commit comments

Comments
 (0)